源码里的宏定义用法

1,187 阅读4分钟

C/C++的宏定义,我想没有多少人现在会关心这个话题。我也不曾深入了解,但是在看C源码时,到处是各种宏定义,菜鸡我连语法都看不懂,只好来研究一下。

虽然Effective C++里建议不要使用宏,而是尽量用inline来代替宏函数,用静态或者枚举来代替宏定义的值,理由是宏不够安全。不过很多源码是用C写的,里面宏定义发挥了很大的作用,有些写法很巧妙,所以宏的一些用法还是需要了解一下。

宏的语法相关不多介绍,这篇文章主要描述几种我们平时可能没有用到,但是源码里经常出现的用法。

宏定义里的do{}while(0)有什么用

在很多C源码中都经常可以看到do{}while(0)的写法,在redis的源码中也存在这样的写法:

#define dictSetVal(d, entry, _val_) do { \
    if ((d)->type->valDup) \
        entry->v.val = (d)->type->valDup((d)->privdata, _val_); \
    else \
        entry->v.val = (_val_); \
} while(0)

先说结论:对于宏函数总是用do{}while(0)的结构包围起来是为了让宏函数总是按照预期的方案运行,不会受到分支或者其他符号的影响

举个例子,下面有个宏定义的函数

#define foo(x) a(x); b(x)

场景一:

if (1)
    foo(x);

宏被解析为:
if (1)
    a(x);
    b(x);

期待的结果是,if条件满足的话就执行foo(x),也就是执行函数a和b,如果不满足就不执行。但是宏展开后的结果是,不管if条件满不满足,都会执行函数b,因为if的执行语句没有用大括号括起来。显然这不是预期的结果

你肯定会想,为什么不用大括号将宏扩起来呢?

为了回答这个问题,看下面这个场景:

宏定义用大括号括起来
#define foo(x) { a(x); b(x) }

上面的场景宏展开后是下面这个样子,可以满足需求了
if (1) {
    a(x);
    b(x);
}

到这里,用括号似乎解决了问题,但是考虑下面这种写法
if (1) 
    foo(x);
else
    fun(x);
宏被解析为:
if (1) {
    a(x);
    b(x);
};
else 
    fun(x)
这种情况下,编译就报错了, 所以给宏加个大括号显然不行。

实际上采用do while(0)就是相当于给宏加了一个大括号,而且不会出现编译错误

宏定义里的#和##是干嘛的

宏定义里#的功能是将其后面的宏参数进行字符串化操作,简单来说就是在输入参数的两侧分别加一个引号。 看下面的例子:

#include <stdio.h>

#define VALUE(a) do { \
	printf("value is: %s\n", #a); \
} while(0)

int main()
{
	int i = 100;
	VALUE(12);
	VALUE("hello");
	VALUE(i);
	return 0;
}

宏定义展开后:
int main()
{
    int i = 100;
    do { printf("value is: %s\n", "12"); } while(0);
    do { printf("value is: %s\n", "\"hello\""); } while(0);
    do { printf("value is: %s\n", "i"); } while(0);
    return 0;
}

输出:
value is: 12
value is: "hello"

两个连续的井号##的作用是将两个宏参数连接起来,看下面的例子

#include <stdio.h>
#include <stdint.h>

#define INDEX(i) index_##i

int main()
{
    int INDEX(1) = 1;
    return 0;
}

宏定义展开后:
int main()
{
    int index_1 = 1;
    return 0;
}

宏定义里的可变参数

宏定义里还可以使用可变参数,可以像可变参数函数里一样使用3个点的省略号,也可以用一个参数标识然后再加3个点的省略号。可变数量参数函数,在日志系统里用的最多了。

如果使用了参数加省略号的模式,那么这个参数就代表了整个可变参数,使用时用这个参数来表示可变参数,如果只用了省略号来表示参数则使用默认的宏__VA_ARGS__来表示可变参数。

#include <stdio.h>

#define LOG_INFO_FORMAT "FILE:%s LN:%d FUN:%s "
#define LOG_INFO_CONTENT __FILE__, __LINE__, __func__

#define LOG(format, args...) do { \
    printf(LOG_INFO_FORMAT format, LOG_INFO_CONTENT, ##args); \
    printf("\n"); \
} while (0)

int main()
{
    LOG("name[%s], age[%d]", "peter", 23);
    LOG("END");
    return 0;
}

宏展开后:
int main()
{
    do { printf("FILE:%s LN:%d FUN:%s " "name[%s], age[%d]", "define_test.cpp", 39, __func__, "peter", 23);  printf("\n"); } while (0);
    do { printf("FILE:%s LN:%d FUN:%s " "END", "define_test.cpp", 40, __func__); printf("\n"); } while (0);
    return 0;
}

输出:
FILE:define_test.cpp LN:39 FUN:main name[peter], age[23]
FILE:define_test.cpp LN:40 FUN:main END

上面的宏也可以定义为下面的形式,效果是一样的。

#define LOG(format, ...) do { \
    printf(LOG_INFO_FORMAT format, LOG_INFO_CONTENT, ##__VA_ARGS__); \
    printf("\n"); \
} while (0)

你肯定已经注意到在宏定义的可变参数前加了##,这个目的是当可变参数个数为0时,去掉前面的逗号。

可变参数的个数为0时,前面的宏定义展开后的形式是

int main()
{
    do { printf("FILE:%s LN:%d FUN:%s " "name[%s], age[%d]", "define_test.cpp", 39, __func__, "peter", 23);  printf("\n"); } while (0);
    // 如果没有加##的话,下面这行宏展开后会多出一个逗号,这时候就会编译报错
    do { printf("FILE:%s LN:%d FUN:%s " "END", "define_test.cpp", 40, __func__, ); printf("\n"); } while (0);
    return 0;
}