`
897371388
  • 浏览: 522157 次
文章分类
社区版块
存档分类
最新评论

《Effective C++》item7:为多态基类声明virtual析构函数

 
阅读更多

今天研究了一下虚函数,解决了一直困扰的几个问题,尤其是在前段时间找工作面试的时候说不清的几个问题,早知如此,何必当初哎!


Questions:


(1)为什么要用虚函数?

(2)为什么要定义virtual析构函数?

(3)什么时候该定义virtual析构函数和什么时候不该定义virtual析构函数?


Answers:


(1)为什么要用虚函数?


C++中的虚函数的作用主要是实现了多态的机制。关于多态,简而言之就是用父类类型的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。这种技术可以让父类的指针有“多种形态”,这是一种泛型技术。Java中的多态是通过interface和abstract来实现的,java没有virtual一说!
定义一个函数为虚函数,不代表函数为不被实现的函数。定义他为虚函数是为了允许用基类的指针来调用子类的这个函数。而在Java中,只要子类实现了interface中定义的函数,那么接口声明的引用一定会调用子类的这个函数,如果子类没有定义,则调用接口中的函数!
定义一个函数为纯虚函数,才代表函数没有被实现。定义他是为了实现一个接口,起到一个规范的作用,规范继承这个。类的程序员必须实现这个函数。

先看一个不使用virtual的例子:

该程序先定义了一个基类:TimeKeeper,基类中有一个实现了的函数print,然后定义一个类AtomicClock来继承这个TimeKeeper,TimeKeeper中也定义了一个函数print,在main函数中先定义一个基类指针,让其指向一个通过AtomicKeeper类的成员函数getTimeKeeper返回的实例,然后用这个基类指针调用print,那么到底会调用基类和子类中的哪个print呢?

// VirtualConstructor.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include<iostream>

using namespace std;

class TimeKeeper {
public:
	TimeKeeper() {}
	void print()
	{
		cout<<"call print() of TimeKeeper......"<<endl;
	}
	virtual ~TimeKeeper() {}
	virtual TimeKeeper* getTimeKeeper() = 0;
};

class AtomicClock: public TimeKeeper {
public:
	AtomicClock(){}
	~AtomicClock(){}
	TimeKeeper* getTimeKeeper()
	{
		return new AtomicClock();
	}
	void print()
	{
		cout<<"call print() of AtomicClock......"<<endl;
	}
};

int main(int argc, _TCHAR* argv[])
{
	AtomicClock ato;
	TimeKeeper* ptk = ato.getTimeKeeper();

	ptk->print();

	delete ptk;
	return 0;
}
执行结果可能出乎各位看官的意料,调用的基类TimeKeeper中的print方法!

下面将这个程序修改一下,将基类中的print函数改为virtual函数:

// VirtualConstructor.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include<iostream>

using namespace std;

class TimeKeeper {
public:
	TimeKeeper() {}
	virtual void print()   // virtual函数
	{
		cout<<"call print() of TimeKeeper......"<<endl;
	}
	virtual ~TimeKeeper() {}
	virtual TimeKeeper* getTimeKeeper() = 0;
};

class AtomicClock: public TimeKeeper {
public:
	AtomicClock(){}
	~AtomicClock(){}
	TimeKeeper* getTimeKeeper()
	{
		return new AtomicClock();
	}
	void print()
	{
		cout<<"call print() of AtomicClock......"<<endl;
	}
};

int main(int argc, _TCHAR* argv[])
{
	AtomicClock ato;
	TimeKeeper* ptk = ato.getTimeKeeper();

	ptk->print();

	delete ptk;
	return 0;
}

这次的执行结果就是调用了子类的print函数!

这就是虚函数的神奇功能,也是跟Java的多态机制的不同之处,至于为什么会出现这种效果,那就是涉及到虚函数表的知识了,每个类的实例存放有本类实现了的虚函数列表,当用基类指针指向的时候,就会调用对象列表中的函数!这里不做赘述!


(2)为什么要定义virtual析构函数?


如果你有一个带有虚函数功能的类,则它需要一个虚析构函数,原因如下:
1.如果一个类有虚函数功能,它经常作为一个基类使用,为了能够被子类继承。
2.如果它是一个基类,它的派生类经常使用new来分配。
3.如果一个派生类对象使用new来分配,并且通过一个指向它的基类的指针来控制,那么它经常通过一个指向它的基类的指针来删除它。
C++明白指出,当derived class对象经由一个base class指针被删除,而该base class带着一个non-virtual析构函数,其结果将是不确定的,通常会调用基类的析构函数而导致派生类定义的成员没有被析构,产生内存泄露等问题。

基类有虚析构函数的话,最底层的派生类的析构函数最先被调用,然后各个基类的析构函数被调用。

看如下的例子:

不适用virtual虚构函数:

// VirtualConstructor.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include<iostream>

using namespace std;

class TimeKeeper {
public:
	TimeKeeper()
	{
		cout<<"constructor of TimeKeeper......"<<endl;
	}
	~TimeKeeper()
	{
		cout<<"deconstructor of TimeKeeper......"<<endl;
	}
	virtual TimeKeeper* getTimeKeeper() = 0;
};

class AtomicClock: public TimeKeeper {
public:
	AtomicClock()
	{
		cout<<"constructor of AtomicClock......"<<endl;
	}
	~AtomicClock()
	{
		cout<<"deconstructor of AtomicClock......"<<endl;
	}
	TimeKeeper* getTimeKeeper()
	{
		return new AtomicClock();
	}
};

int main(int argc, _TCHAR* argv[])
{
	AtomicClock ato;
	TimeKeeper* ptk = ato.getTimeKeeper();

	delete ptk;
	return 0;
}

执行的结果是,ptk释放的时候没有只调用了基类TimeKeeper中的析构函数,没有调用子类的析构函数,这样的话子类的“成分”就没有得到释放,就很容易造成内存泄露问题!其实原因跟上面一样,析构函数也是一个成员函数!

使用virtual析构函数的例子:

// VirtualConstructor.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include<iostream>

using namespace std;

class TimeKeeper {
public:
	TimeKeeper()
	{
		cout<<"constructor of TimeKeeper......"<<endl;
	}
	virtual ~TimeKeeper() 
	{
		cout<<"deconstructor of TimeKeeper......"<<endl;
	}
	virtual TimeKeeper* getTimeKeeper() = 0;
};

class AtomicClock: public TimeKeeper {
public:
	AtomicClock()
	{
		cout<<"constructor of AtomicClock......"<<endl;
	}
	~AtomicClock()
	{
		cout<<"deconstructor of AtomicClock......"<<endl;
	}
	TimeKeeper* getTimeKeeper()
	{
		return new AtomicClock();
	}
};

int main(int argc, _TCHAR* argv[])
{
	AtomicClock ato;
	TimeKeeper* ptk = ato.getTimeKeeper();

	delete ptk;
	return 0;
}

这次运行结果会先调用子类AtomicKeeper的析构函数,然后调用基类TimeKeeper的析构函数,实现完成的内存释放操作!这也是利用多态来实现的,要想使用多态,就必须使用virtual析构函数!

实际上,优秀的程序员常常把基类的析构函数定义为虚函数。因为,将基类的析构函数定义为虚函数后,当利用delete删除一个指向派生类定义的对象指针时,系统会调用相应的类的析构函数(包括派生类的和基类的)。而不将析构函数定义为虚函数时,只调用基类的析构函数。

构造函数的调用顺序总是如下:
1.基类构造函数。如果有多个基类,则构造函数的调用顺序是某类在类派生表中出现的顺序,而不是它们在成员初始化表中的顺序。
2.成员类对象构造函数。如果有多个成员类对象则构造函数的调用顺序是对象在类中被声明的顺序,而不是它们出现在成员初始化表中的顺序。
3.派生类构造函数。

析构函数的调用顺序总是如下:
析构函数的调用顺序与构造函数的调用顺序正好相反,将上面3个点反过来用就可以了,首先调用派生类的析构函数;其次再调用成员类对象的析构函数;最后调用基类的析构函数。

析构函数在下边3种情况时被调用:
1.对象生命周期结束,被销毁时(一般类成员的指针变量与引用都i不自动调用析构函数);
2.delete指向对象的指针时,或delete指向对象的基类类型指针,而其基类虚构函数是虚函数时;
3.对象i是对象o的成员,o的析构函数被调用时,对象i的析构函数也被调用。


(3)什么时候该定义virtual析构函数和什么时候不该定义virtual析构函数?


1.多态性质的base class应该声明一个virtual析构函数。如果class带有任何virtual函数,它就应该拥有一个virtual析构函数。因为只有在“多态”的情况下才能通过父类的指针调用实际子类的成员函数。

2.class的设计目的如果不是作为base class使用,或不是为了具备多态性,就不应该声明virtual析构函数。











分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics