C预处理详解1
前面我们学习完 编译与链接 部分,接下来详细学习编译中的预处理部分
一、预定义符号
C语言设置了⼀些预定义符号,可以直接使用,预定义符号也是在预处理期间处理的。
__FILE__ //进行编译的源文件
__LINE__ //文件当前的行号
__DATE__ //文件被编译的日期
__TIME__ //文件被编译的时间
__STDC__ //如果编译器遵循 ANSI C ,其值为1,否则未定义
这些预定义符号都是语言内置的
例如:
int main()
{printf("%s\n",__FILE__);printf("%d\n",__LINE__);printf("%s\n",__DATE__);printf("%s\n",__TIME__);return 0;
}
我们可以看到VS2022编译器不遵循 ANSI C:
printf("%d\n",__STDC__);
其实还有一个预定义符号,它的作用是可以知道当前的函数名:
__FUNCTION__ //获取当前函数名称
我们也可以通过 VS code(遵循 ANSI C) 来观察,依然输入预处理命令得到:
二、#define
2.1 #define 定义标识符
语法:
#define name stuff
举例:
#define MAX 1000
#define reg register //为register这个关键字,创建一个简短的名字
#define do_forever for(;;) //用更形象的符号来替换一种实现
#define CASE break;case //在写case语句的时候自动把break写上
// 如果定义的 stuff过⻓,可以分成几行写,除了最后一行外,每⾏的后⾯都加⼀个反斜杠(续行
符)。
#define DEBUG_PRINT printf("file:%s\tline:%d\t \date:%s\ttime:%s\n" ,\__FILE__,__LINE__ , \__DATE__,__TIME__ )
#define do_forever for(;;)
int main()
{do_forever;//for(;;)return 0;
}#define CASE break;case
int main()
{int n = 0;switch (n){case 1:CASE 2://break;case 2:CASE 3://break;case 3:CASE 4://break;case 4:}return 0;
}
思考:在define定义标识符的时候,要不要再后面加上 ; ?
比如:
#define MAX 1000;
#define MAX 1000
建议不要加上 ;,这样容易导致问题,比如下面场景:
#define MAX 1000;int main()
{int m = 0;if (m >= 0)m = MAX;//m=1000;;elsem = -1;printf("%d\n", m);return 0;
}
如果是加了分号的情况,等替换后,if和else之间就是2条语句,而没有大括号的时候,if后边只能有⼀ 条语句。这⾥会出现语法错误。
2.2 #define 定义宏
#define 机制包括了⼀个规定,允许把参数替换到文本中,这种实现通常称为宏或定义宏
下面是宏的申明方式:
#define name( parament-list ) stuff
【#define 宏名(参数列表) 替换文本】
其中的 parament-list 是⼀个由逗号隔开的符号表,它们可能出现在stuff中。
注意:参数列表的左括号必须与 name 紧邻,如果两者之间有任何空白存在,参数列表就会被解释为 stuff 的 ⼀部分。
举例:
#define SQUARE(x) x*x
int main()
{printf("%d\n",SQUARE(5));//printf("%d\n",5*5);printf("%lf\n",SQUARE(5.0));//printf("%d\n",5.0*5.0);return 0;
}
这个宏接收⼀个参数 x .如果在上述声明之后,你把 SQUARE( 5 ); 或 SQUARE( 5 ); 置于程序中,预处理器就会用这个表达式替换上面的表达式: 5 * 5 或 5.0 * 5.0
警告:这个宏存在一个问题:观察下面的代码:
#define SQUARE(x) x*x
int main()
{printf("%d\n",SQUARE(5+1));return 0;
}
我们想要的结果是6*6=36,而实际上宏接收这个参数后的结果是11(原因:5+1*5+1=11),宏并不会像函数那样将参数计算好后再进行传参,而是直接原封不动的传过去(即替换文本时,参数x被替换成5+1,而不是6)
这样就比较清晰了,由替换产生的表达式并没有按照预想的次序进行求值
如何解决这个问题呢?在宏定义上加上两个括号,这样预处理之后就产生了预期的结果
#define SQUARE(x) (x)*(x)
但是,这样的宏还有一个问题,观察以下代码:定义中我们使用了括号,想避免之前的问题,但是这个宏可能会出现新的错误。
#define DOUBLE(X) (X)+(X)
int main()
{printf("%d\n", 10 * DOUBLE(3));return 0;
}
我们想要的结果是10*(3+3)=60,而实际上宏接受这个参数后的结果是33(原因:10*(3)+(3)=33)
乘法运算先于宏定义的加法,所以出现了 33
这个问题的解决办法是在宏定义表达式两边再加上⼀对括号就可以了。
#define DOUBLE(x) ((x)+(x))
总结:所以用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时由于参数中的操作符或邻近操作符之间不可预料的相互作用。
2.3 带有副作用的宏参数
当宏参数在宏的定义中出现超过⼀次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能出现危险,导致不可预测的后果。副作用就是表达式求值的时候出现的永久性效果。
例如:
x + 1; //不带副作用
x++; //带副作用
观察以下代码,MAX宏可以证明具有副作用的参数所引起的问题:
#define MAX(x,y) ((x)>(y)?(x):(y))
int main()
{int x = 5;int y = 8;int z = MAX(x++,y++);printf("x=%d y=%d z=%d\n", x, y, z);return 0;
}
我们期待输出的结果是 6 9 8 【z = max(5, 8) = 8,然后 x 和 y 各自增一次(x=6, y=9)】
而实际上我们知道预处理器处理之后的结果是6 10 9:
z = ((x++) > (y++) ? (x++) : (y++))
执行步骤:
1.求值比较 (x++) > (y++):
x++ 返回当前值 5,然后 x 自增为 6;y++ 返回当前值 8,然后 y 自增为 9。
比较 5 > 8 为假
2.由于条件为假,执行 (y++):
y 当前值为 9,返回 9,然后 y 自增为 10。
3.赋值 z = 9
而如果MAX是函数时:
int MAX(int x,int y)
{return x > y ? x : y;
}
int main()
{int x = 5;int y = 8;int z = MAX(x++,y++);printf("x=%d y=%d z=%d\n", x, y, z);return 0;
}
则输出的结果就是 6 9 8(z = MAX(5,8) = 8,x,y自增一次)
2.4 宏替换的规则
在程序中扩展#define定义符号和宏时,需要涉及几个步骤:
1. 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。
2. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。
3. 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。
注意:
1. 宏参数和 #define 定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。
2. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。
例如:
在宏参数中出现其他#define定义的符号,会首先被替换(MAX(100,3)),然后再继续进行:
#define MAX(x, y) ((x) > (y) ? (x) : (y))
#define M 100
int main()
{int m = MAX(M, 3);printf("%d\n", m);return 0;
}
预处理器在展开宏时,会忽略字符串常量(即双引号"括起来的文本)中的内容,不会将其视为宏符号进行替换:
比如:
#define NAME Alice
printf("My name is NAME"); // 输出: My name is NAME
此处字符串中的NAME不会被替换为Alice,输出结果仍是My name is NAME。
如果宏名称出现在字符串外(如代码标识符、数值等),则正常替换:
#define NAME Alice
printf("%s", NAME); // 输出: Alice
为什么这样设计?
1.保护字符串的原意:字符串常量的内容通常是直接显示的文本(如提示信息、格式字符串)。如果预处理器替换其中的内容,可能导致意外结果
2.避免歧义:若替换字符串内的宏,可能破坏代码逻辑
正确使用宏替换字符串内容:
若需要在字符串中插入宏的值,需将宏置于字符串外,通过拼接实现:
#define NAME "Alice"
printf("My name is " NAME); // 输出: My name is Alice
或使用运行时格式化:
#define NAME Alice
printf("My name is %s", NAME); // 输出: My name is Alice
2.5 宏和函数的对比
宏通常被应用于执行简单的运算。
比如在两个数中找出较大的⼀个时,写成下面的宏,更有优势⼀些。
#define MAX(a, b) ((a)>(b)?(a):(b))
那为什么不用函数来完成这个任务? 原因有二:
1. 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏比函数在程序的规模和速度方面更胜⼀筹。
2. 更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。反之,这个宏可以适用于整形、长整型、浮点型等类型。宏的参数是类型⽆关 的。
和函数相比宏的劣势:
1. 每次使用宏的时候,⼀份宏定义的代码将插⼊到程序中。除非宏比较短,否则可能⼤幅度增加程序的长度。
2. 宏是没法调试的。(宏的代码在预处理阶段就替换成宏的内容对应的代码了,所以调试的时候,执行的代码和你在源代码中看到的代码是不一致的,所以没办法调试)
3. 宏由于类型无关,也就不够严谨。
4. 宏可能会带来运算符优先级的问题,导致程容易出现错。
宏有时候可以做函数做不到的事情。比如:宏的参数可以出现类型,但是函数做不到
#define MALLOC(num,type) (typr*)malloc(num*sizeof(type))int main()
{//使用int* p1 = MALLOC(10,int);//预处理替换后的结果int* p1 = (int*)malloc(10*sizeof(int));return 0;
}
宏和函数的⼀个对比
2.6 # 和 ##
在学习之前,我们首先了解一下,在C语言中,相邻的字符串字面量(用双引号包围的字符串)会在编译阶段被自动连接成一个完整的字符串。
例如:
printf("hello world\n");
直接输出完整的字符串 "hello world\n"。
printf("hello " "world\n");
编译器会将相邻的两个字符串 "hello " 和 "world\n" 合并成 "hello world\n",因此实际输出与第一条语句完全相同。
2.6.1 #运算符
#运算符将宏的⼀个参数转换为字符串字面量。它仅允许出现在带参数的宏的替换列表中。
#运算符所执行的操作可以理解为”字符串化“。
例如:当我们想通过创建宏实现变量值的替换时:
#define PRINT(x) printf("the value of x is %d\n",x)int main()
{int a = 10;PRINT(a);int b = 20;PRINT(b);return 0;
}
而这样并没有达到预期的效果 —— 变量实现了替换,但是字符串内的变量名并没有随着被替换:
这时候 #运算符 就派上了用场:它可以将宏的一个参数字符串化(即将参数x变为"x")
#x 会被替换成 "a"
我们可以在#x的前后分别再添加一个 " ,使其变成三个相邻的字符串
#define PRINT(,x) printf("the value of "#x" is %d\n",x)int main()
{int a = 10;PRINT(a);int b = 20;PRINT(b);return 0;
}
不过变量的类型不只有整型类型,因此我们可以再增加一个宏参数 format,表示其变量的类型
#define PRINT(format,x) printf("the value of "#x" is "format"\n",x)
int main()
{int a = 10;PRINT("%d",a);//printf("the value of ""a"" is ""%d""\n",x);//printf("the value of a is %d\n", a);int b = 20;PRINT("%d",b);//printf("the value of ""b"" is ""%d""\n",x);//printf("the value of b is %d\n", b);float f = 3.14f;PRINT("%f",f);//printf("the value of ""f"" is ""%f""\n",x);return 0;
}
2.6.2 ##运算符
## 可以把位于它两边的符号合成⼀个符号,它允许宏定义从分离的文本片段创建标识符。 ## 被称 为记号粘合
这样的连接必须产生⼀个合法的标识符。否则其结果就是未定义的。
例如:
#define CAT(x,y) x##y
//Class##109
//Class109int main()
{int Class109 = 2025;printf("%d\n", CAT(Class, 109));return 0;
}
写⼀个函数求2个数的较大值的时候,不同的数据类型就得写不同的函数:
比如:
int int_max(int x, int y)
{return x>y?x:y;
}
float float_max(float x, float y)
{return x>yx:y;
}
这样写起来太繁琐了,我们可以用宏来计算,加上 ##运算符 来表示不同类型的变量:
#define GENERIC_MAX(type) \
type type##_max(type x,type y)\
{\return x>y?x:y;\
}GENERIC_MAX(int);//替换到宏体内后int##_max 生成了新的符号 int_max做函数名
GENERIC_MAX(float);//替换到宏体内后float##_max 生成了新的符号 float_max做函数名int main()
{//调用函数int m = int_max(2,3);printf("%d\n",m);float fm = float_max(3.5f, 4.5f);printf("%f\n", fm);return 0;
}
(这些都可以通过VS code gcc 仔细的观察到)
三、命名约定
⼀般来讲函数的宏的使用语法很相似。
所以语⾔本⾝没法帮我们区分⼆者。 那我们平时的⼀个习惯是:
把宏名全部大写
函数名不要全部大写(一般首字母大写)