预处理
1. 预定义符号
__FIEL__ //进行编译的源文件 __LINE__ //文件当前的行号 __DATE__ //文件被编译的日期 __TIME__ //文件被编译的时间 __STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义
例如在VS 2019上:
#include<stdio.h> int main() { printf("%s\n", __FILE__); printf("%d\n", __LINE__); printf("%s\n", __DATE__); printf("%s\n", __TIME__); return 0; }
output:
E:\VS_Code\比特作业\Work_7.20\Work_7.20\Work_7.20.c 67 Jul 20 2023 11:10:52
而如果尝试打印_STDC_,则编译器会报错:
说明BVS2019未遵循ANSI C
2. 宏的命名方式
一般来讲函数和宏的使用语法很相似。所以语言本身没法帮我们区分二者
那我们平时的一个习惯是:
把宏名全部大写
函数名不要全部大写
3. #define
3.1 #define定义标识符
语法:
#define name stuff
这不是C语句
name
是名字
stuff
是内容,是一段字符串实现的是简单替换,系统不会对其进行任何处理
例如:
#include<stdio.h> #define N 50 //数字 #define STR "abcdef" //字符串 #define FOR for(;;) //代码段 int main() { printf("%s\n", STR); FOR { printf("%d\n",N); } return 0; }
output:
abcdef 50 50 50 50 ………… //死循环
注:#define
后面做好不要加;
例如:
#include<stdio.h> #define N 50; int main() { int num = N + 10; printf("%d\n", num); return 0; }
output:
50
是不是和预想的结果不一样?其实int num = N + 10;
这段代码可以改成这样:int num = 50; + 10;
可以看到int num = 50;
是一个独立的语句,所以num被赋予的值为50而不是60
因此,如果#define
后面加了;
,可能得不到想要的结果,甚至程序可能会报错
3.2 #define定义宏
#define
机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义宏(define macro)
下面是宏的声明方式:
#define name(parament-list) stuff
其中parament0list
是一个由逗号隔开的符号表,它们可能出现在stuff中
注意:
参数列表的左括号必须与
name
紧邻。如果两者之间有任何空白符,参数列表就会被解释为
stuff
的一部分
举个例子:
#include<stdio.h> #define MAX(x,y) (x > y) ? x : y //MAX和括号之间不能有空格 int main() { int a = 1; int b = 2; int c = MAX(a, b); printf("c = %d\n", c); return 0; }
这里的语句c = MAX(a, b)
在预处理之后,就变成了:c = (a > b) ? a : b
output:
c = 2
再来看一个例子:
#include<stdio.h> #define POWER(x) x * x int main() { int a = 10; int c = POWER(a + 10); printf("c = %d\n", c); return 0; }
output:
c = 120
可能有许多小伙伴会认为输出结果应该是c = 400
,但我们必须要牢记:宏实现的是简单替换,系统不会对其进行任何处理,因此语句c = POWER(a + 10);
经过预处理后就变成这样了:c = a + 10 * a + 10
,得到的结果自然就是120。为了防止这种错误,我们有必要多加几个括号:
#define POWER(x) ((x) * (x))
注意:
**用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,**避免在使用宏时由于参数中的操作符或邻近操作符之间不可预料的相互作用
3.3 #define的替换规则
在程序中扩展#define定义符号和宏时,需要涉及几个步骤:
- 在调用宏时, 首先对参数进行检查, 看看是否包含任何由#define定义的符号。如果是, 它们首先被替换。
- 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。
- 最后, 再次对结果文件进行扫描, 看看它是否包含任何由#define定义的符号。如果是, 就重复上述处理过程。
注意:
- 宏参数和#define定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。
- 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。
例如:
#define M 20 printf("M = %d",M); //字符串中的M并不会被搜索
3.4 # 和##
我们先来引入一个例子:
我们之前打印字符串Hello Word
是这样打印的:
printf("Hello Word");
其实也可以这样写:
printf("Hello""Word");
又比如下面两串代码:
printf("The val of a is %d\n",a); printf("The val of b is %d\n",b); printf("The val of c is %f\n",c);
这两串代码是极其类似的,不同点只是在于描述和打印的分别是a,b。这是有同学就会问,既然功能相似,那我们能不能封装成一个函数呢?答案是不能(做不到),因为我们通过函数参数无法改变printf
里面字符串里面的类容
但是,宏定义#define
可以做到
那么我们如何把参数插入到字符串中呢?
我们可以使用#
把一个宏参数变成对应的字符串
例如:
如果宏参数为n
,那么#n
就等价于“n”
对于上面的三串代码,如果我们想要用一个宏来实现,我们就可以这样写:
#define PRINT(n,format) printf("the val of "#n" is "format"\n",n) int main() { int a = 1; int b = 2; float c = 3.4f; PRINT(a, "%d"); PRINT(b, "%d"); PRINT(c, "%f"); return 0; }
output:
the val of a is 1 the val of b is 2 the val of c is 3.400000
还有一个操作符为##
##的作用:
##
可以把位于它两边的符号合称为一个符号它允许宏定义从分离的文本片段创建标识符
例如:
#define CAT(a,b) a##b int main() { int num_1 = 2023; int num_2 = CAT(20, 23); int num_3 = CAT(num, _1); printf("%d\n", num_3); if (num_1 == num_2) printf("YES\n"); return 0; }
output:
2023 YES
注:合成的符号必须是有效合法的,否则就是未定义的
3.5 带副作用的宏参数
副作用:在计算机编程中,副作用(Side effect)是指函数或表达式在执行过程中对除了返回值以外的其他状态或变量进行了修改或操作的现象。
例如:
int a = 10; int b = a + 1; //在赋予b值都过程中,a的值未发生改变,没有副作用 int c = a++; //在赋予c值都过程中,a的值发生改变,有副作用
当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么在使用这个宏的时候就可能出现危险,导致不可预测的后果。
例如:
#define MAX(x,y) (x > y) ? x : y int main() { int a = 1; int b = 2; int c = MAX(a++, ++b); printf("a = %d\nb = %d\nc = %d\n", a, b, c); return 0; }
output:
a = 2 b = 4 c = 4
是不是和预想的结果不一样?这就是由副作用引起的: c = MAX(a++, ++b)
经过预处理后变成了c = (a++ > ++b) ? a++ : ++b
,a++ 和 ++b
比较时,a的值为1,b的值为3,显然执行后面的++b
语句,所以最后c被赋予的值就是最后b的值,为4
由此我们可以看到,如果宏的参数带有副作用,那么就会非常危险,应该尽量避免使用
3.6 宏和函数的比较
宏的优点:
- 性能优势: **宏在预处理阶段进行简单的文本替换,没有函数调用的开销,**因此在某些情况下可能比函数更快。对于一些短小的、频繁调用的代码片段,使用宏可以提高程序的执行效率。
- 无函数调用堆栈开销: 宏不涉及函数调用的堆栈操作,因此在嵌入式系统或对性能要求很高的场景中,可以减少栈空间的使用。
- 灵活的参数:宏的参数可以是任意表达式,包括副作用表达式。这使得宏能够以更灵活的方式进行代码替换,可以处理一些复杂的操作。
- 避免函数调用副作用: 宏的参数在替换时是简单的文本替换,不会导致函数调用带来的副作用。这对于一些需要确保参数不会被重复计算的情况很有用。
宏的缺点:
- 可读性和调试难度:宏通常会导致代码膨胀,因为宏会简单地进行文本替换,可能会导致代码变得晦涩难懂。对于宏展开后的代码,很难进行调试,因为调试器无法显示宏展开后的代码。
- 不进行类型检查: 宏在进行文本替换时**不进行类型检查,这可能导致一些潜在的类型错误。**因此,在宏中应该谨慎处理参数,避免可能的类型不匹配问题。
- 没有作用域: 宏在预处理阶段进行文本替换,没有作用域的概念。如果宏的名称与其他标识符冲突,可能会导致错误或意外行为。
- 不可随意调试: 在调试过程中,很难对宏展开后的代码进行单步调试,因为宏展开发生在预处理阶段,调试器无法直接查看宏的展开结果。
- 难以维护: 宏的复杂定义可能导致代码难以维护。如果一个宏出现错误或需要修改,那么可能需要查找所有宏的实例并手动进行修改。
综上所述,宏和函数在使用上有各自的优势和限制。宏适合进行简单的代码替换和性能敏感的场景,但需要谨慎使用以避免潜在的问题。函数则提供了更好的封装、可读性和可维护性,适用于较复杂的任务和需要类型检查的情况。
4. #undef
这条指令用于移除一个宏定义
#undef name //如果现存的一个名字需要重新定义,那么它的旧名字就先要被移除
5. 命令行定义(了解即可)
许多C 的编译器提供了一种能力,允许在命令行中定义符号。用于启动编译过程。
例如:当我们根据同一个源文件要编译出一个程序的不同版本的时候, 这个特性有点用处。 (假定某个程序中声明了一个某个长度的数组,如果机器内存有限, 我们需要一个很小的数组, 但是另外一个机器内存大些,我们需要一个数组能够大些。)
6. 条件编译
在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者是放弃是很方便的。因为我们有条件编译指令。(满足条件就编译,不满足条件就不编译)
常见的条件编译指令:
6.1
# if 常量表达式 ///.... # endif //常量表达式由预处理器求值
如:
#include<stdio.h> int main() { #if 1 printf("Hello World\n"); #endif return 0; }
output:
Hello World\n
需要特别注意,这和C语言if
语句是有本质上的区别的:
我们来看下面的代码:
#include<stdio.h> int main() { for (int a = 1; a < 10; a++) { #if (a >5) printf("Hello World\n"); #endif } for (int a = 1; a < 10; a++) { if(a > 5) printf("Hello World\n"); } return 0; }
大家认为一共会打印多少个Hello World
呢?
output:
Hello World Hello World Hello World Hello World
只打印了4个Hello Word
,也就是说,只有后面的for循环中打印了这个字符串,前面的for循环没有打印。这时有小伙伴就会问了,为什么a > 5了,也不能打印呢?这是因为一开始a的值为1,小于5,不满足条件编译指令的条件,因此在预处理阶段,系统就会将这段printf语句删除,之后就算条件为真,也不能执行。
6.2
多个分支的条件编译
# if 常量表达式 //… # elif 常量表达式 //. # else //… # endif
例如
6.3
判断是否被定义
//如果被定义,就执行 #if defined ( symbol ) #endif #ifdef symbol #endif //如果未被定义,就执行 #if !defined ( symbol ) #endif #ifndef symbol #endif
例如:
#define M 1 int main() { #if defined M printf("M\n"); #endif #ifdef M printf("M\n"); #endif #ifndef NN printf("NN\n"); #endif #if !defined NN printf("NN\n"); #endif return 0; }
output:
M M NN NN
再来看一个例子:
int main() { int m; #ifdef m printf("m\n"); #endif return 0; }
大家是不是会认为输出的是m
,然而事实是什么也不会输出。
注意:
**预处理阶段只会处理带#
的指令,而不会看其他的代码,因此在预处理阶段局部变量m
还不会被定义,因此printf语句就会被删除,**故不会打印任何值。
7. 文件包含
7.1 头文件被包含的方式
本地文件包含:
#include "filename"
查找策略:现在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件;如果找不到就提示编译错误
库文件包含:
#incldue <filename.h>
查找头文件直接去标准路径下去查找,如果找不到就提示编译错误
7.2 嵌套文件的包含
如果出现这样的代码:
#include "test.h" #include "test.h" #include "test.h" #include "test.h" int main() { return ; }
头文件被多次重复调用,将大大降低程序的运行效率和可读性
而为了避免这一情况,我们就可以使用条件编译来解决:
每个头文件这样写:
#ifndef __TEST_H__ #define __TEST_H__ //头文件内容 #endif //__TEST_H__可以是其他名字
也可以:
#pragma once //头文件内容