程序的编译和预处理

目录
1. 程序的翻译环境和执行环境
2. 详解编译+链接
2.1 翻译环境
2.2 编译本身也分为几个阶段:
预处理 ( gcc -E )
编译 ( 源文件 转换成 汇编代码 )
汇编
链接
2.3 运行环境
3. 预处理详解
3.1 预定义符号
3.2 #define
3.2.1 #define 定义标识符
3.2.2 #define 定义宏
3.2.3 #define 替换规则
3.2.4 #和##
3.2.5 带副作用的宏参数
3.2.6 宏和函数对比
3.3 #undef
3.4 命令行定义
3.5 条件编译
3.6 文件包含
3.6.1 头文件被包含的方式:
3.6.2 嵌套文件包含
1. 程序的翻译环境和执行环境 在ANSIC的任何一种实现中,存在两个不同的环境。
第1种是翻译环境,在这个环境中源代码被转换为可执行的机器指令。
第2种是执行环境,它用于实际执行代码。
2. 详解编译+链接 2.1 翻译环境 程序的编译和预处理
文章图片

组成一个程序的每个源文件通过编译过程分别转换成目标代码(object code)。
每个目标文件由链接器(linker)捆绑在一起,形成一个单一而完整的可执行程序。
链接器同时也会引入标准C函数库中任何被该程序所用到的函数,而且它可以搜索程序员个人的程序库,将其需要的函数也链接到程序中。
2.2 编译本身也分为几个阶段:

编译一个 C程序可以分为四阶段:预处理阶段 ---> 生成汇编代码阶段 ---> 汇编阶段 ---> 链接阶段。

程序的编译和预处理
文章图片


gcc 指令的一般格式为:
gcc [选项] 要编译的文件 [选项] [目标文件] 其中,目标文件可缺省,gcc默认生成可执行的文件名为:a.out gcc main.c直接生成可执行文件 a.out gcc -E main.c -o hello.i生成预处理后的代码(还是文本文件) gcc –S main.c -o hello.s生成汇编代码 gcc –c main.c -o hello.o生成目标代码

C程序 目标文件和可执行文件 结构
目标文件和可执行文件可以有几种不同的格式,有ELF(Excutable and linking Format,可执行文件和链接)格式,也有COFF(Common Object-File Format,普通目标文件格式)。
虽然格式不一样,但具有一个共同的概念,那就是 段(segments),这里段指二进制格式文件中的一块区域。
linux下的可执行文件有三个段:( 可用 nm 命令查看目标文件的符号清单 )
  • 文本段(text)
  • 数据段(data)
  • bss段
预处理 ( gcc -E )
预编译:主要处理那些源代码文件中的以 # 开始的预编译指令,如 #include、#define、#if,同时并删除注释行,还会添加行号和文件名标识,以便于编译时编译器产生调试用的行号信息,及用于编译时产生编译错误或警告时能够显示行号。
经过预编译的 .i 文件不包含任何宏定义,因为所有的宏已经被展开并且包含的文件也已经被插入到 .i 文件中。
所以当我们无法判断 宏定义是否正确 或 头文件包含是否正确 时,可以查看已编译后的文件来确认问题。比如:hello.c 中第一行的 #include命令告诉预处理器读取系统头文件 stdio.h 的内容,并且把它直接插入到程序文本中,结果就得到了另一个C程序,通常是以 .i 作为文件扩展名。在该阶段,编译器将 C 源代码中的包含的头文件如 stdio.h 编译进来,用户可以使用 gcc 的选项 -E 进行查看。
用法:#gcc -E main.c -o main.i 作用:将main.c预处理输出main.i文件[user:test] ls main.c [user:test] gcc -E main.c -o main.i [user:test] ls main.cmain.i

使用 gcc -E 参数完成。
程序的编译和预处理
文章图片

预处理会干什么事情:
  • 展开所有的宏定义并删除 #define
  • 处理所有的条件编译指令,例如 #if #else #endif #ifndef …
  • 把所有的 #include 替换为头文件实际内容,递归进行
  • 把所有的注释 // 和 / / 替换为空格
  • 添加行号和文件名标识以供编译器使用
  • 保留所有的 #pragma 指令,因为编译器要使用
  • ……
