一、前言

因为不同的运行环境的运行结果是不同的,特别是不同的编译器对c++类对象模型的实现是很可能存在差异,所以有时不同的编译平台的代码不能兼容也是部分原因于此。本文的运行环境是:

  • ubuntu16.04;
  • 编译器g++ (Ubuntu 5.4.0-6ubuntu1~16.04.12) 5.4.0 20160609

二、一些关于指针的知识

这篇文章事引用于这篇博客内容其中的虚函数表(vtbl)的内容及函数指针存放顺序,这里针对这部分进行了进一步的分析,为了方便理解本篇文章的内容,对于没基础的人,有必要看下前面这篇文章学习一些比较概念和基础知识。不过,这里补充一点关于指针的知识。

指针的类型

在64位机器上,无论任何指针都是占8个字节,8字节=64位,也就是一个机器码。假设有以下代码:

#include <iostream>
#include <typeinfo>
#include <array>
#include <string>using namespace std;class Person
{public:Person():mId(0), mAge(20){ ++sCount; }static int personCount(){return sCount;}virtual void print(){cout << "id: " << mId<< ", age: " << mAge << endl;}virtual void job(){cout << "Person" << endl;}virtual ~Person(){--sCount;cout << "~Person" << endl;}protected:static int sCount;int mId;int mAge;
};int main () {Person* person;int* pint;std::array<std::string, 5>* pta;std::cout << sizeof(person) << std::endl;std::cout << sizeof(pint) << std::endl;std::cout << sizeof(pta) << std::endl;std::cout << typeid(person).name() << std::endl;std::cout << typeid(pint).name() << std::endl;std::cout << typeid(pta).name() << std::endl;
}

输出结果:

如上代码所示,一个指向Person的指针,或是指向简单类型——整形——的指针int,还是指向template Aarry,以内存需求的观点来说,三种指针式没什么不同的!它们三个都需要足够的内存来放置一个机器地址(一般是8个字节)。换另一种说法,指针也是变量,它们都是一种占8个字节的变量,与普通变量无差别,只是这指针变量存放的内容是指针(其实就是机器地址)。
那它们不同类型的指针带来的影响是什么?”指向不同类型之各类指针”间的差异,既不在于其指针表示法(指针变量的命名)不同,也不在于其内容(代表一个地址)不同,而是在于其所寻址出来的object类型不同。也就是说,“指针类型”会教导编译器如何解释某个特定地址中的内存内容及其大小。
那么,一个指针的类型为void时,将涵盖怎样的地址空间呢?是的,我们不知道!这就是为什么一个类型为void的指针只能持有一个地址,而不能通过它操作它所指之object的缘故。(因为编译无法知道该给这个object寻址多大范围)。
所以,C++的四种cast操作:static_cast、dynamic_cast、const_cast和reinterpret_cast(具体含义和用法可以查看这篇文章),其实只是一种编译器指令。大部分情况下这些操作并不改变一个指针所含的真正地址,它只影响“被指出之内存的大小和其内容”的解释方式(理解这句话很重要,后面会用到)

元素类型为指针的一维数组

数组元素类型为指针情况下,元素地址与元素内容所指向地址的区别:

#include <iostream>
using namespace std;int main() {int* a[3];for (int i = 0; i < 3; ++i) {a[i] = new int{i};}for (int i = 0; i < 3; ++i) {cout << "数组第" << i << "元素的地址(从栈中分配的每个元素的地址):" << (a + i) << endl;}for (int i = 0; i < 3; ++i) {cout << "数组第" << i << "元素的内容(元素指向一个在堆中的指针):" << a[i] << endl;}cout << "数组名 a 是第一个元素的地址:" << a << endl;cout << "数组名的解引用( *a )是第一个元素的内容:" << *a << endl;
}

输出:

可见,我们有以下结论:

  • 虽然每个元素没有名称,但每个元素都有由系统从栈中分配的地址;
  • 每个元素的内容是我们从堆中申请的地址;
  • 数组名的第一个元素的地址;
  • 数组名的的解引用是第一个元素的内容;
  • 数组可以作加法运算,每次加运算的结果偏置一个元素类型的大小。

三、虚函数表(vtbl)的内容及虚成员函数指针存放原理

带有虚函数的c++类的内存模型

这里将带有虚函数的c++类的内存模型,是虚指针位于对象地址开头的模型,有的编译器实现不同于此,可能位于第一个类内存分布的尾部。假如有如下代码:


把虚指针放在开头的内存分布方式如下:

把虚指针放在末尾的内存分布方式:

这篇文章所用的编译器的内存分布实现把虚指针放在开头。
至于更详细的内容,这不做介绍,可以看开头的那篇文章或者找图片所在的这本书《深度探索c++对象模型》看看。

元素类型为普通函数指针的一维数组

