我们先来看一个例子:设计一个人-类,Human和一些派生类:
class Human{
public:
Human();
~Human();
...
};
class Teacher:public Human{...};//老师
class Student:public Human{...};//学生
class Boss:public Human{...};//老板
有时候比如在一个系统中会出现很多不同的职业,于是我们就把人这个类抽象成一个基类:
Human* gethuman();//返回一个指针,指向Human派生类的动态分配对象。
被返回的对象位于堆内存,因此在程序结束时我们要delete。
Human* pstu=gethuman();
...
delete pstu;//释放
我们想一下,我们这么做,似乎在流程来说好像都是正确的,但即便如此,我们仍然没办法知道程序如何行动。
问题出在哪呢?
当我们使用gethuman返回的指针指向一个派生类对象时,例如(Student),但是那个对象确是由基类指针删除,但是目前的基类指针有一个非虚析构函数。
我们先来捋一捋,在c++中我们是允许使用一个基类指针去指向派生类的对象的,这也是继承的手段。但是我们这里在delete时发生了什么,我们delete的是一个基类指针,那么基类又正好有一个non-virtual析构函数,这时,c++明白指出了,当派生对象经由一个基类指针删除,而该基类带着一个non-virtual析构函数,其结果未有定义——实际执行时通常发生的是对象的派生成分没被销毁。例如gethuman返回的指针指向一个Student对象,其内的Student成分很可能没被销毁,而且Student的析构函数也未能执行。我们只会销毁基类成分。这可是形成资源泄露、败坏数据结构、浪费调试时间的途径。
想要消除这个问题的做法很简单:给基类一个虚析构函数。之后,删除派生类对象时就会像你想的那样,会销毁整个对象,包括所有的派生类成分:
class Human{
public:
Human();
virtual ~Human();
...
};
Human*pstu=gethuman();
...
delete pstu;//行为正确
像Human这样的基类通常还会有其他的virtual函数,因为virtual函数的目的就是允许派生类去覆盖它。我们可以明确,任何class只要带有virtual函数都几乎带有virtual析构函数。
如果class不含virtual函数,通常意味着它并不意图被用作一个基类。当class不企图被当做基类时,我们也不应该让它的析构函数为virtual。为什么呢?
因为如果一个类,如果没有虚函数,那么就意味着没有虚函数表,虚函数表的实质是一个指针,,编译期就是通过对象的虚函数表找到适当的函数指针,而我们知道,指针需要内存空间。如果本不需要这个指针的类,你给加上了虚函数,那么就浪费了内存,而且也降低了移植性,因为其他语言可能没有这样的东西,比如c语言。
因此,无故的将所有的class的析构函数声明为virtual,也是不对的。许多人的心得是:只有当class内至少含有一个virtual函数,才为它声明virtual析构函数。
即使有些类完全不带虚函数,因为c++没有其他语言那样的禁止派生机制,所以还是会有人犯这样的错误:
class Specialstring:public std::string{
...
};
这是一个馊主意,看起来似乎没什么问题,但是当我们这样做时:
Specialstring* pss=new Specialstring("hello");
std::string*ps;
ps==pss;
delete ps;//未有定义
只要不带virtual析构函数的class就不该被继承。
有的时候让class带一个pure virtual析构函数,可能很便利。pure virtual函数导致抽象类——也就是不能被实体化的class,也就是不能创建对象,这种类就是用来被继承的。有些时候,你希望拥有抽象类,但是没有任何的纯虚函数,那该怎么办,由于抽象类总是被当做基类来使用,又因为基类因该有一个虚析构函数,并且纯虚函数会导致类抽象化,简直完美:当你希望它成为抽象的那个类就声明一个pure virtual析构函数:
class A{
public:
virtual ~A()=0;//声明纯虚析构函数
}
这里还有一个小窍门,必须为这个pure virtual析构函数提供一份定义:
A::~A(){}
析构函数的运作方式是,最深层派生的那个class其析构函数最先被调用,然后是其每一个基类的西苟寒食被调用,编译器会在A的派生类的析构函数中创建一个~A的调用动作,所以你必须为这个函数提供一份定义,如果不这样做,连接器就会报错。
注意:
给基类一个virtual析构函数,这个规则只适用于带多态性质的基类身上。这种基类的设计目的是为了用来“通过基类接口处理派生类对象”。
并非所有的基类设计目的都是为了多态用途,例如标准string和STL容器。
总结:
1、带多态性质的基类应该声明一个virtual析构函数。如果class带有任何virtual函数,就应该拥有一个virtual析构函数。
2、class的设计目的如果不是作为基类使用,或不是为了具备多态性,就不该声明virtual析构函数。