模板究竟为什么不支持分离编译呢?!!!

那天,风和日丽。
于是,我开始了模板类的学习。
还记得,写的是模板类的顺序表和双向链表。
但是,编好程之后,程序编译能通过,但是却无法运行。

这里写图片描述

链接错误。额,那就是间接表明并不是代码出现了什么错误。
那那那那,问题出在哪里呢?

那就是




模板类的编写不支持分离编译

什么是分离编译?

  • 在了解为什么模板类不支持分离编译之前,我们先来了解一下什么是分离编译。
  • 一个程序(项目)由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有目标文件连接起来形成单一的可执行文件的过程,我们将其称之为分离编译
  • 简要来说,分离编译模式是C/C++组织源代码和生成可执行文件的一种方式。

为什么要进行分离编译?

  • 从编程入门的那一刻,我们便被无数的老师和知乎上或者csdn上的大神教导说要形成良好的编程风格。
  • 良好的编程风格在这里难以一言以蔽之,常见的有规范化的使用{}、()等符号,该空行的地方要空行之类的。
  • 而分离编译呢,也可以归属在良好的编程行为之列。
  • 即,当我们在编写一个工程类项目的时候,我们常常会选择将项目内功能性函数的声明和实现分离开来。
  • 这样,有助于我们更好的浏览项目内实现了什么功能,并能够有条理的对函数进行归类维护等行为。
  • 所以我们常常会进行分离编译。
  • 而在实际开发大型项目的时候,不可能把所有的源程序都放在一个头文件中,而是分别由不同的程序员开发不同的模块,再将这些模块汇总成为最终的可执行程序。

这里写图片描述

分离编译是如何实现的?

  • 首先,我们要知道我们编写的C++程序是怎么运行起来的~
  • 具体的步骤取决于计算机环境和使用的编译器,会有细微细节上的不同,但大致步骤如下:

    这里写图片描述

  • 每个源文件都是独立的编译单元,在当前源文件中使用但未在此定义的变量或者函数,就假设在其他的源文件中定义好了。
  • 每个源文件生成独立的目标文件(obj文件),然后通过连接(Linking)将目标文件组成最终的可执行文件。
  • 程序编译的简要过程包括预处理(Preprocessing)、编译(Compilation)、汇编(Assembly)和连接(Linking)。

    这里写图片描述

  • 在实现分离编译的时候,我们通常在SeqList.h中对在类中需要用到的函数进行声明,然后再SeqList.cpp中对函数进行定义,在Test.cpp中进行实现&调用。
  • 在Test.cpp中,调用了函数,然而当编译器编译Test.cpp时,他仅仅知道在所包含的头文件中有一个关于函数的声明。
  • 换句话说,编译器在这里将函数看作外部连接类型,认为函数实现代码在另外的.obj文件中(SeqList.obj)。
  • 那么,在Test.obj中实际上是没有关于所调用函数的二进制代码的,这些代码实际存在于SeqList.obj中。在Test.obj中对函数的调用只会生成相应的call指令。

    例如:call f [C++中这个名字当然是经过mangling[处理]过的]

  • 在编译时,这样的call指令实际上是错误的,因为在生成的.obj文件中并没有关于函数实现的代码。

  • 这个时候,连接器便出现了。连接器会在其它的obj文件中查找所调用函数的实现代码,找到之后将call指令里的函数调用地址换成实际函数进入点的地址。
  • 值得一提的是,连接器实际上将工程里的obj文件生成了一个exe文件,而它最最最关键的任务就是,寻找一个外部连接符号在另外的obj文件中的地址,然后替换原来的虚假地址。
  • 更深入说的话,call指令实际上是stub,即jmp 0xABCDEF。地址是任意的,关键是这个地址上有一行真正的指令来进行call函数的动作。
  • 即,这个obj文件中所以对函数的调用最终都会jmp到同一个地址,在那儿,才会进行真正函数的调用。
  • 这样做的好处是,连接器修改地址时只要对真正调用函数的地方进行改动。
  • 那连接器是如何找到函数的实际地址的呢?
  • 因为obj和exe的格式实际上是一样的,而在这样的文件中有一个符号导入表和符号导出表。其中,会将所有的符号和他们的地址关联起来。这样连接器只要在SeqList.obj的符号导出表中查找符好函数的地址就行了。
  • 在做一些偏移量处理之后,写入Test.obj符号导入表中调用函数所占有的那部分即可。

