将C ++类的定义放入头文件是一个好习惯吗?

时间:2011-02-10 09:11:40

标签: c++ declaration definition header-files

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

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

5 个答案:

答案 0 :(得分:50)

答案取决于你正在创造什么样的课程。

C ++的编译模型可以追溯到C的时代,因此将数据从一个源文件导入另一个源文件的方法比较原始。 #include指令从字面上将您所包含文件的内容复制到源文件中,然后将结果视为您一直编写的文件。您需要注意这一点,因为C ++策略称为一个定义规则(ODR),毫不奇怪地说,每个函数和类最多只能有一个定义。这意味着如果在某个地方声明一个类,那么该类的所有成员函数应该根本没有定义,或者只在一个文件中定义一次。有一些例外(我会在一分钟内得到它们),但是现在只需将此规则视为一项严格的,无异常的规则。

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

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

当然,ODR有一些例外。第一个是模板函数和类。 ODR明确声明您可以为同一模板类或函数提供多个不同的定义,前提是它们都是等效的。这主要是为了使编译模板更容易 - 每个C ++文件都可以实例化相同的模板而不会与任何其他文件冲突。出于这个原因,以及其他一些技术原因,类模板往往只有一个没有匹配的.cpp文件的.h文件。任意数量的客户都可以#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文件。

答案 1 :(得分:5)

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

头文件A - 包含metahodA()

的定义

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

现在让我们说你改变了methodA的定义。您需要编译文件A和B,因为在B中包含头文件A.

答案 2 :(得分:2)

最大的区别是每个函数都被声明为内联函数。通常,您的编译器将足够智能,这不会是一个问题,但最糟糕的情况是它会定期导致页面错误并使您的代码变得非常缓慢。通常,代码是出于设计原因而分开的,而不是性能。

答案 3 :(得分:0)

通常,将实现与标头分开是一种很好的做法。但是,在像模板这样的情况下有例外,其中实现在标题本身中。

答案 4 :(得分:0)

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

  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();
    }