使用C++头文件(.h)与头文件加实现文件(.h + .cpp),有哪些缺点?

6
作为一名初学者的C++程序员,我通常会将我的类接口放在.h文件中,实现则在.cpp文件中。然而,最近我尝试了一段时间的C#,我非常喜欢它清晰的语法和文件组织方式,特别是没有头文件和实现文件之分,你通常为每个.cs文件实现一个类,而且不需要头文件。
我知道在C++中这也是可能的(你可以在.h文件中编写"inline"函数),但到目前为止,在C++项目中,我总是看到明确的.h.cpp文件之间的区别。这种方法的优缺点是什么呢?
谢谢。

1
可能是C++头文件中的代码重复问题 - Steve Townsend
9个回答

10

在C++中,将接口和实现分开有几种好处。首先,如果您想要更新一个库而不更改接口,则在C++文件中编写代码意味着您只需要更新该库,而不是库加上头文件。其次,它隐藏了实现细节。也就是说,它强制人们仅从接口的角度来查看您的类,这是应该关注的事情,如果代码编写得很好的话。最后,将接口和文档分离具有某种美学上的清晰度。尽管您可能需要一些时间来适应这种方式,但在一段时间后,它会变得自然(个人观点)。


2
“将接口与实现分离”的论点在我看来是虚假的。是的,写得好的代码只关注接口。但让我们面对现实吧,使用单独的头文件并不能强制任何人在这方面编写良好的代码(除非你锁定了实现,即使如此,某些人可能会使用未记录的API)。这就像说Python通过要求一些缩进来鼓励可读性好的代码一样。 - user395760
1
@delnan 除了代码审查之外,很少有什么方法可以阻止糟糕的代码。 - wheaties
嗯,这个争论毫无根据。在像C#这样的语言中同样可以实现。关键特性是类的可访问性可以在模块级别进行控制,而这是C++所没有的特性。这是由于编译模型所带来的。 - Hans Passant

5

不要忘记构建时间。

将实现代码放在头文件中会使其更容易被更改。而更改头文件将导致包含它们的所有CPP文件重新构建,从而增加构建时间。在大型项目中,这可能是相当重要的。

我也喜欢将实现隐藏在我的库的用户之外。不幸的是,这对于模板类不起作用。

我的经验法则:将声明放在.H文件中,将定义放在.CPP文件中。


@dripfeed 很棒的回答!你能告诉我为什么我们不像链接 .cpp 文件一样链接 .h 文件,而是要包含它们吗? - Alexander Suraphel

2

如果你想要将C ++与已编译的二进制文件(通常使用库时)合并,那么在一个地方定义符号会更加方便。想象一下,如果你需要为二进制文件中的全局内容定义外部符号。如果你在同一个文件中有.cpp和.h代码,那么你必须为每个这样的文件定义二进制文件的符号。而如果你采用两个文件的方式,你只需有一个.h文件来定义二进制文件的符号,然后有很多.cpp文件来使用它。


1
主要的区别是,将某些内容实现在一个.h文件中的话,它会被放置在包含该头文件的每个编译单元中,这样在编译阶段会产生冗余,而使用.h.cpp分割的方式会将其编译为一个单独的目标文件,然后通过链接与其他目标文件一起使用,从而只生成一个已实现该头文件的编译二进制代码。
此外,如果你只在.h文件中声明变量和结构体,那么你将无法在其他.cpp文件之间共享它们。

链接器将会移除冗余,因此最终的可执行文件大小不会更大。然而,所有目标文件的组合大小会更大。 - Sjoerd
缓解这个问题的方法是将所有内容放入.h文件中,然后只有一个可编译的.cpp文件包含它们。(这对于库不起作用,但对于可执行文件有效。) - Kristopher Johnson

1

有趣的是,最近 C# 在某种程度上似乎朝着 C/C++ 的方向发展,引入了部分类。

在 IDE 中,这样做的特殊优势是 Visual Studio 设计器将修改处理可视控件或数据成员及其布局的类部分,而不必担心会搞乱存储在单独文件中的方法(应用程序逻辑)。


1
部分类不仅仅是实现的分离,还包括声明。在C++中没有相应的等价物。 - Hans Passant
1
部分类是设计师想象中的产物,实际上只是为了分离编码的物理关注点,例如分离工具生成的代码。它们在编译后不再以头文件存在的方式存在。 - user1228

1

我想回应@wheaties并补充一些内容

  1. 编译更容易(也许只是我),如果你仅修改头文件(如所有已包含它的实现文件),我从未能够使编译正常工作。我相信在Makefiles中,您必须手动添加依赖项,这对于非常大规模的项目来说是真正痛苦的事情(可能只是我)。因此,如果您的代码在实现文件中,则更改仅意味着重新编译该特定文件-当您想要进行快速更改、构建和测试时非常有用。

  2. 让我重申隐藏方面,由于代码的敏感性质,通常您不希望人们知道实现细节,因此仅公开头文件加预构建库,分离在这里是关键。

  3. 前向声明是一个巧妙的技巧,如果在头文件中没有任何代码“使用”类的实现细节,那么您不需要在头文件中包含它,但是在实现文件中,您可以包含真正的头文件,“它都可以很好地工作”(如果您有循环依赖关系,则有所帮助-为什么您有它们是不同的问题!)


1
在最近的一个大型项目中,我想使用的系统作者将很多代码放在了.h文件中。当将他们的.h文件包含到我的源代码中时,它增加了我的文件的进一步依赖性。在包含了他们项目的依赖项之后,我最终遇到了typedef冲突。如果他们将代码分离并仅将声明放在.h文件中,那么就会简单得多。我建议使用posix类型,并仅将声明放入.h文件中。

0

我看到很多回复都主张分离,主要是为了构建时和实现隐藏的好处。这两点肯定是优点,但我会举出反例:Boost

大多数Boost库使用没有外部链接的.hpp文件。原因是在模板的情况下通常需要这样做,当编译器必须从调用程序中知道参数类型时。因此,如果您想坚持放弃类而选择模板的“现代”C++方法,可能就没有选择了。


模板是一种不同的东西。我认为,OP指的是通常将实现放在头文件中的做法。 - Moo-Juice

0
关于比较 .cs.cpp/.h 的部分,我认为你需要记住 C# 的首席架构师 Anders Hejlsberg 的背景。在 Delphi 中,您也没有头文件和模块的区别(忽略包含文件以进行讨论)。您只需在单元文件中有两个部分 initializationimplementation
其他要点已经提到了。

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