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

在大型C / C ++项目上工作时,您是否对源文件或头文件中的#include有一些特定的规则?

例如,我们可以想象遵循以下两个过度规则之一:

  1. .h文件中禁止#include; 由每个.c文件决定是否需要它的所有标头
  2. 每个.h文件应包括其所有依赖项,即 它应该能够单独编译而不会出现任何错误。

我想任何项目之间都需要权衡取舍,但是您的计划是什么? 您有更具体的规则吗? 或任何寻求任何解决方案的链接?

calandoa asked 2020-02-13T07:46:23Z
13个解决方案
39 votes

仅在C文件中包含.h意味着如果我仅包含标头(定义要在C文件中使用的内容)可能会失败。 它可能会失败,因为我必须预先包含其他20个标头。 更糟糕的是,我必须以正确的顺序包括它们。 从长远来看,由于存在大量的.h文件,因此该系统最终会变成管理地狱。 您只想在一个.c文件中包含一个.h文件,然后花2个小时来找出所需的其他.h文件以及必须按什么顺序包含它们。

如果.h文件需要另一个.h文件成功地包含在一个C文件中,该C文件除了该.h文件之外不包含其他任何内容,并且不会引起编译错误,我将在.h文件本身中包含另一个.h文件。 通过这种方式,我可以确定每个C文件都可以包含每个.h文件,并且绝不会导致错误。 .c文件从不必担心要导入哪个其他.h文件或以哪种顺序包括它们。 即使在大型项目(1000个.h文件及更高版本)中也可以使用。

另一方面,如果没有必要,我绝不会在另一个文件中添加.h文件。 例如。 如果我有hashtable.h和hashtable.c,并且hashtable.c需要hashing.h,但是hashtable.h不需要它,那么我就不在其中了。 我只将其包括在.c文件中,因为其他.c文件(包括hashtable.h)也不需要hashing.h,并且如果出于某种原因需要它,则应将其包括在内。

Mecki answered 2020-02-13T07:46:44Z
16 votes

我认为这两个建议的规则都是不好的。 就我而言,我总是适用:

仅包括仅使用此头文件中定义的文件编译文件所需的头文件。 这表示:

  1. 仅作为引用或指针存在的所有对象都应向前声明
  2. 包括所有定义标头本身中使用的功能或对象的标头。
PierreBdR answered 2020-02-13T07:47:17Z
13 votes

我会使用规则2:

所有标头都应自给自足,方法是:

  • 不使用其他地方定义的任何东西
  • 转发声明其他地方定义的符号
  • 包括定义了无法向前声明的符号的标头。

因此,如果您有一个空的C / C ++源文件,则应正确编译包含标头的文件。

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

请注意,使用模板会使此验证“包括您的标头的空源应编译”更加复杂(并且很有趣...)。

paercebal answered 2020-02-13T07:48:11Z
8 votes

一旦存在循环依赖性,第一个规则将失败。 因此不能严格应用。

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

我都赞成规则2(尽管最好包括“转发声明标头”而不是真正的协议,例如<iosfwd>,因为这样可以减少编译时间)。 通常,我认为,如果头文件“声明”它具有什么依赖关系,那是一种自我说明文件,还有什么比包括所需文件更好的方法呢?

编辑:

在评论中,我一直受到挑战,标题之间的循环依赖关系是不良设计的标志,应避免使用。

那是不对的。 实际上,类之间的循环依赖可能是不可避免的,并且根本不是不良设计的迹象。 例子很多,让我只提及观察者模式,该模式在观察者和主题之间具有循环引用。

要解决类之间的循环问题,必须使用前向声明,因为声明的顺序在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
Konrad Rudolph answered 2020-02-13T07:49:14Z
4 votes

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

David Sykes answered 2020-02-13T07:49:35Z
4 votes
  1. 始终具有某种标题保护。
  2. 不要通过在标题中放置任何using namespace语句来污染用户的全局名称空间。
Ferruccio answered 2020-02-13T07:49:59Z
1 votes

我建议选择第二种方法。 您通常会遇到这样的情况,您想向一个头文件中添加somwhing,而突然需要另一个头文件。 使用第一个选项,您将不得不遍历并更新许多C文件,有时甚至不在您的控制之下。 使用第二个选项,您只需更新头文件,甚至不需要您刚刚添加的新功能的用户甚至都不知道您已完成了操作。

dkagedal answered 2020-02-13T07:50:28Z
1 votes

第一种选择(标头中没有#includes)对我来说是主要的禁止。 我想要自由地#include我可能需要的任何内容,而不必担心手动#include也要依赖其依赖项。 因此,总的来说,我遵循第二条规则。

关于循环依赖,我个人的解决方案是按照模块而不是类来构造项目。 在模块内部,所有类型和功能可能相互之间具有任意依赖关系。 跨模块边界,模块之间可能没有循环依赖关系。 对于每个模块,都有一个* .hpp文件和一个* .cpp文件。 这样可以确保头中的任何前向声明(循环依赖所必需,只能在模块内部发生)最终始终在同一头中解决。 不需要任何仅用于前向声明的标头。

pyon answered 2020-02-13T07:50:54Z
0 votes

铂 1当您想通过某个特定的头文件预编译头文件时失败; 例如。 这就是StdAfx.h在Visual Studio中的作用:将所有常见的标头放在此处...

Marcin Gil answered 2020-02-13T07:51:14Z
0 votes

这归结为界面设计:

  1. 始终通过引用或指针传递。 如果您不想检查指针,请通过参考。
  2. 尽可能向前声明。
  3. 切勿在班级中使用new-创建工厂来为您做到这一点并将它们传递给班级。
  4. 切勿使用预编译的头文件。

在Windows中,我的stdafx仅包含afx ___。h标头-没有字符串,向量或boost库。

graham.reeds answered 2020-02-13T07:51:56Z
0 votes

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

正如其他几个人提到的那样,技巧是尽可能使用前向声明,即是否使用引用或指针。 为了以这种方式最小化构建依赖关系,pimpl习惯用法可能很有用。

andreas buykx answered 2020-02-13T07:52:22Z
0 votes

我同意Mecki的说法,

对于您项目中的每个foo.h,仅包含制作它们所需的标头

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

编译。

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

peterchen answered 2020-02-13T07:52:58Z
0 votes

我个人是这样的:
1请预先声明在.h文件中包括其他.h文件。 如果可以在该.h文件或类中将某些内容用作指针/引用,则可以进行前向声明而不会产生编译错误。 这可能使标头较少包含依赖项(节省编译时间?不确定:()。
2使.h文件简单或特定。 例如 在名为CONST.h的文件中定义所有常量是不好的,最好将它们分成多个常量,例如CONST_NETWORK.h,CONST_DB.h。 因此,要使用一个数据库常数,就不必包含有关网络的其他信息。
3不要将实现放在标头中。 标头用于为他人快速查看公共事物; 实施它们时,请勿污染其他细节的声明。

Steel answered 2020-02-13T07:53:37Z
translate from https://stackoverflow.com:/questions/181921/your-preferred-c-c-header-policy-for-big-projects