1. 程式人生 > 其它 >【C語言】預處理、巨集定義、行內函數 _

【C語言】預處理、巨集定義、行內函數 _

一、由原始碼到可執行程式的過程

1. 預處理: 原始碼經過前處理器的預處理變成預處理過的.i中間檔案

 
1 gcc -E test.c -o test.i

2. 編譯: 中間檔案經過編譯器編譯形成.s的彙編檔案

 
1 gcc -S test.i -o test.s

 

3. 彙編: 彙編檔案經過彙編器生成目標檔案.o(機器語言)

 
1 gcc -c test.s -o test.o

 

4. 連結: 連結器將目標檔案連結成.exe可執行程式(Linux下是.elf)

 
1 gcc test.o -o test.exe

 

在整個過程中,預處理用前處理器,編譯用編譯器,彙編用匯編器,連結用連結器,這幾個工具再加上其他額外的可能會用到的工具,合起來叫編譯工具鏈。gcc/g++

就是一個編譯工具鏈,在實際工程中並不會去手動生成那麼多中間檔案,而是直接一步到位:

 
1 gcc test.c -o test.exe

 

其中,編譯器的主要目的是編譯原始碼,即將.c的原始碼(.i本質上就是預處理過的.c)轉化成.s彙編程式碼。為了讓編譯器聚焦核心功能,就將一些非核心功能剝離到前處理器去了,也就是所讓前處理器幫編譯器做一些編譯前的準備工作。

二、程式設計中常見的預處理

  1. 標頭檔案包含:#include
  2. 註釋
  3. 條件編譯:#if #elif #endif...
  4. 巨集定義

2.1 標頭檔案包含

2.1.1 #include <>和 #include ""
的區別

  • #include <>專門用來包含系統提供的標頭檔案,如果使用<>,編譯器只會到系統指定目錄(編譯器中配置的或OS配置的目錄,如在Ubuntu中是/usr/include,編譯器還允許用-I來附加指定其他的包含路徑)去尋找這個標頭檔案,如果找不到就會提示這個標頭檔案不存在。
  • #include ""用來包含程式設計師寫的標頭檔案,編譯器預設先在當前目錄下尋找相應的標頭檔案,如果沒找到再到系統指定目錄去尋找,如果還沒找到則提示檔案不存在。
使用原則
  • 系統自帶的用<>
  • 程式設計師寫的放在當前目錄下用""
  • 程式設計師寫的集中放專門存放標頭檔案的目錄下,在編譯器中用-I
    引數尋找用<>

2.1.2 標頭檔案包含在預處理時的處理方式

在預處理的時候,前處理器將所包含的標頭檔案的內容原處展開替換這#include語句。
如下所示分別是同一目錄下的.c檔案和.h檔案:

對其進行預編譯生成.i檔案後,.i檔案的內容如下所示:

2.2 註釋

我們在.c原始檔中寫的註釋,前處理器在預處理階段會將其擦除(在.c檔案依然存在,在.i檔案中不存在)。其實這也正順應了註釋是寫給使用程式的人看的,而不是給編譯器看的。如下所示:

2.3 條件編譯

一般情況下,源程式中所有行都參與編譯,但有時希望程式有多種配置,對一部分內容指定編譯條件(如產品的除錯版與正式版),這就是條件編譯(conditional compile),條件編譯有以下兩種判定方法:

2.3.1 #if

這種判定方法類似於if...else...語句,格式為#if (條件表示式),它的判定標準是()中的表示式是true還是flase,在進行預編譯的過程中,只會保留條件表示式為真的那部分內容,如下所示測試程式碼:

   
 1 #include <stdio.h>
 2 #define DEBUG 1
 3 
 4 int main(void)
 5 {
 6     #if(DEBUG == 1)
 7     printf("Version: DEBUG!");
 8     #elif(DEBUG == 2)
 9     printf("Version: TEST!");
10     #else
11     printf("Version: LAST!");
12     #endif
13 
14     return 0;
15 }
 

 

預編譯結果如下所示:

我們也可以不在原始碼中對DEBUG進行巨集定義,而在編譯的時候可以用如下方法對其進行巨集定義並指定值:

 
1 gcc -E -D DEBUG=2 test.c -o test.i

 

結果如下所示:

2.3.2 #ifdef

#ifdef XXX判定條件成立與否時主要是看XXX這個符號在本語句之前有沒有被定義,只要定義了,判斷就成立,並不關心XXX的巨集值為多少。測試程式碼及結果參見下文3.1非帶參巨集的內容

三、巨集定義

3.1 非帶參巨集

非帶參巨集主要結合條件編譯使用,比較簡單,其定義格式為

 
  #define 巨集名 替換列表(替換列表可有可無)

如下所示:

 
1 #define DEBUG
2 #define TEST 1
3 #define TEST2 TEST

巨集定義的預處理

  1. 在預處理階段由前處理器進行機械替換,而不做型別檢查
  2. 巨集定義替換會遞迴進行,直到替換出來的值本身不再是一個巨集為止

如下是在STM32開發過程中常用的列印除錯資訊的一個除錯程式碼段:

   
 1 #include <stdio.h>
 2 
 3 #define USER_TIMER_DEBUG
 4 #ifdef USER_TIMER_DEBUG
 5 #define user_timer_printf(format, ...)  printf( format "\r\n", ##__VA_ARGS__)
 6 #define user_timer_info(format, ...)  printf("[\ttimer]info:" format "\r\n", ##__VA_ARGS__)
 7 #define user_timer_debug(format, ...) printf("[\ttimer]debug:" format "\r\n", ##__VA_ARGS__)
 8 #define user_timer_error(format, ...) printf("[\ttimer]error:" format "\r\n",##__VA_ARGS__)
 9 #else
