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


76

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

但是,今晚复习一些C ++代码,我在类的头文件中找到了它。

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

private:
    UInt32 _numberChannels;

那么,为什么在标头中有实现?与const关键字有关吗?内联类方法吗?与在CPP文件中定义实现相比,以这种方式实现的目的/好处到底是什么?


6
该函数是内联的
某位编程人员花了

1
REconst预选赛;这仅意味着该方法不会更改对象的状态。
2013年

4
@Alex:您不正确地认为编译器必须内联函数。编译器/链接器必须处理多个定义(内联函数不受一个定义规则的约束)。
Michael Burr

3
@Alex no编译器不必限制它。它可以在某些翻译中内联,但不必在所有TU中都这样做。是的,有多个定义,但是由于该函数是(隐式)声明为内联声明,因此如果编译器未内联该符号,则编译器会对其进行标记,并且链接程序知道它仅需选择导出的符号之一。模板实例化是相同的。
Arne Mertz 2013年

1
例如,如果VC2010神奇的“内联预算”已用尽,则不会内联这种功能。
ActiveTrayPrntrTagDataStrDrvr

Answers:


135

好的,无论如何都不是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文件中定义实现相比,以这种方式实现的目的/好处到底是什么?

有效使用后,编译器通常可以生成更快,更好的优化机器代码。


2
根据您的解释,这是否意味着您可以直接在CPP文件中声明一个类,甚至在包装该类声明的大括号内声明成员函数的内容,因此您不必在外部使用::语法呢?(我知道这不是很好的编码。我只是在问它是否是有效的编码。)从这个意义上讲,这是否意味着所有成员都将被内联,或者至少被标记为?(有什么话你可以说不要内联吗?)
Mark A. Donohoe 2013年

@MarqueIV您所描述的在技术上是可行的,但是它将阻止您在.cpp文件定义的编译单元之外的任何地方使用该类(本质上是cpp文件本身,除非您将其#include在其他文件中。巨大的禁忌!)。但是,如果在其他文件中向前声明该类,则仍可以使用指向该类的指针或引用(永远不要取消引用它们或通过指针或引用访问成员)。
Agentlien 2013年

是的,我知道我不能那样使用它们。这更像是“你能做到”,而不是“你应该做到”。我更多地询问它是否可以成功编译。无论如何,由于细节,您得到了答案,而且您还是唯一一个在答案中引用const关键字的人(至少在我同意的情况下)。谢谢!:)
Mark A. Donohoe 2013年

该示例中的标题保护似乎没有执行任何操作。您能否解释标题保护何时生效,何时不生效?
Helin Wang

@RemyLebeau谢谢!另一个问题,如果实现是在带有标头保护的标头文件中。头文件被共享库项目和主项目使用。并且主项目使用库项目。在链接期间,链接器会抱怨同一函数被定义两次(重复的符号)吗?
Helin Wang

35

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

但是,有一个例外。如果您声明一个函数为内联函数,则该函数不受一定义规则的限制。这就是这里发生的情况,因为在类定义内定义的成员函数是隐式内联的。

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

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


2
“因为在类定义内定义的成员函数是隐式内联的。” 有价值的信息。不知道。但是那个const词呢?
Mark A. Donohoe 2013年

5
感谢您提及一个定义规则
dubbaluga 2014年

6

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

如果它是非成员函数,则必须将其显式声明为inline

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

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


通过一个定义规则,您是否在这里表示如果在头文件中定义了函数,则可能无法在另一个cpp文件中定义它吗?
ashu

3

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

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

C ++的设计师希望启用具有良好数据隐藏功能的面向对象的编程,因此他们希望看到大量的getter和setter函数。他们不希望表现不合理。因此,他们设计了C ++,以便使getter和setter不仅可以在标头中声明,而且可以实际实现,因此可以内联。您显示的该函数是一个吸气剂,并且在编译该C ++代码时,将不会进行任何函数调用。获取该值的代码将在适当位置编译。

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


1

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

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

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


0

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


0

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.