24.面向对象-多态-虚析构函数
24.1 知识点
虚析构函数的作用
- 在 C++ 的多态中,虚析构函数是极其重要的概念
- 用于确保通过父类指针删除子类对象时,能正确调用子类的析构函数,避免资源泄漏或行为未定义
说人话:
- 如果用父类指针装载子类对象
- 在释放父类指针时,如果父类析构函数不是虚函数
- 将导致子类析构函数不会被调用,进而造成内存泄漏等问题
举例说明
#pragma once
#include <iostream>
using namespace std;
class Father
{
public:
virtual ~Father()
{
cout << "Father的析构" << endl;
}
};
#pragma once
#include "Father.h"
class Son : public Father
{
public:
~Son()
{
cout << "Son的析构" << endl;
}
};
Father* f = new Son();
delete f;
// 如果父类析构函数中没有 virtual 关键字
// 那么只会执行父类析构
// 父类析构函数中加了 virtual
// 会先执行子类析构,再执行父类析构
注意点:
- 如果存在多层继承关系,只需将最上层基类的析构函数设置为虚函数即可
- 对于多重继承,也只需保证最上层的公共基类的析构函数是虚函数即可
多重继承情况举例:
普通多重继承
A B
\ /
C
A 和 B 都需要将析构函数设置为虚函数
菱形继承
A
/ \
B C
\ /
D
A 的析构函数设置为虚函数即可
栈上的调用情况
Father f2 = Son();
// 输出:
// Son的析构
// Father的析构
// Father的析构
过程解析:
- 临时对象的生成:
Son()
会在栈上创建一个匿名的Son
临时对象 - 对象切片操作:
Father f2 = Son()
会将临时Son
对象中属于Father
的部分复制到f2
中,此时f2
成为了一个Father
对象,Son
特有部分被“切掉” - 临时对象的析构:临时
Son
对象生命周期结束,系统会先调用Son
的析构函数,再调用Father
的析构函数 - 局部对象的析构:当
f2
的作用域结束时,f2
作为一个Father
对象,会再次调用Father
的析构函数
Son s = Son();
Father& f3 = s;
// 输出:
// Son的析构
// Father的析构
过程解析:
- 对象的创建:
Son s = Son()
创建了一个Son
对象,构造时若存在拷贝操作通常会被优化,实质上等价于直接构造 - 引用的绑定:
Father& f3 = s
让基类引用f3
绑定到s
,没有产生新对象 - 对象的析构:当
s
的作用域结束时,s
是Son
类型,系统会先调用Son
析构函数,然后是Father
析构函数
总结
- 当存在继承关系时,建议始终将析构函数设为虚函数
- 因为你永远无法保证不会通过指针(堆上的里氏替换)来释放对象
- 为避免子类资源未被释放,应使用虚析构函数以保证行为的正确性
24.2 知识点代码
Father.cpp
#pragma once
#include <iostream>
using namespace std;
class Father
{
public:
virtual ~Father()
{
cout << "Father的析构" << endl;
}
};
Son.cpp
#pragma once
#include "Father.h"
class Son :
public Father
{
public:
~Son()
{
cout << "Son的析构" << endl;
}
};
Lesson24_面向对象_多态_虚析构函数.cpp
#include <iostream>
#include "Son.h"
int main()
{
#pragma region 知识点一 虚析构函数的作用
//在 C++ 的多态中,虚析构函数是极其重要的概念
//用于确保通过父类指针删除子类对象时,能正确调用子类的析构函数,避免资源泄漏或行为未定义
//说人话:
//如果用父类指针装载子类指针对象
//在对父类指针容器进行释放时,如果父类析构函数不是虚函数
//会导致无法执行子类析构函数,造成内存泄漏等问题
#pragma endregion
#pragma region 知识点二 举例说明
Father* f = new Son();
delete f;
//如果父类析构函数中没有virtual关键字
//那么只会执行父类析构
//父类析构函数中加了virtual
//会先执行子类析构 再执行父类析构
//注意点:
//1.如果存在多层继承关系,那么我们只需要将最上层的析构函数设置为 虚函数即可
//2.多重继承,只需要保证最上层单的基类 析构函数时虚函数即可
//
// 普通多重继承
// A B
// \ /
// C
// A和B都需要将析构函数设置为虚函数
//
//
// 菱形继承
// A
// / \
// B C
// \ /
// D
// A的析构函数设置为虚函数
#pragma endregion
#pragma region 知识点三 栈上的调用情况
//栈上会进行切片后释放
Father f2 = Son();
//输出:
//Son的析构
//Father的析构
//Father的析构
//
//具体过程解析:
//临时对象的生成:Son()会在栈上创建一个匿名的Son临时对象。
//对象切片操作:Father f2 = Son()会把这个临时Son对象里属于Father类的那部分数据复制到f2中。此时,f2仅仅是一个Father对象,Son类特有的成员已被 “切掉”。
//临时对象的析构:临时Son对象的生命周期结束,系统会先调用Son的析构函数,再调用Father的析构函数。
//局部对象的析构:当程序执行到f2的作用域末尾时,f2作为一个Father对象,会再次调用Father的析构函数。
//使用引用后释放
Son s = Son();
Father& f3 = s;
//输出:
//Son的析构
//Father的析构
//
//具体过程解析:
//对象的创建:Son s = Son()创建了一个Son对象s,这里的拷贝操作(如果存在的话)会被编译器优化掉,实际上相当于直接构造s。
//引用的绑定:Father & f3 = s让基类引用f3指向s,但并没有生成新的对象。
//对象的析构:当s的作用域结束时,由于s是Son类型的对象,系统会先调用Son的析构函数,接着调用Father的析构函数。
#pragma endregion
//总结
//当存在继承关系时,都建议大家使用虚析构函数
//因为我们无法保证不会使用指针(堆上)的里氏替换来释放对象
}
24.3 练习题
下列关于 C++ 析构函数的说法,哪个是正确的
A. 所有析构函数默认都是虚函数
B. 如果基类没有虚析构函数,通过基类指针删除派生类对象可能会导致资源泄露
C. 子类必须显式写 virtual 析构函数,才能覆盖父类
D. 虚析构函数无法继承
正确答案:B
选项 A 错误
析构函数默认不是虚函数,除非你显式声明为virtual
。C++ 不会自动将所有析构函数当作虚函数处理。选项 B 正确
如果你没有将基类的析构函数声明为virtual
,当通过Base* base = new Derived(); delete base;
删除派生类对象时,只会调用基类的析构函数,导致派生类中堆上分配的资源不会被释放,从而发生内存泄漏。选项 C 错误
子类的析构函数即使不写virtual
,只要基类的析构函数是virtual
,它仍然是虚函数,因为虚函数具有继承性。选项 D 错误
虚析构函数是可以继承的。一旦基类的析构函数被声明为virtual
,则所有派生类的析构函数默认也是虚函数。
如果基类的析构函数声明为 virtual,则派生类的析构函数也必须显式写 virtual。
正确答案:错误
virtual
的特性是 可以被继承的。当基类的析构函数被声明为虚函数时,派生类中即使不写 virtual
,其析构函数依然是虚函数。因此不需要再次显式声明 virtual
。
使用父类指针指向子类对象时,是否声明虚析构函数不会影响对象析构的完整性
正确答案:错误
如果没有将基类的析构函数声明为 virtual
,当你用父类指针删除子类对象时,只会调用父类的析构函数,而不会触发子类的析构。这会导致子类中的资源无法被释放,造成资源泄漏。
正确做法是为基类声明虚析构函数,确保析构链完整。
你设计了一个基类 GameObject,若未来会通过 GameObject* 指向各种子类(如 Enemy、Player、Item)并通过指针删除,请问应当如何设计析构函数?为什么?
参考答案:
应该将 GameObject
的析构函数声明为 virtual
。
原因是如果不这么做,当通过 GameObject*
指向派生类对象并执行 delete
操作时,仅会调用 GameObject
的析构函数,而不会调用 Enemy
、Player
等子类的析构函数,从而导致资源泄露或未释放干净的情况发生。
声明为 virtual
后,C++ 的多态机制将确保正确调用整个析构链上的每一个析构函数,从子类开始,一直析构到基类,保证资源安全释放。
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com