c++虚析构

内存泄漏问题

class BaseA {
public:
    BaseA() {
        printf("BaseA::BaseA()\n");
    };
    ~BaseA() {
        printf("BaseA::~BaseA()\n");
    };
    virtual void func1() {};
};


class BaseC : public BaseA {
public:
    BaseC() {
        printf("BaseC::BaseC()\n");
    };
    ~BaseC() {
        printf("BaseC::~BaseC()\n");
    };
    virtual void func3() {};
};

int main()
{
    BaseA* test = new BaseC();
    delete test;
    return 0;
}

上述代码打印如下,也就是并未调用继承类BaseC的析构函数,如果BaseC中析构函数中存在资源释放操作的话就会造成内存泄漏。

BaseA::BaseA()
BaseC::BaseC()
BaseA::~BaseA()

让基类BaseA声明为虚函数后就可以解决这个问题,基类BaseA声明为虚函数后编译器会创建代理析构函数放在虚表中,delete 基类指针的时候就会调用虚表中的代理析构函数BaseC::'scalar deleting destructor'(unsigned int),代理析构会分别调用BaseCBaseA的析构函数。

img

此时test对象的内存布局如下

img

崩溃或未知问题

class BaseA {
public:
    BaseA() {
        printf("BaseA::BaseA()\n");
    };
    ~BaseA() {
        printf("BaseA::~BaseA()\n");
    };
    virtual void func1() {};
};

class BaseB {
public:
    BaseB() {
        printf("BaseB::BaseB()\n");
    };
    ~BaseB() {
        printf("BaseB::~BaseB()\n");
    };
    virtual void func2() {};
};

class BaseC : public BaseB, public BaseA {
public:
    BaseC() {
        printf("BaseC::BaseC()\n");
    };
    ~BaseC() {
        printf("BaseC::~BaseC()\n");
    };
    virtual void func3() {};
};

int main()
{
    BaseA* test = new BaseC();
    delete test;
    return 0;
}

多层继承中未定义虚析构造成内存泄漏的问题这种场景是比较常见的。不过还有一种不常见的场景,就是多重继承下未定义虚析构带来的崩溃和其他未知问题。例子中BaseC同时继承了BaseB和BaseA,定义一个BaseC对象并用BaseA基类指针保存。此程序运行会出现崩溃问题,看一下崩溃堆栈是调用delete test-->free崩溃的。

img

看一下当前内存的一个布局,可以发现此时BaseA *test基类指针指向的是整个BaseC对象中BaseA对象的起始地址。这就造成了调用free的时候传入的地址不是BaseC对象的起始地址,因为new/malloc申请内存的时候会将申请内存的大小以一定格式保存在申请基地址的前面(可能是以结构体的形式保存,这是由编译器决定的,每个编译器都不一样),这样free再释放内存的时候才能去索引到结构并确定释放多少内存,如果传给free的起始地址不对,其索引得到的需要释放的内存大小有可能就是错误的,就可能造成程序的崩溃。

img

但是如果定义成BaseB* test = new BaseC(),也就是将new的BaseC对象保存在最先继承的类的基类指针中,在delete的时候就不会有这种问题,因为此时test确实是指向了BaseC对象中BaseB对象的起始地址,刚好其内存布局就在BaseC对象的起始位置,这样free传入的起始地址就不会崩溃。

img

最终的解决办法就是基类BaseABaseB的析构函数定义为虚析构,这样编译器就会创建一个代理析构保存在虚表中,代理析构会对传给free的地址进行自动调整。例如本来BaseA* test = new BaseC()test指针的位置并不在BaseC对象的头部,而是跳过了BaseB对象并指向了BaseA对象,代理析构BaseC::'scalar deleting destructor'(unsigned int)在调用free前会对test指针的值进行修正,因为这里BaseB对象没有成员只有一个虚表,所以修正的时候就是减去一个虚表地址的大小4

img

基类析构函数设置成虚函数后整个内存布局变化如下

img

最后总结为一句话:析构函数一定要设置成虚函数,避免不必要的问题。

posted @ 2023-08-25 17:44  怎么可以吃突突  阅读(110)  评论(0编辑  收藏  举报