.h文件应该放入什么?


93

将您的代码分成多个文件时,.h文件中到底应该放什么,.cpp文件中应该放什么?



7
这是一个纯粹的样式问题,但是我相信C ++声明放入.hpp文件中,而C声明放入.h文件中。这在混合C和C ++代码(例如C中的旧模块)时非常有用。
Thomas Matthews

@ThomasMatthews很有道理。经常使用这种做法吗?
ty

@lightningleaf:是的,通常在混合C ++和C语言时经常使用这种做法。
托马斯·马修斯

Answers:


113

头文件(.h)旨在提供多个文件中所需的信息。诸如类声明,函数原型和枚举之类的东西通常放在头文件中。一句话,就是“定义”。

代码文件(.cpp)用于提供只需要在一个文件中知道的实现信息。通常,文件中应该包含/不应该被其他模块访问的函数主体和内部变量.cpp。一句话,“实现”。

问自己确定属于哪个问题的最简单的问题是:“如果更改此内容,是否必须更改其他文件中的代码以使事情再次编译?” 如果答案为“是”,则它可能属于头文件;如果答案为“否”,则它可能属于代码文件。


4
除私有类数据外,其他数据都必须进入标头。模板必须完全由标头定义(除非您使用少数支持的编译器之一export)。围绕#1的唯一方法是PIMPL。如果export受支持,则可能会#2,并且可能会使用c ++ 0x和extern模板。IMO,c ++中的头文件失去了很多用处。
KitsuneYMG,2009年

23
都不错,但是用的术语不正确。一句话,“声明”-术语“定义”与“实现”同义。标头中应仅包含声明性代码,内联代码,宏定义和模板代码;即没有实例化代码或数据的东西。
克利福德,

8
我必须同意克利福德。您使用术语“声明”和“定义”相当松散,并且有些可互换。但是它们在C ++中具有精确的含义。示例:类声明引入了类的名称,但未说明其中的内容。类定义列出了所有成员和朋友功能。两者都可以毫无问题地放入头文件中。您所谓的“函数原型”是函数声明。但是函数定义是包含函数代码的东西,应该放置在cpp文件中-除非它是内联的或模板的一部分。
sellibitze

5
它们在C ++中具有精确的含义,在英语中不具有精确的含义。我的答案写在后者。
琥珀色

54

实际上,在C ++中,这比C标头/源组织要复杂得多。

编译器会看到什么?

编译器会看到一个大型源文件(.cpp),其中包含正确的标头。源文件是将被编译成目标文件的编译单元。

那么,为什么需要标题呢?

因为一个编译单元可能需要有关另一编译单元中的实现的信息。因此,可以在一个源中编写例如函数的实现,然后在需要使用它的另一源中编写该函数的声明。

在这种情况下,有两个相同信息的副本。哪个是邪恶的...

解决方案是共享一些细节。尽管实现应保留在Source中,但可能需要共享共享符号的声明(例如函数)或结构,类,枚举等的定义。

标头用于放置那些共享的详细信息。

将需要在多个源之间共享的内容的声明移到标头

而已?

在C ++中,可以在标头中放入其他一些内容,因为它们也需要共享:

  • 内联代码
  • 范本
  • 常量(通常是您要在开关内部使用的常量...)

移至标题“所有需要共享的内容”,包括共享的实现

这是否意味着标题内可能有源?

是。实际上,“标头”中可能有很多不同的东西(即在源之间共享)。

  • 转发声明
  • 函数/结构/类/模板的声明/定义
  • 内联和模板化代码的实现

它变得复杂,并且在某些情况下(符号之间的循环依赖性),不可能将其保留在一个标头中。

标头可分为三部分

这意味着,在极端情况下,您可能会:

  • 前向声明头
  • 声明/定义头
  • 一个实现头
  • 实现源

假设我们有一个模板化MyObject。我们可以有:

// - - - - MyObject_forward.hpp - - - - 
// This header is included by the code which need to know MyObject
// does exist, but nothing more.
template<typename T>
class MyObject ;

// - - - - MyObject_declaration.hpp - - - - 
// This header is included by the code which need to know how
// MyObject is defined, but nothing more.
#include <MyObject_forward.hpp>

template<typename T>
class MyObject
{
   public :
      MyObject() ;
   // Etc.
} ;

void doSomething() ;

// - - - - MyObject_implementation.hpp - - - - 
// This header is included by the code which need to see
// the implementation of the methods/functions of MyObject,
// but nothing more.
#include <MyObject_declaration.hpp>

template<typename T>
MyObject<T>::MyObject()
{
   doSomething() ;
}

// etc.

// - - - - MyObject_source.cpp - - - - 
// This source will have implementation that does not need to
// be shared, which, for templated code, usually means nothing...
#include <MyObject_implementation.hpp>

void doSomething()
{
   // etc.
} ;

// etc.

哇!

在“现实生活”中,它通常不那么复杂。大多数代码只有一个简单的标头/源代码组织,源代码中带有一些内联代码。

但是在其他情况下(模板对象彼此了解),我必须为每个对象分别使用声明和实现标头,并且包括这些标头的空源只是为了帮助我看到一些编译错误。

将标头分解为单独的标头的另一个原因可能是加快编译速度,将符号解析的数量限制在严格必要的范围内,以及避免在内联方法实现发生更改时仅关心正向声明的源的不必要的重新编译。

