答案取决于您要创建什么类型的类。
C++的编译模型可以追溯到C时代,因此其将数据从一个源文件导入到另一个源文件的方法相对原始。这#include
指令实际上将您要包含的文件的内容复制到源文件中,然后将结果视为您一直编写的文件。您需要小心这一点,因为 C++ 策略称为一个定义规则(ODR) 毫不奇怪地指出,每个函数和类最多应该有一个定义。这意味着,如果您在某处声明一个类,则该类的所有成员函数要么根本不定义,要么在一个文件中只定义一次。有一些例外(我将在一分钟内讨论它们),但现在只需将此规则视为硬性的、无例外的规则。
如果您采用非模板类并将类定义和实现都放入头文件中,则可能会遇到单一定义规则的麻烦。特别是,假设我编译了两个不同的 .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
然后你would会破坏 ODR,因为成员函数未标记为内联,并且如果有多个客户端#include
这个文件会有多个定义MyClass::DoSomething
.
总而言之,您应该将类拆分为 .h/.cpp 对,以避免破坏 ODR。但是,如果您正在编写一个类模板,则不需要 .cpp 文件(并且可能根本不应该有),并且如果您可以将类的每个成员函数标记为内联,您也可以避免使用 .cpp 文件。