C++对象模型3——vptr的位置、手动调用虚函数、从汇编代码看普通调用和多态调用
一、vptr的位置
class test
{
public:int i; virtual void testfunc() {}
};int main()
{test a;char* p1 = reinterpret_cast<char*>(&a); char* p2 = reinterpret_cast<char*>(&(a.i)); if (p1 == p2) { //如果a.i和a的地址相同,则成员变量i在a对象内存的开头位置,那么虚函数表指针在i的后面位置cout << "虚函数表指针位于对象内存的末尾" << endl;}else {cout << "虚函数表指针位于对象内存的开头" << endl; //本条件会成立}return 0;
}
test的对象模型就是这样的
二、手动调用虚函数
class Base
{
public:virtual void f() {cout << "Base::f()" << endl; }virtual void g() { cout << "Base::g()" << endl; }virtual void h() { cout << "Base::h()" << endl; }
};
class Derive : public Base {
public:void g() {cout << "Derive::g()" << endl; }
};
上述代码中基类和子类的虚函数表如下
可以看出,虚表中除了包含虚函数的函数指针,而且还包括了用于RTTI的typeinfo,而且vptr默认并不使指向虚表的第一个元素,而是指向第一个虚函数,而且虚函数的返回值从void变成了int
基于对象模型和虚表结构,可以手动获取vptr并手动调用虚函数
typedef void (*Func)();
int main()
{Derive* d = new Derive(); long* dvptr = reinterpret_cast<long*>(d); long* dvfuncptr = reinterpret_cast<long*>(*dvptr);for (int i=0;i<3;++i) {((Func)dvfuncptr[i])();}Base* b = new Base();long* bvptr = (long*)b;long* bvfuncptr = (long*)(*bvptr);for (int i=0;i<3;++i) {((Func)bvfuncptr[i])();}return 0;
}
上述代码中,主要是这两行代码
long* dvptr = reinterpret_cast<long*>(d);
long* dvfuncptr = reinterpret_cast<long*>(*dvptr);
因为vptr在类对象的最开头,占8个字节,所以将Derive*强转成long*,此时解引用就能得到对象的前8个字节的内容,也就是derive对象的vptr。因为有三个虚函数,每个虚函数指针占8个字节,所以再将vptr强转为long*,此时得到的结果就是第一个虚函数的地址。
根据虚表内容和分析结果:derive和base的对象内存模型如下
三、从汇编代码看普通调用和多态调用
int main()
{Derive d;Base b=d;b.g();Base *pb=new Derive();pb->g();return 0;
}
pb将能在编译时期做有以下两点:1、判定Base中函数的权限。2、根据访问权限,调用Base中的可用接口。也就是说,在main中,pb在编译期只能够调用Base的public接口。
第七行对g()的调用会被编译期转化为*(this->vptr[1])(this),在编译期可以确定的就是vptr指向了一个vtbl,而且知道g()在vtbl中的第二个位置(也就是知道g的索引),不确定的就是调用的到底是那个类vtbl中的g(),这一点需要在运行时确定。这就是所谓运行时多态
上述代码的对应的部分汇编代码如下
主要是三个红框中的call,第一个对应的是静态调用,直接调用Base::g(),Base::g()的地址在编译期就已经确定(已经被写死),是c72;第二个是创建Derive对象,也是在编译器进行的。而第三个最终调用的是64位累加寄存器RAX存储的函数指针,而RAX中的地址在编译器无法确定,是个变化的值。
所以,只能在运行期计算后得到RAX中的地址值。这就是为啥多态调用需要在运行期确定具体的调用函数,因为累加寄存器中RAX中的地址值是变化的,在运行期才能确定。也正因为静态调用的函数地址直接被写死,而多态调用需要在运行期确定具体的调用函数,所以,静态调用一般要比多态调用速度更快
这就是为什么在C++中被指定的对象的真实类型在每一个特定执行点之前,是无法在编译器解析的,只有通过指针和引用才能完成。相反,如果处理的只是一个类型的实例,它在编译时期就已经完全定义好了。
参考
《深度探索C++对象模型》
《C++新经典:对象模型》
欢迎大家评论交流,作者水平有限,如有错误,欢迎指出
C++对象模型3——vptr的位置、手动调用虚函数、从汇编代码看普通调用和多态调用相关推荐
- C++核心准则C.82:不要在构造函数或析构函数中调用虚函数
C.82: Don't call virtual functions in constructors and destructors C.82:不要在构造函数或析构函数中调用虚函数 Reason(原因 ...
- 中有atoi函数吗_C++ 多态的实现及原理,深挖vptr指针,手动调用虚函数
什么是多态? 父类指针即根据指向的不同对象,响应同一消息(函数调用),产生不同行为. 多态三要素? 1,继承 2,虚函数重写 3,父类指针(引用)指向子类对象 多态的实现很简答,让我们来看一段代码 # ...
- C++对象模型8——构造函数和析构函数中对虚函数的调用、全局对象构造和析构、局部static数组的内存分配
一.构造函数和析构函数中对虚函数的调用 仍然以https://blog.csdn.net/Master_Cui/article/details/109957302中的代码为例 base3构造函数和析构 ...
- 为什么构造函数不能声明为虚函数,析构函数可以,构造函数中为什么不能调用虚函数?
为什么构造函数不能声明为虚函数,析构函数可以,构造函数中为什么不能调用虚函数 构造函数中为什么不能调用虚函数? 第一个理由是概念上的 第二个理由是机械上的. 构造函数不能声明为虚函数的原因是 1 构造 ...
- C++中最好不要在构造函数和析构函数中调用虚函数!!!
1.最好不要在基类和派生类的构造和析构函数中调用虚函数,不会出现多态性 实例如下: #include "iostream"using namespace std;class Bas ...
- 构造函数中不应调用虚函数
今天调试程序,遇到一个很费解的问题,现在做个记录: class CS3Adapter : public CBaseAdapter 类CS3Adapter继承于CBaseAdapter,其中 CBase ...
- C# 构造函数中调用虚函数
C# 构造函数中调用虚函数 using System; using System.Diagnostics; using System.Text; using System.Collections; u ...
- C++学习笔记-----不要在构造函数和析构函数中调用虚函数
考虑下面的程序: #include <iostream> using namespace std;class Base { public:Base() { cout << &q ...
- C++中最好不要在构造函数和析构函数中调用虚函数
1.最好不要在基类和派生类的构造和析构函数中调用虚函数,不会出现多态性 实例如下: #include "iostream"using namespace std;class Bas ...
最新文章
- MySQL的编译安装
- 在“DNS管理器”中手工增加DNS主机(A)或者别名(CNAME)记录时,出现被拒绝的错误...
- HBase+Phoenix整合入门--集群搭建
- MSP430学习小结3-MSP430基本时钟模块
- 熟练操作计算机办公软件英语怎么说,办公软件用英语怎么说英文表达
- PMP-项目沟通管理
- java用下划线分开字母和数字_数字文字中的Java 7下划线
- 21天攻克PET核心词汇,加油!
- 能量原理与变分法笔记06:高阶导数的变分问题(包含函数的高阶导数)
- arm交叉编译ntpdate与服务器进行时间同步
- 山东高速资产注入承诺何时兑现 期待画饼成真
- 寺庙公众号开发:vue实现祈福牌位的前端部分
- ABAP-SAP 账号批量创建分配权限程序
- 跟Java面试官对线的一天!唬住就要50K,唬不住就要5K
- Spring Init Destory
- 高德足迹点Android,高德地图怎么点亮城市 足迹地图查看方法
- 数据可视化--实验五:高维非空间数据可视化
- 【光通信】常见光模块与光纤收发器说明及作用区别
- w10投影全屏设置_win10投影怎么全屏显示,投影和电脑同时显示
- C2000 系列DSP使用Syscfg配置CLB模块记录