头文件中有多个类,而每个类中只有一个头文件


75

无论出于何种原因,我们公司都有一个编码准​​则,其中指出:

Each class shall have it's own header and implementation file.

因此,如果我们编写了一个名为的类,MyString则需要一个关联的MyStringh.hMyString.cxx

还有其他人这样做吗?结果有人看到过任何编译性能影响吗?10000个文件中的5000个类的编译速度是否与2500个文件中的5000个类的编译速度一样快?如果不是,差异是否明显?

[我们编码C ++,并使用GCC 3.4.4作为我们的日常编译器]


74
您的公司准则中有错字。
Lightness Races in Orbit

2
顺便说一句,这是丑陋的。多么愚蠢,膨胀的要求。
Lightness Races in Orbit

8
错字是流浪撇号。(“可能”是“它”)(“它是”是“它是”的收缩)。
ctrl-alt-delor 2012年

9
我将MyString类放在哪个文件中?我确定我把它留在这里的某个地方。有没有一种方法可以使它更容易记住?我知道如果我们…
ctrl-alt-delor 2012年

5
我已经看到在一个头文件中具有多个类定义的代码,而cpp文件定义了多个类的成员函数。一场恶梦
UmNyobe 2014年

Answers:


81

这里的术语是翻译单元,您真的希望(如果可能)每个翻译单元具有一个类,即每个.cpp文件具有一个类实现,并且具有相同名称的相应.h文件。

从这种方式(从编译/链接)的角度来看,这样做通常更有效(尤其是在执行诸如增量链接之类的操作时)。想法是,翻译单元是隔离的,这样,当一个翻译单元发生更改时,您不必重新构建很多东西,就像您开始将许多抽象集中到一个翻译单元中一样。

此外,您还会发现许多错误/诊断是通过文件名报告的(“ Myclass.cpp中的错误,第22行”),如果文件和类之间存在一一对应的关系,则将很有帮助。(或者我想您可以将其称为2对1对应关系)。


1
如果您的类在逻辑上捆绑在一起,则不想将它们放在单独的文件中。这是我们编程中的许多神话之一。问题在于我们得到的文件数量爆炸并且变得难以管理。如果文件变得很大,可能是因为您的设计错误。尝试消除指针(裸,共享和唯一),并停止使用分配。这将使您的程序变得更好,更全面。
清晰的时间是

68

数千行代码不知所措?

在目录中每个类具有一组头文件/源文件似乎有点过头了。如果班级数量达到100或1000,甚至会令人恐惧。

但是,按照“让所有东西放在一起”的理念玩弄了源代码之后,得出的结论是,只有撰写文件的人才有希望不会丢失。即使使用IDE,也很容易遗漏任何东西,因为当您处理20,000行的源代码时,您会为任何与您的问题不完全相关的事情而闭嘴。

现实生活中的例子:在这千行源代码中定义的类层次结构将自身封闭为菱形继承,并且某些方法在子类中被具有完全相同代码的方法覆盖。这很容易被忽略(谁想浏览/检查20,000行源代码?),并且当更改原始方法(错误纠正)时,效果不如预期的普遍。

依赖关系变得循环了吗?

我在模板代码中遇到了这个问题,但是在常规C ++和C代码中也遇到了类似的问题。

将源分成每个结构/类1个标头,您可以:

  • 加快编译速度,因为您可以使用符号前向声明而不是包括整个对象
  • 类(§)之间具有循环依赖关系(即,类A具有指向B的指针,而类B具有指向A的指针)

在源代码控制的代码中,类依赖关系可能导致类在文件中上下移动,以使标头得以编译。在比较不同版本的同一文件时,您不想研究此类移动的演变。

具有单独的标头可以使代码更具模块化,编译速度更快,并且可以更轻松地通过不同版本的差异来研究其演变

对于我的模板程序,我必须将标头分成两个文件:包含模板类声明/定义的.HPP文件,以及包含上述类方法的定义的.INL文件。

将所有这些代码放在一个且只有一个唯一的标头中,意味着将类定义放在此文件的开头,并将方法定义放在末尾。

然后,如果某人只需要一小部分代码,并且仅使用一个标头的解决方案,他们仍然将不得不为较慢的编译付出代价。

(§)请注意,如果知道哪个类拥有哪个类,则可以在类之间具有循环依赖关系。这是关于知道其他类的存在的类的讨论,而不是shared_ptr循环依赖关系反模式。

最后一句话:标题应该是自给自足的

但是,必须通过多个标头和多个源的解决方案来尊重这一点。

