C++头文件如何包含实现

How can a C++ header file include implementation?

本文关键字:何包含 实现 文件 C++      更新时间:2023-10-16

好吧,无论如何都不是 C/C++ 专家,但我认为头文件的重点是声明函数,然后 C/CPP 文件是定义实现。

但是,今晚查看一些C++代码时,我在类的头文件中发现了这个......

public:
    UInt32 GetNumberChannels() const { return _numberChannels; } // <-- Huh??
private:
    UInt32 _numberChannels;

那么为什么标头中有实现呢?它与const关键字有关吗?这是否内联了类方法?与在 CPP 文件中定义实现相比,这样做的好处/意义究竟是什么?

好吧,无论如何都不是 C/C++ 专家,但我认为头文件的目的是声明函数,然后 C/CPP 文件是定义实现。

头文件的真正用途是在多个源文件之间共享代码。 它通常用于将声明与实现分开,以便更好地管理代码,但这不是必需的。 可以编写不依赖于头文件的代码,也可以编写仅由头文件组成的代码(STL 和 Boost 库就是很好的例子)。 请记住,当预处理器遇到 #include 语句时,它会将该语句替换为被引用的文件的内容,然后编译器只能看到已完成的预处理代码。

因此,例如,如果您有以下文件:

#ifndef FooH
#define FooH
class Foo
{
public:
    UInt32 GetNumberChannels() const;
private:
    UInt32 _numberChannels;
};
#endif

福.cpp:

#include "Foo.h"
UInt32 Foo::GetNumberChannels() const
{
    return _numberChannels;
}

酒吧.cpp:

#include "Foo.h"
Foo f;
UInt32 chans = f.GetNumberChannels();

预处理器分别分析 Foo.cpp 和 Bar.cpp,并生成以下代码,然后编译器对其进行分析:

福.cpp:

class Foo
{
public:
    UInt32 GetNumberChannels() const;
private:
    UInt32 _numberChannels;
};
UInt32 Foo::GetNumberChannels() const
{
    return _numberChannels;
}

酒吧.cpp:

class Foo
{
public:
    UInt32 GetNumberChannels() const;
private:
    UInt32 _numberChannels;
};
Foo f;
UInt32 chans = f.GetNumberChannels();

Bar.cpp编译成Bar.obj,并包含调用Foo::GetNumberChannels()的引用。 Foo.cpp编译成Foo.obj,包含Foo::GetNumberChannels()的实际实现。 编译后,链接器会匹配 .obj 文件并将它们链接在一起以生成最终的可执行文件。

那么为什么标头中有实现呢?

通过将方法实现包含在方法声明中,它被隐式声明为内联(也可以显式使用实际的inline关键字)。 指示编译器应该内联函数只是一个提示,并不能保证函数实际上会被内联。但如果是这样,那么无论从哪里调用内联函数,函数的内容都会直接复制到调用站点中,而不是生成一个 CALL 语句来跳转到函数并在退出时跳回到调用方。 然后,编译器可以考虑周围的代码,并在可能的情况下进一步优化复制的代码。 

它与 const 关键字有关吗?

不。 const 关键字仅向编译器指示该方法不会更改在运行时调用它的对象的状态。

与在 CPP 文件中定义实现相比,这样做的好处/意义究竟是什么?

当有效使用时,它通常允许编译器生成更快、更优化的机器代码。

头文件中实现函数是完全有效的。唯一的问题是打破一个定义规则。也就是说,如果包含来自多个其他文件的标头,则会收到编译器错误。

但是,有一个例外。如果将函数声明为内联函数,则该函数不受单一定义规则的约束。这就是这里发生的事情,因为在类定义中定义的成员函数是隐式内联的。

内联

本身是向编译器发出的提示,表明函数可能是内联的良好候选项。也就是说,将对它的任何调用扩展到函数的定义中,而不是简单的函数调用。这是一种优化,它以生成的文件的大小换取更快的代码。在现代编译器中,为函数提供这种内联提示大多被忽略,除了它对一个定义规则的影响。此外,编译器始终可以自由地内联它认为合适的任何函数,即使它没有被声明inline(显式或隐式)。

