您对大项目的首选C / C ++标头策略?

时间:2008-10-08 09:19:13

标签: c++ c coding-style include header

在处理大型C / C ++项目时,您是否在源文件或头文件中有关于 #include 的特定规则?

例如,我们可以想象遵循这两个过分规则中的一个:

    .h 文件中禁止
  1. #include ;每个 .c 文件都包含所需的所有标题
  2. 每个 .h 文件应包含其所有依赖项,即它应该能够单独编译而不会出现任何错误。
  3. 我认为任何项目都需要权衡,但你的是什么?你有更具体的规则吗?或任何解决任何解决方案的链接?

13 个答案:

答案 0 :(得分:38)

仅在C文件中包含.h意味着如果我只包含一个标题(定义我想在C文件中使用的内容)可能会失败。它可能会失败,因为我必须预先包含20个其他标题。更糟糕的是,我必须将它们包含在正确的顺序中。有了很多.h文件,从长远来看,这个系统最终会成为一个管理地狱。您只想在一个.c文件中包含一个.h文件,并且花费2个小时来查找您需要的其他.h文件以及必须包含它们的顺序。

如果.h文件需要另一个.h文件成功包含在一个C文件中,除了这个.h文件之外不包含任何其他内容而且不会导致编译错误,我会在.h中包含另一个.h文件文件本身。这样我就确定每个C文件都可以包含每个.h文件,它永远不会导致错误。 .c文件永远不必担心要导入哪些其他.h文件或以哪种顺序包含它们。这适用于大型项目(1000 .h文件及以上)。

另一方面,如果没有必要,我永远不会将.h文件包含在另一个文件中。例如。如果我有hashtable.h和hashtable.c,而hashtable.c需要hashing.h,但hashtable.h不需要它,我不在那里包含它。我只将它包含在.c文件中,因为包括hashtable.h在内的其他.c文件也不需要hashing.h,如果他们因任何原因需要它,它们应该包含它。

答案 1 :(得分:17)

我认为两条建议的规则都很糟糕。就我而言,我总是申请:

仅包含仅使用此标头中定义的文件编译文件所需的头文件。这意味着:

  1. 仅作为参考或指针出现的所有对象应该是前向声明的
  2. 包括定义标题本身中使用的函数或对象的所有标题。

答案 2 :(得分:13)

我会使用规则2:

所有标题都应该是自给自足的,无论如何:

  • 没有使用别处定义的任何内容
  • 转发声明在别处定义的符号
  • 包括定义无法向前声明的符号的标题。

因此,如果你有一个空的C / C ++源文件,包括一个标题应该正确编译。

然后,在C / C ++源文件中,仅包含必要的内容:如果HeaderA正向声明了在HeaderB中定义的符号,并且您使用此符号,则必须同时包含两个...好消息是如果您不使用前向声明的符号,那么您将只能包含HeaderA,并避免包含HeaderB。

请注意,使用模板会使此验证“包含您的标题应该编译的空源”更复杂一些(并且很有趣......)

答案 3 :(得分:8)

只要存在循环依赖关系,第一条规则就会失败。所以它不能严格应用。

(这仍然可以工作,但这会将大量工作从程序员转移到这些库的消费者,这显然是错误的。)

我全都支持规则2(尽管在<iosfwd>中包含“前向声明标题”而不是真正的交易可能会很好,因为这会减少编译时间)。一般来说,我认为如果头文件“声明”它具有哪些依赖关系,它是一种自我文档 - 还有什么比包含所需文件更好的方法?

编辑:

在评论中,我一直受到质疑,标题之间的循环依赖性是设计不良的标志,应该避免。

这不正确。实际上,类之间的循环依赖可能是不可避免的,并且根本不是糟糕设计的标志。例子很丰富,我只想提一下Observer模式,它在观察者和主体之间有一个循环引用。

要解决类之间的循环,必须使用前向声明,因为声明的顺序在C ++中很重要。现在,以循环方式处理此前向声明以减少整个文件的数量并集中代码是完全可以接受的。无可否认,以下情况不符合这种情况,因为只有一个前向声明。但是,我已经在一个图书馆里工作了很多。

