在阅读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 的析构函数而已。