【问题标题】:C++ - One Function Declaration, Multiple definitions, And it still works?C++ - 一个函数声明,多个定义,它仍然有效吗?
【发布时间】:2017-11-03 02:39:43
【问题描述】:

好的,我有 3 个文件、一个标头、该标头的来源和一个主文件。在头文件中,我定义了一个带有函数的类。在源文件中,我定义了函数。但是,在主文件中,我重新定义函数,然后在主函数中创建类的实例并调用该函数。这编译得很好 - 没有任何警告。至少可以说输出是可怕的。

头文件:testme.h

#ifndef testme_h_
#include <iostream>
using namespace std;

class wtf {
public:
  string getStr();
};

#endif

来源:testme.cpp

#include "testme.h"

string wtf::getStr() {
  return "Hello World!";
};

主要:main.cpp

#include <iostream>
using namespace std;

#include "testme.h"

string wtf::getStr()
{
  return "God is Dead.";
}

int main()
{
  wtf f;
  cout << f.getStr() << endl;
}

输出:

God is Dead.

为什么会这样?为什么没有关于多个定义的错误?为什么源文件的定义会被忽略?为什么没有警告?

部分回答 当它被重新编译为“g++ main.cpp testme.cpp -o sanity.o”时,实际上会产生链接器错误。

然而,让我感到惊讶的是,我写的这个小案例反映了我在一个更大的程序中遇到的一个问题,该程序在库中定义了一个函数,然而我们在另一个“测试套件”程序中重新定义了一个函数,几乎相同大大地。为什么会这样?如果它在库中允许它覆盖 ODR 怎么样?

【问题讨论】:

  • 未定义行为,您违反了单一定义规则。请参阅en.cppreference.com/w/cpp/language/definition中的一个定义规则
  • 否,编译器仅将每个 .cpp 文件视为单独的编译单元。链接器可能会抱怨,或者它可能只是选择一个并丢弃另一个。正如他们所说,UB就是UB。
  • 不一定——你的编译器/链接器命令是什么?您是否与 testme.o 链接?
  • @UtMan88 使用该命令,编译器永远不会看到testme.o,因此它不知道还有另一个定义。
  • 未定义行为的部分理由是某些规则难以执行。如果规则易于执行,委员会通常需要错误或某种诊断。但是,如果规则难以执行,那么委员会宣布违反规则将是未定义的。在某些情况下,编译器可能会发现这个问题;特别是如果链接器和编译器是同一个程序。但是在某些情况下(单独编译),编译器不一定有足够的信息来执行一个定义规则。

标签: c++ function-definition


【解决方案1】:

为什么会这样?为什么没有关于多个定义的错误?为什么源文件的定义会被忽略?为什么没有警告?

部分答案当它被重新编译为“g++ main.cpp testme.cpp -o sanity.o”时,实际上会产生链接器错误。

C++ 的设计和演变中,Stroustrup 说,在设计一个特性时,他偶尔会在 (1) 编译器能够通过警告和错误消息强制执行的复杂规则之间做出选择,或 (2) 编译器可能无法在所有情况下强制执行的简单规则。他尝试选择简单的规则,并希望编译器最终能够强制/检测错误。 C 和 C++ 的几个部分有效地展示了 1970 年代、1980 年代、1990 年代等的最新技术(例如,inlineregistervolatile)。可能还有更多。

粗略地说,这种权衡是标准中未定义行为的根源。如果一条简单的规则无法执行(或委员会认为执行该规则代价高昂),则该规则保留在标准中,并且违规行为被宣布为未定义行为。 It is the programmer's responsibility to never trigger undefined behavior。编译器和相关工具有时可能会强制执行其中一些规则。一些编译器或相关工具甚至有可能一直强制执行其中一些规则。但是,一般来说,你是靠自己的。

设计与进化 甚至包括对您的具体问题的讨论。在编写完编译器之后,Stroustrup 也不想再编写链接器了。他想出了一个依赖系统链接器的简单方法,但在早期,许多链接器对符号的长度有严格的限制。他努力说服编写系统链接器的人对它们进行足够的更改,以使它们与 Cfront 一起工作。最终,他成功了。

如今,链接器通常与编译器一起提供,但标准委员会仍将其视为一个单独的工具,可能不受编写编译器的人的控制。存在一个定义规则主要是为了确保编译器的输出与大多数标准链接器兼容,但委员会并不要求这些链接器能够检测到违反 ODR 的情况。此外,程序可能会以增量方式编译和链接,因此无法保证链接器将有足够的信息来检测 ODR 违规。

值得注意的是many compilers 将为inline 函数和template 函数生成多个符号,期望链接器抛出冗余定义。所以链接器看到多个定义不一定是错误。 (就 ODR 而言,这些定义必须相同,以便链接器可以丢弃除一个之外的所有内容。)

如果它在库中允许它覆盖 ODR 呢?

我不会说“覆盖”。听起来这种行为是故意的。我会说“违反而不被发现。”答案很简单,“标准委员会不希望链接器总是检测违规行为,而许多链接器也没有。”避免违反 ODR 是您的责任。不过,您不是靠自己:命名空间的存在部分是为了让这个问题更容易解决。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-12-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-05-30
    • 2017-06-21
    相关资源
    最近更新 更多