2.为什么析构函数一般写成虚函数

2.为什么析构函数一般写成虚函数

为了避免在基类指针指向派生类对象时,只调用基类析构函数、不调用子类析构函数,造成内存泄漏。

1. 不写虚析构会发生什么?

class Base {
public:
    ~Base() { cout << "~Base()"; }
};

class Derived : public Base {
public:
    ~Derived() { cout << "~Derived()"; }
};

// 用基类指针指向子类对象
Base* p = new Derived();
delete p;

结果:

只调用~Base (),不调用~Derived ()

子类资源泄漏。

原因:

  • delete p 看指针静态类型是 Base
  • 析构不是虚函数,静态绑定,直接调用基类析构
  • 子类析构完全没执行

2. 写成虚析构就正常了

class Base {
public:
    virtual ~Base() { ... }
};

结果:

先调用~Derived (),再调用~Base ()

完整析构,不会泄漏。

原因:

  • 虚析构是动态绑定
  • 通过虚表找到实际对象类型(Derived)
  • 完整调用子类 + 基类析构

3. 什么时候必须写?

只要满足:

基类指针可能指向派生类对象,并且会通过 delete 基类指针释放

就必须把基类析构函数声明为 virtual

4. 什么时候不需要?

  • 不做继承
  • 不会用基类指针指向子类
  • 纯接口类但不用 delete 释放

最终背诵版

基类指针指向派生类对象并 delete 时,如果析构不是虚函数,只会调用基类析构,造成子类资源泄漏。

将基类析构设为虚函数,可实现动态绑定,保证子类和基类析构函数都被正确调用,避免内存泄漏。

在C++实现多态里,有一个关于 析构函数的重写问题:基类中的析构函数如果是虚函数,那么派生类的析构函数就重写了基类的析构函数。这里他们的函数名不相同,看起来违背了重写的规则,但实际上编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。那么为什么要把基类中的析构函数写成虚函数呢?

当使用多态特性,让基类指针指向派生类对象时,如果析构函数不是虚函数,通过基类指针销毁派生类对象时,会调用静态绑定的析构函数,也就是基类的析构函数,从而只能销毁属于基类的元素,导致派生类析构不完全,程序就会出现资源泄露或未定义行为。

当派生类中不存在使用动态资源或其他自定义析构行为时,可以不写为虚析构函数,来提高程序效率。但为了程序的可扩展性和健壮性,在使用多态特性时,一般都建议将基类的析构函数定义为虚函数。

在 C++ 中,只需要在基类中定义虚析构函数,派生类会自动继承这个虚属性。也就是说,如果基类的析构函数被声明为虚函数,那么所有派生类的析构函数都将自动成为虚函数。

如果你在派生类中显式地声明析构函数,无论你是否将其声明为虚函数,它都将是虚函数。因此,在派生类中声明虚析构函数并非必要,但是为了代码的清晰性,很多开发者仍然会在派生类中显式地声明虚析构函数。

这样的话,当我们看到派生类的代码时,我们就能立刻知道其析构函数是虚函数,这使得代码更易于理解。这也是一种被称为 "programming by contract" 的编程习惯,意味着类的设计者通过接口明确地表明了类的使用方式。

总结来说,基类的析构函数声明为虚函数后,派生类的析构函数自动成为虚函数,不论是否显式声明为虚函数。但为了代码清晰,有时候我们仍然会在派生类中显式地声明虚析构函数。

举个例子:

  • 当基类析构函数不是虚函数时:
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<string>
using namespace std;

class A 
{
public:
	A() { cout << "A的构造" << endl; }
	~A() { cout << "A的析构" << endl; }
	void Work() 
	{
		cout << "A工作" << endl;
	}
};//基类

class B :public A
{
public:
	B() { cout << "B的构造" << endl; }
	~B() { cout << "B的析构" << endl; }
	void Work() { cout << "B工作" << endl; }
}; //派生类

int main()
{
	A* p = new B;  //派生类对象赋给基类指针
	p->Work();//此时调用的是基类的成员函数,因为基类的成员函数覆盖了派生类的同名成员函数
	delete p;

	system("pause");
	return EXIT_SUCCESS;
}

输出:

A的构造
B的构造
A工作
A的析构
请按任意键继续. . .

可以看到在delete p的时候只调用了基类A的析构函数,并没有调用派生类B的析构函数,导致内存释放并不完全,出现内存泄漏的问题。

  • 然后将基类析构函数写为虚函数时
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<string>
using namespace std;

class A
{
public:
	A() { cout << "A的构造" << endl; }
	virtual ~A() { cout << "A的析构" << endl; }
	void Work()
	{
		cout << "A工作" << endl;
	}
};//基类

class B :public A 
{
public:
	B() { cout << "B的构造" << endl; }
	~B() { cout << "B的析构" << endl; } //在派生类中重写的成员函数可以不加virtual关键字
	void Work() { cout << "B工作" << endl; }
};//派生类

int main()
{
	A* p = new B;  //派生类对象赋给基类指针
	p->Work();//此时调用的是基类的成员函数,因为基类的成员函数覆盖了派生类的同名成员函数
	delete p;

	system("pause");
	return EXIT_SUCCESS;
}

输出:

A的构造
B的构造
A工作
B的析构
A的析构
请按任意键继续.

可以看到这次在delete p的时候调用了派生类的析构函数,因为在调用派生类的析构函数后会自动调用基类的析构函数,这样整个派生类的对象被完全释放。

另外上面两个过程中我们发现执行 "p->Work();" 时,也就是p在调用同名成员函数的时候,调用的始终是基类的成员函数,这是因为基类的成员函数覆盖了派生类的同名成员函数,如果想要调用派生类的成员函数,同样将Work()设置为虚函数即可。

#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<string>
using namespace std;

class A 
{
public:
	A() { cout << "A的构造" << endl; }
	virtual ~A() { cout << "A的析构" << endl; }
	virtual void Work() 
	{
		cout << "A工作" << endl;
	}
};

class B :public A
{
public:
	B() { cout << "B的构造" << endl; }
	~B() { cout << "B的析构" << endl; }
	void Work() { cout << "B工作" << endl; }
};

int main()
{
	A* p = new B;  //派生类指针转化成基类指针
	p->Work();
	delete p;
	system("pause");
	return EXIT_SUCCESS;
}

输出:

A的构造
B的构造
B工作
B的析构
A的析构
请按任意键继续. . .

内存切割:

在这里插入图片描述

参考:

C++:基类析构函数为什么要定义为虚函数

posted @ 2023-08-03 08:09  CodeMagicianT  阅读(309)  评论(0)    收藏  举报