[C++经验总结]为什么/什么时候要给基类声明virtual析构?

标题:[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析构。

在这里插入图片描述
完~
转载请注明出处