在您的示例中,在参数列表后使用 const 表示成员函数不会修改调用它的对象。实际上,这意味着this所指向的对象,以及所有类成员所指向的对象,都将被视为const。也就是说,尝试修改它们将生成编译时错误。

它是隐式声明inline,因为它是在类声明中定义的成员函数。这并不意味着编译器必须内联它,但这意味着您不会违反一个定义规则。它与const*完全无关。它也与函数的长度和复杂性无关。

如果它是一个非成员函数,那么你必须显式声明它为 inline

inline void foo() { std::cout << "foo!n"; }

* 有关成员函数末尾const的更多信息,请参阅此处。

即使在普通 C 中,也可以将代码放在头文件中。 如果这样做,通常需要将其声明static否则包含相同标头的多个 .c 文件将导致"乘法定义函数"错误。

预处理器以文本方式包含包含文件,因此包含文件中的代码成为源文件的一部分(至少从编译器的角度来看)。

C++的设计者希望实现具有良好数据隐藏功能的面向对象编程,因此他们希望看到大量的getter和setter函数。 他们不希望不合理的表现惩罚。 因此,他们设计了C++,以便getter和setter不仅可以在标头中声明,而且可以实际实现,因此它们将内联。 您展示的该函数是一个 getter,编译该C++代码时,不会有任何函数调用;用于提取该值的代码将就地编译。

可以

制作一种没有头文件/源文件区别的计算机语言,而只是具有编译器理解的实际"模块"。 (C++没有这样做;他们只是建立在源文件的成功 C 模型之上,并以文本方式包含头文件。 如果源文件是模块,则编译器可以从模块中提取代码,然后内联该代码。 但是C++这样做的方式更容易实现。

我所知,有两种方法,可以在头文件中安全地实现。

  • 内联方法 - 它们的实现被复制到使用它们的地方,因此没有双重定义链接器错误的问题;
  • 模板方法 - 它们实际上是在模板实例化的那一刻编译的(例如,当有人输入类型代替模板时),因此再次不存在双重定义问题的可能性。

我相信,你的例子符合第一种情况。

C++ 标准引号

C++17 N4659标准草案10.1.6"内联说明符"表示方法是隐式内联的:

4 类定义中定义的函数是内联函数。

再往下看,我们看到内联方法不仅可以,而且必须在所有翻译单元上定义:

6 内联函数或变量应在使用它的每个翻译单元中定义,并且应 在每种情况下都有完全相同的定义(6.2)。

在 12.2.1 "成员函数"的注释中也明确提到了这一点:

1 成员函数可以在

其类定义中定义 (11.4),在这种情况下,它是一个内联成员函数 (10.1.6) [...]

3 [ 注意:程序中非内联成员函数最多只能有一个定义。可能有 程序中的多个内联成员函数定义。见 6.2 和 10.1.6。— 尾注 ]

GCC 8.3 实施

主.cpp

struct MyClass {
    void myMethod() {}
};
int main() {
    MyClass().myMethod();
}

编译和查看符号:

g++ -c main.cpp
nm -C main.o

输出:

                 U _GLOBAL_OFFSET_TABLE_
0000000000000000 W MyClass::myMethod()
                 U __stack_chk_fail
0000000000000000 T main

然后我们从man nm中看到,MyClass::myMethod符号在 ELF 对象文件上被标记为弱,这意味着它可以出现在多个对象文件中:

"W" "w" 该符号是弱符号,尚未被专门标记为弱对象符号。 当弱定义符号与法线定义符号链接时,将使用法线定义符号而不会出错。 链接弱未定义符号时 并且未定义符号,则以系统特定的方式确定符号的值,没有错误。 在某些系统上,大写表示已指定默认值。

将实现保留在类头文件中是有效的,因为我相信您知道您是否编译了代码。const 关键字可确保您不会更改任何成员,并使实例在方法调用期间保持不可变。