首页
社区
课程
招聘
[原创]C++代码分析
发表于: 2011-8-25 14:29 20153

[原创]C++代码分析

2011-8-25 14:29
20153

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个地址了,我们知道一个类如果有虚析构函数,第一个地址就指向虚析构函数的地址,EAX+8就是调用了
pTest->pointf();至于为什么,自己想一下,MOV ECX,ESI通过ECX来保证堆栈的平衡
004010D3  |.  8B06          MOV EAX,DWORD PTR DS:[ESI]                    ;  C++构造.0040218C
004010D5  |.  8B50 04       MOV EDX,DWORD PTR DS:[EAX+4]
004010D8  |.  8BCE          MOV ECX,ESI
004010DA  |.  FFD2          CALL EDX
这里就调用了pTest->prointer();因为我们是根据类的地址来决定调用哪个函数的
004010DC  |.  8B06          MOV EAX,DWORD PTR DS:[ESI]
004010DE  |.  8B10          MOV EDX,DWORD PTR DS:[EAX]                    ;  C++构造.00401050
004010E0  |.  6A 01         PUSH 1
004010E2  |.  8BCE          MOV ECX,ESI
004010E4  |.  FFD2          CALL EDX
这里就是调用TestA类的虚折构函数,也就是当前类的地址的第一个指针,我们跟踪进去看一下,下面是反汇编代码:
00401050   .  56            PUSH ESI
00401051   .  57            PUSH EDI
00401052   .  8B3D B0204000 MOV EDI,DWORD PTR DS:[<&MSVCR90.printf>]      ;  msvcr90.printf
00401058   .  8BF1          MOV ESI,ECX
0040105A   .  68 2C214000   PUSH 0040212C                     ; /format = "TestA::TestA"
0040105F   .  C706 8C214000 MOV DWORD PTR DS:[ESI],0040218C               ; |
00401065   .  FFD7          CALL EDI                                      ; \printf
00401067   .  68 18214000   PUSH 00402118                        ;  ASCII "Virtual ~Test()"
0040106C   .  C706 7C214000 MOV DWORD PTR DS:[ESI],0040217C
00401072   .  FFD7          CALL EDI
00401074   .  83C4 08       ADD ESP,8
00401077   .  F64424 0C 01  TEST BYTE PTR SS:[ESP+C],1
0040107C   .  74 09         JE SHORT 00401087
0040107E   .  56            PUSH ESI
0040107F   .  E8 76000000   CALL <JMP.&MSVCR90.operator delete>
00401084   .  83C4 04       ADD ESP,4
00401087   >  5F            POP EDI
00401088   .  8BC6          MOV EAX,ESI
0040108A   .  5E            POP ESI
0040108B   .  C2 0400       RETN 4
这里调用了两个虚析构函数的地方,为什么是先调用~TestA,而不是先调用~Test呢,因为我们把这两个析构函数定义为了虚函数,虚函数是在运行期决定调用谁的,当我们把TestA的成员函数调用完毕之后,析构函数会自动调用,因此,TestA完了之后就调用自己的析构函数,释放最新分配的内存,所以,先调用TestA的折构函数,再调用Test的折构函数,这也是为什么把析构函数声明为虚函数的原因,
这里调用了两个虚析构函数之后,就是用delete指针删除了由new分配的地址,分析完毕。
总结一下:
当我们定义了一个带有虚函数的类的时候,这个类的虚函数就会被放在一个地址表里面,这个地址表被放在类的入口里面,当我们调用哪个类的时候,就使用哪个类的入口来调用里面的虚函数,这就证名明了C++中得函数同名的虚函数的多态机制。
讲得不好,如果有错误的地方请各位指出来,谢谢!


