***C++的类模板的声明和定义都只能在一个 .h文件中编写***
背景知识:
建议参考这篇博客:http://t.csdnimg.cn/vB2Akhttp://t.csdnimg.cn/vB2Ak
分文件编写也叫分离式编译:
一个编译单元(translation unit)是指一个.cpp文件以及它所#include的所有.h文件,.h文件里的代码将会被扩展到包含它的.cpp文件里,然后编译器编译该.cpp文件为一个.obj文件,当编译器将一个工程里的所有.cpp文件以分离的方式编译完毕后,再由连接器(linker)进行连接成为一个.exe文件。
分文件编写的编译完整过程:
//test.h声明一个函数fun()
#pragma once
void fun();
//test.cpp 写函数fun的定义(实现)
#include"test.h"
#include<iostream>
void fun() {
std::cout << "test fun";
}
// main.cpp 含有main()--程序的入口
#include"test.h"
int main(){
fun();
}
test. cpp和main.cpp各自被编译成不同的.obj文件(test.obj和main.obj)在main.cpp中,调用了f函数,然而当编译器编译main.cpp时,它只知道的只是main.cpp中所包含的test.h文件中的一个关于函数void fun();的声明,所以,编译器将这里的f看作外部连接类型(在编译时确定全局变量和函数的地址,并将它们链接到程序中),认为它的函数实现代码在另一个.obj文件中,在main.obj对函数fun的调用只会生成一行call指令:call fun (本质是用存根”(stub)或“跳转指令”(jump instruction)来代替直接的函数调用)
链接器负责在其它的.obj中(本例为test.obj)寻找fun的实现代码,找到以后将call fun这个指令的调用地址换成实际的f的函数进入点地址。
链接器是如何找到f的实际地址:
.obj和.exe文件(都是二进制文件)中有一个符号导入表和符号导出表(import table和export table)其中将所有符号和它们的地址关联起来。这样连接器只要在test.obj的符号导出表中寻找符号fun()的地址
符号导入表和符号导出表参考博客:http://t.csdnimg.cn/stC2Vhttp://t.csdnimg.cn/stC2V
编译main.cpp时,碰到对fun()的调用时只是给出一个指示,指示连接器应该为它寻找f的实现体。
编译test.cpp时,编译器才找到了fun()的实现。
链接时,链接器在test.obj中找到fun()的实现代码(二进制)的地址(通过符号导出表)。然后将main.obj中的fun()函数的调用地址换成test.cpp中实际的fun()函数进入完成。
模板定义很特殊。
由template<>
处理的任何东西都意味着编译器在当时不为它分配存储空间,它一直处于等待状态直到被一个模板实例告知。在编译器和链接器的某一处,有一机制能去掉指定模板的多重定义。所以为了容易使用,几乎总是在头文件中放置全部的模板声明和定义。
C++中只有.cpp文件才会被编译器直接编译,如果类模板在.cpp文件中,编译器不知道参数T 是什么类型,编译器就不知道怎么处理这样的数据,就会报错
那我们来看看分离编写类模板有什么后果(模板类的类声明放在.h文件中,类实现放在.cpp文件)
// test.h 声明一个模板类
#pragma once
template<typename T>
class test {
private:
T val;
public:
test(T element);
T getval();
};
// test.cpp 定义模板类的成员函数
#include"test.h"
template<typename T>
test<T>::test(T element) {
val = element;
}
template<typename T>
T test<T>::getval() {
return val;
}
// main.cpp
#include"test.h"
#include<iostream>
int main()
{
test<int> ts(2);
std::cout << ts.getval();
}
错误 LNK2019 无法解析的外部符号 "public: __cdecl test<int>::test<int>(int)" (??0?$test@H@@QEAA@H@Z),函数 main 中引用了该符号
如果模板的定义(实现)没有放在头文件中,而是放在了源文件中,那么当其他源文件包含这个头文件并尝试使用模板时,它们将无法找到模板的具体实现。这会导致编译器生成不完整的实例化代码,因为没有具体的实现可供编译器参考。
在链接过程中,链接器会尝试将所有编译单元中的代码和符号引用组合起来。如果链接器在任何编译单元中找到了模板实例的引用,但它无法找到该实例的定义,那么链接器将报告链接错误,因为模板实例的定义是必需的,以便链接器可以解析这些引用。
所以为了避免这种情况,模板类的实现通常也放在头文件中。这样,每个包含该头文件的源文件都会获得模板的定义,编译器可以在每个编译单元中生成模板的实例化代码。这确保了链接器在链接过程中可以找到所有必要的模板实例化定义。
问题1:由于模板需要在每个编译单元(单个.cpp文件)中实例化,编译器需要访问模板的完整定义。模板的完整定义是由模板的声明与实现还有模板的实例构成。如果模板的实现分散在多个文件中,可能会导致编译器无法找到模板的完整定义,从而引发编译错误。
问题2:编译器在链接阶段会检查模板实例的唯一性。如果模板的声明和实现分开放置,可能会导致链接时找不到模板的实现,或者在不同的编译单元中生成了多个模板实例。
PS:
在C++17及以后的版本中,引入了一种新的模板内联变量和函数的定义方式,这允许将模板的定义放在源文件中,同时仍然保持模板声明在头文件中。这种方式允许编译器在编译时进行优化,同时确保链接器可以找到所有必要的模板定义。这通过在源文件中使用inline
关键字和模板变量或函数的内联定义来实现。
总之,对于模板类和模板函数,将实现放在头文件中是标准做法,以确保编译器可以生成完整的模板实例化代码,并确保链接器可以找到所有必要的模板定义。在C++17及以后的版本中,也可以使用新的模板内联定义方式来分离模板的声明和实现。