博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

注意头文件规则,避免链接错误:重复定义(multiple defination)

Posted on 2016-03-30 22:01  bw_0927  阅读(6462)  评论(0)    收藏  举报

https://zybuluo.com/uuprince/note/81709

 

程序编译的时候遇到了一个重复定义的问题,研究一下发现自己在编译和链接过程中还有一些不清楚的地方,发文章总结一下。

几个问题:

  • 头文件中只可以放置函数声明,不可以放置函数定义吗?
  • 为什么有些头文件中直接把函数定义都写进去了?
  • 模板函数/类中要求头文件中必须包含定义才能进行模板实例化,这种定义放在头文件的情况会不会有问题?
 

头文件中只可以放置函数声明,不可以放置函数定义吗?

先分析下面的程序:

 
  1. // a.h
  2. #ifndef __a_h__
  3. #define __a_h__
  4. void funcA(void); // 声明
  5. void funcA(void) // 定义
  6. {}
  7. #endif
 
  1. // b.h
  2. #ifndef __b_h__
  3. #define __b_h__
  4. void funcB(void);
  5. #endif
 
  1. // b.cpp
  2. #include "b.h"
  3. #include "a.h"
  4. void funcB(void)
  5. {
  6. funcA();
  7. }
 
  1. //c.h
  2. #ifndef __c_h__
  3. #define __c_h__
  4. void funcC(void);
  5. #endif
 
  1. //c.cpp
  2. #include "c.h"
  3. #include "a.h"
  4. void funcC(void)
  5. {
  6. funcA();
  7. }
 
  1. //main.cpp
  2. #include "b.h"
  3. #include "c.h"
  4. int main(int argc, char* argv[])
  5. {
  6. funcB();
  7. funcC();
  8. return 0;
  9. }

上述代码编译链接的时候编译器(g++)就会complain:

 
  1. c.o: In function `funcA()':
  2. c.cpp:(.text+0x0): multiple definition of `funcA()'
  3. b.o:b.cpp:(.text+0x0): first defined here
  4. collect2: ld returned 1 exit status

那么,问题来了。为什么编译器在链接的时候会抱怨“funcA()重复定义”?

其实本质问题就是funcA的定义被放在了a.h中,如果写在a.cpp中,就不会有重复定义的问题。下面分析一下编译过程都发生了什么,这样更容易从编译器的角度理解此问题。

编译器处理include的时候很简单,就是直接把include中的内容包含进来。所以b.cpp、c.cpp和main.cpp代码展开后可以简化为:

 
  1. // b.cpp
  2. void funcA(void); // 声明
  3. void funcA(void) // 定义
  4. {}
  5. void funcB(void);
  6. void funcB(void)
  7. {
  8. funcA();
  9. }
 
  1. // c.cpp
  2. void funcA(void); // 声明
  3. void funcA(void) // 定义
  4. {}
  5. void funcC(void);
  6. void funcC(void)
  7. {
  8. funcA();
  9. }
 
  1. // main.cpp
  2. void funcB(void);
  3. void funcC(void);
  4. int main(int argc, char* argv[])
  5. {
  6. funcB();
  7. funcC();
  8. return 0;
  9. }

编译的时候,C++是采用独立编译,就是每个cpp单独编译成对应的.o文件,最后链接器再将多个.o链接成可执行程序。所以从编译的时候,从各个cpp文件看,编译没有任何问题。但是能发现一个问题,b.o中声明和定义了一次funcA(),c.o中也声明和定义funcA(),这个就是编译器报错的问题所在了。有人可能会问,既然是从同一份文件include过来的函数funcA,那么定义都是同一份,为什么编译器不会智能的处理一下,让链接时候不报错呢? 
其实编译器链接的时候,并不知道b.cpp中定义的funcA与c.cpp中定义的funcA是同一个文件include过来的,它只会认为如果有两份定义,而且这两份定义如果实现不同,那么到底以哪个为准呢?既然决定不了,那就干脆报错好了。


 

