问题提出的背景:最近在自己动手,用C来实现各类经典算法,还搬到了Github上,但是有一个问题比较困扰我,就是这些可以复用的,作为工具方法的算法,究竟应该放在头文件还是源文件里?一般的、通用的准则到底是什么呢?或者说头文件与源文件的作用究竟是什么?在编译连接等过程中,编译器会对他们有怎样的区别对待呢?
一、实现究竟放在哪里?
首先回答第一个问题,一般来说,什么时候需要把实现放在头文件里,什么时候又需要把实现放在源文件里?
(此部分参考:http://www.cnblogs.com/moodlxs/archive/2012/03/08/2385118.html)
不把实现放在头文件中,往往是出于以下几种顾虑:
1、暴露了实现细节
2、头文件被包含到不同的源文件中,会导致链接冲突
3、头文件被包含到不同的源文件中,会导致有多份实现被编译出来,增大可执行体的体积
如果有顾虑 1 ,那很显然应该在第一时间抛弃完全在头文件中实现的念头。
至于顾虑 2和3 的,我们举例如下。例如有以下头文件 c_function.h:
int integer_add(const int a, const int b)
{
return a + b;
}
如果在同一工程中,有 a.c 和 b.c 两个(或两个以上)源文件包含了此头文件,则在链接时期就会发生冲突,因为在两个源文件编译得到的目标文件中都有一份 integer_add 的函数实现,导致链接器不知道对于调用了此函数的调用者,应该使用哪一个副本。解决冲突办法有两个,一个是加上 inline ,另一个是加上 static 。使用这两个关键字的任意一个来修饰 integer_add 函数,然而本质却大不相同。
如果使用 inline ,则意味着编译器会在调用此函数的地方把函数的目标代码直接插入,而不是放置一个真正的函数调用,实际作用就是这个函数事实上已经不再存在,而是像宏一样被就地展开了。使用 inline 的副作用,首先在于毋庸置疑地,代码的体积变大了;其次则是,这个关键字严格算起来并不是 C 语言的关键字,使用它多少会带来一些移植性方面的风险。而且inline不对编译器做强制要求,编译器有权把它实现为非inline的状态(可能的原因有,函数太大或者复杂度过高)。这样的后果是不确定的。
如果是使用static,那么包含此头文件的源文件中都会存在此函数的一份副本。因为 static 关键字保证了该函数为单个源文件之内可见,所以不会产生冲突问题。虽然代码也有一定程度的膨胀,但至少结果是可预料的。
另外,应该避免使用extern关键字,如果在两个文件中重复定义了一个函数,并且在其中一个文件中对这个函数使用了extern关键字进行修饰,那么就会发生连接错误。
所以把实现放在头文件里,似乎不是一个很好的办法,但并不是不能这么做。
虽然这些讨论主要聚焦在 C 语言上,但由于 C++ 是 C 语言的超集,并且在这些方面并没有做太多的修改,因此讨论结果同样也适用于 C++ 。
二、源文件与头文件的关系
接下来,我们谈一下头文件和源文件在编译与组建的过程中的关系。
编译器就将源文件(.cpp)编译成目标文件(.obj),目标文件就是编译单元。一个程序可以由一个编译单元组成,也可以有多个编译单元组成。一个函数不能放到两个编译单元里面,但两个函数或以上就可以分别放在一个单元里面。那么就是一个源文件对应一个目标文件,然后通过链接器组成一个.exe,也就是程序了。
在C++中,使用函数或者变量之前必须要进行声明。那么如果一个源文件要用到另一个源文件定义的函数,只需在这个源文件中写上他的函数声明就可以了,其余工作由链接器帮你完成。但是当多个文件都需要使用同一个函数时,那么就要在多份源文件中进行声明。而且如果要修改这个函数时,就必须逐个修改每个源文件。
头文件(.h)就是为了解决这个问题而诞生,他包含了这些公共的函数定义,而且如果需要修改,也只修改头文件中的内容即可。对于商业C++程序库,一般把头文件随二进制的库文件发布,而把源代码保留,这也是上面所说的顾虑1。
所有需要使用该函数的源文件只需要用#include语句将相应的头文件包含进去便可。预处理器发现#include指令后,就会寻找指令后面的文件名并把这个文件的内容包含到当前文件中。被包含文件中的文本将替换源代码文件中的#include指令,就像你把被包含文件中的全部内容键入到源文件中的这个位置一样。头文件是没有编译意义的,编译器只编译源文件生成目标文件,而头文件不参与编译过程。
另外,使用#include指令包含源文件也是可行的,编译器完全能够正常处理,甚至可以使用#include指令包含任意扩展名的文件。因此从设计角度上讲,源代码区分为.h和.c,仅仅是为了接口与实现的分离,实际上两者没什么本质的差别。头文件只是工具,但不是必须的。
三、在这个场景下的结论
经过以上的讨论,最终的结果是,似乎在开源的工具方法中,把实现放在头文件里是个非常不错的选择。主要的有点有三个:
1、没有隐藏实现细节的要求,因为这是一份开源的、用作学习与练习的代码。
2、头文件不参与编译,再单独使用卫兵宏(#ifndef... #define... #endif),就可以避免多个文件引用本工具方法时,可能带来的重复编译(即链接错误,比如说LNK2005)。
3、不需要像商业程序库一样,把实现部分单独打包生成lib或dll等二进制文件,所以代码体积小,结构也简单。