环境:
环境准备参考:《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
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:

这下我们能一目了然了吧:
另外,C语言不允许头文件之间循环引用,如果你在c.h中再引用a.h,那么 gcc test.c -E -o test.i将会死循环:

另外,我们注意到:test.c中使用了printf()函数,但是我们并没有#include 也没有报错,这就证明 预处理器只是做了源代码文件的整理和替换,并没有涉及到编译。
还有几项问题:
和 "stdio.h" 有设么区别?标准库的路径在哪里,我们能不能指定头文件的寻找目录?
前者表示直接从标准库里找这个头文件,后者表示先在c文件同目录下寻找,找不到再去系统目录下寻找。
我们从上面第一次编译输出的test.i里能清晰的看到gcc寻找标准库的目录。
一般我们约定,使用C语言的标准库就是<>这种形式,而其他第三方库或自己写的都用""这种形式。
.
如果,我们的头文件和C文件不在一块,可以添加gcc参数,如下:
如果我们用的是vs,可以在工程中配置:
直接看示例:

我们可以看到,预处理器处理完后,代码中不再有 PI这个字符串,它已经被替换成 3.1415926了,就连 #define PI 3.1415926这行也没有了。
这就是最简单的宏定义了,它的本质和#include一样,还是字符串替换。
不过,预处理器也并不是无脑的替换,比如,当PI出现在字符串位置时就不会被替换:

另外,宏定义是可以嵌套使用的,如下:

注意:虽然可以嵌套使用,但我们不要死循环了(就像 #include 一样)!!!
宏定义也可以带参数的,就像定义函数一样。。。直接看示例吧:

不过,有几点我们需要注意下:
(之间不能有空格,否则,,,看示例:

我们可以使用#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中的使用:

我们最常用的场景应该是像 __WIN32 和 __linux__ 一样,代码本身不用定义宏,但可以根据环境传入的值进行选择性编译。
那么,我们如何从环境传入宏定义呢?我们可以使用-D定义宏,使用-U取消宏定义,如:
-DMY_MACRO=123 或 -DMY_MACRO 或-UMY_MACRO。
看下面示例:

预处理的存在让我们有机会在编译之前去修整我们的代码(本质是字符串替换),但它实际上也算是"篡改"我们的源代码了。
正因为如此,我们可以在上面test.i中看到会有专门标识各个原文件行号的地方,并且预处理指令删除的地方仍然保留空行,这就是为了在实际编译报错的时候能准确定位到test.c中的位置,而非是test.i中的。

经过,#include 指令的讲解,我们知道,头文件的内容会被拷贝到源文件中,那么,既然如此,我们手写一个mystdio.h代替stdio.h会不会时一样的效果呢(内容同 stdio.h)?或者说,我们直接在 test.c的上方声明 prinf函数是不是就可以不用 #include 了??
我们直接来看下面示例:

看了这个例子,是不是对 头文件 和 #include 的认识又多了些。