梦回阑珊
2021-01-21 14:04
采纳率: 66.7%
浏览 116

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内存的时候直接程序崩溃了,求大神帮助!

  • 写回答
  • 关注问题
  • 收藏
  • 邀请回答

20条回答 默认 最新

  • KeLiaoo 2021-01-22 16:50
    已采纳
    #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是相等的。

    打赏 评论
  • 王高1990 2021-01-21 15:01

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

    打赏 评论
  • 泡视界 2021-01-21 15:14

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

    分别在基类里添加:

    ~Animal1(){};

    以及

    ~Animal2(){};

    打赏 评论
  • KeLiaoo 2021-01-21 15:14

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

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

    打赏 评论
  • 梦回阑珊 2021-01-21 16:56

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

    打赏 评论
  • 泡视界 2021-01-21 17:56

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

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

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

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

    打赏 评论
  • 梦回阑珊 2021-01-22 09:34

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

     

    打赏 评论
  • KeLiaoo 2021-01-22 10:41

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

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

    打赏 评论
  • KeLiaoo 2021-01-22 10:41

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

    打赏 评论
  • 梦回阑珊 2021-01-22 10:51

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

    打赏 评论
  • 梦回阑珊 2021-01-22 10:54

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

    打赏 评论
  • peng450 2021-01-22 13:53

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

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

    打赏 评论
  • peng450 2021-01-22 14:59

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

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

    打赏 评论
  • 梦回阑珊 2021-01-22 15:36

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

    打赏 评论
  • peng450 2021-01-22 16:09

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

    打赏 评论
  • 梦回阑珊 2021-01-22 16:34

    不是虚析构就崩溃

    打赏 评论
  • KeLiaoo 2021-01-22 16:39

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

    当你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这个运算符,是先调用析构函数,再回收内存。前面是没有析构函数,所以不会变动,现在有了析构函数,在析构函数结束时会把偏移过的地址改回原样,这一点很容易验证。地址对了回收内存自然也对了。

    打赏 评论
  • 梦回阑珊 2021-01-22 17:35

    谢谢大佬们~

    打赏 评论
  • peng450 2021-01-22 19:05

    我查了下汇编,发现如果有虚函数的时候,在赋值给父类指针的时候会根据虚表加上偏移(编译阶段确定大小),而后在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 

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

    打赏 评论
  • 梦回阑珊 2021-01-25 09:28

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

    打赏 评论

相关推荐 更多相似问题