#include <iostream>using namespace std;void fun1() { cout << "This is fun1" << endl; }
void fun2() { cout << "This is fun2" << endl; }
void fun3() { cout << "This is fun3" << endl; }// fun与FuncPtr等效
typedef void (*FuncPtr)();
using fun = void (*)();int main() {fun a[3];int** pa = reinterpret_cast<int**>(a);a[0] = &fun1;a[1] = &fun2;a[2] = &fun3;for (int i = 0; i < 3; ++i) {cout << "数组第" << i<< "元素的地址(从栈中分配的每个元素的地址):" << (a + i) << endl;fun func = (fun) * (a + i);func();}cout << endl;for (int i = 0; i < 3; ++i) {cout << "数组第" << i << "元素的内容(元素指向一个普通函数指针):"<< reinterpret_cast<int*>(a[i]) << endl;FuncPtr func = (FuncPtr)*a[i];func();}cout << endl;for (int i = 0; i < 3; ++i) {cout << "数组第" << i << "元素的地址(从栈中分配的每个元素的地址):"<< reinterpret_cast<int*>(pa) << endl;FuncPtr func = (FuncPtr)*pa;func();++pa;}
}

输出:
sas

上面的例子实现了一个一维数组存放了三个普通函数指针,并通过获取元素的内容获得函数指针,并运行获得函数功能。

从这个例子我们知道:

  • 函数指针同样可以强转换为一个普通的指针地址;
  • 函数指针通过转换为一个函数类型,可以正确执行函数体的内容;
  • 一维数组的的数组名可以按照指针大小进行偏置,从过在只知道数组首指针的情况下遍历所有元素内容,进而可以执行函数。
int** pa = reinterpret_cast<int**>(a);
  • 重点解释下这句语句的意思:

    • (1)对于一个数组而言,数组名是一个指向首地址的常量指针,所以不能对这里的数组名a进行++a操作,这样会改变a的值,编译肯定不通过,但采用a+i会产生一个临时变量,所以可行。
    • (2)我们也可以把这个数组名代表的地址复制给一个自由变量,代码里的pa就是这个自由变量,因此可以++pa
  • 上面这两点分别对应第三节的两种解释,由于虚函数表的其实是不存在数组名的,所以可以进行(1)种里操作。

int** pa = (int**)a;int**即指针的指针,应该这么理解,因为数组名指向数组的首地址,所以数组名是一个指针类型,而数组的元素内容是指针,所以这里的指针的指针可以翻译为,一个变量为指针类型,这个变量指向一个指针类型:(int*)(*)

虚函数表是一个元素类型为虚函数指针的一维数组

如标题所示,虚函数表可以理解为一个元素类型为虚函数指针的一维数组,所以这个一维数组的遍历应该和上一小节的元素类型为普通函数指针的一维数组遍历方式思路一致。
其次,虚指针和虚函数表的概念看开头文章的介绍,这里只是简单贴出带有虚指针和虚函数表的简略模型,但是这个模型并不完全正确,但在本文的代码实验下是正确的(或者说这个模型的正确性仅在本文简单代码例子下条件性成立),如下:

这里提供两种虚函数表的每个虚函数指针(元素)的遍历方法和解释。

第一种解释,虚函数指针vptr的指向的地址就是数组名,直接使用数组名操作

#include <iostream>
#include <typeinfo>
using namespace std;class Person {public:Person() : mId(0), mAge(20) { ++sCount; }static int personCount() { return sCount; }virtual void print() { cout << "id: " << mId << ", age: " << mAge << endl; }virtual void job() { cout << "Person" << endl; }virtual ~Person() {--sCount;cout << "~Person" << endl;}protected:static int sCount;int mId;int mAge;
};
int Person::sCount = 0;typedef void (*FuncPtr)();int main() {Person person;int64_t** vptr = reinterpret_cast<int64_t**>(&person);int64_t* vtbl = *vptr;jieshfor (int i = 0; i < 3; ++i) {FuncPtr func = (FuncPtr) * (vtbl + i);func();}// 由于虚函数表没有数组名之说,所以是可以用首地址自增的,运行结果与上面的用法一样// for (int i = 0; i < 3; ++i) {//   FuncPtr func = (FuncPtr) * (vtbl);//   func();//   ++vtbl;// }// 以数组的形式调用// for (int i = 0; i < 3; ++i) {//   FuncPtr func = (FuncPtr)(vtbl[i]);//   func();// }cout << "!!!!!!!!!!!!!!" << endl;return 0;
}

输出:

这一种用法相当于直接操作数组名,由于虚函数表没有数组名之说,所以是可以用首地址自增的, 同时也能用(vtbl + i)进行偏置操作
代码分析:

