c++ 为什么不同cpp文件中的类的重复定义可以被链接而没有错误?

z4bn682m  于 2023-07-01  发布在  其他
关注(0)|答案(2)|浏览(147)

在src1.cpp中,我定义了一个名为“Test”的类和一个名为“f1”的函数,该函数创建了Test的一个示例。

// src1.cpp
#include <iostream>

class Test {
    int a = 1;

public:
    Test() {
        std::cout << "src1 Test\n";
    }
};

void f1() {
    Test t;
}

在src1.h中,“f1”被暴露。

// src1.h

#pragma once

void f1();

src2.cpp和src2.h的创建方式大致相同。具有相同名称的类和构造它的示例的函数。

// src2.cpp

#include <iostream>

class Test {
    long a;

public:
    Test() {
        std::cout << "src2 Test\n";
    }
};

void f2() {
    Test t;
}
// src2.h

#pragma once

void f2();

然后在www.example.com中main.cc,我调用f1和f2。

// main.cpp

#include "src1.h"
#include "src2.h"

int main() {
    f1();
    f2();
    return 0;
}

我用下面的命令编译**,没有警告和错误**。g++ -Wall -o main main.cpp src1.cpp src2.cpp程序输出为:

src1 Test  
src1 Test

编译器似乎允许Test类的不同定义,f1和f2都调用src1.cpp中Test的构造函数。
当我以相反的顺序编译时,比如g++ -Wall -o main main.cpp src1.cpp src2.cpp,程序输出变为:

src2 Test  
src2 Test

当我用一个重复的变量替换class Test时,会发生编译错误。在这种情况下,链接器如何处理重复的定义?

e0bqpujr

e0bqpujr1#

您的代码是不允许的,即使您没有得到任何警告和错误。

// a.cpp
class Test {
    int a = 1;
    // ...
};

// b.cpp
class Test {
    long a;
    // ...
};

这段代码违反了单定义规则(ODR),因为Test的定义在不同的翻译单元(TU)中必须始终相同(即.cpp文件)。
然而,odr违反是**格式不良,不需要诊断(IFNDR)**的情况。这意味着您的代码不是有效的C++,但编译器不需要发出任何警告或错误。C实际上有大量这样的IFNDR情况。
C
标准中的措辞是这样的:
对于具有多个翻译单元中的定义的任何可定义项D

  • 如果D是一个非内联非模板化函数或变量,或者
  • 如果不同翻译单元中的定义不满足以下要求,

程序是病态的;[...]

*每一个这样的定义应包含相同的令牌序列

  • [basic.def.odr]/14
    这意味着T的所有定义必须完全相同,基本上是彼此的复制/粘贴版本。

解决方案

很难确保您不会手动违反ODR。这就是为什么你通常把类型放在头文件中,这样就可以保证它们在所有包含它们的cpp文件中是相同的:

// test.hpp
class Test {
    int a;
};

// a.cpp
#include "test.hpp"

// b.cpp
#include "test.hpp"

或者,如果您想重用Test名称,但给予不同的定义:

// a.cpp
namespace { // anonymous namespace
class Test {
    int a = 1;
    // ...
};
}

// b.cpp
namespace {
class Test {
    long a;
    // ...
};
}

这也解决了这个问题,因为两个Test类具有内部链接,这意味着a.cppb.cpp中的Test引用不同的类型。因为它们是不同的类型,所以它们也可以被不同地定义。

为什么链接器检测不到这个,却检测到重复的变量?

原因很简单:类型不是像函数和变量那样被发出的实体,它们只是存在于程序中。它们仍然必须在任何地方都被定义为相同的,但是单独定义Test根本不会产生任何程序集。
即使它是id,也很难检测到odr违规,因为Test * 可以在多个地方定义。它只需要以同样的方式定义。链接器必须以某种方式测试两个类之间的相等性,但C标准对链接器的要求非常低。只有在使用C20模块时才能得到有保证的诊断。

参见

w3nuxt5m

w3nuxt5m2#

程序的行为未定义。
类的定义包括它们的内联构造函数应该由相同的标记组成,但是它们具有不同的标记和实体。
例如,在C++14标准中有3.2一个定义规则)
6一个类类型(第9章)、枚举类型(第7.2章)、具有外部链接的内联函数(第7.1.2章)、类模板(第14章)、非静态函数模板(第14.5.6章)、类模板的静态数据成员(第14.5.1.3章)、类模板的成员函数(第14.5.1.1章)或程序中某些模板参数未指定的模板专用化(第14.7章、第14.5.5章)的定义可以不止一个,只要每个定义出现在不同的翻译单元中,并且定义满足以下要求。给定在多于一个翻译单元中定义的名为D的这样的实体,则

(6.1)-D的每个定义都应包含相同的标记序列;和

等等
如果D是一个模板,并且在多个翻译单元中定义,那么前面的要求既适用于模板定义中使用的模板的封闭范围中的名称(14.6.3),也适用于示例化时的从属名称(14.6.2)。如果D的定义满足所有这些要求,那么行为就好像D只有一个定义。如果D的定义不满足这些要求,那么行为是未定义的

相关问题