模板的分离式编译
分离式编译是指一个完整的程序或项目由若干个源文件共同实现,每个源文件单独编译生成目标文件,最后将该项目中的所有目标文件连接成一个单一的可执行文件的过程。
每个.cpp源文件经过预处理,它所包含的.h文件的代码都会被展开到其中。再经过编译器的编译汇编等过程,将该.cpp文件转变为.obj文件,这是此文件已经变为二进制文件,本身包含的就是二进制代码。这时,该文件还不一定能够执行,因为并不保证其中一定有main函数,或者该源文件中的函数可能引用了另一个源文件中定义的某个变量或者函数调用,又或者在程序中可能调用了某个库文件中的函数,等等。这些都要通过链接器将该项目中的所有目标文件连接成一个单一的可执行文件来解决。
举个例子:
//****************************** Test.h *************************************/ #pragma once #include<iostream> using namespace std; #include<stdlib.h> void fun(); // 仅仅只声明 //****************************** Test.h *************************************/
//****************************** Test.cpp *************************************/ #include"Test.h" void fun() // 对函数fun()进行定义 { cout << "fun" << endl; } //****************************** Test.cpp *************************************/
//****************************** main.cpp *************************************/ #include"Test.h" int main() { fun(); //调用fun()函数 system("pause"); return 0; } //****************************** Test.cpp *************************************/
main.cpp包含的Test.h头文件只有对fun()函数的声明,所以在main.cpp中没有任何与fun()函数定义相关的代码,这里就会把fun函数看做为外部链接类型。在主函数中调用fun函数时,会产生如上图的红框中的call命令,当然这里的地址是一个虚假的地址。链接器在Test.obj文件中找到fun函数的实现代码,将call fun地址通过jmp指令切换成真正的fun函数地址。
再看模板的分离编译:
///************************** SeqList.h **************************// #include<iostream> using namespace std; #include<assert.h> // 定义模板类 (类型参数为T) template<typename T> class SeqList { public: SeqList(); //构造函数声明 private: T* _array; size_t _size; };
///************************** SeqList.cpp **************************// #include"SeqList.h" template<typename T> //模板声明 SeqList<T>::SeqList() //在类模板体外定义构造函数 :_array(NULL) , _size(0) {}
///************************** Test.cpp **************************// #include"SeqList.h" int main() { SeqList<int> list1; system("pause"); return 0; }
编译链接会出以下错误:
1>------ 已启动生成: 项目: 4_4, 配置: Debug Win32 ------
1> Test.cpp
1> SeqList.cpp
1> 正在生成代码...
1>Test.obj : error LNK2019: 无法解析的外部符号 "public: __thiscall SeqList<int>::SeqList<int>(void)" (??0?$SeqList@H@@QAE@XZ),该符号在函数 "void __cdecl Test(void)" (?Test@@YAXXZ) 中被引用
1>E:\CODE\4_4\Debug\4_4.exe : fatal error LNK1120: 1 个无法解析的外部命令
========== 生成: 成功 0 个,失败 1 个,最新 0 个,跳过 0 个 ==========
这是因为编译时SeqList<T>没有实例化出SeqList<int>实例,所以链接时出错。
到底是怎么回事呢?
对于模板来说,模板只有被调用的时候才会被实例化,如果不被调用它是不会实例化的。因此,如果你不调用某模板函数或模板类时,该模板中的许多语法错误编译器是懒得检查的。
在执行主函数中的SeqList<int> list1语句时,要调用SeqList<int>的构造函数,链接器在Test.obj中找不到此构造函数的定义,于是在SeqList.obj文件中找。大家如果认为SeqList<int>的构造函数的实现代码就在SeqList.obj中,那就错了。模板只有被调用的时候才会被实例化! SeqList.cpp中没有任何对此构造函数的调用,因此SeqList.obj中是没有任何有关SeqList<int>构造函数实现的二进制代码(因为它没有被实例化),链接器找不到函数实现的二进制代码,因此只能报错。
如果我们在SeqList.cpp中添加对SeqList<int>构造函数的调用,编译器就会将构造函数实例化,产生相应的二进制代码,这时候链接器就会在SeqList.obj文件中找此构造函数的定义。程序编译就不会出错了。
当然,我们也可以将声明和定义放到一个文件 "xxx.hpp" 里面,来解决这类问题。
附: