实现类头文件的优缺点是什么?

12

我喜欢DRY(不要重复自己)的概念,但是C++中头文件的概念违背了这个编程规则。在头文件中完全定义一个类成员是否存在任何缺点?如果对于模板来说是正确的,那么为什么不对普通类也是呢?我有一些关于利弊的想法,但你有什么看法呢?


那不是DRY的反义词,正好相反吗? - ndim
9
实际上,这并不是模板应该做的正确事情,但不幸的是这是唯一需要做的事情。 - Eric
请参考这个相关问题,特别是我的回答,引用了Pedro Guerreiro的一篇文章。 - Daniel Daranas
6个回答

24

将所有内容放在头文件中的可能优势:

  • 减少冗余(导致更容易进行更改、重构等操作)
  • 有可能让编译器/链接器进行更好的优化
  • 通常更容易整合到现有项目中

将所有内容放在头文件中的可能劣势:

  • 较长的编译/链接周期
  • 失去接口与实现的分离
  • 可能导致难以解决的循环依赖
  • 大量的内联可能会增加可执行文件的大小
  • 防止共享库/DLL的二进制兼容性
  • 会让更喜欢传统C++方式的同事不满意

我认为在这种情况下不适用内联。编译器应该看到相同的源代码,只是没有原型。虽然我可能完全错了。 - Matt
2
内联是有效的。编译器在每个翻译单元中并不会看到完整的函数定义,除非定义在头文件中。这意味着通常情况下在调用函数时编译器无法看到完整的定义,那也就失去了内联的相关性。 - jalf

17

问题之一是,通常实现会比类定义经常更改 - 因此对于大型项目,您最终需要为每个小更改重新编译整个代码库。

注:无需翻译内容中的英文字符和标点符号。

2
这确实是一个原因,但它在不包括实现在头文件中的原因列表中排名相对较低。 - Eric
3
每个人都知道编译时间会降低生产力。我认为这是一个不编译的好理由。 - Ben S
9
@Eric - 我目前正在一款拥有上百万行代码的软件深处工作,其中许多类的定义都在头文件中(没有什么好的原因)。每当我做出更改时,都需要耐心等待5-10分钟的重新编译 - 这实在是极大地影响了工作效率。(但这也为我回答StackOverflow上的问题提供了机会!) - Aaron
4
@Eric - 这完全取决于具体项目。如果我更改一个被所有编译单元包含的头文件,重新编译需要数小时的时间。如果代码在头文件中,那么就无法进行任何工作了。 - Nemanja Trifunovic
使用g++的-j选项进行多核编译,我认为这不是一个问题。 - May Oakes
1
@Phineas - 这只是意味着你需要一个更大的程序来看到问题 - 并不意味着问题消失了。你仍然在编译比必要更多的文件 - 只是编译速度更快了。 - Aaron

6
不在头文件中实现类的主要原因是:使用您的类的消费者需要知道其实现细节吗?答案几乎总是否定的。他们只想知道可以用什么接口与类交互。将类实现可见于头文件中,会使了解此接口变得更加困难。
除了紧凑性和将接口与实现分离的考虑外,还存在商业动机。如果您开发一个要销售的库,则(可能)不希望公开出售的库的实现细节。

6
您并非在重复自己,您只需在一个头文件中编写代码一次。它会被预处理器重复,但这不是您的问题,并且这也不违反DRY原则。

如果对于模板来说这么做是正确的,为什么对于普通类不可以呢?

并不是说这样做是正确的,而是在普遍情况下只能这样做。
无论如何,如果您在一个头文件中实现一个类,您将获得以下优缺点:
  • 完整的实现在任何使用它的地方都是可见的,这使编译器可以根据需要轻松进行内联。
  • 相同的代码将被解析和编译多次,导致编译时间更长。
  • 另一方面,如果所有内容都位于头文件中,那可能会导致较少的翻译单元,因此编译器运行的次数更少。最终,您可能会得到一个仅包含所有内容的单个翻译单元,从而可以实现非常快速的编译。

就是这样。

我的大多数代码倾向于位于头文件中,但那是因为我的大多数代码都是模板。


2
除了构建时间很长之外,主要的缺点是界面和实现没有明确的分离。理想情况下,你不应该需要看到一个直观且文档良好的接口的实现。

不幸的是,C++的头文件通常包含了很多实现细节,所以它并不是那么清晰。(在其他人说之前:是的,是的,pimpl惯用法,啦啦啦)。 - Kristopher Johnson

0

尚未提到:每个包含文件都会实例化虚函数,因此您可以使可执行文件膨胀(我不确定这是否适用于所有编译器)。

有一个替代方案:
在源文件中声明的类中执行大量操作。 一个示例是pimpl习惯用语,但也有人害怕在头文件之外声明类。 但是,对于私有类来说,这是有意义的。


虚函数?你是指模板函数吗? 它们不会为每个包含而实例化,而是为每个它们所实例化的类型实例化。例如,如果有3个翻译单元实例化了std::vector<int>,链接器将把这3个实例合并成一个。膨胀来自于使用大量不同类型进行实例化。 - jalf
我所说的虚函数是指在虚表中定义的函数,正如http://www.ddj.com/cpp/184403747中所提到的那样,尽管他们说这不再是一个真正的问题,但它仍然取决于编译器的意愿。 - stefaanv

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接