结论

您应该使代码组织尽可能简单,并且尽可能模块化。尽可能将其放在源文件中。仅在标头中公开需要共享的内容。

但是有一天,模板对象之间会产生循环依赖关系,如果您的代码组织变得比普通标头/源组织更“有趣”,也不要感到惊讶。

^ _ ^


17

除了所有其他答案外,我还会告诉您不要放在头文件中的内容:
using声明(最常见的是using namespace std;)不应出现在头文件中,因为它们会污染包含它的源文件的名称空间。


+1,但您可以使用警告,只要它在某个详细名称空间(或匿名名称空间)中即可。但是,是的,永远不要使用using将标头中的内容带入全局名称空间。
KitsuneYMG,2009年

+1这个答案很容易回答。:)另外,头文件应包含匿名的命名空间。
sellibitze

只要您了解这意味着什么,头文件就可以包含匿名名称空间,即每个翻译单元将具有定义名称空间的不同副本。对于static inline在C99中使用的情况,建议在C ++中使用匿名命名空间中的内联函数,因为这与将内部链接与模板结合在一起时会发生什么情况有关。Anon名称空间使您可以“隐藏”函数,同时保留外部链接。
史蒂夫·杰索普

史蒂夫,你写的东西并没有说服我。请选择一个具体的示例,其中您认为anon命名空间在头文件中完全有意义。
sellibitze

6

什么编译成什么(零二进制足迹)进入头文件。

变量不会编译为空,但是类型声明会编译(因为它们仅描述变量的行为)。

函数不会,但是内联函数(或宏)会这样做,因为它们仅在被调用的地方生成代码。

模板不是代码,它们只是创建代码的秘诀。因此它们也放入h文件中。


1
“内联函数...仅在被调用的地方生成代码”。这不是真的。内联函数可以在调用站点内联,也可以不内联,但是即使内联函数也内联,真实函数主体仍然存在,就像非内联函数一样。可以在标头中使用内联函数与它们是否生成代码无关,这是因为内联函数不会触发一个定义规则,因此与非内联函数不同,将两个不同的翻译单元链接在一起没有问题两者都包含标题。
史蒂夫·杰索普


1

您的类和函数声明以及文档以及内联函数/方法的定义(尽管有些人更喜欢将它们放在单独的.inl文件中)。


1

主要头文件包含类框架声明(不经常更改)

并且cpp文件包含类的实现(经常更改)。


5
请避免使用非标准术语。什么是“类框架”,什么是“类实现”?同样,在类上下文中称为声明的内容可能包括类定义。
sellibitze

0

头文件(.h)应该用于类,结构及其方法,原型等的声明。这些对象的实现在cpp中进行。

在.h中

    class Foo {
    int j;

    Foo();
    Foo(int)
    void DoSomething();
}

0

我希望看到:

  • 声明书
  • 注释
  • 内联标记的定义
  • 范本

真正的答案是不应该输入什么:

  • definitons(可能导致事物被多重定义)
  • 使用声明/伪指令(将它们强加于任何人,包括您的标头,都可能导致名称冲突)

1
当然,您也可以将类定义放入头文件中。一类声明并没有说明它的成员什么。
sellibitze

0

标头定义一些内容,但未说明任何有关实现的内容。(不包括此“ metafore”中的模板。

话虽如此,您需要将“定义”分为多个子组,在这种情况下,有两种类型的定义。

  • 您可以定义结构的“布局”,仅告诉周围使用组所需的内容。
  • 变量,函数和类的定义。

现在,我当然是在谈论第一个小组。

标头用于定义结构的布局,以帮助其余软件使用该实现。您可能希望将其视为实现的“摘要”,这是不客气的说,但是,我认为这种情况非常适合。

如前所述,并显示您声明了私有和公共使用区域及其标题,其中还包括私有和公共变量。现在,我不想在这里进行代码设计,但是,您可能要考虑您在标头中放置的内容,因为这是最终用户和实现之间的层。


0
  • 头文件-在开发过程中不应经常更改->您应该考虑一下,并立即写入它们(理想情况下)
  • 源文件-实施期间的更改

这是一种做法。对于一些较小的项目,这可能是可行的方法。但是您可以尝试弃用函数及其原型(在头文件中),而不是更改其签名或删除它们。至少直到更改主号码为止。就像当1.9.2升至2.0.0 beta时一样。
TamusJRoyce

0

标头(.h)

  • 宏,包括接口所需的内容(尽可能少)
  • 函数和类的声明
  • 接口文件
  • 内联函数/方法的声明(如果有)
  • 全局变量的外部变量(如果有)

正文(.cpp)

  • 其余的宏,包括
  • 包括模块头
  • 功能和方法的定义
  • 全局变量(如果有)

根据经验,您可以将模块的“共享”部分放在.h(其他模块需要能够看到的部分)上,而将“非共享”部分放在.cpp上

PD:是的,我包括了全局变量。我已经使用了它们几次,重要的是不要在标题上定义它们,否则您将获得很多模块,每个模块都定义自己的变量。

编辑:大卫的评论后修改


根据经验,.h文件中包含的内容应尽可能少,.cpp文件应包含所需的任何标头。这样可以缩短编译时间,并且不会污染名称空间。
David Thornley
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.