我有以下小程序:
#include <iostream>
#include <map>
using namespace std;
class A {
public:
virtual void hello(int i)
{
cout << "A Hello " << i << endl;
};
};
class B {
public:
virtual void nothing() = 0;
};
class C : public A, public B {
public:
virtual void hello(int i) override
{
cout << "C Hello " << i << endl;
};
virtual void nothing() override
{
cout << "C Nothing " << endl;
}
};
int main() {
map<int, B*> map_;
A* testA = new C();
map_[0] = (B*)testA;
B* myB = static_cast<B*>(map_[0]);
myB->nothing();
C* testC = new C();
map_[1] = (B*)testC;
myB = static_cast<B*>(map_[1]);
myB->nothing();
return 0;
}
作为输出,我期待以下内容:
C Nothing
C Nothing
但这是我得到的:
C Hello 0
C Nothing
因此调用了错误的函数:即使在代码中从未调用过hello(int i),也会调用它。我知道它与演员有关,但我无法理解错误在哪里。
为什么要调用hello(int i)?
答案 0 :(得分:7)
基类指针之间的转换(有时称为交叉投射)需要dynamic_cast
。给定A
指针,无法(静态地)知道它是C
对象的一部分,因此无法静态找到相应的B
子对象。
你称之为“常规”演员是最危险的演员类型。你永远不应该使用那种样式的强制转换(至少不要转换指针或引用)。除了dynamic_cast
之外,它将使用任何强制转换强制进行转换。所以在这种情况下,它等同于reinterpret_cast
,假装在与B
对象相同的地址处有一个A
对象。由于那里没有B
(它位于C
内的其他地方),因此您会得到未定义的行为。
(特定的风味或未定义的行为可能是它使用nothing
中包含B
的虚拟表条目,但实际上包含hello
,因此最终调用,有一些未定义的值作为参数。但当然,未定义,任何事情都可能在原则上发生。)
答案 1 :(得分:1)
不要使用C样式转换,尤其是在使用继承层次结构时。在您的情况下,您应该使用(B*)testA
,而不是dynamic_cast<B*>(testA)
,然后检查它是否返回NULL
。这是必需的,因为C ++编译器不知道testA
指向的对象类型是否继承自B
,如果是,则如何调整指针值以反映该值。
所以你遇到的是一个未定义的行为,可能会发生这种行为,因为C ++实现使用错误的vtable,或错误的偏移到vtable。
答案 2 :(得分:0)
我对这里发生的事情的猜测:通过将指向A
对象的C
指针转换为B
指针,您正在访问A
的虚拟表而不是B
秒。
多重继承的常用内存布局是
+---+ +-------+ +---------------------+
| C | = | A | B | = | A vtable | B vtable |
+---+ +-------+ +----------+----------+
由于您的对象没有任何数据字段,但虚拟功能只包含虚拟表(+可能是一些填充)。
指针A* testA
指向C
对象内存的开头,因为这也是A
类型子对象的开头。如果您使用B
初始化了new C
指针,那么它将指向B
类型对象的开头。这是在第二部分中将C
指针转换为B
指针的情况,这是一个有效的操作。
但是,在另一部分中,您试图将A
指针强制转换为B
指针reinterpret_cast
。指针仍将指向A
子对象使用的内存。
现在当你尝试调用虚函数nothing
时,查找vtable中的第一个虚函数,其中就是这个(因为你实际上正在查看A
对象案例点的vtable改为hello
。此函数现在不带参数调用(这里发生的事情完全取决于操作系统使用的调用约定,但由于hello
只有int
参数I&#39 ; d猜测这是在一个处理器寄存器而不是堆栈中查找的。 p>
答案 3 :(得分:0)
你的行
map_[0] = (B*)testA;
具有未指定的行为,因此未定义强制转换的结果。这是因为类A
和B
不相关,所以你可以得到它们之间唯一的指针转换是盲目地将指向一个类的对象的指针解释为指向另一个类的对象的指针,这是未定义的行为。
您需要帮助编译器稍微理解您的需求:
map_[0] = static_cast<C*>testA;
然后您将指向编译器您的对象是C
,它可以隐式转换为B
而没有未定义的行为。你可以把它写成(C*)testA
,但是应该避免使用C ++中的C风格的强制转换,因为它们具有惊人的性质 - 正如你所看到的那样。