虚函数表的内存分析

本文详细解析了C++中虚函数的运用形式,通过一个简单的程序实例展示了虚函数如何在父类指针调用子类函数时实现多态性,并深入探讨了虚函数表的概念及其在内存模型中的作用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

代码如下

#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函数






CGerman类的对象

       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的虚函数表,也就是说,指针改变了,终于回复正常了。

所以我们如果在父类的构造函数调用了虚函数,实际上是调用了父类的虚函数。







评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值