C语言#define预处理指令的用法(非常详细,图文并茂)
在 C语言中,预处理指令是在编译过程中进行的一些预处理操作。它们并非 C语言的语句,而是由预处理器处理的指令。
源代码中以 # 开头的内容都是预处理指令,预处理指令通常出现在 C语言源文件的开头。
在 C语言代码编译前,预处理器会先处理预处理指令,根据指令的含义修改 C语言代码。修改后的代码会被另存为中间文件或直接输入编译器中,而不会保存到源文件中。因此,预处理器不会改动源文件。
本节讲解 C 语言中的 #define 预处理指令。#define 的用法如下:

图 1 符号常量的预处理
经过预处理后,代码将变为下面程序中的代码,接着编译器会对预处理后的代码进行编译:
例如,下面展示了一个正确的替换示例:

图 2 替换宏
运行结果为:
从上面的示例中可以看出,宏的替换是无差别的,它仅把代码当作文本来处理,遇到宏就替换为宏对应的替换体:
然而,下面展示了一个错误的替换示例,也是初学者经常会犯的错误。
尽管带参数的 #define 定义的宏函数在使用方式上类似于函数,但其本质仍然是将宏替换为对应的替换内容。因此,如果将其简单地视为函数使用,则可能会出现问题。
现在写一个宏函数,它的作用是求一个数的平方:
运行结果如下图所示:

图 3 SQUARE(n)
将 2 代入运算,2*2 结果为 4。预期结果也是 4,因此运算结果符合预期。

图 4 SQUARE(n + 2)
将 2 代入运算,2 + 2 * 2 + 2,乘法的优先级高于加法,先算 2 * 2,再从左到右算加法,结果为 8。预期结果为 16,因此运算结果不符合预期。
我们的本意是希望 n + 2 作为一个整体优先进行运算。想要实现这个预期,需要使用括号将 n + 2 括起来。因此,我们将预处理命令修改为 #define SQUARE(x) (x)*(x),以 n + 2 作为它的参数,它展开后为 (n + 2) * (n + 2)。这样可以保证结果正确。

图 5 100 / SQUARE(n)
将 2 代入运算,100 / 2 * 2,除法优先级与乘法一致,因此从左向右计算,结果为 100。预期结果为 25,因此运算结果不符合预期。
我们的本意是希望 n * n 作为一个整体优先进行运算。但实际上,因为优先级问题可能被左右两边的运算符影响。因此,应该在 n * n 表达式两边添加括号,以确保它优先完成计算。因此,我们将预处理指令修改为 #define SQUARE(x) (x * x),使其展开后的表达式为 100 /(2 * 2),从而保证结果正确。

图 6 SQUARE(++n)
在一个表达式中多次对同一个变量 n 使用 ++ 运算符,结果是不确定的。
宏函数展开后的表达式应当作为一个整体,以避免被左右运算符优先级影响。在本例中为
结合以上两点,本例中的宏函数应当修改为
若宏函数的替换体多次使用参数,那么不要在宏函数的参数内填自增、自减表达式。
由于宏函数仅仅是完成替换操作,将参数替换并拼接到替换体的表达式中,而不是先让参数运算得到结果后,再进行运算,因此为了保证参数不被其他运算符优先级影响,需要在参数两边加上括号。
此外,宏函数展开后的表达式如果作为一个更大表达式的子表达式,则可能会受到左右两边运算符优先级的影响。为了保证宏函数展开后的表达式能够优先计算,需要在替换体两边加上括号。
最后,为了避免在一个表达式中对同一个变量多次进行自增、自减操作,如果宏函数的替换体在一个表达式中多次使用同一个参数,则不要在宏函数的参数内填写自增、自减表达式。
下面是一个更加复杂的示例:
由于两个字符串之间出现了一个变量 number,不符合 printf() 第一个参数需要字符串的写法,因此无法编译通过。
如果不使用 ##,则会发生什么?
不使用 ##,展开后的两个参数之间留有空格,无法正常使用。那如果删除替换体中的空格呢?
其余的类似上面的处理。代码预处理之后,将会变为如下所示:

