首頁 > 軟體

一起來學習C語言的程式環境與預處理

2022-03-24 13:01:42

1.程式的翻譯環境和執行環境

要支援c語言的實現,會有不同的編譯器出現,而這些編譯器都要遵循ANSI C,都存在兩種環境

第1種是翻譯環境,在這個環境中原始碼被轉換為可執行的機器指令。 第2種是執行環境,它用於實際執行程式碼。

.obj為字尾的就是目標檔案

而一個專案中可能會有很多.c字尾的原始檔,分別處理後每經過編譯器單獨處理,然後會生成對應的目標檔案(.obj),然後總體經過聯結器處理,最終變成可執行程式。

目標檔案最後還要加上連結庫整體一起通過連結器連結,變成可執行程式.

連結庫:在編寫程式碼的時候,會有一些不屬於我們自己寫的函數(如printf),這些函數是自帶的庫裡面包含的,這些庫就叫連結庫

(補函數的宣告與定義裡面的靜態庫)

從原始檔生成可執行程式的這一個過程就叫做翻譯環境

2.gcc C語言編譯器來演示編譯過程

2.1編譯

預編譯→編譯→組合

預編譯(預處理):

文字操作:

1.標頭檔案的包含,#include——預編譯指令,將包含的標頭檔案給展開

2.刪除註釋(註釋被空格替換)

3.#define定義符號的替換

2.2編譯:

生成.s的檔案

把c語言程式碼轉換成組合程式碼

1.語法分析

2.詞法分析

3.語意分析

4.符號彙總——彙總的是全域性符號

《程式設計師的自我修養》——通俗地講解程式碼編譯過程的細節

組合:

生成了test.o

把組合程式碼轉換成二進位制指令

形成符號表:

框內是十六進位制是地址

連結:

最終將.o檔案連結成.exe可執行程式

1.合併段表

2.符號表的合併和重定位(像Add一開始地址為預設0和另一個.c檔案內的Add地址的為0x200,會重新定位)

符號表的意義:多個目標檔案進行連結的時候會通過符號表檢視來自外部的符號是否真實存在

2.3執行環境

1.程式必須載入記憶體中。在有作業系統的環境中:一般這個由作業系統完成。在獨立的環境中,程式的載入必須由手工安排(電焊好伐),也可能是通過可執行程式碼置入唯讀記憶體來完成。

2.程式的執行便開始。接著便呼叫main函數。

3.開始執行程式程式碼。這個時候程式將使用一個執行時堆疊(stack)(也就是之前部落格中寫到的函數棧幀的建立與銷燬),儲存函數的區域性變數和返回地址。程式同時也可以使用靜態(static)記憶體,儲存於靜態記憶體中的變數在程式的整個執行過程一直保留他們的值。

4.終止程式。正常終止main函數;也有可能是意外終止。

3詳解預處理

3.1預定義符號

  • __DATE__
  • __FILE__
  • __LINE__
  • __TIME__
  • __STDC__   //如果編譯器遵循ANSI C,其值為1,否則未定義

作用:記錄紀錄檔:可記錄在哪個檔案在哪個日期在什麼時候在哪個檔案在哪一行

#define _CRT_sECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
    printf("%sn", __FILE__);
    printf("%sn", __TIME__);
    printf("%dn", __LINE__);
    printf("%sn", __DATE__);
    return 0;
}

預處理符號的應用:

//預處理符號的應用——寫紀錄檔
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <string.h>
#include <errno.h>
int main()
{
    int i = 0;
    FILE* pf = fopen("test.txt", "w");
    if (NULL == pf)
    {
        printf("error is%sn", strerror(errno));
        return 0;
    }
    for (i = 0; i < 5; i++)
    {
        fprintf(pf, "%st%st%st%dti=%dn", __FILE__, __DATE__, __TIME__, __LINE__, i);
    }
    fclose(pf);
    pf = NULL;
    return 0;
}

3.2#define

3.2.1#define定義識別符號

兩種用法:

#define MM 100
#define reg register——關鍵字替換
 

#define末尾有時候可以加分號有時又不可以加上分號,

不可以加上分號的情況:

//不可以加上分號的情況
#define _CRT_SECURE_NO_WARNINGS 1
#define MAX(x,y) ((x)>(y)?x:y);
#include <stdio.h>
int main()
{
    int a = 5;
    int b = 3;
    printf("%dn", MAX(a, b));
    return 0;
}

因為加上分號會使得宏在替換的時候也帶上分號,所以在呼叫在一些函數內部的時候會出現錯誤。

綜上,當我們定義宏的時候,最好不要加分號在末尾。

3.2.2 #define定義宏

這裡也是將全部引數給替換掉,在預處理的時候就替換掉了,不信的話可以在解決方案處右擊,點選屬性後選擇預處理,然後就可以在debug裡面發現又應該.i檔案,點開後就可以發現這裡已經被替換掉了。

