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()