如何将一个类及其成员函数分离到头文件和源文件中

248
我对如何将一个简单类的实现和声明代码分离到一个新的头文件和cpp文件中感到困惑。例如,我应该如何分离以下类的代码?
class A2DD
{
  private:
  int gx;
  int gy;

  public:
  A2DD(int x,int y)
  {
    gx = x;
    gy = y;
  }

  int getSum()
  {
    return gx + gy;
  }
};

15
只是一些评论:构造函数应始终使用初始化列表而不是在函数体中设置成员变量。有一个好的简单解释,请参见:http://www.codeguru.com/forum/showthread.php?t=464084 。此外,在大多数地方,公共字段习惯上应该放在顶部。这不会影响任何事情,但由于公共字段是类的文档,将其放在顶部是有意义的。 - martiert
2
@martiert 如果用户按照这个建议移动了public:成员,但是成员之间存在顺序依赖关系并且还不知道成员按照声明的顺序初始化,那么将会对很多东西产生影响。 - underscore_d
1
@underscore_d 是对的。但是我们都在将警告作为错误编译,并且尽可能考虑所有警告,对吧?这至少可以告诉你正在搞砸它,但是很多人使用太少的警告,而且只是忽略它们 :( - martiert
@martiert 说得好,我有点忘了它会生成警告 - 如果大多数人只读取警告就好了 :-) 我使用它们并尝试将它们全部编码。有些是不可避免的 - 所以我说“谢谢你的警告,但我知道我在做什么!” - 但大多数最好修复以避免后来的混淆。 - underscore_d
将公共字段放在顶部只是一种风格,不幸的是许多人都采用了这种风格,但你还需要记住像 @martiert 提到的一些事情。 - Vassilis
链接 @martiert 的404错误。已更新:https://forums.codeguru.com/showthread.php?t=464084 - Ludovic Kuty
8个回答

340
类声明放在头文件中。很重要的一点是要添加#ifndef的包含保护。现在大多数编译器也支持#pragma once。另外,我省略了private,因为默认情况下C++类成员是私有的。
// A2DD.h
#ifndef A2DD_H
#define A2DD_H

class A2DD
{
  int gx;
  int gy;

public:
  A2DD(int x,int y);
  int getSum();

};

#endif

实现部分写在CPP文件中。
// A2DD.cpp
#include "A2DD.h"

A2DD::A2DD(int x,int y)
{
  gx = x;
  gy = y;
}

int A2DD::getSum()
{
  return gx + gy;
}

84
请记住,如果您正在进行模板编程,则必须将所有内容保留在 .h 文件中,以便编译器会在编译时刻实例化正确的代码。 - linello
4
你的头文件里有 #ifndef 的内容吗? - Ferenc Deak
5
这意味着所有包含你的头文件的文件都能够“看到”私有成员。例如,如果你想发布一个库及其头文件,那么必须展示类的私有成员吗? - Gauthier
2
不,这里有一个很棒的私有实现惯用语:http://en.wikipedia.org/wiki/Opaque_pointer 你可以使用它来隐藏实现细节。 - Ferenc Deak
7
细微的修正:“类声明放在头文件中”这样的说法有些不太准确。虽然这确实是一个声明,但它也是一个定义,但由于后者包含前者,我更愿意说类定义应该放在头文件中。在翻译单元中,你拥有成员函数的定义,而不是类的定义。如果您同意,这可能值得进行轻微的修改。 - lubgr
显示剩余14条评论

27

通常,.h文件包含类定义,其中包含所有数据和方法声明。在您的情况下,可以像这样:

A2DD.h:

class A2DD
{
  private:
  int gx;
  int gy;

  public:
  A2DD(int x,int y);    
  int getSum();
};

然后你的 .cpp 文件包含了这样的方法实现:

A2DD.cpp:

A2DD::A2DD(int x,int y)
{
  gx = x;
  gy = y;
}

int A2DD::getSum()
{
  return gx + gy;
}

19