#define Max(x,y) ((x)>(y)?(x):(y))
//      Max->宏的名字  
//         x和y->宏的引數
//                ((x)>(y)?(x):(y))->宏的內容

ps:在定義宏的內容的時候,最好每個引數都要加上小括號,然後最後整體加上小括號,否則如果傳入引數不是單獨一個值而是表示式的時候,會產生一些沒有意料到的優先順序計算改變

Tips:宏後面的引數的小括號一定要緊挨著宏的名

3.2.3 #define替換規則

1.先看宏的引數內是不是有define的符號,優先替換掉define符號

2.對於宏,引數名被他們的值替換

注意!!

1.宏的引數裡可以出現其他#define定義的符號,但不可以遞迴

2.當define掃描預處理時,字串常數的內容並不被搜尋(也就是說字串裡面的東西是不會被宏預處理的)

3.2.4 #和##

#

相當於把宏的引數放進字串中變成所對應的字串

// #的用法
#define _CRT_SECURE_NO_WARNINGS 1
#define print(x) printf("the value of " #x " is %dn",x)
#include <stdio.h>
int main()
{
    int a = 5;
    int b = 4;
    print(a);
    print(b);
    return 0;
}

##

可以把兩邊分離片段合成一個符號

#define CAT(C,num) C##num
int main()
{
    int Class104=10000;
    printf("%dn",CAT(Class,104));
    return 0;
}

3.2.5帶副作用的宏引數

#define MAX(x,y) ((x)>(y)?(x):(y))
int main()
{
    int a=3;
    int b=5;
    int m=MAX(a++,b++);//宏的引數是直接替換進去
    所以替換完之後為:
    int m=((a++)>(b++)?(a++):(b++));//會出現錯誤
    printf("%dn",m);
    printf("%d %dn",a,b);
    return 0;
}

3.2.6宏和函數對比

宏的優點:

1.用於呼叫函數和從函數返回的程式碼可能比實際執行這個小型計算工作所需要的時間更多。 所以宏比函數在程式的規模和速度方面更勝一籌

2.更為重要的是函數的引數必須宣告為特定的型別。 所以函數只能在型別合適的表示式上使用。反之這個宏怎可以適用於整形、長整型、浮點型等可以 用於>來比較的型別。

宏的缺點:

1.每次使用宏的時候,一份宏定義的程式碼將插入到程式中。除非宏比較短,否則可能大幅度增加程式的長度。

2.宏是沒法偵錯的。

3.宏由於型別無關,也就不夠嚴謹。

4.宏可能會帶來運運算元優先順序的問題,導致程容易出現錯

可從以下方面比較宏與函數的區別:

  • 程式碼長度——宏如果不是特別小的話,每一次使用的時候都要替換成宏的定義,可能會導致最終程式碼特別長,大幅增長程式長度,而函數每次都只呼叫那一段程式碼
  • 執行速度——宏只需要執行一行的程式碼,而函數擁有呼叫函數,執行程式碼,返回引數這三步操作,所以相對來說會慢一些
  • 操作符優先順序——由於宏是不經過計算直接將引數傳進去的,所以在傳參後可能會有優先順序的不同導致結果與我們想要的最終結果有出入,除非加上括號。相對的函數引數只在函數呼叫的時候求值一次,會比較容易猜測結果。
  • 帶有副作用的引數——引數可能被替換到宏體中的多個位置,所以帶有副作用的引數求值可能會產生不可預料的結果。函數引數只在傳參的時候求值一次,結果更容易控制。
  • 引數型別——宏的引數型別相對自由,只要對引數的操作合法,就可以任何只要符合規定的引數型別。函數的引數型別是固定死的。
  • 偵錯——宏不方便偵錯。函數能夠偵錯。
  • 遞迴——宏不能夠遞迴,而函數可以遞迴

3.2.7 命名的約定

一般來講宏與函數的使用語法很類似,所以以後使用這種方法區分宏與函數:

  • 宏名全部大寫
  • 函數名不要全部大寫(可開頭或部分大寫)

3.3 undef

去除一個宏定義

#undef 宏名

3.4命令列定義

許多C 的編譯器提供了一種能力,允許在命令列中定義符號。用於啟動編譯過程。 例如:當我們根據同一個原始檔要編譯出不同的一個程式的不同版本的時候,這個特性有點用處。(假定某個程式中宣告了一個某個長度的陣列,如果機器記憶體有限,我們需要一個很小的陣列,但是另外一個機器記憶體大些,我們需要一個陣列能夠大些。)