处理完成之后看看我们的 Hello.i,发现原来8行代码现在变成了接近700行,因为将的文件被替换进来了,在最后几行找到了我们自己 Hello.c 的代码:
程序的编译和预处理
文章图片


使用系统默认的预处理器 cpp 完成。
预处理除了使用 GCC -E 参数完成之外,我们还可以使用系统默认的预处理器 cpp 完成。如下所示
程序的编译和预处理
文章图片

我们看看Hello.ii的代码:
程序的编译和预处理
文章图片

虽然 Hello.i 和 Hello.ii 的代码对应的行数不同,但是内容却是一模一样的,只是中间空行的数量不同而已。
OK ,接下来,继续向编译出发。
编译 ( 源文件 转换成 汇编代码 ) gcc -S
编译是将 源文件 转换成 汇编代码 的过程,具体的步骤主要有:词法分析 ---> 语法分析 ---> 语义分析及相关的优化 ---> 中间代码生成 ---> 目标代码生成(汇编文件.s)。
具体生成过程可以参考《编译原理》。在这个阶段中,gcc 首先要检查代码的规范性、是否有语法错误等,以确定代码的实际要做的工作,在检查无误后,gcc 把代码翻译成汇编语言。
用户可以使用 -S 选项来进行查看,该选项只进行编译而不进行汇编,生成汇编代码。
选项 -S 用法:[user]# gcc –S main.i –o main.s 作用:将预处理输出文件main.i汇编成main.s文件。[user:test] ls main.c main.i [user:test] gcc -S main.i -o main.s [user:test] ls main.c main.i main.s

程序的编译和预处理
文章图片


注意:gcc 命令只是一个后台程序的包装,会根据不同的参数要求去调用预编译编译程序cc1(c)、汇编器 as、连接器 ld。
使用 gcc -S 参数完成。
程序的编译和预处理
文章图片

查看 Hello.s 发现已经是汇编代码了。
程序的编译和预处理
文章图片

使用系统默认的编译器 cc1 完成这个过程。
前面的预处理命令 cpp 可能大家的系统上都有,我们输入cp,然后 Tab 两下(Linux系统上表示提示补全命令),系统提示如下:
程序的编译和预处理
文章图片

倒数第二个命令就是 cpp 了。但是我们 cc 同样的过程的时候却发现:
程序的编译和预处理
文章图片


并没有 cc1 这个命令,但是 cc1 确实是 Linux 系统上默认的编译器呀,我们在系统上找找看:
程序的编译和预处理
文章图片

看上图第二条,/usr/libexec/gcc/x86_64-redhat-linux/4.8.2/cc1,尝试着去看下:
程序的编译和预处理
文章图片

有可执行权限,那为何不试试能不能用来编译 Hello.ii 呢?
程序的编译和预处理
文章图片

好像没有什么报错,迫不及待的看看 Hello.ss 的内容:
程序的编译和预处理
文章图片

发现和 Hello.s 的是一样的。编译成功。
汇编
汇编阶段是把编译阶段生成的 ”.s” 文件转成二进制目标代码。汇编器(as)将 hello.s 翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目标文件hello.o中。hello.o文件是一个二进制文件,它的字节编码是机器语言指令而不是字符。如果我们在文本编译器中打开 hello.o 文件,看到的将是一堆乱码。
选项 -c 用法:[user]# gcc -c main.s -o main.o 作用:将汇编输出文件main.s编译输出main.o文件。[user:test] ls main.c main.i main.s [user:test] gcc -c main.s -o main.o [user:test] ls main.c main.i main.o main.s

使用 gcc -c 参数完成。
程序的编译和预处理
文章图片

其实也可以查看下 Hello.o 的内容:
程序的编译和预处理
文章图片

只是乱码罢了。要是想看,我们可以使用 hexedit, readelf 和 objdump 这三个工具。
hexedit 只是个将二进制文件用十六进制打开的工具,我们执行:
$ sudo yum install hexedit $ hexedit Hello.o

可以看到:
程序的编译和预处理
文章图片

最右边是源文件被翻译成可见字符,点.表示的都是不可见字符。这样看当然没有多大实际意义,但是一些输出的字符串 Hello World,包括整个文件的类型 ELF 都是可以看到的。
readelf 和 objdump 我们后面再说。

