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()
11-27 11:18