#include <stdio.h>
int main()
{
    int array [SZ];
    int i = 0;
    for(i = 0; i< SZ; i ++)
   {
        array[i] = i;
   }
    for(i = 0; i< SZ; i ++)
   {
        printf("%d " ,array[i]);
   }
    printf("n" );
    return 0;
}
 

在這裡我們可以知道SZ這個符號始終沒有被定義,到這裡為止我們的程式還是無法執行的,會報錯,但是:

編譯指令:

//linux 環境演示
gcc -D SZ=10 programe.c

在編譯完這一行以後程式就能夠執行了,這是因為我們在命令列中已經將SZ這個符號定義好了。

3.5 條件編譯

有一段程式碼,編譯了麻煩,刪去了可惜,這時可以選擇是否編譯,這時候就要用到條件編譯。

應用場景:當我們使用在不同系統時,比如在用到windows系統時我們需要用到這一段程式碼,而在Linus系統上又要用到另一段程式碼而不能用windows那段程式碼的時候,不可以刪除,因為要實現一個程式的跨平臺使用,這時候就需要用到條件編譯來選擇什麼時候使用哪段程式碼。

Tips:我們要明確條件編譯指令也是預處理指令

//條件編譯
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
    int i = 0;
    for (i = 0; i < 10; i++)
    {
#if 1 //這裡的常數非0,於是執行,如果為0,則不執行
        printf("%dn", i);
#endif
        return 0;
    }
}

不能放變數,因為是預處理階段執行的,而變數在預處理中還沒有出現,所以我們只能放常數進去,否則放變數進去的話只能判定為0。

常見的條件編譯指令:

1.#if 常數/常數表示式     #endif

2.#if 常數表示式 #elif 常數表示式 #else 常數表示式    #endif

3.判斷是否被定義:只要你定義了宏就為真

  • #if defined(宏名) ...#endif #ifdef 宏名 ... #endif
  • 如果要做到的是沒有定義,則在前面加上!

#if !defined(宏名) ...#endif             #ifdef !宏名 ... #endif

4.巢狀指令

//全部型別的條件編譯
#define _CRT_SECURE_NO_WARNINGS 1
#define VALUE 200
#define TEST 20
#include <stdio.h>
int main()
{
    int i = 0;
    for (i = 0; i < 10; i++)
    {
#if 1
        printf("%dn", i);
#endif
    }
#if VALUE<100
    printf("value<100n");
#elif VALUE>=100&&VALUE<=150
    printf("value>=100且value<=150n");
#else
    printf("value>150n");
#endif
#ifdef VALUE
    printf("VALUE已定義n");
#else
    printf("VALUE未定義n");
#endif
#if defined(VALUES)
    printf("VALUES已定義n");
#else
    printf("VALUES未定義n");
#endif
    //巢狀指令
#if VALUE<=150
#if defined(VALUE)
    printf("VALUE小於或等於150,VALUE已定義n");
#else
    printf("VALUE小於或等於150,VALUE未定義n");
#endif
#elif VALUE>150&&VALUE<=200
#ifdef TEST
    printf("VALUE大於150且小於等於200,TEST已定義n");
#else
    printf("VALUE大於150且小於等於200,TEST未定義n");
#endif
#endif
    return 0;
}

3.6檔案包含

我們知道在預編譯時會包含標頭檔案,而標頭檔案例如<stdio.h>從預編譯.i檔案上看我們可以知道有2000多行程式碼,若真的重複包含了五六次,則程式碼量直接上升到了一萬多行,這時候就會使得程式碼過於冗長,同時也佔用很多記憶體,這個時候我們就需要檔案包含來確認是否重複包含了同一個標頭檔案。

方法一:

//在標頭檔案中
#ifndef __TEST_H__
#define __TEST_H__
int Add(int x,int y);
#endif

方法二:

//在標頭檔案中
#pragma once
int Add(int x,int y);
 

ps:這個#pragma once是比較高階的寫法,在遠古編譯器裡面是無法使用的(如vc)

3.6.1標頭檔案被包含的方式

在小綠本人之前的三子棋以及掃雷的部落格中都有自己創造標頭檔案而我們在參照自己創造的標頭檔案時是以#include "fun.h" 這樣的形式參照的,但是在參照庫函數時確實以#include <stdio.h>的方式參照,那麼用不同的符號參照標頭檔案有什麼不一樣呢?

""的查詢策略

""的查詢策略是:先在原始檔所在的目錄下查詢,如果沒有找到,編譯器就像查詢庫函數標頭檔案一樣在標準位置查詢標頭檔案。再如果找不到就提示編譯錯誤。

<>的查詢策略

查詢標頭檔案直接去標準位置下查詢,如果找不到就直接提示編譯錯誤。

總結

本篇文章就到這裡了,希望能夠給你帶來幫助,也希望您能夠多多關注it145.com的更多內容!      


IT145.com E-mail:sddin#qq.com