为什么 C 语言宏喜欢写成 do { ... } while (0)?看完终于明白了!
看 Linux 内核、STM32 HAL的源码时,发现很多语句宏都喜欢写成下面这样:
#define MY_MACRO(x) \ do \ { \ foo(x); \ bar(x); \ } while (0)第一次看到的时候,我的第一反应就是:
while(0)永远不会成立,这不是脱裤子放屁吗?
其实这个是有原因的。
一、先说结论
do { ... } while (0)并不是为了循环,而是为了把整个宏包装成一条完整的语句(Single Statement)。
它主要解决的是:
多语句宏在
if...else中的语法问题调用时可以放心在后面写分号
可以在宏内部使用
break是 Linux Kernel、FreeRTOS、STM32 HAL 等大型项目的标准写法
二、为什么不用普通宏?
假设我们有这样一个宏:
#define MY_MACRO(x) \ foo(x); \ bar(x);平时这样调用:
MY_MACRO(1);展开以后:
foo(1); bar(1);完全没有问题。
但是如果放到if里面:
if (flag) MY_MACRO(1); else baz();预处理之后实际上变成了:
if (flag) foo(1); bar(1); else baz();注意这里:
if只控制第一句foo(1)bar(1)已经跑到了if外面else根本找不到对应的if
最终编译器直接报错:
error: else without a previous if这就是多语句宏最经典的问题。
三、执行流程其实是这样的
错误的宏展开以后,大概就是下面这样:
if(flag) │ ▼ foo(1) │ ▼ if结束 │ ▼ bar(1) │ ▼ else ??编译器看到else的时候已经懵了。
四、那这会就会有扁桃体要发言了,直接加{}不行?
很多人都会想到:
#define MY_MACRO(x) \ { \ foo(x); \ bar(x); \ }乍一看没问题。
但是我们一般都会这样调用:
MY_MACRO(1);注意最后那个分号。
展开以后:
{ foo(1); bar(1); };那个分号实际上变成了一条空语句。
如果还是放到if中:
if(flag) MY_MACRO(1); else baz();展开以后:
if(flag) { foo(1); bar(1); }; else baz();这里};已经结束了整个if。
所以还是会报:
error: else without a previous if因此:
仅仅加大括号,并不能解决问题。
五、为什么do while(0)就可以?
改成:
#define MY_MACRO(x) \ do \ { \ foo(x); \ bar(x); \ } while(0)再来看:
if(flag) MY_MACRO(1); else baz();展开以后:
if(flag) do { foo(1); bar(1); } while(0); else baz();整个:
do { ... } while(0);本身就是一条完整语句。
于是整个结构就变成:
if statement; else statement;语法完全正确。
六、还有一个隐藏功能:break
这是很多新手不知道的一个好处。
例如:
#define CHECK() \ do \ { \ if(a == NULL) \ break; \ \ if(b == NULL) \ break; \ \ foo(); \ } while(0)如果:
a为 NULL或者
b为 NULL
直接:
break;就能跳出整个宏。
执行流程:
进入宏 │ ▼ a为空? │ Yes────────────┐ │ ▼ break退出 │ No ▼ 结束 继续判断 b ↓ 执行 foo()不用写很多层:
if(...) { if(...) { ... } }代码可读性会高很多。
七、哪些宏需要这样写?
其实很好判断。
单语句宏
例如:
#define LED_ON() GPIO_SetBits(GPIOA, GPIO_PIN_1)或者:
#define MAX(a,b) ((a)>(b)?(a):(b))这种不用。
多语句宏
例如:
#define RESET() \ stop(); \ clear(); \ start();建议统一改成:
#define RESET() \ do \ { \ stop(); \ clear(); \ start(); \ } while(0)以后放在哪里调用都安全。
八、为什么源码里几乎都是这种写法?
因为它已经成为 C 语言开发中的一种约定。
像下面这些项目,都大量采用这种写法:
Linux Kernel
FreeRTOS
STM32 HAL
CMSISRenesas DA145xx SDK
Zephyr RTOS
可以说:
只要看到多语句宏,基本就能看到
do { } while(0)。
这是多年工程实践总结出来的最佳方案。
以后自己写宏时,可以养成一个习惯:
#define XXX(...) \ do \ { \ /* 多条语句 */ \ } while(0)这样基本就不会因为宏展开导致一些莫名其妙的 Bug 了。