[TOC]
前言😜
上篇博客,我们提到了C语言程序运行的几个环节。
本篇博客中提到的预处理指令,就是在预处理阶段运行的一些代码。
本篇博客使用的编译器🎰
- VS2019(win10)
- 树莓派(linux-gcc)
1.预定义符号
1 | __FILE__ //进行编译的源文件 |
2.#define
2.1定义标识符
1 |
1 |
|
加分号问题
define定义标识符的时候,最好不要在结尾加上;
1 |
2.2定义宏
除了定义标识符以外,define还可以定义一个语句为标识符,允许把参数替换到文本中。这种机制叫做定义宏
下面是宏的申明方式:
1 |
其中的parament-list
是一个由逗号隔开的符号表,它们可能出现在stuff中
参数列表的左括号必须与name紧邻。 如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分
1 | //如 |
宏需要注意的问题
这个宏有一个问题👇
1 |
|
这个式子输出的结果是什么呢?是60吗?
答案是否定的:#define
宏在使用的时候执行的是直接替换
这个语句就相当于10*3+3
,根据操作符优先级可知,结果为30+3=33
想解决这个问题,我们需要记住这个原则:
- 给变量加上括号以确保优先级
- 给整个宏再加上一个括号防止外部数据影响
1 |
再运行程序,发现答案变成了60
用于对数值表达式进行求值的宏定义,都应该用这种方式加上括号。避免在使用宏时,参数中的操作符或邻近操作符之间产生不可预料的相互作用
2.3define替换规则
2.4使用#
和##
2.4.1#
将字符串插入字符串
1 | int main() |
在这个代码里面,打印的前置内容a和b需要根据打印的变量进行更改
有没有一种办法,可以让他自己进行更改?
1 |
|
可以看到,#X
处的内容被替换成了我们需要打印的变量
实际上,这里#X
旁边的双引号,是将整个字符串拆分成了3份进行打印
- “the num of”
#X
- “is %d\n”
字符串在打印的时候具有自动拼接的特性,所以我们可以通过这种方式将一个需要更改的字符插入到字符串中
2.4.2##
将两个符号合并
##
可以把位于它两边的符号合成一个符号。它允许宏定义从分离的文本片段创建标识符。
如图,这个宏将Class和10两个分离的符号合并成了Class10,printf识别出来并打印的了Class10的值
当我们同名变量有很多的时候,就可以利用这种宏来给不同的变量增加数据。
1 |
|
这样的连接必须产生一个合法的标识符,否则其结果就是未定义的。
2.5带副作用的宏参数
当宏在定义中出现超过一次时,如果参数带有副作用,使用这个宏的时候就有可能出现危险,导致无法预测的结果
副作用指表达式求值时出现的一些附带效果,如前置++和后置++
1 | x+1//不带副作用 |
以下这个宏可以体现上面描述的问题
1 | #define MAX(x,y) ((x)>(y)?(x):(y)) |
如果是一个函数封装,我们的理解是,a和b原来的值3和5被传入MAX,然后再各自++一次
但实际上并不是这样,可以看到a++了一次,但是b++了两次
这是为什么呢?
- 宏执行的是直接替换
1 |
|
这个表达式中,执行比较的时候,a和b各++一次,但是在最后返回b的时候,末尾b++
被执行了一次
得到的结果就是a=4,b=7
2.6宏和函数对比
宏经常被用作执行简单的计算(如上面提到多次的MAX)
这时候宏对比函数有几个优点
- 函数的调用需要压栈出栈,比实际执行这个小型代码需要的时间更长。宏比函数在程序的执行速度方面更胜一筹
- 函数的参数必须声明位特定的类型,只适用于特定类型的表达式上。反之,宏可以用整型、长整型、浮点型等可以用>来比较的类型。宏的调用与类型无关
除了MAX这个简单宏语句外,宏的参数还可以出现数据类型
如下图中我们调用的这个宏,就可以做到用一种更简单的方式来调用malloc函数。此时的调用只需要写入待开辟数据个数和数据类型,不需要再写强制类型转换等语句,方便使用
有得就有失,宏当然也有缺点:
- 每次使用宏,执行的是直接替换。如果此时宏比较长,则会增加程序的长度
- 宏在预处理阶段执行了替换,无法进行调试
- 宏与类型无关,不够严谨
- 宏会出现操作符优先级问题,容易出错
2.7命名约定
一般来讲函数的宏的使用语法很相似,所以语言本身没法帮我们区分二者。
那我们平时的一个习惯是:
把宏名全部大写:
MAX
函数名不要全部大写:
Max
1 |
|
2.7 #undef
这条指令用于移除一个宏定义
1 | #undef NAME |
如果不移除,就无法重复定义同名宏
3.命令行定义
一些C语言的编译器提供了一个功能,允许我们在命令行中定义一个符号,用于启动编译过程
当我们需要一个程序的不同版本时,可以使用该指令
如:在不同情况下需要不同长度的数组
如果我们直接编译这部分代码,编译器会报错,SZ未定义
但当我们写上这么一行命令
1 | gcc test.c -D SZ=10 |
可以看到编译器没有报错,再次ls,发现多了一个a.out
文件
执行该文件,可以看到SZ被定义成10并成功打印
4.条件编译
在编译一个程序的时候,我们可以通过条件编译指令来控制一组语句的使用与否
4.1if/endif
1 |
|
#if
语句后为真即执行,为假不执行
- 该语句之后只能跟随常量表达式,不能跟随定义的变量
1 |
|
在linux环境下,使用编译语句执行预处理操作,可以看到生成的test.i文件中,printf代码是被包含进去的
如果把M更改为0,再次执行预处理操作。printf语句并没有包含在for循环中
- 如果这里放入变量n,不起作用
你可能想问,如果这一行代码我不需要,直接注释掉不就ok了吗?
并不然。
有些时候我们为了验证之前写的程序是否正确,会编写一些测试代码,用于debug。这些代码在测试完成后可以删除,但是如果我们下次还需要测试同一个函数的时候,就有需要重新写一遍,很是不方便。
有了条件编译指令,我们就可以在程序的最上方#define
定义一个常量,来控制是否进行测试。
4.2多个分支的调节编译
1 |
|
和之前一样,不运行的代码,VS2019会显示为灰色
4.3判断符号是否已被定义
1 |
|
如果想把条件改为未定义的时候执行,可以使用下面这两种方式
!
:逻辑反操作符
4.4嵌套指令
和其他语句一样,条件编译语句也可以嵌套使用
1 |
|
5.文件包含
我们知道,#include
指令可以使另外一个文件被编译。它同时也是一个替换:
- 编译器在预处理阶段删除这条指令,用包含文件的内容替换
- 一个源文件被包含10次,就会被编译10次
5.1包含方式
1 |
你可能会有这个疑问,这两种包含方式之间有什么区别呢?
双引号方式
- 现在源文件(项目文件)所在目录下查找。
- 如果该头文件未找到,编译器就会像查找库函数头文件一样在标准位置查找头文件。
- 如果找不到就提示编译错误
VS2019标准位置(去VS的安装目录找)
1 | C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.29.30133\include |
Linux环境标准位置
1 | /usr/include |
库文件方式
- 只在标准路径下查找,如果找不到就提示编译错误
既然双引号方式也会去标准路径下查找,那是不是说我们可以用双引号方式包含库函数的头文件?
答案是肯定的
但是这么做,会让我们难以分辨头文件到底是库函数还是自定义头文件。两次查找也会影响程序编译效率
5.2嵌套文件包含问题
假如在项目合作中,出现了这种情况
- comm.h和.c是公共文件
- test1.h和test1.c使用了公共模块
- test2.h和test2.c也使用了公共模块
- test.h和test.c最终使用了test1和2模块
这种情况下,就相当于有两份comm.h的内容被拷贝到最终的程序中
假如comm.h
中有define或者全局变量的定义,这就相当于一个定义语句写了两遍,出现了重复定义
如何解决?
我们可以在每个头文件的开头写
1 |
|
这样,如果__TEST_H__
符号已经被定义过,编译器就不会二次展开头文件中的代码,也就避免了这个问题
如果你使用的是VS编译器,在创建.h文件的时候,VS会自己包含一个语句
1 | #pragma once |
这个语句也有相同的作用
warning: #pragma once in main file
在我尝试在linux环境下使用#pragma once
语句时,遇到了这个报错
解决这个问题的办法很简单,就是不要编译头文件
编译器会自动展开头文件,无需手动编译
网上查了查:出现这个问题的原因是编译器在编译头文件的时候,#pragma once
本身是没有含义的语句,所以报错了。
- 也有人说是因为linux不支持这个语句,我们来试试
右侧代码中包含了两个test.h的引用,在预处理中只包含了一次
去掉头文件中的#pragma once
,再次编译,可以看到预处理文件中出现了两次头文件的内容
这说明linux-gcc编译器是支持该语句的,并非网上说的不支持!
还有更多……
其实预处理指令还远不止本博客中包含的这些
1 | #error |
这些预处理指令还等待我的学习~记录在小本本上了
如果这篇博客对你有帮助,还请点个👍吧!
- 本文标题:【C语言】预处理操作(详解)
- 创建时间:2022-03-10 22:41:59
- 本文链接:posts/3942304404/
- 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!