- 问题发现?
在维护一份原有代码时,看到类似下面的代码:(模型简化版)
#include<iostream>
using namespace std;
//定义一个纯虚基类
class Base
{
public:
Base()
{
cout<< "Construct Base!"<< endl;
};
~Base() //NOTE: non-virtual
{
cout<< "Destruct Base!"<< endl;
};
virtual void DoSomething() = 0;
};
//实现派生类
class Derived : public Base
{
public:
Derived()
{
cout<< "Construct Derived!" <<endl;
};
~Derived()
{
cout<< "Destruct Derived!" <<endl;
};
void DoSomething()
{
cout<< "Do whatever in Derived class!" <<endl;
};
};
这个实现有问题吗? 让我们简单运行一下看看:
int main()
{
//case1: 使用基类指针指向派生类对象
Base *pBase = new Derived;
pBase->DoSomething();
delete pBase;
pBase = NULL;
//case2: 使用派生类指针指向派生类对象
Derived* pDerived = new Derived;
pDerived = dynamic_cast<Base*>(pDerived);
pDerived ->DoSomething();
delete pDerived ;
pDerived = NULL;
return 0;
}
下面是运行结果:
结果出现了诡异的“局部销毁”现象,case1中的对象在delete 时没有调用派生类Derived的析构函数,这将很可能造成内存资源泄露。
问题出现在Derived 对象被基类Base 指针删除,而目前的基类Base有个non-virtual 析构函数,这将引起一个灾难性结果,因为C++明确指出:当Derived class对象经由一个Base class 指针被删除,而该Base class 带着non-virtual 析构函数时,其结果是未定义的,实际执行时通常发生对象的Derived成分没有被销毁。
- 怎么解决呢?
给Base class 一个virtual 的析构函数,此后删除Derived class 对象时就按照预期销毁整个对象,包括所有Derived class。可以更改后自行测试一下,此处不再粘贴结果。
像Base class 这样除了析构函数之外通常还有其他virtual函数,此处virtual目的是允许Derived class 的实现得以具体化,在不同的Derived class中有不同的实现代码。任何class 只要带有virtual函数基本都应该明确也应有一个virtual 析构函数。
- 问题延伸
某些情况下,欲使一个类成为抽象类,但刚好又没有任何纯虚函数。怎么办? 实际上,抽象类的产生也可由纯虚析构函数实现。
抽象类是准备被用做基类的,如上面所讲,基类必须要有一个virtual析构函数,在没有纯虚函数会产生抽象类的情况下,可以借助于声明一个纯虚析构函数实现:
class AbstractBase
{
public:
virtual ~AbstractBase () = 0; // 声明一个纯虚析构函数
};
该AbstractBase 类有一个纯虚函数,是抽象类,而且有一个virtual析构函数,因此不会产生析构函数导致对象释放不全的问题。
除此声明之外,因为要实例化其派生类对象,还必须提供纯虚析构函数的定义, 即:
AbstractBase::~AbstractBase () {} // 纯虚析构函数的定义
而且该定义是必需的,因为虚析构函数工作的方式是:最底层的派生类的析构函数最先被调用,然后各个基类的析构函数被调用。也就是说,即使是抽象类,编译器也要产生对~AbstractBase() 的调用,因此必须要保证为它提供实现函数体,否则,编译连接时就会报错,直到实现该函数体为止,示例代码如下:
class AbstractBase
{
public:
virtual ~AbstractBase() = 0;
};
class ConcreteDerived :public AbstractBase
{
public:
virtual ~ConcreteDerived(){};
};
//AbstractBase的纯虚函数实现体,此时AbstractBase仍为抽象类
AbstractBase::~AbstractBase(){}; //必须提供,否则编译报错