欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

模板不能分离编译及相关问题

程序员文章站 2024-03-26 09:44:47
...

首先我们要知道程序跑起来的过程

  • 预处理:test.c -> test.i(头文件展开、宏替换、去掉注释)
  • 编译:test.i -> test.s (语法检查、生成汇编代码)
  • 汇编:test.s -> test.o (把汇编代码转换成机器码)
  • 链接:test.o ->.exe (生成可执行程序)

1 分离编译

1.1理解概念:

分离编译就是一个程序的由很多个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有的目标文件连接起来形成单一的可执行文件的过程

//声明文件
//test.h
void Swap();//函数声明

//定义文件
//test.cpp
#include "test.h"
void Swap(...)
{
	...
	...
}

//测试文件
//main.cpp
int main()
{
	int a = 1;
	int b = 2;
	Swap(a,b);//函数调用
	return 0;
}

一个编译单元是指一个.cpp文件以及它所#include的所有.h文件,.h文件里面的代码会被扩展到包含它的.cpp文件里面,然后编译器编译该.cpp文件为一个.obj文件(平台是win),当编译器将一个工程里面所有的.cpp文件以分离的方式编译完成后,再由连接器进行连接成为一个.exe文件
在这个例子里面,test.cpp和main.cpp会被编译程不同的.obj文件,(这里就命名为test.obj和main.obj文件),在main.cpp中,调用了Swap函数,然而当编译器编译main.cpp文件时,它只知道main.cpp包含test.h文件中的一个Swap函数的声明,所以编译器将这个的Swap看作是外部连接类型,就是认为它的函数实现是在另外一个.obj文件中,在这里也就是test.obj,也就是说main.obj中实际没有关于Swap函数的一行二进制代码,而这些代码实际存在于test.cpp所编译生成的test.obj中,在main.obj中对Swap的调用只会生成一行call指令:(类似这样的)
call Swap【地址】
连接器的作用:寻找一个外部连接符号在另一个.obj中的地址,替换原来的“虚假”地址
在编译时,这个call指令显然是错误的,因为main.obj中并没有一行Swap实现代码,那么这时候就是连接器的作用,连接器负责在其他的.obj文件中讯在Swap的实现代码,找到以后将call指令后面的调用地址换成实际的f函数的进入点地址。
那么连接器是如何找到的,因为在.obj文件中和.exe文件一样由一个符号导入表和符号导出表,其中将所有符号和它们的地址关联起来,这样连接器只需要在test.obj文件中的符号导出表中寻找符号Swap的地址就可以了,然后做一些偏移处理后(因为将两个.obj文件合并,当然地址会有一定的偏移)写入main.obj中的符号导入表中Swap所占有的那一项。

总结的讲就是:
编译main.cpp时,编译器不知道Swap函数的实现,所以当碰到对它的调用只是给出一个指示,指示连接器应该为它寻找Swap的实现体,这也就是说main.obj中没有关于Swap的任何一行二进制代码

编译test.cpp时,编译器找到了Swap的实现,于是Swap的实现(二进制代码)出现在test.obj中。

连接时,连接器在test.obj中找到Swap的实现代码的地址(通过符号导出表)。然会将main.obj中悬而未决的call XXX地址改成Swap实际的地址。

1.2分离编译优点

1.有错误可以迅速找到
2.实现模块多用
但是当模板分离编译的时候就有问题了

//test.h
template<class T>
void Swap(T& a, T& b);

//test.cpp
#include "test.h"
//template void Swap<int>(int &a, int &b);//显式实例化
template<class T>
void Swap(T& a, T& b)
{
	T temp;
	temp = a;
	a = b;
	b = temp;
}

//main.cpp
#include "test.h"
int main()
{
	int a = 1;
	int b = 2;
	Swap(a, b);
	return 0;
}

当运行程序时:(vs2013)
模板不能分离编译及相关问题
编译器在调用Swap(a,b);的时候并不知道这个函数的定义,因为它不在test.h里面,所以编译器只能靠连接器,希望能够在.obj里面找Swap的实例,在这里就是test.obj,然而,在test.obj中并没有Swap的二进制代码,这里并没有,因为在C++标准明确表示,当一个模板被用到的时候它就不该被实例化出来,test.cpp中没有用到Swap函数,所以test.cpp编译出来的test.obj文件中没有一行关于Swap的二进制代码,所以这时候连接器也没办法,只好报错。如上图。但是这时候并不是没有办法,我们可以在test.cpp中实例化从而使其在.obj文件中生成关于Swap的二进制代码,从而也就是test.obj的符号导出表中就有了福关于Swap的地址。当然也是因为test.cpp中知道模板的定义才能够实例化。

因为模板仅在需要的时候才实例化出来,所以当编译器看到模板的声明时,他不能实例化该模板,只能创建一个具有外部连接的符号并等待连接器能够将符号的地址填进来。

2 实例化

C++只有模板显式实例化、隐式实例化、特化

2.1隐式实例化

int main()
{
	...
	Swap<int>(a,b);
	...
}

它会在运行到这里的时候才生成相应的实例,很显然影响效率

2.2 显式实例化

前面提到隐式实例化可能影响效率,所以需要提高效率的显式实例化,显式实例化在编译期间就会生成实例,方法如下:

template void Swap<int>(int& a,int& b);

这样就不会影响运行时的效率,但编译时间会随之增加。但是这仅限于是在Vs中,因为在linux操作系统下会产生这样的错误:
main.cpp:(.text+0x25):对‘void Swap(int&, int&)’未定义的引用
collect2: 错误:ld 返回 1

3 特化

这个Swap可以处理一些基本内置类型如int、long等等,但是如果象处理用户自定义的就不行了,特化就是为了解决这个问题而出现的:

template <> void Swap<job>(job a,job b){...}

其中job就是用户自定义的类型