使用系统默认的汇编器as完成。
程序的编译和预处理
文章图片

hexedit 看看 :
程序的编译和预处理
文章图片

使用 cmp 命令比较 Hello.oo 和 Hello.o
程序的编译和预处理
文章图片

只有极少数字符不同。可能也是格式问题。
总结:上面的过程中,我们已经将 Hello.c 源程序经过预处理、编译、汇编阶段变成了二进制代码,这三个过程我们都是用两种方法完成的,一种是 GCC + 参数的方法,另一种是使用系统默认的预处理器,编译器,汇编器。这两种方法都达到了我们的目的,最后给它加上x权限。然后运行
chmod a+x a.out ./a.out

链接
这阶段就是把汇编后的机器指令集变成可以直接运行的文件,而对目标文件进行链接主要是因为在目标文件中可能用到了在其他文件当中定义的字段(或者函数),通过链接来把多个不同目标文件关联到一起。
比如:有2个目标文件 a 和 b,在 b 中定义了一个函数 "method",而在文件 a 中则使用到了b文件中的函数 "method",通过链接文件a才能调用到函数"method",不然文件a根本就不知道到函数 "method" 底做了些什么操作。
hello 程序调用了一个 printf 函数,它是每个 C 编译器都会提供的标准C库中的一个函数,printf 函数存在于一个名为 printf.o 的单独预编译好了的标准文件中,而这个文件必须以某种方式合并到我们的 hello.o 程序中,链接器(ld)就负责处理这种合并,结果就得到 hello 文件,他是一个可执行目标文件(简称:可执行文件),可以被加载到内存中,有系统执行。
gcc的无选项的编译就是链接 用法:[user]# gcc main.o -o main.elf 作用:将编译输出文件main.o链接成最终可执行文件main.elf[user:test] ls main.c main.i main.o main.s [user:test] gcc main.o -o main.elf [user:test] ls main.c main.elf* main.i main.o main.s

模块之间的通信有两种方式:一种是模块间的函数调用,另一种是模块间的变量访问。函数访问需知道目标函数的地址,变量访问也需要知道目标变量的地址,所以这两种方式都可以归结为一种方式,那就是模块间符号的引用。模块间依靠符号来通信类似于拼图版,定义符号的模块多出一块区域,引用该符号的模块刚好少了那一块区域,两者一拼接刚好完美组合。这个模块的拼接过程就是“链接”。
在链接中,函数和变量统称为符号(symbol),函数名或变量名就是符号名(symbol name)。可以将符号看做是链接中的粘合剂,整个链接过程正是基于符号才能够正确完成。链接过程中很关键的一部分就是符号的管理,每一个目标文件都会有一个相应的符号表(symbol table),这个表里面记录了目标文件中所用到的所有符号。每个定义的符号有一个对应的值,叫做符号值(symbol value),对于变量和函数来说,符号值就是它们的地址。符号表中所有的符号分类:
  • 1、定义在本目标文件的全局符号,可以被其他目标文件引用。
  • 2、在本目标文件中引用的全局符号,却没有定义在本目标文件,这一般叫做外部符号(external symbol),比如printf。
  • 3、段名,这种符号往往由编译器产生,它的值就是该段的起始地址,比如“.text”、“.data”。
  • 4、局部符号,这类符号只在编译单元内部可见。这些局部符号对于链接过程没有作用,链接器往往忽略它们。
  • 5、行号信息,即目标文件指令与源代码中代码行的对应关系。
程序的编译和预处理
文章图片

链接过程主要包括了地址和空间分配、符号决议和重定位。符号决议有时候也叫做符号绑定、名称绑定、名称决议,甚至还有叫做地址绑定、指令绑定,大体上它们的意思都一样,但从细节角度来区分,它们之间还存在一定区别,比如“决议”更倾向于静态链接,而“绑定”更倾向于动态链接,即它们所使用的范围不一样。
每个目标文件都可能定义一些符号,也可能引用到定义咋其他目标文件的符号。重定位的过程中,每个重定位的入口都是对一个符号的引用,那么当链接器须要对某个符号的引用重定位时,它就是要确定这个符号的目标地址。这时候链接器就会去查找由所有输入目标文件的符号表组成的全局符号表,找到相应的符号后进行重定位。

