第24讲:预处理详解

发布时间:2026/10/14 3:35:12
第24讲:预处理详解 目录预定义符号#define 定义常量#define 定义宏带有副作用的宏参数宏替换的规则宏函数的对比# 和 ##命名约定#undef命令行定义条件编译头文件的包含其他预处理指令1. 预定义符号C 语言设置了一些预定义符号可以直接使用预定义符号也是在预处理期间处理的。__FILE__// 进行编译的源文件__LINE__// 文件当前的行号__DATE__// 文件被编译的日期__TIME__// 文件被编译的时间__STDC__// 如果编译器遵循 ANSI C其值为 1否则未定义举个例子printf(file:%s line:%d\n,__FILE__,__LINE__);2. #define 定义常量基本语法#definenamestuff举个例子#defineMAX1000#defineregregister// 为 register 这个关键字创建一个简短的名字#definedo_foreverfor(;;)// 用更形象的符号来替换一种实现#defineCASEbreak;case// 在写 case 语句的时候自动把 break 写上。// 如果定义的 stuff 过长可以分成几行写除了最后一行外每行的后面都加一个反斜杠续行符。#defineDEBUG_PRINTprintf(file:%s\tline:%d\t \ date:%s\ttime:%s\n,\__FILE__,__LINE__,\__DATE__,__TIME__)思考在#define定义标识符的时候要不要在最后加上;比如#defineMAX1000;#defineMAX1000建议不要加上;这样容易导致问题。比如下面的场景if(condition)maxMAX;elsemax0;如果是加了分号的情况等替换后if和else之间就是 2 条语句而没有大括号的时候if后边只能有一条语句。这里会出现语法错误。3. #define 定义宏#define机制包括了一个规定允许把参数替换到文本中这种实现通常称为宏macro或定义宏define macro。下面是宏的声明方式#definename(parameter-list)stuff其中的parameter-list是一个由逗号隔开的符号表它们可能出现在stuff中。注意参数列表的左括号必须与name紧邻如果两者之间有任何空白存在参数列表就会被解释为stuff的一部分。举例#defineSQUARE(x)x*x这个宏接收一个参数x。如果在上述声明之后你把SQUARE(5);置于程序中预处理器就会用下面这个表达式替换上面的表达式5*5警告这个宏存在一个问题。观察下面的代码段inta5;printf(%d\n,SQUARE(a1));乍一看你可能觉得这段代码将打印 36事实上它将打印 11为什么呢替换文本时参数x被替换成a 1所以这条语句实际上变成了printf(%d\n,a1*a1);这样就比较清晰了由替换产生的表达式并没有按照预想的次序进行求值。在宏定义上加上两个括号这个问题就轻松地解决了#defineSQUARE(x)(x)*(x)这样预处理之后就产生了预期的效果printf(%d\n,(a1)*(a1));这里还有一个宏定义#defineDOUBLE(x)(x)(x)定义中我们使用了括号想避免之前的问题但是这个宏可能会出现新的错误。inta5;printf(%d\n,10*DOUBLE(a));这将打印什么值呢看上去好像打印 100但事实上打印的是 55。我们发现替换之后printf(%d\n,10*(5)(5));乘法运算先于宏定义的加法所以出现了55。这个问题的解决办法是在宏定义表达式两边加上一对括号就可以了。#defineDOUBLE(x)((x)(x))提示所以用于对数值表达式进行求值的宏定义都应该用这种方式加上括号避免在使用宏时由于参数中的操作符或邻近操作符之间不可预料的相互作用。4. 带有副作用的宏参数当宏参数在宏的定义中出现超过一次的时候如果参数带有副作用那么你在使用这个宏的时候就可能出现危险导致不可预测的后果。副作用就是表达式求值的时候出现的永久性效果。例如x1;// 不带副作用x;// 带有副作用MAX宏可以证明具有副作用的参数所引起的问题。#defineMAX(a,b)((a)(b)?(a):(b))...x5;y8;zMAX(x,y);printf(x%d y%d z%d\n,x,y,z);// 输出的结果是什么这里我们得知道预处理器处理之后的结果是什么z((x)(y)?(x):(y));所以输出的结果是x6 y10 z9。5. 宏替换的规则在程序中扩展#define定义符号和宏时需要涉及几个步骤。在调用宏时首先对参数进行检查看看是否包含任何由#define定义的符号。如果是它们首先被替换。替换文本随后被插入到程序中原来文本的位置。对于宏参数名被它们的值所替换。最后再次对结果文件进行扫描看看它是否包含任何由#define定义的符号。如果是就重复上述处理过程。注意宏参数和#define定义中可以出现其他#define定义的符号。但是对于宏不能出现递归。当预处理器搜索#define定义的符号的时候字符串常量的内容并不被搜索。6. 宏函数的对比宏通常被应用于执行简单的运算。比如在两个数中找出较大的一个时写成下面的宏更有优势一些。#defineMAX(a,b)((a)(b)?(a):(b))那为什么不用函数来完成这个任务原因有二用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏比函数在程序的规模和速度方面更胜一筹。更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。反之这个宏可以适用于整型、长整型、浮点型等可以用于来比较的类型。宏的参数是类型无关的。和函数相比宏的劣势每次使用宏的时候一份宏定义的代码将插入到程序中。除非宏比较短否则可能大幅度增加程序的长度。宏是没法调试的。宏由于类型无关也就不够严谨。宏可能会带来运算符优先级的问题导致程序容易出现错误。宏有时候可以做函数做不到的事情。比如宏的参数可以出现类型但是函数做不到。#defineMALLOC(num,type)\(type*)malloc(num*sizeof(type))...// 使用MALLOC(10,int);// 类型作为参数// 预处理器替换之后(int*)malloc(10*sizeof(int));宏和函数的一个对比7. # 和7.1 # 运算符#运算符将宏的一个参数转换为字符串字面量。它仅允许出现在带参数的宏的替换列表中。#运算符所执行的操作可以理解为字符串化。当我们有一个变量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 107.2 ## 运算符##可以把位于它两边的符号合成一个符号它允许宏定义从分离的文本片段创建标识符。##被称为记号粘合。这样的连接必须产生一个合法的标识符。否则其结果就是未定义的。这里我们想想写一个函数求 2 个数的较大值的时候不同的数据类型就得写不同的函数。比如intint_max(intx,inty){returnxy?x:y;}floatfloat_max(floatx,floaty){returnxy?x:y;}但是这样写起来太繁琐了现在我们这样写代码试试// 宏定义#defineGENERIC_MAX(type)\type type##_max(type x,type y)\{\return(xy?x:y);\}使用宏定义不同函数GENERIC_MAX(int)// 替换到宏体内后 int##_max 生成了新的符号 int_max 做函数名GENERIC_MAX(float)// 替换到宏体内后 float##_max 生成了新的符号 float_max 做函数名intmain(){// 调用函数intmint_max(2,3);printf(%d\n,m);floatfmfloat_max(3.5f,4.5f);printf(%f\n,fm);return0;}输出3 4.500000在实际开发过程中##使用得很少很难取出非常贴切的例子。8. 命名约定一般来讲函数和宏的使用语法很相似。所以语言本身没法帮我们区分二者。那我们平时的一个习惯是把宏名全部大写函数名不要全部大写9. #undef这条指令用于移除一个宏定义。#undefNAME// 如果现存的一个名字需要被重新定义那么它的旧名字首先要被移除。10. 命令行定义许多 C 的编译器提供了一种能力允许在命令行中定义符号用于启动编译过程。例如当我们根据同一个源文件要编译出一个程序的不同版本的时候这个特性有点用处。假定某个程序中声明了一个某个长度的数组如果机器内存有限我们需要一个很小的数组但是另外一个机器内存大些我们需要一个数组能够大些。#includestdio.hintmain(){intarray[ARRAY_SIZE];inti0;for(i0;iARRAY_SIZE;i){array[i]i;}for(i0;iARRAY_SIZE;i){printf(%d ,array[i]);}printf(\n);return0;}编译指令# Linux 环境演示gcc-DARRAY_SIZE10programe.c11. 条件编译在编译一个程序的时候我们如果要将一条语句一组语句编译或者放弃是很方便的。因为我们有条件编译指令。比如说调试性的代码删除可惜保留又碍事所以我们可以选择性的编译。#includestdio.h#define__DEBUG__intmain(){inti0;intarr[10]{0};for(i0;i10;i){arr[i]i;#ifdef__DEBUG__printf(%d\n,arr[i]);// 为了观察数组是否赋值成功。#endif// __DEBUG__}return0;}常见的条件编译指令1.#if条件编译#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#endif12. 头文件的包含12.1 头文件被包含的方式12.1.1 本地文件包含#includefilename查找策略先在源文件所在目录下查找如果该头文件未找到编译器就像查找库函数头文件一样在标准位置查找头文件。如果找不到就提示编译错误。Linux 环境的标准头文件的路径/usr/includeVS 环境的标准头文件的路径C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\include // 这是 VS2013 的默认路径注意按照自己的安装路径去找。12.1.2 库文件包含#includefilename.h查找头文件直接去标准路径下去查找如果找不到就提示编译错误。这样是不是可以说对于库文件也可以使用#include filename.h的形式包含答案是肯定的可以但是这样做查找的效率就低些当然这样也不容易区分是库文件还是本地文件了。12.2 嵌套文件包含我们已经知道#include指令可以使另外一个文件被编译就像它实际出现在#include指令的地方一样。这种替换的方式很简单预处理器先删除这条指令并用包含文件的内容替换。一个头文件被包含 10 次那就实际被编译 10 次如果重复包含对编译的压力就比较大。test.c#includetest.h#includetest.h#includetest.h#includetest.h#includetest.hintmain(){return0;}test.hvoidtest();structStu{intid;charname[20];};如果直接这样写test.c文件中将test.h包含 5 次那么test.h文件的内容会被拷贝 5 份在test.c中。如果test.h文件比较大这样预处理后代码量会剧增。如果工程比较大有公共使用的头文件被大家都能使用又不做任何的处理那么后果真的不堪设想。如何解决头文件被重复引入的问题答案条件编译。每个头文件的开头写#ifndef__TEST_H__#define__TEST_H__// 头文件的内容#endif// __TEST_H__或者#pragmaonce就可以避免头文件的重复引入。注推荐《高质量 C/C 编程指南》中附录的考试试卷很重要。笔试题头文件中的ifndef/define/endif是干什么用的#include filename.h和#include filename.h有什么区别13. 其他预处理指令#error#pragma#line...// 不做介绍自己去了解。#pragmapack()在结构体部分介绍。参考《C 语言深度解剖》学习。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询