【C语言】认识预处理
文章目录一、预定义符号二、#define定义常量用法示例注意事项三、#define定义宏关键注意点四、带有副作用的宏参数五、宏替换的规则六、宏与函数的对比七、#和##运算符7.1 # 运算符字符串化7.2 ##运算符记号粘合八、命名约定区分宏与函数九、#undef移除宏定义十、命令行定义编译时动态配置十一、条件编译常见条件编译指令应用示例调试代码控制十二、头文件的包含12.1 包含方式及查找策略12.2 避免头文件重复包含十三、其他预处理指令一、预定义符号C语言为我们内置了一些预定义符号它们在预处理期间就会被处理我们可以直接在代码中使用无需额外定义。这些符号能为我们提供很多有用的信息__FILE__进行编译的源文件的文件名__LINE__当前代码在文件中的行号__DATE__文件被编译的日期格式为“Mmm dd yyyy”__TIME__文件被编译的时间格式为“hh:mm:ss”__STDC__如果编译器遵循ANSI C标准其值为1否则未定义这些符号在调试代码时非常有用例如printf(Error in file: %s at line: %d\n,__FILE__,__LINE__);当程序运行到这里时会自动打印出错误所在的文件名和行号帮助我们快速定位问题。二、#define定义常量#define最基本的用法是定义常量基本语法为#definenamestuff用法示例定义数值常量#define MAX 1000为关键字创建简短别名#define reg register替换复杂实现为更形象的符号#define do_forever for(;;)简化代码编写#define CASE break;case在switch语句中自动添加break注意事项不要随意添加分号例如#define MAX 1000;这样的定义是不推荐的。当在if(condition)maxMAX;elsemax0;中使用时替换后会变成if(condition)max1000;;elsemax0;导致if和else之间出现两条语句引发语法错误。长内容的分行处理如果定义的内容过长可以分成多行书写除最后一行外每行末尾都加上反斜杠续行符。反斜杠后面不能有任何内容#defineDEBUG_PRINTprintf(file:%s\tline:%d\t \ date:%s\ttime:%s\n,\__FILE__,__LINE__,\__DATE__,__TIME__)三、#define定义宏#define还允许我们定义带参数的宏macro实现更灵活的文本替换。宏的声明方式为#definename(parameter-list)stuff其中parameter-list是由逗号分隔的参数列表它们会出现在stuff中。关键注意点参数列表与名称紧连参数列表的左括号必须与name紧邻中间不能有空白否则参数列表会被解释为stuff的一部分。运算符优先级问题例如#define SQUARE(x) x * x这个宏当传入SQUARE(a 1)时会被替换为a 1 * a 1结果为a a 1而非预期的(a1)^2。解决方法是给参数和整体加上括号#define SQUARE(x) (x) * (x)。更复杂的情况#define DOUBLE(x) (x) (x)当调用10 * DOUBLE(5)时会被替换为10 * (5) (5)结果为55而非100。正确的定义应为#define DOUBLE(x) ((x) (x))。结论用于数值表达式求值的宏应给参数和整体都加上括号避免运算符优先级导致的意外结果。四、带有副作用的宏参数当宏参数在宏定义中出现多次且参数带有副作用时可能会产生不可预测的结果。副作用指表达式求值时产生的永久性效果如x会改变x的值而x1则不会。例如#defineMAX(a,b)((a)(b)?(a):(b))intx5,y8,z;zMAX(x,y);预处理后会变成z((x)(y)?(x):(y));执行后x的值变为6y变为10z变为9与直观预期可能不符。这就是副作用参数带来的问题。五、宏替换的规则在扩展#define定义的符号和宏时预处理器遵循以下步骤调用宏时首先检查参数是否包含#define定义的符号若有则先替换。将替换文本插入到原位置宏的参数名被其值替换。再次扫描结果文件若包含#define定义的符号重复上述过程。注意宏参数中可以包含其他#define定义的符号但宏不能递归。预处理器不会搜索字符串常量的内容即字符串中的符号不会被替换。六、宏与函数的对比宏和函数都能实现代码复用但它们在多个方面存在差异属性#define定义宏函数代码长度每次使用都会插入代码可能大幅增加程序长度除非宏很短代码只出现一次每次调用都使用同一份代码执行速度更快无函数调用和返回开销较慢存在函数调用和返回的额外开销操作符优先级可能受周围表达式优先级影响需谨慎使用括号参数只在传参时求值结果传递给函数表达式求值可预测副作用参数参数若多次出现副作用可能导致不可预料的结果参数只在传参时求值一次副作用易控制参数类型与类型无关只要操作合法即可用于任何类型与类型相关不同类型可能需要不同函数即使功能相同调试不方便调试预处理阶段已替换可逐语句调试递归不能递归可以递归宏的独特优势宏可以接受类型作为参数而函数无法做到。例如#defineMALLOC(num,type)(type*)malloc(num*sizeof(type))// 使用int*pMALLOC(10,int);// 替换后(int *)malloc(10 * sizeof(int));七、#和##运算符7.1 # 运算符字符串化#运算符能将宏的参数转换为字符串字面量仅用于带参数的宏的替换列表中。例如#definePRINT(n)printf(the value of #n is %d,n);// 调用inta10;PRINT(a);// 替换后printf(the value of a is %d, a);输出结果为the value of a is 107.2 ##运算符记号粘合##可以将两边的符号合并为一个符号允许从分离的文本片段创建标识符称为“记号粘合”。为不同类型定义求最大值的函数#defineGENERIC_MAX(type)\type type##_max(type a,type b)\{\returnab?a:b;\}GENERIC_MAX(int);//int_max()GENERIC_MAX(float);//float_max()intmain(){intr1int_max(3,9);floatr2float_max(4.6,8.9);printf(%d %f,r1,r2);return0;}八、命名约定区分宏与函数宏和函数的使用语法相似为了区分二者通常遵循以下约定宏名全部大写如MAX、SQUARE函数名不要全部大写如int_max、add九、#undef移除宏定义#undef指令用于移除已有的宏定义语法为#undefNAME如果需要重新定义一个已存在的宏应先使用#undef移除其旧定义。十、命令行定义编译时动态配置许多C编译器允许在命令行中定义符号用于启动编译过程。这在根据同一源文件编译程序的不同版本时非常有用。根据内存大小动态调整数组长度// 源文件program.c#includestdio.hintmain(){intarray[ARRAY_SIZE];for(inti0;iARRAY_SIZE;i){array[i]i;printf(%d ,array[i]);}return0;}编译时在命令行定义ARRAY_SIZE# Linux环境gcc-DARRAY_SIZE10program.c# 定义数组长度为10十一、条件编译条件编译指令允许我们选择性地编译或放弃某些语句常用于调试代码的开关控制。常见条件编译指令基本形式#if常量表达式// 代码段#endif预处理器会求值常量表达式为真则编译代码段。多分支形式#if常量表达式// 代码段1#elif常量表达式// 代码段2#else// 代码段3#endif判断符号是否定义只判断是否被定义不判断真假#ifdefined(symbol)// 等价于 #ifdef symbol// 代码段#endif#if!defined(symbol)// 等价于 #ifndef symbol// 代码段#endif嵌套指令#ifdefined(OS_UNIX)#ifdefOPTION1unix_version_option1();#endif#elifdefined(OS_MSDOS)#ifdefOPTION2msdos_version_option2();#endif#endif应用示例调试代码控制#define__DEBUG__1// 定义调试符号intmain(){intarr[10]{0};for(inti0;i10;i){arr[i]i;#ifdef__DEBUG__printf(arr[%d] %d\n,i,arr[i]);// 仅调试时编译#endif}return0;}十二、头文件的包含头文件包含是预处理阶段的重要操作用于将其他文件的内容插入到当前文件中。12.1 包含方式及查找策略本地文件包含#include filename查找策略先在源文件所在目录查找若未找到再到标准库目录查找。库文件包含#include filename.h查找策略直接到标准库目录查找效率更高。注意库文件也可以用包含但会降低查找效率且不易区分是本地文件还是库文件。12.2 避免头文件重复包含头文件被多次包含会导致代码冗余、编译时间增加甚至出现重复定义错误。解决方法是使用条件编译方式一ifndef/define/endif// test.h#ifndef__TEST_H__// 如果未定义__TEST_H__#define__TEST_H__// 定义__TEST_H__// 头文件内容函数声明、结构体定义等voidtest();structStu{intid;charname[20];};#endif// __TEST_H__方式二#pragma once更简洁的方式直接在头文件开头添加#pragmaonce// 确保头文件只被包含一次// 头文件内容十三、其他预处理指令除了上述内容C语言还有一些其他预处理指令#error在编译时输出错误信息终止编译#pragma用于向编译器提供额外信息如#pragma pack()控制结构体对齐#line修改当前行号和文件名