C++多继承下多态内存释放问题

#include <iostream>
#include <string>
using namespace std;

class Animal1
{
public:
    virtual void fun1() = 0;
};
class Animal2
{
public:
    virtual void fun2() = 0;
};

class Cat : public Animal1, public Animal2
{
public:
    virtual void fun1()
    {
        cout << "fun1" << endl;
    }
    virtual void fun2()
    {
        cout << "fun2" << endl;
    }
};

int main(void)
{
    Animal1 *a1 = new Cat;
    Animal2 *a2 = new Cat;    
    a1->fun1(); //fun1
    a2->fun2(); //fun2
    delete a1;
    delete a2;
    return 0;
}

释放指针a2内存的时候直接程序崩溃了,求大神帮助!

#include <iostream>
#include <string>
using namespace std;

class Animal1
{
public:
    virtual void fun1() {}
    //virtual ~Animal1(){}
};
class Animal2
{
public:
    virtual void fun2() {}
    virtual ~Animal2(){}
};

class Cat : public Animal1, public Animal2
{
public:
    virtual void fun1()
    {
        cout << "fun1" << endl;
    }
    virtual void fun2()
    {
        cout << "fun2" << endl;
    }
};

int main(void)
{
    Cat* c = new Cat;
    Animal2* a2 = c;
    delete a2;
    return 0;
}
void __CRTDECL operator delete(void* const block) noexcept
{
    #ifdef _DEBUG
    _free_dbg(block, _UNKNOWN_BLOCK);
    #else
    free(block);
    #endif
}

如果给animal2写了虚析构函数,block和c是相等的,如果没写,block和a2是相等的。

我仔细研究了一下这个问题。

当你delete时,最终都会调用这么一个函数(vs2019):

void __CRTDECL operator delete(void* const block) noexcept
{
    #ifdef _DEBUG
    _free_dbg(block, _UNKNOWN_BLOCK);
    #else
    free(block);
    #endif
}
也就是说,当你delete时,如果没有析构函数,到底怎么回收内存,仅仅是由指针所指的地址确定的,和指针的类型无关,指针的类型在这里没有作用。

然后回到你这个问题,事实上,delete a1时是没有错误的,错误出现再delete a2。原因很好猜到,a1所指的地址就是Cat对象的首地址,所以delete a1可以正常运行;但是a2 所指的地址不是Cat对象的首地址,由于这里是多继承,a2所指的地址相对Cat对象的首地址有偏移,编译器发现它自己没有给这个地址分配空间,所以就报错了。

为什么加了析构函数后运行正常呢?原因也很简单,delete这个运算符,是先调用析构函数,再回收内存。前面是没有析构函数,所以不会变动,现在有了析构函数,在析构函数结束时会把偏移过的地址改回原样,这一点很容易验证。地址对了回收内存自然也对了。

基类 Animal1 和Animal2 添加虚析构函数

在析构 指向派生类对象的 基类指针 时,需要将基类析构函数写成虚函数,不然存在内存泄漏。

分别在基类里添加:

~Animal1(){};

以及

~Animal2(){};

派生类必须有虚析构函数,否则有可能挂掉。

你可以分别用sizeof看一看这三个类的大小就知道为什么崩溃了。

我不是通过delete释放掉了a1和a2了吗,为什么还要加析构

delete会调用析构……大兄弟。

你的问题是delete 是通过 指向派生类对象的 基类指针 完成的,其在析构时 会调用基类析构,而派生类的析构会因为没有调用到而造成 内存泄漏,程序崩溃。

所以在你非要用 这种指针的方式的情况下,你需要给基类写明 virtual ~Animal1(){};以及virtual ~Animal2(){},用这种方式在delete的时候,会执行派生类的析构函数,从而避免了内存泄漏。

我上面的回答少写了virtual ,在次更正。

但是我的派生类里面没有堆区的属性需要释放内存啊,所以走不走派生类的析构都没关系把,因为调用基类虚析构是为了能走派生类析构的,所以如果派生类没有堆区属性要释放,那么基类写不写虚析构都没关系把?

 

不是让你用sizeof了吗?基类和派生类的大小都不一样,所以你delete基类指针释放的栈区空间和delete派生类指针释放的栈区空间不一样,当然会跑飞了。

事实上,这里Animal1和Animal2存了一个虚函数表的指针,Cat存了两个虚函数表的指针。

