为了实现泛型编程(即与类型无关的代码)在c++中引入了模板这一概念,在写一个项目的时侯我们通常将一个项目分为.h文件(写函数的声明),.cpp文件(函数的定义实现),.cpp(测试文件)。通常情况下普通函数这样是没问题的,但是模板这样写是不行的。
让我们看下会发生什么样的情况:
#ifndef __TEMPLATE_H__
#define __TEMPLATE_H__
#include <iostream>
using namespace std;
template<typename T>
void TestTemplate(T x);
#endif
#include "TemplateCompile.h"
template<typename T>
void TestTemplate(T x)
{
cout << x << endl;
}
#include "TemplateCompile.h"
int main()
{
int x = 10;
TestTemplate<int>(x);
system("pause");
return 0;
}
错误 1 error LNK2019: 无法解析的外部符号 "void __cdecl TestTemplate<int>(int)" (??$TestTemplate@H@@YAXH@Z),该符号在函数 _main 中被引用 F:\刷题\bolg\bolg\test.obj bolg
很显然这是一个链接性质的错误,那么他是怎么样产生的呢?
我们需要明白一个项目到一个可执行的文件编译器做了什么事情,为什么普通函数这样实现是可以的,而模板为什么会发生错误?
第一个项目到达可执行文件发生了什么:
链接过程呢实在符号表中找到函数名然后将函数名和实现它的地址对应起来,所以在链接时出了错,而一般出现出现链接错误是因为声明了一个函数,但是没有实现它。因此,当程序在链接时,从符号表中只找到了函数名,找不到具体函数实现的地址,因此编译器会报这样的错误。
那么,为什么模板在分离编译时会报这样的错误呢?
我们知道对比于普通函数模板需要两次编译,第一次编译是在实例化之前,用来检查基本的语法错误。第二次编译是在实例化之后,当把它实例化具体的类型时,再次判断有没有语法错误。即模板只有在使用时才会具体实例化出执行的代码。
模板代码的实现在实现文件里,而实例化的测试代码在测试文件里,编译器编译时并不知道它们是分开的,也就是编译实现文件时并不知道实例化代码在测试文件里,就没有实例化出真正的代码,因此才会报出这样的错误。
不是很明白的话接着看下面的东西:
/---------------test.h-------------------//
void f();//这里声明一个函数f
//---------------test.cpp--------------//
#include”test.h”
void f()
{
…//do something
} //这里实现出test.h中声明的f函数
//---------------main.cpp--------------//
#include”test.h”
int main()
{
f(); //调用f,f具有外部连接类型
}
这个程序中会生成两个文件:test. cpp和main.cpp各被编译成为不同的.obj文件[姑且命名为test.obj和main.obj], .h 会展开在这两个文件里。
我们都知道程序是从main函数开始走的,也就是说在main.obj中调用f函数,但是在main.obj中只有f函数的声明(.h文件的展开),编译器呢会认为f函数具有外部链接属性,这样呢会在test.obj这个文件中找f函数的实现。
也就是说,main.obj中实际没有关于f函数的哪怕一行二进制代码,而这些代码实际存在于test.cpp所编译成的test.obj中。在main.obj中对f的调用只会生成一行call指
令。call指令后是f函数的地址,也就是说在链接中的链接地址做的就是这件事.
可见:编译main.cpp时,编译器不知道f的实现,所有当碰到对它的调用时只是给出一个指示,指示连接器应该为它寻找f的实现体。这也就是说main.obj中没有关于f的任何一行二进制代码。编译test.cpp时,编译器找到了f的实现。于是乎f的实现[二进制代码]出现在test.obj里 ,连接时,连接器在test.obj中找到f的实现代码[二进制]的地址[通过符号导出表]。然后将main.obj中悬而未决的call XXX地址改成f实际的地址。
对比这个过程,我们来看模板函数的编译:
模板我们都知道,它不会直接编译,会有一个实例化的过程,
//-------------test.h----------------//
template<class T>
class A
{
public:
void f(); //这里只是个声明
};
//---------------test.cpp-------------//
#include”test.h”
template<class T>
void A<T>::f() //模板的实现,但注意:不是具现
{
…//do something
}
//---------------main.cpp---------------//
#include”test.h”
int main()
{
A<int> a;
a. f();
}
我们在mian.obj中走到A::f时,我们无法知道它的定义,因为在test.h里面只有它的声明,于是编译器只好寄希望于连接器,希望它能够在其他.obj里面找A::f的定义,也就是在test.obj中寻找,然而,后者中真有A::f的二进制代码吗?答案是找不到的。因为当一个模板不被用到的时侯它就不会实例化被出来,test.cpp中用到了A::f了吗?没有!实际上test.cpp编译出来的test.obj文件中关于A::f的一行二进制代码也没有。连接器找不到它的实现,只有声明没有实现,只好给出一个链接错误。
解决方案1:(显示实例化)
如果在test.cpp中写一个函数,其中调用A::f,则编译器会将其具现出来,因为在这个点上[test.cpp中],编译器知道模板的定义,所以能够具现化,于是,test.obj的符号导出表中就有了A::f这个符号的地址,于是连接器就能够完成任务。
但这样显然是与我们的初衷是不符合的,使用了模板,然后还要把每个需要的类型实现一遍,显然变得更加麻烦了。
推荐使用
解决方案(2):模板的生命和定义都放在一个.hpp文件中,这样展开在每个包它的.obj
文件中,然后靠编译器去推演。