看代码:
sum.c
int g_val = 2016; void print(const char *str) { printf("%s\n", str); }

test.c
#include int main() { extern void print(char *str); extern int g_val; printf("%d\n", g_val); print("hello bit.\n"); return 0; }


如何查看编译期间的每一步发生了什么呢?
test.c
#include int main() { int i = 0; for(i=0; i<10; i++) { printf("%d ", i); } return 0; }

1. 预处理 选项 gcc -E test.c -o test.i
预处理完成之后就停下来,预处理之后产生的结果都放在test.i文件中。
2. 编译 选项 gcc -S test.c
编译完成之后就停下来,结果保存在test.s中。
3. 汇编 gcc -c test.c
汇编完成之后就停下来,结果保存在test.o中。
2.3 运行环境 程序执行的过程:
1. 程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序
的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。
2. 程序的执行便开始。接着便调用main函数。
3. 开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回
地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程
一直保留他们的值。
4. 终止程序。正常终止main函数;也有可能是意外终止。
3. 预处理详解 3.1 预定义符号
__FILE__ //进行编译的源文件 __LINE__ //文件当前的行号 __DATE__ //文件被编译的日期 __TIME__ //文件被编译的时间 __STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义

这些预定义符号都是语言内置的。
举个栗子:
printf("file:%s line:%d\n", __FILE__, __LINE__);


3.2 #define 3.2.1 #define 定义标识符
语法: #define name stuff

举个栗子:
#define MAX 1000 #define reg register //为 register这个关键字,创建一个简短的名字 #define do_forever for(; ; ) //用更形象的符号来替换一种实现 #define CASE break; case //在写case语句的时候自动把 break写上。 // 如果定义的 stuff过长,可以分成几行写,除了最后一行外,每行的后面都加一个反斜杠(续行符)。 #define DEBUG_PRINT printf("file:%s\tline:%d\t \ date:%s\ttime:%s\n" ,\ __FILE__,__LINE__ , \ __DATE__,__TIME__ )

在define定义标识符的时候,要不要在最后加上 ; ?
比如:
#define MAX 1000; #define MAX 1000

建议不要加上 ; ,这样容易导致问题。
比如下面的场景:
if(condition) max = MAX; else max = 0;

这里会出现语法错误。

3.2.2 #define 定义宏
#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定
义宏(define macro)。
下面是宏的申明方式:
#define name( parament-list ) stuff

其中的 parament-list 是一个由逗号隔开的符号表,它们可能出现在stuff中。
注意:
参数列表的左括号必须与name紧邻。
如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。
如:

#define SQUARE( x ) x * x

这个宏接收一个参数 x .
如果在上述声明之后,你把
SQUARE( 5 );

置于程序中,预处理器就会用下面这个表达式替换上面的表达式:
5 * 5

【程序的编译和预处理】警告:
这个宏存在一个问题:
观察下面的代码段:
int a = 5; printf("%d\n" ,SQUARE( a + 1) );

乍一看,你可能觉得这段代码将打印36这个值。
事实上,它将打印11.
为什么?
替换文本时,参数x被替换成a + 1,所以这条语句实际上变成了: printf ("%d\n",a + 1 * a + 1 );

这样就比较清晰了,由替换产生的表达式并没有按照预想的次序进行求值。
在宏定义上加上两个括号,这个问题便轻松的解决了:

#define SQUARE(x) (x) * (x)

这样预处理之后就产生了预期的效果:
printf ("%d\n",(a + 1) * (a + 1) );

这里还有一个宏定义:
#define DOUBLE(x) (x) + (x)

定义中我们使用了括号,想避免之前的问题,但是这个宏可能会出现新的错误。
int a = 5; printf("%d\n" ,10 * DOUBLE(a));

这将打印什么值呢?
warning:
看上去,好像打印100,但事实上打印的是55.
我们发现替换之后:
printf ("%d\n",10 * (5) + (5));

乘法运算先于宏定义的加法,所以出现了55
这个问题,的解决办法是在宏定义表达式两边加上一对括号就可以了。
#define DOUBLE( x) ( ( x ) + ( x ) )

提示:
所以用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时由于参数
中的操作符或邻近操作符之间不可预料的相互作用。