图 7 宏重定义
因此,更合适的做法是使用预处理指令 #undef 取消这个宏的定义,然后重新定义它。具体代码如下:
源代码中以 # 开头的内容都是预处理指令,预处理指令通常出现在 C语言源文件的开头。
在 C语言代码编译前,预处理器会先处理预处理指令,根据指令的含义修改 C语言代码。修改后的代码会被另存为中间文件或直接输入编译器中,而不会保存到源文件中。因此,预处理器不会改动源文件。
本节讲解 C 语言中的 #define 预处理指令。#define 的用法如下:
#define 宏 替换体例如,预处理指令 #define 定义了一个符号常量,其值为 3:
#include <stdio.h> #define PRICE 3 // 商品的价格为 3 元 int main() { int num; int total = 0; // 买一件 num = 1; total = num * PRICE; printf("num:%d total:%d\n", num, total); // 买两件 num = 2; total = num * PRICE; printf("num:%d total:%d\n", num, total); // 买三件 num = 3; total = num * PRICE; printf("num:%d total:%d\n", num, total); return 0; }查看下图,预处理器会根据预处理指令的含义,将符号 PRICE 替换为 3,同时删除代码中的预处理指令。

图 1 符号常量的预处理
经过预处理后,代码将变为下面程序中的代码,接着编译器会对预处理后的代码进行编译:
#include <stdio.h> int main() { int num; int total = 0; num = 1; total = num * 3; printf("num:%d total:%d\n", num, total); num = 2; total = num * 3; printf("num:%d total:%d\n", num, total); num = 3; total = num * 3; printf("num:%d total:%d\n", num, total); return 0; }注意,宏的命名规则遵循 C语言标识符的命名规则:只能使用字母、数字、下画线,且首字符不能是数字。替换体不仅限于值,它的形式非常丰富,唯一要求是替换后的代码仍能正常通过编译。
例如,下面展示了一个正确的替换示例:
#include <stdio.h> # define INTGER int # define FMT "n1 = %d, n2 = %d, n3 = %d" # define VAR n3 int main() { INTGER n1, n2, n3; n1 = 1; n2 = 2; VAR = 3; printf(FMT, n1, n2, VAR); return 0; }预处理器会进行如下图所示的替换操作:

