千家信息网

C++中虚函数的调用及对象的内部布局是什么样的

发表于:2025-11-07 作者:千家信息网编辑
千家信息网最后更新 2025年11月07日,C++中虚函数的调用及对象的内部布局是什么样的,很多新手对此不是很清楚,为了帮助大家解决这个难题,下面小编将为大家详细讲解,有这方面需求的人可以来学习下,希望你能有所收获。这次我依然用分析C++代码编
千家信息网最后更新 2025年11月07日C++中虚函数的调用及对象的内部布局是什么样的

C++中虚函数的调用及对象的内部布局是什么样的,很多新手对此不是很清楚,为了帮助大家解决这个难题,下面小编将为大家详细讲解,有这方面需求的人可以来学习下,希望你能有所收获。

这次我依然用分析C++代码编译后生成的汇编代码来说明C++中虚函数调用的实现方法,顺便也说明一下C++中的对象内部布局。下面所有的汇编代码都是用VC2005编译出来的。虽然,不同的编译器可能会编译出不同的结果,对象的内部布局也不尽相同;但是,只要是符合C++标准的编译器,编译结果和对象的内部布局应该是大同小异。

首先,是一个有着简单继承关系的两个类: class CBase
{
public:
virtual void VFun1() = 0;
virtual void VFun2() = 0;
void Fun1();
};

// 这里仅仅是为了生成函数的汇编代码,因此函数体为空
void CBase::Fun1()
{
}

class CDerived : public CBase
{
public:
virtual void VFun1();
virtual void VFun2();
void Fun2();
private:
int m_iValue1;
int m_iValue2;
};

// 这里仅仅是为了生成函数的汇编代码,因此函数体为空
void CDerived::VFun1()
{
}

// 这里仅仅是为了生成函数的汇编代码,因此函数体为空
void CDerived::VFun2()
{
}

// 这里是为了分析对象的内部布局,因此仅仅是给成员变量赋值
void CDerived::Fun2()
{
m_iValue1 = 13;
m_iValue2 = 13;
}
现在用下面的代码来调用成员函数:

CDerived derived;

// 用对象调用虚函数
derived.VFun1();
derived.VFun2();
// 用对象调用非虚函数
derived.Fun1();
derived.Fun2();

// 用指向派生类的基类的指针调用虚函数,实现多态
CBase *pTest = &derived;
pTest->VFun1();
pTest->VFun2();
下面就是用VC2005编译上面的代码后生成的汇编代码:

CDerived derived;
0041195E lea ecx,[derived]
00411961 call CDerived::CDerived (411177h)

// 代码段1
derived.VFun1();
00411966 lea ecx,[derived]
00411969 call CDerived::VFun1 (411078h)
derived.VFun2();
0041196E lea ecx,[derived]
00411971 call CDerived::VFun2 (4111B8h)
derived.Fun1();
00411976 lea ecx,[derived]
00411979 call CBase::Fun1 (411249h)
derived.Fun2();
0041197E lea ecx,[derived]
00411981 call CDerived::Fun2 (4111BDh)

// 代码段2
CBase *pTest = &derived;
00411986 lea eax,[derived]
00411989 mov dword ptr [pTest],eax
pTest->VFun1();
0041198C mov eax,dword ptr [pTest] // 行1
0041198F mov edx,dword ptr [eax] // 行2
00411991 mov esi,esp
00411993 mov ecx,dword ptr [pTest]
00411996 mov eax,dword ptr [edx] // 行3
00411998 call eax // 行4
0041199A cmp esi,esp
0041199C call @ILT+495(__RTC_CheckEsp) (4111F4h)
pTest->VFun2();
004119A1 mov eax,dword ptr [pTest]
004119A4 mov edx,dword ptr [eax]
004119A6 mov esi,esp
004119A8 mov ecx,dword ptr [pTest]
004119AB mov eax,dword ptr [edx+4] // 行5
004119AE call eax
004119B0 cmp esi,esp
004119B2 call @ILT+495(__RTC_CheckEsp) (4111F4h)
通过对代码段1的观察我们可以发现:通过对象调用类的虚成员函数和调用非虚成员函数是相同的(对调用成员函数的汇编代码的分析可以看我的那篇《浅析C++中的this指针》)。也就是说,用对象是无法实现多态的。
下面主要来分析实现多态的代码段2。
行1、将pTest指针指向的地址前2个字(4个字节,也就是32位系统中一个指针的大小)的内容当成一个指针放到eax寄存器中
行2、将eax寄存器中的指针的值放入edx寄存器
行3、将dex寄存器中的指针的值放入eax寄存器
行4、调用eax寄存器指向的函数
这样分析似乎对怎样调用对象derived的虚函数VFun1()并不是很清楚。那么我们先来看下面的这张图:

这张图是一个假设的对象derived在内存中的内部布局图。指针pTest指向对象derived,而对象derived的前4个字节是一个虚表指针,指向虚函数表。
看着这张图再来分析上面的汇编代码就会清晰很多:
行1、取得虚表指针值放入eax寄存器中
行2、取得虚表指针的值放入edx寄存器中
行3、取得虚表指针指向的地址的值(也就是VFun1)放入eax寄存器中
行4、调用eax寄存器指向的函数
行5证明了上面图中对虚函数表的假设。第二个虚函数VFun2()的地址就是通过在第一虚函数VFun1()的地址加4(32位系统中一个指针的大小)而得到的。
通过上面的分析,可以得出C++中虚函数的调用方法:首先,取得对象中的虚表指针;然后,通过虚表指针找到相应的虚表;最后,通过在虚表内的偏移量找到相应的函数来调用。
下面通过分析类CDerived的非虚成员函数Fun2()来证明上面图中虚函数表指针的存在。

void CDerived::Fun2()
{
004118F0 push ebp
004118F1 mov ebp,esp
004118F3 sub esp,0CCh
004118F9 push ebx
004118FA push esi
004118FB push edi
004118FC push ecx
004118FD lea edi,[ebp-0CCh]
00411903 mov ecx,33h
00411908 mov eax,0CCCCCCCCh
0041190D rep stos dword ptr es:[edi]
0041190F pop ecx
00411910 mov dword ptr [ebp-8],ecx
m_iValue1 = 13;
00411913 mov eax,dword ptr [this] // 行6
00411916 mov dword ptr [eax+4],0Dh // 行7
m_iValue2 = 13;
0041191D mov eax,dword ptr [this]
00411920 mov dword ptr [eax+8],0Dh
}
00411927 pop edi
00411928 pop esi
00411929 pop ebx
0041192A mov esp,ebp
0041192C pop ebp
0041192D ret
上面是类CDerived的非虚成员函数Fun2()的汇编代码。可以看到,行6是将this指向的地址放入eax寄存器,而行7是给this指针指向的地址加4的地址赋值(具体的分析,可以看《浅析C++中的this指针》),而这个地址里面存放的是类CDerived的第一个成员变量。我们知道this指针是指向对象首地址的,那么为什么要给第一个成员变量赋值的时候要向后移动4个字节?答案是因为对象的前4个字节是用来存放虚表指针的。
下面的代码是《浅析C++中的this指针》一文中的不含虚函数的类的C++代码和编译后的汇编代码:

class CTest
{
public:
void SetValue();

private:
int m_iValue1;
int m_iValue2;
};

void CTest::SetValue()
{
m_iValue1 = 13;
m_iValue2 = 13;
}

void CTest::SetValue()
{
004117E0 push ebp
004117E1 mov ebp,esp
004117E3 sub esp,0CCh
004117E9 push ebx
004117EA push esi
004117EB push edi
004117EC push ecx
004117ED lea edi,[ebp-0CCh]
004117F3 mov ecx,33h
004117F8 mov eax,0CCCCCCCCh
004117FD rep stos dword ptr es:[edi]
004117FF pop ecx
00411800 mov dword ptr [ebp-8],ecx
m_iValue1 = 13;
00411803 mov eax,dword ptr [this] // 行8
00411806 mov dword ptr [eax],0Dh // 行9
m_iValue2 = 13;
0041180C mov eax,dword ptr [this]
0041180F mov dword ptr [eax+4],0Dh
}
00411816 pop edi
00411817 pop esi
00411818 pop ebx
00411819 mov esp,ebp
0041181B pop ebp
0041181C ret
通过行8、行9和行6、行7的比较就可以看出:类CTest的对象前4个字节存放的是自己的第一个成员变量;而类CDerived的对象从第5个字节开始才是存放的自己的第一个成员变量,它的前4个字节是用来存放虚表指针的。这再一次证明了上面图中对象内部布局的正确

看完上述内容是否对您有帮助呢?如果还想对相关知识有进一步的了解或阅读更多相关文章,请关注行业资讯频道,感谢您对的支持。

函数 指针 对象 代码 寄存器 成员 C++ 指向 地址 分析 编译 布局 字节 面的 变量 多态 也就是 图中 生成 不同 数据库的安全要保护哪些东西 数据库安全各自的含义是什么 生产安全数据库录入 数据库的安全性及管理 数据库安全策略包含哪些 海淀数据库安全审计系统 建立农村房屋安全信息数据库 易用的数据库客户端支持安全管理 连接数据库失败ssl安全错误 数据库的锁怎样保障安全 微软的服务器防护软件下载 软件开发主要学习什么 苏州阿里云服务器加固 苹果手机更换邮箱服务器 魔兽世界数据库7.0 对外软件开发需要哪学证 重庆卓欧互联网科技有限公司 广州新华互联网科技学校招生简章 在电脑桌面找不到速达软件服务器 服务器的声音自动关闭怎么打开 数据库如何保证服务器安全 苏州科贷互联网科技有限公司 郑州嵌入式软件开发收费报价表 2003 服务器没有响应 加强网络安全立法工作 网络安全专题宣传片 外网怎么访问ftp服务器 网络技术费专票可以抵税吗 scum服务器前期 万德数据库使用 虚拟机 数据库导入数据 春秋网络技术 西门子服务器rx2520规格 网络安全统一品牌的好处 网络安全人民是根本阅兵 软件开发能使自己什么提升 app软件开发要学哪些 网络技术什么是男生学的 网络安全日小结 软件开发信息系统安全的意义
0