C++中的有些析构函数也被定义为virtual虚函数,这是为什么?

在阅读C++项目(caffe)源码时,发现不少基类不仅把常规的成员函数定义成虚函数(virtual),也会把

析构函数定义为虚函数,结合前面几节的介绍,稍稍思考下,这样做的确是有原因的,本文将结合C++代码实例尝试探讨下。

C++中的有些析构函数也被定义为virtual虚函数,这是为什么?

为什么要把析构函数定义为虚函数?

常规

随便写一段C++代码作为实例,在这个例子中,我们先不把析构函数定义为虚函数:

<code>class Base {
public:
    Base () {
        cout << "Base construct\n";
    }
    ~Base() {
        cout << "Base deconstruct\n";
    }
    virtual void foo() {
        cout << "Base::foo\n";    
    }
    
    char *buf;
};

class Child: public Base {
public:
    Child() {
        cout << "Child construct\n";    
        buf = new char[16];
    }
    ~Child() {
        delete[] buf;
        cout << "Child deconstruct, delete buf\n";
    }
    void foo() {
        buf[0] = 3;
        cout << "Child::foo\n";
    }
};/<code>

这段代码的逻辑很简单,无非就是定义了两个类:类 Base 的成员函数 foo() 为虚函数,构造函数和析构函数都是常规函数,此外它还有个 public 的成员变量 buf。类 Child 则公开继承了 Base,因此它可以直接使用 Base::buf——在构造函数中 new 了一段内存,并且在析构函数 delete 掉它。

<code>Child c;
c.foo();/<code>

我们直接使用 Child 实例化一个对象 c,调用 c.foo(),此时得到如下输出:

<code>Base construct
Child construct
Child::foo
Child deconstruct, delete buf
Base deconstruct/<code>

一切尽在预料中。

不安全的问题

虽说对象 c 调用 foo() 的输出完全符合预计,但像上面那样定义类仍然是非常危险的做法。在这一节我们曾讨论过,父类指针可以调用派生类的重写函数,因此下面这两行C++代码也是合法的,请看:

<code>Base *pb = new Child();
pb->foo();

delete pb;/<code>

编译这段C++代码完全没有问题,运行也不会报错,输出如下:

<code>Base construct
Child construct
Child::foo
Base deconstruct/<code>

可是,从输出信息能够看出,派生类 Child 的析构函数没有被调用,对于本例而言,new 出来的 buf 没有对应的 delete,势必会造成内存泄漏。

解决问题

要解决所谓的“不安全问题”,其实很简单,按照题目说的做——将基类的析构函数也定义为虚函数就可以了,请看修改后的C++代码:

<code>class Base {
public:
    Base () {
        cout << "Base construct\n";
    }
    virtual ~Base() {
        cout << "Base deconstruct\n";
    }
.../<code>

也即尽在基类 Base 的析构函数前加上 virtual 关键字,其他的所有代码都无需改动。现在再执行下面的这几行C++代码:

<code>Base *pb = new Child();
pb->foo();

delete pb;/<code>

输出如下:

<code>Base construct
Child construct
Child::foo
Child deconstruct, delete buf
Base deconstruct/<code>

显然,此时派生类 Child 的析构函数也会被调用了,内存泄漏的问题被解决了。

小结

C++ 中的 virtual 关键字是非常好用,也是C++程序员必须掌握的关键字,其实,“不安全问题”出现的原因也是简单的:我们在静态类型与动态绑定一节中提到过,基本上只有涉及到 virtual 函数时,才会发生动态绑定,此时通过对象指针(pb)调用的函数由它指向的类(Child)决定,所以此时派生类 Child 的析构函数会被调用。如果基类 Base 的析构函数不是虚函数,那么对象指针(pb)调用的函数由其静态类型(Base)决定,也即调用的其实只是基类 Base 的析构函数而已。


分享到:


相關文章: