1.3 预编译指令
1.3.1文件包含
实际上,绝大多数的汽车厂商都不能生产出完整的产品。为了生产一辆汽车,厂商需要的所有配件来源大致分成了三个部分:第一部分是厂商自己生产的,比如车体,方向盘,厂商完全了解如何生产和使用这部分配件。第二部分是其他厂家生产的,比如轮胎,但是生产技术不是保密的,即使不是自己生产的,厂商也完全了解如何生产和使用。第三部分也是其他厂家生产的,厂商可以使用,但是不了解生产的工艺。这往往是关键的组成部分,比如发动机。
问题描述:
完成一个程序时往往也同样会需要其他程序的协助。如果把一个程序看作汽车,在输入输出操作时,我们经常使用的“printf()”其实可以看做是一个“发动机”,本身我们并不知道这个函数是怎么实现的,但是我们知道如何使用,所做的只是把这个“发动机”放到合适的位置就可以了。由于生产配件的厂商数量很多,所以每当我们使用这些配件的时候,需要标明配件的生产厂商。程序的配件生产厂商实际上就是“库文件”,每一个它们生产的配件就是一个库函数。
实例分析:
/*example1_3_1.c*/
int main(void){ …… return 0; } |
/*example1_3_2.c*/ #include<stdio.h> int main(void){ …… return 0; } |
对应的汇编代码
/*example1_3_1.c*/
int main(void){ 00411350 push ebp 00411351 mov ebp,esp 00411353 sub esp,0C0h 00411359 push ebx 0041135A push esi 0041135B push edi 0041135C lea edi,[ebp-0C0h] 00411362 mov ecx,30h 00411367 mov eax,0CCCCCCCCh 0041136C rep stos dword ptr es:[edi] return 0; 0041136E xor eax,eax } |
/*example1_3_2.c*/ #include "stdio.h" int main(void){ 00411350 push ebp 00411351 mov ebp,esp 00411353 sub esp,0C0h 00411359 push ebx 0041135A push esi 0041135B push edi 0041135C lea edi,[ebp-0C0h] 00411362 mov ecx,30h 00411367 mov eax,0CCCCCCCCh 0041136C rep stos dword ptr es:[edi] return 0; 0041136E xor eax,eax } |
对这两个程序进行比较,第二个程序里多了一个#include "stdio.h"语句,但是在对两段程序进行汇编的时候却可以发现,两段程序的汇编代码是完全相同的。
还记得上一节提到过的C语言的编译阶段吗?#include <stdio.h>这条语句的处理实际上是发生在预处理阶段(第一阶段),而上面的汇编代码是在编译阶段(第二阶段)完成的。
对于#include <stdio.h>指令来说,它在预处理阶段完成了两个工作
1预编译器首先在计算机系统中找到<stdio.h>这个文件。
2预编译器在预编译阶段把程序中的“#include <stdio.h>”语句替换为找到的那个文件中内容。
需要注意的是,对于#include <stdio.h>文件中的内容,在这个阶段中是没有做任何处理,只是简单的替换而已。
比如这样的一个头文件MaxNum.h
/*MaxNum.h*/ int maxNum(int a, int b) { if(a>b)return a; else return b; }
|
/*example1_3_4.c*/ #include<maxnum.h> int main(void) { int i=1,j=3; maxNum(i,j); return 0; } |
在经过预编译处理阶段以后, example1_3_2.c程序会被处理成下面这样:
int maxNum(int a, int b) { if(a>b)return a; else return b; } int main(void) { int i=1,j=3; maxNum(i,j); return 0; } |
深入剖析
有时我们在编写程序时,会发现某一段代码会被反复的使用(比如求两个数中哪个更大),如果只是在同一个文件中被调用,那么写成函数形式就可以。可是有时需要在另外一个文件中调用这个函数呢,这种情形又如何处理呢?
其实上一节已经给出了解决的方法,就是将这个函数单独的写成一个头文件,然后在需要使用的时候,在程序的开始部分用#include<文件名>的方式把这个函数引入,就可以正常使用了。(可以参见上一节的程序/*example1_3_4.c*/)
其实汽车工厂在使用发动机的时候,并不需要知道发动机是怎么生产出来的,他们所关心的只是这个发动机怎么使用。因此在装配汽车的时候,需要的只是一份使用说明书和一件发动机就可以了,头文件其实也是包含了说明书和产品两部分,函数中的声明就是一份说明书,函数的实现就相当于发动机。在使用的时候,这两者一般是分离的,例如上节中的MaxNum.h,一般是分成声明和实现两部分的,如:
/*MaxNum.h,声明部分*/ int maxNum(int a, int b)
|
/*MaxNum.c,实现部分*/ int maxNum(int a, int b) { if(a>b)return a; else return b; } |
这里面就完成了代码的实现与声明相分离。
实现这种分离其实还有另外一个好处,这里是不是有很多人依靠写代码为生呢?当他们辛辛苦苦的写完一些代码,通过调用这些代码可以完成某些功能。但是这些程序员也许根本不是开源的狂热追求者,甚者有些人可能还希望出售这些代码来还银行的房屋贷款。那么如果这些代码的实现如同上述的那些一样,每个人都可以看见,那么还有谁会出钱去购买这些代码呢?
显然,有人好像进入了两难的尴尬境地,怎样做才能做到两全其美呢?
函数的调用者只需要看到MaxNum.h声明部分,就可以正常的使用MaxNum这个函数的全部功能。如果我们能把MaxNum.c这部分代码变成一段可以执行,但是不可以查阅其中代码的文件,那么好像一切就可以迎刃而解了。
好在lib文件解决了这个问题,对于MaxNum.c,可以把它编译成一个MaxNum.lib文件,这个文件在执行的时候和MaxNum.c是没有任何区别的,但是在阅读的时候,却是以2进制的格式显示的,试图把这种格式从新恢复成C语言是极难实现的,这就既完成了代码的可调用性,又完成了实现代码的隐蔽性,实现了两全其美。
总结
#include<file1.h>起的作用很简单,就是先找到了file1.h这个文件,然后在预编译的阶段用file1.h文件中的内容替换以上程序中的#include<file1.h>这条语句。
使用 #include包含文件的好处可以总结为以下几点:
1代码书写的可读性更高
2很多函数的编写一次以后,可以多次使用。
3实现了代码的实现与声明相分离。
扩展:
经常会出现#include<file1.h>和#include”file2.h”这两种写法,这里<>和””是有区别的。
这里举一个例子,当你在读大学的时候,突然有人通知你的一门考试出了问题,要你去找管理教务的李进老师,那么接下来会做什么呢?
肯定是去找这个李进老师了,问题是学院里有一个专门管理教务的老师,学校里还有专门的教务处,里面都是管理教务的老师!
那么先到哪里去找呢?
这个问题就和头文件的位置问题一样,通常头文件有两个常用的位置,一个是系统设定的目录(就好像学校里的教务处),另一个是当前工作的目录(就好像学院里的院办公室)。
把你陷入这个接下来不知去哪的困境的原因就是通知并没有说清楚到底是李进老师到底是哪个部门的。
C语言为了避免这个问题,做了这样一个约定,那就是“”和<>的使用,比如说系统的默认工作目录为:
C:\Program Files\Microsoft Visual Studio\VC98\Include\
当前的工作目录为:
D:\My Program \test \
如果你使用的是#include<MaxNum.h>,那么C语言编译器会在开发环境设定的搜索路径中去查找所需的头文件。
#include”MaxNum.h”通常首先在当前工作目录下搜索头文件,如果找不到的话,再到开发环境设定的路径去查找。
另外,在#include后面可以加上文件的路径(绝对路径和相对路径都可以的)。
例如#include “D:\My Program \test \MaxNum.h”
我们编写的头文件通常放在当前的工作目录,所以引用自己写的文件通常也会使用#include”file2.h”的方式。