参考内容:
do {…} while (0) in macros
do {...} while (0) 在宏定义中的作用
do{}while(0)只执行一次无意义?你可能真的没理解

近期参与的项目属于嵌入式软件领域,自然而然就得用 C 语言进行开发,开发过程中发现引入的第三方库里面有一些奇奇怪怪的写法,比如大佬们都喜欢使用do {...} while(0)的宏定义,在 Stack Overflow 上也有人提出了这个问题。之前从事 Linux 内核开发的谷歌大佬 Robert Love 给出了如下的解释:

do {…} while(0) is the only construct in C that lets you define macros that always work the same way, so that a semicolon after your macro always has the same effect, regardless of how the macro is used (with particularly emphasis on the issue of nesting the macro in an if without curly-brackets).

do {...} while(0) 在 C 中是唯一的构造程序,让你定义的宏总是以相同的方式工作,这样不管怎么使用宏(尤其在没有用大括号包围调用宏的语句),宏后面的分号也是相同的效果。

这句话读起来有些拗口,只觉得大佬的表述曲高和寡,翻译翻译就是:使用do {...} while(0)构造后的宏定义不会受到大括号、分号等影响,总能按照我们期望的方式调用运行。下面我们举几个实际的例子来加深理解。

// 现有如下宏定义
#define foo(x) bar(x); baz(x)

// 1. 可以这样调用
foo(wolf);

// 上述调用将会被展开为下列代码,完美没有问题
bar(wolf); baz(wolf);


// 2. 如果我们像下面这样调用呢?
if (!feral)
    foo(wolf);

// 上述调用将会被展开为下列代码,很明显这是错误的,并且是很容易犯的错误
if (!feral)
    bar(wolf);
baz(wolf);

为了避免上面例子所出现的问题,我们可以考虑使用{ }直接把整个宏包裹起来,如下所示:

// 修改宏定义为
#define foo(x) { bar(x); baz(x) }

// 3. 例 1 调用
if (!feral)
    foo(wolf);

// 现在上述调用将展开为下列代码
if (!feral) {
    bar(wolf);
    baz(wolf);
};


// 4. 我们再考虑一下如下调用呢
if (!feral)
    foo(wolf);
else
    bin(wolf);

// 上述调用将会被展开为下列代码,很明显又出现了语法错误
if (!feral) {
    bar(wolf);
    baz(wolf);
};
else
    bin(wolf);

我们继续考虑比使用{ }直接把整个宏包裹起来更好的方法,即本文标题所说的使用do {...} while (0),即上述宏将定义为如下形式。

// 终极版宏定义
#define foo(x) do { bar(x); baz(x) } while (0)

// 5. 例 4 调用
if (!feral)
    foo(wolf);
else
    bin(wolf);

// 现在上述调用将展开为下列形式,很完美
if (!feral)
    do { bar(wolf); baz(wolf) } while (0);
else
    bin(wolf);

do {...} while (0)除了在宏定义中可以发挥完美的作用外,在某些情况下还可以当作goto使用。因为goto不符合软件工程的结构化,并且容易使得代码晦涩难懂,所以很多公司都不倡导使用甚至禁止使用。那么我们可以使用do {...} while (0)来做同样的事情。

#include <stdio.h>
#include <stdlib.h>
int main()
{
   char *str;
   /* 最初的内存分配 */
   str = (char *) malloc(15);
   if(str != NULL)
     goto loop;
   printf("hello world\n");
loop:
   printf("malloc success\n");
   return 0;
}

上述代码我们可以修改为下列形式,使用do {...} while (0)将函数主体包裹起来,而break语句则替代了goto语句的作用,并且代码的可读性与可维护性都比上述goto方式更好。

#include <stdio.h>
#include <stdlib.h>
int main()
{
  do{
      char *str;
      /* 最初的内存分配 */
      str = (char *) malloc(15);
      if(str != NULL)
       break;
      printf("hello world\n");
  }while(0);
   printf("malloc success\n");
   return 0;
}