- 什么是分离编译
- 模板的分离编译
什么是分离编译
一个程序(项目)由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有目标文件链接起来形成单一的可执行文件的过程称为分离编译模式。
模板的分离编译
假如有以下场景,模板的声明与定义分离开,在头文件中进行声明,源文件中完成定义:
//test.h
#include<iostream>
using namespace std;
//函数模板的声明
template<class T>
T Add(const T& left, const T& right);
//test.cpp
#include"test.h"
template<class T>
//函数模板的定义
T Add(const T& left, const T& right)
{
return left + right;
}
//amin.cpp
#include"a.h"
int main()
{
Add(1, 2);
return 0;
}
如果模板进行分离编译会发现程序会出现链接错误,错误提示如下:
正常的函数进行分离编译不会有任何问题,而函数模板进行分离编译却会出现链接错误,这是为什么?
我们都知道一个程序要先经过编辑、编译、链接,才能运行,而上面的模板分离编译的链接错误,不就是链接阶段发生的吗?那让我们一起看看链接之前会发生什么。
我们将一个程序的代码编辑完之后,就会进入编译阶段,而编译阶段又可以细分为预处理、编译、汇编,看看它们分别发生了什么?
预处理:源文件中头文件的展开/宏替换/条件编译/去掉注释。
上面模板的分离编译中会将test.h在test.cpp和main.cpp中展开,那两个源文件中就都有了函数模板的声明,这一步是没有问题的。
编译:进行语法检查、生成汇编代码
在进行语法检查的时候,main.cpp中调用了模板函数,而在它调用之前已经有了函数模板的声明,说明语法没有问题,然后生成对应的汇编语言(要注意,调用函数肯定需要对应的函数地址,但现在只有函数的声明,那它的函数地址就是有待确定的)。而test.cpp中函数的定义在在这一阶段是需要被实例化的,但因为函数模板的模板参数无法确定类型,所以无法将函数模板实例化,也就无法生成对应的函数地址,那符号表中也就无法找到该函数的地址,但语法并没有问题(如果是普通函数,那就可以生成对应的函数地址)。
汇编:将汇编代码转换成机器码。其实只要上面的编译不检查出语法错误,那这一阶段就能进行。
经过以上阶段,就该进行链接了,前面main.cpp编译生成的汇编代码里待查的函数地址就要去符号表中找,但因为函数模板的定义并没有被实例化,符号表中并没有对应的函数地址,那main.cpp中调用的模板函数不就相当于找不到了吗,那就会报链接的错误。
那如何解决呢?
方法一:既然是因为编译阶段test.cpp中函数模板无法确定类型而无法实例化,那我们可以保持模板分离编译不表,然后在test.cpp中显示实例化。加上如下代码:
template
double Add<int>(const int& left,const int& right);
这样虽然解决当前的问题,但如果调用函数时类型变成了double类型,那又得显示实例化,这样太麻烦了。
方法二:如果函数模板的声明和定义要分离也要都放在一个文件中(比如上面就是放在test.h中),那这样编译阶段就函数模板的模板类型可以确定,也就可以生成对应的函数地址。链接时就不会出问题了。很明显方法二更好。