VC++中多重继承的指针转换

C++是支持多重继承的。不是像其他一些面向对象语言那样是“一个类实现多个接口”,而是真的多重继承。C++又是能从源代码生成机器码的语言,这种情况下,就要使得 在某个类的指针上调用对象的方法或访问成员变量(不管这个指针指向的就是这个类或者是这个类的子类)都能用同样的机器码实现,就会出现同一个对象,在用不同类型的指针表示它的时候,指针变量保存的地址会不同。它是把一个这样的对象分成几个部分,每个部分都是一个完整的父类型代表的对象,这样当父类型指向对应部分的时候,就能够完全像对待父类型的对象那样对待它。比如有类A和B,然后C同时继承了A和B,那么在C对象的内存空间里,就会有一块可以用A*指,还有另一块可以用B*指。

因为在调用delete的时候,它会先调用对象的析构函数,然后释放对象所占的内存。在这种情况下我想到,按照堆的特性,从堆申请的一块空间在释放的时候,要把这个空间的首地址送进去,而不是送入这块空间中的其他的什么地址。但是如果一个类X继承了B1和B2(B1在前B2在后),那么用X*指向这个对象和用B2*指向这个对象,指针保存的地址是不同的。此时如果对着B2*来delete,那么会发生什么样的事情……于是有了下面这个简单的代码进行试验。

struct B1 { int a; };
struct B2 { int b; };
struct X : public B1, public B2 { int c; };

int main()
{
    X* x = new X();
    B2* b2 = x;
    delete b2;
}

转到反汇编之后,可以看到它就是把b2的地址送到delete里面去调用,果然崩掉了。

这段代码从字面上来一眼似乎看不出什么问题,仔细考虑以后想到,以前学C++的时候,有这么个说法,一个类如果要用来做基类,那么应该要有虚拟析构函数。于是就又有了下面这样的试验代码。

struct B1 { virtual ~B1(){} int a; };
struct B2 { virtual ~B2(){} int b; };
struct X : public B1, public B2 { int c; };

int main()
{
    X* x = new X();
    B2* b2 = x;
    delete b2;
}

加了虚拟析构函数之后,程序果然运行起来就不崩了。查看反汇编,会发现在delete b2的时候,它直接调用虚函数表里的B2析构函数的指针(在X对象里,B2虚表里的析构函数是X对象的),而这个函数里面的内容是把指针从B2偏移回来,变成X*类型,然后执行X的析构函数,析构函数的最后自带delete的调用,于是这样释放起来就没有问题了。

虚表里的析构函数不太一样,虚表里保存的析构函数指针的是执行了原来的析构函数之后还delete的。如果对象是栈上创建的,那么就是只调用原来的析构函数)。

其实这都很早以前做的实验了。然而为什么会突然想到这个问题呢?今天上班的时候,同事在试验这个东西,就是在对象里面把this指针打印出来,看看它到底是什么样的。比如下面这样的代码

#include <stdio.h>

struct B1 {
    virtual ~B1(){}
    int a;
};

struct B2 {
    virtual ~B2(){} 
    virtual void F() { printf("%p\n", this); }
    int b; 
};

struct X : public B1, public B2 {
    void F() override { printf("%p\n", this); }
    int c; 
};

int main()
{
    X* x = new X();
    B2* b2 = x;
    x->F();
    b2->F();
    delete b2;
}

只是在刚才实验的代码上做了少量修改。根据前文的结论,x和b2虽然指向的是同一个对象,但是里面存的地址是不同的。也就是说,在下面调用F的时候,送入的this指针参数是不同的(后面证明这个想法是错的)。但是运行的时候,这两条输出确实输出了同样的地址。这是怎么做到的呢?

这种调整不能是在b2->F()的时候做的。因为在编译期的时候,不知道这个b2指向的是什么对象,可能是X可能是B2,不能随便给指针做偏移、转类型。这样可以得出,那么也就是这个F调用的时候必须要能接受一个类型为B2的指针;但是前面x->F()调用的时候,送进去的是类型为X的指针(看了反汇编,这个说法是错的),但不管是B2*上调用还是X*上调用,F是同一个函数。这里面的矛盾,最直白的方法还是看反汇编了。

结果看了反汇编,才注意到刚才思考的时候漏掉的东西:虽说在执行b2->F()的时候不能给this指针做类型转换,因为不知道它真实的类型;但是对于X类型来说,它却可以不管三七二十一就让F执行的时候接受的this参数是B2类型的:因为能看到X类型的定义的地方,都知道这个F是从B2来的,又因为能调用F的地方,都是能看到X定义的地方,所以在调用F的地方是一定可以把X*转成B2*的。然后真正的B2对象的虚表里是不可能有X::F函数的,所以在这个函数里,就可以把接收到的this参数,从B2*转回X*。

这种实现方式,外面用X类型来调用F的时候,因为知道F是从B2来的,可以把this指针转成B2*再送入;外面用B2类型调用F的时候,因为F就是B2里声明的,那么直接让this类型为B*送入就成。因此,就可以不管调用者是通过X类型调用的F还是通过B2类型调用的F,执行出一样的结果。

所以前面想当然认为“送入的this指针是不同的”是不正确的。送进去的this指针,还真就是相同的,类型都是B2*。

发表评论