
文章目录C语言梦开始的地方22预处理详解预定义符号#define定义常量#define定义宏带有副作用的宏参数宏替换的规则宏和函数的对比#和###运算符##运算符命名约定#undef命令行定义条件编译头文件的包含头文件被包含的方式本地头文件包含库文件包含嵌套文件包含其他预处理指令C语言梦开始的地方22预处理详解上一章节我们主要了解C语言的编译和链接本章节则是详细了解编译的第一步操作预处理。预定义符号C语言设置了一些预定义符号可以直接使用预定义符号也是在预处理期间处理的。__FILE__//进行编译的源文件__LINE__//文件当前的行号__DATE__//文件被编译的日期__TIME__//文件被编译的时间__STDC__//如果编译器遵循ANSI C其值为1否则未定义#includestdio.hintmain(){printf(file:%s\n,__FILE__);printf(line:%d\n,__LINE__);printf(date:%s\n,__DATE__);printf(time:%s\n,__TIME__);printf(stdc:%d\n,__STDC__);return0;}上述这些预定义符号有什么用呢当我们在写一些C语言项目并运行时有可能出现一些问题比如文件打开了未关闭这种导致程序突然崩溃如果单纯靠调试或者猜的这种后期定位问题就会很麻烦。所以这里就可以使用上述这几种预定义符号比如__FILE__就可以获取是哪个文件出的错__LINE__就可以获取是那一行代码报错以及__DATE__和__TIME__我们就能知道报错的时间是什么时候。#define定义常量基本语法#definenamestuff示例#defineMAX1000#defineregregister// 为 register这个关键字创建一个简短的名字#definedo_foreverfor(;;)// 用更形象的符号来替换一种实现#defineCASE\break;\case// 在写case语句的时候自动把 break写上。// 如果定义的 stuff过长可以分成几行写除了最后一行外每行的后面都加一个反斜杠(续行符)。#defineDEBUG_PRINTprintf(file:%s\tline:%d\t \ date:%s\ttime:%s\n,\__FILE__,__LINE__,\__DATE__,__LINE)我们发现#define定义的结尾没有;为什么#define定义结尾不写;呢如果加上会怎么样呢我们看这个示例#includestdio.h#defineMAX11000#defineMAX21000;intmain(){intarr1[MAX1]{0};intarr2[MAX2]{0};return0;}此时运行程序报错了下面错误test.c: In function main: test.c:4:18: error: expected ] before ; token 4 | #define MAX2 1000; | ^ test.c:10:14: note: in expansion of macro MAX2 10 | int arr2[MAX2] {0}; |这是什么原因呢这是因为#define定义在预处理阶段是通过文本替换的方式来实现其定义的常量比如上面代码在预处理阶段就如下所示#includestdio.h#defineMAX11000#defineMAX21000;intmain(){intarr1[1000]{0};intarr2[1000;]{0};return0;}#define定义宏#define 机制包括了一个规定允许把参数替换到文本中这种实现通常称为宏macro或定义宏define macro。下面是宏的申明方式#definename(parament-list)stuffname宏的名称标识符。( parameter-list )参数列表用逗号分隔比如 x, y。注意左括号必须紧挨着 name中间不能有空格。stuff替换体宏体。这里是参数要被填入并展开的地方。示例#includestdio.h#defineSQUARE(x)x*xintmain(){inta5;printf(%d\n,SQUARE(a1));return0;}结果会是多少呢 5 1 6 6 * 6 36。对吗运行后我们发现是11为什么因为#define本质上是文本替换所以这里就变成了printf(%d\n,a1*a1);所以更合理的写法应该是这样的#includestdio.h#defineSQUARE(x)(x)*(x)intmain(){inta5;printf(%d\n,SQUARE(a1));return0;}所以用于对数值表达式进行求值的宏定义都应该用这种方式加上括号避免在使用宏时由于参数中的操作符或邻近操作符之间不可预料的相互作用。带有副作用的宏参数当宏参数在宏的定义中出现超过一次的时候如果参数带有副作用那么你在使用这个宏的时候就可能出现危险导致不可预测的后果。副作用就是表达式求值的时候出现的永久性效果。x1;//不带副作用x;//带有副作用下面通过代码的方式来展现这种副作用#includestdio.h#defineMAX(a,b)((a)(b)?(a):(b))intmain(){intx5;inty8;intzMAX(x,y);printf(x%d y%d z%d\n,x,y,z);return0;}运行程序我们会得到x6 y10 z9通过上述两个#define定义我们不难理解为什么这里结果是这样的。所以宏定义中一定不要使用 和 – 这种操作。宏替换的规则在程序中扩展#define定义符号和宏时需要涉及几个步骤。在调用宏时首先对参数进行检查看看是否包含任何由#define定义的符号。如果是它们首先被替换。替换文本随后被插入到程序中原来文本的位置。对于宏参数名被他们的值所替换。最后再次对结果文件进行扫描看看它是否包含任何由#define定义的符号。如果是就重复上述处理过程。注意宏参数和#define 定义中可以出现其他#define定义的符号。但是对于宏不能出现递归。当预处理器搜索#define定义的符号的时候字符串常量的内容并不被搜索。宏和函数的对比宏通常被应用于执行简单的运算。比如在两个数中找出较大的一个时写成下面的宏更有优势一些。#defineMAX(a,b)((a)(b)?(a):(b))那为什么不用函数来完成这个任务原因有二用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏比函数在程序的规模和速度方面更胜一筹。更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。反之这个宏怎可以适用于整形、长整型、浮点型等可以用于 来比较的类型。宏的参数是类型无关的。那么既然有了宏这种通用的语法为什么还要有函数呢原因如下每次使用宏的时候一份宏定义的代码将插入到程序中。除非宏比较短否则可能大幅度增加程序的长度。宏是没法调试的。宏由于类型无关也就不够严谨。宏可能会带来运算符优先级的问题导致程容易出现错。宏有时候可以做函数做不到的事情。比如宏的参数可以出现类型但是函数做不到。#defineMALLOC(num,type)\(type)malloc(numsizeof(type))...//使用MALLOC(10,int);//类型作为参数//预处理器替换之后(int*)malloc(10sizeof(int));对比维度宏函数执行速度更快无调用/返回开销直接展开较慢有压栈、跳转、返回开销类型检查无参数无类型传什么算什么严格类型检查参数副作用极其危险多次求值安全传参时只求值一次调试不可打断点可单步调试代码长度每次调用展开一次代码膨胀仅一份代码递归不能可以传类型可以如MALLOC10int不可以#和###运算符#运算符将宏里面的参数直接转换为字符串。当我们有一个变量int a 10; 的时候我们想打印出 the value of a is 10 .就可以写#definePRINT(n)printf(the value of #n is %d,n);当我们按照下面的方式调用的时候PRINT(a);//当我们把a替换到宏的体内时就出现了#a而#a就是转换为a时一个字符串代码就会被预处理为printf(the value of a is %d,a);运行代码就能在屏幕上打印the value of a is 10##运算符##运算符将左右两边的符号连接成一个新的标识符。比如说我们要实现一个floatint类型的比较大小的函数就可以使用下面这种宏定义#defineGENERIC_MAX(type)\type type##_max(type x,type y){return((x)(y)?(x):(y));}GENERIC_MAX(int)// 生成 int_max 函数GENERIC_MAX(float)// 生成 float_max 函数使用如下#includestdio.h#defineGENERIC_MAX(type)\type type##_max(type x,type y){return((x)(y)?(x):(y));}GENERIC_MAX(int)// 生成 int_max 函数GENERIC_MAX(float)// 生成 float_max 函数intmain(){// 调用函数intmint_max(2,3);printf(%d\n,m);floatfmfloat_max(3.5f,4.5f);printf(%f\n,fm);return0;}命名约定一般来讲函数的宏的使用语法很相似。所以语言本身没法帮我们区分二者。那我们平时的一个习惯是把宏名全部大写函数名不要全部大写#undef这条指令用于移除一个宏定义。#defineMAX100#undefMAX#defineMAX200// 必须先 undef 才能重新定义命令行定义许多的C语言编译器提供了一种能力即在编译命令中定义宏用于同一源码编译不同版本如不同数组大小。#includestdio.hintmain(){// 注意ARRAY_SIZE 没有在源码中定义intarr[ARRAY_SIZE];for(inti0;iARRAY_SIZE;i){arr[i]i;printf(%d ,arr[i]);}printf(\n);return0;}环境为GCC输入命令为gcc-DARRAY_SIZE10test.c-otest底层处理的时候就是在预处理器在扫描源代码前先插入 #define ARRAY_SIZE 10然后进行常规预处理。条件编译根据条件决定编译哪些代码可以被编译器使用哪些不可以被编译器使用常用于调试、跨平台、头文件保护。因为Linux和Windows的系统环境不一样那么在编写程序的时候就会有所不同了同时为了代码的跨平台性就可以使用条件编译。比如#includestdio.hintmain(){// 场景1调试开关 // 编译时加 -DDEBUG 才会打印日志否则这行代码完全消失#ifdefDEBUGprintf([调试] 当前变量值: 100\n);#endif// 场景2跨平台兼容 // Windows 和 Linux 执行不同的逻辑#ifdefined(_WIN32)||defined(_WIN64)printf(当前系统: Windows\n);// Windows 下特有的函数比如 system(pause);system(pause);#elifdefined(__linux__)printf(当前系统: Linux\n);// Linux 下特有的函数比如 system(read);system(read);#elseprintf(未知系统\n);#endifreturn0;}常见的条件编译指令1.#if常量表达式//...#endif// 常量表达式由预处理器求值。如#define__DEBUG__1#if__DEBUG__//..#endif2.多个分支的条件编译#if常量表达式//...#elif常量表达式//...#else//...#endif3.判断是否被定义#ifdefined(symbol)#ifdefsymbol#if!defined(symbol)#ifndefsymbol4.嵌套指令#ifdefined(OS_UNIX)#ifdefOPTION1unix_version_option1();#endif#ifdefOPTION2unix_version_option2();#endif#elifdefined(OS_MSDOS)#ifdefOPTION2msdos_version_option2();#endif#endif头文件的包含头文件被包含的方式本地头文件包含#includefilename先在源文件所在目录查找。若未找到则按标准库路径查找如 /usr/include 或 VS 下的 VC/include。注意查找时要按照自己的安装路径进行查找或者借助软件如Everything库文件包含#includefilename.h直接按标准库路径查找同样库文件也可以使用来进行包含因为本身库文件就在本地文件夹中但是这样查找速率会很慢。嵌套文件包含我们已经知道 #include 指令可以使另外一个文件被编译。就像它实际出现于#include 指令的地方一样。这种替换的方式很简单预处理器先删除这条指令并用包含文件的内容替换。一个头文件被包含10次那就实际被编译10次如果重复包含对编译的压力就比较大。test.c#includetest.h#includetest.h#includetest.h#includetest.h#includetest.hintmain(){return0;}test.hvoidtest();structStu{intid;charname[20];};上述代码的问题头文件被多次 #include内容被拷贝多次导致编译冗余、可能重定义错误。那么该如何去解决呢答案条件编译。方法1#ifndef__TEST_H#define__TEST_H// 头文件内容#endif方法2#pragmaonce// 头文件内容需注意方法2非标准用法但是被主流编译器支持。那么如果是多文件包含呢我们上一章了解了编译和链接的部分知识点我们知道在编译的时候每个文件夹都是自己顾自己的直到链接的时候才会进行形成最后的可执行程序也就是说在编译期间它们只会管自己所在的文件中是否存在重复包含链接时就分两种情况了一种是静态变量和非静态变量静态变量会导致同一份数据被两个文件获取并不会报错但可能会有bug非静态变量在链接的时候会进行审查处理如果出现了一份数据被多个文件获取就会报错。其他预处理指令#error产生编译错误消息用于条件检查。#pragma编译器特定的指令如#pragma pack(1)设置结构体对齐。#line修改__LINE__和__FILE__的值。#空指令无效果。如果想深入了解可以去看《C语言深度解剖》完