// observer.hpp

class Observer; // Forward declaration.

#ifndef MYLIB_OBSERVER_HPP
#define MYLIB_OBSERVER_HPP

#include "subject.hpp"

struct Observer {
    virtual ~Observer() = 0;
    virtual void Update(Subject* subject) = 0;
};

#endif

// subject.hpp
#include <list>

struct Subject; // Forward declaration.

#ifndef MYLIB_SUBJECT_HPP
#define MYLIB_SUBJECT_HPP

#include "observer.hpp"

struct Subject {
    virtual ~Subject() = 0;
    void Attach(Observer* observer);
    void Detach(Observer* observer);
    void Notify();

private:
    std::list<Observer*> m_Observers;
};

#endif

答案 4 :(得分:4)

2.h文件的最小版本仅包含它特别需要编译的头文件,使用前向声明和pimpl尽可能多。

答案 5 :(得分:4)

  1. 总是有一些头盔。
  2. 不要在标题中放入任何using namespace语句来污染用户的全局命名空间。

答案 6 :(得分:1)

我建议使用第二个选项。您经常会遇到想要将somwhing添加到突然需要另一个头文件的头文件的情况。使用第一个选项,您将不得不通过并更新大量C文件,有时甚至不在您的控制之下。使用第二个选项,您只需更新头文件,甚至不需要您刚刚添加的新功能的用户甚至不需要知道您这样做了。

答案 7 :(得分:1)

第一个替代方案(标题中没有#include)对我来说是一个重要的禁忌。我想自由#include无论我需要什么,而不必担心手动#include它的依赖关系。所以,一般来说,我遵循第二条规则。

关于循环依赖,我的个人解决方案是根据模块而不是类来构建我的项目。在模块内部,所有类型和功能可能彼此具有任意依赖性。跨模块边界,模块之间可能没有循环依赖关系。对于每个模块,都有一个* .hpp文件和一个* .cpp文件。这确保了标头中的任何前向声明(循环依赖所必需的,只能在模块内发生)最终总是在同一个头内解析。无需任何前向声明标题。

答案 8 :(得分:0)

的Pt。如果您希望通过某个标头预编译标头,则1会失败;例如。这就是StrAfx.h在VisualStudio中的用途:您将所有常用标题放在那里......

答案 9 :(得分:0)

这取决于界面设计:

  1. 始终通过引用或指针传递。如果您不打算检查指针,请转过 参考。
  2. 尽可能向前宣布。
  3. 永远不要在类中使用new - 创建工厂为您执行此操作并将它们传递给类。
  4. 绝不使用预编译的标题。
  5. 在Windows中,我的stdafx只包含afx ___。h header - 没有字符串,向量或boost库。

答案 10 :(得分:0)

规则nr。 1将要求您以非常特定的顺序列出您的头文件(包括基类的文件必须在包含派生类的文件之前等),如果您的订单错误,这很容易导致编译错误。

正如其他几个人所提到的,诀窍是尽可能使用前向声明,即使用引用或指针。为了以这种方式最小化构建依赖性,pimpl成语可能很有用。

答案 11 :(得分:0)

我同意Mecki,把它缩短,

对于项目中的每个foo.h,都包含 那些需要生成的标题

// foo.c
#include "any header"
// end of foo.c

编译。

(当使用预编译头时,当然允许它们 - 例如MSVC中的#include“stdafx.h”)

答案 12 :(得分:0)

我个人这样做:
1 Perfer forward声明在.h文件中包含其他.h文件。如果某些内容可以用作.h文件或类中的指针/引用,则可以在没有编译错误的情况下进行前向声明。这可能会使标题更少包含依赖项(节省编译时间?不确定:()。
2使.h文件简单或具体。例如在一个名为CONST.h的文件中定义所有constance是不好的,最好将它们分成多个,如CONST_NETWORK.h,CONST_DB.h。因此,要使用一个DB的constance,它不需要包含有关网络的其他信息 3不要将实现放在头文件中。标题用于快速审查其他人的公共事物;在实施它们时,不要污染其他人的详细声明。