当您包含一个标头时,无论使用哪个标头,您的源代码都必须干净地编译。

每个标题都应该是自给自足的。您应该开发代码,而不是通过捉住10,000多个源文件项目来寻宝,以发现哪一个标头定义了仅因一个枚举而需要包含的1,000行标头中的符号。

这意味着每个标头都定义或正向声明其使用的所有符号,或者包括所有必需的标头(并且仅包含必需的标头)。

关于循环依赖的问题

下划线-d问:

您能解释一下使用单独的标题对循环依赖有何影响吗?我不这样认为。即使两个类都在同一个标​​头中完全声明,我们也可以简单地创建一个循环依赖关系,只需在我们在另一个声明句柄之前预先向前声明一个就可以了。其他所有内容似乎都是不错的地方,但是分开的标题有助于循环依赖的想法似乎遥不可及

underscore_d,11月13日在23:20

假设您有2个类模板A和B。

假设类A(分别为B)的定义具有指向B(分别为A)的指针。我们还说A类(分别为B)的方法实际上是从B(分别为A)调用方法的。

您在类的定义及其方法的实现中都具有循环依赖关系。

如果A和B是普通类,并且A和B的方法在.CPP文件中,则不会有问题:您将使用前向声明,为每个类定义提供标头,然后每个CPP都将包含两个HPP。

但是,因为有了模板,实际上您必须在上面重现该模式,但仅使用标头。

这表示:

  1. 定义头A.def.hpp和B.def.hpp
  2. 实现标头A.inl.hpp和B.inl.hpp
  3. 为了方便起见,“天真”标头A.hpp和B.hpp

每个标头将具有以下特征:

  1. 在A.def.hpp(分别为B.def.hpp)中,您具有B类的前向声明(resp.A),这将使您能够声明对该类的指针/引用
  2. A.inl.hpp(分别为B.inl.hpp)将同时包括A.def.hpp和B.def.hpp,这将使A(分别为B)中的方法能够使用B类(分别为A) 。
  3. A.hpp(分别为B.hpp)将直接包含A.def.hpp和A.inl.hpp(分别为B.def.hpp和B.inl.hpp)
  4. 当然,所有标头都必须自给自足,并由标头防护装置保护

天真的用户将包括A.hpp和/或B.hpp,从而忽略了整个混乱。

具有这种组织结构意味着库编写者可以解决A和B之间的循环依赖关系,同时将两个类都保存在单独的文件中,一旦您了解了该方案,就可以轻松浏览。

请注意,这是一个极端情况(两个模板彼此了解)。我希望大多数代码都不需要该技巧。


您能解释一下使用单独的标头对循环依赖有何不同吗?我不这样认为。即使两个类都在同一个标​​头中完全声明,我们也可以简单地创建循环依赖关系,只需在我们在另一个声明句柄之前预先向前声明一个即可。其他所有内容似乎都是要点,但是分开的标头促进循环依赖的想法似乎遥不可及。
underscore_d

@underscore_d:我在回答中回答了您的问题。请告诉我是否错过了什么。
paercebal

1
谢谢,涵盖了所有内容!实际上,令人尴尬的是,当我进一步阅读时,我开始意识到我以前在模板类上遇到过该问题,并通过.hpp.tpp文件基本上以相同的方式解决了这个问题……但是我的记忆不足以建立关联-也许我阻止了它。:D值得庆幸的是,自那以来,我已经对该区域进行了重构,以减少混乱,因为正如您所指出的那样,这实际上可能是一种很少需要的模式。
underscore_d

11

我们在工作时会这样做,如果类和文件具有相同的名称,则很容易找到东西。至于性能,您实际上不应在单个项目中拥有5000个类。如果这样做,则可能需要进行一些重构。

也就是说,在某些情况下,一个文件中有多个类。那时它只是文件主类的私有帮助器类。


13
你真的不应该在一个项目中有5000个类。当然,您不想一直重建它们,但是如果系统那么大,那么它就那么大。
Billy ONeal 2010年

5
“ ...您真的不应该在一个项目中有5000个类”。这是可怕的建议。您应该有很多课程,因为项目需要合理维护。
birgersp

8

+1分隔。我刚进入一个项目,其中某些类位于不同名称的文件中,或者与另一类混在一起,因此无法快速有效地找到它们。您可以在构建时投入更多资源-您不能弥补程序员的时间浪费,因为他找不到合适的文件进行编辑。


8