为什么模板类不支持分离编译?

  • 我们知道,C++标准中规定了模板函数的代码并不会直接编译成二进制代码,必须在模板类实例化成对象之后,才会生成相应的二进制代码。
  • 也就是说,如果你在Test.cpp文件中没有调用过函数,函数也就得不到实例化,从而Test.obj中也就没有关于f的任意一行二进制代码!如果你这样调用了:
SeqList<int> s1;//实例化对象
  • 这样Test.obj中也就有了函数的二进制代码段。以此类推。

  • 然而实例化要求编译器知道模板的定义,不是吗?

  • 在分离式编译的环境下,编译器编译某一个.cpp文件时并不知道另一个.cpp文件的存在,也不会去查找(当遇到未决符号时它会寄希望于连接器)。
  • 这种模式在没有模板的情况下运行良好,但遇到模板时就傻眼了,因为模板仅在需要的时候才会实例化出来。
  • 所以,当编译器只看到模板的声明时,它不能实例化该模板,只能创建一个具有外部连接的符号并期待连接器能够将符号的地址决议出来。
  • 然而当实现该模板的.cpp文件中没有用到模板的实例时,编译器懒得去实例化,所以,整个工程的.obj中就找不到一行模板实例的二进制代码,于是连接器也黔驴技穷了。
  • 所以便会出现链接错误啦~

处理方法

这里写图片描述

  • 那遇到这样的错误我们该怎么处理呢?
  • 1、在模板头文件 xxx.h 里面显示实例化->模板类的定义后面添加 template class SeqList; 一般不推荐这种方法, 一方面老编译器可能不支持,另一方面实例化依赖调用者。(不推荐)
  • 2、 将声明和定义放到一个文件 “xxx.hpp” 里面,推荐!

参考资料

参考博客:http://blog.csdn.net/K346K346/article/details/48879021
参考博客:http://blog.csdn.net/pongba/article/details/19130

ppps:写完博客有点累,写个睡前小故事吧

大概是在六月吧,和一位老师还有几位同学于夏日的深夜一起回宿舍。那时的西安已经十分燥热了,蝉在树上叫啊叫啊,在疲累十分的夜晚实在十分烦人了。
我们一行人本来一路无言,此时老师突然打破静寂说,“夏天的夜晚啊,就应该站在有蝉鸣的树下谈情说爱。”
听罢这句话,大家一齐笑起来。
老师大概是毕业还没几年,心中还保有了罗曼蒂克的情怀。
近现代的翻译实在拗口,但读起来却又动人的可爱,比起romantic来,罗曼蒂克不知怎得生生多出几丝情。
月光和灯光交相辉映之下,已经微有些发福的老师硬是散发出罗曼蒂克的劲儿来。
当时我想,啊真好,还没有在日复一日的学生工作中丧失自己的本心,还能有如此的情怀,可爱极了。
老师总是很幽默的把自己身上发生的一切堪称操蛋至极的事笑话一般的说出来,老师也说啊,人呢在青春尚在的时候,要不计代价的成长。
这位老师,可谓是把中年的自己活成了少年模样啊哈哈。
人呢,不怕老去。怕的是,白白老去了。
嗯,写到这里,听着窗外的蝉鸣,也算是有不那么差劲的罗曼蒂克了。
忘记什么也不能忘了生活,把自己活成花生人一样,那可太没意思~
夏日的晚上,要听成诗京的歌。一位来自韩国的抒情歌手,出道二十多年了吧,以温柔的声线著称。
当然也是因为我的歌单里面,只有他还没被完全和谐掉了。
以上,晚安啦~

阅读更多
想对作者说点什么?

博主推荐

换一批

没有更多推荐了,返回首页