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