c++ 析构函数为虚函数的问题
2015-08-31 23:27
267 查看
昨天去XX公司面试,面试官问了一个关于C++类析构函数为虚函数时,如果是父类的指针用子类来new,如果发生析构时,析构函数是virtual与不是virtual有什么区别。当时答的不好,回来总结了一下,在机器上实现了一遍,终于搞明白了。记录下来,以后遇到这种情况自己一定不要犯错了
一、先看第一种最简单的情况吧,教科书上教的,析构函数不是virtual,正常定义一个子类对象
[cpp] view plaincopy
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;
}
这时构造顺序是先1后2,下面是反汇编代码
[plain] view plaincopy
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
}
可以看到在执行m_iNum = 1前先调用了父类的构造函数。
再来看看析构时的顺序(教科书上写的是先调用子类的析构函数,在调用父类的,与构造过程相反)
[plain] view plaincopy
~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,正常定义一个子类对象
构造函数顺序就略过了,看析构汇编代码
[plain] view plaincopy
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,构造过程略过
[cpp] view plaincopy
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处的汇编代码
[plain] view plaincopy
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的汇编代码
[plain] view plaincopy
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 //调用析构函数,这个函数是子类的,这个地址构造时写入虚函数表
由于子类的虚构函数最后会调用父类的析构函数(不管是否为virtual,子类析构函数最后都会调用父类析构函数),所以最终父类的析构函数会得到执行。(这时调用的析构函数相当于pStu->vpTable->析构函数(),这个析构函数是构造时写入的,由于构造时使用子类类型去new,此时虚函数表中析构函数的地址是子类的析构函数地址)
最后的结论:
1、无论父类与子类的析构函数是否是virtual,子类的析构函数都会调用父类的析构函数
2、如果父类与子类的析构函数不为virtual,用一个父类指针指向一个用子类类型new的对象,delete时,直接调用父类的析构函数,这是在编译时刻就决定的。如果子类析构函数中有释放资源的代码,这是会发生资源泄漏。
3、如果父类与子类的析构函数是virtual,用一个父类指针指向一个用子类类型new的对象,delete时,这时由于是通过虚函数表调用析构函数,而虚函数表中的地址是构造时写入的,是子类的析构函数的地址,由于结论第一条,所以子类与父类的析构函数都会得到调用,不会发生资源泄漏。
写的不对的地方,欢迎拍砖
一、先看第一种最简单的情况吧,教科书上教的,析构函数不是virtual,正常定义一个子类对象
[cpp] view plaincopy
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;
}
这时构造顺序是先1后2,下面是反汇编代码
[plain] view plaincopy
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
}
可以看到在执行m_iNum = 1前先调用了父类的构造函数。
再来看看析构时的顺序(教科书上写的是先调用子类的析构函数,在调用父类的,与构造过程相反)
[plain] view plaincopy
~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,正常定义一个子类对象
构造函数顺序就略过了,看析构汇编代码
[plain] view plaincopy
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,构造过程略过
[cpp] view plaincopy
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处的汇编代码
[plain] view plaincopy
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的汇编代码
[plain] view plaincopy
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 //调用析构函数,这个函数是子类的,这个地址构造时写入虚函数表
由于子类的虚构函数最后会调用父类的析构函数(不管是否为virtual,子类析构函数最后都会调用父类析构函数),所以最终父类的析构函数会得到执行。(这时调用的析构函数相当于pStu->vpTable->析构函数(),这个析构函数是构造时写入的,由于构造时使用子类类型去new,此时虚函数表中析构函数的地址是子类的析构函数地址)
最后的结论:
1、无论父类与子类的析构函数是否是virtual,子类的析构函数都会调用父类的析构函数
2、如果父类与子类的析构函数不为virtual,用一个父类指针指向一个用子类类型new的对象,delete时,直接调用父类的析构函数,这是在编译时刻就决定的。如果子类析构函数中有释放资源的代码,这是会发生资源泄漏。
3、如果父类与子类的析构函数是virtual,用一个父类指针指向一个用子类类型new的对象,delete时,这时由于是通过虚函数表调用析构函数,而虚函数表中的地址是构造时写入的,是子类的析构函数的地址,由于结论第一条,所以子类与父类的析构函数都会得到调用,不会发生资源泄漏。
写的不对的地方,欢迎拍砖
![](http://static.blog.csdn.net/xheditor/xheditor_emot/default/tongue.gif)
相关文章推荐
- Visual C++防止窗口和控件闪烁的方法
- C语言基础学习——第4天(数组)
- [C++11 并发编程] 16 在期望中保存异常
- C++ Primer 5e chapter 17.1
- 算法导论计数排序实现(C++)
- C语言数组与指针详解
- c语言练习题 3-5 回文数
- c++ map sort by value and sort by key(字典的遍历)
- 服务器初学---1,C++ MySQL 连接。
- C与OC
- c语言练习题 3-4 连续正整数的和
- c语言练习题 3-3 计算e^x
- C++11 中STL库中新增内容
- C++中的多态
- c语言练习题 3-2 计算矩形面积
- c++の文件读写
- c++学习一:指针基础
- 在operator=中处理自我赋值(Effective C++_11)
- C语言基础知识之(十六):结构体和指针
- C++中的友元