99

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

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

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

private:
    UInt32 _numberChannels;

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

4

7 回答 7

161

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

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

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

富.h:

#ifndef FooH
#define FooH

class Foo
{
public:
    UInt32 GetNumberChannels() const;

private:
    UInt32 _numberChannels;
};

#endif

Foo.cpp:

#include "Foo.h"

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

酒吧.cpp:

#include "Foo.h"

Foo f;
UInt32 chans = f.GetNumberChannels();

处理器分别解析 Foo.cpp 和 Bar.cpp 并生成编译器随后解析的以下代码:

Foo.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 并包含调用 into 的引用Foo::GetNumberChannels()。Foo.cpp 编译成 Foo.obj 并包含Foo::GetNumberChannels(). 编译后,链接器将匹配 .obj 文件并将它们链接在一起以生成最终的可执行文件。

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

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

它与 const 关键字有关吗?

不会。const关键字仅向编译器表明该方法不会改变运行时正在调用的对象的状态。

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

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

于 2013-01-25T08:22:59.507 回答
46

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

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

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

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

于 2013-01-25T07:56:32.327 回答
9

由于是在类声明中定义的成员函数,因此它被隐式声明。 这并不意味着编译器必须内联它,而是意味着您不会破坏单一定义规则。它与*完全无关。它也与函数的长度和复杂性无关。inlineconst

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

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

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

于 2013-01-25T07:54:58.947 回答
4

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

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

C++ 的设计者希望能够实现具有良好数据隐藏的面向对象编程,因此他们希望看到大量的 getter 和 setter 函数。他们不想要不合理的性能惩罚。因此,他们设计了 C++,使得 getter 和 setter 不仅可以在头文件中声明,而且可以实际实现,因此它们可以内联。你展示的那个函数是一个getter,当编译那个C++代码时,不会有任何函数调用;获取该值的代码将被编译到位。

可以制作一种没有头文件/源文件区别的计算机语言,但只有编译器可以理解的实际“模块”。(C++ 没有这样做;它们只是建立在源文件和文本包含的头文件的成功 C 模型之上。)如果源文件是模块,编译器可能会从模块中提取代码,然后内联该代码。但是 C++ 的方式实现起来更简单。

于 2013-01-25T08:02:23.093 回答
2

据我所知,有两种方法,可以在头文件里面安全的实现。

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

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

于 2013-01-25T07:59:12.903 回答
1

C++ 标准引号

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

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

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

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

这也在 12.2.1 “成员函数”的注释中明确提到:

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

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

GCC 8.3 实现

主文件

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

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

“W” “w” 该符号是一个弱符号,尚未被专门标记为弱对象符号。当弱定义符号与正常定义符号链接时,使用正常定义符号不会出错。当链接弱未定义符号且未定义符号时,符号的值以系统特定的方式确定而不会出错。在某些系统上,大写表示已指定默认值。

于 2019-08-08T06:45:40.730 回答
0

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

于 2013-01-25T08:24:45.847 回答