10

C++中有的类的析构函数为什么要定义成虚函数?

 3 years ago
source link: https://blog.popkx.com/why-some-deconstruct-functions-are-defined-as-virtual/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

C++中有的类的析构函数为什么要定义成虚函数?

发表于 2020-05-11 19:05:00   |   已被 访问: 306 次   |   分类于:   杂谈 , C++虚函数   |   暂无评论

在阅读C++项目(caffe)源码时,发现不少基类不仅把常规的成员函数定义成虚函数(virtual),也会把析构函数定义为虚函数,结合前面几节的介绍,稍稍思考下,这样做的确是有原因的,本文将结合C++代码实例尝试探讨下。

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

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";
    }
};

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

Child c;
c.foo();

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

Base construct
Child construct
Child::foo
Child deconstruct, delete buf
Base deconstruct

一切尽在预料中。

不安全的问题

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

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

delete pb;

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

Base construct
Child construct
Child::foo
Base deconstruct

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

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

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

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

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

delete pb;

输出如下:

Base construct
Child construct
Child::foo
Child deconstruct, delete buf
Base deconstruct

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

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


Recommend

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK