先把代码贴上来,这是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,因此非常适合用来编写公用的开源库。
不知道有什么更好的方法可以解决这个问题,希望看到的朋友,能给个建议。
原文地址:http://blog.csdn.net/denkensk/article/details/45312013