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*。