为什么 C 语言宏喜欢写成 do { ... } while (0)?看完终于明白了!

📅 2026/8/4 1:46:15
为什么 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 KernelFreeRTOSSTM32 HALCMSISRenesas DA145xx SDKZephyr RTOS可以说只要看到多语句宏基本就能看到do { } while(0)。这是多年工程实践总结出来的最佳方案。以后自己写宏时可以养成一个习惯#define XXX(...) \ do \ { \ /* 多条语句 */ \ } while(0)这样基本就不会因为宏展开导致一些莫名其妙的 Bug 了。