50

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

[1] 更快的编译时间。所有头文件只被解析一次,因为只有一个 .cpp 文件。此外,一个头文件不能包含多次,否则会导致构建中断。使用替代方法时,还有其他方法可以实现更快的编译,但这很简单。

[2] 它通过使它们绝对清晰来避免循环依赖。如果ClassAin对inClassA.h有循环依赖,我必须提出一个前向引用并且它会突出。(请注意,这与编译器自动解析循环依赖关系的 C# 和 Java 不同。这鼓励了 IMO 的不良编码实践)。同样,如果您的代码在文件中,您可以避免循环依赖,但在实际项目中,文件往往包含随机标题,直到您无法弄清楚谁依赖于谁。ClassBClassB.h.cpp.cpp

你的意见?

4

17 回答 17

36

原因 [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. */ }

解决该问题的一个好方法是将此源分解为每个类至少一个源/标头(以类似于 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 文件往往会包含随机标题,直到您无法弄清楚谁依赖于谁

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

  • 分解标题以使它们尽可能模块化
  • 永远不要包含你不需要的标题
  • 如果你需要一个符号,前向声明它
  • 仅当上述失败时,才包含标头

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

  • 标头包括所有需要的标头(并且只有需要的标头 - 见上文)
  • 必须编译包含一个标头的空 CPP 文件,而无需包含其他任何内容

这将消除排序问题和循环依赖。

编译时间有问题吗?然后...

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

结论

您所做的并不是将所有内容都放在标题中。

您基本上是将所有文件包含在一个且只有一个最终源中。

也许您在完整项目编译方面获胜。

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

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

如果我的项目按照你的方式组织,我会浪费很多时间。

于 2008-10-11T15:14:55.967 回答
26

我不同意第 1 点。

是的,只有一个 .cpp 并且从头开始构建的时间更快。但是,您很少从头开始构建。您进行了一些小的更改,并且每次都需要重新编译整个项目。

我更喜欢反过来做:

  • 将共享声明保存在 .h 文件中
  • 保留仅在 .cpp 文件中一处使用的类的定义

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

但是,在设计系统时, “将内容保存在 .h”中的方法很好,因为第 2 点。你做了。我通常在构建类层次结构时这样做,然后当代码架构变得稳定时,我将代码移动到 .cpp 文件。

于 2008-10-11T08:50:57.303 回答
16

你说你的解决方案有效是对的。它甚至可能对您当前的项目和开发环境没有任何不利之处。

但...

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

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

如果您希望能够将当前代码转换为动态库,这将是一个问题。因为您没有与实际代码分离的正确接口声明,所以您将无法将编译的动态库及其使用接口作为可读的头文件提供。

您可能还没有这些问题,这就是为什么我告诉您您的解决方案在您当前的环境中可能没问题。但最好为任何变化做好准备,其中一些问题应该得到解决。

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

于 2008-10-11T09:38:30.280 回答
12

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

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

至于内联代码和模板代码——因为它们都用于由编译器而不是链接器生成代码——它们必须始终对每个 .cpp 文件的编译器可用。因此,唯一的解决方案是将其包含在您的 .h 文件中。

但是,我开发了一个解决方案,其中我的类声明在一个 .h 文件中,所有模板和内联代码在一个 .inl 文件中,所有非模板/内联代码的实现都在我的 .cpp 文件中。.inl 文件 #included 在我的 .h 文件底部。这使事情保持清洁和一致。

于 2008-10-11T09:14:35.820 回答
11

对我来说明显的缺点是您总是必须一次构建所有代码。使用.cpp文件,您可以进行单独编译,因此您只需要重建真正改变的部分。

于 2008-10-11T08:47:32.490 回答
4

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

于 2008-10-12T16:25:27.963 回答
3

你放弃的一件事是匿名命名空间,如果没有我将很难生活。

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

于 2008-10-11T15:55:33.577 回答
3

您超出了语言的设计范围。虽然你可能有一些好处,但它最终会咬你一口。

C++ 专为具有声明的 h 文件和具有实现的 cpp 文件而设计。编译器是围绕这种设计构建的。

的,人们争论这是否是一个好的架构,但它是设计。最好把时间花在解决问题上,而不是重新发明设计 C++ 文件体系结构的新方法。

于 2008-10-11T16:23:28.747 回答
3

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

缓慢的编译时间通常是由于用 C++ 编写的系统内过度耦合造成的。也许您需要将代码拆分为具有外部接口的子系统。这些模块可以在单独的项目中编译。这样,您可以最大限度地减少系统不同模块之间的依赖关系。

于 2008-10-11T12:56:35.263 回答
2

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

最初的问题指定项目中只有一个 cpp,但如果您正在创建一个用于可重用库的组件,则情况并非如此。

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

于 2008-10-12T02:31:58.317 回答
2

我相信,除非您使用 MSVC 的预编译头文件,并且您使用的是 Makefile 或其他基于依赖项的构建系统,否则在迭代构建时,拥有单独的源文件应该编译得更快。因为,我的开发几乎总是迭代,我更关心它可以多快重新编译我在文件 x.cpp 中所做的更改,而不是我没有更改的其他 20 个源文件。此外,我对源文件的更改比对 API 的更改要频繁得多,因此它们的更改频率较低。

关于循环依赖。我会把paercebal的建议更进一步。他有两个类相互指向。相反,我更频繁地遇到一个类需要另一类的情况。发生这种情况时,我将依赖项的头文件包含在另一个类的头文件中。一个例子:

// 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.
}

我包含这个与循环依赖关系稍微无关的原因是,我觉得除了包含头文件之外还有其他选择。在此示例中,struct bar 源文件不包含 struct foo 头文件。这是在头文件中完成的。这样做的好处是,使用 bar 的开发人员不必知道开发人员使用该头文件需要包含的任何其他文件。

于 2008-10-11T17:58:46.433 回答
2

我喜欢考虑在接口和实现方面分离 .h 和 .cpp 文件。.h 文件包含另一个类的接口描述,而 .cpp 文件包含实现。有时会出现一些实际问题或明确性会阻止完全干净的分离,但这就是我开始的地方。例如,为了清楚起见,我通常在类声明中内联编码小的访问器函数。较大的函数编码在 .cpp 文件中

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

于 2008-10-11T14:06:20.380 回答
1

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

例如,如果尝试使用不同的开源库,则可以将它们设置为使用不同的方法链接到您的程序——一些可能使用操作系统的动态加载库代码,而另一些则设置为静态链接;有些可能被设置为使用多线程,而另一些则不是。对于程序员来说,尝试整理这些不兼容的方法很可能是一项艰巨的任务——尤其是在时间有限的情况下。

然而,当使用完全包含在标头中的库时,所有这些都不是问题。“它只是工作”对于一个合理的编写良好的库。

于 2008-10-11T16:49:18.913 回答
0

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

于 2014-04-16T08:29:27.217 回答
0

静态或全局变量组合更不透明,可能无法调试。

例如计算迭代的总数以进行分析。

在 MY kludged 文件中,将这些项目放在 cpp 文件的顶部可以很容易地找到它们。

通过“可能无法调试”,我的意思是我通常会将这样一个全局变量放入 WATCH 窗口。由于它始终在范围内,因此无论程序计数器现在恰好在哪里,WATCH 窗口都可以始终访问它。通过将这些变量放在头文件顶部的 {} 之外,您可以让所有下游代码“看到”它们。通过将它们放在 {} 内,我认为如果您的程序计数器在 {} 之外,调试器将不再认为它们“在范围内”。而使用 kludge-global-at-Cpp-top,即使它可能是全局的,可以显示在您的链接映射 pdb-etc 中,但没有外部语句,其他 Cpp 文件无法访问它,避免意外耦合。

于 2008-10-11T09:07:12.497 回答
0

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

于 2008-10-30T01:16:24.490 回答
0

如果您使用的是模板类,则无论如何都必须将整个实现放在标题中...

一次性编译整个项目(通过单个基本 .cpp 文件)应该允许像“整个程序优化”或“跨模块优化”这样的东西,这仅在少数高级编译器中可用。如果您将所有 .cpp 文件预编译到目标文件中,然后进行链接,这对于标准编译器来说实际上是不可能的。

于 2008-11-19T05:23:17.173 回答