为什么不能在C中在C ++中的头文件中包含方法定义?


Answers:


28

在C语言中,如果在头文件中定义函数,则该函数将出现在编译的每个包含该头文件的模块中,并且将为该函数导出公共符号。因此,如果在header.h中定义了功能additup,并且foo.c和bar.c都包含header.h,则foo.o和bar.o都将包含additup的副本。

当您将这两个目标文件链接在一起时,链接器将看到符号additup被定义了多次,并且不允许这样做。

如果您声明该函数为静态函数,则不会导出任何符号。目标文件foo.o和bar.o仍将包含该函数代码的单独副本,并且它们将能够使用它们,但是链接器将看不到该函数的任何副本,因此它不会抱怨 当然,其他模块也无法看到该功能。并且您的程序将被两个具有相同功能的相同副本所肿。

如果仅在头文件中声明该函数,但未定义它,然后仅在一个模块中对其进行定义,则链接器将看到该函数的一个副本,并且程序中的每个模块都将能够看到它,并且用它。并且您的编译程序将仅包含该函数的一个副本。

因此,您可以在C的头文件中拥有函数定义,这只是样式不好,形式不好以及全面的想法。

(通过“声明”,我的意思是提供没有主体的函数原型;通过“定义”,我的意思是提供函数主体的实际代码;这是标准的C术语。)


2
这不是一个 主意-即使在GNU Libc标头中也可以找到这种类型的东西。
SK-logic

但是在条件编译指令中头文件的惯用换行又如何呢?然后,即使在标头中定义了声明为AND的函数,也只会加载一次。我是C的新手,所以我可能会误会。
user305964 '17

2
@papiro问题是包装仅在编译器的一次运行中提供保护。因此,如果在一次运行中将foo.c编译为foo.o,在另一次运行中将bar.c编译为bar.o,然后在第三次(通常)中将foo.o和bar.o链接到a.out,则该包装不会阻止它的多个实例,每个目标文件一个。
戴维·康拉德

这里不是要解决的问题#ifndef HEADER_H吗?
罗伯特·哈维

27

在这方面,C和C ++的行为非常相似-您可以inline在标头中包含函数。在C ++中,主体位于类定义内的任何方法都是隐式的inline。如果要在C中执行相同的操作,请声明函数static inline


声明函数static inline ” ...,并且在使用该函数的每个翻译单元中,您仍然会有该函数的多个副本。在具有非static inline功能的C ++中,您只有一个副本。要在C的标头中实际包含实现,您必须1)将实现标记为inline(例如inline void func(){do_something();}),并且2)实际上说此函数将位于某个特定的转换单元中(例如void func();)。
Ruslan

6

头文件的概念需要一些解释:

您可以在编译器的命令行上提供文件,也可以执行“ #include”。大多数编译器接受扩展名为c,C,cpp,c ++等的命令文件作为源文件。但是,它们通常包括一个命令行选项,以启用对源文件的任意扩展名的使用。

通常,命令行上给出的文件称为“源”,其中包含的文件称为“标头”。

预处理器步骤实际上将所有内容都带走了,并使所有内容对于编译器而言就像一个大文件。此时,标题或源中的内容实际上并不重要。通常有一个编译器选项可以显示此阶段的输出。

因此,对于在编译器命令行上给出的每个文件,都将一个巨大的文件分配给编译器。这可能具有代码/数据,这些代码/数据将占用内存和/或创建要从其他文件引用的符号。现在,每一个将生成一个“对象”图像。如果在两个以上链接在一起的目标文件中找到相同的符号,则链接器可以给出“重复符号”。也许这就是原因;不建议将代码放在头文件中,头文件可以在目标文件中创建符号。

“内联”通常是内联的..但是在调试时,它们可能不会内联。那么,链接器为什么不给出乘法定义的错误?简单...这些是“弱”符号,只要来自所有对象的弱符号的所有数据/代码都具有相同的大小和内容,链接将保留一个副本并从其他对象中删除副本。有用。


3

您可以在C99中执行此操作:inline保证可以在其他地方提供函数,因此,如果未内联函数,则其定义将转换为声明(即,实现被丢弃)。当然,您可以使用static


1

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


-4

可能出于同样的原因,您必须将完整的方法实现放入Java的类定义中。

它们看起来很相似,带有弯曲的方括号和许多相同的关键字,但是它们是不同的语言。


1
没有,实际上是一个真正的答案,以及C ++的主要目标之一是向后兼容C.
埃德S.

4
不,它被设计为具有“高度的C兼容性”和“没有与C的不必要的不​​兼容性”。(均来自Stroustrup)。我同意可以给出更深入的答案,以强调为什么这种特殊的不兼容性不是没有根据的。随时提供一个。
Paul Butcher

我本来可以,但是我发布时已经有西蒙·里希特了。我们可以质疑“向后兼容”和“高度C兼容性”之间的区别,但事实是这个答案是不正确的。如果我们要比较C#和C ++,那么最后一条语句是正确的,但对于C和C ++来说,则不是那么正确。
Ed S.
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.