10 #define user_timer_printf(format, ...)
11 #define user_timer_info(format, ...)
12 #define user_timer_debug(format, ...)
13 #define user_timer_error(format, ...)
14 #endif
 

預編譯結果如圖所示:

3.2 帶參巨集

巨集可以帶引數,稱為帶參巨集。帶參巨集的使用和帶參函式非常相似,只是在使用上和處理上有一些差異,其定義格式為:

 
  #define 識別符號(引數1,引數2,...,引數n) 替換列表

在定義帶參巨集時,每一個引數在巨集體中引用時都必須加括號,最後整體再加括號,括號缺一不可。

不帶括號的後果:

   
1 #include <stdio.h>
2 #define M(a, b) a * b
3 int main(void)
4 {
5     int result = M(2 + 3, 5)
6     printf("%d", result);
7     return 0;
8 }
 

 

如上測試程式碼,我們想得到(2 + 3) * 5的結果,但是由於巨集在預處理的時候也是進行機械替換,int result = M(2 + 3, 5)變成了int result = 2 + 3 * 5,這及其容易出現邏輯上的錯誤

3.2.1 帶參巨集示例

1.MAX巨集: 求2個數中較大的一個

 
  #define MAX(a, b) (((a)>(b)) ? (a) : (b))

2.SEC_PER_YEAR巨集 用巨集定義表示一年中有多少秒

 
  #define SEC_PER_YEAR (365*24*60*60UL)

這個巨集需要注意的是

  1. 當一個數字直接出現在程式中時,它的是型別預設是int
  2. 一年有多少秒,這個數字超過了int型別儲存的範圍
  3. UL將其轉為無符長整型

3.2.2 帶參巨集和帶參函式的區別

1.時間與空間
  • 巨集定義在預處理期間處理,進行簡單的內容替換,無需額外空間
  • 函式是在編譯期間處理的,呼叫時需要為形參分配空間並將實參的值賦給形參
2.執行速度
  • 巨集只進行文字替換,函式執行階段引數需要進行出入棧的操作,速度比巨集慢
3.型別檢查
  • 巨集定義不會檢查引數的型別,返回值也不會附帶型別
  • 而數有明確的引數型別和返回值型別。當呼叫函式時編譯器引數的靜態型別檢查,如果編譯器發現實際傳參和引數宣告不同時會報警告或錯誤。
綜合比較

巨集和函式各有千秋,最大的特點是:用函式的時候程式設計師不太用操心型別不匹配因為編譯器會檢查,用巨集的時候程式設計師必須注意實際傳參和巨集所希望的引數型別一致,或者自行加入型別檢查,否則可能編譯不報錯但是執行有誤。

如對MAX巨集加入型別檢查:

   
1 #define MAX(a, b) ({\
2 typeof(a) _a = (a); \
3 typeof(b) _b = (b); \
4 (void) (&_a == &_b);\
5 _a > _b ? _a : _b;})
 

測試程式碼:

   
 1 #include <stdio.h>
 2 
 3 #define MAX(a, b) ({\
 4 typeof(a) _a = (a); \
 5 typeof(b) _b = (b); \
 6 (void) (&_a == &_b);\
 7 _a > _b ? _a : _b;})
 8 
 9 #define MAX2(a, b) a > b ? a : b
10 
11 int main(void)
12 {
13     int a = 2;
14     float b = 3.1;
15     int result2 = MAX2(a, b);
16     typeof(MAX(a, b)) result = MAX(a, b);
17     printf("result = %f\n", result);
18     printf("result2 = %d\n", result2);
19     return 0;
20 }
 

四、行內函數

行內函數本質上是函式,通過在函式定義前加inline關鍵字實現,是編譯器負責處理的,可以做引數的靜態型別檢查。同時也有帶參巨集的展開特性,執行時沒有呼叫開銷。

4.1 與常規函式對比

  • 當函式體很短的時候,使用常規函式會造成很大的呼叫開銷,行內函數採用原地展開的方式,沒有呼叫開銷
  • 當函式體長的時候,由於行內函數展開會降低定址效率,所以長函式體不會使用行內函數
  • 行內函數本質上是函式,函式的性質行內函數都有

4.2 與巨集的對比

  • 引數型別檢查。編譯過程中,編譯器仍可以對其行內函數進行引數檢查
  • 便於除錯。函式支援的除錯功行內函數同樣支援,而巨集不支援
  • 介面封裝。有些行內函數可以用來封裝一個介面,而巨集不具備這個特性

4.3 noinlinealways_inline

當函式的函式體很小,而且被大量頻繁呼叫,應該做內聯展開時,就可以使用行內函數。但編譯器會不會作內聯展開,編譯器也會有自己的權衡(不合理的行內函數會降低CPU定址效率、函式執行效率、降低程式碼的可移植性…)。如果想告訴編譯器一定要展開,或者不作展開,就可以使用noinlinealways_inline對函式作一個屬性宣告。

 
1 static inline int func(int *, int);//編譯器權衡是否內聯展開
2 static inline __attribute__((noinline)) int func(int *, int);//不內聯展開
3 static inline __attribute__((always_inline)) int func(int *, int);//內聯展開

static修飾呢是因為行內函數不一定會內聯展開,當多個檔案都包含同一個行內函數的定義時,如果沒有static將函式的作用域限制在各自本地檔案內,編譯時就有可能報重定義錯誤