模板编译过程

来源:互联网 发布:centos cp 复制文件夹 编辑:程序博客网 时间:2024/05/17 15:40
首先说一下c++编译的过程:

1. 预编译

将文件中有#...的命令都进行展开,例如define MAX 100,就是建立起MAX与100间的对待关系,好在编译阶段进行堆的。而include就是就是把include的文件全部复制到当前文件中,如果当前文件是cpp文件,一个cpp文件就形成一个编译单元。

这里有几点需要注意的地方:(1)如果一个h文件没有被任何cpp文件所include,那么它将不会被编译,即,即使这个.h文件有错误也不会被发现;(2)如果一个cpp文件直接或者间接地包含了同一个.h文件,比如说,main.cpp包含了a.h和b.h,而a.h又包含了b.h,这样b.h中的定义的变量或者函数就会被视为重复定义,出现编译错误(注意此处说的是定义,如果是函数声明则没有关系),要防止这种情况可以使用#ifndef 或者#progma once。(3)include的文件是顺序加入到当前文件中的,比如说main.cpp包含a.h和b.h,a.h中有语句,int i,b.h中有语句;,则当前cpp文件仍然能正常编译。

2。编译

c++的编译公为编译和链接两个步骤。在编译的时候编译器会为每一个编译单元生成.obj文件,这个obj文件本身已经包含PE(Portable Executable,即windows可执行文件),而且本身也是二进制代码,但是却并不一定可以执行,因为不一定包含main函数。当所有的cpp文件都编译完成后,编译器会将这些obj文件都进行链接,生成exe文件。

这里也有需要注意的地方:

会存在main.cpp调用一个函数f,而这个f的定义却未include在这个cpp的情况,比如说f的声明在a.h,而实现在a.cpp中,main.cpp只包含了a.h;但是通过平时的实践我们知道这是可以通过编译的,原因是,当main.cpp中调用不在当前编译单元的函数f时,编译器不会管f是否已经实现,而是把它当成一个外部链接类型,认为在外部obj文件中将会找到它的定义。其处理是在此外加上一个jump ###的语句,此处###是一个任意的地址,这个地址上有CALL ###指令,然后继续编译接下来的内容。

当所有的编译单元都编译完成后,编译器会为这些编译单元生成符号导入导出表, 这样b.cpp中会形成f函数与一个地址的映射,所以在链接阶段,编译器会找到这个f函数对应的这个地址,根据这个地址增加一定的偏移量,将当初jump ###部分的CALL的地址进行修改,然后生成exe文件。

如果f这个函数并不存在的话,那么链接的时候会因为找不到f的真实地址而报错。当然,这里还有一个问题,就是如果f函数并没有被调用,那么虽然f函数只有声明而没有定义,也是不会出错的,因为按上面所说的,是在调用的时候给其加上jump地址。

接下来说一下文章的重点,也就是模板的编译过程

首先我们知道模板需要模板参数化成为具体的类或者函数后才能使用。单纯模板的编译并不产生二进制代码。另外,模板成员函数只有在调用的时候才会被初始化。

先考虑两种情况:两个文件,main.cpp, A.h,其中a.h中有模板类A的声明,但没有定义。第一种情况,main.cpp中没有出现A,很明显编译通过没有问题;第二种情况,main.cpp中实例化了一个A的对象,在这种情况下编译也没有问题,为什么?因为A的声明可以通过inclucde的a.h来找到,所以没有问题。

考虑第三种情况:如果main.cpp中调用了A的成员函数f<int>,这个时候,链接就会出现错误,这个是肯定的,根据上面说的普通函数编译链接的说明也能发现这一点。

第四情况:除了main.cpp和a.h还有文件a.cpp,而且a.cpp的内容是A的成员函数f<T>的定义。main.cpp包含了a.h但是没有包含a.cpp,而且main.cpp中调用了A的成员函数f<int>,这时编译会发现出现与第三种情况一样的链接错误。这是为什么?原因在于上面说到的,模板的编译并不产生二进制代码,成员函数只有在调用的时候才会初始化,因为它需要知道模板参数,即一个类型,如int,这样,对于a.cpp的编译,因为没有任何成员函数被调用,而并没有实例化成员函数f<int>,即并没有首先f<int>的二进制代码;而对于main.cpp的编译,因为只包含了a.h,所以当调用f<int>的时候,会因为没有在文件内部找到实现而做一些标记,就是上面的提到的jump###的形式,等待链接的时候去其它外部文件中找到f<int>的定义。然而我们知道,并没有其它的文件对f<int>进行了定义,所以在链接的时候会找到不f<int>而报错。

那继续考虑第五种情况,如果在a.cpp中有f<int>的函数调用,那么编译就没有问题,因为在编译a.cpp的时候,因为f<int>的调用,产生了f<int>的二进制代码(具备两个条件,f<T>的定义和f<int>的调用)



所以,模板代码得保证使用模板的函数在编译的时候就能够找到模板代码,从而实例化模板,关于解决方法可以会了包含编译和分离编译

包含编译
(1)在实例化要素中让编译器看到模板的定义。比如可以在a.h中用行内函数定义所有的成员函数,或者可以直接在a.h的末尾加上#include"a.cpp",再或者可以在main.cpp中也包含a.cpp文件(编译器和链接器对模板的代码反复进行了处理),这样做的坏处是使得编译文件变得很大,会降低编译和链接速度(这里不太理解,正常情况下不也是把文件都include到当前文件吗?)。

(2)用另外的文件来地实例化类型,使得链接器可以看到该类型。比如可以在新文件b.cpp中#include"a.cpp" template class A<int>;这样做不会产生很大的头文件,但是它显式地生成所有的成员函数,这和只在成员函数被调用时才对其进行实例化是有冲突的。

分离模式:
使用export关键字

参考资料:

http://blog.csdn.net/look01/archive/2008/11/05/3228134.aspx
http://www.cnblogs.com/zhaoxb1982/archive/2009/08/07/1540713.html
http://blog.csdn.net/FlowShell/archive/2010/11/10/5999588.aspx
http://www.cnblogs.com/xgchang/archive/2004/11/12/63139.aspx
http://blog.163.com/feng_qihang/blog/static/71291991200951010557598/
原创粉丝点击