c语言中的宏(一)

学习 c 语言有一段时间了,以为 c 中的基础知识已经掌握的差不多。直到最近研究了一
下 linux 内核代码,才发现 c 的基础知识没有我想得那么简单。书本中和网站上容易找到
的资料大致覆盖了 c 中 80% 的基础知识,而剩下的 20% 却不容易找到。但这 20% 的基础
知识却对理解源代码十分重要,于是开始补习这 20% 的基础知识。结合网上网友的技术博
客,在 GNU gcc 的网站上下载了一些手册(v4.9.3),研究了一下。由于一时间看不完全部内
容,只好走一步算一步,将暂时了解到的东西整理出来。其中的逻辑难免有些不明确甚至混
乱,姑且当这仅是读书笔记,之后如果需要的话,还应做进一步的整理。文中所有关于宏的
东西都是关于 GNU gcc 编译器的,其中可能有些不是 c 标准中的东西,对于其他的编译,
器,比如微软的Visual Studio,是否适用我就不知道了。请自行区分。

c 语言中的宏是由 gcc 中的预处理器处理。与其说 gcc 是一个软件,不如说它是一个工具
链,在编译的过程中,gcc 依次调用 cpp、cc1、as 和 ld。其中 cpp 就是 c 语言的预处理
器。这里所说的cpp并不指 c++, 估计 cpp 是 C Preprocessor 的简写。我用的 gcc 版本
中,cpp 被安装在了 /usr/bin/ 中,所以能够直接调用。cpp 的用起来很简单,可以从标准
输入输入,也可以由文件输入。经过 cpp 处理后由标准输出输出结果。如果想观察 cpp 对宏
的处理结果,最好不要在文件中包含头文件,因为头文件中的大量内容会被包含在输出中,
使我们难以快速找到想要的信息。上面的 cpp 调用可以用 gcc -E 代替,只不过 gcc -E 只
支持从文件中输入,而不支持标注输入。下面举个例子:

file: "example.c"
------
#define STR(x) #x

int main()
{
    STR(navy);
    return 0;
}

在命令行中输入 cpp ./example.c 可以在标准输出中找到如下片段

int main()
{
    "navy";
    return 0;
}

不难看出程序中的 STR(navy) 被替换成了 “navy” 。上面讲过,cpp 可以用 gcc -E 代替。
在 gcc 中,经 cpp 处理后的文件拓展名为 .i ,可用 gcc 的 -o 选项指定。知道了如何
验证宏处理的结果,下将讨论宏处理的内容,参考资料主要是GUN gcc 的 The C
Preprocessor 手册,以下简称“手册。

一、两种宏

宏(Macros) 在不同的语境中含义有所不同。c 语言的宏在手册中的定义如下:

A macro is a fragment of code which has been given a name. Whenever the name is
used, it is replaced by the contents of the macro.

在这里,宏其实是一种替换的规则。c 语言中的宏包括两种: Object-like Macros 和
Function-like Macros。从名称中可以大致了解这两种宏所指的内容。但这里还是简单的介绍
一下:

Object-like Macros

下面定义一个宏

#define BUFFER_SIZE 1000

宏的名称可以是大写或小写,但为了与 c 文件中的其他符号区分,宏的名称经常全被写成大
写。cpp 会将 c 文件中的 BUFFER_SIZE 替换为1000。这里的替换是将 BUFFER_SIZE 看成
一个 Object,直接替换。

Function-like Macros

宏函数相对来说复杂一些

#define PLUS(a, b) a+b

cpp 会将PLUS(x, y) 替换成 x+y。记住,直接用宏函数括号中的参数替换后面表达式中
的内容。但是其实这里有一个问题。如果我们想利用PLUS()来计算 (x1+x2)*y 这个表达式,
我们应该怎样做呢?有些人很自然的想到

PLUS(x1, x2)*y

我们按照直接替换的原则,自行推导下

PLUS(x1, x2)*y => x1+x2*y ?!

出错了,为了修复这个bug我们可以将计算表达式改为(PLUS(x1, x2))*y 或者将最开始的宏定
义改为 #define PLUS(a, b) (a+b)。显然后者能方便我门以后对这个宏的使用。其实这次修
还有个问题。例如PLUS(x1+x2, y)这个表达式的会不会产生 (x1+x2)*y这个结果呢?直接替
换试试看,结果如下

(x1+x2*y)

只好再次修改宏定义,这下似乎没什么问题了。

#define PLUS(a, b) ((a)*(b))

其实在Object-like Macros也有类似的问题

#define SIZE SIZE_1 + SIZE_2

如果用SIZE来进行四则运算的话应该不是很安全,不妨改为

#define SIZE (SIZE_1 + SIZE_2)

这下暂时安全了。

二、宏参数

在 Function-like Macros 的括号中能是能接受参数的,这里称为宏参数。说
Function-like Macros 复杂,一个重要的原因就是因为宏参数。

1、字符串化和连接

宏参数能够字符串化和进行符号连接。这里主要介绍两个符号:# 和 ##。# 能够将宏参数字
符串化,## 能够将两个符号连接,产生新的符号。下面分别简要介绍下者两个符号。

#define STR(x) #x
...
printf("%s\n",STR(hello_world));

