当我们进行向下转换时,内部会发生什么?

时间:2014-05-22 12:44:15

标签: c++ casting virtual-functions downcast

我试图理解向下...这是我试过的......

class Shape
{
public:
    Shape() {}
    virtual ~Shape() {}
    virtual void draw(void)     { cout << "Shape: Draw Method" << endl; }
};

class Circle : public Shape
{
public:
    Circle(){}
    ~Circle(){}
    void draw(void)     { cout << "Circle: Draw Method" << endl; }
    void display(void)  { cout << "Circle: Only CIRCLE has this" << endl; }
};

int main(void)
{
    Shape newShape;
    Circle *ptrCircle1 = (Circle *)&newShape;
    ptrCircle1->draw();
    ptrCircle1->display();

    return EXIT_SUCCESS;
}

这里我通过将基类指针转换为派生类来减少向下转换。我理解的是......

Circle* ptrCircle1 -->  +------+ new Shape()
                        |draw()|
                        +------+

基类没有关于派生调用中的display()方法的信息。我原本期待崩溃,但确实将输出打印为

Shape: Draw Method
Circle: Only CIRCLE has this

有人可以解释内部发生的事情。

...谢谢

2 个答案:

答案 0 :(得分:7)

在这种情况下,由于继承关系,C样式转换等同于static_cast。与大多数强制转换一样(dynamic_cast除外,其中注入了一些检查),当你告诉它对象真的是Circle时,编译器会相信你并假设它是。{在这种情况下,行为未定义,因为对象 a Circle,您对编译器撒谎并且所有赌注都已关闭。

这里真正发生的是编译器确定该组合是否存在从基础到派生类型的偏移,并相应地调整指针。此时,您将获得一个指向已调整地址的派生类型的指针,并且窗口中的类型安全性已关闭。通过该指针进行的任何访问都将假定该位置的内存是您告诉它的内容并将其解释为未定义的行为,因为您正在读取内存,就好像它是一种不是它的类型。

指针何时调整?

struct base1 { int x; };
struct base2 { int y; };
struct derived : base1, base2 {};
base2 *p = new derived;

derivedbase1base1::x的地址相同,但与base2base2::y的地址不同。如果您从derived转换为base2,则编译器会在从sizeof(base1)转换为{{1}时调整转换中的指针(将base2添加到地址中) },编译器会调整相反的方向。

为什么会得到你得到的结果?

  

形状:绘制方法

     

圈子:只有CIRCLE有这个

这与编译器如何实现动态调度有关。对于具有至少一个虚函数的每个类型,编译器将生成一个(或多个)虚拟表。虚拟表包含指向类型中每个函数的最终覆盖的指针。每个对象都包含指向完整类型的虚拟表的指针。调用虚函数涉及编译器在表中执行查找并跟随指针。

在这种情况下,对象实际上是derived,vptr将引用Shape的虚拟表。当您从Shape转换为Shape时,您告诉编译器这是Derived(即使它不是)。当你调用Circle时,编译器遵循vptr(在这种情况下,draw()子对象的vptr和Shape子对象碰巧在同一偏移量(大多数ABI中为0)对象的开头。编译器注入的调用遵循Circle vptr(强制执行更改内存的任何内容,vptr仍然是Shape的内容)并点击Shape

Shape::draw的情况下,调用不是通过vptr动态调度的,因为它不是虚函数。这意味着编译器将注入一个直接调用display()传递您拥有的Circle::draw()指针的地址。您可以通过禁用动态分派来模拟虚拟功能:

this

请记住,这只是对逃避C ++标准的编译器详细信息的解释,标准只是 Undefined Behavior ,无论编译器做什么都没问题。不同的编译器可以做一些不同的事情(虽然我看到的所有ABI在这里做的基本相同)。

如果您真的对这些内容的工作原理感兴趣,可以查看Lippman的 Inside the C ++ object model 。这是一本不太老的书,但它解决了编译器必须解决的问题以及编译器使用的一些解决方案。

答案 1 :(得分:2)

由于display()不是虚拟的,因此在大多数c ++实现中调用它不会使用指针值。因此,您通过静态地址呼叫display()。 由于display()不使用this,因此无效。

然而,正如评论所指出的,这仍然是未定义的行为。另一个编译器可能导致崩溃。

您也可以从display()指针调用nullptr,这在大多数实现中都会提供相同的结果。但仍然是未定义的行为。