【问题标题】:What should go into an .h file?.h 文件应该包含什么内容?
【发布时间】:2010-12-29 02:03:42
【问题描述】:

将您的代码分成多个文件时,究竟应该将哪些内容放入 .h 文件,哪些内容应放入 .cpp 文件?

【问题讨论】:

  • 这是一个纯粹的风格问题,但我相信 C++ 声明进入 .hpp 文件,而 C 声明进入 .h 文件。这在混合 C 和 C++ 代码(例如 C 中的遗留模块)时非常有用。
  • @ThomasMatthews 有道理。这种做法经常使用吗?
  • @lightningleaf:是的,这种做法经常被使用,尤其是在混合 C++ 和 C 语言时。

标签: c++ header-files


【解决方案1】:

标题 (.h)

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

正文 (.cpp)

  • 其余的宏和包含
  • 包含模块的标题
  • 函数和方法的定义
  • 全局变量(如果有)

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

PD:是的,我已经包含了全局变量。我已经用过几次了,重要的是不要在标题中定义它们,否则你会得到很多模块,每个模块都定义了自己的变量。

【讨论】:

  • 根据经验,.h 文件中应包含尽可能少的包含,而 .cpp 文件应包含所需的任何标头。这样可以缩短编译时间并且不会污染命名空间。
【解决方案2】:

事实上,在 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.

哇!

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

但在其他情况下(模板对象相互了解),我必须为每个对象拥有单独的声明和实现标头,并使用包含这些标头的空源来帮助我查看一些编译错误。

将标头分解为单独标头的另一个原因可能是加快编译速度,将解析的符号数量限制在严格必要的范围内,并避免在实现内联方法时对只关心前向声明的源进行不必要的重新编译改变了。

结论

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

但是,如果您的代码组织变得比普通的标头/源代码组织更“有趣”...

^_^

【讨论】:

    【解决方案3】:
    • 头文件 - 在开发过程中不应该经常更改 -> 你应该思考并立即编写它们(在理想情况下)
    • 源文件 - 实施期间的更改

    【讨论】:

    • 这是一种做法。对于一些较小的项目,这可能是要走的路。但是您可以尝试弃用函数及其原型(在头文件中),而不是更改它们的签名或删除它们。至少在更改主要号码之前。就像 1.9.2 升级到 2.0.0 测试版一样。
    【解决方案4】:

    标题定义一些东西,但没有说明任何有关实现的信息。 (不包括此“元前”中的模板。

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

    • 您定义结构的“布局”,只根据周围使用群体的需要进行说明。
    • 变量、函数和类的定义。

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

    标题用于定义结构的布局,以帮助软件的其余部分使用实现。您可能希望将其视为您的实现的“抽象”,这是粗俗的说法,但我认为它非常适合这种情况。

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

    【讨论】:

      【解决方案5】:

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

      【讨论】:

      • +1 有一个警告,只要它位于某个详细的命名空间(或匿名命名空间)中,您就可以使用它。但是,是的,永远不要使用 using 将内容带入标头中的全局命名空间。
      • +1 这个更容易回答。 :) 此外,头文件应该包含匿名命名空间。
      • 头文件可以包含匿名命名空间,只要你理解这意味着什么,即每个翻译单元都会有你定义命名空间的东西的不同副本。对于在 C99 中使用 static inline 的情况,建议在 C++ 中使用匿名命名空间中的内联函数,因为这与将内部链接与模板结合时发生的情况有关。 Anon 命名空间允许您“隐藏”函数,同时保留外部链接。
      • 史蒂夫,你写的并没有说服我。请选择一个你认为匿名命名空间在头文件中完全有意义的具体示例。
      【解决方案6】:

      编译为空(零二进制占用)进入头文件。

      变量不会编译成空,但类型声明可以(因为它们只描述变量的行为方式)。

      函数不会,但内联函数(或宏)会,因为它们只在调用时生成代码。

      模板不是代码,它们只是创建代码的秘诀。所以它们也放在 h 文件中。

      【讨论】:

      • “内联函数......仅在调用的地方生成代码”。这不是真的。内联函数可能会或可能不会在调用站点内联,但即使它们被内联,真正的函数体仍然存在,就像它对非内联函数一样。可以在标题中使用内联函数的原因与它们是否生成代码无关,这是因为内联函数不会触发一个定义规则,因此与非内联函数不同,将两个不同的翻译单元链接在一起没有问题这两个都包含标题。
      【解决方案7】:

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

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

      【讨论】:

      • 请不要使用非标准术语。什么是“类骨架”,什么是“类实现”?此外,您在类的上下文中所谓的声明可能包括类定义。
      【解决方案8】:

      通常,您将声明放在头文件中,将定义放在实现 (.cpp) 文件中。例外情况是模板,其中定义也必须放在标题中。

      这个问题和类似的问题在 SO 上经常被问到 - 例如,请参阅 Why have header files and .cpp files in C++?C++ Header Files, Code Separation

      【讨论】:

      • 当然,你也可以把类定义放到头文件中。它们甚至不必是模板。
      【解决方案9】:

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

      代码文件 (.cpp) 旨在提供只需要在一个文件中知道的实现信息。通常,其他模块应该/永远不会访问的函数体和内部变量属于.cpp文件。总而言之,“实现”。

      问自己确定什么属于哪里的最简单的问题是“如果我更改它,我是否必须更改其他文件中的代码才能再次编译?”如果答案是“是”,它可能属于头文件;如果答案是“否”,它可能属于代码文件。

      【讨论】:

      • 除非私有类数据必须进入标题。模板必须完全由标头定义(除非您使用支持export 的少数编译器之一)。 #1 的唯一方法是 PIMPL。如果支持export,则可以使用#2,并且可以使用c++0x 和extern 模板。 IMO,c++ 中的头文件失去了很多用处。
      • 一切都好,但术语不准确。总之,“声明”——“定义”一词与“实现”同义。只有声明性代码、内联代码、宏定义和模板代码应该在标头中;即没有任何实例化代码或数据。
      • 我必须同意克利福德的观点。您使用术语声明和定义相当松散并且有些可互换。但它们在 C++ 中具有精确的含义。示例:类声明引入了类的名称,但没有说明其中的内容。类定义列出了所有成员和友元函数。两者都可以毫无问题地放入头文件中。你所说的“函数原型”是一个函数声明。但是函数定义是包含函数代码的东西,应该放在cpp文件中——除非它是内联的或(部分)模板。
      • 它们在 C++ 中有精确的含义,在英语中没有精确的含义。我的答案写在后者中。
      【解决方案10】:

      我希望看到:

      • 声明
      • cmets
      • 标记为内联的定义
      • 模板

      但真正的答案是不要放入:

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

      【讨论】:

      • 您当然也可以将类定义放入头文件中。 类声明并没有说明它的成员。
      【解决方案11】:

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

      在.h中

          class Foo {
          int j;
      
          Foo();
          Foo(int)
          void DoSomething();
      }
      

      【讨论】:

        【解决方案12】:

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

        【讨论】:

          猜你喜欢
          • 2011-08-18
          • 2018-08-14
          • 2011-05-06
          • 2019-11-09
          • 2013-10-31
          • 2016-11-26
          • 2021-05-17
          • 2017-05-21
          • 2012-05-26
          相关资源
          最近更新 更多