宏 do{}while(0)

作者: | 更新日期:

看库函数的时候,经常看到 把语句放到 do{}while(0) 中,于是查了一下资料。

本文首发于公众号:天空的代码世界,微信号:tiankonguse

心中的疑惑

在 c 的库函数中会经常看到下面的语句

#define FOO(X) do { something;} while (0)
#define FOO(X) if (1) { something; } else

浅层原因

对于第一个,还可以解释通。

一般比较复杂的宏会执行多条语句,为了保持逻辑的正确性,需要使用大括号把宏里面的语句全部抱起来,比如

#define FOO(X) { something; }

这样可以避免的问题是宏用在在了 if() else 中,然后由于看到 if 中是”一条语句”,于是没有加括号。

比如:

#define FOO(X,y) x++;y--;

if(x > y)
    FOO(x,y)
else
    FOO(y,x)

这个坑很多人都会遇到的,即使工作了,在公司里也有很多人会遇到的。

至少在我实习的时候,我的导师就遇到了这个问题。我让他先把所有的if 语句加上大括号,结果之前发现的问题不见了。

追踪 if 里面的语句,发现是个宏,而且宏里只是简单地多条语句,于是宏里的第一条总是正确的,后面的不管条件是否满足都是执行,坑爹呀。

另一方面,如果宏里面定义了变量,不用大括号包起来,还可能遇到重定义变量的错误。

真正的原因

使用大括号避免了if没加括号的问题,但是我们一般调用一个宏的函数后习惯加个分号,比如

#define FOO(X,y) { x++;y--;}

if(x > y)
    FOO(x,y);
else
    FOO(y,x);

悲剧是竟然编译不通过。

编码习惯和编码规范都摆在那里:每条语句后面都有一个分号。

难道对于宏,我们不加分号,这样将会显得格格不入了。

但是加了分号,我们那种使用大括号的方案也失效了。

这时我们就需要一个有大括号(形成代码块)且以分号结尾的宏了。

此时再看看文章开头介绍的那两个疑问的宏

#define FOO(X) do { something;} while (0)
#define FOO(X) if (1) { something; } else

if(x > y)
    FOO(x);
else
    FOO(y);

这时,我们的代码没有任何副作用。

回顾副作用

第一个原因是宏里面有多个语句,在没加大括号的if语句中将编译不通过,有时宏里面有定义变量时还可能遇到重定义的编译错误。

第一个解决方案:加一个大括号。

加了大括号后,发现一个奇怪的问题:我们调用宏时不能加分号。

于是第二个原因出来了:对于加了大括号的宏,在没加大括号的if语句中依旧可能会遇到编译不通过的情况。

于是就出现了 do{}while(0) 的奇葩宏定义了。

终极解决方案

发现宏都是因为在if语句里出错的,原因是if语句没有使用大括号。

当然,在所有需要加大括号,而你由于看到只有一条语句而没有加大括号的代码中,都可能遇到这个问题。

所以我们要养成良好的编码习惯,所有的if,while,for等语句都加上大括号,即使只有一条语句也要加上大括号。

if语句加上大括号还可以避免 dangling else 问题。

官方的解决方案

由于宏定义出的问题有很多,gcc 加了一个 Statement-Exprs 的语法。

#define FOO(X,y) ({ x++;y--;})

if(x > y)
    FOO(x,y);
else
    FOO(y,x);

就是对宏语句加个大括号,然后大括号外加个小括号。

官方有这么一句话:A compound statement enclosed in parentheses may appear as an expression in GNU C。

大概意思是混合的语句使用括号包起来后可以作为一个表达式。

do-while的好处

难道 do-while 只有这么一个好处吗?

不是的,还有其他好处。

避免宏的某些错误

见上文

避免使用 goto 语句

goto 一般用于调到结尾,但是编码规范中说了禁止使用 goto 语句。

看看下面的 goto 使用,如果不适用 goto 怎么实现呢.

void foo(){
    if(error){
        goto END;
    }
    dosomething...;
    if(error){
        goto END;
    }
    dosomething...;
 
END:
    dosomething...;
    return 0;
}

就是使用只执行一次的循环来代替 goto.

int foo(){
    do{
        if(error) {
            break;
        }
 
        dosomething...;
        if(error){
            break;
        }
        dosomething...;
    }while(0);
    dosomething...;
    return 0;
}

代替空的宏语句

#define EMPTYMICRO do{}while(0)

如果是空的宏,会有 warnning 的。

参考资料

本文首发于公众号:天空的代码世界,微信号:tiankonguse
如果你想留言,可以在微信里面关注公众号进行留言。

关注公众号,接收最新消息

tiankonguse +
穿越