在第一天的文章中,我們提到 C 語言要從程式碼變成電腦可以執行的機器碼,得經過四個步驟,而這當中的第一步就是「預處理(preprocess)」。
預處理發生在編譯之前,所有指令皆以 # 符號開頭,最常見的包含:
以下我們就來一一檢視。
例如第一天文章中使用的 #include,如果是引用如 stdio.h 的標準函式庫,便用 < > 包起來;如果是引用自己的檔案,便用 " " 包起來。
# include <stdio.h> // 包含標準函式庫
# include "myfile.h" // 包含自定義檔案
指令以 # 開頭,可用來定義常數或函數,通常使用大寫名稱,且最後不加分號。
在 C 語言中,如果定義的常數是整數,則預設的資料型別為 int;如果定義的常數不是整數,則預設的資料型別為 double,例如:
#define SECONDS_PER_MINUTE 60 # 資料型別為 int
#define PI 3.14 # 資料型別為 double
然而,如果巨集定義的值超越 int 或 double 的可容納大小,則會產生「溢位(overflow)」問題,因此可用型別後綴手動更改包含更多位元組的資料型別,例如:
#define SECONDS_PER_CENTURY (60 * 60 * 24 * 365 * 100)
的值為 3153600000,大幅超過 int 型別可容納的最大值 2147483647,因此可改成包含至少八個位元組的 long long,或者可以改成不考慮正負號而包含更多正號範圍的 unsigned long 如下:
#define SECONDS_PER_YEAR (60LL * 60LL * 24LL * 365LL * 100LL) # 使用 long long
#define SECONDS_PER_YEAR (60UL * 60UL * 24UL * 365UL * 100UL) # 使用 unsigned long
至於非整數的圓周率,如果要精確表達更多位數,則改用 long double 如下:
#define PI 3.141592653589793238462643383279L # 資料型別為 long double
當記憶體空間有限時,也可以改用包含較少位元組的資料型別,例如:
define PI 3.14159F # 資料型別指定為 float
因為 short 沒有型別後綴,因此無法在巨集中定義成 short 型別。
常見的 C 語言資料型別與範圍可參考 GeeksforGeeks 網站。
巨集也可以吃引數(arguments)進行運算,功能與函數相當,例如要計算正方形面積時,可以寫成:
# include <stdio.h>
# define SQUARE_AREA(a) (a*a)
int main(){
int side_len = 5;
// 以上述 SQUARE_AREA() 巨集計算正方形面積
printf("%d", SQUARE_AREA(side_len));
return 0;
}
中的巨集 SQUARE_AREA() 吃 a 為引數,進行 (a*a) 運算。
下方主函式呼叫 SQUARE_AREA() 時,以 side_len 為參數,在 int side_len = 5; 中賦值為 5,因此輸出為:
25
如果巨集參數本身就會改變變數狀態(例如 i++、*p++、a = b 等),將造成副作用,例如:
#include <stdio.h>
#define MIN(a, b) ((a) < (b) ? (a) : (b))
int main() {
int arr[] = {5, 8, 12};
int *p = arr;
int b = 10;
int least;
least = MIN(*p++, b);
printf("least = %d\n", least);
printf("p points to = %d\n", *p);
return 0;
}
當 MIN(*p++, b); 展開時,會變成 (((*p++) < (b)) ? (*p++) : (b)),而 p 為 arr[0],因此 p++ 指向 arr[1],其值為 8。
8 < 10 成立,因此走三元運算子的 (*p++),但這邊又多做了一次 p++,因此走到 arr[2],解引用後其值為 12。
輸出的結果變成:
least = 8
p points to = 12
明明比較時,p 指標指向 8,但最後輸出時,p 又多往右跑了一個位置,變成指向 12。
運算式也可以用三元運算子,例如:
#define ABS(x) ((x) < 0 ? -(x) : (x))
表示輸入為 x,若這個 x 小於 0,則輸出為 -(x),否則為 (x)。
在進入編譯前,我們也可以透過預處理以 #if/#elif/#else/#endif 來決定有哪寫程式碼要編譯,寫法有點類似程式碼的 if/else if/else,例如:
#if defined(OS_WINDOWS)
printf("Windows\n");
#elif defined(OS_LINUX)
printf("Linux\n");
#else
printf("Other OS\n");
#endif
可決定要編譯哪些程式碼,例如:
#define OS_WINDOWS
#if defined(OS_WINDOWS)
printf("Windows\n");
#elif defined(OS_LINUX)
printf("Linux\n");
#else
printf("Other OS\n");
#endif
因為有定義名為 OS_WINDOWS 的巨集,因此就只會編譯 printf("Windows\n"); 這一行,而如果是:
#define OS_LINUX
#if defined(OS_WINDOWS)
printf("Windows\n");
#elif defined(OS_LINUX)
printf("Linux\n");
#else
printf("Other OS\n");
#endif
就只會編譯 printf("Linux\n"); 這行,如果巨集中沒有指定作業系統,變成:
#if defined(OS_WINDOWS)
printf("Windows\n");
#elif defined(OS_LINUX)
printf("Linux\n");
#else
printf("Other OS\n");
#endif
那就只會編譯 printf("Other OS\n"); 這行。
條件編譯讓我們得以決定哪些程式碼要往下走到編譯步驟、哪些不用,如上述範例就可讓我們更專注於特定作業系統下的開發工作。
另外還有其他讓程式在進入編譯階段前就跑的預處理指令,例如 #error 可讓程式在編譯前就先行報錯,避免開始編譯後才發現錯誤而更難追蹤。
在以下程式碼中:
#define BUFFER_SIZE 500
#if BUFFER_SIZE < 1024
#error "BUFFER_SIZE must be at least 1024!"
#endif
#include <stdio.h>
int main() {
printf("BUFFER_SIZE = %d\n", BUFFER_SIZE);
return 0;
}
因為巨集定義的 BUFFER_SIZE < 1024 成立,所以還沒進入編譯階段就已經進入 if BUFFER_SIZE < 1024 區塊,並跑出如下的自定義錯誤訊息:
main.c:4:2: error: #error "BUFFER_SIZE must be at least 1024!"
4 | #error "BUFFER_SIZE must be at least 1024!"
| ^~~~~
而如果把巨集改成:
#define BUFFER_SIZE 2048
則不會進入 BUFFER_SIZE < 1024 區塊,程式碼可正常編譯與執行,顯示:
BUFFER_SIZE = 2048