题10-析构函数在什么情况下,应为虚函数?
1.1 问题与分析
一个对象指针,如果其声明类型是基类,但实际指向派生类对象,比如:
class Flyable() {};
class Bird : public Flyable {};
Flyable* f = new Bird;
注意指针 f ,它的声明类型是 “Flyable *”,但实际 创建(new) 出来的是 Bird 类型 —— 这种情况下,delete f
会发生什么?
delete f; // 发生什么?
答:f 要被释放。释放又最少可分为两大步骤:首先是调用析构函数,然后才是收回对象自身所占用的内存。 重点 来了:在本例中,第一步所调用的析构函数,是基类Flyable的析构,而不是派生类 Bird 的析构函数。
说得更直一点:一个实质的派生类的对象,“死”的时候却只调用了基类的析构函数——没有多态。
正确的情况应该怎样?答:正确的情况应该是,释放派生类对象时(哪怕它的声明类型的是基类),也应该先调用派生类自己的析构函数,然后再调用直接基类的析构。如果直接基类还有基类,那就一层层往上走。
我们给个更完整的例子,请大家对比。参与对比的两个指针是:
- pa ,声明类型和实际指向对象类型一致,都是 Bird;
- pb ,声明类型是 Flyable,实际指向对象类型 是 Bird。
#include <iostream>
using namespace std;
class Flyable
{
public:
~Flyable() { cout << "~Flyable\n"; }
};
class Bird : public Flyable
{
public:
~Bird() { cout << "~Bird\n"; }
};
int main()
{
Bird* pa = new Bird; // 名实一致
Flyable* pb = new Bird; // 名实不一
delete pb;
cout << "---------------------\n";
delete pa;
}
在线运行: 例1-非虚析构情况下,名实一致 vs. 名实不一
运行输出:
~Flyable --------------------- ~Bird ~Flyable
横线上孤零零的 ~Flyable
是 delete pb
的输出,虽然,pb 实际是一只鸟,但它却死得像一个 “Flyable”。
横线下两行,先是派生类的析构:~Bird
,然后是基类的析构: ~Flyable
。这才是一个派生类对象正常的死法,也是析构函数特殊的地方:从当前类的析构开始,然后会自动调用直接基类的析构,如果基类还有基类,就一层一层地往上调基类的析构。
1.2 解决之道
我们希望在 delete pb
时,能够调用 pb 的实质类型 (派生类)的 析构函数;说得简单点,就是(不管声明类型是什么),基类有基数的 “死法”,不同的派生类又有不同“死法”。各种 “死法” 千姿百态,这不就是 “虚函数” 应该派上场的时候吗?
这次,我们把 Flyable 中的析构函数,声明为虚的;然后在派生类 Bird 中,使用 “override” 关键字(见上一节 “override 的作用”),明确表明要覆盖(override)基类的原有“死法”,给出自己的“死法”:
#include <iostream>
using namespace std;
class Flyable
{
public:
virtual /*虚的哦*/ ~Flyable() { cout << "~Flyable\n"; }
};
class Bird : public Flyable
{
public:
~Bird() override /* 好习惯 */ { cout << "~Bird\n"; }
};
int main()
{
Bird* pa = new Bird; // 名实一致
Flyable* pb = new Bird; // 名实不一
delete pb;
cout << "---------------------\n";
delete pa;
}
在线运行: 例2:虚析构情况下, 名实一致 vs 名实不一
运行输出:
~Bird ~Flyable --------------------- ~Bird ~Flyable
完美,在 “名实不一”的情况下,析构也实现了:先调派生类,再自动调基类析构。
1.3 内存泄漏实例
我们给的例子,创建对象时,对象内部都没有做什么实际操作。假设我们希望 Bird 在构建时,需要准备好个鸟巢,为了简化实现,我们就假设需要申请 100个字节的 byte (unsigned char) 内存:
class Bird : public Flyable
{
unsigned char * nest; // 鸟巢
public:
Bird()
{
nest = new unsigned char [100]; // 建筑鸟巢需要的100个字节
}
~Bird()
{
cout << "~Bird\n";
delete [] nest; // 释放鸟巢占用的内存
}
};
这情况下,例1中 delete pb;
(名实不一) 时,尽管派生类的析构很自觉地释放了 nest,可是,挡不住此时派生类的析构函数根本没有被调用啊……所以,100字节就此泄漏。
请结合上面带鸟巢的例子,动手写出正确代码的完整实现吧。