long** vptr = reinterpret_cast<long**>(&person);
  • 根据上面所说的,对象的首地址是虚指针vptr的地址,所以我们取的对象地址就是虚指针的地址;(不要混淆了,vptr只是一个指针类型的变量,它的地址是person的首地址,而vptr的内容才是虚函数表的地址)
  • 根据前面指针的类型的解释,我们已经知道&person在编译器的解释为一个Person对象,再根据前面一维数组对int** 的分析,不难理解为什么我们需要调用reinterpret_cast让编译器重新解释vptr这段地址为int64_t** 类型;
long* vtbl = *vptr;
  • 根据一维数组的首地址是第一个元素的地址,因此vptr是第一个元素的地址,而vptr的指向地址才是虚函数表的地址。因此,*vptr就是虚函数表的第一个元素的地址,同时这个首指针指向的是一个虚函数指针。
for (int i = 0; i < 3; ++i)
{FuncPtr func = (FuncPtr)*vtbl;func();++vtbl;
}
  • 这里解释下,为什么要把指针类型定义为int64_t,而不是int,不然程序编译时会有:
 warning: cast to pointer from integer of different size [-Wint-to-pointer-cast]FuncPtr func = (FuncPtr) * (vtbl + i);
  • 运行时程序崩溃:
Segmentation fault (core dumped)
  • 因为int64_t是八个字节,而int是4个字节,在这一行语法(FuncPtr)*vtbl;里,funcptr是一个八个字节的指针,而*vtbl是一个整数,这个整数是函数地址的整数转换值,如果用int,根据我们前面说的指针的类型会教导编译器解释某个特定地址中的内存内容及其大小。用int的话,编译器将转换4个字节大小的地址,然而,指针的地址是8个字节,这样的转换是不完整的,将引发内存出错!!!所以这里必须是一个8字节的int64_t。
  • 对*vptr解引用就是第一个元素的内容,内容是一个虚函数地址,经由FuncPtr func = (FuncPtr)*vtbl;我们将这个虚函数地址转化为一个可调用对象。
  • 这里的++vtbl就是数组元素地址偏置一个元素类型大小(这里偏置一个指针类型的大小,8个字节),每进行一次偏置后进行解引用(*vtbl;)就能获得元素的内容(函数地址),进而调用func();执行函数。

第二种,把虚函数指针vptr的指向的地址,再转换为一个指针的指针后再操作

#include <iostream>
#include <typeinfo>
using namespace std;class Person {public:Person() : mId(0), mAge(20) { ++sCount; }static int personCount() { return sCount; }virtual void print() { cout << "id: " << mId << ", age: " << mAge << endl; }virtual void job() { cout << "Person" << endl; }virtual ~Person() {--sCount;cout << "~Person" << endl;}protected:static int sCount;int mId;int mAge;
};
int Person::sCount = 0;typedef void (*FuncPtr)();int main() {Person person;int** vptr = reinterpret_cast<int**>(&person);int** vtbl = reinterpret_cast<int**>(*vptr);for (int i = 0; i < 3; ++i) {FuncPtr func = (FuncPtr)*vtbl;func();++vtbl;}// 以数组的形式调用// for (int i = 0; i < 3; ++i) {//   FuncPtr func = (FuncPtr)(vtbl[i]);//   func();// }cout << "!!!!!!!!!!!!!!" << endl;return 0;
}

这种用法相当于把数组名转换为一个自由变量的用法。
输出:

代码分析:

int** vptr = reinterpret_cast<int**>(&person);

这行含义与第一种的一样。

int** vtbl = reinterpret_cast<int**>(*vptr);

这里可以用int的原因是,*vptr的结果是指针,是8个字节。
这一行把虚函数表的首地址转化为一个,指向指针的指针变量,相当于把前面的把数组名转化为一个自由变量的操作。

    for (int i = 0; i < 3; ++i){FuncPtr func = (FuncPtr)*vtbl;func();++vtbl;}

对自由变量进行自增操作,并执行函数。

四、本文潜在问题阐述

至此,完整的说明了虚函数表是一个一位数组了。对比两种解释,第二种是正确的用法,第一种是利用数组自增时,偏置元素类型的大小。不过,对于在一片连续的地址里,对于编译器,这两种操作应当是一致的?

但是,就像前面说的,这里虚函数表的这个模型不是完全正确的:

(1)虚函数是类的成员函数,所以我们调用虚函数需要传递一个对象的指针给它,但这里我们没有传递也能成功调用,这是为什么?
(2)虚函数表并不是只有虚函数的地址,还有对象类型信息,以及其他的一些信息,这些在虚函数表的位置和用法其实在本文并没有涉及。

