繁体   English   中英

C++ header 文件如何包含实现?

[英]How can a C++ header file include implementation?

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

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

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

private:
    UInt32 _numberChannels;

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

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

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

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

foo.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;
}

Bar.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;
}

Bar.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文件中的实现相比,这样做的好处/意义究竟是什么?

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

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

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

内联本身是对编译器的暗示,函数可能是内联的良好候选者。 也就是说,将对它的任何调用扩展为函数的定义,而不是简单的函数调用。 这是一种优化,它可以交换生成的文件的大小以获得更快的代码。 在现代编译器中,除了它对单一定义规则的影响之外,为函数提供这种内联提示大多被忽略。 此外,编译器总是可以自由地内联它认为合适的任何函数,即使它没有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 ++的方式实现起来更简单。

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

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

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

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

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实施

main.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”符号是一个弱符号,未被特别标记为弱对象符号。 当弱定义符号与正常定义的符号链接时,使用正常定义的符号而没有错误。 当链接弱未定义符号且未定义符号时,符号的值以特定于系统的方式确定而没有错误。 在某些系统上,大写表示已指定默认值。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM