在处理大型 C/C++ 项目时,您是否对源文件或头文件中的#include有一些特定规则?
例如,我们可以想象遵循以下两个过度规则之一:
- .h文件中禁止使用#include ;由每个.c文件来包含它需要的所有头文件
- 每个.h文件都应包含其所有依赖项,即它应该能够单独编译而不会出现任何错误。
我想任何项目之间都有权衡,但你的呢?你有更具体的规则吗?或者任何支持任何解决方案的链接?
在处理大型 C/C++ 项目时,您是否对源文件或头文件中的#include有一些特定规则?
例如,我们可以想象遵循以下两个过度规则之一:
我想任何项目之间都有权衡,但你的呢?你有更具体的规则吗?或者任何支持任何解决方案的链接?
如果您仅将 H 文件包含到 C 文件中,则将 H 文件包含到 C 文件中可能会导致编译失败。它可能会失败,因为您可能必须预先包含 20 个其他 H 文件,更糟糕的是,您必须以正确的顺序包含它们。拥有大量 H 文件,从长远来看,该系统最终将成为管理方面的噩梦。您想要做的只是包含一个 H 文件,结果您花了两个小时找出还需要包含哪些其他 H 文件。
如果一个 H 文件只有在先包含另一个 H 文件的情况下才能成功包含到 C 文件中,那么第一个 H 文件应该包含第二个 H 文件,依此类推。这样,您可以简单地将每个 H 文件包含到您喜欢的每个 C 文件中,而不必担心这可能会破坏编译。这样,您只需指定您的直接依赖项,但如果这些依赖项本身也有依赖项,则由他们来指定这些依赖项。
另一方面,如果没有必要,不要将 H 文件包含到 H 文件中。hashtable.h
应该只包含使用哈希表实现所需的其他头文件。如果实现本身需要hashing.h
,则将其包含在 中hashtable.c
,而不是包含在 中hashtable.h
,因为只有实现需要它,而不是只想使用最终哈希表的代码。
我认为这两个建议的规则都很糟糕。就我而言,我总是申请:
仅包含使用此标头中定义的内容编译文件所需的头文件。这表示:
我会使用规则2:
所有标题都应该是自给自足的,可以通过:
因此,如果您有一个空的 C/C++ 源文件,包括头文件应该可以正确编译。
然后,在 C/C++ 源文件中,仅包含必要的内容:如果 HeaderA 前向声明了在 HeaderB 中定义的符号,并且您使用此符号,则必须同时包含这两者……好消息是,如果您不要使用前向声明的符号,那么您将能够仅包含 HeaderA,并避免包含 HeaderB。
请注意,使用模板会使此验证“包括您的标头在内的空源应该编译”变得更加复杂(并且有趣......)
一旦存在循环依赖,第一条规则就会失效。所以不能严格应用。
(这仍然可以工作,但这将大量工作从程序员转移到这些库的消费者,这显然是错误的。)
我完全赞成规则 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
using namespace
语句放在标题中来污染用户的全局命名空间。2. .h 文件的最小版本仅包括它特别需要编译的头文件,尽可能多地使用前向声明和 pimpl。
我建议使用第二个选项。您经常会遇到这样的情况:您想要将 somwhing 添加到突然需要另一个头文件的头文件中。使用第一个选项,您将不得不检查并更新大量 C 文件,有时甚至不受您的控制。使用第二个选项,您只需更新头文件,甚至不需要您刚刚添加的新功能的用户甚至不需要知道您做了。
第一种选择(#include
标题中没有 s)对我来说是一个主要的禁忌。我想自由地使用#include
我可能需要的任何东西,而不必担心手动设置#include
它的依赖项。所以,总的来说,我遵循第二条规则。
关于循环依赖,我个人的解决方案是根据模块而不是根据类来构建我的项目。在一个模块内部,所有类型和函数可能相互之间具有任意依赖关系。跨模块边界,模块之间可能不存在循环依赖关系。对于每个模块,都有一个 *.hpp 文件和一个 *.cpp 文件。这确保了标头中的任何前向声明(循环依赖所必需的,只能发生在模块内部)最终总是在同一个标头中解析。不需要任何仅前向声明的标头。
我同意 Mecki 的观点,简而言之,
对于您项目中的每个 foo.h仅包含那些需要制作的标头
// foo.c
#include "any header"
// end of foo.c
编译。
(当使用预编译的头文件时,它们当然是允许的——例如 MSVC 中的 #include "stdafx.h")
我个人是这样做的:
1 Perfer forward 声明以在 .h 文件中包含其他 .h 文件。如果某些东西可以用作该 .h 文件或类中的指针/引用,则可以进行前向声明而不会出现编译错误。这可以使标题更少包含依赖项(节省编译时间?不确定:()。
2 使 .h 文件简单或具体。例如,在一个名为 CONST.h 的文件中定义所有常量是不好的,最好将它们分成多个,如 CONST_NETWORK.h, CONST_DB.h。所以使用一个 DB 的 constance,它不需要包含有关网络的其他信息
。3 不要将实现放在 headers。headers 用于快速查看其他人的公共事物; 实施时,不要污染他人的详细声明。
铂。当您希望通过某个标头预编译标头时,1 失败;例如。这就是 StdAfx.h 在 VisualStudio 中的用途:您将所有常见的标头放在那里...
这归结为界面设计:
在 Windows 中,我的 stdafx 只包含 afx___.h 标头 - 没有字符串、向量或 boost 库。
规则编号。1 将要求您以非常特定的顺序列出头文件(基类的包含文件必须在派生类的包含文件等之前),如果您弄错顺序,很容易导致编译错误。
正如其他几个人所提到的,诀窍是尽可能多地使用前向声明,即如果使用引用或指针。为了以这种方式最小化构建依赖关系,pimpl习惯用法可能很有用。