再谈C语言中的预处理系统
C语言中的预处理器系统是一种十分古老而又神奇的存在。其实大部分汇编语言也具备预处理系统。预处理系统其实就是对当前宿主语言的 元语言(Meta-Language),可用来根据当前的环境以及配置来“生成”相应的源代码。因此预处理系统的语法体系与当前宿主语言的语法体系可以被看作为是独立的,我们可以参考GAS汇编语言的预处理语法就更接近于C语言的预处理器。
我们常用的C语言中的预处理器主要有:条件包含(#if
、#ifdef
、#else
等),头文件包含(#include
),宏替换(#define
),行控制(#line
),错误指示符(#error
),编译杂项(#pragma
)以及空指示符(#
)。本文将主要介绍前三个。
我们先来谈谈预处理器中的宏。有一定C语言基础的朋友已经知道,在C语言中定义的一个宏,当在源文件中使用它的时候,它会被展开,替换为它原来的形式,比如:#define MY_MACRO 5 + 5
,当你用MY_MACRO
这宏的时候,像:int a = MY_MACRO * 2;
,那么其实这里的语句就相当于:int a = 5 + 5 * 2;
,也就是说,MY_MACRO
在源文件中不被计算,而是很机械地进行展开。那我们是否就能认为C语言的预处理系统就如此简单,基本就靠替换展开吃饭的呢?大部分情况确实如此,但预处理器系统也有对表达式进行计算的时候!当我们使用了 条件包含 预处理器时,这其中的 整数常量表达式 将会被计算。我们来看以下一个例子:
#define MACRO_VALUE1 5 + 5
#define MACRO_VALUE2 (-6U * 1)
#if MY_DEFINED_MACRO == 0 && 100 % 5 == 0 && MACRO_VALUE1 < MACRO_VALUE2 && (MACRO_VALUE1 * 2 < 20)
#warning MY_DEFINED_MACRO not zero!
#endif // MY_DEFINED_MACRO
以上这个短小的例子所包含的信息量倒是不少。这里首先要强调的一点是,#if
后面必须跟的是 整数常量表达式,如果不满足要求的话,预处理器直接报错。比如以下都是非法的条件包含:
// 这里出现了浮点数字面量,尽管 > 操作之后的结果是个整数常量表达式,
// 但它却是非法的。
#if 5.0 > 1.0
#endif
#define DUMMY_MACRO
// 这里没有对DUMMY_MACRO做任何整数常量表达式的定义,因此也是非法的
#if DUMMY_MACRO
#endif
我们再回到第一个代码例子。这里的#if
后面跟着四个条件表达式,并且结果都是“真”。我们先看第一个,MY_DEFINED_MACRO == 0
为何是“真”?因为C语言的预处理器有个神奇的约定——如果一个宏符号没有被定义,那么在#if
中常量表达式的计算结果即为0,表示“假”,因此这里就相当于“0 == 0”,那么这个比较的结果就是“真”了。
第二个条件表达式比较容易理解,它是一个很常规的整数常量表达式的判断。
第三个又有点儿意思了。我们首先能看到,在对#if
进行计算时,如果碰到一个宏符号,那么也会对它做展开。然后,C语言的预处理器是可以计算带符号整数与无符号整数的,由于这里的-6U在32/64位系统环境下其实是 232 - 6,因此结果确实大于 5 + 5,因而条件成立。
在第四个条件表达式中我们可以看到,在做条件包含计算时,宏符号也会被机械式地展开,而不是直接做计算,因此这里的MACRO_VALUE1 * 2
展开后也是5 + 5 * 2
,结果为15,因此小于20,条件成立。
由此我们可以看到,C语言的条件包含也会做计算,并且也会对之前定义的宏做展开,然后进行计算。
下面我们再来谈谈宏替换。C语言预处理器系统中的宏替换功能确实比较强大,而且大部分符号都能作为有效的宏函数的“实参”。我们可以利用这一点来封装一些固定样式的代码。我们看以下例子:
#define MACRO_NOP_ARG
#define MEMBERS_INITIALIZER(obj_access) obj_access member0, \
obj_access member1, \
obj_access member2, \
obj_access member3
#define MY_TEST_MACRO(param1, param2) param1 param2
int main(void)
{
struct {
int member0, member1, member2, member3;
}obj = { 1, 2, 3, 4 };
int member0 = 10, member1 = 20, member2 = 30, member3 = 40;
int arrObj[] = { MEMBERS_INITIALIZER(obj.) };
int arr[] = { MEMBERS_INITIALIZER(MACRO_NOP_ARG) };
printf("The values are: ");
for (size_t i = 0; i < sizeof(obj) / sizeof(obj.member0); i++)
printf("%d ", arrObj[i] + arr[i]);
puts("");
MY_TEST_MACRO(int tst = 100; , (++tst, tst += 10));
// 输出:tst = 111
printf("tst = %d\n", tst);
}
上述代码举了两个利用宏替换的例子。第一个例子是通过一个宏函数定义来满足两种不同的数组初始化的效果。
首先,MEMBERS_INITIALIZER
宏函数的定义允许使用结构体对象去访问固定的member0到member3这四个成员,表示为:obj_access
。不过这个参数可以缺省,若是缺省,那么其实就是直接对变量member0到member3进行依次访问了。不过对于确定参数的宏函数,我们必须要传个实参进去,但这依旧阻止不了我们想将obj_access
进行缺省。以上代码例子定义了一个MACRO_NOP_ARG
这个宏,这个技巧就在于用这个缺省定义的宏能提供缺省宏函数实参的效果。
第二个例子很夸张地呈现出宏替换的逼格,我们发现,对于宏函数“调用”的实参中充满了=
、甚至还有;
分号。但这些符号都是合法的,唯独,
逗号在此宏函数调用中才起到参数分隔的作用。