除了简单地“清晰”之外,将类分成单独的文件还可以使多个开发人员更轻松地相互踩踏。将更改提交到版本控制工具时,合并将更少。


1
版本控制是一个很好的观点。我还要补充一点,由于同样的原因,这种好处还扩展到了几乎不必担心合并的程序员,包括像我这样独立编写代码的程序员。使用更多和较小的文件可以使提交历史变得更容易阅读,无论是直接使用grep还是使用其他方式。例如,键入要容易git log SmallFile.cpp得多git log GiganticFile.cpp | grep -b4 -a4 SureHopeIPutTheMagicWordInEveryCommitMsg(一个人为的示例,但是您明白了)。
underscore_d

5

我工作过的大多数地方都遵循这种做法。我实际上已经编写了BAE(澳大利亚)的编码标准,并说明了为什么没有理由没有理由仅在石头上雕刻东西的原因。

关于您关于源文件的问题,这不是花太多时间进行编译,而是更多的是能够首先找到相关代码段的问题。并非每个人都在使用IDE。并且知道只查找MyClass.h和MyClass.cpp确实比在多个文件上运行“ grep MyClass *。(h | cpp)”然后过滤掉#include MyClass.h语句节省了时间。

请注意,有一些变通办法可以解决大量源文件对编译时间的影响。有关有趣的讨论,请参见John Lakos的《大规模C ++软件设计》。

您可能还希望阅读Steve McConnell撰写的Code Complete,以获得有关编码准则的出色章节。实际上,这本书是一本不错的书,我会经常回到


3

这样做是很常见的做法,特别是能够在需要它的文件中包含.h。当然,性能会受到影响,但请尽量不要考虑这个问题,直到它出现:)。
最好从分开的文件开始,然后在需要时尝试合并通常一起使用的.h来提高性能。归结为文件之间的依赖关系,这对于每个项目而言都是非常特定的。


3

正如其他人所说,最佳实践是从代码维护和易懂性的角度将每个类放在自己的翻译单元中。但是,在大型系统上,这有时是不可取的-有关权衡的讨论,请参见Bruce Dawson在本文中标题为“使那些源文件更大”的部分。



1

每个文件只有一个类非常有帮助,但是如果您通过包含所有单个C ++文件的bulkbuild文件进行构建,则由于许多编译器的启动时间相对较长,因此编译速度更快。


1

令我惊讶的是,几乎每个人都赞成每堂课一个文件。这样做的问题是,在“重构”时代,保持文件名和类名同步可能很困难。每次更改类名时,都必须也更改文件名,这意味着还必须在包含文件的所有位置进行更改。

我个人将相关的类分组到一个文件中,然后为该文件提供一个有意义的名称,即使类名称发生更改也不必更改。更少的文件也使滚动浏览文件树变得更加容易。我在Windows上使用Visual Studio,在Linux上使用Eclipse CDT,它们都有快捷键,可以直接转到类声明,因此查找类声明既简单又快捷。

话虽如此,我认为一旦项目完成或者其结构已经“巩固”,并且名称更改变得罕见,那么每个文件只有一个类是有意义的。我希望有一个可以提取类并将其放置在不同的.h和.cpp文件中的工具。但是我认为这不是必不可少的。

选择还取决于一个项目的类型。在我看来,这个问题不值得一black而就,因为这两种选择都有其优缺点。


AFAICT在主管的开发环境中更改一堆文件名完全不是问题。我的意思是,在shell中,您只需使用已经用来替换文件中名称的命令并对其进行稍微更改:sed -i 's/\<TheClassName\>/TheNewName/g' In*.?pp=> rename 's/\<TheClassName\>/TheNewName/' In*.?pp。如有必要,重复几个子目录。而且,如果这种方法sed在没有冲突的情况下起作用(或者您写了一个更精确的方法),那么这种rename方法更有可能实现。我几乎没有使用GUI IDE的经验,但是希望它们能使这变得更加容易,所以我没有看到任何问题。
underscore_d

0

同样的规则在这里适用,但是它在允许的地方记录了一些例外,就像这样:

  • 继承树
  • 仅在非常有限的范围内使用的类
  • 某些实用程序只是放在通用的“ utils.h”中

0

两个字:奥康的剃刀。每个文件保留一个类,并将相应的标头保存在单独的文件中。如果不这样做,就像每个文件都保留一个功能,那么就必须创建关于什么构成一个功能的各种规则。每个文件只保留一个类,可以带来更多的收益。而且,即使一半不错的工具也可以处理大量文件。保持简单,我的朋友。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.