虚析构函数? vptr? 多态数组? delete 基类指针 崩溃?
四条基本规则:
1、如果基类已经插入了vptr, 则派生类将继承和重用该vptr
2、在遇到通过基类指针或引用调用虚函数的语句时,首先根据指针或引用的静态类型来判断所调函数是否属于该class或者它的某个public 基类,如果属于再进行调用语句的改写:
C++ Code其中p是基类指针,vptr是p指向的对象的隐含指针,而slotNum 就是调用的虚函数指针在vtable 的编号,这个数组元素的索引号在编译时就确定下来,并且不会随着派生层的增加而改变。如果不属于,则直接调用指针或引用的静态类型对应的函数,如果此函数不存在,则编译出错。
3、C++标准规定对对象取地址将始终为对应类型的首地址,这样的话如果试图取基类类型的地址,将取到的则是基类部分的首地址。我们常用的编译器,如vc++、g++等都是用的尾部追加成员的方式实现的继承(前置基类的实现方式),在最好的情况下可以做到指针不偏移;另一些编译器(比如适用于某些嵌入式设备的编译器)是采用后置基类的实现方式,取基类指针一定是偏移的。
4、delete[] 的实现包含指针的算术运算,并且需要依次调用每一个元素的析构函数,然后释放整个数组元素的内存。
如下面的例子:
C++ Code按照上面的规则2,pI->Draw(200); 会编译出错,因为在基类并没有定义Draw(int) 的虚函数,于是查找基类是否定义了Draw(int),还是没有,就出错了,从出错提示也可以看出来:“IRectangle::Draw”: 函数不接受 1 个参数。
此外,上述小例子还隐含另一个知识点,我们把出错的语句屏蔽掉,看输出:
Rectangle::Draw()
~Rectangle()
~IRectangle()
即派生类和基类的析构函数都会被调用,这是因为我们将基类的析构函数声明为虚函数的原因,如果没有这样做的话,只会输出基类的析构函数(这种输出情况通过比对规则2也可以理解,pI 现在虽然指向派生类对象首地址,但执行pI->~IRectangle() 时 发现不是虚函数,故直接调用;在pI 指向派生类首地址的前提下,如果~IRectangle() 是虚函数,那么会找到实际的函数~Rectangle() 执行,而~Rectangle() 会进一步调用~IRectangle()),假如在派生类析构函数内有释放内存资源的操作,那么将造成内存泄漏。更甚者,问题远远没那么简单,我们知道delete pI ; 会先调用析构函数,再释放内存(operator delete),上面的例子因为派生类和基类现在的大小都是4个字节即一个vptr,故不存在释放内存崩溃的情况,即pI 现在就指向派生类对象的首地址。如果大小不一致呢?问题就严重了,直接崩溃,看下面的例子分析。
现在来看下面这个问题:
C++ Code
由于基类的fun不是虚函数,故p->fun() 调用的是Base::fun()(规则2),而且delete p 还会崩溃,为什么呢?因为此时基类是空类1个字节,派生类有虚函数故有vptr 4个字节,基类“继承”的1个字节附在vptr下面,现在的p 实际上是指向了附属1字节,即operator delete(void*) 传递的指针值已经不是new 出来时候的指针值,故造成程序崩溃。 将基类析构函数改成虚函数,fun() 最好也改成虚函数,只要有一个虚函数,基类大小就为一个vptr ,此时基类和派生类大小都是4个字节,p也指向派生类的首地址,问题解决,参考规则3。
最后来看一个所谓的“多态数组” 问题
C++ Code由于sizeB != sizeD,参照规则4,pb[1] 按照B的大小去跨越,指向的根本不是一个真正的B对象,当然也不是一个D对象,因为找到的D[1] 虚函数表位置是错的,故调用析构函数出错。程序在g++ 下是segment fault 的,但在vs 中却可以正确运行,在C++的标准中,这样的用法是undefined 的,只能说每个编译器实现不同,但我们最好不要写出这样的代码,免得庸人自扰。
参考:
《高质量程序设计指南C++/C语言》
http://coolshell.cn/articles/9543.html
http://blog.csdn.net/unituniverse2/article/details/12302139