iT邦幫忙

2026 iThome 鐵人賽

DAY 2
0
自我挑戰組

韌體工程師的不只 0x10 個問題系列 第 2

Day 2-C 語言的預處理器

  • 分享至 

  • xImage
  •  

在第一天的文章中,我們提到 C 語言要從程式碼變成電腦可以執行的機器碼,得經過四個步驟,而這當中的第一步就是「預處理(preprocess)」。

預處理發生在編譯之前,所有指令皆以 # 符號開頭,最常見的包含:

  1. 檔案包含(file inclusion)
  2. 巨集(macro)
  3. 條件編譯(conditional compilation)

以下我們就來一一檢視。

檔案包含 File Inclusion

例如第一天文章中使用的 #include,如果是引用如 stdio.h 的標準函式庫,便用 < > 包起來;如果是引用自己的檔案,便用 " " 包起來。

# include <stdio.h>    // 包含標準函式庫
# include "myfile.h"   // 包含自定義檔案

巨集 Macro

指令以 # 開頭,可用來定義常數或函數,通常使用大寫名稱,且最後不加分號。

巨集定義常數

在 C 語言中,如果定義的常數是整數,則預設的資料型別為 int;如果定義的常數不是整數,則預設的資料型別為 double,例如:

#define SECONDS_PER_MINUTE 60  # 資料型別為 int
#define PI 3.14                # 資料型別為 double

使用更大資料型別,避免溢位

然而,如果巨集定義的值超越 intdouble 的可容納大小,則會產生「溢位(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)),而 parr[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)

條件編譯 Conditional Compilation

在進入編譯前,我們也可以透過預處理以 #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

參考資料

  1. 韌體工程師的0x10個問題
  2. C Preprocessor and Macros (W3Schools)
  3. C Preprocessors (GeeksforGeeks)
  4. C Preprocessor Directives (GeeksforGeeks)

上一篇
Day 1-C 語言從原始碼到執行檔過程
下一篇
Day 3 - 為何用 typedef 比 #define 定義新資料型別更好?
系列文
韌體工程師的不只 0x10 個問題5
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言