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

时间:2015-08-20 14:37:02

标签: c++

一个例子:

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

如果我在一个翻译单元(一个cpp文件)中包含此标题两次,我将收到链接器错误,它没问题。但如果我将它包含在2个不同的翻译单元中,那也没关系,对吧?

现在考虑一个全局函数:

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

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

5 个答案:

答案 0 :(得分:3)

  

如果我在一个翻译单元(一个cpp文件)中包含此标题两次,我将收到链接器错误

我相信你得到编译错误而不是链接器错误。我看不到应该生成的代码,因此链接器看不到它可以抱怨的任何内容。

  

void foo {}

我相信你的意思是void foo(){}

C ++使用“一个定义规则”。这只是一个定义,从用户的角度来看,定义两次,也许是不同的语义是没有意义的。这只是语言,它是在一个程序中摆脱多个不同定义的简单方法。

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

答案 1 :(得分:2)

在多个翻译单元中允许声明类型(否则头文件无效),您也可以定义静态和内联函数,但是(简化)您无法定义任何内容“外部联动”多次。

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

C ++有ODR:One Definition Rule。基本上,它表示您可以在多个位置复制相同的定义,但它们必须匹配,否则将遵循未定义的行为。

(我上面写的不那么简单的版本是链接器有巧妙的方法来统一不可避免地多次生成的C ++主义(模板,构造函数等):所谓的“COMDAT”部分,但这些并不适用于普通的功能。)

如果你想获得真正的技术,那么你可以探索“弱”联系。基本上,您可以说链接器应该使用此定义,除非另一个强大的可用(即没有“弱”属性的那个)。当您具有可选功能时,如果它们可用时启用但不是一般感兴趣,则此功能非常有用。

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

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

答案 2 :(得分:1)

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

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

现在,假设文件A.o定义foo(),即它提供实际代码。现在说B.o需要一个函数foo()。然后我们告诉链接器将A.o和B.o链接到一个可执行文件或库中。在此,链接器希望填补B.o中的差距,即重定位。所以当它看到B.o正在寻找foo(),并且A.o提供它时,它将foo()的最终地址放在B.o的调用代码中的A.o中。

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

这就是为什么默认情况下,主链接器禁止对相同功能的多个定义。通常,您可以使用标志启用它。但通常情况下,它会发出即时错误或未来头痛的信号。

答案 3 :(得分:1)

inline将允许在多个翻译单元中定义相同的功能。

inline
void foo() {}

但是你要确保在所有情况下都提供相同的定义 - 如果它们不同,你将得到未定义的行为。

或者,不是在头文件中定义这样的函数,而是在extern的头文件中声明它:

<强> foo.hh:

extern void foo();

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

模板函数隐含inline我相信。

答案 4 :(得分:1)

您在声明定义之间混淆了。您可以在翻译单元中包含尽可能多的相同声明。因此,如果foo是一个函数返回和int并且取一个int和一个字符串,则声明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文件中,并且只在整个程序中出现一次。

(*)常见的用法是在每个翻译单元中只使用一次使用包含警戒来包含一次。

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

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