• C++中基类的析构函数为什么要用virtual虚析构函数


    知识背景

             要弄明白这个问题,首先要了解下C++中的动态绑定。 

             关于动态绑定的讲解,请参阅:  C++中的动态类型与动态绑定、虚函数、多态实现

    正题

             直接的讲,C++中基类采用virtual虚析构函数是为了防止内存泄漏。具体地说,如果派生类中申请了内存空间,并在其析构函数中对这些内存空间进行释放。假设基类中采用的是非虚析构函数,当删除基类指针指向的派生类对象时就不会触发动态绑定,因而只会调用基类的析构函数,而不会调用派生类的析构函数。那么在这种情况下,派生类中申请的空间就得不到释放从而产生内存泄漏。所以,为了防止这种情况的发生,C++中基类的析构函数应采用virtual虚析构函数。

    示例代码讲解

    现有Base基类,其析构函数为非虚析构函数。Derived1和Derived2为Base的派生类,这两个派生类中均有以string* 指向存储其name的地址空间,name对象是通过new创建在堆上的对象,因此在析构时,需要显式调用delete删除指针归还内存,否则就会造成内存泄漏。

    class Base {
     public:
    ~Base() {
      cout << "~Base()" << endl;
    }
    };
    class Derived1 : public Base {
     public:
      Derived1():name_(new string("NULL")) {}
      Derived1(const string& n):name_(new string(n)) {}
    
      ~Derived1() {
        delete name_;
        cout << "~Derived1(): name_ has been deleted." << endl;
      }
    
     private:
      string* name_;
    };
    
    class Derived2 : public Base {
     public:
      Derived2():name_(new string("NULL")) {}
      Derived2(const string& n):name_(new string(n)) {}
    
      ~Derived2() {
        delete name_;
        cout << "~Derived2(): name_ has been deleted." << endl;
      }
    
     private:
      string* name_;
    };

    我们看下面对其析构情况进行测试:

    int main() {
      Derived1* d1 = new Derived1();
      Derived2 d2 = Derived2("Bob");
      delete d1;
      return 0;
    }

    d1为Derived1类的指针,它指向一个在堆上创建的Derived1的对象;d2为一个在栈上创建的对象。其中d1所指的对象需要我们显式的用delete调用其析构函数;d2对象在其生命周期结束时,系统会自动调用其析构函数。看下其运行结果:


    刚才我们说,Base基类的析构函数并不是虚析构函数,现在结果显示,派生类的析构函数被调用了,正常的释放了其申请的内存资源。这两者并不矛盾,因为无论是d1还是d2,两者都属于静态绑定,而且其静态类型恰好都是派生类,因此,在析构的时候,即使基类的析构函数为非虚析构函数,也会调用相应派生类的析构函数。

    下面我们来看下,当发生动态绑定时,也就是当用基类指针指向派生类,这时候采用delete显式删除指针所指对象时,如果Base基类的析构函数没有virtual,会发生什么情况?

    int main() {
      Base* base[2] = {
        new Derived1(),
        new Derived2("Bob")      
      };
      for (int i = 0; i != 2; ++i) {
        delete base[i];    
      }
      return 0;
    }


            从上面结果我们看到,尽管派生类中定义了析构函数来释放其申请的资源,但是并没有得到调用。原因是基类指针指向了派生类对象,而基类中的析构函数却是非virtual的,之前讲过,虚函数是动态绑定的基础。现在析构函数不是virtual的,因此不会发生动态绑定,而是静态绑定,指针的静态类型为基类指针,因此在delete时候只会调用基类的析构函数,而不会调用派生类的析构函数。这样,在派生类中申请的资源就不会得到释放,就会造成内存泄漏,这是相当危险的:如果系统中有大量的派生类对象被这样创建和销毁,就会有内存不断的泄漏,久而久之,系统就会因为缺少内存而崩溃。

            也就是说,在基类的析构函数为非虚析构函数的时候,并不一定会造成内存泄漏;当派生类对象的析构函数中有内存需要收回,并且在编程过程中采用了基类指针指向派生类对象,如为了实现多态,并且通过基类指针将该对象销毁,这时,就会因为基类的析构函数为非虚析构函数而不触发动态绑定,从而没有调用派生类的析构函数而导致内存泄漏。

            因此,为了防止这种情况下内存泄漏的发生,最好将基类的析构函数写成virtual虚析构函数。

    下面把Base基类的析构函数改为虚析构函数:

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

    再看下其运行结果:


    这样就会实现动态绑定,派生类的析构函数就会得到调用,从而避免了内存泄漏。







                 原创文章,转载请注明: 转载自  IIcyZhao's Road

              本文链接地址:  http://blog.csdn.net/iicy266/article/details/11906457


  • 相关阅读:
    JVM 综述
    看 Netty 在 Dubbo 中如何应用
    Netty 心跳服务之 IdleStateHandler 源码分析
    Netty 高性能之道
    Netty 解码器抽象父类 ByteToMessageDecoder 源码解析
    Netty 源码剖析之 unSafe.write 方法
    Netty 出站缓冲区 ChannelOutboundBuffer 源码解析(isWritable 属性的重要性)
    Netty 源码剖析之 unSafe.read 方法
    Netty 内存回收之 noCleaner 策略
    Netty 源码阅读的思考------耗时业务到底该如何处理
  • 原文地址:https://www.cnblogs.com/riskyer/p/3333706.html
Copyright © 2020-2023  润新知