24.虚析构函数

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的析构

过程解析:

  1. 临时对象的生成:Son() 会在栈上创建一个匿名的 Son 临时对象
  2. 对象切片操作:Father f2 = Son() 会将临时 Son 对象中属于 Father 的部分复制到 f2 中,此时 f2 成为了一个 Father 对象,Son 特有部分被“切掉”
  3. 临时对象的析构:临时 Son 对象生命周期结束,系统会先调用 Son 的析构函数,再调用 Father 的析构函数
  4. 局部对象的析构:当 f2 的作用域结束时,f2 作为一个 Father 对象,会再次调用 Father 的析构函数
Son s = Son();
Father& f3 = s;

// 输出:
// Son的析构
// Father的析构

过程解析:

  1. 对象的创建:Son s = Son() 创建了一个 Son 对象,构造时若存在拷贝操作通常会被优化,实质上等价于直接构造
  2. 引用的绑定:Father& f3 = s 让基类引用 f3 绑定到 s,没有产生新对象
  3. 对象的析构:当 s 的作用域结束时,sSon 类型,系统会先调用 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 的析构函数,而不会调用 EnemyPlayer 等子类的析构函数,从而导致资源泄露或未释放干净的情况发生

声明为 virtual 后,C++ 的多态机制将确保正确调用整个析构链上的每一个析构函数,从子类开始,一直析构到基类,保证资源安全释放



转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com

×

喜欢就点赞,疼爱就打赏