代码如下
#include <iostream>
#include <sstream>
#include <string>
#include <cstdio>
using namespace std;
class CPerson
{
public:
CPerson()
{}
virtual ~CPerson(){}
virtual void ShowSpeak()
{
}
};
class CChinese:public CPerson
{
public:
CChinese()
{}
virtual ~CChinese()
{}
virtual void ShowSpeak()
{
cout<<"l am Chinese"<<endl;
}
};
class CGerman:public CPerson
{
public:
CGerman()
{}
virtual ~CGerman()
{}
virtual void ShowSpeak()
{
cout<<"l am German"<<endl;
}
};
void Speak(CPerson *p)
{
p->ShowSpeak();
}
int main()
{
CPerson person;
CChinese chinese;
CGerman german;
Speak(&chinese);
Speak(&german);
return 0;
}
本程序就是一个简单的虚函数运用,C++中虚函数的使用形式主要是使用父类指针调用子类的函数,类似于JAVA中的接口,父类声明一个方法,子类用不同方式去实现,而在调用时候不需要很麻烦的使用子类A的对象去访问该方法,子类B的对象去访问该方法,我们只需要使用虚函数机制,在父类中将该函数声明为虚函数,这样就可以只使用父类指针访问不同的子类的中的同名函数,这是普通的函数覆盖所无法代替的,这个情况在设计模式中亦有应用,不多累说。
先看主函数代码,省却初始栈的代码,直接看这里
50: CPerson person;
0040127D lea ecx,[ebp-10h]
00401280 call @ILT+125(CPerson::CPerson) (00401082)
00401285 mov dword ptr [ebp-4],0
51: CChinese chinese;
0040128C lea ecx,[ebp-14h]
0040128F call @ILT+55(CChinese::CChinese) (0040103c)
00401294 mov byte ptr [ebp-4],1
52: CGerman german;
00401298 lea ecx,[ebp-18h]
0040129B call @ILT+45(CGerman::CGerman) (00401032)
004012A0 mov byte ptr [ebp-4],2
我们在初始栈的时候,已经压入了三个双字类型,最上面的对象,CPerson类对象地址,就是ebp-C-4=ebp-10h,同样,CChinese为ebp-14h,CGerman为ebp-18h,分别是这三个对象的ths指针
来看CPerson的构造函数
CPerson()
00401340 push ebp
00401341 mov ebp,esp
00401343 sub esp,44h
00401346 push ebx
00401347 push esi
00401348 push edi
00401349 push ecx
0040134A lea edi,[ebp-44h]
0040134D mov ecx,11h
00401352 mov eax,0CCCCCCCCh
00401357 rep stos dword ptr [edi]
00401359 pop ecx
0040135A mov dword ptr [ebp-4],ecx
0040135D mov eax,dword ptr [ebp-4]
00401360 mov dword ptr [eax],offset CPerson::`vftable' (0043201c)
00401366 mov eax,dword ptr [ebp-4]
00401369 pop edi
0040136A pop esi
我们已经知道,CPerson对象的大小为四个字节,这四个字节并不包含成员变量,那这四个字节是什么呢?答案就是传说中的虚函数表指针
00401359 pop ecx
0040135A mov dword ptr [ebp-4],ecx
0040135D mov eax,dword ptr [ebp-4]
00401360 mov dword ptr [eax],offset CPerson::`vftable' (0043201c)
这几行的意思就是先把this保存起来,然后将虚函数表的地址付给CPerson类的对象,我们在内存中查看下0043201c,发现了004010e6,00401046这两个双字,这两个双字就是虚函数表中的表项,两个分别对应虚析构函数和虚函数ShowSpeak,当然这两个地址,我们查看后都是跳转到别处的,跟踪到最后,可以发现是00401055,和004013C0
所以我们可以知道这个CPerson类对象的内存模型
CPerson类的对象
0043201C(指向一个虚函数表) |
00401055(虚析构函数) |
004013C0 ShowSpeak函数 |
ps:不得不吐槽啊,我连箭头都没找到,好费劲 ==
同样
CChinese类的对象
00432028(指向一个虚函数表 ) |
----------------------------------------------------------------------------------------->>>
00401055(虚析构函数) |
00401510 ShowSpeak函数 |
00432044(指向一个虚函数 ) |
----------------------------------------------------------------------------------------->>>
00401055(虚析构函数) |
00401690 ShowSpeak函数 |
以上就是三个对象的虚函数表,虽然都有同名的ShowSpeak函数,但是指向了不同的地址,这也是后面为什么一个父类指针访问不同子类的同名函数,却可以出现不同结果的原因。
OK。往下走
53: Speak(&chinese);
004012A4 lea eax,[ebp-14h]
004012A7 push eax
004012A8 call @ILT+120(Speak) (0040107d)
004012AD add esp,4
我们分析先分析这个,取this指针,然后调用ShowSpeak函数,不多说,来看0040107d这个地址(当然这个地址是属于CChinese的),属于调用本类的ShowSpeak函数,而不是其他比如CGerman的ShowSpeak函数。
0040107d是个跳转,跳转到00401200处,我们可以看看。
void Speak(CPerson *p)
45: {
00401200 push ebp
00401201 mov ebp,esp
00401203 sub esp,40h
00401206 push ebx
00401207 push esi
00401208 push edi
00401209 lea edi,[ebp-40h]
0040120C mov ecx,10h
00401211 mov eax,0CCCCCCCCh
00401216 rep stos dword ptr [edi]
46: p->ShowSpeak();
00401218 mov eax,dword ptr [ebp+8]
0040121B mov edx,dword ptr [eax]
0040121D mov esi,esp
0040121F mov ecx,dword ptr [ebp+8]
00401222 call dword ptr [edx+4]
00401225 cmp esi,esp
00401227 call __chkesp (00409590)
47: }
我们首先已经把this指针压栈了,所以如果访问,则为ebp+8
mov eax,dword ptr [ebp+8]
0040121B mov edx,dword ptr [eax]
0040121D mov esi,esp
0040121F mov ecx,dword ptr [ebp+8]
00401222 call dword ptr [edx+4]
这一段意思就是讲this指针先给一个寄存器,然后使用地址为寄存器+4处的内存(如果不熟悉,请查阅汇编语言的几种寻址方式),明显edx+4就是CChinese类的第二个虚函数的地址,也就是调用了CChinese类的第二个虚函数也就是他的ShowSpeak。
至于Speak(&german);也是一个道理。
综上,虚函数的访问机制已经解释清楚。
如果我们做个小小改动,让代码变形,比如这样
#include <iostream>
#include <sstream>
#include <string>
#include <cstdio>
using namespace std;
class CPerson
{
public:
CPerson()
{
ShowSpeak();
}
virtual ~CPerson(){}
virtual void ShowSpeak()
{
cout<<"l am person"<<endl;
}
int data;
};
class CChinese:public CPerson
{
public:
CChinese()
{
}
virtual ~CChinese()
{}
virtual void ShowSpeak()
{
cout<<"l am Chinese"<<endl;
}
};
class CGerman:public CPerson
{
public:
CGerman()
{}
virtual ~CGerman()
{}
virtual void ShowSpeak()
{
cout<<"l am German"<<endl;
}
};
void Speak(CPerson *p)
{
p->ShowSpeak();
}
int main()
{
CChinese chinese;
Speak(&chinese);
return 0;
}
这里的改动是Person类构造函数里增加了
ShowSpeak();
并且把ShowSpeak函数改成输出 l am person,我们应该清楚,在初始化CChines对象时候,要先调用父类CPerson的构造函数,而在调用它的时候,传递的this指针其实还是CChines对象的地址,我们可以知道这样的信息
进入CPerson构造函数 CChinese对象地址处指向的其实是CPerson的虚函数表,也就是说表的第二个位置ShowSpeak函数打印出来,就是l am person
再进入CChinese构造函数,CChinese对象地址处指向的就是CChinese的虚函数表,也就是说,指针改变了,终于回复正常了。
所以我们如果在父类的构造函数调用了虚函数,实际上是调用了父类的虚函数。