3.2.3 #define 替换规则
在程序中扩展#define定义符号和宏时,需要涉及几个步骤。
1. 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先
被替换。
2. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值替换。
3. 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上
述处理过程。
注意:
1. 宏参数和#define 定义中可以出现其他#define定义的变量。但是对于宏,不能出现递归。
2. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。

3.2.4 #和##
如何把参数插入到字符串中?
首先我们看看这样的代码:
char* p = "hello ""bit\n"; printf("hello"," bit\n"); printf("%s", p);

这里输出的是不是hello bit?
答案是确定的:是。
我们发现字符串是有自动连接的特点的。
1. 那我们是不是可以写这样的代码?:
#define PRINT(FORMAT, VALUE)\ printf("the value is "FORMAT"\n", VALUE); ... PRINT("%d", 10);


这里只有当字符串作为宏参数的时候才可以把字符串放在字符串中。
1. 另外一个技巧是:
使用 # ,把一个宏参数变成对应的字符串。
比如:
int i = 10; #define PRINT(FORMAT, VALUE)\ printf("the value of " #VALUE "is "FORMAT "\n", VALUE); ... PRINT("%d", i+3); //产生了什么效果?

代码中的 #VALUE 会预处理器处理为:
"VALUE" .
最终的输出的结果应该是:
the value of i+3 is 13

## 的作用
##可以把位于它两边的符号合成一个符号。
它允许宏定义从分离的文本片段创建标识符
#define ADD_TO_SUM(num, value) \ sum##num += value; ... ADD_TO_SUM(5, 10); //作用是:给sum5增加10.

注:
这样的连接必须产生一个合法的标识符。否则其结果就是未定义的。

3.2.5 带副作用的宏参数
当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能
出现危险,导致不可预测的后果。副作用就是表达式求值的时候出现的永久性效果。
例如:
x+1; //不带副作用 x++; //带有副作用

MAX宏可以证明具有副作用的参数所引起的问题。
#define MAX(a, b) ( (a) > (b) ? (a) : (b) ) ... x = 5; y = 8; z = MAX(x++, y++); printf("x=%d y=%d z=%d\n", x, y, z); //输出的结果是什么?

这里我们得知道预处理器处理之后的结果是什么:
z = ( (x++) > (y++) ? (x++) : (y++));

所以输出的结果是:
x=6 y=10 z=9

3.2.6 宏和函数对比
宏通常被应用于执行简单的运算。比如在两个数中找出较大的一个。

#define MAX(a, b) ((a)>(b)?(a):(b))

那为什么不用函数来完成这个任务?
原因有二:
1. 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏比
函数在程序的规模和速度方面更胜一筹。
2. 更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。反之
这个宏怎可以适用于整形、长整型、浮点型等可以用于>来比较的类型。宏是类型无关的。
当然和宏相比函数也有劣势的地方:
1. 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序
的长度。
2. 宏是没法调试的。
3. 宏由于类型无关,也就不够严谨。
4. 宏可能会带来运算符优先级的问题,导致程容易出现错。
宏有时候可以做函数做不到的事情。比如:宏的参数可以出现类型,但是函数做不到。
#define MALLOC(num, type)\ (type *)malloc(num * sizeof(type)) ... //使用 MALLOC(10, int); //类型作为参数 //预处理器替换之后: (int *)malloc(10 * sizeof(int));

宏和函数的一个对比
属 性 #define定义宏 函数
代 码 长 度 每次使用时,宏代码都会被插入到程序中。除了非
常小的宏之外,程序的长度会大幅度增长
函数代码只出现于一个地方;每
次使用这个函数时,都调用那个
地方的同一份代码
执 行 速 度 更快 存在函数的调用和返回的额外开
销,所以相对慢一些
操 作 符 优 先 级 宏参数的求值是在所有周围表达式的上下文环境
里,除非加上括号,否则邻近操作符的优先级可能
会产生不可预料的后果,所以建议宏在书写的时候
多些括号。
函数参数只在函数调用的时候求
值一次,它的结果值传递给函
数。表达式的求值结果更容易预
测。
带 有 副 作 用 的 参 数 参数可能被替换到宏体中的多个位置,所以带有副
作用的参数求值可能会产生不可预料的结果。
函数参数只在传参的时候求值一
次,结果更容易控制。
参 数 类 型 宏的参数与类型无关,只要对参数的操作是合法
的,它就可以使用于任何参数类型。
函数的参数是与类型有关的,如
果参数的类型不同,就需要不同
的函数,即使他们执行的任务是
不同的。
调 试 宏是不方便调试的 函数是可以逐语句调试的
递 归 宏是不能递归的 函数是可以递归的

