#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
很高兴与你们这次的交流。
厉害啊,汇编看不懂,但知道那个意思了