1. 为作为基类的类声明virtual析构函数:
在C++中,当子类对象由一个基类指针来删除,而该基类带有非虚析构函数,实际执行过程中通常子类对象的Derived成分没有被销毁。可以通过以下代码分析,在代码中,基类的析构函数为非虚函数,main函数中通过基类指针指向子类对象,并通过基类指针来删除子类对象:
class Base
{
public:
Base() { cout << "base 构造" << endl; }
~Base(){cout<<"base 析构"<<endl;} //基类析构函数为非虚函数
virtual void fun() { cout << "base fun" << endl; }
private:
};
class Derived :public Base
{
public:
Derived() { cout << "drived 构造" << endl; }
~Derived() { cout << "drived 析构" << endl; }
void fun() { cout << "drived fun" << endl; }
};
int main()
{
Base *pd = new Derived(); //基类指针指向子类对象
delete pd; //通过基类指针删除对象
}
代码的运行结果如下:
从运行结果可看出,通过基类指针删除子类对象时,仅调用了基类的析构函数,而导致子类的Derived成分没有销毁,产生了一个“局部销毁”对象,这将导致资源泄漏、破坏数据结构、在调试器上浪费时间的严重后果。
我们将基类的析构函数改为虚函数,又会有什么样的结果呢?
修改基类析构函数为虚函数,执行的结果如下:
可见将基类的析构函数声明为virtual后,子类对象的Derived成分也得到了销毁,避免了上面提到的“局部销毁”带来的严重后果。
2. 是否所有class的析构函数都要声明为virtual?
当class不企图作为基类,将其析构函数声明为virtual是不合适的:
那么不合适的原因在哪里,通过粗略分析虚函数的实现来说明:
想实现virtual函数,对象必须携带某些信息,来在运行期决定调用哪一个virtual函数,这个信息由vptr(virtual table pointer)指针指出。vptr指向一个由函数指针构成的数组,称为vptl(virtual table)。每个带有virtual函数的class都有一个对应的vptl。当对象调用某一个virtual函数时,实际被调用的函数取决于该对象的vptr指向的那个vptl,编译器在虚表中寻找适当的函数指针。
我们的问题里,并不十分关心虚函数的具体实现细节是什么,只要知道为了实现虚函数,我们必须携带额外的指针,这将导致对象的体积增加。因此,无端将一个类的析构函数声明为virtual也是不可取的。根据大多数开发人员的经验:只有当class中至少含有一个virtual函数时,才把该类的析构函数声明为virtual。
根据上述经验,那么,类中不含virtual函数时,析构函数就一定是non-virtual函数吗?
3. 类中不含virtual函数时,析构函数一定是non-virtual吗?
class SpecialString : public std::string {
//string不含任何virtual函数,std::string有non-virtual析构函数
};
int main()
{
SpecialString* ps = new SpecialString("Hello world");
std::string* p;
p = ps; //基类String指针指向子类SpecialString的对象
delete p; //通过基类指针删除子类对象,SpecialString的析构函数未被调用
}
上述代码中,子类SpecialString继承了拥有non-virtual析构函数的string基类,导致子类SpecialString的析构函数未被调用,造成资源泄漏的恶果。同样的,对于STL中的容器vector,list,set,tr1::unordered_map等容器的继承,以及对含有non-virtual析构函数的基类的继承(这些类设计为基类,但并不服务于多态性质的实现),都会导致上述的恶果。
因此,我们要避免将含有non-virtual析构函数的类作为基类来继承。
4. 利用pure virtual析构函数获得抽象类
设想这样的场景,想得到了一不能实例化对象的抽象类,而尴尬的是类中没有一个函数适合作为virtual,于是,析构函数将成为实现目标的利器。我们将析构函数设为pure virtual,达到了不能实例化对象目标;同时,作为基类拥有virtual析构函数,满足了我们上述的要求,很完美!
而这完美的设计方案中有一个重要点值得注意,我们必须为基类的pure virual析构函数提供定义:
析构函数的运作方式是:最深层派生的那个子类的析构函数先被调用,然后是其每一个基类的析构函数被调用。编译器在子类析构函数中会创建对基类析构函数的调用动作,所以必须要为这个析构函数提供定义。
有关构造函数和析构函数的运作方式可以通过以下代码运行得到:
class Base1 {
public:
Base1() { cout << "base1 构造" << endl; }
virtual ~Base1() = 0 { cout << "base1 析构" << endl; } //基类析构函数为非虚函数
};
class Base2 : public Base1
{
public:
Base2() { cout << "base2 构造" << endl; }
virtual ~Base2() = 0 { cout << "base2 析构" << endl; } //基类析构函数为非虚函数
//virtual ~Base() = 0;
private:
};
class Derived :public Base2
{
public:
Derived() { cout << "drived 构造" << endl; }
~Derived() { cout << "drived 析构" << endl; }
};
int main()
{
Base2 *pd = new Derived(); //基类指针指向子类对象
delete pd; //通过基类指针删除对象
}
执行上述代码,可得到程序的最终运行结果为:
根据代码的实际执行结果可清楚地看到构造函数和析构函数的运作方式。
在代码中注释了不提供基类析构函数的代码,当不定义基类析构函数的定义时,程序运行报错。
5.总结:
- classes的设计目的如果不是作为base classes使用,或者不是为了具备多态性,就不该声明virtual析构函数。
- 带有多态性质的base classes应该声明一个virtual析构函数。
- class带有任何的virtual函数,就应该拥有virtual析构函数。