把C++类的定义放在头文件中是一个好的实践吗?

38

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

如果我只使用头文件并把所有内容都放在里面,像Java那样,会发生什么?会有性能惩罚或其他问题吗?


5
那样做会彻底违背将接口与实现分离的初衷。 - Ed S.
2
不仅如此,你最终会将整个应用程序编译为一个大的源文件,每一点小改动都会导致整个应用程序重新编译。 - Patrick
2
术语。在C++中,这是一个类的声明class Foo;。这是一个类的定义class Foo { void foo(); int bar(const char*);};。在C++中,通常将类的定义放在头文件中,需要决定的是在哪里放置成员函数的定义。 - Steve Jessop
2
@Ed S.:不,你不需要。标准模板容器和算法在头文件中有完整的定义,但它们仍然成功地将接口与实现逻辑上分离。接口在标准中,而不是在头文件中。也许从C语言中,人们有时使用函数签名等在头文件中作为接口定义来将接口与实现分离。然而,这并不是定义接口以将其与实现分离的唯一方法。我怀疑这不是C++中最常见的方式(希望不是)。 - Steve Jessop
显示剩余2条评论
5个回答

75
答案取决于您正在创建什么类型的类。
C++的编译模型可以追溯到C语言时代,因此它从一个源文件中导入数据到另一个源文件的方法相对较为原始。#include指令字面上将您要包含的文件的内容复制到源文件中,然后将结果视为您一直编写的文件。由于C++政策所提出的一个定义规则(ODR),您需要小心处理此问题。该规则表明,每个函数和类应最多拥有一个定义。这意味着如果您在某个地方声明一个类,则该类的所有成员函数应不定义或者在一个文件中精确地定义一次。虽然有一些例外情况(稍后会详细说明),但现在请将此规则视为一个硬性规定,无例外情况。
如果您将非模板类的类定义和实现都放在头文件中,那么可能会遇到定义规则问题。特别是,假设我编译两个不同的.cpp文件,两者都#include您的包含了实现和接口的头文件。这种情况下,如果我尝试链接这两个文件,链接器将发现每个文件都包含类的成员函数实现代码的副本。此时,链接器将报告错误,因为您已经违反了定义规则:所有类成员函数都有两个不同的实现。
为了避免这种情况,C++程序员通常将类分成一个包含类声明的头文件和成员函数声明(不包括成员函数实现)的头文件。然后将实现放入一个单独的.cpp文件中,可以编译并链接。这使您的代码避免了与ODR冲突的问题。这是其原因:每当您将类头文件#include到多个不同的.cpp文件中时,它们都只会得到成员函数的声明,而不是定义,因此没有任何一个类的客户端最终得到定义。这意味着任意数量的客户端都可以#include您的头文件,而不会在链接时遇到问题。由于您自己的实现.cpp文件是唯一包含成员函数实现的文件,在链接时,您可以将其与任意数量的其他客户对象文件合并而无需麻烦。这就是为什么要将.h和.cpp文件分开的主要原因。
当然,ODR有一些例外情况。其中之一是模板函数和类。ODR明确指出,您可以为相同的模板类或函数有多个不同的定义,前提是它们都是等效的。这主要是为了更容易地编译模板-每个C++文件可以实例化同样的模板而不会与任何其他文件发生冲突。出于这个原因和一些其他技术原因,类模板 tend to 只有一个.h文件而没有匹配的.cpp文件。任意数量的客户端都可以#include该文件而不会遇到问题。
ODR的另一个主要例外涉及内联函数。规范明确指出,ODR不适用于内联函数,因此如果您在头文件中具有标记为inline的类成员函数的实现,那么这是完全可以的。任意数量的文件都可以#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

如果你不将成员函数标记为inline,并且多个客户端#include此文件,则会违反ODR,因此你会打破ODR规则

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


截至今天,在同一文件中放置声明和定义是否是一个好的实践?至少在更新类时有帮助。我不必在头文件和cpp文件之间切换。 - Oh Xyz
那么预处理器中的 include guards 有什么作用呢?它们不是防止 ODR 的破坏吗? - Abdel Aleem
1
@AbdelAleem,多个翻译单元具有相同类或结构的定义,或相同模板或内联函数的定义是可以的。但是,一个翻译单元具有同一类或结构的多个定义是不可以的。这就是包含保护所解决的问题 - 它确保同一翻译单元不会获得单个类定义的多个副本。 - templatetypedef

7

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

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

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

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


1
最大的区别在于每个函数都被声明为内联函数。通常情况下,您的编译器会足够聪明,这不会成为问题,但最坏的情况是它会导致页面错误并使您的代码变得尴尬缓慢。通常情况下,代码是出于设计原因而分离的,而不是出于性能考虑。

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

0
通常来说,将实现与头文件分离是一个好的编程实践。但在模板等情况下,实现会放在头文件本身中,这是一种例外。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接