命名约定
一般来讲函数的宏的使用语法很相似。所以语言本身没法帮我们区分二者。
那我们平时的一个习惯是:
把宏名全部大写
函数名不要全部大写
3.3 #undef这条指令用于移除一个宏定义。
#undef NAME //如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除。

3.4 命令行定义 许多C 的编译器提供了一种能力,允许在命令行中定义符号。用于启动编译过程。
例如:当我们根据同一个源文件要编译出不同的一个程序的不同版本的时候,这个特性有点用处。(假
定某个程序中声明了一个某个长度的数组,如果机器内存有限,我们需要一个很小的数组,但是另外一
个机器内存大写,我们需要一个数组能够大写。)
#include int main() { int array [ARRAY_SIZE]; int i = 0; for(i = 0; i< ARRAY_SIZE; i ++) { array[i] = i; } for(i = 0; i< ARRAY_SIZE; i ++) { printf("%d " ,array[i]); } printf("\n" ); return 0; }

编译指令:
gcc -D ARRAY_SIZE=10 programe.c

3.5 条件编译 在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者放弃是很方便的。因为我们有条件
编译指令。
比如说:
调试性的代码,删除可惜,保留又碍事,所以我们可以选择性的编译。
#include #define __DEBUG__ int main() { int i = 0; int arr[10] = {0}; for(i=0; i<10; i++) { arr[i] = i; #ifdef __DEBUG__ printf("%d\n", arr[i]); //为了观察数组是否赋值成功。 #endif //__DEBUG__ } return 0; }

常见的条件编译指令

1. #if 常量表达式 //... #endif //常量表达式由预处理器求值。 如: #define __DEBUG__ 1 #if __DEBUG__ //.. #endif 2.多个分支的条件编译 #if 常量表达式 //... #elif 常量表达式 //... #else //... #endif 3.判断是否被定义 #if defined(symbol) #ifdef symbol #if !defined(symbol) #ifndef symbol 4.嵌套指令 #if defined(OS_UNIX) #ifdef OPTION1 unix_version_option1(); #endif #ifdef OPTION2 unix_version_option2(); #endif #elif defined(OS_MSDOS) #ifdef OPTION2 msdos_version_option2(); #endif #endif

3.6 文件包含 我们已经知道, #include 指令可以使另外一个文件被编译。就像它实际出现于 #include 指令的地方
一样。
这种替换的方式很简单:
预处理器先删除这条指令,并用包含文件的内容替换。
这样一个源文件被包含10次,那就实际被编译10次。

3.6.1 头文件被包含的方式:

本地文件包含
#include "filename"

查找策略:先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标
准位置查找头文件。
如果找不到就提示编译错误。

Linux环境的标准头文件的路径:
/usr/include

VS环境的标准头文件的路径:
C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\include

注意按照自己的安装路径去找。
库文件包含
#include

查找头文件直接去标准路径下去查找,如果找不到就提示编译错误。
这样是不是可以说,对于库文件也可以使用 “” 的形式包含?
答案是肯定的,可以。
但是这样做查找的效率就低些,当然这样也不容易区分是库文件还是本地文件了。
3.6.2 嵌套文件包含
如果出现这样的场景:
程序的编译和预处理
文章图片

comm.h和comm.c是公共模块。
test1.h和test1.c使用了公共模块。
test2.h和test2.c使用了公共模块。
test.h和test.c使用了test1模块和test2模块。
这样最终程序中就会出现两份comm.h的内容。这样就造成了文件内容的重复。
如何解决这个问题?
答案:条件编译。
每个头文件的开头写:
#ifndef __TEST_H__ #define __TEST_H__ //头文件的内容 #endif //__TEST_H__

或者:
#pragma once

就可以避免头文件的重复引入。

    推荐阅读