一、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的位置、手动调用虚函数、从汇编代码看普通调用和多态调用相关推荐

  1. C++核心准则C.82:不要在构造函数或析构函数中调用虚函数

    C.82: Don't call virtual functions in constructors and destructors C.82:不要在构造函数或析构函数中调用虚函数 Reason(原因 ...

  2. 中有atoi函数吗_C++ 多态的实现及原理,深挖vptr指针,手动调用虚函数

    什么是多态? 父类指针即根据指向的不同对象,响应同一消息(函数调用),产生不同行为. 多态三要素? 1,继承 2,虚函数重写 3,父类指针(引用)指向子类对象 多态的实现很简答,让我们来看一段代码 # ...

  3. C++对象模型8——构造函数和析构函数中对虚函数的调用、全局对象构造和析构、局部static数组的内存分配

    一.构造函数和析构函数中对虚函数的调用 仍然以https://blog.csdn.net/Master_Cui/article/details/109957302中的代码为例 base3构造函数和析构 ...

  4. 为什么构造函数不能声明为虚函数,析构函数可以,构造函数中为什么不能调用虚函数?

    为什么构造函数不能声明为虚函数,析构函数可以,构造函数中为什么不能调用虚函数 构造函数中为什么不能调用虚函数? 第一个理由是概念上的 第二个理由是机械上的. 构造函数不能声明为虚函数的原因是 1 构造 ...

  5. C++中最好不要在构造函数和析构函数中调用虚函数!!!

    1.最好不要在基类和派生类的构造和析构函数中调用虚函数,不会出现多态性 实例如下: #include "iostream"using namespace std;class Bas ...

  6. 构造函数中不应调用虚函数

    今天调试程序,遇到一个很费解的问题,现在做个记录: class CS3Adapter : public CBaseAdapter 类CS3Adapter继承于CBaseAdapter,其中 CBase ...

  7. C# 构造函数中调用虚函数

    C# 构造函数中调用虚函数 using System; using System.Diagnostics; using System.Text; using System.Collections; u ...

  8. C++学习笔记-----不要在构造函数和析构函数中调用虚函数

    考虑下面的程序: #include <iostream> using namespace std;class Base { public:Base() { cout << &q ...

  9. C++中最好不要在构造函数和析构函数中调用虚函数

    1.最好不要在基类和派生类的构造和析构函数中调用虚函数,不会出现多态性 实例如下: #include "iostream"using namespace std;class Bas ...

最新文章

  1. MySQL的编译安装
  2. 在“DNS管理器”中手工增加DNS主机(A)或者别名(CNAME)记录时,出现被拒绝的错误...
  3. HBase+Phoenix整合入门--集群搭建
  4. MSP430学习小结3-MSP430基本时钟模块
  5. 熟练操作计算机办公软件英语怎么说,办公软件用英语怎么说英文表达
  6. PMP-项目沟通管理
  7. java用下划线分开字母和数字_数字文字中的Java 7下划线
  8. 21天攻克PET核心词汇,加油!
  9. 能量原理与变分法笔记06:高阶导数的变分问题(包含函数的高阶导数)
  10. arm交叉编译ntpdate与服务器进行时间同步
  11. 山东高速资产注入承诺何时兑现 期待画饼成真
  12. 寺庙公众号开发:vue实现祈福牌位的前端部分
  13. ABAP-SAP 账号批量创建分配权限程序
  14. 跟Java面试官对线的一天!唬住就要50K,唬不住就要5K
  15. Spring Init Destory
  16. 高德足迹点Android,高德地图怎么点亮城市 足迹地图查看方法
  17. 数据可视化--实验五:高维非空间数据可视化
  18. 【光通信】常见光模块与光纤收发器说明及作用区别
  19. w10投影全屏设置_win10投影怎么全屏显示,投影和电脑同时显示
  20. C2000 系列DSP使用Syscfg配置CLB模块记录

热门文章

  1. App-IOS与Android弱网环境测试
  2. Redis持久存储-AOFRDB
  3. Spark环境搭建(一)-----------HDFS分布式文件系统搭建
  4. getReadableDatabase与getWritableDatabase的区别
  5. CSS3的边框(border)属性-radius
  6. iframe父子页面交互
  7. JAVA数据结构 线性表的链式存储及其实现
  8. Windows Phone 7 隔离存储空间“.NET研究”资源管理器
  9. 利用PermutationImportance挑选变量
  10. 暴 雨 雲 于 7月17日