您的位置:首页 > 其它

基类的析构函数需不需要写成虚函数

2011-11-05 01:10 190 查看
我之前一直认为,基类的析构函数应当是虚函数,否则会留下析构时的隐患,并且在没有其他虚函数的时候,dynamic_cast将不能工作。

举个例子,如下,基类Base仅仅提供一个唯一的ID来标识一个实例化的对象,它没有其他任何使用虚函数的需求。

typedef long ClassID;
ClassID	gID;

class Base
{
public:
Base() { mClassID = gID++; }
~Base() { printf("Base::~Base()\n"); }

public:
ClassID		getClassID() { return mClassID; }

private:
ClassID		mClassID;
};

class DerivedA : public Base
{
public:
DerivedA() { mX = mY = 0; }
~DerivedA() { printf("DerivedA::~DerivedA()\n"); }

public:
int			getX() { return mX; }
void		setX( int x ) { mX = x; }

int			getY() { return mY; }
void		setY( int y ) { mY = y; }

private:
int			mX;
int			mY;
};

class DerivedAObj : public DerivedA
{
public:
DerivedAObj() { mRaduis = 0; }
~DerivedAObj() { printf("DerivedAObj::~DerivedAObj()\n"); }

public:
int			getRaduis() { return mRaduis; }
void		setRaduis( int raduis ) { mRaduis = raduis; }

private:
int			mRaduis;
};

首先,一个隐患:

DerivedAObj* pAObj = new DerivedAObj;
DerivedA* pA = pAObj;
delete pA;


会输出什么呢?

DerivedA::~DerivedA()

Base::~Base()

而DerivedAObj的析构函数并没有调用。假设该类的析构函数实际上对一些堆分配的内存进行了清理工作,那么这种隐患就会造成内存泄漏。

其次假设这个时候,我有个方法:

void		printRaduis(  DerivedA* pObj )
{
DerivedAObj* pAObj = dynamic_cast< DerivedAObj* >( pObj );

if( pAObj != NULL )
{
printf( "Raduis: %d\n", pAObj->getRaduis() );
}
}


这个时候编译会有错误提示:error C2683: 'dynamic_cast' : 'DerivedA' is not a polymorphic type

这个错误是说,DerivedA不是一个多态类型,无法使用dynamic_cast进行动态转换。我们知道,dynamic_cast的动态转换依靠的是RTTI,该信息是存在于虚表中。但这里继承中没有使用任何虚函数,因此无法在运行时得到类型信息。

那么,是不是一定要给基类,比如Base的析构函数加上virtual呢?

如果Base::~Base()是虚函数的话,那么首先当子类对象的指针转换为基类指针后再删除时,依然会正确的进行析构操作。

比如上面的输出就会变成:

DerivedAObj::~DerivedAObj()

DerivedA::~DerivedA()

Base::~Base()

其次,dynamic_cast也会正确操作。

这似乎都是成为基类一定要使用虚析构函数的理由,但是,同样有一些其他的理由试图说明,没有必要为没有需求的基类设置虚析构函数。

class DerivedB : public Base
{
public:
DerivedB() { }
~DerivedB() { }

//Data block

};

比如Base,它仅仅提供了一个ClassID,如果声明虚析构函数,则标志着所有继承于它的子类都将有着自己的虚表,特别的,继承于它的DerivedB可能仅仅就是一个数据类,但它仍然要建立自己的虚表,这是显然都会降低性能。

而多态的需求,可能并不从继承链的顶端开始,比如对于Base的子类DerivedA,从这一层开始,可能会使用到多态的特性,那么,完全可以从这一节点声明虚析构函数。而不是Base类。因为继承于Base类的其他子类(如DerivedB)完全没有必要消耗性能去建立一张虚表。

既然这样,为了解决正确析构的问题,我们声明DerivedA::~DerivedA()为虚析构函数

virtual ~DerivedA() { printf("DerivedA::~DerivedA()\n"); }



DerivedAObj* pAObj = new DerivedAObj;
DerivedA* pA = pAObj;
delete pA;

会正确输出:

DerivedAObj::~DerivedAObj()

DerivedA::~DerivedA()

Base::~Base()

那么,如果这样使用呢?

DerivedAObj* pAObj = new DerivedAObj;
Base* pB = pAObj;
delete pB;

遗憾的是,这只会输出:

Base::~Base()

为什么?因为~Base()不是虚函数。

纠结么?其实不纠结。如果从性能上考虑,的确不需要Base使用虚析构函数,那么就应该将Base的析构函数定义为private,以防止对Base进行delete操作,如果使用者尝试这样做,编译器将会报错,强制使用者正确的使用对象,防范隐患的产生。

还有问题,dynamic_cast呢?

事实上,dynamic_cast依赖于RTTI,而RTTI并不是在所有的情况下都是开启的,所以本来安全的转换其实并不一定安全。假如你写的是个底层库,你不知道它会用在什么地方,那个地方是否开启或是否支持RTTI,因此,更安全的方法是自己实现一套RTTI的机制。

一般来说,默认开启RTTI,dynamic_cast依赖于虚表中的RTTI信息,如果没有定义虚函数,编译器会报错。并且dynamic_cast在菱形/cross继承等方面还存在着问题,所以说,dynamic_cast是“不一定安全”的转换。

结论:

当然,这是见仁见智的看法,通常情况下,我们还是大量使用到动态转换,但在写一些特定的组件或底层时,应当慎重考虑RTTI的实现问题。

对于基类是否定义虚析构函数的问题,在不关注性能的情况下,定义虚析构函数可以避免delete的隐患。同样,关注性能的情况下,在未定义虚析构函数的层级,就应该注意将析构函数私有化(private),防止造成隐藏的bug。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: