1、多态时virtual析构函数的必要性
1.1、问题描述
下面的例子,是关于类的继承和动态绑定:
class TimeKeeper{
public:
TimeKeeper();
~TimeKeeper();
//~~~
};
class AtomicClock: public TimeKeeper{/*~~~*/}; //原子钟
class WaterClock: public TimeKeeper{/*~~~*/}; //水钟
class WristClock: public TimeKeeper{/*~~~*/}; //腕表
通过设计factory(工厂)函数,返回一个指针指向一个计时对象,factory函数会返回一个base class的指针,指向新生成的derived class对象:
TimeKeeper* getTimeKeeper();//返回一个指针指向一个TimeKeeper派生类的动态分配对象
TimeKeeper* ptk = getTimeKeeper();
//~~~
delete ptk;
如上面的例子,getTimeKeeper返回的指针指向一个derived class对象,而那个对像却经由一个base calss的指针删除,而base class有一个non-virtual的析构函数,这样会带来一个严重的问题:当derived class 对象经由一个base class指针被删除,而该base class带着一个non-virtual析构函数,实际执行时通常发生的是对象的derived成分没有被销毁。如果getTimeKeeper返回的指针指向一个AtomicClock对象,那么AtomicClock的成分(该类内部的成员变量)很可能没有被销毁,这就造成的局部销毁,造成资源泄漏。
1.2、解决方法
class TimeKeeper{
public:
TimeKeeper();
virtual ~TimeKeeper();
//~~~
};
TimeKeeper* getTimeKeeper();//返回一个指针指向一个TimeKeeper派生类的动态分配对象
TimeKeeper* ptk = getTimeKeeper();
//~~~
delete ptk; //现在行为正确
任何class只要带有virtual函数都几乎确定应该有一个virtual析构函数。
2、不要盲目的令类的析构函数为virtual
class Point{
public:
Point(int xCode, int yCoord);
~Point();
private:
int x, y;
};
如果int占用32bits,那么point可以放入一个64-bits的缓存器中,这个Point对象甚至可被当作一个64-bits的量传递给其他语言如C撰写的函数。
但是当Point的析构函数为virtual,形势就会发生变化:
实现出virtual函数,对象就会携带某些信息,主要是在运行期间决定哪一个virtual函数应该被调用。这个信息通常由vptr(virtual table pointer)指针指出。vptr指向一个由函数指针构成的数组,称为vtbl(virtual table);每一个带有virtual函数的class都有一个相应的vtbl,当对象调用某一个virtual函数,实际被调用的函数取决于该对象的vptr所指向的那个vtbl —- 编译器在其中寻找适当的函数指针。
上面的例子,如果是32-bits计算机体系结构,将原来的占有64bits的空间变为96bits(64+32[vptr]);而在64-bits计算机体系结构,将原来的占有64bits的空间变为128bits(64+64[vptr])。这时,Point对象无法放入一个64-bits的缓存器中,而C++的Point对象也不再和其他语言(如C)内的相同声明有着一样的结构,就无法将其传递给其他语言实现的函数,也就不再具有移植性。
无端的将所有的class的析构函数设置为virtual也是错误的,只有当class内部至少含有一个virtual函数的时候才为其声明为virtual析构函数。
3、类的不可继承性
class SpecialString: public std::string{
//~~~
};
SpecialString* pss = new SpecialString("Impending Doom");
std::string* ps;
//~~~
ps = pss;
delete ps;
//现实中*ps的SpecialString资源会泄漏,因为SpecialString析构函数没有被调用。
基于任何不带virtual的析构函数的class,包括所有的STL容器如vector、list、set、tr1::unorderd_map等等,如果你企图继承一个标准的容器或者任何其他non-virtual析构函数的class,都有可能出现上述的问题。
4、纯虚函数(pure virtual)
纯虚函数导致抽象类(abstract classes),这种类不能被实例话,不能创建这种类的对象,总是企图被当作一个base class来用,但同时base class应该有一个virtual的析构函数,所以最简单的解决方式就是:为希望成为抽象的类声明一个pure virtual的析构函数。
class AWOV{
public:
virtual ~AWOV() = 0;//声明为pure virtual的析构函数
};
这个类有一个pure virtual函数,所以他是一个抽象类,同时又由于它有virtual析构函数,所以无需担心析构函数的问题,但这里有一点就是要为这个pure virtual析构函数提供一份定义:
AWOV::~AWOV(){} //pure virtual析构函数的定义
析构函数的运作方式:最深层派生的class的析构函数最先被调用,然后是其每一个base class的析构函数被调用。编译器会在AWOV的派生类的析构函数中创建一个对~AWOV的调用动作,所以必须为这个函数提供一份定义。
⚠️注意:
并非所有的base class的设计目的是为了多态的用途,如标准的string和STL容器甚至不会作为base class使用。很多的类设计的目的是作为base class但不是用于多态,因此不需要virtual的析构函数。
5、总结
(1)带有多态性的base class应该声明一个virtual析构函数;如果class带有任何的virtual函数,那么这个类应该拥有一个virtual的析构函数。
(2)class的设计目的并不是作为base class使用或者不是为了具备多态性,那就不应该声明为virtual析构函数。