c++模板声明和定义编译过程的分析

先把代码贴上来,这是c++ primer第4版习题16.17

首先,模板的声明和定义分开

<span style="font-size:18px;">//median.h
#ifndef __MEDIAN_H__
#define __MEDIAN_H__

#include <vector>
#include <algorithm>
using namespace::std;

template <typename T>
bool median(const vector<T>&, T&);

//#include "median.cpp"

#endif</span>

<span style="font-size:18px;">//median.cpp
#include "median.h"
#include <vector>
#include <iostream>
using namespace::std;

template <typename T>
bool median(const vector<T>& vec, T& middle){
	vector<T> temp(vec);
	if(temp.size() % 2 == 0)
		return false;
	sort(temp.begin(), temp.end());
	typename vector<T>::size_type index =  temp.size() / 2;
	if((temp[index] > temp[index - 1]) && (temp[index] < temp[index + 1])){
		middle = temp[index];
		return true;
	}
	else
		return false;
}</span>

接下来是简单的测试用例

<span style="font-size:18px;">#include "median.h"
#include <vector>
#include <iostream>
using namespace::std;

int main()
{
    int ia1[] = {1, 2, 3, 4, 5, 6, 7};
    int ia2[] = {1, 2, 3, 4};
    int ia3[] = {1, 2, 3, 4, 5, 6};
    vector<int> ivec1(ia1, ia1+7);
    vector<int> ivec2(ia2, ia2+4);
    vector<int> ivec3(ia3, ia3+6);
    int m;
    
	if(median(ivec1, m))
		cout << "median:" << m << endl;
	else
		cout << "no median" << endl;
		
	if(median(ivec2, m))
		cout << "median:" << m << endl;
	else
		cout << "no median" << endl;
	if(median(ivec3, m))
		cout << "median:" << m << endl;
	else
		cout << "no median" << endl;
		
	return 0;
}</span>

先说一下问题吧

1、如果头文件中不添加#include “median.cpp”的时候,程序运行报错。


分析原因应该是c++primer上写得模板与普通函数不同,进行实例化的时候,编译器必须能够访问定义模板的源代码,此处找不到源代码,所以显示未定义。

2、如果头文件中添加了#include “median.cpp”的时候,程序运行报错。


分析原因是应该是上面头文件中包含cpp,cpp又同样包含了头文件,然后就显示重复定义了。

为什么会有第二种错误呢?经过查资料才了解到,程序编译的过程。


首先分析一下普通的函数编译链接的基本过程:

主要内容在http://blog.csdn.net/bichenggui/article/details/4207084中有详细的介绍,我只针对部分进行分析一下。

从源代码生成exe文件要经过两步,编译和链接。

1 、编译过程中,一个编译单元(translation unit)是指一个.cpp文件以及它所#include的所有.h文件,.h文件里的代码将会被扩展到包含它的.cpp文件里,然后编译器编译该.cpp文件为一个.obj文件,并且本身包含的就已经是二进制码。

2、链接过程中,编译器将一个工程里的所有.cpp文件以分离的方式编译完毕后,再由连接器(linker)进行连接成为一个.exe文件。

借鉴一个例子:

<span style="font-size:18px;">//---------------test.h-------------------//
void f();//这里声明一个函数f

//---------------test.cpp--------------//

#include”test.h”
void f()
{
    …//do something
}  //这里实现出test.h中声明的f函数

//---------------main.cpp--------------//

#include”test.h”
int main()
{
    f(); //调用f,f具有外部连接类型
}</span>
编译阶段:test. cpp和main.cpp各自被编译成不同的.obj文件(姑且命名为test.obj和main.obj),在main.cpp中,调用了f函数,然而当编译器编译main.cpp时,它所仅仅知道的只是main.cpp中所包含的test.h文件中的一个关于void f();的声明,所以,编译器将这里的f看作外部连接类型,即认为它的函数实现代码在另一个.obj文件中,本例也就是test.obj,也就是说,main.obj中实际没有关于f函数的哪怕一行二进制代码,而这些代码实际存在于test.cpp所编译成的test.obj中。

链接阶段:在main.obj中对f的调用只会生成一行call指令,在编译时,这个call指令显然是错误的,因为main.obj中并无一行f的实现代码。那怎么办呢?这就是连接器的任务,连接器负责在其它的.obj中(本例为test.obj)寻找f的实现代码,找到以后将call f这个指令的调用地址换成实际的f的函数进入点地址。需要注意的是:连接器实际上将工程里的.obj“连接”成了一个.exe文件,而它最关键的任务就是上面说的,寻找一个外部连接符号在另一个.obj中的地址,然后替换原来的“虚假”地址

借助原文中的内容

call f这行指令其实并不是这样的,它实际上是所谓的stub,也就是一个jmp 0xABCDEF。这个地址可能是任意的,然而关键是这个地址上有一行指令来进行真正的call f动作。也就是说,这个.obj文件里面所有对f的调用都jmp向同一个地址,在后者那儿才真正”call”f。这样做的好处就是连接器修改地址时只要对后者的call XXX地址作改动就行了。但是,连接器是如何找到f的实际地址的呢(在本例中这处于test.obj中),因为.obj与.exe的格式是一样的,在这样的文件中有一个符号导入表和符号导出表(import table和export table)其中将所有符号和它们的地址关联起来。这样连接器只要在test.obj的符号导出表中寻找符号f(当然C++对f作了mangling)的地址就行了,然后作一些偏移量处理后(因为是将两个.obj文件合并,当然地址会有一定的偏移,这个连接器清楚)写入main.obj中的符号导入表中f所占有的那一项即可。

简单的来讲就是:

1、编译test.cpp时,编译器找到了f的实现,所以f的实现(二进制代码)出现在test.obj里。

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

3、链接时,链接器在test.obj中找到f的实现代码(二进制)的地址(通过符号导出表),然后将main.obj中悬而未决的call XXX地址改成f实际的地址。


接下来,分析一下函数模板编译的过程,最主要的特点是C++标准明确表示,当一个模板不被用到的时侯它就不该被实例化出来

“main.cpp”中需要掉用模板函数,但是在所包含的头文件中没有该函数的实现,所以在编译过程中,同样没有生成对应的二进制代码,只能寄希望于链接过程中能够在其他.obj里面找到对应的函数的实例,但是在“median.cpp”中的median(const vector<T>& vec, T& middle),没有被实例化,简单说就是在编译生成的median.o中没有二进制代码。所以,在最后链接过程中找不到该函数的定义,只能报错。


针对以上的问题,在网上查找,发现,为了避免这个情况,只能讲函数声明和函数定义,类的声明和类的定义,写在同一文件中,起名问.hpp文件,这样调用模板的时候可以看得到实现的源代码,不会出现以上情况。

补充一点:hpp,其实质就是将.cpp的实现代码混入.h头文件当中,定义与实现都包含在同一文件,则该类的调用者只需要include该hpp文件即可,无需再 将cpp加入到project中进行编译。而实现代码将直接编译到调用者的obj文件中,不再生成单独的obj,采用hpp将大幅度减少调用 project中的cpp文件数与编译次数,也不用再发布烦人的lib与dll,因此非常适合用来编写公用的开源库。


不知道有什么更好的方法可以解决这个问题,希望看到的朋友,能给个建议。




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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值