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

时间:2013-01-25 07:50:05

标签: c++ header-files

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

但是,今晚回顾一些C ++代码,我在类的头文件中找到了这个...

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

private:
    UInt32 _numberChannels;

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

7 个答案:

答案 0 :(得分:90)

  

好的,不是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文件中的实现定义相比,这样做的好处/点是什么?

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

答案 1 :(得分:27)

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

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

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

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

答案 2 :(得分:4)

由于在类声明中是定义的成员函数,它隐含地声明 inline。这并不意味着编译器来内联它,但这意味着你不会破坏one definition rule。它与const * 完全无关。它与函数的长度和复杂性无关。

如果它是非成员函数,那么您必须明确声明它为inline

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

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

答案 3 :(得分:2)

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

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

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

可以使计算机语言没有头文件/源文件的区别,但只有编译器能理解的实际“模块”。 (C ++没有这样做;它们只是建立在源文件的成功C模型和文本包含的头文件之上。)如果源文件是模块,编译器可以将代码拉出模块然后内联代码。但是C ++的做法更容易实现。

答案 4 :(得分:1)

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

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

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

答案 5 :(得分:0)

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

答案 6 :(得分:0)

C ++标准报价

C++17 N4659 standard draft 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”符号是一个弱符号,尚未专门标记为弱对象符号。当弱定义符号与普通定义符号链接时,使用普通定义符号不会出错。当弱的未定义符号链接时                  并且未定义符号,则以系统特定的方式确定符号的值而不会出错。在某些系统上,大写表示已指定默认值。