首先我们需要知道程序运行起来需要四个阶段:
1、预处理(进行宏替换,去注释,条件编译,加行号,头文件展开等一系列工作)
2、编译(把进行完预处理之后的程序进行翻译转换成汇编代码)
3、汇编(把对应的汇编代码转换成机器能够识别的二进制序列)
4、链接(把一个工程中所有的obj文件链接起来变成一个可执行的exe的程序)
接下来我们分析模板的分离编译会产生什么样的结果:
首先我们在工程里建立一个tem.h的头文件:
#include<iostream>
#include<stdlib.h>
using namespace std;
template<class T>
void FunTest(T a);
再建立一个tem.cpp的.文件
#include"tem.h"
template<class T>
void FunTest(T a)
{
a = 10;
}
在建立一个test.cpp的文件
#include"tem.h"
int main()
{
FunTest(10);
system("pause");
return 0;
}
运行这个程序它会报一个链接型的错误
分析为什么会出现链接型的错误
1、我们在tem.cpp文件里定义了这个模板函数,但是并没有对这个模板函数进行实例化,因此在tem.cpp里面并没有生成对应的代码。
2、我们需要知道根据C++标准我们可以知道每一个cpp文件都是一个编译单元,并且每个编译单元是相互独立的并且互相是不可见的,当编译器将所有的cpp文件分离编译完成之后才会使用链接器将这些obj文件链接在一起生成可执行的exe文件。
3、在test.cpp文件里我们在编译的时候只能看到tem.h这个头文件里面FunTest()这个函数的声明,因此编译器将这个FunTested()函数看成外部链接类型的(也就是说这个函数的具体实现是在另外一个.cpp文件中),在对它进行编译之后生成的test.obj文件中会看到对FunTest()函数调用是生成了调用指令 call [FunTest(int)…]
在编译的时候显然在test.obi里面是找不到的,因为在test.obj里是没有这个函数的实现体的,那么它通过连接器就要到别的obj里面去查找,然后在其他obj中找到这个函数实现的代码然后将它的地址去替换test.obj中call指令中的虚假地址。链接器将所有的obj文件链接起来生成exe文件。
但是在我们的tem.cpp文件中是没有FunTest()这个函数的实际代码的,因为没有实例化生成对应的代码。因此在链接过程中就会出错。
解决方案1:在tem.cpp文件里生成与test.cpp中类型对应的代码,也就是在tem.cpp文件里将函数模板实例化成与test.cpp一致类型
#include"tem.h"
template<class T>
void FunTest(T a)
{
a = 10;
}
void FunTest1()
{
FunTest(10);
}
解决方案2:使用.hpp文件将声明和定义封装在一起
将tem.cpp修改成tem.hpp
再简单剖析以下编译-链接
obj文件里不仅有自己的代码段和数据段和一些头信息之外,还有三个表(未解决符号表、导出符号表、地址重定向表)
未解决的符号表:里面是在本cpp文件里使用的符号,但是在本cpp文件里只能看到它的声明,因为该符号定义不在本cpp文件中。(也就是说这个未解决的符号表会高数编译器在这个编译单元偏移量为xxx处有一个符号x,但是该符号的地址不明确)
导出的符号表:该符号x在本cpp文件里面有明确的定义并且可以提供给其他编译单元这个符号,以及这个符号在本单元地址)
地址重定向表:为了解决地址冲突的问题(因为每个编译单元的地址都是从0x00000000开始的,那么链接器将多个obj文件链接在一起时地址可能会重复导致发生冲突)为了解决这种冲突我们加上它在相对于可执行程序exe文件中的偏移量也就是该编译单元相对可执行程序exe的位置)