您的位置:首页 > 编程语言 > C语言/C++

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时,这时由于是通过虚函数表调用析构函数,而虚函数表中的地址是构造时写入的,是子类的析构函数的地址,由于结论第一条,所以子类与父类的析构函数都会得到调用,不会发生资源泄漏。

写的不对的地方,欢迎拍砖
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: