前言
在c语言中,预处理(预编译)是整个程序流程中很重要的一个环节。本篇文章我们主要介绍一些关于c语言中预处理的相关知识以及指令。
一、预定义符号
c语言当中设置了几个预定义符号,可以直接使用:
__FILE__ //进行编译的源文件 __LINE__ //文件当前的行号 __DATE__ //⽂文件被编译的日期 __TIME__ //文件被编译的时间 __STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义
举个例子:
运行结果:
二、#define定义常量
接下来,我们着重介绍一下define定义常量的相关知识。它的基本语法是:
#define name stuff
举例:
在我们定义了define常量后,其他地方使用的这些表示符在编译前就会自动文本替换成后面的语句。
三、#define定义宏
define的定义不仅可以是常量的形式,它还运行将参数传入进行替换。这种定义被称作宏。它的语法格式是:
#define name( parament-list ) stuff
其中,name是宏名,括号中的内容是宏参数(一个或多个(由逗号隔开的)符号表),这些符号可以出现在之后的stuff中。举个例子:
int main() { int a = 3; int b = 5; printf("%d\n", add(a, b)); return 0; }
运行结果:
可以看出,宏就像函数一样,传入参数并且进行计算。
不过,由于宏是完全的文本替换,所以它是有副作用的。举个例子:
int main() { int a = 3; int b = 5; printf("%d\n", 3 * add(a, b)); return 0; }
运行结果:
在以上程序中,我们要计算3 *(a+b)的值,预期应是24,但是实际结果却是14.为什么呢?这就是文本替换的问题。我们的宏替换到主函数当中就是这样的:
printf("%d\n", 3 * a + b);
所以它的运行结果就是14。为了避免这样的情况发生,我们就需要加上括号:
int main() { int a = 3; int b = 5; printf("%d\n", 3 * add(a, b)); return 0; }
运行结果:
所以我们得出一个结论:使用define定义宏时:一定不要吝啬于加括号。一般将每个参数都用括号括起来,然后整体再加括号。
接下来我们介绍一下宏替换的规则:
1.在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果有,它们首先被替换。
2.替换的文本随后被插入到程序中原来文本的位置。对于宏,参数名被值所替换。
3.最后,对替换后的文本进行扫描,看看是否包含任何由#define定义的符号。如果有,重复上述过程。
这里需要注意两点:
1.宏参数和#define的定义中可以出现其他#define定义的符号。但是不能出现自己定义的符号(不能递归)
2.当预处理器搜索#define定义的符号时,字符串常量不会被搜索。
四、宏与函数的对比
从刚才的例子我们看到,宏的功能和函数十分相似,但是两者之间各有优劣。
宏的优势:
1.由于函数在调用和返回时需要消耗更多时间,所以宏在程序的运行速度方面更胜一筹。
2.函数的参数必须有特定的类型,只能在特定的表达式中使用。而宏的参数没有类型的要求。
函数的优势:
1.如果宏的内容较长并且多次使用时,替换到程序中会大幅度增加程序的长度。而函数的定义只在程序中出现一次。
2.宏无法调试,函数可以调试。
3.由于函数有类型检查,所以严谨性要高于宏。
所以,当内容较短,类型易错率低时,可以考虑使用宏;反之可以选择使用函数。另外,对于有些功能,宏是可以实现的,但是函数绝对无法实现。举个例子:
int main() { int* arr = MALLOC(int,10); //替换之后的内容: int* arr = (int*)malloc(10 * sizeof(int)); return 0; }
这个宏可以根据我们传入的数据类型来申请内存空间。而函数是无法把数据类型本身当作参数的。
另外,在我们定义宏或者函数时,一般将宏名全部大写,函数名不完全大写。
五、#操作符和##操作符
接下来我们介绍两个在宏定义中使用的操作符:#操作符和##操作符。
1.#操作符
#操作符的作用是将它之后的参数转换为一个字符串常量。举个例子:
int main() { int a = 1; float b = 5.5f; char c = 'w'; PRINT("%d", a); PRINT("%f", b); PRINT("%c", c); return 0; }
运行结果:
可以看到,#操作符能够实现根据不同的变量名将其转换成字符串打印出来。
2.##操作符
##操作符又被称为记号粘合操作符,它可以将我们传入的宏参数与其他字符粘合起来,成为一个合法的标识符。
比如我们现在想要实现求两个变量的最大值。为了提高复用率,你很快会想到:定义一个函数。像这样:
int max(int a, int b) { return a > b ? a : b; }
但是,如果要针对多种数据类型,就需要重复写好几遍代码,太繁琐了。而宏定义配合##操作符就可以让我们一劳永逸。比如:
// "\"是续行符,使宏定义可以连续到下一行,提高代码可读性 MAX(int) MAX(float) int main() { printf("%d\n", int_max(1, 2));//“int” 与 “_max”粘合成为一个标识符 printf("%f\n", float_max(1.4, 5.2));//“float” 与 “_max”粘合成为一个标识符 }
运行结果:
有了这样的模板,我们只需要简短的几个字符就可以创建一个函数,十分方便。而这里的##操作符就通过传入的类型产生了不同的函数名。
六、#undef
#undef用于撤销一个宏定义。例如:
七、条件编译
条件编译会让包含的指令或者代码在满足条件时参与编译,否则不编译。常见的条件编译指令:
//1. //... //常量表达式由预处理器求值。 如: //.. //2.多个分支的条件编译 //... //... //... //3.判断是否被定义
注意:这里的编译条件都与预处理有关,不能将程序代码中的变量或者某些情况当作编译条件。因为,这些编译条件都是在程序执行前判断的。
八、头文件的包含
在c语言中,头文件的包含有两种方式:#include <...> 和 #include "..."。那么这两种方式有什么区别呢?
#include "..." 用于本地文件的包含。它的查找策略是:先在源文件目录下查找,如果未找到,则在标准库位置查找。如果未找到,则会报错。
#include <...> 用于库文件的包含。它的查找策略是:直接在标准库位置查找。如果查找不到,则报错。
既然两种方式都会在标准库位置查找,为什么不直接用 “ ” 的方式来包含呢?由于 “ ” 的包含方式会先在源文件目录下查找,如果包含库文件,则会牺牲一些效率,减慢程序运行速度。
总结
今天我们探讨了关于预处理的一些知识以及指令的使用技巧。到这里,c语言的内容就告一段落了,接下来博主将会开启一个新的篇章:数据结构。希望能够和大家一起探讨数据结构相关的知识。如果你觉得博主讲的还不错,就请留下一个小小的赞在走哦,感谢大家的支持❤❤❤