口误,释放的堆区空间不一样

delete基类指针不是释放指向的堆区空间吗?

派生类在堆区new出的对象给到基类指针,delete基类指针释放的不是派生类堆区内存?

按照上面所说,在delete a1,的时候为啥不crash呢?

题主的情况应该是必现的吧。

你在A1和A2都加入 fun1 和fun2的虚函数 应该就运行成功了,A1 和A2大小一样,cat的大小应该是多了一个指针大小(4或8)区别。

我怀疑的是,当析构A2的时候,在虚表中没有找到对应的fun2函数,所以崩溃了。

为什么走基类默认析构就崩溃了,而走虚析构不会这样?

不清楚,如果你试过改虚析构也正常的话。能否试下直接添加为A和B都添加一个析构函数,不是虚析构。看看执行情况。

不是虚析构就崩溃

谢谢大佬们~

我查了下汇编,发现如果有虚函数的时候,在赋值给父类指针的时候会根据虚表加上偏移(编译阶段确定大小),而后在delete的时候,会先执行某个函数把地址减去偏移,再执行析构函数。

在没有析构函数的情况下是直接调用free,因此崩溃。

31	 B *b = new C;
   0x00000000004009a8 <+59>:	mov    $0x10,%edi
   0x00000000004009ad <+64>:	callq  0x400860 <_Znwm@plt>
   0x00000000004009b2 <+69>:	mov    %rax,%rbx
   0x00000000004009b5 <+72>:	mov    %rbx,%rdi
   0x00000000004009b8 <+75>:	callq  0x400c7c <C::C()>
   0x00000000004009bd <+80>:	test   %rbx,%rbx
   0x00000000004009c0 <+83>:	je     0x4009c8 <main()+91>
   0x00000000004009c2 <+85>:	lea    0x8(%rbx),%rax
   0x00000000004009c6 <+89>:	jmp    0x4009cd <main()+96>
   0x00000000004009c8 <+91>:	mov    $0x0,%eax
   0x00000000004009cd <+96>:	mov    %rax,-0x28(%rbp)

35	 delete a;
   0x0000000000400a45 <+216>:	mov    -0x20(%rbp),%rax
   0x0000000000400a49 <+220>:	mov    %rax,%rdi
   0x0000000000400a4c <+223>:	callq  0x4007d0 <_ZdlPv@plt>

36	 delete b;
   0x0000000000400a51 <+228>:	cmpq   $0x0,-0x28(%rbp)
   0x0000000000400a56 <+233>:	je     0x400a6f <main()+258>
   0x0000000000400a58 <+235>:	mov    -0x28(%rbp),%rax
   0x0000000000400a5c <+239>:	mov    (%rax),%rax
   0x0000000000400a5f <+242>:	add    $0x10,%rax
   0x0000000000400a63 <+246>:	mov    (%rax),%rax
   0x0000000000400a66 <+249>:	mov    -0x28(%rbp),%rdx
   0x0000000000400a6a <+253>:	mov    %rdx,%rdi
   0x0000000000400a6d <+256>:	callq  *%rax

Dump of assembler code for function _ZThn8_N1CD0Ev:
24		virtual ~C(){}
   0x0000000000400c4a <+0>:	sub    $0x8,%rdi
   0x0000000000400c4e <+4>:	jmp    0x400c24 <C::~C()>

Dump of assembler code for function C::~C():
24		virtual ~C(){}
   0x0000000000400c24 <+0>:	push   %rbp
   0x0000000000400c25 <+1>:	mov    %rsp,%rbp
   0x0000000000400c28 <+4>:	sub    $0x10,%rsp
   0x0000000000400c2c <+8>:	mov    %rdi,-0x8(%rbp)
   0x0000000000400c30 <+12>:	mov    -0x8(%rbp),%rax
   0x0000000000400c34 <+16>:	mov    %rax,%rdi
   0x0000000000400c37 <+19>:	callq  0x400bd0 <C::~C()>
   0x0000000000400c3c <+24>:	mov    -0x8(%rbp),%rax
   0x0000000000400c40 <+28>:	mov    %rax,%rdi
   0x0000000000400c43 <+31>:	callq  0x4007d0 <_ZdlPv@plt>
   0x0000000000400c48 <+36>:	leaveq 
   0x0000000000400c49 <+37>:	retq 

很高兴与你们这次的交流。

厉害啊,汇编看不懂,但知道那个意思了