分文件编写原理&&类模板的所有内容只能在一个 .h文件中编写的原因

***C++的类模板的声明和定义都只能在一个 .h文件中编写***

背景知识:

建议参考这篇博客http://t.csdnimg.cn/vB2Akicon-default.png?t=N7T8http://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/stC2Vicon-default.png?t=N7T8http://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及以后的版本中,也可以使用新的模板内联定义方式来分离模板的声明和实现。

  • 26
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值