【问题标题】:Why is a multiple definition of a function an error?为什么函数的多重定义是错误的?
【发布时间】:2015-08-20 14:37:02
【问题描述】:

一个例子:

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

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

现在考虑一个全局函数:

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

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

【问题讨论】:

    标签: c++


    【解决方案1】:

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

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

    void foo{}

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

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

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

    【讨论】:

    • Bjarne 没有发明 ODR,尽管他可能已经命名了它。
    【解决方案2】:

    在多个翻译单元中允许声明 types(否则头文件将不起作用),您也可以定义静态和内联函数,但(简化)您不能定义任何内容多次“外部链接”。

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

    C++ 有 ODR:一个定义规则。基本上,它表示您可以在多个位置复制相同的定义,但它们必须匹配,否则会出现未定义的行为。

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


    如果您想获得真正的技术,那么您可以探索“弱”链接。基本上,你可以说链接器应该使用这个定义,除非另一个强定义可用(即没有“弱”属性的定义)。当您有一些可选功能想要在可用时启用但不是普遍感兴趣的功能时,这很有用。

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

    当然,对于共享库,您也可能会出现函数名称冲突,但这是违反 ODR 和错误的。

    【讨论】:

      【解决方案3】:

      你混淆了声明定义。您的翻译单元中可以有尽可能多的相同声明。因此,如果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 文件中,并且在整个程序中只发生一次。

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

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

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

      【讨论】:

        【解决方案4】:

        这是 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() 存根。它应该选择哪一个?首先?第二?它是如何选择的?如果您不小心,这可能会导致麻烦。

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

        【讨论】:

        • 是的,这正是我的直觉告诉我的。但是,如果您将放置class A 而不是foo(),该怎么办?基本上行为应该是相同的,但实际上类没有问题。
        【解决方案5】:

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

        inline
        void foo() {}
        

        但您可以确保在所有情况下都提供相同的定义 - 如果它们不同,您将获得未定义的行为。

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

        foo.hh:

        extern void foo();
        

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

        我相信模板函数是隐含的inline

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2020-01-26
          • 2021-06-09
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多