模板为甚么不能分离编译,但普通函数却可以?
- 一、前置知识
- 二、普通函数能分离编译的原因
- 三、模板不能分离编译的原因
一、前置知识
编译阶段:
- 源代码到目标代码:
编译器首先将源代码(如C/C++文件)翻译成汇编语言,然后进一步翻译成目标代码(通常是机器代码1或0),这些目标代码存储在可重定位的目标文件(.o文件或.obj文件)中。
在这个过程中,编译器会处理函数和变量的声明,并生成对它们的引用(即符号)。然而,此时编译器并不直接分配最终的内存地址给这些符号。 - 符号表
编译器会为每个目标文件生成一个符号表,该表列出了文件中所有外部可见的函数和变量(即那些在文件外部被声明或引用的符号)。
这些符号在符号表中以未定义(UND)或已定义(DEF)的状态出现,取决于它们是在当前文件中定义的还是仅在当前文件中被引用。
链接阶段:
- 符号解析:
链接器的主要任务之一是解析这些符号。它遍历所有目标文件的符号表,并尝试匹配每个未定义的符号(即引用)到一个已定义的符号(即定义)。
如果链接器成功找到所有符号的定义,它将为这些符号分配最终的内存地址。这些地址是程序在运行时实际使用的地址。 - 重定位:
在分配了地址之后,链接器会修改目标代码中的引用,以反映这些符号的最终地址。这个过程称为重定位。
通过重定位,链接器确保程序中的每个函数调用和变量访问都指向正确的内存位置。
二、普通函数能分离编译的原因
以下方例子为例:
test.cpp会先进行编译:
先将头文件func.h展开,找到了Add(int ,int)函数的声明 ,然后生成Add函数的符号,编译后生成test.obj文件。
再到func.cpp编译:
先将头文件func.h展开,找到了Add(int ,int)函数的声明 ,然后生成Add函数的符号并对Add函数的定义进行编译,编译后生成func.obj文件。
最后在链接阶段时,链接器会根据两个.obj文件的符号表,发现func.obj文件中有与test.obj文件中对应的Add函数的符号,这时链接器就回为该函数分配地址,运行时就可根据该地址实现特定的功能。
三、模板不能分离编译的原因
如下模板分离编译例子:
报错:
可以看到,这里是报了链接错误。
原因需要结合模板实例化机制说明:
- 模板本身并不生成代码,只有当模板被实例化时,编译器才会根据模板定义生成相应的代码。这意味着在编译过程中,如果模板的定义不可见(例如,当模板的声明与定义分离,并且定义在另一个文件中时),编译器在实例化模板时可能找不到对应的模板定义,从而导致编译或链接错误。
- 符号表记录了程序中所有符号(如函数名、变量名等)的信息,包括它们的类型、作用域等。对于模板来说,如果模板在某个源文件中被实例化,那么实例化后的函数或对象将会出现在该源文件的符号表中。但是,如果模板的声明与定义分离,且定义在另一个文件中,而这个文件在链接前没有被正确编译或包含,那么实例化后的符号可能就不会出现在最终的符号表中,从而导致链接错误。
因此,当test.cpp编译时,虽然展开.h文件找到了Add函数的声明,但是由于定义不再.h文件中,即使明确了T的类型,也无法实例化Add函数。而对于func.cpp,由于它编译时并不确定Add( T x,T y) 函数的T的类型,因此也无法实例化Add函数,因此Add函数就没有被定义。在链接过程中,因为找不到Add的定义,因此报链接错误。