永远不要拒绝C ++。它会得到。
我习惯为我所做的一切编写单元测试。作为其中的一部分,我经常定义类似A和B的类,在测试的.cxx中运行代码,安全知道i)因为此代码永远不会成为库的一部分或在测试之外使用,名称冲突可能非常快,ii)可能发生的最糟糕的事情是链接器会抱怨多次定义的A :: A()或者我会解决这个问题。我错了。
以下是两个编译单元:
#include <iostream>
using namespace std;
// Fwd decl.
void runSecondUnit();
class A {
public:
A() : version( 1 ) {
cerr << this << " A::A() --- 1\n";
}
virtual ~A() {
cerr << this << " A::~A() --- 1\n";
}
int version; };
void runFirstUnit() {
A a;
// Reports 1, correctly.
cerr << " a.version = " << a.version << endl;
// If you uncomment these, you will call
// secondCompileUnit: A::getName() instead of A::~A !
//A* a2 = new A;
//delete a2;
}
int main( int argc, char** argv ) {
cerr << "firstUnit BEGIN\n";
runFirstUnit();
cerr << "firstUnit END\n";
cerr << "secondUnit BEGIN\n";
runSecondUnit();
cerr << "secondUnit END\n";
}
和
#include <iostream>
using namespace std;
void runSecondUnit();
// Uncomment to fix all the errors:
//#define USE_NAMESPACE
#if defined( USE_NAMESPACE )
namespace mySpace
{
#endif
class A {
public:
A() : version( 2 ) {
cerr << this << " A::A() --- 2\n";
}
virtual const char* getName() const {
cerr << this << " A::getName() --- 2\n"; return "A";
}
virtual ~A() {
cerr << this << " A::~A() --- 2\n";
}
int version;
};
#if defined(USE_NAMESPACE )
} // mySpace
using namespace mySpace;
#endif
void runSecondUnit() {
A a;
// Reports 1. Not 2 as above!
cerr << " a.version = " << a.version << endl;
cerr << " a.getName()=='" << a.getName() << "'\n";
}
好的,好的。显然我不应该声明两个叫做A的类。我的不好。但是我打赌你不能猜到接下来会发生什么......
我编译了每个单元,并链接了两个目标文件(成功)并运行。嗯...
这是输出(g ++ 4.3.3):
firstUnit BEGIN
0x7fff0a318300 A::A() --- 1
a.version = 1
0x7fff0a318300 A::~A() --- 1
firstUnit END
secondUnit BEGIN
0x7fff0a318300 A::A() --- 1
a.version = 1
0x7fff0a318300 A::getName() --- 2
a.getName()=='A'
0x7fff0a318300 A::~A() --- 1
secondUnit END
所以有两个单独的A类。在第二次使用中,使用了第一个on的析构函数和构造函数,即使只有第二个在其编译单元中可见。更奇怪的是,如果我在runFirstUnit中取消注释行,而不是调用A ::〜A,则调用A :: getName。显然,在第一次使用时,对象获取第二个定义的vtable(getName是第二个类中的第二个虚函数,析构函数是第一个中的第二个)。它甚至可以从第一个开始正确地获取构造函数。
所以我的问题是,为什么链接器没有抱怨多重定义的符号。 它似乎选择了第一场比赛。重新排序链接步骤中的对象确认。
Visual Studio中的行为是相同的,所以我猜这是一些标准定义的行为。我的问题是,为什么?很明显,如果给出重复的名称,链接器很容易被barf。 如果我添加,
void f() {}
它抱怨的两个文件。为什么不为我的类构造函数和析构函数?
编辑问题不在于“我应该做些什么来避免这种情况”,或“行为是如何解释的”。它是,“为什么链接器不能抓住它?”项目可能有数千个编译单元。明智的命名实践并没有真正解决这个问题 - 它们只会让问题变得模糊不清,只有这样才能培养每个人跟随它们。
上述示例导致模糊行为,编译器工具可以轻松且明确地解决这些问题。那么,他们为什么不呢?这只是一个错误。 (我怀疑不是。)
**编辑**请参阅下面的litb答案。我重复回来确保我的理解是正确的:
链接器仅为强引用生成警告。 因为我们有共享头文件,所以内联函数定义(即在相同位置进行声明和定义,或模板函数)被编译为每个看到它们的TU的多个目标文件。因为没有简单的方法来限制将此代码生成到单个目标文件,所以链接器可以选择许多定义中的一个。因此链接器不会生成错误,这些编译定义的符号在目标文件中被标记为弱引用。
答案 0 :(得分:6)
编译器和链接器依赖于两个类完全相同。在你的情况下,他们是不同的,所以奇怪的事情发生。一个定义规则说结果是未定义的行为 - 因此行为根本不需要在编译器之间保持一致。 。我怀疑在runFirstUnit
中,在删除行中,它会调用第一个虚拟表条目(因为在其转换单元中,析构函数可能占用第一个条目)。
在第二个翻译单元中,此条目恰好指向A::getName
,但在第一个翻译单元(执行delete
的位置)中,条目指向A::~A
。由于这两个名称不同(A::~A
vs A::getName
),因此您不会获得名称冲突(您将为析构函数和getName
发出代码)。但由于它们的类名是相同的,它们的v-table 将故意发生冲突,因为由于两个类具有相同的名称,链接器会认为它们 是同一个类并假设相同的内容。
请注意,所有成员函数都是在类中定义的,这意味着它们都是内联函数。这些功能可以在程序中多次定义。对于类内定义,基本原理是您可以将相同的类定义包含在其头文件中的不同转换单元中。但是,您的测试函数不是内联函数,因此将其包含在不同的转换单元中将触发链接器错误。
如果你启用名称空间,那么就不会发生冲突,因为::A
和::mySpace::A
是不同的类,当然会得到不同的v表。
答案 1 :(得分:3)
将每个类限制为当前转换单元的一种简单方法是将其封装在匿名命名空间中:
// a.cpp
namespace {
class A {
// ...
};
}
// b.cpp
namespace {
class A {
// ...
};
}
完全合法。因为这两个类位于不同的翻译单元中,并且位于匿名名称空间内,所以它们不会发生冲突。
答案 2 :(得分:0)
函数定义为内联。内联函数可以在程序中多次定义。请参见摘要中的第3点:
http://en.wikipedia.org/wiki/One_Definition_Rule
重点是:
对于给定的实体,每个定义必须相同。
尽量不要将函数定义为内联函数。链接器应该开始给出重复的符号错误。