为什么在C/C++中存在一次定义规则?

6
在C和C++中,你不能有一个函数有两个定义。例如,假设我们有以下两个文件:

1.c:

int main(){ return 0;}

2.c:

int main(){ return 0;}

发出命令gcc 1.c 2.c将会导致一个重复符号的链接错误。为什么在结构体和类中不会发生同样的情况呢?为什么我们可以拥有多个相同标记的相同结构体定义?

4
“class”/“struct” 的定义不是函数,它们是类型。ODR规则适用于函数和对象,而不是类型。 - R Sahu
7
你觉得头文件是做什么的? #include 指令会读取一个文件并直接复制整个内容到该指令的位置。通过使用头文件,你可以在每个包含(include)此头文件的文件中获得该结构体的定义副本。 - Yksisarvinen
4
@Josh,这些是(成员)函数定义类定义是指在关键字class/struct之后以及名称前的{};之间的部分。 - Yksisarvinen
3
关键点在于这是一个链接器错误。C编译器不打算成为一个“整个程序”编译器。它的目的是分别编译每个“翻译单元”(即.c文件)。因此,当编译器编译2.c时,它不会“记住”main1.c中被定义。链接器也看不到main的源代码,因此它不知道这两个定义是相同的。因此,如果链接器看到重复的符号,它会抛出一个错误。 - user3386109
1
简短回答:没有使用 static,函数对编译单元可见,而结构体则不是。 - ikegami
显示剩余6条评论
6个回答

5
要回答这个问题,就必须深入了解编译过程以及每个部分所需的内容(为什么执行这些步骤的问题更多是历史性的,追溯到 C 标准化之前)。
C 和 C++ 程序需要经过多个编译步骤:
  1. 预处理
  2. 编译
  3. 链接
预处理是以 # 开头的所有内容,这里并不重要。
编译是在每个翻译单元上执行的(通常是一个单独的 .c.cpp 文件以及它包含的标头)。编译器逐个读取每个翻译单元,生成类及其成员的内部列表,然后根据结构列表生成每个函数的汇编代码。如果函数调用没有被内联(例如,在不同的 TU 中定义),编译器会为链接器生成一个“链接”-“请在此处插入函数 X”,以供链接器读取。
然后链接器将所有已编译的翻译单元合并为一个二进制文件,并替换编译器指定的所有链接。
现在,在每个阶段都需要什么?
对于编译阶段,您需要文件中使用的每个类的定义 - 编译器需要知道每个类成员的大小和偏移量才能生成汇编代码。您还需要文件中使用的每个函数的声明 - 用于生成这些“链接”。
由于在生成汇编代码时不需要函数定义(只要它们在某个地方被编译),因此它们在编译阶段无需,只在链接阶段需要。

总之:

一个定义规则的存在是为了保护程序员免受自己的伤害。如果他们意外地定义了一个函数两次,链接器将会注意到并且不会生成可执行文件。

然而,类定义在每个翻译单元中都是必需的,因此不能为它们设置这样的规则。由于语言无法强制执行,程序员必须是负责任的人,并且不要以不同的方式定义相同的类。

ODR还有其他限制,例如,您 必须在头文件中定义模板函数(或模板类方法)。您也可以承担责任并告诉编译器“这个函数的每个定义都是相同的,请相信我”,并使该函数成为inline


谢谢您的回答,现在当您说“每个翻译单元都需要类定义”时,这只是C/C++的设计原则,因为类和结构体不能仅仅被声明,它们必须同时被声明和定义(不像函数)。我理解得对吗? - user12848549
您可以在不定义它的情况下声明一个类。class myclass;。然后后续的函数可以具有对该类型的引用或指针,但不能具有该类型的值,也不能访问任何其成员/方法。通常情况下这并没有什么用处。 - Mooing Duck
你可以声明一个未定义的类。class myclass;。然后后续的函数可以有该类型的引用或指针,但不能有该类型的值,也不能访问其成员/方法。这通常是没有用的。 - undefined

3

一个函数有两个定义是没有用处的。要么这两个定义是相同的,使它变得无用,要么编译器无法确定你想要哪个。

但这不适用于类或结构。允许它们有多个定义也有很大的优点,例如如果我们想在多个文件中使用一个 classstruct。(由于包含文件的原因)这最终会间接导致多个定义。


0

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

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

C/C++编译非常注重编译性能。检查两个对象(如函数或类)是否相同是一项耗时的任务。因此,不会这样做。仅考虑名称进行比较。最好认为2种类型是不同的并出错,而不是检查它们是否相同。唯一的例外是文本宏。

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

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

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

正如我已经提到的,很少检查函数是否具有相同的内容,但有一些情况,但不适用于C或C++。


0

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

如果您有两个不同的实现来共存于一个程序中,则存在将它们别名化(每个都用不同的名称)成一个公共引用的可能性,以便在运行时决定使用其中之一。

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

如何以有效的方式使用这种歧义?这实际上是您必须问自己的问题。


0

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

对于函数(如果它们不是内联函数),只需要具有其声明而不是定义即可生成例如函数调用。

但是,函数定义应该是唯一的。否则,编译器将不知道调用哪个函数,或者由于重复而使目标代码过大,并且容易出现错误。


0
很简单:这是一个范围的问题。非静态函数可以被链接在一起的每个编译单元看到(可调用),而结构体只能在定义它们的编译单元中看到。
例如,以下代码可以链接在一起,因为可以清楚地知道使用哪个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(); }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接