# 预处理 在编译和链接之前,还需要对源文件进行一些文本方面的操作,比如文本替换、文件包含、删除部分代码等,这个过程叫做预处理,由预处理程序完成。较之其他编程语言,C/C++ 语言更依赖预处理器,所以在阅读或开发 C/C++ 程序过程中,可能会接触大量的预处理指令,比如 #include、#define 等。 ## 预处理过程 前面各章中,已经多次使用过`#include`命令。使用库函数之前,应该用`#include`引入对应的头文件。这种以`#`号开头的命令称为预处理命令。 C语言源文件要经过预处理、编译、链接才能生成可执行程序: 1. 预处理,对源文件进行简单加工的过程。 2. 编译(Compile)会将源文件(`.c`文件)转换为目标文件。对于 VC/VS,目标文件后缀为`.obj`;对于GCC,目标文件后缀为`.o`。 > 编译是针对单个源文件的,一次编译操作只能编译一个源文件,如果程序中有多个源文件,就需要多次编译操作。 3. 链接(Link)是针对多个文件的,它会将编译生成的多个目标文件以及系统中的库、组件等合并成一个可执行程序。 关于编译和链接的过程、目标文件和可执行文件的结构、.h 文件和 .c 文件的区别。 在实际开发中,有时候在编译之前还需要对源文件进行简单的处理。例如,我们希望自己的程序在 Windows 和 Linux 下都能够运行,那么就要在 Windows 下使用 VS 编译一遍,然后在 Linux 下使用 GCC 编译一遍。但是现在有个问题,程序中要实现的某个功能在 VS 和 GCC 下使用的函数不同(假设 VS 下使用 a(),GCC 下使用 b()),VS 下的函数在 GCC 下不能编译通过,GCC 下的函数在 VS 下也不能编译通过,怎么办呢? 这就需要在编译之前先对源文件进行处理:如果检测到是 VS,就保留 a() 删除 b();如果检测到是 GCC,就保留 b() 删除 a()。 这些在编译之前对源文件进行简单加工的过程,就称为**预处理**(即预先处理、提前处理)。 预处理主要是处理以`#`开头的命令,例如`#include `等。预处理命令要放在所有函数之外,而且一般都放在源文件的前面。 预处理是C语言的一个重要功能,由**预处理程序**完成。当对一个源文件进行编译时,系统将自动调用预处理程序对源程序中的预处理部分作处理,处理完毕自动进入对源程序的编译。 编译器会将预处理的结果保存到和源文件同名的`.i`文件中,例如 main.c 的预处理结果在 main.i 中。和`.c`一样,`.i`也是文本文件,可以用编辑器打开直接查看内容。 C语言提供了多种预处理功能,如宏定义、文件包含、条件编译等,合理地使用它们会使编写的程序便于阅读、修改、移植和调试,也有利于模块化程序设计。 ```mermaid flowchart LR A[xxx.c, xxx.h] -->|预处理| B[xxx.i] -->|编译| C[xxx.o/xxx.obj] -->|链接| D[xxx/xxx.exe] ``` ## 实例 下面我们举个例子来说明预处理命令的实际用途。假如现在要开发一个C语言程序,让它暂停 5 秒以后再输出内容,并且要求跨平台,在 Windows 和 Linux 下都能运行,怎么办呢? 这个程序的难点在于不同平台下的暂停函数和头文件都不一样: - Windows 平台下的暂停函数的原型是`void Sleep(DWORD dwMilliseconds)`(注意 S 是大写的),参数的单位是“毫秒”,位于 头文件。 - Linux 平台下暂停函数的原型是`unsigned int sleep (unsigned int seconds)`,参数的单位是“秒”,位于 头文件。 不同的平台下必须调用不同的函数,并引入不同的头文件,否则就会导致编译错误,因为 Windows 平台下没有 sleep() 函数,也没有 头文件,反之亦然。这就要求我们在编译之前,也就是预处理阶段来解决这个问题。请看下面的代码: ```c #include //不同的平台下引入不同的头文件 #if _WIN32 //识别windows平台 #include #elif __linux__ //识别linux平台 #include #endif int main(int argc, char *argv[]){ //不同平台调用不同的函数 #if _WIN32 Sleep(5000); #elif __linux__ sleep(5); #endif puts("wangyuedong."); return 0; } ``` \#if、#elif、#endif 就是预处理命令,它们都是在编译之前由预处理程序来执行的。这里我们不讨论细节,只从整体上来理解。 你看,在不同的平台下,编译之前(预处理之后)的源代码都是不一样的。这就是预处理阶段的工作,它把代码当成普通文本,根据设定的条件进行一些简单的文本替换,将替换以后的结果再交给编译器处理。 ## #include用法 `#include`叫做文件包含命令,用来引入对应的头文件(`.h`文件)。#include 也是C语言预处理命令的一种。 \#include 的处理过程很简单,就是将头文件的内容插入到该命令所在的位置,从而把头文件和当前源文件连接成一个源文件,这与复制粘贴的效果相同。 \#include 的用法有两种,如下所示: \#include \#include "myHeader.h" 使用尖括号`< >`和双引号`" "`的区别在于头文件的搜索路径不同: - 使用尖括号`< >`,编译器会到系统路径下查找头文件; - 而使用双引号`" "`,编译器首先在当前目录下查找头文件,如果没有找到,再到系统路径下查找。 也就是说,使用双引号比使用尖括号多了一个查找路径,它的功能更为强大。 stdio.h 和 stdlib.h 都是标准头文件,它们存放于系统路径下,所以使用尖括号和双引号都能够成功引入;而我们自己编写的头文件,一般存放于当前项目的路径下,所以不能使用尖括号,只能使用双引号。当然,你也可以把当前项目所在的目录添加到系统路径,这样就可以使用尖括号了,但是一般没人这么做,纯粹多此一举,费力不讨好。 在以后的编程中,大家既可以使用尖括号来引入标准头文件,也可以使用双引号来引入标准头文件;不过,我个人的习惯是使用尖括号来引入标准头文件,使用双引号来引入自定义头文件(自己编写的头文件),这样一眼就能看出头文件的区别。 关于 #include 用法的注意事项: - 一个 #include 命令只能包含一个头文件,多个头文件需要多个 #include 命令。 - 同一个头文件可以被多次引入,多次引入的效果和一次引入的效果相同,因为头文件在代码层面有防止重复引入的机制。 - 文件包含允许嵌套,也就是说在一个被包含的文件中又可以包含另一个文件。 「在头文件中定义定义函数和全局变量」这种认知是原则性的错误!不管是标准头文件,还是自定义头文件,都只能包含变量和函数的声明,不能包含定义,否则在多次引入时会引起重复定义错误。