前言
今天一个老同学帮人做毕设,不过他不熟悉C++,所以来问我,是个简单的二叉树遍历程序。源代码也有,按理说没有任何的难度,整理一下应该就能编译通过了。但是在处理模板类的时候,我按照传统的h声明、cpp实现的方式写了,一编译竟然是链接错误。百度之后发现,C++的类模板并不能像传统的类那样h声明、cpp实现。
模板到底是什么
模板是C++的编译时多态性,这个在学校和面试的时候都已经被问烂了,但是这个编译时多态性到底意味着什么呢?先看看C++源代码是到底是怎么转化为可执行程序的吧。
编译
概括来说编译器会将每个cpp文件翻译成为机器可以识别的机器码,这个过程就是编译。对于C++编译器来说,编译的主体就是cpp文件,每个cpp生成一个目标文件,对于windows来说就是obj,对于linux来说就是o。这些目标文件具有一定的结构,可以让链接器方便的找到各个功能模块(函数)。
首先是预编译,编译器会把所有的预编译命令按照规则进行操作。如#include这个指令就是将include的文件替换到include这个指令的位置,msdn中对include这一指令是这样描述的
#include "path-spec" #include <path-spec>
两种语法形式都会导致指令被替换为指定包含文件的整个内容。 两种形式之间的区别在于,在未完全指定路径时预处理器搜索标头文件的顺序。 下表显示了这两种语法形式之间的差异。
也因此重复的包含可能会造成重定义,这个使用现代编译器的#pragma once就可以处理。
链接
然后链接器负责把这些obj和o组合起来,链接成为一个可执行程序。这时链接器需要按照各个目标文件中的功能(函数)调用,把分布在各个obj中的函数整合起来,链接器所使用的函数名称就是各个函数的修饰名,不同编译器不同平台的函数修饰名不同。
编译时多态性
模板的多态是编译时就决定了的,也就是在编译阶段,预编译之后所有的cpp被整合成为一个完整的文件,这里的然后根据该文件中的所有模板调用,为每种模板分别编译生成对应的机器码,例如
template <class T>
class A
{
...
T a;
void replace(T t);
...
};
...
A<int> a;
A<char> b;
A<double> c;
这段代码编译器会编译出来三个类,它们的修饰名不同。可以理解为A<int>、A<char>、A<double>类型只是具有相似结构,但是在程序中是独立存在互不相干的三个类。但是这样的类对于文件来说是独立的,也就是说可能A.cpp编译出的obj有这三个类,但是B.cpp编译出来的是完全没有这三个类的。想想如果把replace函数的实现放在单独的cpp中而这些声明放在h文件中会发生什么吧。编译器将h文件中的声明替换到各个include它的地方,然后编译各个cpp文件,假设有如下几个文件
A.h
template <class T>
class A
{
T a;
void replace(T t);
};
A.cpp
#include "A.h"
template <class T>
void A<T>::replace(T t)
{
a = t;
}
main.cpp
#include "A.h"
int main()
{
A<char> a;
A<int> b;
return 0;
}
这样的代码会报数个链接错误,对于A.obj来说,它里面并没有包含任何的replace函数的代码,因为它没有实现任何一个类,即是说模板类本身必须被使用才会被实现。而main.obj里更是没有对replace的实现,因为main.cpp本身就不包含任何replace的代码。链接器执行链接的时候,当然会找不到任何
void A<char>::replace(char t)
和
void A<int>::replace(int t)
的实现。想要解决也会有两个方案,一个是让replace的实现出现在main.obj中,可以将replace的定义放在A.h中或者main.cpp中,或者是让replace出现在A.obj中,在A.cpp中声明两个类的实现
template class A<char>;
template class A<int>;
或者在A.cpp中使用到实现出来的类。
到此关于模板类的问题解决了。