• Effective C++学习笔记 条款07:为多态基类声明virtual析构函数


    一、C++明确指出:当derived class对象经由一个base class指针被删除,而该base class带着一个non-virtual析构函数,其结果未定义——实际执行时通常发生的是对象的derived成分没有被销毁!(注:使用基类引用派生类的方式使用多态,由于引用只是对原对象的一个引用或者叫做别名,其并没有分配内存,对其引用对象内存的销毁,将由原对象自己负责,所以使用引用的时候,如果析构函数不为virtual,那么也不会发生derived成员没有销毁的情况

      例如:

    class base
    {
        public:
            base(const string& name): m_baseName(name) {}
        private:
            string m_baseName;
    };
    
    class derived: public base
    {
        public:
            derived(const string& baseName, const string& derivedName): base(baseName), m_derivedName(derivedName) {}
        private:
            string m_derivedName;
    };
    int main(void)
    {
        base *pd = new derived("base", "derived");
        delete pd;
    }

      经由gdb单步调试发现,delete pd的时候,只调用了base的析构函数,销毁了基类的数据成员,但是没有调用派生类的析构函数,导致派生类的数据成员没有被销毁,造成一个诡异的“局部销毁”对象。这会导致资源泄漏。

      gdb调试如下:

    Breakpoint 1, main () at clause07_1.cpp:30
    30        base *pd = new derived("base", "derived");
    (gdb) n
    31        delete pd;
    (gdb) s
    base::~base (this=0x603070, __in_chrg=<optimized out>) at clause07_1.cpp:11
    11    class base
    (gdb) s
    main () at clause07_1.cpp:32
    32    }
    (gdb) s
    0x00007ffff752576d in __libc_start_main () from /lib/x86_64-linux-gnu/libc.so.6

      注:使用基类引用派生类的方式使用多态,由于引用只是对原对象的一个引用或者叫做别名,其并没有分配内存,对其引用对象内存的销毁,将由原对象自己负责,所以使用引用的时候,如果析构函数不为virtual,那么也不会发生derived成员没有销毁的情况

      例如:

    class base
    {
        public:
            ~base() 
            {
                cout << "base::~base" << endl;
            }
            virtual void f1() 
            {
                cout << "base::f1()" << endl;
            }
            void f2() 
            {
                cout << "base::f2()" << endl;
            }
    };
    
    class derived: public base
    {
        public:
            virtual void f1() 
            {
                cout << "derived::f1()" << endl;
            }
    
            void f2() 
            {
                cout << "derived::f2()" << endl;
            }
    
            ~derived()
            {
                cout << "derived::~derived()" << endl;
            }
    };
    int main(void)
    {
        derived d;
        base &rd = d;
        rd.f1();
        rd.f2();
    }

      输出为:

    derived::f1()
    base::f2()
    derived::~derived()
    base::~base

      derived的析构,是有对象d的销毁产生的。由于rd只是一个引用,没有自己的内存,所以不需要销毁。此时就不会有资源的泄漏(这里的引用的对象是静态对象,自动会销毁)。

    二、对于(1)出现的问题的解决方法:给base class一个virtual析构函数。

      仅仅将base class的析构函数定义为virtual,如下:

    class base
    {
        public:
            base(const string& name): m_baseName(name) {}
            virtual ~base() {}
        private:
            string m_baseName;
    };

      这样,derived class的析构函数,由于继承的原因,自动变为virtual,此时,通过delete指针,会触发多态机制,由于指针的动态类型是derived class,又派生类的析构函数是虚函数,所以调用derived class的析构函数,这样就会销毁派生类的数据成员,当派生类析构完成,自动会调用基类的析构函数,这样就没有资源的泄漏了。

      gdb单步调试验证如下:

    Breakpoint 1, main () at clause07_2.cpp:31
    31        base *pd = new derived("base", "derived");
    (gdb) n
    32        delete pd;
    (gdb) s
    derived::~derived (this=0x603070, __in_chrg=<optimized out>) at clause07_2.cpp:20
    20    class derived: public base
    (gdb) s
    derived::~derived (this=0x603070, __in_chrg=<optimized out>) at clause07_2.cpp:20
    20    class derived: public base
    (gdb) s
    base::~base (this=0x603070, __in_chrg=<optimized out>) at clause07_2.cpp:15
    15            virtual ~base() {}
    (gdb) s
    main () at clause07_2.cpp:33
    33    }

    三、任何class只要带有virtual函数都几乎确定应该也有一个virtual析构函数。

      例如:

    class base
    {
        public:
            virtual void f1() {}
    };
    
    class derived: public base
    {
        public:
            virtual void f1()
            {
                ptr = operator new(sizeof(int));
            }
            ~derived()
            {
                delete ptr;
            }
        private:
            void* ptr;
    };
    int main(void)
    {
        base *pd = new derived();
        pd->f1();
        delete pd;
    }

      虚函数机制使调用派生类函数分配了内存,希望在派生类的析构函数里释放,但是由于析构函数不是virtual,所以delete只会调用基类的析构函数。所以这里有内存泄漏!
      解决方法同样是base class的析构函数定义为virtual,这样就可以利用虚函数的多态机制了。

    四、如果base class不含virtual函数,通常表示它并不意图被用作一个base class。当class不企图被当作base class,令其析构函数为virtual往往是个馊主意。

      因为当一个类有虚函数后,那么它的所以对象的内存布局中都会多包含一个vptr(虚函数表指针),指向vtbl(vritual table)。这样会使对象膨胀,由于其他语言没有vptr,所以这就导致了不再具有移植性。

    五、对于继承任何析构函数不是virtual的类,如果通过指针使用多态机制,最后通过delete该指向derived类型的base类型指针销毁原derived对象,就会出现泄漏的问题。

      (注,通过引用的方式,引用对象的销毁,由其引用的对象自己负责(静态对象自动销毁,动态分配的需要delete pd(派生类的指针);)。但是如果你打算使用多态,就可能会用到使用指针的多态的方式,所以最好声明基类的析构为virtual)原因就是如(1)所说明的。

    六、当你打算使用的多态的时候,base class一般应该有个virtual析构函数。另外此时,不要继承析构不是virtual的类。然而,并非所有的基类都是为了多态 ,对于不打算使用多态的情况,基类不需要virtual析构函数。

    总结

    a.带多态性质的base class应该声明一个virtual析构函数。
    b.如果class带有virtual函数,它就应该拥有一个virtual析构函数。
    c.对于一个class的设计目的不是为了作为一个base class使用,或者对于base class设计目的不是为了多态用途,就不该virtual析构函数。

  • 相关阅读:
    关于mybatis中mapper.xmlSQL语句书写的心得
    笔记
    SpringMVC的小总结
    配置generatorConfig.xml自动生成的代码的sql书写问题
    关于SQL中的排序问题
    鼠标的change事件
    Git学习笔记 --第一章
    XHR对象
    黑马程序员——java学习6(127-151)——多线程
    黑马程序员——java学习5(107-126)——内部类,异常,包
  • 原文地址:https://www.cnblogs.com/wangjzh/p/4064088.html
Copyright © 2020-2023  润新知