大型项目的首选C / C ++标题策略?
在处理一个大的C / C ++项目时,你有关于源代码或头文件中#include的一些特定规则吗?
例如,我们可以设想遵循这两个过分的规则之一:
- .h文件中禁止#include ; 每个.c文件都要包含所有需要的头文件
- 每个.h文件都应该包含所有的依赖关系,即它应该能够单独编译而不会出现任何错误。
我想在任何项目之间都有权衡,但是你的是什么? 你有更具体的规则吗? 或任何解决scheme争论的任何链接?
做.h只包含在C文件中意味着如果我只包含一个头文件(定义我想在C文件中使用的文件)可能会失败。 它可能会失败,因为我必须预先包含20个其他标头。 更糟糕的是,我必须按照正确的顺序包括它们。 有了很多的.h文件,从长远来看,这个系统最终会成为一个pipe理的地狱。 你只想把一个.h文件包含在一个.c文件中,然后花费两个小时来找出你需要的其他.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,并且无论出于何种原因需要它们,都应该包含它。
我认为这两个build议的规则是不好的。 在我的部分,我总是应用:
仅包含使用此头中定义的文件编译文件所需的头文件。 意即:
- 所有仅作为参考或指针存在的对象应该被前向声明
- 包含所有标题,用于定义标题中使用的函数或对象。
我会使用规则2:
所有的标题应该是自给自足的,不pipe是:
- 不使用别处定义的任何东西
- 正向声明在别处定义的符号
- 包括定义不能被前向声明的符号的头文件。
因此,如果你有一个空的C / C ++源文件,包括一个头文件应该能够正确编译。
然后,在C / C ++源文件中,只包含必要的内容:如果HeaderA向前声明了在HeaderB中定义的符号,并且使用这个符号,则必须同时包含…好消息是,如果你不要使用前面声明的符号,那么你将只能包含HeaderA,并避免包含HeaderB。
请注意,使用模板使这个validation“包括您的头文件的空源代码应该编译”有点复杂(和有趣的…)
一旦有循环依赖关系,第一条规则就会失败。 所以不能严格适用。
(这仍然可以起作用,但是这会把很多工作从程序员转移到这些库的消费者身上,这显然是错误的。)
我都赞同规则2(尽pipe可能包含“forward declaration headers”而不是真正的交易,如<iosfwd>
因为这会减less编译时间)。 一般来说,如果头文件“声明”它具有什么依赖关系,那么我相信这是一种自我logging – 还有什么比包含所需文件更好的方法呢?
编辑:
在评论中,我一直质疑,标题之间的循环依赖是一个糟糕的devise的标志,应该避免。
这是不正确的。 实际上, 类之间的循环依赖关系可能是不可避免的,并不是一个糟糕的devise的标志。 例子非常丰富,我只想提一下在观察者和主题之间有循环引用的观察者模式。
为了解决类之间的循环性,你必须使用前向声明,因为声明的顺序在C ++中很重要。 现在,以循环的方式处理这个前向声明是完全可以接受的,以减less整个文件的数量并集中代码。 无可否认,以下情况不适用于这种情况,因为只有一个前向声明。 不过,我已经在一个更多的图书馆工作。
// 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
2.h文件的最小版本只包含它特别需要编译的头文件,使用前向声明和pimpl尽可能多。
- 总是有一些头球卫。
- 不要通过将任何
using namespace
语句放在标题中using namespace
污染用户的全局名称using namespace
。
我build议去第二个选项。 通常情况下,您最终需要将头文件添加到突然需要另一个头文件的头文件中。 有了第一个选项,你将不得不通过并更新大量的C文件,有时甚至不在你的控制之下。 有了第二个选项,你只需要更新头文件,而那些甚至不需要你刚添加的新function的用户也不需要知道你做了什么。
第一个替代scheme(头文件中没有#include
)对我来说是一个主要的禁忌。 我想自由地#include
我可能需要的任何东西,而不用担心手动#include
它的依赖关系。 所以,一般来说,我遵循第二条规则。
关于循环依赖,我个人的解决scheme是用模块而不是按照类来构build我的项目。 在模块内部,所有types和函数都可以相互依赖。 在模块边界上,模块之间可能没有循环依赖关系。 对于每个模块,都有一个* .hpp文件和一个* .cpp文件。 这确保了头中的任何前向声明(对于只能在模块内发生的循环依赖所必需的)最终总是在同一头文件中被parsing。 不需要任何前向声明标题。
铂。 1当你想通过某个头部预编译头文件时失败; 例如。 这是什么StdAfx.h在VisualStudio中:你把所有常见的标题在那里…
这归结为接口devise:
- 总是通过引用或指针传递。 如果你不打算检查指针,通过引用传递。
- 向前申报尽可能多。
- 不要在课堂上使用新的东西 – build立工厂来为你做,并把它们传递给课堂。
- 切勿使用预编译的标头。
在Windows中,我的stdafx只包含afx ___。h头文件 – 没有string,vector或boost库。
规则nr。 1将要求你以一个非常特定的顺序列出你的头文件(包含基类的文件必须在包含派生类的文件之前等),如果你的命令错误,很容易导致编译错误。
诀窍是,正如其他几个人所提到的,尽可能地使用前向声明,即如果使用引用或指针。 为了以这种方式最小化构build依赖性, pimpl成语可能是有用的。
我同意麦基,把它缩短,
对于项目中的每个foo.h, 都只包含需要创build的头文件
// foo.c #include "any header" // end of foo.c
编译。
(当使用预编译头文件时,它们是被允许的,当然 – 例如MSVC中的#include“stdafx.h”)
我个人是这样做的:
1 Perfer向前声明将其他.h文件包含在.h文件中。 如果在.h文件或类中可以使用某些指针/引用,那么forward declare是可能的,而不会出现编译错误。 这可能会使头less包括依赖项(节省编译时间?不知道:()。
2使.h文件简单或特定。 例如,在名为CONST.h的文件中定义所有的常量是不好的,最好将它们分成多个,如CONST_NETWORK.h,CONST_DB.h。 所以要使用DB的一个常量,就不需要包含其他关于networking的信息。
3不要把实现放在标题中。 标题用于快速审查其他人的公共事物; 在实施时,不要污染他人详细的声明。