• c++中多态调用场景下基类析构函数的virtual声明


    一.基类析构函数未加virtual声明的情况

      在多态场景中,可通过基类的指针指向子类对象,并完成对子类对象的成员函数调用;在函数对象析构时,根据继承顺序,往往是先调用子类的析构函数,然后调用基类的析构函数,但是在多态场景中,可能会出现以下错误的情况:

    1.1 基础示例演示

    #include 
    
    using namespace std;
     
    class base
    {
    public:
        base(){cout<<"基类构造"<<endl;};
        ~base(){cout<<"基类析构"<<endl;};
    };
     
    class obj:public base
    {
    public:
        obj(){cout<<"子类构造"<<endl;};
        ~obj(){cout<<"子类析构"<<endl;};
    };
    
    int main()
    {
        base *pbase=new obj;    // 创建一个基类指针,并指向子类对象
        delete pbase;           // 析构基类对象
        pbase=nullptr;
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

      上述程序看似没问题,但是执行下来发现,输出结果如下:
    在这里插入图片描述
      可以发现此处在析构时仅仅调用了基类的析构函数,并没有调用子类的析构函数,在上述例子中并不会造成什么太坏影响,但是如果在子类的析构函数中做了内存释放、资源清楚等事情,上述情况就会造成内存泄漏,具体见下面的示例。

    1.2 进阶示例演示

    #include 
    #include 
    
    using namespace std;
     
    class base
    {
    public:
        base(){cout<<"基类构造"<<endl;};
        ~base(){cout<<"基类析构"<<endl;};
        virtual void display(void) = 0;
    };
     
    class obj:public base
    {
    public:
        obj(){
            p = (char *)malloc(100);
            strcpy(p,"子类申请的内存空间调用");
            cout<<"子类构造"<<endl;
        };
        ~obj(){
            free(p);
            p=nullptr;
            cout<<"子类析构"<<endl;
        };
        virtual void display(void)
        {
            std::cout<<string(p)<<std::endl;
        };
    private:
        char *p =nullptr;
    };
    
    int main()
    {
        base *pbase=new obj;    // 创建一个基类指针,并指向子类对象
        pbase->display();       // 基类指针调用子类对象,多态
        delete pbase;           // 析构基类对象
        pbase=NULL;
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    程序执行结果:
    在这里插入图片描述
      在上述示例中,可以发现在子类的构造函数中申请了100 byte的内存空间给指针p,在析构函数中释放了该内存,因此在执行子类构造函数后必须执行其析构函数,否则会造成内存泄漏,上述的执行结果显然没有调用子类的析构函数,必然造成p申请的内存空间得不到释放,造成内存泄漏,如何解决?基类析构函数需要加virtual声明。具体见下。

    二.基类析构函数添加virtual声明的情况

      如何解决在多态调用的场景中确保子类的对象能够被及时析构------在基类的析构函数加virtual声明。

    #include 
    #include 
    
    using namespace std;
     
    class base
    {
    public:
        base(){cout<<"基类构造"<<endl;};
        virtual ~base(){cout<<"基类析构"<<endl;};    // !!! virtual声明很关键
        virtual void display(void) = 0;    
    };
     
    class obj:public base
    {
    public:
        obj(){
            p = (char *)malloc(100);
            strcpy(p,"子类申请的内存空间调用");
            cout<<"子类构造"<<endl;
        };
        ~obj(){
            free(p);
            p=nullptr;
            cout<<"子类析构"<<endl;
        };
        virtual void display(void)
        {
            std::cout<<string(p)<<std::endl;
            // std::cout<<"dasdasdsa"<
        };
    private:
        char *p =nullptr;
    };
    
    int main()
    {
        base *pbase=new obj;    // 创建一个基类指针,并指向子类对象
        pbase->display();       // 基类指针调用子类对象,多态
        delete pbase;           // 析构基类对象
        pbase=NULL;
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    程序执行结果:
    在这里插入图片描述

      上述示例中,在基类的析构函数声明时添加了virtual关键字,可见执行结果中调用了子类的析构函数,自然在子类析构函数中释放了p申请的内存,避免了内存泄漏。

    三.总结

    (1)基类的的析构函数不是虚函数的话,删除指针时,只有基类的内存被释放,派生类的没有,这样就内存泄漏了。

    (2)析构函数不是虚函数的话,直接按指针类型调用该类型的析构函数代码,因为指针类型是基类,所以直接调用基类析构函数代码。

    (3)当基类指针指向派生类的时候,如果析构函数不声明为虚函数,在析构的时候,不会调用派生类的析构函数,从而导致内存泄露。

    (4)子类对象创建时先调用基类构造函数然后在调用子类构造函数,在清除对象时顺序相反,所以delete pbase只清除了基类,而子类没有清除。

    (5) 什么时候才要用虚析构函数呢?通常情况下,程序员的经验是,当类中存在虚函数时要把析构函数写成virtual,因为类中存在虚函数,就说明它有想要让基类指针或引用指向派生类对象的情况,此时如果派生类的构造函数中有用new/malloc动态产生的内存,那么在其析构函数中务必要delete这个数据,但是一般的像以上这种程序,这种操作只调用了基类的析构函数,而标记成虚析构函数的话,系统会先调用派生类的析构函数,再调用基类本身的析构函数

    (6)一般情况下,在类中有指针成员的时候要写copy构造函数,赋值操作符重载和析构函数。

  • 相关阅读:
    JVM运行时数据区——虚拟机栈
    Linux系统中利用C语言控制LED的方法
    (附源码)node.js宠物医生预约平台 毕业设计 030945
    手把手教你驱动墨水屏
    Android应用程序启动源码浅析-(三万字长文慎点&Android14)
    【构建并发程序】4-原子变量-CAS-ABA问题
    Powershell历史执行记录
    基于Springboot实现旧物置换网站平台演示【项目源码+论文说明】分享
    vuex、接口模块式开发
    接口隔离原则(Interface Segregation Principle)
  • 原文地址:https://blog.csdn.net/weixin_42700740/article/details/126869347