为什么一个函数的多重定义是错误的?

Why is a multiple definition of a function an error?

本文关键字:定义 错误 函数 一个 为什么      更新时间:2023-10-16

示例:

//a.h
// no include guards
class A {};

如果我在一个翻译单元(一个cpp文件)中包含这个头两次,我会得到一个链接器错误,没关系。但是如果我把它包含在两个不同的翻译单元中也是可以的,对吧?

现在考虑全局函数:

// b.h
// no include guards
void foo() {}

不仅不允许在同一单元中包含它两次,而且也不允许在任何其他翻译单元中第二次包含它。为什么?

如果我在一个翻译单元(一个cpp文件)中包含这个头文件两次,我会得到一个链接错误

我相信你得到一个编译错误,而不是一个链接错误。我看不到应该生成的代码,所以链接器看不到它可以抱怨的东西。

空白foo {}

我相信你指的是void foo(){} !

c++使用"一个定义规则"。这只是一个定义,从用户的角度来看,获得两次定义是没有意义的,可能具有不同的语义。这就是语言本身,也是在一个程序中摆脱多个不同定义的一种简单方法。

为什么发起者决定使用ODR在这里不能给出。也许Bjarne读到这里,可以给你一个更详细的答案:-)

声明类型在多个翻译单元中是允许的(否则头文件将不起作用),你也可以定义静态和内联函数,但是(简化)你不能多次使用"外部链接"定义任何东西。

如果选择了,您希望链接器选择哪个副本?

c++有ODR: One定义规则。基本上,它说你可以在多个地方复制相同的定义,但是它们必须匹配,否则将会出现未定义的行为。

(我上面写的不太简化的版本是,链接器有聪明的方法来统一不可避免地要多次生成的c++主义(模板、构造函数等):所谓的"COMDAT"节,但这些不适用于普通函数。)


如果你想要真正的技术,那么你可以探索"弱"链接。基本上,你可以说链接器应该使用这个定义,除非另一个强定义可用(即没有"弱"属性的定义)。当你有可选的特性,你想在它们可用的时候启用它们,但不是一般的兴趣,这是很有用的。

另一个有趣的问题是共享库;有时,出于性能或依赖的原因,库将有一个链接到它的函数的私有副本。这可能导致程序的不同部分使用相同函数的不同副本,可能具有不同的功能和bug等。当函数中有你想要共享的静态数据时,这尤其麻烦。

当然,对于共享库,您也有冲突函数名称的可能性,但这违反了ODR,并且是一个错误。

你混淆了声明定义。在翻译单元中可以有许多相同的声明。所以如果foo是一个函数,返回一个整型,接受一个整型和一个字符串,那么声明int foo(int i, std::string s);可以重复很多次。在第一次使用之前,它应该存在于每个翻译单元(cpp文件)中。你也可以声明类。A类的前向声明是:class A;,没有其他内容,可以在一个翻译单元中重复。

函数定义在整个程序中只允许出现一次。foo的定义类似于:

int foo(int i, std::string s) {
    return i + s.size();
}
类A的完整声明可以是:
class A {
    int a;
    std::string s;
public:
    A(int a):s("") { this->a = a;} // this constructor is declared and defined inline
    int bar(int a);   // this method is only declared here, will be defined elsewhere
    ...
};

在每个翻译单元中只能出现一次。

方法bar定义(只能在类声明中声明)可以是:

int A::bar(int i) {
    return i + a;
}

这个定义在整个程序中只能出现一次。

一旦说了,规则是:

  • 包含只包含函数声明或前向类声明的文件,可以在每个翻译单元中包含多次(*)
  • 包含完整类声明的文件在每个翻译单元(*)中最多包含一次
  • 全局函数和方法的实现在cpp文件中,在整个程序中只出现一次。

(*)通常的用法是在每个翻译单元中使用include保护符只包含它们一次。

以上原因:首先,这是法律,我们都必须遵守。但是编译器很容易看到函数声明和前向类声明是相同的,所以很容易允许它们在一个翻译单元中多次出现。它们自己不生成代码,所以链接器不关心它们。

函数声明可以生成代码。所以它们必须在每个翻译单元中只出现一次。因为它们确实是声明,它们可以出现在任何翻译单元中,并且链接器将只保留生成代码的一个版本。定义实际上生成代码,它们没有理由在程序中重复。因此,如果同一个函数或方法在一个程序中定义了多次,链接器就会抛出错误。

这是编译和链接c++文件的自然结果。

当您将翻译单元编译成目标文件时,每个外部函数都由重定位项引用。当链接器将所有内容链接在一起时,它会尝试用实际函数的实际地址填充这些重定位存根。

现在,假设文件A.o定义了foo(),也就是说,它提供了实际的代码。现在假设B.o需要一个函数foo()。然后我们告诉链接器将A.o和B.o链接到一个可执行文件或库中。在这里,链接器想要在B.o中填补空白,即重定位。因此,当它看到B.o正在寻找foo(),而A.o提供了它时,它将A.o中foo()的最终地址放入B.o的调用代码中。

现在,假设我们告诉链接器将A.o, B.o C.o链接在一起。假设C.o,由于某种原因,也提供了foo()的定义,例如,因为它包含了一个定义它的头,A.o也包含了这个头。现在链接器想要填充B.o中的foo()存根。它应该选择哪一个?第一个?第二个吗?它是如何选择的?如果你不小心,这可能会导致麻烦。

这就是为什么默认情况下,主链接器禁止对同一个函数定义多个。通常,您可以使用标志启用它。但通常情况下,它要么是一个即时错误的信号,要么是未来令人头痛的信号。

inline将允许在多个翻译单元中定义相同的函数

inline
void foo() {}

但这取决于你是否在所有情况下提供相同的定义-如果它们不同,你将得到未定义的行为。

或者,不需要在头文件中定义这样的函数,您可以在头文件中用extern: 来声明它。

foo.hh:

extern void foo();

并在一个翻译单元中提供一个(非inline)定义。cc文件)。

模板函数是隐式inline,我相信。