C++如何获取虚函数表(vtbl)的内容及虚成员函数指针存放原理相关推荐

  1. C++ 虚函数详解(虚函数表、vfptr)——带虚函数表的内存分布图

    前言 总所周知,虚函数是实现多态的基础. 引用或指针的静态类型与对象本身的动态类型的不同,才是C++支持多态的根本所在. 当使用基类的引用或指针调用一个虚函数成员时,会执行动态绑定. 所有的虚函数都必 ...

  2. java 虚函数表_啊!Java虚方法

    什么是Java的虚方法呢,我们首先看看什么是虚函数 虚函数 百度百科的解释为: 在某基类中声明为 virtual 并在一个或多个派生类中被重新定义的[成员函数],用法格式为:virtual 函数返回类 ...

  3. C++中的虚函数表介绍

            在C++语言中,当我们使用基类的引用或指针调用一个虚成员函数时会执行动态绑定.因为我们直到运行时才能知道到底调用了哪个版本的虚函数,所以所有虚函数都必须有定义.通常情况下,如果我们不使 ...

  4. C++之多态 虚函数表

    多态 多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为. 需要区分一下:1.菱形虚拟继承,是在继承方式前面加上virtual: class Person {}; class Studen ...

  5. Cpp 对象模型探索 / 虚函数表和虚函数表指针的创建时机

    一.虚函数表 在编译期间创建.编译器会为每个类确定好虚函数表(vtbl)的内容. 二.虚函数表指针 虚函数表指针跟随着对象,在运行期间创建.由于在编译期间编译器为每个类创建好了 vtbl,并且编译器会 ...

  6. C++多态:多态实现原理剖析,虚函数表,评价多态,常见问答与实战【C++多态】(55)

    虚函数表 一般继承(无虚函数覆写) 一般继承( 有虚函数覆写) 静态代码发生了什么 评价多态 常见问答与实战 问答 为什么虚函数必须是类的成员函数? 为什么类的静态成员函数不能为虚函数? 为什么构造函 ...

  7. C++虚函数与虚函数表

    多态性可分为两类:静态多态和动态多态.函数重载和运算符重载实现的多态属于静态多态,动态多态性是通过虚函数实现的. 每个含有虚函数的类有一张虚函数表(vtbl),表中每一项是一个虚函数的地址, 也就是说 ...

  8. 通过对象指针的方式强行指定到子类_C++中的虚指针与虚函数表

    ​ 最近在逛B站的时候发现有候捷老师的课程,如获至宝.因此,跟随他的讲解又复习了一遍关于C++的内容,收获也非常的大,对于某些模糊的概念及遗忘的内容又有了更深的认识. 以下内容是关于虚函数表.虚函数指 ...

  9. C++——Hook教程[1]:虚函数表(VMT)Hook

    前言 虚函数表(VMT)Hook,又叫指针重定向,是一种常见的Hook技术,在游戏外挂程序中最常见.例如,使用VMTHook在Direct3D / OpenGL引擎游戏里实现内置叠加层. 虚函数表(V ...

最新文章

  1. Centos7单端口单配置文件多IP
  2. ML之PPMCC:PPMCC皮尔逊相关系数(Pearson correlation coefficient)、Spearman相关系数的简介、案例应用之详细攻略
  3. python3--迭代器
  4. mysql dump hbase_导出mysqldump数据
  5. java 怎么调用clojure_从java调用Clojure时Clojure状态的范围
  6. iec61508最新2020_IEC61508标准
  7. 如何提取PDF文件中的图片
  8. linux 查看java_opts_Linux Tomcat 设置 JAVA_OPTS 异常
  9. 填空什么的月牙_部编一年级上册语文第四单元知识梳理填空,附答案
  10. mysql自定义函数实现
  11. (zhuan) 自然语言处理中的Attention Model:是什么及为什么
  12. 快手AI实验室Y-tech招聘暑期算法实习生
  13. 《桃花庵歌》- 唐寅
  14. 基于爬虫的数据分析--Python3抓取网易云音乐原理及实践
  15. Source Code Pro字体使用
  16. TortoiseGit的使用详解
  17. 选择测径仪 13点注意事项
  18. 【编程开发】之短信注册用户流程
  19. android读取本地网页
  20. excel操作技巧:聊聊关于打印的一些事儿

热门文章

  1. 国际十大炒黄金期货正规平台排名(2023精选榜)
  2. 阿愚呱呱作为一个非技术人员,是如何做到不到3年时间成为RPA行业的一个头部IP的?
  3. unity调用安卓手机物理返回键和home键
  4. 修改.srt格式字幕文件
  5. 机器学习——周志华读书笔记
  6. elasticsearch7.x catAPI之shards
  7. 运维危险操作之windows server打开或关闭windows功能
  8. 台式计算机外观组成部分,台式电脑由哪些部分组成?
  9. 项目实训--Unity多人游戏开发(十六、草丛隐身与道具隐身)
  10. 阿昆同学的Java学习日记Day1