为什么基类析构函数需要是虚函数
析构函数作用
析构函数是进行类的清理工作,比如释放内存、关闭DB链接、关闭Socket等等,为实现多态性(C++多态),可以通过基类的指针或引用访问派生类的成员。也就是说,声明一个基类指针,这个基类指针可以指向派生类对象。
基类析构函数不是虚函数
#include <iostream>
class Base {
public:
// 注意,这里的析构函数没有定义为虚函数
~Base() {
std::cout << "Base destructor called." << std::endl;
}
};
class Derived : public Base {
public:
Derived() {
resource = new int[100]; // 分配资源
}
~Derived() {
std::cout << "Derived destructor called." << std::endl;
delete[] resource; // 释放资源
}
private:
int* resource; // 存储资源的指针
};
int main() {
Base* ptr = new Derived();
delete ptr; // 只会调用Base的析构函数,Derived的析构函数不会被调用
return 0;
}
/*
运行结果:
Base destructor called.
*/
由于基类Base
的析构函数没有定义为虚函数,当创建一个派生类Derived
的对象,并通过基类指针ptr
删除它时,只有基类Base
的析构函数被调用(因为这里没有多态,构造多态的必要条件就是虚函数)。派生类Derived
的析构函数不会被调用,导致指针指向的内存(resource
)没有被释放,从而产生内存泄漏。
基类析构函数设置为虚函数
class Base {
public:
virtual ~Base() { std::cout << "Base destructor called." << std::endl; }
};
class Derived : public Base {
public:
~Derived() { std::cout << "Derived destructor called." << std::endl; }
};
int main() {
Base *ptr = new Derived();
delete ptr; // 调用Derived的析构函数,然后调用Base的析构函数
return 0;
}
/*
运行结果:
Derived destructor called.
Base destructor called.
*/
在这个例子中,基类Base
的析构函数是虚函数,所以当删除ptr
时,会首先调用派生类Derived
的析构函数,然后调用基类Base
的析构函数,这样可以确保对象被正确销毁。
为什么类的默认析构函数不设置为虚函数
虚函数不同于普通成员函数,当类中有虚成员函数时,类会自动进行一些额外工作。这些额外的工作包括生成虚函数表和虚表指针,虚表指针指向虚函数表。每个类都有自己的虚函数表,虚函数表的作用就是保存本类中虚函数的地址,我们可以把虚函数表形象地看成一个数组,这个数组的每个元素存放的就是各个虚函数的地址。这样一来,就会占用额外的内存,当们定义的类不被其他类继承时,这种内存开销无疑是浪费的。因此当我们创建一个类时,系统默认我们不会将该类作为基类,所以就将默认的析构函数定义成非虚函数,这样就不会占用额外的内存空间。同时,系统也相信程序开发者在定义一个基类时,会显示地将基类的析构函数定义成虚函数,此时该类才会维护虚函数表和虚表指针。
零成本抽象原则
这也就是 C++ 的一个设计哲学:zero overhead abstraction
我的理解,所谓「零成本抽象」有两个层面的意思。
- 不需要为没有使用到的语言特性付出代价。
- 使用某种语言特性,不会带来运行时的代价。
放在这个地方就是,如果我们知道一个类不会被其它类继承,那么也就没必要将析构函数设置为虚函数,因为一旦引入虚函数就会引入虚表机制,这会造成额外的开销。
总结
在C++中,基类的析构函数需要定义为虚函数,以确保在通过基类指针或引用删除派生类对象时,能够正确地调用派生类的析构函数,否则派生类的析构函数不会被调用,这部分资源也就并无法被释放。
将基类的析构函数定义为虚函数后,C++运行时会自动识别指向的对象的实际类型,并确保调用正确的派生类析构函数。当派生类析构函数执行完毕后,基类的析构函数也会自动调用,以确保对象完全销毁。