C语言#define
定义define时,建议不要再后面加;号
例如:分别尝试使用以下代码段中,带;定义的MAX和不带;的MAX替代main函数中的宏。
#define Max 1000
#define MAX 1000;
int main()
{
int a = MAX;
printf("%d\n", MAX);
}
-
使用#define Max 1000替换后的代码段
int main() { int a = 1000; printf("%d\n", 1000); }
-
使用#define MAX 1000;替换后的代码段
int main() { int a = 1000;; // 出现了双分号,两个分号之间是一条空语句 printf("%d\n", 1000;); // 语法错误 }
总结:通过以上2次替换可以看出,如果#define带分号,很有可能导致语法错误,建议定义时不要加上;号。
define定义宏
#define定义了一种规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或者定义宏(define macro)。
语法:#define name(parament-list) stuff
解释:
name
为宏名parament-list
是一个由逗号隔开的符号表,它们可能出现在stuff
中。- 补充:
parament-list
的左括号必须与name紧邻。如果两者有任何空白存在,参数列表都会被解释为stuff的一部分
定义带参数的宏时,要关注参数可能是表达式的场景
所有慵与表达式进行求值的宏定义都应该使用这种方式加上括号,避免在使用宏时由于参数中的操作符或者临近操作符之间的相互作用。
-
例如:思考以下代码段的输出结果
#define ADD(X) X*X int main() { printf("%d\n", 10 * ADD(1+2)); return 0; }
答案:输出结果为14
解析:核心原因在于,
ADD(X)
并不会把parament-list中的数据计算后再替换,而且直接将表达式进行宏替换,替换后的代码如下所示。#define ADD(X) X*X int main() { printf("%d\n", 10 *1+2*1+2); return 0; }
迭代:如果想实现预期的表达式含义,让X*X,应该将
#define ADD(X) X*X
修改为#define ADD(X) ((X)*(X))
,通过括号来限制运算的优先级。#define ADD(X) ((X)*(X)) int main() { printf("%d\n", 10 * ADD(1+2)); // printf("%d\n", 10 *((1+2)*(1+2))); 宏替换后的结果 return 0; }
宏替换的步骤
在程序扩展#define定义符号和宏时,需要涉及几个步骤。
- 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。
- 替换文本随后被插入到原来的文本的位置。对于宏,参数名被它们的值替换。
- 最后,再次对结果文件进行扫描,看看他是否包含由#defin定义的符号。如果是,就重复上述执行步骤。
例如:请思考以下代码段的替换过程
#define MAX 100
#define DOUBLE(X) ((X)*X))
int main()
{
int a = 10 * DOUBLE(MAX + MAX);
return 0;
}
解释:
- 在调用DOUBLE宏时,检查到它的参数中包含了#define定义的符号MAX,所以MAX需要先被替换。
- 替换后的文本会被插入到原来的文本位置,变成
int a = 10 * DOUBLE(100 + 100);
- 再次对结果文件扫描,又检测到了DOUBLE宏,此时它的参数中的宏都已经被替换,所以对DOUBLE进行替换,变成
int a = 10 * ((100 + 100)*(100 +100)));
注意:
- 宏参数和#define定义中可以出现其他#define定义的变量。但是对于宏,不能出现递归。
- 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。
将宏插入字符串中(#)
思考:在以下代码段中,每定义一个新的变量,printrf函数中的字符串都需要修改(例如新定义一个变量c,字符串的信息就需要更新为"变量c的值为:"),有没有一种方式可以定义一个通用的printf函数,可以不用频繁的人为修改字符串哪。
int main()
{
int a = 10;
int b = 20;
printrf("变量a的值为:",a);
printrf("变量b的值为:",a);
return 0;
}
答案:可以使用将宏插入字符串的方式解决以该问题。
解释:在宏定义中,
#parament-list
表示将宏的参数表达式列表以字符串的格式插入到宏体中。案例:
#define PRINT(X) printrf("变量" #X "的值为:",X); int main() { int a = 10; int b = 20; PRINT(a); // printrf("变量" "a" "的值为:",a); 宏替换后结果 PRINT(b); // printrf("变量" "b" "的值为:",b); 宏替换后结果 return 0; }
宏合并(##)
##可以把位于它两边的符合合成一个符号,它允许宏定义从分离的文本片段创建标识符。
案例:直接来看看代码吧。
#denfine TEST(X,Y) X##Y
int main()
{
int Test84= 2024;
printf("%d\n", TEST(Test, 84));
// printf("%d\n", TEST(Test84)); 宏替换后结果
return 0;
}
解释:宏体中的
X##Y
,会将X和Y认为是一个整体进行拼接。
带有副作用的宏参数
带有副作用的宏参数指执行过一次后,其原值可能发生变化的参数,例如:
X + 1
执行后X的值不会发生变化,不带副作用
X++
执行后,X的值会发生永久性变化,带副作用当宏参数在宏的定义中超过一次的时候,如果参数带有副作用,那么在使用这个宏的时候就可能出现不可预测的后果。
思考以下代码段的输出结果
#define MAX(X, Y) ((X) > (Y)?(X):(Y))
int main()
{
int a = 10;
int b = 20;
int max = MAX(a++, b++);
printf("%d\n", max); // 输出结果:12
printf("%d\n", a); // 输出结果:11
printf("%d\n", b); // 输出结果:13
}
解释:对
int max = MAX(a++, b++);
进行宏替换后,结果为int max = (a++) > (b++)?(a++):(b++);
,所有的宏都展开后,才会进行计算。显而易见,对宏传递带有副作用的参数,代码可读性非常差,其结果是难以预测的。总结:在使用宏的时候,要尽量避免传递使用带副作用的参数
宏和函数的对比
宏通常被应用于执行简单的运算。比如在两个数据中找出更大的一个。
-
使用宏:
#define MAX(a, b) ((a) > (b)? (a):(b))
-
使用函数:
int compare(int a,int b) { return a>b?a:b; }
在这种场景下使用宏比使用函数更具有以下优势:
- 如果使用函数,必须为函数参数和返回值指定类型。如果是比较2个double类型的数据,就又需要定义一个新的函数。而宏可以对任意类型进行>运算,宏是类型无关的。
- 函数的调是由开销的,例如在main函数中调用compare函数会涉及到原函数的信息压栈保存,地址跳转,执行结束后又恢复原函数的运行环境等一系列动作。而宏属于单纯的替换,最终的结果是在同一个函数中执行的,所以宏比函数在程序的规模和速度方面更具有优势。
当然宏比函数也有劣势;
- 每次预编译宏的时候,都会有一份宏定义的代码插入到程序中。会大幅增加程序的长度。
- 宏没有变法Debug,出现问题难以定位
- 宏由于类型无关,不够严谨,代码运行更容易出现问题。
- 宏可能带来运算符优先级的问题,导致程序容易出现错误。
宏具备函数做不到的能力
-
宏的参数列表可以传数据类型,而函数不可以,例如如下代码函数无法做到:
#define MALLOC(num, type) (type* )malloc(num * sizeof(tpye)) int main() { int* p = MALLOC(10,int); double* p2 = MALLOC(20,double); return 0; }
移除一个宏
使用
#undef NAME
可以移除一个宏
#define MAX 100
int main()
{
printf("%d\n", MAX);
#undef MAX
printf("%d\n", MAX); // 这行代码会报错,因为MAX宏被移除了,编译器找不到这个宏
}
条件编译
在编译一个程序的时候,我们如果要将一条语句(一组语句)编译或者放弃是很方便的,因为我们有条件编译指令。
比如说:调试性的代码、删除可惜、保留又碍事,所以我们可以选性的编译。
案例:
#define DEBUG
int main()
{
#ifdef DEBUG
printf("Hello World");
#endif
return 0;
}
解释:在上述代码段中,会判断是否定义了DEBUG
,如果定义了就执行#ifdef
和#endif
之间的代码段,如果没有定义就不执行。
常见的条件编译指令:
-
常量表达式条件编译
语法:
#if 常量表达式 // ..... #endif
示例:
#define DEBUG 1-1 #if DEBUG // ..... #endif
-
多分支条件编译
语法:
#if 常量表达式 // ... #elif 常量表达式 // ... #else // ... #endif
示例:
#define DEBUG 1 #edfine MYDEBUG 2 #if DEBUG // ... #elif MYDEBUG // ... #else // ... #endif
-
判断是否被定义
语法:
#if defined(symbol) // 定义了symbol宏就执行,否则就不执行 #ifdef symbol // 同上,两种不同写法 #if !defined(symbol)// 没有定义symbol宏就执行,否则就不执行 #ifndef symbol // 同上,两种不同写法
-
嵌套指令
语法:
#if defined(OS_UNIX) #ifdef defined OPTION1 unix_version_option1(); #endif #ifdef OPTION2 untx_version_option2(); #endif #eldef defined(OS_MSDOS) #ifdef OPTION2 msos_version_option2(); #endif #endif
#define解决头文件重复引入问题
思考以下代码段,当test.h头文件被多次引入时,会发生什么问题。
// test.h文件内容
int ADD(int a, int b);
// 在test.c中多次引入test.h头文件
#include "test.h"
#include "test.h"
#include "test.h"
int main()
{
return 0;
}
// 在test.c文件的展开结果如下
int ADD(int a, int b);
int ADD(int a, int b);
int ADD(int a, int b);
int main()
{
return 0;
}
可以看到int ADD(int a, int b);
函数重复声明,如何解决这一问题哪?
-
可以将test.h文件改造
// test.h文件内容 #ifndef _TEST_ #define TEST int ADD(int a, int b); #endif
-
根据改造后的test.h,再次解析test.c文件
// 在test.c文件的展开结果如下 #ifndef _TEST_ #define TEST int ADD(int a, int b); #endif #ifndef _TEST_ #define TEST int ADD(int a, int b); #endif #ifndef _TEST_ #define TEST int ADD(int a, int b); #endif int main() { return 0; }
补充:在头文件首部使用
#pragma once
也可以避免头文件被重复引入