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,因此非常适合用来编写公用的开源库。

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

时间: 2024-08-01 18:44:50

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

C++类模板声明与定义为何不能分开

C++中每一个对象所占用的空间大小,是在编译的时候就确定的,在模板类没有真正的被使用之前,编译器是无法知道,模板类中使用模板类型的对象的所占用的空间的大小的.只有模板被真正使用的时候,编译器才知道,模板套用的是什么类型,应该分配多少空间.这也就是模板类为什么只是称之为模板,而不是泛型的缘故. 既然是在编译的时候,根据套用的不同类型进行编译,那么,套用不同类型的模板类实际上就是两个不同的类型,也就是说,stack<int>和stack<char>是两个不同的数据类型,他们共同的成员函

模板与泛型编程——定义模板

一.定义模板 1.函数模板 模板定义以关键字template开始,后跟一个模板参数列表,这是一个逗号分隔的一个或多个模板参数的列表,用<>括起来.在模板定义中,模板参数列表不能为空.模板参数表示在类或函数定义中用到的类型或值.当使用模板时,我们(隐式地或显式地)指定模板实参,将其绑定到模板参数上. 1 #include <iostream> 2 #include <string> 3 #include <vector> 4 #include <algo

模板类和模板函数的声明和定义分开文件写的方法

据说模板类和模板函数的声明和定义要写在同一个文件. 不然编译会出现"未定义的引用". 其实是有解决方法的. 模板类 template class CNNConvLayer<double>; 就是 template class className<typeName>; 模板函数 template void poolBackPropForKernal<double>(double*, double*, int, int, double*, double*

C++将模板的声明和定义放置在同一个头文件里

1. 一个类: 头文件用于保存类的声明:定义文件保存类的实现. 2. 分离编译模式: 允许在一个编译单元(.cpp文件)中定义函数.类型.类对象等,然后在另一个编译单元中引用它们.编译器处理完所有的编译单元后,链接器接下来会处理所有指向extern符号的引用(有时为缺省),从而生成单一可执行文件. 3. 模板类型: 模板类型不是一种实类型,它必须等到类型绑定后才能确定最终类型,所以在实例化一个模板时,必须要能够让编译器“看到”在哪里使用了模板,而且必须看到模板确切的定义,而不仅仅是它们的声明,否

【C】 05 - 声明和定义

仅从形式上看,C程序就是由各种声明和定义组成的.它们是程序的骨架和外表,不仅定义了数据(变量),还定义了行为(函数).规范中的纯语言部分,声明和定义亦花去了最多的篇幅.完全说清定义的语法比较困难,这里也只是个人的理解. 1. 标识属性 对C编译器而言,标识(identifier)包括对象名.函数名.复合类型及枚举tag.typedef类型名.label和枚举常量.标识的各种属性构成了C的复杂功能,理清这些概念对C的高级使用尤其重要. 域(scope)可以看做是标识的活动范围,一个编译单元中该范围

c++编译过程理解

1.Makefile就知道了.先直接用命令行操作,然后用集成的IDE来写代码. 2.对于编译过程,总体上是这样 <1>源代码(*.h,*.cpp/c)经过预编译,编译,生成目标文件(Windows下应该是.obj文件,Linux/unix下是.o文件) <2>然后通过链接(将各种目标文件.obj(.o) 和 目标文件的集合(动态静态库dll(windows下),so(linux/unix下))) <3>最终成功可执行文件(Windows下叫exe,Linux/unix下

[转]c++应用程序文件的编译过程

原文地址 这里讲下C++文件的编译过程及其中模板的编译过程: 一:一般的C++应用程序的编译过程.    一般说来,C++应用程序的编译过程分为三个阶段.模板也是一样的. 在cpp文件中展开include文件. 将每个cpp文件编译为一个对应的obj文件. 连接obj文件成为一个exe文件(或者其它的库文件). 下面分别描述这几个阶段.1.include文件的展开.    include文件的展开是一个很简单的过程,只是将include文件包含的代码拷贝到包含该文件的cpp文件(或者其它头文件)

C语言程序编译过程

最近在编译DM8168的ARM端程序时经常出现未定义.重定义等报错,由于源码文件多,包含关系比较多,所以自己添加时容易乱.深深的体会到,好的代码风格是如此重要,之前也在看代码重构,以后应该更加注意代码的质量.经思考总结规律如下: 1.公用的数据结构等写为一个头文件,其他源文件包含此头文件.同时为了让不同源文件里的函数都可以使用,公用的函数可以放在此头文件中声明. 2.其他源文件里声明的变量,如果想在另一个文件里用,需要extern声明,这样可以避免各种全局变量的交互混杂. 理解的比较浅,希望高人

简单了解一下c编译过程

大一的时候,学习c语言,用的是VC6.0.用了1年多,到后来了解了Linux,知道了gcc编译器,开始使用gcc Hello.c -o a.out 这样的命令进行编译.后来又学了gcc的一些其他的命令,不同的命令可以编译出不同的目标代码.现在想想类似于VC这种IDE虽然方便,但是对于具体是怎样的一个过程就不得而知了.作为一个优秀的程序员怎么可以不了解这个过程呢. Gcc/g++ 在执行编译工作的时候,总共4步 1.预处理,生成.i的文件  (预处理器cpp) 2.将预处理后的文件转换成汇编语言,