C语言之预处理和宏
(注:本文所讲为C语言生一些基本语法,其中主体为C语言,部分中采用了c++的一些语法。)
一.预定义符号
以下是C语言核心预定义符号的其中5个:
1. __FILE__ :表示当前正在编译的源文件名称,是字符串类型,包含文件的完整路径 。常用于记录错误或调试信息,明确问题出现的文件。例如 printf("错误发生在文件:%s\n", __FILE__); 。
2. __LINE__ :代表当前源代码所在的行号,为整型。配合 __FILE__ 能精准定位代码位置,如 fprintf(stderr, "错误在文件 %s 的第 %d 行\n", __FILE__, __LINE__); 。
3. __DATE__ :字符串类型,格式为 "MMM DD YYYY" ,表示源文件被编译的日期 。可用于标记代码版本或记录编译时间相关信息。
4. __TIME__ :字符串类型,格式为 "HH:MM:SS" ,代表源文件被编译的具体时间 。
5. __STDC__ :整型,如果编译器遵循ANSI C标准,其值为1,否则未定义 。用于检测编译器对标准C的遵循情况,方便进行条件编译。
以上代码中,通过 cout 输出了 __FILE__ (源文件名)、 __LINE__ (当前行号)、 __DATE__ (编译日期)、 __TIME__ (编译时间)这些预定义宏。调试控制台展示对应的输出结果,直观呈现这些宏在程序调试和信息记录方面的作用。
二.#define
1.#define定义常量
基本格式:#define name stuff
以上代码在VS2022环境中编写。代码首先通过宏定义 PI 表示圆周率近似值,以及用 do_forever 定义了一个死循环结构。在主函数里,先输出 PI 的值,接着进入死循环。循环内变量 i 自增,当 i 等于 5 时输出 i 的值并跳出循环。这展示了宏定义在简化代码和构建特定逻辑结构上的应用,以及循环和条件判断语句的基本用法。
注:在用宏定义标识符时,不要再最后加上引号
原因:
1. 语法错误风险
如果宏定义时不小心加了分号,如 #define PI 3.14159; ,在使用宏的地方,替换后会引入多余分号,可能导致语法错误。
2. 逻辑错误隐患
在宏定义函数 - like宏(带参数的宏)时,加分号问题更严重。
2.#define定义函数
#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏或定义宏。
下面是宏的申明方式:
1 #define name( parament-list ) stuff
不过宏定义有一些问题需要注意,请查看以下代码
以上是两段代码编写与运行结果。代码中包含头文件引入、命名空间使用,以及宏定义 ADD 和 SQUARE 。主函数里定义变量 x 并调用 SQUARE 宏计算表达式值。上方截图中 SQUARE 宏定义存在缺陷,未加括号,运行结果异常;下方截图修正宏定义,加上括号,运行输出正确结果 16 。体现宏定义中括号使用对运算优先级及结果的关键影响。
三.带有副作用的宏参数
当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能出现危险,导致不可预测的后果。副作用就是表达式求值的时候出现的永久性效果。
这段C++ 代码借助宏定义 MAX 来找出两数中的较大值。代码中对宏进行了两组测试:一组使用带有副作用的 a++ 和 b++ 作为参数,运行结果揭示了宏处理这类参数时会改变变量原本的值,导致结果与预期有偏差;另一组使用无副作用的 x + 1 和 y + 1 ,运行后变量值未受影响,结果正常。这鲜明地展示了宏在处理带副作用参数时的风险,警示我们编写代码时需谨慎对待宏定义中参数的副作用,以免引发程序逻辑错误。
四.宏的替换规则
1.调用宏时参数检查
在调用宏时,首先会对传入的参数进行检查,查看参数中是否包含其他由 #define 定义的符号。若包含,这些符号会首先被它们所代表的内容替换 。例如,若已定义 #define PI 3.14 , #define R 2 ,又有宏 #define CIRCUMFERENCE 2 * PI * R ,在使用 CIRCUMFERENCE 时, PI 和 R 会先被替换成 3.14 和 2 。
2.文本替换
完成参数中符号的替换后,预处理器会将宏定义中的替换文本插入到程序中调用宏的原始位置,此时宏定义中的参数名会被传入的实际参数值所替换 。比如 #define MAX(a, b) ((a) > (b)? (a) : (b)) ,当使用 MAX(3, 5) 时,会被替换为 ((3) > (5)? (3) : (5)) 。
3.再次扫描
插入替换文本后,预处理器会再次扫描结果文件,查看其中是否还包含由 #define 定义的符号。若存在,就重复上述替换过程,直至不再有可替换的 #define 定义符号 。
此外,还有一些注意事项:
①宏定义不能递归:宏参数和 #define 定义中虽可出现其他 #define 定义的符号,但宏定义自身不能直接或间接引用自身,否则会导致无限循环替换 。
②字符串常量不参与搜索:预处理器搜索 #define 定义的符号时,字符串常量的内容不会被搜索替换 。例如 #define HELLO "Hello" ,程序中字符串 "Hello World" 里的 Hello 不会被当作宏来替换。
五.宏和函数对比
关键区别总结:
1. 文本替换 vs 逻辑执行:宏是简单的文本替换,函数是运行时执行的指令。
2. 安全性与灵活性:函数更安全(类型检查、作用域),宏更灵活(可操作符号、条件编译等)。
3. 代码膨胀:宏可能引发多次替换导致代码膨胀,函数通常更节省空间。
根据需求选择:性能敏感且简单的操作可用宏,复杂逻辑或需要安全性的场景优先用函数。
宏和函数的命名区别:
六.头文件的包含
1.头文件的被包含方式
"filename"与<filename>的区别
标准库头文件不建议用 "" 包含的原因如下:
①查找效率低:用 "" 时编译器先在当前源文件目录找,没找到才去系统默认库路径找,而标准库头文件在系统默认库路径,这样会多一次不必要查找,浪费时间。
②可移植性问题:不同系统中标准库头文件位置相对固定且统一,用 "" 指定从当前目录找,在不同环境下可能找不到,导致代码在不同平台上移植时出错。 用 <> 直接去系统默认库路径找,可移植性更好。
2.嵌套文件的包含
在C语言中,嵌套文件包含指一个被包含的头文件中又包含另一个头文件 。比如文件a.h包含b.h ,b.h又包含c.h 。
(1)注意事项
①防止重复包含:可能导致头文件内容被多次编译,出现重定义等错误。可使用头文件保护符(如 #ifndef 、 #define 、 #endif 组合 )或 #pragma once 避免。
②包含顺序:不合理顺序可能引发标识符未定义等问题,一般按使用依赖确定顺序。
③避免复杂嵌套:会让代码结构复杂、可读性变差,还影响编译速度。
(2)解决C语言中嵌套文件包含问题的方法如下:
①使用头文件保护符:在头文件中用 #ifndef 、 #define 、 #endif 防止重复定义。
②合理安排包含顺序:先包含被依赖的头文件,再包含依赖它的头文件。
③简化嵌套结构:尽量减少不必要的嵌套,使代码结构清晰。
④使用 #pragma once :在头文件开头添加 #pragma once ,保证头文件只被编译一次。