环境:
- window11
- x86_64-8.1.0-release-win32-seh-rt_v6-rev0.7z (gcc8.1.0)
环境准备参考:《c/c++: window下安装mingw-w64》
一、从一个test.c 文件的编译说起
首先,我们要知道一个 test.c 文件是如何一步步编译成 test.exe 的。 总体来说:
- 第一步:
预处理器 先处理c文件中的预处理指令,如: #include 、#if... 等,得到一个没有预处理指令的代码文件(txt格式) test.i ; - 第二步:
编译器 进行词法/语法分析、优化、编译上一步生成的test.i ,得到汇编文件(txt格式)test.s ; - 第三步:
编译器 将上一步得到的test.s 翻译成二进制格式文件test.o ; - 第四步:
链接器 将上一步得到的test.o 和引用的资源做链接生成test.exe ;
如下面的示例:
file:test.c
#include <stdio.h>
int main()
{
printf("ok");
return 0;
}
编译如下: 我们注意到:预处理器是在编译器开始之前工作的,预处理器的工作内容包含:
摘自:《C程序设计(第四版)学习辅导》 在预处理阶段,预处理器把程序中的注释全部删除; 对预处理指令进行处理,如:把#include 指令指定的头文件(如:stdio.h)的内容复制到#include 指令处; 对#define 指令,进行指定的字符替换(如:将程序中的符号常量用指定的字符串代替),同时删除预处理指令。
当预处理器处理完后,生成的test.i 将不再包含预处理指令了。
二、预处理指令有哪些
在c语言中主要有以下三种预处理指令:
- 文件包含:
#include ; - 宏定义:
#define - 条件编译:
#if...
下面,我们一一讲解:
注意:在后面的test.i 文件中我们可能看到#pragma ,但它不是预处理指令,#pragma 是用来指导编译器行为的。。
三、预处理之文件包含
#include :简单来讲,它就是将指定的文件拷贝到这个指令的地方,并删除这个#include指令。
打开,上面我们生成的test.i 文件: 这里,因为牵扯到系统库,有很多级联的东西,我们可以改下test.c 的代码: 然后进行编译: 然后,我们再来观察生成的 test.i :
这下我们能一目了然了吧:
- #include 做文件内容的拷贝和替换,同时删除所有注释。
- 头文件可以嵌套引用,替换时做遍历,直到把所有引用的都找到;
另外,C语言不允许头文件之间循环引用,如果你在c.h 中再引用a.h ,那么 gcc test.c -E -o test.i 将会死循环:
另外,我们注意到:test.c 中使用了printf() 函数,但是我们并没有#include <stdio.h> 也没有报错,这就证明 预处理器 只是做了源代码文件的整理和替换,并没有涉及到编译。
还有几项问题:
<stdio.h> 和 "stdio.h" 有设么区别?标准库的路径在哪里,我们能不能指定头文件的寻找目录?
前者表示直接从标准库里找这个头文件,后者表示先在c文件同目录下寻找,找不到再去系统目录下寻找。 我们从上面第一次编译输出的test.i 里能清晰的看到gcc 寻找标准库的目录。 一般我们约定,使用C语言的标准库就是<> 这种形式,而其他第三方库或自己写的都用"" 这种形式。 . 如果,我们的头文件和C文件不在一块,可以添加gcc参数,如下: 如果我们用的是vs,可以在工程中配置:
四、预处理之宏定义
4.1 简单宏定义
直接看示例: 我们可以看到,预处理器 处理完后,代码中不再有 PI 这个字符串,它已经被替换成 3.1415926 了,就连 #define PI 3.1415926 这行也没有了。 这就是最简单的宏定义了,它的本质和#include 一样,还是字符串替换。
不过,预处理器 也并不是无脑的替换,比如,当PI 出现在字符串位置时就不会被替换: 另外,宏定义是可以嵌套使用的,如下: 注意:虽然可以嵌套使用,但我们不要死循环了(就像 #include 一样)!!!
4.2 带参数的宏定义
宏定义也可以带参数的,就像定义函数一样。。。直接看示例吧: 不过,有几点我们需要注意下:
- 带参数的宏定义虽然可以实现函数的功能,但它本质还是字符串替换,使用时尤其小心;
- 带参数的宏定义要求宏名和
( 之间不能有空格,否则,,,看示例:
- 带参数的宏最好将参数用括号包裹起来,否则,,,看示例:
4.3 取消宏定义
我们可以使用#undef 取消宏定义,看如下示例:
五、预处理之条件编译
一般情况下,C文件中的所有行都会参与编译,但有时希望程序中的一部分只在满足一定条件时才能参与编译。
对应的语法为:
#ifdef 标识符
程序段1
#elif 标识符
程序段2
#else
程序段3
#endif
还有
#if 标识符
程序段1
#elif 标识符
程序段2
#else
程序段3
#endif
直接看示例: 上面是我模拟不同平台的代码,实际上,有个经典的例子:
在window和linux下调用sleep 函数让程序睡眠是不一样的,如果我们代码想同时兼容linux和window的话,我们可以像下面这样写:
我们注意到,#if 和#ifdef 有点像啊,但它们是不同的,#if 后面可以跟常量表达式,而 #ifdef 后面只能跟宏名: 另外,补充下,还有 #ifndef ,这个表示某个宏不被定义时,如stdio.h 中的使用:
六、关于预处理的一些思考
预处理的存在让我们有机会在编译之前去修整我们的代码(本质是字符串替换),但它实际上也算是"篡改" 我们的源代码了。
正因为如此,我们可以在上面test.i 中看到会有专门标识各个原文件行号的地方,并且预处理指令删除的地方仍然保留空行,这就是为了在实际编译报错的时候能准确定位到test.c 中的位置,而非是test.i 中的。
|