【问题标题】:Why does the one definition rule exist in C/C++为什么C/C++中存在一个定义规则
【发布时间】:2020-02-05 23:13:17
【问题描述】:

在 C 和 C++ 中,您不能拥有具有两个定义的函数。例如,假设我们有以下两个文件:

1.c:

int main(){ return 0}

2.c:

int main(){ return 0}

发出命令gcc 1.c 2.c 会给你一个duplicate symbol 链接器错误。 为什么结构和类不会发生同样的情况?为什么我们可以拥有多个 相同结构的定义,只要它们具有相同的标记?

【问题讨论】:

  • class/struct 定义不是函数。它们是类型。 ODR 规则适用于函数和对象,而不是类型。
  • 但是为什么呢?您何时希望在另一个文件中具有相同的类定义?头文件不是用来做那个的吗?
  • 你认为头文件在做什么? #include 指令读取文件并复制整个内容 直接在该指令的位置。通过使用头文件,您可以在includes 这个头文件的每个文件中获得这个结构定义的副本。
  • @Josh 这些是(成员)函数定义类定义是在class/struct关键字和名称之后的{};之间的部分。
  • 关键是它是一个链接器错误。 C 编译器并非旨在成为“整个程序”编译器。它旨在分别编译每个“翻译单元”(即.c文件)。所以编译器在编译2.c 时不会“记住”main 是在1.c 中定义的。并且链接器没有看到main的源代码,所以它不知道这两个定义是相同的。所以如果链接器看到重复的符号,它会抛出一个错误。

标签: c++ c one-definition-rule


【解决方案1】:

要回答这个问题,必须深入研究编译过程以及每个部分需要什么(为什么要执行这些步骤的问题更具历史意义,可以追溯到 C 标准化之前的开始)

C 和 C++ 程序是通过多个步骤编译的:

  1. 预处理
  2. 编译
  3. 联动

预处理是所有以#开头的东西,在这里并不重要。

对每个翻译单元(通常是单个 .c.cpp 文件加上它包含的标题)执行编译。编译器一次获取一个翻译单元,读取它并生成类及其成员的内部列表,然后生成给定单元中每个函数的汇编代码(基于结构列表)。如果函数调用未内联(例如,它在不同的 TU 中定义),编译器会生成一个“链接”-“请在此处插入函数 X”供链接器读取。

然后链接器获取所有已编译的翻译单元并将它们合并为一个二进制文件,替换编译器指定的所有链接。


现在,每个阶段需要什么?

对于编译阶段,您需要

  • 此文件中使用的每个类的定义 - 编译器需要知道每个类成员的大小和偏移量以生成程序集
  • 声明此文件中使用的每个函数 - 以生成这些“链接”。

由于生成程序集不需要函数定义(只要在某处编译),因此在编译阶段不需要它们,仅在链接阶段需要。


总结一下:

有一个定义规则可以保护程序员免受自身伤害。如果他们不小心定义了一个函数两次,链接器会注意到并且没有生成可执行文件。

但是,每个翻译单元都需要类定义,因此无法为它们设置这样的规则。由于语言不能强求,程序员必须是负责任的存在,不能以不同的方式定义同一个类。

ODR 还有其他限制,例如你have to define template functions (or template class methods) in header files。你也可以承担责任,对编译器说“这个函数的每个定义都是一样的,相信我,伙计”,然后将函数设为inline

【讨论】:

  • 感谢您的回答,现在当您说class definitions are required in every translation unit, 时,这就是 c/c++ 的设计方式吗?因为不能只声明类和结构。它们是同时声明和定义的(与函数不同)。我说的对吗?
【解决方案2】:

具有 2 个定义的函数没有用例。要么这两个定义必须相同,使其无用,要么编译器无法分辨你的意思。

这不是类或结构的情况。允许对它们进行多个定义还有一个很大的优势,即如果我们想在多个文件中使用classstruct。 (由于包含,这间接导致了多个定义。)