重要的是要指出,对于在更广泛的范围内研究这个主题的读者来说,在你只想将项目拆分成文件时,接受的答案中的过程是不必要的。只有当您需要单个类的多个实现时才需要它。如果每个类的实现都是一个,则每个类只需要一个头文件即可。

因此,从接受的答案的示例中,只需要这部分:

#ifndef MYHEADER_H
#define MYHEADER_H

//Class goes here, full declaration AND implementation

#endif
#ifndef等预处理器定义使得该代码可以被多次使用。
PS. 一旦您意识到C/C++是“愚蠢的”,而#include仅仅是一种说“在这个位置倒入这段文本”的方式,这个主题就变得更加清晰了。

你能否将“split”文件放在“.cpp”中,或者只有“.h”对于这种代码组织方法才是“好”的? - Benny Jobigan
4
有些项目会将头文件和(单个)实现文件分开,以便于分发头文件而不需要暴露实现代码的源代码。 - Carl G
1
我很高兴你指出这一点,因为我最初学习的是C ++,然后在许多年前转换到了C#,最近又开始大量使用C++,忘记了将文件拆分起来有多么繁琐和麻烦,而开始把所有东西都放在标头文件中。当我正在寻找任何人给出不要这样做的好理由时,我发现了这个。@CarlG说得很有道理,但除了那种情况外,我认为全部内联执行是正确的方式。 - Emperor Eto
2
“多个实现”中的一个可能是用于单元测试的模拟对象。 - user5534993

8
基本上是函数声明/定义的修改语法: a2dd.h
class A2DD
{
private:
  int gx;
  int gy;

public:
  A2DD(int x,int y);

  int getSum();
};

a2dd.cpp

A2DD::A2DD(int x,int y)
{
  gx = x;
  gy = y;
}

int A2DD::getSum()
{
  return gx + gy;
}

7

A2DD.h

class A2DD
{
  private:
  int gx;
  int gy;

  public:
  A2DD(int x,int y);

  int getSum();
};

A2DD.cpp

  A2DD::A2DD(int x,int y)
  {
    gx = x;
    gy = y;
  }

  int A2DD::getSum()
  {
    return gx + gy;
  }

这个想法是将所有函数签名和成员都保留在头文件中。


这样,其他项目文件就可以看到类的外观,而不必知道实现细节。


此外,您可以在实现中包含其他头文件,而不是在头文件中包含它们。这很重要,因为无论哪些头文件包含在您的头文件中,都将被包含(继承)在包括您的头文件的任何其他文件中。


5

您需要在头文件中留下声明:

class A2DD
{
  private:
  int gx;
  int gy;

  public:
    A2DD(int x,int y); // leave the declarations here
    int getSum();
};

在实现文件中放置定义。
A2DD::A2DD(int x,int y) // prefix the definitions with the class name
{
  gx = x;
  gy = y;
}

int A2DD::getSum()
{
  return gx + gy;
}

您可以将两者混合使用(例如在头文件中保留getSum()的定义)。这很有用,因为它可以使编译器更好地进行内联。但这也意味着如果更改实现(如果保留在头文件中),可能会触发包括该头文件的所有其他文件的重建。
请注意,对于模板,您需要将其全部保留在头文件中。

1
将私有成员和函数放在头文件中不被认为是泄露实现细节吗? - Jason
1
@Jason,有点像。这些都是必要的实现细节。例如,我必须知道一个类将在堆栈上占用多少空间。函数实现对于其他编译单元来说并非必要。 - Paul Draper

1
通常情况下,您只需在头文件中放置声明和非常短的内联函数:
例如:
class A {
 public:
  A(); // only declaration in the .h unless only a short initialization list is used.

  inline int GetA() const {
    return a_;
  }

  void DoSomethingCoplex(); // only declaration
  private:
   int a_;
 };

0

我不会参考你的示例,因为它对于一般答案来说相当简单(例如它不包含模板函数,这会强制你在头文件中实现它们),我遵循的经验法则是 pimpl惯用语

它有很多好处,因为你可以获得更快的编译时间和语法糖:

class->member 而不是 class.member

唯一的缺点是你需要额外付出指针。


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