昨天去XX公司面试,面试官问了一个关于C++类析构函数为虚函数时,如果是父类的指针用子类来new,如果发生析构时,析构函数是virtual与不是virtual有什么区别。当时答的不好,回来总结了一下,在机器上实现了一遍,终于搞明白了。记录下来,以后遇到这种情况自己一定不要犯错了
一、先看第一种最简单的情况吧,教科书上教的,析构函数不是virtual,正常定义一个子类对象
- class
student - {
- public:
-
int *m_pInt; -
student() -
{ -
m_pInt = new int[10]; //1 -
memset(m_pInt, 0, 10*4); -
} -
-
~student() -
{ //3 -
delete []m_pInt; -
} -
- };
-
- class
GradeOneStue:public student - {
- public:
-
int m_iNum; -
GradeOneStue() -
{ //2 -
m_iNum = 1; -
} -
-
~GradeOneStue() -
{ //4 -
m_iNum = 0; -
} - };
-
- int
_tmain(int argc, _TCHAR* argv[]) - {
-
GradeOneStue gd; -
return 0; - }
- GradeOneStue()
- 00411470
push ebp - 00411471
mov ebp,esp - 00411473
sub esp,0CCh - 00411479
push ebx - 0041147A
push esi - 0041147B
push edi - 0041147C
push ecx - 0041147D
lea edi,[ebp-0CCh] - 00411483
mov ecx,33h - 00411488
mov eax,0CCCCCCCCh - 0041148D
rep stos dword ptr es:[edi] - 0041148F
pop ecx - 00411490
mov dword ptr [ebp-8],ecx - 00411493
mov ecx,dword ptr [this] - 00411496
call student::student (411109h) -
{ //2 -
m_iNum = 1; - 0041149B
mov eax,dword ptr [this] - 0041149E
mov dword ptr [eax+4],1 -
}
再来看看析构时的顺序(教科书上写的是先调用子类的析构函数,在调用父类的,与构造过程相反)
- ~GradeOneStue()
-
{ //4 - 00411550
push ebp - 00411551
mov ebp,esp - 00411553
sub esp,0CCh - 00411559
push ebx - 0041155A
push esi - 0041155B
push edi - 0041155C
push ecx - 0041155D
lea edi,[ebp-0CCh] - 00411563
mov ecx,33h - 00411568
mov eax,0CCCCCCCCh - 0041156D
rep stos dword ptr es:[edi] - 0041156F
pop ecx - 00411570
mov dword ptr [ebp-8],ecx -
m_iNum = 0; - 00411573
mov eax,dword ptr [this] - 00411576
mov dword ptr [eax+4],0 -
} - 0041157D
mov ecx,dword ptr [this] - 00411580
call student::~student (41102Dh)
二、析构函数是virtual,正常定义一个子类对象
构造函数顺序就略过了,看析构汇编代码
- virtual
~GradeOneStue() -
{ //4 - 004116D0
push ebp - 004116D1
mov ebp,esp - ...
- 004116F3
mov eax,dword ptr [this] - 004116F6
mov dword ptr [eax],offset GradeOneStue::`vftable' (415640h) -
m_iNum = 0; - 004116FC
mov eax,dword ptr [this] - 004116FF
mov dword ptr [eax+8],0 -
} - 00411706
mov ecx,dword ptr [this] - 00411709
call student::~student (411091h) ...
三、用一个父类指针去new一个子类对象,析构函数不是virtual,构造过程略过
- class
student - {
- public:
-
int *m_pInt; -
student() -
{ -
m_pInt = new int[10]; //1 -
memset(m_pInt, 0, 10*4); -
} -
-
~student() -
{ //3 -
delete []m_pInt; -
} -
- };
-
- class
GradeOneStue:public student - {
- public:
-
int m_iNum; -
GradeOneStue() -
{ //2 -
m_iNum = 1; -
} -
-
~GradeOneStue() -
{ //4 -
m_iNum = 0; -
} - };
-
- int
_tmain(int argc, _TCHAR* argv[]) - {
-
student *pStu = new GradeOneStue(); -
delete pStu; -
return 0; - }
- delete
pStu; - 00413726
mov eax,dword ptr [ebp-14h] - 00413729
mov dword ptr [ebp-0E0h],eax - 0041372F
mov ecx,dword ptr [ebp-0E0h] - 00413735
mov dword ptr [ebp-0ECh],ecx - 0041373B
cmp dword ptr [ebp-0ECh],0 - 00413742
je wmain+0C9h (413759h) - 00413744
push 1 - 00413746
mov ecx,dword ptr [ebp-0ECh] - 0041374C
call student::`scalar deleting destructor' (4111E5h) - 00413751
mov dword ptr [ebp-10Ch],eax - 00413757
jmp wmain+0D3h (413763h) - 00413759
mov dword ptr [ebp-10Ch],0 -
return 0;
四、用一个父类指针去new一个子类对象,析构函数是virtual,构造过程略过
这里直接看delete pStu的汇编代码
- delete
pStu; - 004114E6
mov eax,dword ptr [ebp-14h] - 004114E9
mov dword ptr [ebp-0E0h],eax - 004114EF
mov ecx,dword ptr [ebp-0E0h] - 004114F5
mov dword ptr [ebp-0ECh],ecx - 004114FB
cmp dword ptr [ebp-0ECh],0 - 00411502
je wmain+0D9h (411529h) - 00411504
mov esi,esp - 00411506
push 1 - 00411508
mov edx,dword ptr [ebp-0ECh] //edx等于pStu,指向new出来的子类对象 - 0041150E
mov eax,dword ptr [edx] //将edx指向的dword传给eax,eax现在是保存的虚函数表指向的地址 - 00411510
mov ecx,dword ptr [ebp-0ECh] - 00411516
mov edx,dword ptr [eax] //将虚函数表指向的第一个函数的地址传给edx,也就是唯一的一个虚析构函数的地址 - 00411518
call edx //调用析构函数,这个函数是子类的,这个地址构造时写入虚函数表
最后的结论:
1、无论父类与子类的析构函数是否是virtual,子类的析构函数都会调用父类的析构函数
2、如果父类与子类的析构函数不为virtual,用一个父类指针指向一个用子类类型new的对象,delete时,直接调用父类的析构函数,这是在编译时刻就决定的。如果子类析构函数中有释放资源的代码,这是会发生资源泄漏。
3、如果父类与子类的析构函数是virtual,用一个父类指针指向一个用子类类型new的对象,delete时,这时由于是通过虚函数表调用析构函数,而虚函数表中的地址是构造时写入的,是子类的析构函数的地址,由于结论第一条,所以子类与父类的析构函数都会得到调用,不会发生资源泄漏。
写的不对的地方,欢迎拍砖