C语言中,预处理指令是由#字符开头的一些命令。大多数预处理指令属于下面3种类型之一:
- 宏定义:#define指令定义一个宏,#undef指令删除一个宏定义。
- 文件包含:#include指令导致一个指定文件的内容被包含到程序中。
- 条件编译。#if、#ifdef、#ifndef、#elif、#else和#endif指令能根据预处理器可以测试的条件来确定,是将一段文本块包含到程序中,还是将其排除在程序之外。
剩下的#error、#line、#pragma指令较少用到。
本文主要讲宏定义和条件编译。
宏定义
在C语言中,宏定义(Macro Definition)是一种在预处理阶段进行的文本替换机制。它允许程序员为一段代码或数据定义一个别名(即宏),以便在程序的后续部分中通过简单地引用这个别名来使用该代码或数据。宏定义通常使用#define指令来实现。
简单的宏
简单的宏定义格式如下:
举例:
- #define PI 3.14159
- #define N 100
- ...
- int a[N];
复制代码
注意,在宏定义的末尾不要添加分号,下面的语句是错误的:
- #define N 100;
- ...
- int a[N];
复制代码
带参数的宏
带参数的宏的定义格式如下:
- #define 标识符(x1,x2,..,xn) 替换列表
复制代码
例如:
- #define MAX(x,y) ((x)>(y)?(x):(y))
复制代码
宏的通用属性
-
宏的替换列表可以包含对其他宏的调用。例如,我们可以用宏PI来定义宏TWO_PI:
- #define PI 3.1415926
- #define TWO_PI (2*PI)
复制代码当预处理器在后面的程序中遇到TWO_PI时,会将它替换成(2*PI)。接着,预处理器会重新检查替换列表,看它是否包含其他宏的调用(在这个例子中,调用了宏PI)。预处理器会不断重新检查替换列表,直到将所有的宏名字都替换完为止。
-
宏定义的作用范围通常到出现这个宏的文件末尾。由于宏是由预处理器处理的,它们不遵从通常的作用域规则。定义在函数中的宏并不是仅在函数内起作用,而是作用到文件末尾。
-
宏不可以被定义两遍,除非新的定义与旧的定义是一样的。
-
宏可以使用#undef指令取消定义。#undef指令有如下形式:
其中,标识符是一个宏名。例如,指令#undef N会删除宏N当前的定义。(如果N没有被定义成一个宏,则#undef指令没有任何作用。)#undef指令的一个用途是取消宏的现有定义,以便于重新给出新的定义。
实际编程中,遵守的一些规范
在实际编程中,对于宏的使用,良好的编程习惯至少需要遵循以下规范:
-
使用宏定义表达式时,要使用完备的括号,以避免运算优先级问题。
示例:如下定义的宏都存在一定的风险
当运行一下代码时:
- a = 5;
- printf("%d\n", SQUARE(a + 1));
复制代码预期结果是36,但是实际上我们将用到的宏的地方替换,其实是:
- printf("%d\n", a+1 * a + 1);
复制代码即输出结果为11。
为了解决这个问题,只要在宏参数中加上两个括号就行:
- #define SQUARE(x) (x) * (x)
复制代码但是现在有另一个宏:
- #define DOUBLE(x) (x) + (x)
复制代码又会出现另一个问题。例如:
- a = 5;
- printf("%d\n", 10 * DOUBLE(a));
复制代码我们期望的值是 100。但是通过宏展开得到:
- printf("%d\n", 10 *(x) + (x));
复制代码得到的结果是 55。
这个错误也很容易修正:只要在整个表达式两边加上一对括号即可:
- #define DOUBLE(x) ((x) + (x))
复制代码所以,一般在定义宏时,首先每个宏参数都加上括号,其次整体表达式也要加上一对括号。
-
不要使用带副作用的宏参数。当宏参数在宏定义中出现的次数超过一次时,如果这个参数有副作用,那么当你使用这个宏时就可能出现危险,导致不可预料的结果。
例如:
- #define MAX(a, b) ((a) > (b) ? (a) : (b))
- ....
- x = 5;
- y = 8;
- z = MAX(x++, y++);
- printf("x=%d, y=%d, z= %d\n", x, y, z);
复制代码第一个表达式是条件表达式,用于确定执行两个表达式中的哪一个,剩余的那个表达式将不会执行。
那上面这段代码的输出是多少呢?
x =6, y=10, z= 9;
为什么呢?
我们将宏定义展开:
- z = ((x++) > (y++) ? (x++) : (y++));
复制代码首先是比较两个表达式,比较完后,x= 6, y = 9.并且由于 y 比 x 大,所以在比较完后 y 还会再执行一次 y++。所以最终的结果是 y =10。
-
当宏定义中包含多条语句时,最好使用do-while(9)的结构来包裹这些语句,以避免在使用宏时产生意外的副作用。
比如:
- #define SWAP(x, y) do { \
- (x)->buffer = (y)->buffer; \
- (x)->orig_buffer = (y)->orig_buffer; \
- } while(0)
复制代码
预定义宏
C语言中有一些预定义宏,每个宏表示一个整型常量或字面串。下面是一些常用的预定义宏(注意,下面的__是两个下划线_):
- __LINE__:表示当前宏所在行的行号
- __FILE__:当前文件的名字
- __DATE__:编译的日期(格式"mm dd yyyy")
- __TIME__:编译的时间(格式"hh:mm:ss")
- __STDC__:如果编译器符合C标准(C89或C99),那么值为1.
上述宏中,__LINE和__FILE__是用得最多的两个,而且通常两个一起用,用来定位实际问题。
- zld@zld:~/Codes/C_TEST$ cat -n test6.c
- 1 #include <stdio.h>
- 2
- 3 #define LOG_PRINT(message) do \
- 4 { \
- 5 printf("Debug: %s at %s:%d\n", message, __FILE__, __LINE__); \
- 6 } while (0)
- 7
- 8
- 9 int main()
- 10 {
- 11 LOG_PRINT("Start of the program");
- 12
- 13
- 14 LOG_PRINT("End of the program");
- 15
- 16 return 0;
- 17 }
复制代码
运行结果:
- zld@zld:~/Codes/C_TEST$ ./test6
- Debug: Start of the program at test6.c:11
- Debug: End of the program at test6.c:14
复制代码
从上面的结果可以看出,通过使用__FILE__和__LINE__两个预定义宏,能够正确显示文件名和行号。
但是没有显示所在的函数名。
C99中定义了一个新特性__func__标识符。__func__与预处理器无关,但是由于它也是一般用于调试,所以经常和__FILE以及__LINE__一起使用。
- zld@zld:~/Codes/C_TEST$ cat -n test7.c
- 1 #include <stdio.h>
- 2
- 3 #define LOG_PRINT(message) do \
- 4 { \
- 5 printf("Debug: %s at %s:%d in function:%s\n", message, __FILE__, __LINE__, __func__); \
- 6 } while (0)
- 7
- 8 void Foo()
- 9 {
- 10 LOG_PRINT("Start of the Foo function");
- 11 }
- 12
- 13 int main()
- 14 {
- 15 LOG_PRINT("Start of the program");
- 16
- 17 Foo();
- 18
- 19 LOG_PRINT("End of the program");
- 20
- 21 return 0;
- 22 }
复制代码
运行结果:
- zld@zld:~/Codes/C_TEST$ ./test7
- Debug: Start of the program at test7.c:15 in function:main
- Debug: Start of the Foo function at test7.c:10 in function:Foo
- Debug: End of the program at test7.c:19 in function:main
复制代码
参数个数可变的宏
我们知道,C语言中,函数的参数个数是支持可变的。而对于宏,在C89标准中,如果宏有参数,那么参数的个数是固定的。在C99中,这个条件被放宽了,允许宏具有可变长度的参数列表。
C99引入了一个特殊的预定义宏__VA_ARGS__,它允许你定义接受参数可变数量参数的宏。这是通过宏定义中的省略号(...)来实现的。注意省略号(...)只能出现在宏参数列表的最后,前面是普通参数。
举例:
- zld@zld:~/Codes/C_TEST$ cat test8.c
- #include <stdio.h>
- #define DEBUG(fmt, ...) printf(fmt, ## __VA_ARGS__)
- int main()
- {
- int a = 5;
- float b = 3.14;
- DEBUG("Integer: %d\n", a);
- DEBUG("Float:%f\n", b);
- DEBUG("No extra args\n");
- return 0;
- }
复制代码
在这个例子中,DEBUG宏接受一个格式字符串fmt和任意数量的额外参数(由...表示)。在宏定义中,__VA_ARGS__被替换为传递给宏的所有额外参数。
运行结果:
- zld@zld:~/Codes/C_TEST$ ./test8
- Integer: 5
- Float:3.140000
- No extra args
复制代码
#运算符与##运算符(了解即可,用的不多)
宏定义可以包含两个专用的运算符:#和##。编译器不会识别这两种运算符,它们会在预处理时被处理。
(1) 字符串常量化运算符(#):在宏定义中,当需要把一个宏的参数转换成字符串常量时,可以使用字符串常量运算符(#):
- #define PRINT_INT(n) printf(#n " = %d\n", n)
复制代码
n之前的#运算符通知预处理器根据PRINT_INT的参数创建一个字面串。因此,调用PRINT_INT(i/j);会变为:
- printf("i/j" " = %d\n", i/j);
复制代码
在C语言中,相邻的字面串会被合并,因此上面的语句等价于:
- printf("i/j = %d\n", i/j);
复制代码
当程序执行时,printf函数会同时显示表达式i/j和它的值。例如,如果i是11,j是2,则输出为:
(2)标记粘贴运算符(##):宏定义内的标记粘贴运算符(##)会合并两个参数。
- #include <stdio.h>
- #define P(A, B) printf("%d##%d = %d", A, B, A##B)
- int main() {
- P(5, 6);
- return 0;
- }
复制代码
输出:
注意:当宏参数是另一个宏的时候,需要注意的是凡宏定义中有用#或者##的地方宏参数时不会再展开:
- #include <stdio.h>
- #define f(x, y) x##y
- #define g(x) #x
- #define h(x) g(x)
- int main()
- {
- printf("%s, %s\n", g(f(1, 2)), h(f(1,2)));
- return 0;
- }
复制代码
输出:
- zld@zld:~/Codes/C_TEST$ ./test5
- f(1, 2), 12
复制代码
解析:第一个表达式 g(f(1,2)),g(x)的定义中有#,不展开 f(x,y)的宏,直接替换成#f(1,2),打印输出为 f(1,2)。
第二个表达式 h(f(1, 2)),h(x)的定义中没有#或者##,需要展开 f(x, y)的宏, 即 1##2,即 h(12)->g(12), 最终结果是 12。
条件编译
C语言的条件编译时一种预处理功能,它允许程序在编译时根据特定的条件包含或排除代码段。这种功能通过预处理指令来实现,最常用的预处理指令有**#if指令和#endif指令**、#ifdef指令和#ifndef指令、#elif指令和#else指令。
#if指令和#endif指令
一般来说,#if指令的格式如下:
#endif指令的格式如下:#endif
当预处理器遇到#if指令时,会计算常量表达式的值。如果常量表达式的值为0,那么#if与#endif之间的行将在预处理过程中从程序中删除;否则,#if和#endif之间的行会被保留在程序中,继续留给编译器处理——这时#if和#endif对程序没有任何影响。
- zld@zld:~/Codes/C_TEST$ cat -n test1.c
- 1 #include <stdio.h>
- 2
- 3 #define VERSION 1
- 4 // #define VERSION 2
- 5 int main()
- 6 {
- 7 #if VERSION == 1
- 8 printf("Running version 1 of the program.\n");
- 9 #endif
- 10
- 11 printf("Program execution continues...\n");
- 12 return 0;
- 13 }
复制代码
上述代码,当第3行没有注释,第4行注释了时,运行结果如下:
- zld@zld:~/Codes/C_TEST$ ./test1
- Running version 1 of the program.
- Program execution continues...
复制代码
当地3行注释,第4行没有注释时,运行结果:
- zld@zld:~/Codes/C_TEST$ ./test1
- Program execution continues...
复制代码
在实际项目开发中,如果有些代码当前没有用,但是又不想删,以后可能又会用到这段代码。相比于用/* */来注释这段代码,其实用#if 0更方便。这是因为在C语言中,注释不能嵌套,会导致编译错误:
初始时的一段程序如下:
- zld@zld:~/Codes/C_TEST$ cat -n test2.c
- 1 #include <stdio.h>
- 2
- 3 int main()
- 4 {
- 5 /*
- 6 * This is a comment
- 7 */
- 8 printf("Hello world.\n");
- 9
- 10 /*
- 11 * This is another comment
- 12 */
- 13 printf("Hello C.\n");
- 14
- 15 /* This is the third comment */
- 16 printf("Hello C++\n");
- 17 return 0;
- 18
- 19 }
- zld@zld:~/Codes/C_TEST$
复制代码
上述程序,如果我第13行到第16行的程序想注释掉(大家想象一下这个代码有很多行,中间穿插着注释)。如果我用/* */注释的话将会报错:
- zld@zld:~/Codes/C_TEST$ cat -n test2.c
- 1 #include <stdio.h>
- 2
- 3 int main()
- 4 {
- 5 /*
- 6 * This is a comment
- 7 */
- 8 printf("Hello world.\n");
- 9
- 10 /*
- 11 /*
- 12 * This is another comment
- 13 */
- 14 printf("Hello C.\n");
- 15
- 16 /* This is the third comment */
- 17 printf("Hello C++\n");
- 18 */
- 19 return 0;
- 20
- 21 }
- zld@zld:~/Codes/C_TEST$ gcc test2.c -o test2
- test2.c: In function ‘main’:
- test2.c:18:10: error: expected expression before ‘/’ token
- 18 | */
- | ^
复制代码
而用#if 0注释则不会有这个问题:
- zld@zld:~/Codes/C_TEST$ cat -n test2.c
- 1 #include <stdio.h>
- 2
- 3 int main()
- 4 {
- 5 /*
- 6 * This is a comment
- 7 */
- 8 printf("Hello world.\n");
- 9
- 10 #if 0
- 11 /*
- 12 * This is another comment
- 13 */
- 14 printf("Hello C.\n");
- 15
- 16 /* This is the third comment */
- 17 printf("Hello C++\n");
- 18 #endif
- 19 return 0;
- 20
- 21 }
- zld@zld:~/Codes/C_TEST$ ./test2
- Hello world.
复制代码
defined运算符
在C语言中,defined运算符是一个预处理运算符,如果标识符是一个定义过的宏则返回1,否则返回0。
通常和#if指令结合使用,可以这样写:
- #if defined(DEBUG)
- ...
- #endif
复制代码
仅当DEBUG被定义成宏时,#if和#endif之间的代码会被保留在程序中,DEBUG两侧的括号不是必须的,因此可以简单地写成:
因为defined运算符仅检测DEBUG是否有定义,所以不需要给DEBUG赋值:
比如在头文件中为了防止被重复include,我们可以在头文件中这么写:
- // 头文件的开始处
- #if !defined XXX_XXX
- #define XXX_XXX
- ...
- #头文件的结束处
- #endif
复制代码
#ifdef指令和#ifndef指令
#ifdef指令用于测试一个标识符是否已经定义为宏,其格式如下:
#ifdef指令的使用与#if指令类似:
严格地说,并不需要#ifdef指令,因为可以结合#if指令和defined运算符来得到相同的效果。换言之,指令
等价于:
#ifndef指令与#ifdef指令类似,但测试的是标识符是否没有被定义成宏,其格式如下:
上述指令等价于:
在C语言的头文件中,为了防止头文件被重复include,在头文件中更多的是用#ifndef的形式:
- #ifndef __SDS_H
- #define __SDS_H
- ...
- #endif
复制代码
#elif指令和#else指令
为了提供更多的遍历,预处理器还支持#elif和#else指令。它们的格式如下:
#elif指令和#else指令可以与#if指令、#ifdef指令、#ifndef指令结合使用,来测试一系列条件:
- zld@zld:~/Codes/C_TEST$ cat test4.c
- #include <stdio.h>
- // 假设我们用一些宏来标识不同的操作系统
- // #define __WIN32 // Windows操作系统
- // #define __Linux__ // Linux操作系统
- // #define __APPLE__ // macOS操作系统
- int main ()
- {
- #if defined(__WIN32)
- printf("This is Windows.\n");
- // 处理Windows特有的流程
- #elif defined(__Linux__)
- printf("This is Linux.\n");
- // 处理Linux特有的流程
- #elif defined(__APPLE__)
- printf("This is macOS.\n");
- // 处理macOS特有的流程
- #else
- printf("Unknown operating system.\n");
- // 处理其他未知操作系统的流程
- #endif
- // 通用流程
- printf("Program continues...\n");
- return 0;
- }
复制代码