《随笔十》—— C++中的 “ 虚析构函数 ”

 

●  在创建一个派生类对象时我们会首先调用基类的构造函数, 然后调用派生类的构造函数, 一般情况下, 在使用虚函数的时候,我们一般会把基类的指针指向派生类对象, 那么我们删除指向派生类对象的指针时会发生什么情况呢?

下面看一个程序代码示例:

class Base
{
public:
	~Base() 
	{
		cout << "基类中的析构函数被调用!" << endl;
	}
	Base()
	{
		cout << "基类的构造函数被调用!" << endl;
	}
	virtual void func()
	{
		cout << "基类中的func函数被调用!" << endl;
	}
};
class Derived :public Base
{
public:
	Derived()
	{
		cout << "派生类的构造函数被调用!" << endl;
		m_pointer = new int[20];
	}
	~Derived()
	{
		cout << "派生类的析构函数被调用" << endl;
		delete[] m_pointer;
	}
	virtual void func()
	{
		cout << "派生类中的func函数被调用!" << endl;
	}
private:
	int *m_pointer;
};
int main()
{
	{
		//重点在这段代码中
		Base *pp = new Derived;
		pp->func();
		pp->Base::func();
		delete pp;
	}
	{
		cout << endl;
		Derived myDerived;
		Base *ppp = &myDerived;
		ppp->func();
		ppp->Base::func();
		
	}
	{
		cout << endl;
		Derived d;
		Base &b = d;
		b.func();
		b.Base::func();
	}
	system("pause");
	return 0;
}

输出结果为:

基类的构造函数被调用!
派生类的构造函数被调用!
派生类中的func函数被调用!
基类中的func函数被调用!
基类中的析构函数被调用!

基类的构造函数被调用!
派生类的构造函数被调用!
派生类中的func函数被调用!
基类中的func函数被调用!
派生类的析构函数被调用
基类中的析构函数被调用!

基类的构造函数被调用!
派生类的构造函数被调用!
派生类中的func函数被调用!
基类中的func函数被调用!
派生类的析构函数被调用

在该示例中, 基类的析构函数不是虚函数, 从输出结果可以看出, 在主函数中第一个 { } 中,我们通过基类指针删除派生类对象时调用的是基类的析构函数, 派生类的析构函数并没有被执行。 因此派生类对象中动态分配的内存空间没有得到释放, 造成了内存泄漏。

也就是说, 派生类对象成员 m_pointer 所指向的内存空间在对象消失后既不能被本程序继续使用, 也没有被释放。 

对于内存的需求量大、长期连续运行的程序来说, 如果持续发生这样的错误是很危险的, 最终将导致因内存不足而引起的程序异常。

 

那为什么派生类的对象没有被销毁?

 所有问题都源于该对象是在堆上动态创建的, 程序在销毁那两个对象时都调用了错误的析构函数。 

出现这种情况的原因是析构函数的链接是在编译时静态解析的。 对自动创建的对象而言,这样做没有任何问题,编译器知道它们是什么, 也能够安排调用正确的析构函数—— 比如说 上面程序主函数中的 第二个 { } 和 第三个 { } 中的代码,它们都正确的调用了虚函数, 从输出结果中,可以看出, 当该对象销毁时, 基类的析构函数 和 派生类的析构函数都被调用了。

但对于动态创建并通过指针访问的对象来说, 情况就不同了。  当执行delete 操作时, 编译器知道的唯一信息是该指针的类型“ 指向基类的指针”。  编译器不知道该指针实际指向的对象类型, 因为这是程序执行时确定的。 因此, 编译器只能确保使 delete 操作调用基类的析构函数。  在实际的应用程序中,会造成严重的内存泄漏, 解决方式很简单, 只需要在程序执行时动态解析对析构函数的调用。 通过在类中使用虚析构函数,  就可以让编译器使用这样的解析方式。

不过要注意的是:  只要是 在堆上创建派生类对象,并且用基类指针指向它,如果基类中的析构函数不是虚函数, 当我们delete 基类指针时, 只会调用基类的析构函数, 派生类的析构函数不会被调用。 跟 基类中是否有其它虚函数没有关系 。


所以说 虚析构函数是为了解决这样的一个问题: 在堆上创建派生类对象,并且用基类指针指向它, 然后并用基类的指针删除派生类对象, 此时即调用派生类析构函数 和 基类的析构函数。

 

如果某个类不包含虚函数, 那么一般是表示它将不作为一个基类来使用。 当一个类不作为基类使用时, 使析构函数为虚不是一个好主意。 因为它会为类增加一个虚函数表, 使的对象的体积翻倍, 还有可能降低其可移植性。  所以基本的一条原则是:  无故的声明虚析构函数 和永远不去声明一样是错误的。

 


那么只要我们把上面的程序的基类中的析构函数加上 virtual ,那么当 delete 基类指针时。那么系统就会获得对象运行时的类型并调用正确的析构函数。 看下图显示:

从输出结果可以看出, 当我们在基类中的析构函数加上virtual 时,  delete 基类指针时,  首先调用的是派生类的析构函数, 然后基类析构函数。

 

在使用虚析构函数时,要注意以下几点:

只要基类的析构函数被声明为虚函数, 则派生类的析构函数, 无论是否使用virtual 关键字进行声明, 都自动成为虚函数。

一个虚函数无论被继承多少次, 仍然保持其虚函数的特性, 与继承的次数无关。

 

如果基类的析构函数为虚函数, 则当派生类未定义析构函数时, 编译器自动生成的析构函数也为虚函数。

delete 与析构函数一起工作, 当使用delete 删除一个对象或者指针时, 也会默认地调用析构函数。 如果该析构函数是虚函数, 那么这个调用的过程将是动态的。

构造函数不能声明为虚函数, 这是因为在执行构造函数时类对象还未完成建立, 当然谈不上函数与类对象的关联。

建议:

当使用继承时, 总是将基类中的析构函数声明为虚函数是个好主意。 这将使所有派生类的析构函数自动成为虚函数。 这样, 如果程序中 delete 运算符 删除的对象是指向派生类对象的基类指针, 系统就会调用相应类的析构函数;  否则系统只会执行基类的析构函数, 而不执行派生类的析构函数, 从而导致异常。 

所以专业编程人员一般都习惯声明为虚析构函数, 即使基类并不需要使用虚析构函数, 也显示地定义一个函数体为空的虚析构函数, 以保证在撤销动态存储空间时能够得到正确的处理。

 

该文章后续还会更新修改, 先写到这里。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值