C语言的宏(Macro)是天使与魔鬼的混合体。它能让你像魔法师一样生成代码,但如果使用不当,它就是无数“莫名其妙Bug”的源头。
这一期,我们先从宏的基础陷阱讲起,最终通过解析 Linux 内核最著名的 container_of 宏,带你触摸 C 语言宏操作的“天花板”。
1. 文本替换的陷阱:括号满天飞
宏的核心本质是:单纯的、暴力的文本替换。预处理器不进行计算,不进行类型检查,它只负责“复制粘贴”。
经典惨案:副作用 (Side Effect)
#define MIN(a, b) ((a) < (b) ? (a) : (b))
int i = 0, j = 1;
int k = MIN(i++, j);
展开后:((i++) < (j) ? (i++) : (j)) 结果:i 被加了两次。这种 Bug 在调试时极其隐蔽。 铁律:不要在宏参数里使用自增/自减运算符 (++/--) 或函数调用。
2. 工程标准范式:do { ... } while(0)
你经常会在优秀的开源项目(如 FreeRTOS、Linux)中看到这种写法:
#define RESET_SYSTEM() \
do { \
Disable_IRQ(); \
Reset_Peripherals(); \
} while(0)
为什么要包一个永远只执行一次的循环? 是为了“吞掉”分号,让宏在 if-else 语句中能像普通函数一样安全使用,防止因多余的分号导致 else 分支报错。
3. 宏的元编程:# 和 ##
这是宏生成代码的基础工具。
-
#(Stringify):把参数变成字符串。-
#define STR(x) #x->STR(3.14)变成"3.14"。
-
-
##(Concatenate):把两个代码片段粘连在一起。-
#define GPIO(n) GPIO##n->GPIO(A)变成GPIOA。
-
4. 宏定义的王者:container_of
在 Linux 内核和嵌入式链表(如 FreeRTOS 的 xList)中,有一个宏被称为“王者”。 它的作用是:已知结构体中某个成员的地址,反推出整个结构体的首地址。
这是实现**“侵入式链表”**(Intrusive Linked List)的基石。
4.1 场景描述
struct Student {
int id;
char name[20];
int score; // 假设你只拿到了 score 的地址
};
如果你手里只有一个指向 score 的指针 ptr,怎么算出这个 Student 变量的起始地址?
4.2 物理原理
逻辑很简单:起始地址 = 成员地址 - 成员偏移量。
4.3 实现
怎么算出偏移量?利用 0地址强转。
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
#define container_of(ptr, type, member) ({ \
const typeof( ((type *)0)->member ) *__mptr = (ptr); \
(type *)( (char *)__mptr - offsetof(type, member) ); \
})
解析:
-
((type *)0)->MEMBER:欺骗编译器,假设结构体在 0 地址。 -
&...:取这个成员的地址。因为基地址是 0,所以“地址”数值上就等于“偏移量”。 -
(char *)__mptr:关键!必须转成char *才能按字节进行减法运算。 -
typeof(GNU扩展):增加了一层类型检查,防止你传错指针类型(比如把name的指针传给score的逻辑),提高安全性。
实用价值: 有了它,我们就不需要为 Student、Teacher、Worker 分别定义链表节点了。我们只需要定义一个通用的 struct list_head,把它“埋”进所有数据结构里,然后用 container_of 随时还原出业务数据。
1195

被折叠的 条评论
为什么被折叠?



