永远不要拒绝C++.它会得到.
我习惯于为我所做的一切编写单元测试.作为其中的一部分,我经常喜欢用A和B的名称定义类,在测试的.CXX行使代码,在安全知识,我),因为该代码永远不会成为一个图书馆的一部分,或者被用来测试之外,名称冲突可能非常快,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";
}
Run Code Online (Sandbox Code Playgroud)
和
#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";
}
Run Code Online (Sandbox Code Playgroud)
好的好的.显然我不应该声明两个叫做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
Run Code Online (Sandbox Code Playgroud)
所以有两个独立的A类.在第二次使用中,使用了第一个on的析构函数和构造函数,即使只有第二个在其编译单元中可见.更奇怪的是,如果我在runFirstUnit中取消注释行,而不是调用A ::〜A,则调用A :: getName.显然,在第一次使用时,对象获取第二个定义的vtable(getName是第二个类中的第二个虚函数,析构函数是第一个中的第二个).它甚至可以从第一个开始正确地获取构造函数.
所以我的问题是,为什么链接器没有抱怨多重定义的符号.它似乎选择了第一场比赛.重新排序链接步骤中的对象确认.
Visual Studio中的行为是相同的,所以我猜这是一些标准定义的行为.我的问题是,为什么?很明显,如果给出重复的名称,链接器很容易被barf.如果我添加,
void f() {}
Run Code Online (Sandbox Code Playgroud)
它抱怨的两个文件.为什么不为我的类构造函数和析构函数?
编辑问题不是,"我应该做些什么来避免这种情况",或"行为是如何解释的".它是,"为什么链接器不能抓住它?" 项目可能有数千个编译单元.明智的命名实践并没有真正解决这个问题 - 它们只会让问题变得模糊不清,只有这样才能培养每个人遵循它们.
上面的示例导致模糊行为,编译器工具可以轻松且明确地解决这些行为.那么,他们为什么不呢?这只是一个错误.(我怀疑不是.)
**编辑**请参阅下面的litb答案.我重复回来确保我的理解是正确的:
链接器仅为强引用生成警告.因为我们有共享头,内联函数定义(即在同一位置进行声明和定义,或模板函数)被编译为每个看到它们的TU的多个目标文件.因为没有简单的方法来限制将此代码生成到单个目标文件,所以链接器可以选择许多定义中的一个.因此链接器不会生成错误,这些编译定义的符号在目标文件中被标记为弱引用.
编译器和链接器依赖于两个类完全相同.在你的情况下,它们是不同的,所以奇怪的事情发生.一个定义规则说结果是未定义的行为 - 因此行为根本不需要在编译器之间保持一致..我怀疑在runFirstUnit删除行中,它调用了第一个虚拟表条目(因为在它的翻译单元中,析构函数可能占用第一个条目).
在第二个翻译单元中,此条目恰好指向A::getName,但在第一个翻译单元(执行的位置delete)中,条目指向A::~A.由于这两个名称不同(A::~Avs A::getName),因此不会出现名称冲突(您将为析构函数和代码发出代码getName).但由于它们的类名相同,因此它们的v表会故意发生冲突,因为由于两个类具有相同的名称,链接器会认为它们是相同的类并且假设相同的内容.
请注意,所有成员函数都是在类中定义的,这意味着它们都是内联函数.这些功能可以在程序中多次定义.对于类内定义,基本原理是您可以将相同的类定义包含在其头文件中的不同转换单元中.但是,您的测试函数不是内联函数,因此将其包含在不同的转换单元中将触发链接器错误.
如果你启用名称空间,那么就不会发生冲突,因为::A并且::mySpace::A是不同的类,当然会得到不同的v表.