【问题标题】:Is is a good practice to put the definition of C++ classes into the header file?将 C++ 类的定义放入头文件是一种好习惯吗?
【发布时间】:2011-02-10 09:11:40
【问题描述】:

当我们用 Java、Vala 或 C# 设计类时,我们将定义和声明放在同一个源文件中。但在 C++ 中,传统上更倾向于将定义和声明分隔在两个或多个文件中。

如果我只使用头文件并将所有内容都放入其中,例如 Java,会发生什么? 是否有性能损失或其他什么?

【问题讨论】:

  • 那么你就破坏了将接口与实现分离的整个目的。
  • 不仅如此,您最终会将整个应用程序编译为一个大的源文件,并且每一个小改动都会导致重新编译整个应用程序。
  • 术语。在 C++ 中,这是一个类声明class Foo;。这是一个类定义class Foo { void foo(); int bar(const char*);};。在 C++ 中,通常将类 定义 放在头文件中,而决定将成员函数的定义放在哪里。
  • @Ed S.:不,你没有。标准模板容器和算法在头文件中有完整的定义,但它们设法在逻辑上将接口与实现分开。接口在标准中,不在头文件中。也许来自 C,人们有时将接口与 C++ 中的实现分开的一种方法是在头文件中使用函数签名等作为接口定义。但是,这并不是定义接口以便将其与实现分开的唯一方法。我怀疑这是 C++ 中最常见的方式(好吧,我希望不是)。

标签: c++ declaration definition header-files


【解决方案1】:

答案取决于你要创建什么样的类。

C++ 的编译模型可以追溯到 C 时代,因此其将数据从一个源文件导入另一个源文件的方法比较原始。 #include 指令实际上将您包含的文件的内容复制到源文件中,然后将结果视为您一直编写的文件。您需要注意这一点,因为名为一个定义规则 (ODR) 的 C++ 策略毫不奇怪地指出,每个函数和类最多应该有一个定义。这意味着如果你在某处声明一个类,那么该类的所有成员函数要么根本不定义,要么在一个文件中只定义一次。有一些例外(我会在一分钟内讨论),但现在只需将此规则视为硬性规定、无例外规则。

如果您采用非模板类并将类定义和实现都放入头文件中,您可能会遇到单一定义规则的问题。特别是,假设我编译了两个不同的 .cpp 文件,这两个文件 #include 您的标头都包含实现和接口。在这种情况下,如果我尝试将这两个文件链接在一起,链接器会发现每个文件都包含该类成员函数的实现代码的副本。此时链接器会报错,因为你违反了一个定义规则:所有类的成员函数有两种不同的实现。

为了防止这种情况,C++ 程序员通常将类拆分成一个头文件,其中包含类声明及其成员函数的声明,而不包含这些函数的实现。然后将实现放入一个单独的 .cpp 文件中,该文件可以单独编译和链接。这允许您的代码避免遇到 ODR 问题。就是这样。首先,每当您将类头文件#include 转换为多个不同的 .cpp 文件时,它们中的每一个都只会获得成员函数的声明的副本,而不是它们的定义 ,因此您班级的所有客户都不会得到这些定义。这意味着任何数量的客户端都可以#include您的头文件,而不会在链接时遇到问题。由于您自己的带有实现的 .cpp 文件是包含成员函数实现的唯一文件,因此在链接时您可以将其与任意数量的其他客户端对象文件合并,而不会有任何麻烦。这是您将 .h 和 .cpp 文件分开的主要原因。

当然,ODR 也有一些例外。其中第一个提出了模板函数和类。 ODR 明确指出,您可以对同一个模板类或函数有多个不同的定义,前提是它们都是等效的。这主要是为了更容易编译模板——每个 C++ 文件都可以实例化同一个模板,而不会与任何其他文件发生冲突。由于这个原因以及其他一些技术原因,类模板往往只有一个 .h 文件而没有匹配的 .cpp 文件。任意数量的客户端都可以#include 文件而不会遇到麻烦。

ODR 的另一个主要例外涉及内联函数。该规范明确指出 ODR 不适用于内联函数,因此如果您有一个头文件,其中包含标记为内联的类成员函数的实现,那很好。任何数量的文件都可以#include 这个文件而不破坏 ODR。有趣的是,在类的主体中声明和定义的任何成员函数都是隐式内联的,所以如果你有这样的标题:

#ifndef Include_Guard
#define Include_Guard

class MyClass {
public:
    void DoSomething() {
        /* ... code goes here ... */
    }
};

#endif

那么您就不会冒险破坏 ODR。如果你把它改写为

#ifndef Include_Guard
#define Include_Guard

class MyClass {
public:
    void DoSomething();
};

void MyClass::DoSomething()  {
    /* ... code goes here ... */
}

#endif

那么您破坏 ODR,因为成员函数未标记为内联,并且如果多个客户端 #include 此文件将有多个 MyClass::DoSomething 定义。

总结一下 - 您可能应该将您的类拆分为 .h/.cpp 对以避免破坏 ODR。但是,如果您正在编写类模板,则不需要 .cpp 文件(可能根本不应该有),如果您可以将类的每个成员函数标记为内联,您也可以避免使用 .cpp 文件。

【讨论】:

  • 从今天开始,将声明和定义放在同一个文件中是个好习惯吗?至少当我更新课程时它会有所帮助。我不必在头文件和 cpp 文件之间交换。
  • 那么包含守卫的目的是什么?它们不会阻止 ODR 的破坏吗?
  • @AbdelAleem 多个翻译单元具有相同类或结构的相同定义,或者相同模板或内联函数的相同定义是可以的。一个翻译单元不能有同一个类或结构的多个定义。这就是包含守卫地址的问题——它确保同一个翻译单元不会获得单个类定义的多个副本。
【解决方案2】:

将定义放在头文件中的缺点如下:-

头文件 A - 包含 metahodA() 的定义

头文件 B - 包括头文件 A。

现在让我们假设您更改了methodA的定义。由于 B 中包含头文件 A,因此您需要编译文件 A 和 B。

【讨论】:

    【解决方案3】:

    最大的不同是每个函数都被声明为内联函数。一般来说,你的编译器足够聪明,这不会是一个问题,但最坏的情况是它会定期导致页面错误并使你的代码慢得令人尴尬。通常代码是出于设计原因而分开的,而不是出于性能考虑。

    【讨论】:

      【解决方案4】:

      一般来说,将实现与标头分开是一种很好的做法。但是,在模板等实现位于标头本身中的情况下也有例外。

      【讨论】:

        【解决方案5】:

        将所有内容放在标题中的两个特殊问题:

        1. 编译时间会增加,有时会大大增加。 C++ 编译时间足够长,这不是您想要的。

        2. 如果您在实现中存在循环依赖关系,则将所有内容都保存在标头中几乎是不可能的。例如:

          header1.h

          struct C1
          {
            void f();
            void g();
          };
          

          header2.h

          struct C2
          {
            void f();
            void g();
          };
          

          impl1.cpp

          #include "header1.h"
          #include "header2.h"
          
          void C1::f()
          {
            C2 c2;
            c2.f();
          }
          

          impl2.cpp

          #include "header2.h"
          #include "header1.h"
          
          void C2::g()
          {
            C1 c1;
            c1.g();
          }
          

        【讨论】:

          猜你喜欢
          • 2014-10-06
          • 1970-01-01
          • 2016-12-03
          • 2017-07-15
          • 2021-09-28
          • 2016-03-21
          • 2022-01-20
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多