为什么有些头文件中直接把函数定义都写进去了?

刚才的分析,可以得出结论:头文件中变量和函数的声明,而不要放定义进去。否则就会有重复定义的错误出现。但是有几种情况是例外的:

  • 内联函数的定义
  • 类(class)的定义
  • const 和 static 变量
 

内联(inline)函数的定义,可以放在头文件中

因为内联的目的就是在编译期让编译器把使用函数的地方直接替换掉,而不是像普通函数一样通过链接器把地址链接上。这种情况,如果定义没有在头文件的话,编译器是无法进行函数替换的。所以C++规定,内联函数可以在程序中定义多次,只要内联函数定义在同一个cpp中只出现一次就行。

按照这个理论,上述a.h简单修改一下就可以避免重复定义了。

 
  1. // a.h
  2. #ifndef __a_h__
  3. #define __a_h__
  4. inline void funcA(void); // 内联声明
  5. void funcA(void) // 定义
  6. {}
  7. #endif
 

类(class)的定义,可以放在头文件中

用类创建对象的时候,编译器要知道对象如何布局才能分配内存,因此类的定义需要在头文件中。一般情况下,我们把类内成员函数的定义放在cpp文件中,但是如果直接在class中完成函数声明+定义的话,这种函数会被编译器当作inline的,因此满足上面inline函数可以放在头文件的规则。但是如果声明和定义分开实现,但是都放在头文件中,那就会报重复定义了!!

 

const 和 static 变量,可以放在头文件中

const对象默认是static的,而不是extern的,所以即使放在头文件中声明和定义。多个cpp引用同一个头文件,互相也没有感知,所以不会导致重复定义。


 

模板函数/类中要求头文件中必须包含定义才能进行模板实例化,这种定义放在头文件的情况会不会有问题?

前面分析可知,头文件中要么只有函数声明,要么是含有inline函数的定义。但是模板的定义(包括非inline函数/成员函数)要求声明和实现都必须放在头文件中,难道没有“重复定义”的问题???

答案当然是不会有问题(要不template早就被抱怨死了)。其实编译器也考虑到会遇到类似的问题,在编译器或连接器的某处已经有防止重定义的处理了。这里参考stackflow中的答案:http://stackoverflow.com/questions/235616/multiple-definitions-of-a-function-template

 

https://www.zhihu.com/question/20630104

模版类的定义和实现分开写了,结果编译出错,查了两天才查出问题。

C++中每一个对象所占用的空间大小,是在编译的时候就确定的,在模板类没有真正的被使用之前,编译器是无法知道,模板类中使用模板类型的对象的所占用的空间的大小的。只有模板被真正使用的时候,编译器才知道,模板套用的是什么类型,应该分配多少空间。这也就是模板类为什么只是称之为模板,而不是泛型的缘故。

既然是在编译的时候,根据套用的不同类型进行编译,那么,套用不同类型的模板类实际上就是两个不同的类型,也就是说,stack<int>和stack<char>是两个不同的数据类型,他们共同的成员函数也不是同一个函数,只不过具有相似的功能罢了。
 
所以模板类的实现,脱离具体的使用,是无法单独的编译的;把声明和实现分开的做法也是不可取的,必须把实现全部写在头文件里面。为了清晰,实现可以不写在class后面的花括号里面,可以写在class的外面。


C++的模板库,一定是开源的^ ^
 
《C++ Template》第六章讲过这个问题
组织模板代码有三种方式
1.包含模型(常规写法 将实现写在头文件中)
2.显式实例化(实现写在cpp文件中,使用template class语法进行显式实例化)
3.分离模型(使用C++ export关键字声明导出)

第三种方式理论最优,但是实际从C++标准提出之后主流编译器没有支持过,并且在最新的C++11标准中已经废除此特性,export关键字保留待用。
那么实际上能够使用的实现分离也就只有显式实例化

比较有意思的是,《C++ Template》书中作者建议代码为分离模型做准备,等待编译器支持之后替换,没想到最终这个特性被C++标准废弃了。