这个打印 “hello_world” 的方法就是运用了 # 这个符号,将 hello_world 字符串化成
“hello_world”。这里要注意 STR(x) 只能接收一个参数,所以 hello 与 world 之间的下
划线是十分必要的。下一个例子

#define CONS(a,b) a##b
...
int x1 = 1;
int x2 = 2;
...
printf("%d\n",CONS(x,2));

输出结果是2,因为CONS(x,2)会产生x2这个变量符号。

2、可变宏参数

宏参数能接收可变宏参数,GNU CPP 支持 C99 的标准

#define PRINT_1(format, ...) printf(format, __VA_ARGS__)

能够像如下的方式“调用”PRINT_1()

PRINT_1("The result is: %s, %s\n", "hello", "world");

C99 之前的 C 标准不支持可变宏参数(… 和 __VA_ARGS__),但是 GNU CPP 很早之前就
支持可变宏了,只是格式有所不同。GNU CPP 的可变宏定义方式如下

#define PRINT_1(format, args...) printf(format, args)

其中,符号 args 可以任意指定。如果想保持和其他 C 标准实现的可移植性,就应该使用
__VA_ARGS__ 方式定义可变宏参数;如果想保持和以前版本 GNU GCC 的兼容性,就应该
使用 args… 的方式定义可变宏参数。

问题还没结束,如果可变宏参数部分为空的话,结果是什么呢? 我们直接替换

PRINT_1("The result is") => printf("The result is",) ?!

怎么多了个逗号。看来这个宏定义的方式并不支持可变参数为空的情况。问题出现在多出了一
个逗号。这里我们利用 “##” 来修改下宏定义的方式。

#define PRINT_1(format, args...) printf(format ,##args)

如果 args… 部分是 x1 x2 x3,则 args 部分替换为 x1,x2,x3。之后 ## 连接逗号和
args 部分:”,x1,x2,x3”。最终的结果是 printf(“format”,x1,x2,x3)。如果 args… 部分
为空,”,##arg” 的最终部分也是空。最终的结果是 printf(“format”)。问题解决了。对于
__VA_ARGS__ 方式的宏定义,可以采用同样的方式利用 ## 修改。这里有个地方要注意。
宏定义中 format 和逗号直接要有一个空格分开,否则,如果 args 为空,format 会和逗号
一起被“沉默”。

3、参数预扫描(Argument Prescan)

Macro arguments are completely macro-expanded before they are substituted
into a macro body, unless they are stringified or pasted with other tokens.
After substitution, the entire macro body, including the substituted
arguments, is scanned again for macros to be expanded. The result is that the
arguments are scanned twice to expand macro calls in them.

以上是手册中的一段内容。cpp在扩展对宏参数的替换包括的两轮扫描:第一轮替换中,如果
参数中包含宏,则先将参数中的宏替换;第二轮替换包括替换参数后的整个宏。说的有些复
杂,其实就是拓展宏参数,再拓展其他部分。引用用手册中的一个例子:

#define foo a,b
#define bar(x) lose(x)
#define lose(x) (1 + (x))

对于这样的宏定义,如果采用下面的形式调用

bar(foo)

会有什么结果呢?我们期望的是 (1 + (a,b)) ,结果却不是这样的。按照上面对宏参数扫描
(argument prescan) 介绍,宏展开过程如下:

bar(foo) -> bar(a,b) -> lose(a,b) -> ?!

这下出了问题,lose()只能接收一个参数,宏展开出错。要解决这个问题应该将宏定义改成

#define foo (a,b)
#define bar(x) lose(x)
#define lose(x) (1 + (x))

关键是先替换参数,再替换其他。Argument Prescan 另外一个要注意的地方是在宏参数
被字符串化(#)或与其他符号链接(##)的情况下不进行替换
。看上去有些复杂,还是举个例子:

#define STR(x) #x
#define NUM 100

以上的宏定义很简单,这里不多解释。STR(NUM) 会被替换成什么呢?答案是 "NUM" (注
意,这里的双引号是结果的一部分,可以自己用前面讲过的方法直接用cpp扫描替换并且验证。
argument prescan

三、复合语句宏定义

下面定义一个能统计字符串中字符个数的宏,统计 str 中的字符数,字符数复制给 n:

#define COUNT(str, n) \
        char *c = str;\
         n = 0;\
        while (*c++ != '\0')\
            n++;

有时,为了使宏定义中的临时变量有自己的作用域,我们可以用 “{}” 将这些语句包围起来。

#define COUNT(str, n) \
        {\
        char *c = str;\
         n = 0;\
        while (*c++ != '\0')\
            n++;\
        }

使用者在使用时并不区分普通函数和宏函数。都在”函数调用”后面添加分号,组成语句

COUNT(str, n);

如果我们用如下的 if 语句结构调用

if (...)
    COUNT(str, n);
else
    ...

直接替代后

if (...)
    {...};    !?
else
    ...

这样就出错了。为了解决这个问题,可将宏定义改为

#define COUNT(str, n) \
        do {\
        char *c = str;\
         n = 0;\
        while (*c++ != '\0')\
            n++;\
        } while (0)

同样采用直接替代的方法可以发现问题解决了。