[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

收藏
免费 6
支持
分享
最新回复 (32)
雪    币: 1015
活跃值: (235)
能力值: ( LV12,RANK:440 )
在线值:
发帖
回帖
粉丝
2
先顶韬哥一个,希望韬哥可以继续深入的探索 = =
加上自己的东西嘛,分享给大家也好嘛
2011-8-25 14:38
0
雪    币: 278
活跃值: (709)
能力值: ( LV15,RANK:520 )
在线值:
发帖
回帖
粉丝
3
不准笑哈。
2011-8-25 14:58
0
雪    币: 278
活跃值: (709)
能力值: ( LV15,RANK:520 )
在线值:
发帖
回帖
粉丝
4
别笑我,没技术含量的,实在完成不了工作才分析下这个
2011-8-25 15:04
0
雪    币: 209
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
5
看了一半,是在坚持不下去了,  不好意思,我不应该。。。
2011-8-25 15:08
0
雪    币: 1708
活跃值: (586)
能力值: ( LV15,RANK:670 )
在线值:
发帖
回帖
粉丝
6
new 并不是无论如何都成功的。
内存不足的时候,或者内存碎片太多,就会失败。一般情况下, new 调用失败会抛出异常。而不是返回空。
2011-8-25 15:23
0
雪    币: 278
活跃值: (709)
能力值: ( LV15,RANK:520 )
在线值:
发帖
回帖
粉丝
7
谢谢指出错误!
2011-8-25 15:25
0
雪    币: 692
活跃值: (40)
能力值: ( LV6,RANK:90 )
在线值:
发帖
回帖
粉丝
8
析构函数的调用顺序和该析构函数是否为虚函数没有什么关系。

在继承层次的类中,构造的时候都是由基类向派生类的顺序调用构造函数,析构的顺序相反,先调用派生类的析构函数,然后顺着继承层次依次向上调用析构函数,最后是基类的析构函数被调用。

再一个,我没看出楼主的代码发生了多态行为。当基类指针指向派生类的时候发生多台行为。
2011-8-25 16:04
0
雪    币: 278
活跃值: (709)
能力值: ( LV15,RANK:520 )
在线值:
发帖
回帖
粉丝
9
一个虚成员函数覆盖嘛,+个派生,派生类覆盖基类的虚成员函数,就是运行期决定,就是多态
2011-8-25 17:37
0
雪    币: 64
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
10
继承层次析构最好为虚析构!
2011-8-25 19:12
0
雪    币: 287
活跃值: (618)
能力值: ( LV5,RANK:60 )
在线值:
发帖
回帖
粉丝
11
完全看不懂,不过看上去好像很厉害啊!膜拜滔哥
2011-8-25 20:47
0
雪    币: 122
活跃值: (12)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
12
不错,帮顶。。。
2011-8-25 20:51
0
雪    币: 129
活跃值: (358)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
xed
13
太高深了,我都看不懂
多态是什么我都不明白
2011-8-26 16:47
0
雪    币: 692
活跃值: (40)
能力值: ( LV6,RANK:90 )
在线值:
发帖
回帖
粉丝
14

举个例子吧,看了这段代码,看来输出结果, 如果能看明白就知道什么是多台了 ,然后再把基类的 析构函数改成虚的,再试一次。看看结果就晓得来。

#include <iostream>

using std::cout;
using std::endl;

class base {
public:
	base()
	{
		cout<<"constructor of base class\n";
	}
	 ~base()
	{
		cout<<"destructor of base class\n";
	}

	virtual void say_hello(void)
	{
		cout<<"base say hello\n";
	}

};

class child:public base {
public:
	child(){
		cout<<"constructor of child class\n";
	}
	~child(){
		cout<<"destructor of child class\n";
	}
	virtual void say_hello(void)
	{
		cout<<"child say hello\n";
	}

};

void say_hello(base *bp)
{
	if (bp == NULL)
		return;
	bp->say_hello();
}

int main(int argc, char **argv)
{
	base *bp = new child;
	say_hello(bp);
	delete bp;
	bp = new base;
	say_hello(bp);
	delete bp;
	bp = NULL;
	return 0;

}
2011-8-26 18:03
0
雪    币: 278
活跃值: (709)
能力值: ( LV15,RANK:520 )
在线值:
发帖
回帖
粉丝
15
谢谢各位大牛指教!
2011-8-26 21:46
0
雪    币: 278
活跃值: (709)
能力值: ( LV15,RANK:520 )
在线值:
发帖
回帖
粉丝
16
楼上兄弟不是和我上面说的一样吗
2011-8-26 21:46
0
雪    币: 27
活跃值: (127)
能力值: ( LV8,RANK:120 )
在线值:
发帖
回帖
粉丝
17
我c++一直没学好 貌似邓韬也在巡警?
2011-8-26 21:55
0
雪    币: 278
活跃值: (709)
能力值: ( LV15,RANK:520 )
在线值:
发帖
回帖
粉丝
18
离开了。xx职位不好耍,
2011-8-26 22:07
0
雪    币: 287
活跃值: (618)
能力值: ( LV5,RANK:60 )
在线值:
发帖
回帖
粉丝
19
滔哥威武,再次膜拜
2011-8-26 23:43
0
雪    币: 793
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
20
关于C++的 虚函数和多态部分建议去看看侯杰深入浅出MFC里面讲的很清楚了!
2011-8-28 08:57
0
雪    币: 278
活跃值: (709)
能力值: ( LV15,RANK:520 )
在线值:
发帖
回帖
粉丝
21
求指点,不会MFC
2011-8-28 13:32
0
雪    币: 878
活跃值: (496)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
22
这些东西没必要用汇编,不解释,哈
2011-8-29 09:59
0
雪    币: 1708
活跃值: (586)
能力值: ( LV15,RANK:670 )
在线值:
发帖
回帖
粉丝
23
关键是要在反汇编的时候,能正确识别。
2011-8-29 10:05
0
雪    币: 11
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
24
你的例子测试的没多大意义,表面看不出来....基类指针指向派生类对象时,才体现的出虚函数的用处..
多态只是一种行为...  不是所说的“运行时”... 多写两个类就能看出来.

建议询问刘感超大牛.!~
2011-8-30 19:52
0
雪    币: 278
活跃值: (709)
能力值: ( LV15,RANK:520 )
在线值:
发帖
回帖
粉丝
25
谢谢指点,一点随笔,大家不要争议。
2011-8-31 08:31
0
游客
登录 | 注册 方可回帖
返回