c++应用程序文件的编译过程
这里讲下C++文件的编译过程及其中模板的编译过程;
一:一般的C++应用程序的编译过程。
一般说来,C++应用程序的编译过程分为三个阶段。模板也是一样的。
在cpp文件中展开include文件。
将每个cpp文件编译为一个对应的obj文件。
连接obj文件成为一个exe文件(或者其它的库文件)。
下面分别描述这几个阶段。
1.include文件的展开。
include文件的展开是一个很简单的过程,只是将include文件包含的代码拷贝到包含该文件的cpp文件(或者其它头文件)中。被展开的cpp文件就成了一个独立的编译单元。在一些文章中我看到将.h文件和.cpp文件一起看作一个编译单元,我觉得这样的理解有问题。至于原因,看看下面的几个注意点就可以了。
1):没有被任何的其它cpp文件或者头文件包含的.h文件将不会被编译。也不会最终成为应用程序的一部分。先看一个简单的例子:
1 ==========test.h文件==========
2 // 注意,后面没有分号。也就是说,如果编译的话这里将产生错误。
3 void foo()在你的应用程序中添加一个test.h文件,如上面所示。但是,不要在任何的其它文件中include该文件。编译C++工程后你会发现,并没有报告上面的代码错误。这说明.h文件本身不是一个编译单元。只有通过include语句最终包括到了一个.cpp文件中后才会成为一个编译单元。
2):存在一种可能性,即一个cpp文件直接的或者间接的包括了多次同一个.h文件。下面就是这样的一种情况:
1 // ===========test.h============
2 // 定义一个变量
3 int i;
4
5 // ===========test1.h===========
6 // 包含了test.h文件
7 #include "test.h"
8
9 // ===========main.cpp=========
10 // 这里同时包含了test.h和test1.h,
11 // 也就是说同时定义了两个变量i。
12 // 将发生编译错误。
13 #include "stdafx.h"
14 #include "test.h"
15 #include "test1.h"
16
17 void foo();
18 void foo();
19
20 int _tmain(int argc, _TCHAR* argv[])
21 {
22 return 0;
23 }
上面的代码展开后就相当于同时在main.cpp中定义了两个变量i。因此将发生编译错误。解决办法是使用#ifndef或者#pragma once宏,使得test.h只能在main.cpp中被包含一次。关于#ifndef和#pragma once请参考这里。
3):还要注意一点的是,include文件是按照定义顺序被展开到cpp文件中的。关于这个,请看下面的示例。
1 // ===========test.h============
2 // 声明一个函数。注意后面没有分号。
3 void foo()
4
5 // ===========test1.h===========
6 // 仅写了一个分号。
7 ;
8
9 // ===========main.cpp=========
10 // 注意,这里按照test.h和test1.h的顺序包含了头文件。
11 #include "stdafx.h"
12 #include "test.h"
13 #include "test1.h"
14
15 int _tmain(int argc, _TCHAR* argv[])
16 {
17 return 0;
18 }
如果单独看上面的代码中,test.h后面需要一个分号才能编译通过。而test1.h中定义的分号刚好能够补上test.h后面差的那个分号。因此,安这样的顺序定义在main.cpp中后都能正常的编译通过。虽然在实际项目中并不推荐这样做,但这个例子能够说明很多关于文件包含的内容。
有的人也许看见了,上面的示例中虽然声明了一个函数,但没有实现且仍然能通过编译。这就是下面cpp文件编译时的内容了。
2.CPP文件的编译和链接。
大家都知道,C++的编译实际上分为编译和链接两个阶段,由于这两个阶段联系紧密。因此放在一起来说明。在编译的时候,编译器会为每个cpp文件生成一个obj文件。obj文件拥有PE[Portable Executable,即windows可执行文件]文件格式,并且本身包含的就已经是二进制码,但是,不一定能够执行,因为并不保证其中一定有main函数。当所有的cpp文件都编译好了之后将会根据需要,将obj文件链接成为一个exe文件(或者其它形式的库)。看下面的代码:
1 // ============test.h===============
2 // 声明一个函数。
3 void foo();
4
5 // ============test.cpp=============
6 #include "stdafx.h"
7 #include <iostream>
8 #include "test.h"
9
10 // 实现test.h中定义的函数。
11 void foo()
12 {
13 std::cout<<"foo function in test has been called."<<std::endl;
14 }
15
16 // ============main.cpp============
17 #include "stdafx.h"
18 #include "test.h"
19
20 int _tmain(int argc, _TCHAR* argv[])
21 {
22 foo();
23
24 return 0;
25 }
注意到22行对foo函数进行了调用。上面的代码的实际操作过程是编译器首先为每个cpp文件生成了一个obj,这里是test.obj和main.obj(还有一个stdafx.obj,这是由于使用了VS编辑器)。但这里有个问题,虽然test.h对main.cpp是可见的(main.cpp包含了test.h),但是test.cpp对main.cpp并不可见,那么main.cpp是如何找到foo函数的实现的呢?实际上,在单独编译main.cpp文件的时候编译器并不先去关注foo函数是否已经实现,或者在哪里实现。它只是把它看作一个外部的链接类型,认为foo函数的实现应该在另外的一个obj文件中。在22行调用foo的时候,编译器仅仅使用了一个地址跳转,即jump 0x23423之类的东西。但是由于并不知道foo具体存在于哪个地方,因此只是在jump后面填入了一个假的地址(具体应该是什么还请高手指教)。然后就继续编译下面的代码。当所有的cpp文件都执行完了之后就进入链接阶段。由于.obj和.exe的格式都是一样的,在这样的文件中有一个符号导入表和符号导出表[import table和export table]其中将所有符号和它们的地址关联起来。这样连接器只要在test.obj的符号导出表中寻找符号foo[当然C++对foo作了mapping]的 地址就行了,然后作一些偏移量处理后[因为是将两个.obj文件合并,当然地址会有一定的偏移,这个连接器清楚]写入main.obj中的符号导入表中foo所占有的那一项。这样foo就能被成功的执行了。
简要的说来,编译main.cpp时,编译器不知道f的实现,所有当碰到对它的调用时只是给出一个指示,指示连接器应该为它寻找f的实现体。这也就是说main.obj中没有关于f的任何一行二进制代码。编译test.cpp时,编译器找到了f的实现。于是乎foo的实现[二进制代码]出现在test.obj里。连接时,连接器在test.obj中找到foo的实现代码[二进制]的地址[通过符号导出表]。然后将main.obj中悬而未决的jump XXX地址改成foo实际的地址。
现在做个假设,foo()的实现并不真正存在会怎么样?先看下面的代码:
1 #include "stdafx.h"
2 //#include "test.h"
3
4 void foo();
5
6 int _tmain(int argc, _TCHAR* argv[])
7 {
8 foo();
9
10 return 0;
11 }
注意上面的代码,我们把#include "test.h"注释掉了,重新声明了一个foo函数。当然也可以直接使用test.h中的函数声明。上面的代码由于没有函数实现。按照我们上面的分析,编译器在发现foo()的调用的时候并不会报告错误,而是期待连接器会在其它的obj文件中找到foo的实现。但是,连接器最终还是没有找到。于是会报告一个链接错误。
LINK : 没有找到 E:\CPP\CPPTemplate\Debug\CPPTemplate.exe 或上一个增量链接没有生成它;
再看下面的一个例子:
1 #include "stdafx.h"
2 //#include "test.h"
3
4 void foo();
5
6 int _tmain(int argc, _TCHAR* argv[])
7 {
8 // foo();
9
10 return 0;
11 }
这里只有foo的声明,我们把原来的foo的调用也去掉了。上面的代码能编译通过。原因就是由于没有调用foo函数,main.cpp没有真正的去找foo的实现(main.obj内部或者main.obj外部),编译器也就不会在意foo是不是已经实现了。
二:模板的编译过程。
在明白了C++程序的编译过程后再来看模板的编译过程。大家知道,模板需要被模板参数实例化成为一个具体的类或者函数才能使用。但是,类模板成员函数的调用且有一个很重要的特征,那就是成员函数只有在被调用的时候才会被初始化。正是由于这个特征,使得类模板的代码不能按照常规的C++类一样来组织。先看下面的代码:
1 // =========testTemplate.h=============
2 template<typename T>
3 class MyClass{
4 public:
5 void printValue(T
补充:软件开发 , C++ ,