标题:[C++经验总结]为什么/什么时候要给基类声明virtual析构?
@水墨不写bug
文章目录
一、基类声明virtual析构原因:
给多态基类声明virtual
析构函数的原因是:当通过基类的指针delete派生类对象的时候,能够正确调用派生类的析构函数(而不是调用基类的析构函数)。这是多态类设计中的一个重要原则,否则会导致资源泄漏或未定义行为。
实例场景:
具体的实例场景如下:
假设我们有一个多态基类Base
和一个派生类Derived
,并且我们在堆上创建了一个Derived
对象,但通过基类指针来删除它。如果基类的析构函数不是virtual
,则只有基类的析构函数会被调用,而不会调用派生类的析构函数,导致派生类中的资源没有被正确释放。因为基类的析构函数不设置为virtual,不满足多态的条件——**基类的指针/引用指向派生类对象,调用重写/覆盖过的虚函数**。
#include <iostream>
class Base {
public:
Base() {
std::cout << "Base constructor\n"; }
virtual ~Base() {
std::cout << "Base destructor\n"; }
};
class Derived : public Base {
public:
Derived() {
std::cout << "Derived constructor\n"; }
~Derived() {
std::cout << "Derived destructor\n"; }
};
int main() {
Base* obj = new Derived();
delete obj; // 正确调用了Derived和Base的析构函数
return 0;
}
在上面的例子中,Base
类的析构函数被声明为virtual
,因此当我们通过基类指针删除Derived
对象时,首先调用Derived
类的析构函数,然后调用Base
类的析构函数。输出结果如下:
Base constructor
Derived constructor
Derived destructor
Base destructor
如果我们去掉Base
类析构函数的virtual
关键字,运行结果如下:
#include <iostream>
class Base {
public:
Base() {
std::cout << "Base constructor\n"; }
~Base() {
std::cout << "Base destructor\n"; }
};
class Derived : public Base {
public:
Derived() {
std::cout << "Derived constructor\n"; }
~Derived() {
std::cout << "Derived destructor\n"; }
};
int main() {
Base* obj = new Derived();
delete obj; // 只调用了Base的析构函数,导致资源泄漏
return 0;
}
输出结果如下:
Base constructor
Derived constructor
Base destructor
可以看到,Derived
类的析构函数没有被调用,导致派生类中的资源没有被正确释放。因此,为了确保资源管理正确,多态基类的析构函数应该声明为virtual
。
二、virtual函数不应滥用:
但是这并不意味着一个任何一个类的析构函数都应该被设置为virtual
,通常情况下,如果一个类不企图被当作基类,这个时候就不应该另其析构函数为virtual。
考虑下面这样的一个实例:
class size //一个表示钢板长和宽的尺寸
{
public:
size(int x,int y)
:_x(x)
,_y(y)
{
}
~size()
{
}
private:
int _x;
int _y;
};
如果int占用32bits,那么这个size类正好可以塞入一个64bits的缓存器中,进而可以把这个size对象当作一个64bits长度
的量,传递给其他语言,如Java,Python等语言撰写的函数。然而,如果把size的析构函数设置为virtual
,会引起意想不到的崩塌!
一个类如果存在virtual函数
,其对象内一定存在虚函数表指针
(简称“虚表指针”
),指向虚表,虚表本质是一个函数指针数组
,多态就是通过虚表的覆盖
来实现的。
然而,虚析构的存在引起了虚表指针的存在,不要忘了,虚表指针存储在对象中!这意味着size对象从原来的64bits增长到了96bits(x86)/128bits(x64)。
这就完了,因为一个size对象不再能够塞入一个64bits缓存器,而C++的size对象也不再能和其它语言(Java,Python)内声明的有着一样的结构。Java和Python的多态实现方式不同于C++中通过虚表指针(vtable)实现的方式,对应对象并没有虚表指针这一说法。
因此也再也不能把size对象通过64bits缓存器传递给其他语言所写的函数中,除非你明确补偿虚表指针这一结构——但是这样的实现也因此不再具有可移植性
!
三、此外,继承STL也是一个馊主意:
1. 标准的STL不实现虚析构
标准的STL (Standard Template Library) 中,大多数容器和算法类并没有实现虚析构函数。STL的设计初衷是为了提供高效的、非多态的容器和算法。因此,STL中的类通常不涉及多态行为,也不需要虚析构函数。
2. 继承STL类可能出现的问题
由于STL类没有虚析构函数,如果你从STL类继承并在派生类中添加新的成员或资源(如动态分配的内存),在通过基类指针删除派生类对象时,可能会导致资源泄漏或其他未定义行为。
具体示例
假设我们从std::vector
继承并添加新的成员:
#include <iostream>
#include <vector>
class MyVector : public std::vector<int> {
public:
MyVector() {
data = new int[100]; // 动态分配资源
}
~MyVector() {
delete[] data; // 释放资源
}
private:
int* data;
};
int main() {
std::vector<int>* vec = new MyVector();
delete vec; // 这里不会调用MyVector的析构函数,导致资源泄漏
return 0;
}
在以上代码中,由于std::vector
没有虚析构函数,通过基类指针删除派生类对象时,只会调用基类的析构函数,而不会调用派生类的析构函数,导致派生类中的动态分配的内存无法释放,造成资源泄漏。
因此当我们想要使用STL时实现自己的类,优先使用组合而不是继承!
四,本文总结:
- 带有多态性质的base类应该被声明一个virtual析构,一般而言,如果一个类带有任何virtual函数,他就应该拥有一个virtual析构。
- 类的设计目的不是作为base类使用,或是明确不是为了具备多态性,就不应该把析构设置为virtual。
- 继承STL往往是一个馊主意,因为标准STL没有实现virtual析构。
完~
转载请注明出处