C編程的預處理技巧

1. 宏定義中的特殊符號

1.1 “#”

符號"#"的作用是將宏參數轉爲字符串常量。
下例定義一個字符串轉化宏:

#define STRING(argument) #argument

將宏STRING展開:

char *p = STRING(hello);
	||
char *p = "hello";

1.2 “##”

符號"##"用於連接宏參數與鄰接的文本,使之成爲一個Token(標識符)。
下例是將兩個宏參數連接爲一個Token:

#define CONECT(a, b)    a##b;

在代碼中將宏CONECT展開:

int i = CONECT(1, 2);
	||
int i = 12;

下例是在Token中間部分用宏參數替換:

#define INSERT(a)	start_##a##_end

將宏INSET展開:

void INSET(func)(void);
	||
void start_func_end(void;

空格與“.” ","用於兩個Token的間隔,在宏參數左右有這三種符號的情況下,不能使用連接符“##”,否則編譯不通過!

舉個例子,有如下結構體,期望在在結構體的成員變量中進行宏替換:

#define PRICE(type) fruit.##type##_price

struct {
    int apple_price;
    int origin_price;
    int banana_price;
} fruit;

int a = PRICE(apple);
int b = PRICE(origin);
int c = PRICE(banana);

因爲宏參數“type”左側有符號“.”,此時再加入連接符“##”,上述的代碼在編譯時將會提示如下錯誤:


正確的做法是去掉宏參數“type”左側的連接符“##”,修改後的宏定義如下:

#define PRICE(type) fruit.type##_price

1.3 宏定義中的變長參數

宏定義可以接受變長參數,我們可以利用這個特性來定義自己的log打印函數。

#define DEBUG_PRINT(format, ...) fprintf(stderr, format, __VA_ARGS__)

上述宏定義初看好像沒什麼問題,但是當傳遞一個空參數時,這個宏展開後將會多一個逗號。
這個問題怎麼解決呢?可以在變長宏參數前加一個特殊符號“##”,修改後的宏定義如下:

#define DEBUG_PRINT(format, ...) fprintf(stderr, format, ##__VA_ARGS__)

2. 宏與函數

宏函數相對真正函數的優點是:

  • 減少了函數調用/返回的額外開銷,執行更快;
  • 宏參數與類型無關,適用於任意類型。

但要注意的是:宏函數只是將宏定義中的代碼段展開,並不是真正的函數,只適用於代碼邏輯簡單,代碼行數不多的場景。

舉例一個我們常見的宏函數:

#define DEBUG_PRINT(format, ...) \
    do { \
       fprintf(stderr, format, ##__VA_ARGS__); \ 
    } while (0)

起初看到這個宏函數,很多人有以下疑問:

  • Q: 爲什麼行尾使用反斜槓“\”?
    A:宏定義不支持多行展開替換,在每行的行尾使用反斜槓,將多個物理行連接成一個邏輯行,既符合宏定義規則,又提高了代碼可讀性。

  • Q: 爲什麼代碼段要加“do while(0)”呢?
    A:避免宏展開時生成空白語句。

將上述宏函數應用到下面的場景中:

    if (condition) {
        DEBUG_PRINT;
    } else {
        ...
    }

如果不加“do while(0)”,宏展開後多餘的分號“;”將產生一條空白語句,而在“if else”的分支中是不允許多條語句的。

同理,我們可以用括號“()”將代碼段轉爲表達式,也可以避免產生空白語句,如下:

#define DEBUG_PRINT(format, ...) \
({ \
       fprintf(stderr, format, ##__VA_ARGS__); \ 
})

相對“do while(0)”而言,“()”適用的範圍更廣,例如在宏函數需要返回值的場景中,就只能用“()”了。

#define GET_SIZE(a, b) \
({ \
    int size1 = sizeof(a); \
    int size2 = sizeof(b); \
    (siez1 + size2); \
})

3. 頭文件數組展開

在嵌入式開發中,我們經常碰到需要在代碼中嵌入二進制固件的場景,通常的處理方法是定義一個包含二進制數值的數組,以方便引用該固件:

char firmware[] = {
    0x10, 0x11, 0x33, 0x44, 
    ...
    ...
    ...
    0xf8, 0x76, 0x99, 0xc1
};

當固件比較大時,這個數組展開將佔用數千甚至數萬行代碼!極大地影響了主體業務代碼的可讀性。
如何解決這個問題呢?
在這裏我們可以利用預編譯時“#include”展開頭文件的特性,將固件的二進制數值放入頭文件“firmware.h”中,然後在給數組賦值時引用語句:#include “firmware.h”,如下:

char firmware[] = {
  #include "firmware.h"  
};

4. 在編譯時定義宏

gcc支持在編譯時定義宏。

  • 在編譯時定義一個空白宏:
gcc test.c -D DEBUG

等同於代碼:

#define DEBUG
  • 在編譯時定義一個帶替換文本的宏:
gcc test.c -D DEBUG=1

等同於代碼:

#define DEBUG   1

5. 自定義結構體對齊規則

在定義結構體時,編譯器會根據結構體中最大成員對齊,然後分配內存空間。
舉例,定義結構體test:

strcut test {
    char a;
    int b;
}

從字面上看,結構體成員a和b一共只有5個字節大小,但實際上編譯器爲這個結構體分配了8字節的內存空間(以結構體成員b(佔4字節)對齊)。
在上層應用程序開發時,自然不用關心編譯器的這些“小動作”;但在底層驅動程序開發中,出於節約內存空間或是精確描述內存佈局的目的,不能任由編譯器做對齊優化。

那麼,如何讓結構體test只佔用實際大小(5個字節)的空間呢?
這時可以使用命令“#pragma pack()”自定義對齊規則:

//編譯器以1字節對齊方式分配空間
#pragma pack(1)

struct test {
    char a;
    int b;
}

//編譯器恢復默認對齊方式
#pragma pack()
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章