当我们进行下行广播时,内部会发生什么

What happens internally when we do downcasting?

本文关键字:内部 什么 我们 广播      更新时间:2023-10-16

我试着理解向下投射。。。以下是我尝试过的。。。

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

有人能解释一下内部发生了什么吗。

谢谢。。。

在这种情况下,由于继承关系,C样式转换等效于static_cast。与大多数类型转换一样(dynamic_cast除外,在CCD_3中注入了一些检查(,当你告诉它对象实际上是Circle时,编译器会信任你并认为它是。在这种情况下,行为是未定义,因为对象是而不是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)添加到地址(,当从base2强制转换为CCD _15时,编译器会朝相反的方向调整。

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

形状:绘制方法

Circle:只有Circle有这个

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

在这种情况下,对象实际上是Shape,vptr将引用Shape的虚拟表。当您从Shape强制转换为Derived时,您会告诉编译器这是一个Circle(即使不是(。当您调用draw()时,编译器会跟随vptr(在这种情况下,Shape子对象和Circle子对象的vptr恰好位于距离对象开头相同的偏移量(在大多数ABI中为0(。编译器注入的调用遵循Shape vptr(强制转换不会更改内存的任何内容,vptr仍然是Shape的vptr(,然后命中Shape::draw

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

ptrCircle1->Circle::draw();

请记住,这只是对编译器细节的解释,这些细节脱离了C++标准,根据标准,这只是未定义的行为,编译器所做的一切都很好。不同的编译器可以做一些不同的事情(尽管我在这里看到的所有ABI基本上都是一样的(。

如果你真的对这些东西是如何工作的细节感兴趣,你可以看看Lippman的《C++对象模型内部》。这是一本古老的书,但它解决了编译器必须解决的问题以及编译器使用的一些解决方案。

由于display()不是虚拟的,因此在大多数c++实现中调用它不使用指针值。因此,您正在通过其静态地址调用display()。由于display((不使用this,所以它可以工作。

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

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