图 2 替换宏
运行结果为:
n1 = 1, n2 = 2, n3 = 3
代码能够成功运行。从上面的示例中可以看出,宏的替换是无差别的,它仅把代码当作文本来处理,遇到宏就替换为宏对应的替换体:
- 将宏 INTGER 替换为 int。
- 将宏 VAR 替换为 n3。
- 将宏 FMT 替换为"n1 = %d, n2 = %d, n3 = %d"。
然而,下面展示了一个错误的替换示例,也是初学者经常会犯的错误。
#include <stdio.h> #define PI 3.1415926; int main() { float r = 2.0; float area = PI * r * r; printf("圆的面积为 %f\n", area); return 0; }上面的示例在预处理指令 #define 的末尾加上了一个分号。当在程序中使用该宏时,就会导致语法错误,例如下面的语句:
float area = PI * r * r;会被替换如下:
float area = 3.1415926; * r * r;很明显,多了一个分号导致了编译错误。为了避免这类错误,建议在定义宏时不要在末尾加分号。正确的定义应该如下:
#define PI 3.1415926这样,当在程序中使用宏时,替换后的代码就可以被正确地编译和执行。
C语言带参数的#define
在 #define 中使用参数可以创建类似函数的宏函数。宏函数的使用格式如下:#define 宏(参数1, 参数2, ..., 参数n) 替换体例如,用于求 a 和 b 两个数的平均值的宏函数可以写成以下形式:
#define MEAN(a, b) (a + b)/2在程序中,可以按照以下方式使用它:
int result; result = MEAN(2, 4);经过预处理后,宏被替换为以下形式:
int result; result = (2 + 4)/2;在参数 a 的位置填写了 2,因此在替换内容中,所有的 a 都将被替换为 2;在参数 b 的位置填写了 4,因此在替换内容中,所有的 b 都将被替换为 4。
尽管带参数的 #define 定义的宏函数在使用方式上类似于函数,但其本质仍然是将宏替换为对应的替换内容。因此,如果将其简单地视为函数使用,则可能会出现问题。
现在写一个宏函数,它的作用是求一个数的平方:
#define SQUARE(x) x*x接下来,使用这个宏函数:
int n = 2; SQUARE(n); SQUARE(n + 2); 100 / SQUARE(n); SQUARE(++n);如果 SQUARE 是一个函数,那么我们的预期结果如下:
- 函数参数为 2,2 的平方为 4:
- 函数参数为 2 + 2 的结果,即 4,4 的平方为 16;
- 100 除以 2 的平方,即 100 / 4 = 25;
- 参数的值为 3,3 的平方为 9。
运行结果如下图所示:
4 8 100 16其中除了第一条符合预期,其他的均不符合预期。我们将逐条探究其原因。
1) SQUARE(n)
查看下图,SQUARE(n) 展开为 n*n。
图 3 SQUARE(n)
将 2 代入运算,2*2 结果为 4。预期结果也是 4,因此运算结果符合预期。
2) SQUARE(n + 2)
查看图 2,SQUARE(n + 2) 展开为 n + 2 * n + 2:
图 4 SQUARE(n + 2)
将 2 代入运算,2 + 2 * 2 + 2,乘法的优先级高于加法,先算 2 * 2,再从左到右算加法,结果为 8。预期结果为 16,因此运算结果不符合预期。
我们的本意是希望 n + 2 作为一个整体优先进行运算。想要实现这个预期,需要使用括号将 n + 2 括起来。因此,我们将预处理命令修改为 #define SQUARE(x) (x)*(x),以 n + 2 作为它的参数,它展开后为 (n + 2) * (n + 2)。这样可以保证结果正确。
3) 100 / SQUARE(n)
查看下图,100 / SQUARE(n) 展开为 100 / n * n:
图 5 100 / SQUARE(n)
将 2 代入运算,100 / 2 * 2,除法优先级与乘法一致,因此从左向右计算,结果为 100。预期结果为 25,因此运算结果不符合预期。
我们的本意是希望 n * n 作为一个整体优先进行运算。但实际上,因为优先级问题可能被左右两边的运算符影响。因此,应该在 n * n 表达式两边添加括号,以确保它优先完成计算。因此,我们将预处理指令修改为 #define SQUARE(x) (x * x),使其展开后的表达式为 100 /(2 * 2),从而保证结果正确。
4) SQUARE(++n)
查看下图,SQUARE(++n) 展开为 ++n * ++n:
图 6 SQUARE(++n)
在一个表达式中多次对同一个变量 n 使用 ++ 运算符,结果是不确定的。
5) 保证宏函数按照预期运行
宏函数的参数应当作为一个整体,优先运算。在本例中为#define SQUARE(x) (x)*(x)
。宏函数展开后的表达式应当作为一个整体,以避免被左右运算符优先级影响。在本例中为
#define SQUARE(x) (x * x)
。结合以上两点,本例中的宏函数应当修改为
#define SQUARE(x) ((x)*(x))
。若宏函数的替换体多次使用参数,那么不要在宏函数的参数内填自增、自减表达式。
由于宏函数仅仅是完成替换操作,将参数替换并拼接到替换体的表达式中,而不是先让参数运算得到结果后,再进行运算,因此为了保证参数不被其他运算符优先级影响,需要在参数两边加上括号。
此外,宏函数展开后的表达式如果作为一个更大表达式的子表达式,则可能会受到左右两边运算符优先级的影响。为了保证宏函数展开后的表达式能够优先计算,需要在替换体两边加上括号。
最后,为了避免在一个表达式中对同一个变量多次进行自增、自减操作,如果宏函数的替换体在一个表达式中多次使用同一个参数,则不要在宏函数的参数内填写自增、自减表达式。
C语言宏函数的运算符
在 C语言中,# 和 ## 是两个常用的宏函数运算符:- # 运算符可以将宏参数转换为字符串;
- ## 运算符可以将两个宏参数连接起来以形成一个新的标识符。
1) #(井号)
# 运算符可以将宏参数转换为字符串,其语法格式为 # 参数。例如:#define STR(x) #x printf("%s\n", STR(Hello world)); // 输出 "Hello world"在这个例子中,宏函数 STR 将其参数 x 转换为字符串,因此在 printf() 函数中输出的是字符串 "Hello world"。
下面是一个更加复杂的示例:
#include <stdio.h> #define FMT(varname) "The value of " #varname " is %d\n" int main() { int number = 123; printf(FMT(number), number); return 0; }FMT(number) 的展开为 "The value of " "number" " is %d\n"。在 C语言中,相邻的字符串会被自动连接成一个完整的字符串。因此,"The value of " "number" " is %d\n" 会被自动连接成 "The value of number is %d\n"。因此,程序可以正常编译并输出结果,运行结果为:
The value of " #varname 123如果在宏函数中没有使用 #,则 FMT(number) 展开为 "The value of " number " is %d\n"。
由于两个字符串之间出现了一个变量 number,不符合 printf() 第一个参数需要字符串的写法,因此无法编译通过。
2) ##(双井号)
## 运算符可以将两个宏参数连接起来以形成一个新的标识符,其语法格式为:参数1##参数2例如,我们想要使用宏函数来表示如下的两组变量名。这两组变量名是有一定规律的,前缀为 group1 或 group2,后缀为 Apple 或 Orange。
// 第一组变量,group1 int group1Apple = 1, group1Orange = 2; // 第二组变量,group2 int group2Apple = 100, group2Orange = 200;因此,我们可以使用宏函数来组合前缀与后缀,让它们成为一个完整的变量名。
#define VARNAME(group, name) group ## name在这个例子中,宏函数 VARNAME 将其两个参数连接起来形成一个新的标识符。例如:
- VARNAME(group1, Apple) 展开为 group1Apple;
- VARNAME(group1, Orange) 展开为 group1Orange;
- VARNAME(group2, Apple) 展开为 group2Apple;
- VARNAME(group2, Orange) 展开为 group2Orange。
如果不使用 ##,则会发生什么?
#define VARNAME(group, name) group name
- VARNAME(group1, Apple) 展开为 group1 Apple;
- VARNAME(group1, Orange)展开为 group1 Orange;
- VARNAME(group2, Apple) 展开为 group2 Apple;
- VARNAME(group2, Orange) 展开为 group2 Orange。
不使用 ##,展开后的两个参数之间留有空格,无法正常使用。那如果删除替换体中的空格呢?
#define VARNAME(group, name) groupname现在,宏函数出现了问题,它具有两个参数:group 和 name。但是,替换体中没有与这两个参数对应的记号。因此,## 的存在是有意义的。
3) 用法示例
下面程序是一个完整的使用宏函数运算符的示例:#include <stdio.h> #define FMT(group, name) "The value of " #group #name " is %d\n" #define VARNAME(group, name) group ## name int main() { // 第一组变量,group1 int group1Apple = 1, group1Orange = 2; // 第二组变量,group2 int group2Apple = 100, group2Orange = 200; // 使用第一组 printf(FMT(group1, Apple), VARNAME(group1, Apple)); printf(FMT(group1, Orange), VARNAME(group1, Orange)); // 使用第二组 printf(FMT(group2, Apple), VARNAME(group2, Apple)); printf(FMT(group2, Orange), VARNAME(group2, Orange)); return 0; }运行结果为:
The value of group1Apple is 1
The value of group1Orange is 2
The value of group2Apple is 100
The value of group2Orange is 200
- FMT(group1, Apple) 展开为 "变量"、"group1"、"Apple"、"的值是 %d\n" 这 4 个相邻的字符串,而相邻的字符串会被自动连接成一个完整的字符串。
- VARNAME(group1, Apple) 展开为 group1Apple,变为其中一个变量名。
其余的类似上面的处理。代码预处理之后,将会变为如下所示:
#include <stdio.h> int main() { // 第一组变量,group1 int group1Apple = 1, group1Orange = 2; // 第二组变量,group2 int group2Apple = 100, group2Orange = 200; // 使用第一组 printf("The value of \"group1\" \"Apple\" \" is %d\n", group1Apple); printf("The value of \"group1\" \"Orange\" \" is %d\n", group1Orange); // 使用第二组 printf("The value of \"group2\" \"Apple\" \" is %d\n", group2Apple); printf("The value of \"group2\" \"Orange\" \" is %d\n", group2Orange); return 0; }
C语言取消宏定义
当我们定义了一个宏后,我们如果需要更改宏的定义,那么可以重新定义它吗?例如下面实例,在将宏定义为 100 之后,又尝试将其重新定义为 101:#include <stdio.h> #define NUM 100 #define NUM 101 int main() { printf("%d\n", NUM); return 0; }在 Visual Studio 中,重复定义宏并不会导致编译报错,但是它会抛出一个警告,如下图所示:

图 7 宏重定义
因此,更合适的做法是使用预处理指令 #undef 取消这个宏的定义,然后重新定义它。具体代码如下:
#include <stdio.h> #define NUM 100 // 取消宏定义NUM #undef NUM // 重新定义宏NUM为101 #define NUM 101 int main() { printf("%d\n", NUM); return 0; }