C++代码分析
C++虚函数是构成多态的一部分,多态指的是运行期决定调用哪个函数,下面是个纯虚函数例子:
#include "stdafx.h"
class Test{
public:
Test(){
printf("Test::Test\n");
}
virtual ~Test(){
printf("Virtual ~Test()\n");
}
virtual void prointer()=0;
virtual void pointf()=0;
};
class TestA:public Test{
public:
TestA(){
printf("TestA::TestA\n");
}
virtual ~TestA(){
printf("TestA::TestA\n");
}
virtual void prointer(){
printf("Derive Class TestA::Pointer\n");
}
virtual void pointf(){
printf("Derive Class TestA::Pointf\n");
}
};
int _tmain(int argc, _TCHAR* argv[]){
TestA *pTest=new TestA;
pTest->pointf();
pTest->prointer();
delete pTest;
return 0;
}
这段代码定义了一个抽象类,和一个派生类,抽象类不能创建自己的对象,但是可以间接的从派生类创建自己的对象,构成纯虚函数的条件:
1. 一个类中必须要有一个虚函数
2. 在虚函数后面添加一个=0就是一个纯虚函数了
抽象基类的所有纯虚函数必须被派生类定义的虚函数覆盖,否者派生类也是一个抽象基类,不能创建自己的对象;先看下Test类,由于Test类不能创建自己的对象,所以我根据TestA类来解析调用过程。Test类我们可以把它看做一个地址,这个地址里面有些指针,只想函数的地址,假如Test类的地址是0x401000,那么在这个地址里面的第一个就是虚折构函数,方便释放类的对象的时候调用,第二个没有了,因为我们只在Test类中定义一个析构函数,和一个构造函数,构造函数在编译的时候就被编译器从类的里面给趴到Main来了,看下反汇编代码:
00401091 |. 6A 04 PUSH 4
00401093 |. E8 68000000 CALL <JMP.&MSVCR90.operator new>
00401098 |. 8BF0 MOV ESI,EAX
0040109A |. 83C4 04 ADD ESP,4
0040109D |. 85F6 TEST ESI,ESI
0040109F |. 74 27 JE SHORT 004010C8
这里就是TestA *pTest=new TestA这里了,从这段代码我们可以看出,new是无论何如都会调用成功的,因为CALL <JMP.&MSVCR90.operator new>后的返回值,被比较是否等于0了,虽然这个比较不是我们的代码,但是编译器就已经够定了new无论如何都会调用成功,如果CALL <JMP.&MSVCR90.operator new>的返回值是0,那么构造函数都会被跳过,而构造函数是会被程序调用的,如果不调用的话,这样就和C++构造函数的说法相反了,所以new 操作符分配的内存一定会成功的,我们在接着看下下面这段代码:
004010A1 |. 57 PUSH EDI
004010A2 |. 8B3D B0204000 MOV EDI,DWORD PTR DS:[<&MSVCR90.printf>] ; msvcr90.printf
004010A8 |. 68 0C214000 PUSH 0040210C ; /format = "Test::Test"
004010AD |. C706 7C214000 MOV DWORD PTR DS:[ESI],0040217C ; |
004010B3 |. FFD7 CALL EDI ; \printf
004010B5 |. 68 2C214000 PUSH 0040212C ; ASCII "TestA::TestA"
004010BA |. C706 8C214000 MOV DWORD PTR DS:[ESI],0040218C
004010C0 |. FFD7 CALL EDI
这段代码显然是两个类的构造函数被调用了,那么其中传递了两个地址给ESI,我们看下这个地址是什么类容,我们跟随到数据窗口看一下,显示格式选择为地址格式
0040217C 00401000 这就是这个地址的内容,一个代码地址
C++构造.00401000其中第一个地址指向如下地址,跟随一下
00401000 . 56 PUSH ESI
00401001 . 8BF1 MOV ESI,ECX
00401003 . 68 18214000 PUSH 00402118 ; /format = "Virtual ~Test()"
00401008 . C706 7C214000 MOV DWORD PTR DS:[ESI],0040217C ; |
0040100E . FF15 B0204000 CALL DWORD PTR DS:[<&MSVCR90.printf>] ; \printf
这里显然就是折够函数了,所以当一个类中有虚析构函数的时候,这个虚析构函数的地址会被放在类指针的最前面,这里把Test的地址的指针放入ESI里面,然后根据ESP+8来判断是否调用delete操作符,这些都是编译器自动添加的,这是编译器的事,我还没那技术去研究
00401014 . 83C4 04 ADD ESP,4
00401017 . F64424 08 01 TEST BYTE PTR SS:[ESP+8],1
0040101C . 74 09 JE SHORT 00401027
0040101E . 56 PUSH ESI
0040101F . E8 D6000000 CALL <JMP.&MSVCR90.operator delete>
00401024 . 83C4 04 ADD ESP,4
00401027 > 8BC6 MOV EAX,ESI
00401029 . 5E POP ESI
0040102A . C2 0400 RETN 4
继续我们上面的构造函数,类的构造函数被一次从上至下的调用之后,传递了Test和TestA的地址到ESI里面,我们声明的是TestA的对象,所以最后一个地址就是TestA了,看下反汇编代码的调用过程
004010C2 |. 83C4 08 ADD ESP,8
004010C5 |. 5F POP EDI
004010C6 |. EB 02 JMP SHORT 004010CA
004010C8 |> 33F6 XOR ESI,ESI
004010CA |> 8B06 MOV EAX,DWORD PTR DS:[ESI] ;
004010CC |. 8B50 08 MOV EDX,DWORD PTR DS:[EAX+8]
004010CF |. 8BCE MOV ECX,ESI
004010D1 |. FFD2 CALL EDX
这里ESI指向TestA类的起始地址,把这个起始地址传到EAX里面之后,就把这个类里面的一个函数地址放到EDX里面,TestA类本身一共有4个函数,刚才构造函数被外部也就是Main调用了,那么里面只剩下3个地址了,我们知道一个类如果有虚析构函数,第一个地址就指向虚析构
补充:软件开发 , C++ ,