C++头文件 - 包含时的最佳实践

8

C++头文件

如果我有A.cpp和A.h,以及b.h、c.h、d.h,那么我应该怎么做:

在A.h中:

#include "b.h"
#include "c.h"
#include "d.h"

in A.cpp:

#include "A.h"

或者

在A.cpp文件中:

#include "A.h"
#include "b.h"
#include "c.h"
#include "d.h"

有性能问题吗?明显的好处?这方面有什么不好的东西吗?

6个回答

25

为编译程序只需包含必要的内容;添加不必要的包含将会损害编译时间,特别是在大型项目中。

每个头文件应该能够独立编译 -- 也就是说,如果您有一个仅包括该头文件的源文件,那么它应该可以无错误地编译。头文件应该仅包含必要的内容。

尽可能使用前置声明。如果您正在使用一个类,但头文件只处理该类的对象指针/引用,则不需要包含该类的定义 -- 只需使用前置声明:

class SomeClass;
// Can now use pointers/references to SomeClass
// without needing the full definition

@Adam - 你有前向声明的例子吗?什么时候适合使用,什么时候不适合使用? - JT.
1
好建议。我还建议将与 .h 相关的 .c 文件作为第一个 include 包含 .h,这样可以确保所有的 .h 文件都可以单独编译(而不是依赖于 .c 文件中的 include 顺序)。 - Edan Maor
对于最后一部分加上 +1,我之前总是包含头文件。 - Daniel Sloof
如果你通过值返回 SomeClass,你也可以使用前向声明。 - Frerich Raabe
求职者还可以参考下面@FrerichRaabe的答案。 - ParokshaX
是的,头文件本身应该是自包含的。它应该包含所有它需要的依赖项。 - Houcheng

6

这里的关键做法是在每个foo.h文件周围放置一个类似于以下的守卫:

#ifndef _FOO_H
#define _FOO_H

...rest of the .h file...
#endif

这样可以防止多次包含,循环等相关问题。一旦确保每个包含文件都受到了保护,具体细节就不那么重要了。

我喜欢Adam表达的一个指导原则:确保如果一个源文件只包含a.h,它不会因为a.h假设其他文件已经被包含而不可避免地出现错误--例如,如果a.h需要在之前包含b.h,它可以并且应该直接包含b.h本身(如果b.h之前已经被包含了,则守卫将使其成为无操作)

反之,源文件应该包含它所需要的头文件(宏、声明等),而不是假设其他头文件会神奇地出现,因为它已经包含了一些头文件。

如果您正在使用类的值,那么您需要在某个.h中包含所有令人讨厌的类的详细信息。但对于一些通过引用或指针使用的情况,只需要一个裸的class sic;即可。例如,如果类a可以使用类b的指针(即成员class b * my_bp;而不是成员class b * my_b;),则包含文件之间的耦合可以变得更弱(减少大量重新编译)--例如,b.h可能只有class b;这么简单,而所有令人讨厌的细节都在b_impl.h中,它只被真正需要它的头文件包含...


考虑到每次包含时都会读取foo.h,因此它并不是真正的无操作。如果您这样做:#ifndef _FOO_H #include "foo.h" #endif,那么它将是一个无操作。 - Bill
是的,但这样做将使每个包含变得难以阅读。从实际情况来看,多次重复的包含文件已经存在于文件系统的缓存中,因此再次"包含"它不会影响性能 - 而且从语义角度来说,这只是一个空操作,这才是真正重要的。 - Alex Martelli

3
亚当·罗森菲尔德告诉你的是完全正确的。可以使用前向声明的一个例子是:
#ifndef A_H_
#define A_H_

    #include "D.h"
    class B;  //forward declaration
    class C;  //forward declaration

    class A
    {
    B *m_pb;  //you can forward declare this one bacause it's a pointer and the compilier doesn't need to know the size of object B at this point in the code.  include B.h in the cpp file.
    C &m_rc;  //you can also forware declare this one for the same reason, except it's a reference.

    D m_d;    //you cannot forward declare this one because the complier need to calc the size of object D.

    };

#endif

希望能够构建成功,我没有测试过,哈哈。如果有问题请告诉我。 - cchampion

3
答案:只有在构建成功需要时,让A.h包含b.hc.hd.h。经验法则是:只在头文件中包含必要的代码。任何不立即需要的内容都应由A.cpp包含。
推理:头文件中包含的代码越少,就越不可能在某个地方进行更改后需要重新编译使用头文件的代码。如果在不同头文件之间存在大量的#include引用,则更改其中任何一个将需要递归地重新构建所有包含更改的头文件的其他文件。因此,如果您决定触摸某个顶级头文件,则可能会重建代码的大部分。
通过在头文件中尽可能使用前向声明,可以减少源文件的耦合,从而使构建速度更快。这种前向声明可以在许多情况下使用,比您想象的要多得多。作为经验法则,仅在定义类型T的头文件t.h中需要它。
  1. 声明一个类型为T的成员变量(注意,这不包括声明指向T的指针)。
  2. 编写一些内联函数来访问T类型对象的成员。

如果您的头文件仅:

  1. 声明构造函数/函数,这些函数接受对T对象的引用或指针。
  2. 声明通过指针、引用或值返回T对象的函数。
  3. 声明是指向T对象的引用或指针的成员变量。

考虑这个类的声明;您真正需要包含哪些ABC的头文件呢?

class MyClass
{
public:
    MyClass( const A &a );

    void set( const B &b );
    void set( const B *b );
    B getB();
    C getC();

private:
    B *m_b;
    C m_c;
};

你只需要包含类型C的头文件,因为它有成员变量m_c。通常情况下,你可以通过不直接声明成员变量,而是使用不透明指针将所有成员变量隐藏在私有结构中来消除这个要求,这样它们就不会再出现在头文件中了。

你在示例中混淆了A、B和C与X、Y和Z。 - Bill
@Bill:你说得对,我已经将示例调整为现在的A、B、C。 - Frerich Raabe
@FrerichRaabe 的回答非常完美。对于“推理:”部分给予加1。 - ParokshaX

0

最好不要在其他头文件中包含头文件 - 这会减慢编译速度并导致循环引用。


@mgd - 如果我的头文件包含了10个include,而源代码中有10个不同的include,那么我应该将头文件中的include移动到源代码中,使得源代码中有20个include吗? - JT.
请参考Alex的更完整的答案。 唯一的例外是如果您有预编译头文件,在这种情况下,请将所有永远不会更改的平台/框架/操作系统头文件包含到一个文件中,并在任何地方都包含它。 - Martin Beckett

0

不会有性能问题,所有操作都在编译时完成,您的标题应该设置好,以确保它们不会被包含多次。

我不知道是否有标准的方法来做这件事,但我更喜欢在源文件中包含所有需要的头文件,并在头文件中包含它们,如果头文件本身需要它(例如,从另一个头文件中的 typedef)


@Jeff - 啊对了,标题中的元素可能需要它。但是如果所有标题都在源文件中,它们不会获得吗? - JT.

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