【讨论】:

    【解决方案3】:

    结构、类、联合和枚举定义了可以在多个编译单元中用于定义这些类型的对象的类型。因此,每个编译单元都需要知道类型是如何定义的,例如为对象正确分配内存或确保类的指定成员确实存在。

    对于函数(如果它们不是内联函数),只要声明没有定义就足以生成例如函数调用。

    但是函数定义应该是单一的。否则编译器将不知道调用什么函数或者目标代码由于重复而太大而容易出错..

    【讨论】:

      【解决方案4】:

      这很简单:这是一个范围问题。每个链接在一起的编译单元都可以看到(可调用)非静态函数,而结构只能在定义它们的编译单元中看到。

      例如,将以下内容链接在一起是有效的,因为很清楚正在使用struct Foo 的哪个定义以及f 的哪个定义:

      1.c:

      struct Foo { int x; };
      static void f(void) { struct Foo foo; ... }
      

      2.c:

      struct Foo { double d; };
      static void f(void) { struct Foo foo; ... }
      int main(void) { ... }
      

      但是将以下内容链接在一起是无效的,因为链接器不知道要调用哪个f

      1.c:

      void f(void) { ... }
      

      2.c:

      void f(void) { ... }
      int main(void) { f(); }
      

      【讨论】:

        【解决方案5】:

        实际上,每个编程元素都与其适用范围相关联。在此范围内,您不能将相同的名称与元素的多个定义相关联。在编译世界中:

        1. 不能在一个文件中拥有多个同名的类定义。但是您可以在不同的编译单元中使用它。
        2. 不能在单个链接单元(库或可执行文件)中具有相同的函数或全局变量名称,但您可能在不同的库中具有相同名称的函数。
        3. 不能在同一目录中拥有同名的共享库,但您可以将它们放在不同的目录中。

        C/C++编译后的编译性能非常好。检查 2 个对象(如函数或类)的身份是一项耗时的任务。所以,它没有完成。仅考虑名称进行比较。最好考虑到 2 种类型不同并且出错,然后检查它们的身份。此规则的唯一例外是文本宏。

        宏是一个预处理器概念,历史上允许有多个相同的宏定义。如果定义发生更改,则会生成警告。比较宏上下文很容易,只是一个简单的字符串比较,但一些宏定义可能很大。

        类型是编译器的概念,它们由编译器解析。类型在对象库中不存在,由相应变量的大小表示。因此,没有理由在此范围内检查类型名称冲突。

        另一方面,函数和变量被命名为指向可执行代码或数据的指针。它们是应用程序的构建块。在某些情况下,应用程序是由来自世界各地的代码和库组装而成的。为了使用其他人的功能,您最好现在使用它的名称,并且您不希望其他人使用相同的名称。在共享库中,函数和变量的名称通常存储在哈希表中。那里没有重复的地方。

        正如我已经提到的,检查相同内容的函数很少进行,但是有一些情况,但不是在 c 或 c++ 中。

        【讨论】:

          【解决方案6】:

          在编程中阻止对同一事物使用两个不同定义的原因是避免在运行时决定使用哪个定义时产生歧义。

          如果您在一个程序中同时存在同一事物的两个不同实现,则可以将它们(每个具有不同的名称)别名为一个公共引用,以便在运行时决定使用两者中的哪一个。

          无论如何,为了区分两者,您必须能够指出您要使用的编译器。在 C++ 中,您可以重载一个函数,为其赋予相同的名称和不同的参数列表,这样您就可以区分要使用两者中的哪一个。但是在 C 中,编译器只保留函数的名称,以便能够在链接时解决哪个定义与您在不同编译单元中使用的名称匹配。如果链接器以两个同名的不同定义结尾,它无法为您决定使用哪一个,因此它会发出错误并放弃构建过程。

          以富有成效的方式使用这种模棱两可的意图应该是什么?这是您实际上要问自己的问题。

          【讨论】:

            猜你喜欢
            • 2011-05-10
            • 1970-01-01
            • 2016-04-05
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多