24

如果两个 C++ 文件对同名的类有不同的定义,那么当它们被编译和链接时,即使没有警告也会抛出一些东西。例如,

// a.cc
class Student {
public:
    std::string foo() { return "A"; }
};
void foo_a()
{
    Student stu;
    std::cout << stu.foo() << std::endl;
}

// b.cc
class Student {
public:
    std::string foo() { return "B"; }
};
void foo_b()
{
    Student stu;
    std::cout << stu.foo() << std::endl;
}

当使用 g++ 编译并链接在一起时,两者都将输出“A”(如果在命令行顺序中 a.cc 在 b.cc 之前)。

一个类似的话题是here。我看到命名空间会解决这个问题,但我不知道为什么链接器甚至没有发出警告。如果该类的一个定义具有另一个未定义的额外功能,则假设 b.cc 更新为:

// b.cc
class Student {
public:
    std::string foo() { return "B"; }
    std::string bar() { return "K"; }
};
void foo_b()
{
    Student stu;
    std::cout << stu.foo() << stu.bar() << std::endl;
}

然后 stu.bar() 效果很好。感谢任何可以告诉我编译器和链接器在这种情况下如何工作的人。

作为一个额外的问题,如果类是在头文件中定义的,是否应该总是用未命名的命名空间包装它们以避免这种情况?有没有副作用?

4

4 回答 4

22

这违反了一个定义规则(C++03, 3.2/5“一个定义规则”),它说(除其他外):

如果每个定义出现在不同的翻译单元中,并且定义满足以下要求,则可以有多个类类型的定义(第 9 条),... 在程序中。给定这样一个名为 D 的实体在多个翻译单元中定义,则

  • D 的每个定义都应由相同的记号序列组成;

如果您违反了单一定义规则,则行为是未定义的(这意味着可能会发生奇怪的事情)。

链接器看到多个定义Student::foo()- 一个在 a 的目标文件中,一个在 b 的目标文件中。但是它并没有抱怨这一点;它只是选择两者之一(碰巧,它遇到的第一个)。这种对重复函数的“软”处理显然只发生在内联函数中。对于非内联函数,链接器抱怨多个定义并拒绝生成可执行文件(可能有放宽此限制的选项)。GNUld和 MSVC 的链接器都以这种方式运行。

这种行为是有道理的;内联函数需要在它们使用的每个翻译单元中可用。在一般情况下,它们需要有可用的非内联版本(以防调用未内联或函数的地址被占用)。 inline实际上只是围绕单一定义规则的免费通行证 - 但要使其工作,所有内联定义都必须相同。

当我查看目标文件的转储时,我看不到任何明显的东西可以向我解释链接器如何知道一个函数允许有多个定义而其他函数不允许,但我确信有一些标志或记录就是这样做的。不幸的是,我发现链接器的工作原理和目标文件的详细信息并没有得到很好的记录,所以确切的机制对我来说可能仍然是个谜。

至于你的第二个问题:

作为一个额外的问题,如果类是在头文件中定义的,是否应该总是用未命名的命名空间包装它们以避免这种情况?有没有副作用?

您几乎肯定不想这样做,每个类在每个翻译单元中都是不同的类型,因此从技术上讲,它们不能从一个翻译单元传递到另一个翻译单元(通过指针、引用或复制)。此外,您最终会得到任何静态成员的多个实例。那可能效果不好。

将它们放在不同的命名空间中。

于 2012-05-20T08:39:33.517 回答
3

您违反了类定义的单一定义规则,并且该语言明确禁止这样做。编译器/链接器不需要警告或诊断,并且这种情况肯定不能保证在这种情况下按预期工作。

于 2012-05-20T08:38:41.460 回答
0

我认为您的“额外”问题是主要问题的线索。

如果我理解您的额外问题,那么我认为您不想将它们包装在命名空间中,因为如果您将同一类#include 到多个 .cc 文件中,那么您可能只想使用每种方法的一个副本,即使它们是在类内部定义的。

这(有点)解释了为什么您在主要示例中只获得每个函数的一个版本。我希望链接器只是假设这两个函数是相同的——从相同的#included 源生成。如果链接器能够检测到它们何时不同并发出警告,那就太好了,但我想这很难。

正如 Mark B 所指出的,实际的答案是“不要去那里”。

于 2012-05-20T08:40:02.473 回答
0

除了违反一个定义规则之外,您不会看到编译器因为C++ 中的名称修改而抱怨

编辑:正如 Konrad Rudolph 所指出的:在这种情况下,损坏的名称将是相同的。

于 2012-05-20T08:48:01.033 回答