ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

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

2026/8/4 1:54:57 拓冰建站 浏览量
为什么 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 了。