先从源文件和头文件的关系说起,由于是还是初学阶段,只接触了C++语言和windows平台下的编程,所以只讲这两方面的东东,
头文件的作用:对函数,变量,和类的声明,其实在头文件也可对一些特殊函数和变量定义,比如可以在头文件中对内联函数和const类型变量定义,由于对类的声明也就是对类的定义,即在对类完成声明后也就等于对类做了定义,所以头文件中对类的声明也就是对类的定义,但是对类的非内联函数成员的定义则不能放在头件中,
源文件的作用:源文件可以用于实现在头文件中声明的函数原型实现,也可以在源文件中包含一些头文件。
#include命令的实现:
这个命令有两种格试,实现的方法不同,用尖括号实现的格试预编译系统在搜索头件时直接到windows的系统目录下搜索,不行再去其它地方搜索,其它地方是什么地方??往下读就会知道了。
由双引号引出的包含命令感觉会分为三步吧,第一步会在你的程序的当前目录下寻找头文件,第一步结束第二步就会到你的集成开发环境所在的目录下去查找头文件,比如你所安装的VS2012或VC++的包含文件目录,第二步结束就会到系统包含的头文件中去查找,也可以理解成到c盘下的system文件夹中的一个include文件夹中去查找,
这是我自已现在理解的查找方式,三步查找对应了三做不同的目录:1,自已的源文件所在的目录,2,VS或VC++安装文件对应的目录,3,c:\windows\system\include,而这三种目录对应了三种不同的头文件,第一种目录一般是程序作者自已定义的头文件,比如说程序作者为了使自已的程序结构比较清淅,更于修改,把一些适合放在一个头文件中声明的函数放到一个头文件声明,而这个头文件就是作者自已定义的头件,第二种目录一般对应的是C++程序的标准库的头文件,第三种目录对应的头文件是windows系统提供给我们API的头文件,(这里第三种目录我还没有找到确实的文献去验证是不是确实有这么一回事儿,只是通过实践感觉是这样的,并且这样理解起来整个程序的运行机理就比较顺了,)
作了以上解释下面再解释程序的预编译工具就比较容易了。
我把一个程序从源代码开始到被加载到内存中运行为止这段时间分成了几步,预编译,编译,链接,加载,运行。同样在这个过程中程序所处的状态也就分成了好几种,我自已给他们起了名字如下:
纯源代码(状态1)#预编译#
下面对代码的状态和我为什么如此命名这几种状态做一下解译:
纯源代码:就是我们用VS编写出的代码,这个时候这些代码是以文档的形式存放的,我感觉实质上和word或者文本文档没有什么区别,由于未经过计算机做任何的处理仅是由程序作者写出的,所以用纯源代码,用了一个“纯”字儿,
装备后源代码:这是经过预编译后生成的代码,感觉这种代码在实质上还是由ASCII码组成,这里说一下程序的预编译处理对我们的程序做了什么?我们可以把预编译处理做的事情概括为:复制,粘帖,替换。复制我们在源文件中由include命令引用的头文件,把复制来的文件粘帖到include命令出现的位置,把源文件中由#define
有符号意义的二进制代码:
这里举个例子,通过这个例子可以很好的理解头文件,源文件,以及windows的编译器的动作。
这里我们在头文件a.h中作出了函数a()声明,但是函数a()的实现也就是定义是在一个源文件b.c中,而我们在另一个源文件c.c中调用函数a(),下面对上面几步做一下简化编程:
//头文件a.h
声明函数a();
//源文件b.c
#include
实现a();
//源文件c.c
#include
调用a();
一共有一个头文件和两个源文件;
先说一下编译的结果吧,由于微软的编译器才用的分别编译,即对每一个源文件做分别编译并分别形成有一定独立性的目际文件(.o或者.obj),所以这里就形成了b.o和c.o两个目标文件,显然目标文件c.o中是没有函数a的实现的,这里可以看出我们在源文件c.c中包含头文件的唯一作用就是做了函数a的声明,这可以告诉编译器c.c中的那个符号:a()是一个函数,不是别的什么东东,这样编译器就就知道a()这个函数的形参等最基本的形式,生成二进制代码,但是现在的问题时,我们要运行这段二进制代码就要知道函数a的实现呀,这时怎么办呢?而函数a的实现是在文件b.o中的,怎么才能让这两个文件头链呢??这已不是编译器所要处理的任务了,我感觉编译器的任务最重要的并不是生成二进制代码,而是帮我们检查我们的程序在模块层面的罗缉上和语法上是不是有问题,也就是可不可以编译通过衡量的是我们的程序是不是可以翻译成计算机能认识的“字”,至于计算机是不是能读懂我们的程序的意思已不是编程器所做的事儿了,为什么我给这个状态的代码起了一个“有符号意义的二进制代码呢??看上面的例子,c.o虽然是目标代码,但是在该文件中一定存在一种机制,能让下一步的链接程序找到目标文件b.o中的函数a实现的那段代码,这样我们可以理解成链接器能在c.o中找到调用函数a的调用点,并且在b.o中找到函数a的代码,这样看,其实目标文件还且有“符号”意义,也就是说源代码中的函数a()的符号在目标文件中还有一个整体的意义。所以分别编释后才能形成目标代码,我就叫他为”有符号意义的代码”
再说一下链接的步骤:
其实链接步骤完成动作也可以概括为:“代码复制”,和预编译不同的是这里的代码复制只是预编译做的是源代码的复制,而这里做的是对二进制代码的复制,比如这里将b.o中函数a的实现代码复制到b.o调用a函的地方(这里有点静态链接的意思),上面函数a的复制是链接时代码复制的一种,我感觉链接时代码复制的第二种情况是程序中用到的.lib函数库中的一些变量和函数的代码复制,比如你可能在文件b中调用了某个.lib中的函数,这时链接器会把你调用的函数从.lib中复制到文件b的调用点,(这里只是说明代码复制的概念,复制来的代码不一定就一定得插入到b文件中的调用点,也可能“接”到b文件的未尾,再通过指针来实现程序指令地址的跳转,至于把复制来的代码放到何处实链接器的办法而定,但是这种链接,自我感觉,一定是通过代码复制来实现的)这样通过代码的复制,链接程序就把分立编译的程序模块组成了一个整体,也就是人们说的可执行文件.exe,有人说到这里这个程序就以运行了,为什么我还叫他“有缺陷的可执行文件”呢,往下看,
接下来看程序的加载:
如果你把一个.exe文件只接放到没有操作系充的“裸机”上去运行,显然是运行不了的,可是你把这个程序放在一个装有windows系统的电脑上就能运行了,显然,程序的运行还是得依靠windows操作系统,这里就要说到.dll文件,上面说到的链接这一步时的代码复制只讲到对程序作者自已写的文件和.lib文件中用到的代码复制,并没有提到.dll文件中代码复制的情况,从.dll文件的名字可以知道,这部分代码是“动态的链接”到运行着的程序的,比如说我们在文件c中用到了.dll文件中的函数,但是在链接完成后我们并没有将这些函数的代码封到.exe文件中去,可是这些函数又确确实实要用,这是怎么实现的呢,这就是程序加载所要完成的工作,在链接器生成.exe文件后,链接程序把一个“表”放到了.exe文件中,这个表就起录了这个.exe文件调用了哪些.dll中的哪些函数,这样,加载过程中“加载器”就可以跟据这个表去把.dll文件中程序用的那部分代码和.exe文件中的代码一起加载到内存中去,这样就形成了一个“完美的可运行程序”,这个程序驻留在内存中,程序段中的指令真接有CPU取出执行,再和操作系统合作完成程序要完成的各种功能。上面把链接形成的可执行代码叫“有缺陷的可执行文件”到这里可能就解释清楚了,因为它没有.dll文件中的代码,所以不完全,为什么程序要依赖windows我想除了windows会对资源管理外,这也是最重的原因之一,因为windows提供了程序要完成运行必须的.dll文件。
上面提到的.h是头文件,.c是源文件,.lib是静态链接库,.dll