将所有代码放在C ++头文件中的优点和缺点?

您可以构造一个C ++程序,以便(几乎)所有的代码都驻留在头文件中。 它本质上看起来像一个C#或Java程序。 但是,编译时,至less需要一个.cpp文件来引入所有头文件。 现在我知道有些人会绝对厌恶这个想法。 但我还没有发现这样做的任何令人信服的缺点。 我可以列举一些优点:

[1]更快的编译时间。 所有的头文件只被parsing一次,因为只有一个.cpp文件。 此外,一个头文件不能被包含超过一次,否则你将得到一个生成中断。 当使用替代方法时,还有其他方法可以实现更快的编译,但是这非常简单。

[2]它通过使它们绝对清楚,避免循环依赖。 如果ClassA.h中的ClassAClassA.h中的ClassB具有循环依赖关系,那么我必须提供一个前向引用。 (请注意,这不像C#和Java,编译器自动解决循环依赖,这鼓励了错误的编码实践IMO)。 同样,如果你的代码是在.cpp文件中,你可以避免循环依赖,但是在真实世界的项目中, .cpp文件往往会包含随机的头文件,直到找不到谁依赖于谁。

你的想法?

原因[1]编译时间更快

不在我的项目中:源文件(CPP)只包含他们需要的标题(HPP)。 所以当我需要重新编译一个CPP时,由于一个小小的改变,我有十倍于没有重新编译的文件数量。

也许你应该在更多的逻辑资源/头文件中分解你的项目:对A类实现的修改不需要重新编译B,C,D,E等类的实现。

原因[2]它避免了循环依赖

循环依赖代码?

对不起,我还没有把这种问题当成一个真正的问题:假设A取决于B,而B取决于A:

 struct A { B * b ; void doSomethingWithB() ; } ; struct B { A * a ; void doSomethingWithA() ; } ; void A::doSomethingWithB() { /* etc. */ } void B::doSomethingWithA() { /* etc. */ } 

解决这个问题的一个好方法是将这个源代码分解成每个类的至less一个源/头(类似于Java方式,但每个类只有一个源和一个头):

 // A.hpp struct B ; struct A { B * b ; void doSomethingWithB() ; } ; 

 // B.hpp struct A ; struct B { A * a ; void doSomethingWithA() ; } ; 

 // A.cpp #include "A.hpp" #include "B.hpp" void A::doSomethingWithB() { /* etc. */ } 

 // B.cpp #include "B.hpp" #include "A.hpp" void B::doSomethingWithA() { /* etc. */ } 

因此,不存在依赖性问题,而且编译速度仍然很快。

我错过了什么?

在“现实世界”项目上工作时

在现实世界的项目中,cpp文件往往会包含随机标题,直到找不到谁依赖于谁

当然。 但是如果您有时间重新组织这些文件来构build您的“一个CPP”解决scheme,那么您有时间清理这些头文件。 我的标题规则是:

  • 打破标题,使其尽可能模块化
  • 永远不要包含你不需要的标题
  • 如果你需要一个符号,则向前声明它
  • 只有在以上失败的情况下,才会包含标题

无论如何,所有的标题必须是自给自足的,这意味着:

  • 一个头文件包含了所有需要的头文件(只有需要的头文件 – 见上)
  • 包含一个头文件的空白CPP文件必须编译而不需要包含任何其他内容

这将删除订购问题和循环依赖。

编译时间是一个问题? 然后…

如果编译时间真的是一个问题,我会考虑:

结论

你在做什么不是把所有的东西都放在标题中。

你基本上包括所有的文件到一个最后的来源。

也许你正在全项目编译中取胜。

但编制一个小小的变化时,你总是会输。

编码时,我知道我经常编译一些小的改动(如果只是让编译器validation我的代码),然后最后一次,做一个完整的项目改变。

如果我的项目按照自己的方式组织起来,我会失去很多时间。

我不同意第一点。

是的,只有一个.cpp,从头开始构build的时间更快。 但是,你很less从头开始构build。 您做了一些小改动,每次都需要重新编译整个项目。

我更喜欢这样做:

  • 保持共享声明在.h文件中
  • 保留仅在.cpp文件中的一个地方使用的类的定义

所以,我的一些.cpp文件开始看起来像Java或C#代码;)

但是,在devise系统时,由于第二点你做的, “保持.h”的方法是好的。 我通常在构build类层次结构时,在代码体系结构稳定后,将代码移动到.cpp文件中。

你说的是你的解决scheme是正确的。 对于您目前的项目和开发环境甚至没有任何缺点。

但…

正如其他人所说,将所有代码放在头文件中,每次更改一行代码时都会强制进行完整编译。 这可能不是一个问题,但是你的项目可能会变得足够大,编译时间将成为一个问题。

另一个问题是共享代码。 虽然您可能不直接关心,但是尽可能多地隐藏代码的潜在用户隐藏的代码是非常重要的。 通过将代码放入头文件中,任何使用代码的程序员都必须查看整个代码,而只是对如何使用代码感兴趣。 把你的代码放到cpp文件中只允许将一个二进制组件(静态或dynamic库)及其接口作为头文件提供,这在某些环境下可能更简单。

这是一个问题,如果你想能够把你的当前代码变成一个dynamic库。 由于没有正确的接口声明与实际代码分离,因此您将无法将已编译的dynamic库及其使用接口作为可读头文件提供。

您可能还没有这些问题,这就是为什么我说你的解决scheme可能在你现在的环境中是好的。 但是要做好应对变化的准备总是比较好的,应该解决其中的一些问题。

PS:关于C#或者Java,你应该记住,这些语言并没有做你所说的。 他们实际上是独立编译文件(如cpp文件),并为每个文件全局存储接口。 这些接口(和任何其他链接的接口)然后用于链接整个项目,这就是为什么他们能够处理循环引用。 由于C ++只为每个文件执行一次编译传递,因此无法全局存储接口。 这就是为什么你需要在头文件中明确写出它们。

你误解了这个语言是如何被使用的。 .cpp文件是真的(或者应该是内联和模板代码除外)唯一的可执行代码在你的系统中的模块。 .cpp文件被编译成目标文件,然后链接在一起。 .h文件仅用于在.cpp文件中实现的代码的前向声明。

这导致更快的编译时间和更小的可执行文件。 它看起来也相当干净,因为您可以通过查看.h声明快速了解您的课程。

至于内联代码和模板代码 – 因为这两个代码都是由编译器生成代码,而不是链接器 – 它们必须始终可以通过.cpp文件在编译器中使用。 因此,唯一的解决办法是将其包含在你的.h文件中。

但是,我已经开发了一个解决scheme,其中我有一个.h文件中的类声明,.inl文件中的所有模板和内联代码以及我的.cpp文件中的所有非模板/内联代码的实现。 .inl文件#included在我的.h文件的底部。 这保持干净和一致。

对我来说,明显的缺点是你总是要一次构build所有的代码。 使用.cpp文件,你可以分开编译,所以你只能重build真正改变的位。

你可能想看看Lazy C ++ 。 它允许您将所有内容放在单个文件中,然后在编译之前运行,并将代码拆分为.h和.cpp文件。 这可能会为你提供两全其美的方法。

编译速度慢通常是由于用C ++编写的系统中的过度耦合。 也许你需要将代码拆分成具有外部接口的子系统。 这些模块可以编译在单独的项目中。 这样可以最小化系统不同模块之间的依赖关系。

有一件事你放弃了,那就是没有匿名命名空间。

我发现它们对于定义在类的实现文件之外应该不可见的特定于类的实用程序函数是非常有价值的。 对于系统其他部分应该是不可见的全局数据,例如单例实例,它们也非常棒。

你要超越语言的devise范围。 虽然你可能有一些好处,但最终会让你陷入困境。

C ++是为具有声明的h文件和具有实现的cpp文件而devise的。 编译器是围绕此devise构build的。

是的 ,人们争论这是不是一个好的build筑,但这是devise。 最好花一点时间解决问题,而不是重新deviseC ++文件体系结构的新方法。

你的方法的一个缺点是你不能做并行编译。 您可能认为现在编译速度更快,但是如果您有多个.cpp文件,则可以在自己的机器上的多核上并行构build它们,或者使用分布式构build系统(如distcc或Incredibuild)来并行构build它们。

我喜欢根据接口和实现来考虑分离.h和.cpp文件。 .h文件包含多个类的接口描述,.cpp文件包含实现。 有时候会有一些实际的问题或者说清晰的问题来阻止一个完全清晰的分离,但这是我开始的地方 例如,为了清楚起见,我通常在类声明中内联小的访问函数。 更大的function在.cpp文件中编码

无论如何,不​​要让编译时间决定你将如何构build你的程序。 最好有一个可读和可维护的程序,而不是2分钟的1.5分钟。

我相信,除非你使用MSVC的预编译头文件,并且你正在使用一个Makefile或者其他的基于依赖的编译系统,否则在迭代编译的时候,单独的源文件应该编译得更快。 因为,我的开发几乎总是迭代的,所以我更关心它能重新编译我在文件x.cpp中所做的更改的速度,而不是我没有更改的其他二十个源文件。 另外,我对源文件的修改要比对API更频繁,所以它们的更改频率要低一些。

关于循环依赖。 我会更进一步采取帕尔塞尔的build议。 他有两个指向对方的class级。 相反,如果一个class级需要另一个class级,我会更频繁地遇到这种情况。 发生这种情况时,我将依赖项的头文件包含在其他类的头文件中。 一个例子:

 // foo.hpp #ifndef __FOO_HPP__ #define __FOO_HPP__ struct foo { int data ; } ; #endif // __FOO_HPP__ 

 // bar.hpp #ifndef __BAR_HPP__ #define __BAR_HPP__ #include "foo.hpp" struct bar { foo f ; void doSomethingWithFoo() ; } ; #endif // __BAR_HPP__ 

 // bar.cpp #include "bar.hpp" void bar::doSomethingWithFoo() { // Initialize f f.data = 0; // etc. } 

我之所以包含这个与循环依赖关系稍有关系的东西,是因为我觉得可以select包含头文件。 在这个例子中,struct bar源文件不包含struct foo头文件。 这是在头文件中完成的。 这样做的好处是开发人员不必知道开发人员需要包含哪些文件来使用该头文件。

标题中的代码有一个问题,那就是它必须内联,否则在链接包含相同标题的多个翻译单元时会遇到多重定义问题。

原来的问题指出,在项目中只有一个cpp,但是如果你正在创build一个目标为可重用的库的组件,情况并非如此。

因此,为了尽可能创build最可重用和可维护的代码,只需在头文件中放入内联代码即可。

那么,正如许多人指出的那样,这个想法有很多缺点,但为了平衡和提供一个专业人士,我会说,有一些库代码完全在头文件是有道理的,因为它会使它独立于其他使用的项目中的设置。

例如,如果有人试图使用不同的开源库,可以将它们设置为使用不同的方法链接到您的程序 – 有些可能使用操作系统的dynamic加载的库代码,有些可能是静态链接的; 有些可能会设置为使用multithreading,而另一些则不是。 对于程序员来说,这可能是一项压倒一切的任务,特别是在有时间限制的情况下,试图将这些不相容的方法排除出去。

但是,当使用完全包含在标题中的库时,所有这些都不是问题。 “这只是一个合理的写作良好的图书馆”。

静态或全局variableskludges更不透明,可能不可debugging。

例如统计分析的总迭代次数。

在我的kludged文件把这样的项目在cpp文件的顶部,使他们很容易find。

通过“也许是不可debugging的”,我的意思是说,通常我会把这样的一个全局进入WATCH窗口。 由于它始终在范围内,无论程序计数器现在在哪里,WATCH窗口总是可以到达它。 通过将这些variables放在头文件顶部的{}之外,您可以让所有下游代码“看到”它们。 如果你的程序计数器在{}之外,通过把INSIDE放在{}里,我认为debugging器将不再考虑它们在“范围内”。 而在kludge-global-at-Cpp-top中,即使它可能是全局的,但是在你的link-map-pdb-etc中没有extern语句的情况下,其他Cpp文件也不能到达它避免意外的耦合。

有一件事没有人提出,编译大文件需要大量的内存。 一次编译您的整个项目将需要如此巨大的内存空间,即使您可以将所有代码放在标题中也是不可行的。

如果您正在使用模板类,则必须将整个实现放在标题中。

一次性编译整个项目(通过一个基本的.cpp文件)应该允许像“整体程序优化”或“跨模块优化”这样的东西,它只能在几个高级编译器中使用。 如果您将所有的.cpp文件预编译为目标文件,然后进行链接,那么使用标准的编译器并不可行。

面向对象程序devise的重要思想在于具有隐藏的数据隐藏,导致封装类的实现隐藏在用户之外。 这主要是为了提供一个抽象层,其中一个类的用户主要使用公共可访问的成员函数来实现特定于实例的types以及静态types。 然后,类的开发人员可以自由地修改实际的实现,只要实现不暴露给用户。 即使实现是私有的,并在头文件中声明,更改实现将需要所有从属代码库重新编译。 然而,如果实现(成员函数的定义)在源代码(非头文件)中,那么库被改变,并且相关代码库需要与库的修订版本重新链接。 如果这个库像一个共享库那样dynamic地链接,那么保持函数签名(接口)相同并且实现改变也不需要重新链接。 优点? 当然。