C++虚函数和虚函数表原理

2023-05-16

虚函数的地址存放于虚函数表之中。运行期多态就是通过虚函数和虚函数表实现的。

类的对象内部会有指向类内部的虚表地址的指针。通过这个指针调用虚函数。

虚函数的调用会被编译器转换为对虚函数表的访问:

ptr->f();   //ptr代表this指针,f是虚函数
*(ptr->vptr[1])(ptr);

上述代码中,ptr代表一个this指针,ptr指向的vptr是类内部的虚表指针。这个虚表指针会被放在类的最前方(VS2017),1就是虚函数指针在虚函数表中的索引值。在这个索引值表示的虚表的槽中存放的就是f()的地址。

虚表指针的名字也会被编译器更改,所以在多继承的情况下,类的内部可能存在多个虚表指针。通过不同的名字被编译器标识。

虚函数表中可能还存在其他的内容,如用于RTTI的type_info类型。或者直接将虚基类的指针存放在虚表中。

压制多态可以通过域操作符进行。

class A1
{
public:
    virtual void f() { cout << "A1::f" << endl; }
};
class C : public A1
{
public:
    virtual void f() { cout << "C::f" << endl; }
};
c.A1::f();  //A1::f
c.f();  //C::f

单继承

这种情况下,派生类中仅有一个虚函数表。这个虚函数表和基类的虚函数表不是一个表(无论派生类有没有重写基类的虚函数),但是如果派生类没有重写基类的虚函数的话,基类和派生类的虚函数表指向的函数地址都是相同的。

class A1
{
public:
    A1(int _a1 = 1) : a1(_a1) { }
    virtual void f() { cout << "A1::f" << endl; }
    virtual void g() { cout << "A1::g" << endl; }
    virtual void h() { cout << "A1::h" << endl; }
    ~A1() {}
private:
    int a1;
};
class C : public A1
{
public:
    C(int _a1 = 1, int _c = 4) :A1(_a1), c(_c) { }
    //virtual void f() { cout << "C::f" << endl; }
    //virtual void g() { cout << "C::g" << endl; }
    //virtual void h() { cout << "C::h" << endl; }
private:
    int c;
};

类C没有重写A的虚函数,所以虚函数表内部的情况如下:

这里写图片描述

可以看出,两个类的__vfptr的值不同,但是每个槽内部的函数地址都是相同的。

这里写图片描述

如果类C中重写了A类中的函数:

class C : public A1
{
public:
    C(int _a1 = 1, int _c = 4) :A1(_a1), c(_c) { }
    virtual void f() { cout << "C::f" << endl; }
    virtual void g() { cout << "C::g" << endl; }
    virtual void h() { cout << "C::h" << endl; }
private:
    int c;
};

这里写图片描述

那么就会覆盖A类的虚函数,重写一部分就会覆盖一部分,重写全部就会覆盖全部。

这里写图片描述

如果C中重新写了一些别的虚函数,那么这些虚函数将排在父类的后面,这里编译器无法显示,可以通过打印虚表来进行。

打印的过程比较简单,通过访问类C的前8字节(64位编译器)找到虚函数表,再一次遍历虚函数表即可。虚函数表最后一项用的是0,代表虚函数表结束。

C c;
long long *p = (long long *)(*(long long*)&c);
typedef void(*FUNC)();        //重定义函数指针,指向函数的指针
void PrintVTable(long long* vTable)  //打印虚函数表
{
    if (vTable == NULL)
    {
        return;
    }
    cout << "vtbl:" << vTable << endl;
    int  i = 0;
    for (; vTable[i] != 0; ++i)
    {
        printf("function : %d :0X%x->", i, vTable[i]);
        FUNC f = (FUNC)vTable[i];
        f();         //访问虚函数
    }
    cout << endl;
}

通过这样的打印可以得知C的虚函数表为:

vtbl:00007FF6CD2CBE68
function : 0 :0Xcd2c115e->A1::f
function : 1 :0Xcd2c146a->A1::g
function : 2 :0Xcd2c1113->A1::h

vtbl:00007FF6CD2CBEA8
function : 0 :0Xcd2c115e->A1::f
function : 1 :0Xcd2c146a->A1::g
function : 2 :0Xcd2c1113->A1::h
function : 3 :0Xcd2c1023->C::f
function : 4 :0Xcd2c132a->C::g
function : 5 :0Xcd2c11d1->C::h

具体的图解为:

这里写图片描述

多继承

多继承情况下,派生类中有多个虚函数表,虚函数的排列方式和继承的顺序一致。派生类重写函数将会覆盖所有虚函数表的同名内容,派生类自定义新的虚函数将会在第一个类的虚函数表的后面进行扩充

class A1
{
public:
    A1(int _a1 = 1) : a1(_a1) { }
    virtual void f() { cout << "A1::f" << endl; }
    virtual void g() { cout << "A1::g" << endl; }
    virtual void h() { cout << "A1::h" << endl; }
    ~A1() {}
private:
    int a1;
};
class A2
{
public:
    A2(int _a2 = 2) : a2(_a2) { }
    virtual void f() { cout << "A2::f" << endl; }
    virtual void g() { cout << "A2::g" << endl; }
    virtual void h() { cout << "A2::h" << endl; }
    ~A2() {}
private:
    int a2;
};
class A3
{
public:
    A3(int _a3 = 3) : a3(_a3) { }
    virtual void f() { cout << "A3::f" << endl; }
    virtual void g() { cout << "A3::g" << endl; }
    virtual void h() { cout << "A3::h" << endl; }
    ~A3() {}
private:
    int a3;
};

class B : public A1, public A2, public A3
{
public:
    B(int _a1 = 1, int _a2 = 2, int _a3 = 3, int _b = 4) :A1(_a1), A2(_a2), A3(_a3), b(_b) { }
    virtual void f1(){ cout << "B::f" << endl; }
    virtual void g1(){ cout << "B::g" << endl; }
    virtual void h1(){ cout << "B::h" << endl; }
private:
    int b;
};

这里通过编译器的部分可以看出来,未被重写的虚函数指针将和基类指向同一个位置,一旦被重写,函数指针就指向新的位置。

这里写图片描述

在B类中,函数指针指向的位置不变:

这里写图片描述

而这时候B类中第一个虚函数表已经增加了新的项,从打印结果可知。

vtbl:00007FF7DD62BF38
function : 0 :0Xdd621177->A1::f
function : 1 :0Xdd621497->A1::g
function : 2 :0Xdd621127->A1::h

vtbl:00007FF7DD62BF78
function : 0 :0Xdd6212df->A2::f
function : 1 :0Xdd62105f->A2::g
function : 2 :0Xdd6213fc->A2::h

vtbl:00007FF7DD62BFB8
function : 0 :0Xdd621032->A3::f
function : 1 :0Xdd62129e->A3::g
function : 2 :0Xdd621221->A3::h

vtbl:00007FF7DD62BFF8
function : 0 :0Xdd621177->A1::f
function : 1 :0Xdd621497->A1::g
function : 2 :0Xdd621127->A1::h
function : 3 :0Xdd62144c->B::f
function : 4 :0Xdd621019->B::g
function : 5 :0Xdd62133e->B::h

而如果B类重写了函数,那么打印结果将是:

vtbl:00007FF720C8BF38
function : 0 :0X20c8117c->A1::f
function : 1 :0X20c814b5->A1::g
function : 2 :0X20c8112c->A1::h

vtbl:00007FF720C8BF78
function : 0 :0X20c812f8->A2::f
function : 1 :0X20c8105a->A2::g
function : 2 :0X20c8141a->A2::h

vtbl:00007FF720C8BFB8
function : 0 :0X20c8102d->A3::f
function : 1 :0X20c812b2->A3::g
function : 2 :0X20c81230->A3::h

vtbl:00007FF720C8BFF8
function : 0 :0X20c814ab->B::f
function : 1 :0X20c81370->B::g
function : 2 :0X20c81393->B::h

并且此时B类的信息为:

这里写图片描述

从编译器给出的信息我们可以看到在第二个虚函数表中有adjustor{16}的字样,这就是A类的大小,也就是说,这就是告诉编译器,需要进行16字节的偏移(thunk技术)。这就引出了接下来的一个问题:

B类用不同的基类指针指向的时候,运行的是不同的基类中的虚函数(这就是多态的表现),这里可以知道,当A2类指针指向B的时候,虚函数指针是自动跳到B类中A2类所在的地方的,这个跳转是怎么进行的呢?

首先在编译期,就可以知道一个指针需要偏移多少个字节:

A2 *p = new B;

编译器会将这个代码改为:

B *tmp = new B;
A2 *p = tmp ? tmp + sizoef(A1) : 0;

经过这样的调整A1,A2,A3都会指向正确的类的位置。但是这样还不够。

由上面的编译器信息图我们可以知道,当B类重写了函数之后,A2,A3的虚函数表所指对象已经不再是简单的函数指针了,而是一个thunk对象。这就是C++的thunk技术。

所谓的thunk就是一段汇编代码,这段汇编代码可以以适当的偏移值来调整this指针以跳到对应的虚函数中去,并调用这个函数,也就是说当使用A1的指针指向B的对象,不需要发生偏移,而使用A2的指针指向B则需要进行偏移sizeof(A1)个字节。并跳转到A1中的函数来执行。这就是通过thunk的jmp指令跳转到这个函数。

所以具体的虚函数表中的情况如下:

  1. 如果两个基类中的虚函数名字不同,派生类只重写了第二个基类的虚函数,则不会产生thunk用以跳转。
  2. 如果基类中虚函数名字相同,派生类如果重写,将会一次性重写两个基类的虚函数,这时候第二个基类的虚函数表中存放的就是thunk对象,当指针指向此处的时候,会自动跳转到A类的对应虚函数(已经被B重写)执行。
  3. 第一个基类的虚函数被重写与否都不会产生thunk对象,因为这个类是被别的基类指针跳转的目标,而这个类的指针施行多态的时候是不会发生跳转的。
  4. 派生类的重新定义的虚函数将会排在第一个虚函数表内部A1虚函数的后面,但是当A2调用这个函数的时候,会通过thunk技术跳转回第一个类的虚函数表以执行相对应的虚函数。
  5. 除了第一个基类的虚析构函数,其他基类的析构函数都是thunk对象。

综上所述,thunk对象用于所有基类都被派生类重写后,调用虚函数将跳到最开始的基类部分。或者派生类中定义的虚函数也会跳转到第一个基类的虚函数表中。而仅出现在后面的基类的虚函数表中的虚函数,无论被重写与否都不会产生thunk对象。因为这里不会在第一个基类中由对应的虚函数指针。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

C++虚函数和虚函数表原理 的相关文章

  • 响应式布局之viewport-超级简单

    之前文章CSS布局之详解 故里2130的博客 CSDN博客 上面的文章可以实现响应式布局 xff0c 根据浏览器的大小变化而变化 xff0c 但是相对于viewport来说 xff0c 之前的还是有点复杂 xff0c 而使用viewport
  • .net6API使用AutoMapper和DTO

    AutoMapper xff0c 是一个转换工具 xff0c 说到AutoMapper时 xff0c 就不得不先说DTO xff0c 它叫做数据传输对象 Data Transfer Object 通俗的来说 xff0c DTO就是前端界面需
  • 手机/移动端的UI框架-Vant和NutUI

    下面推荐2款手机 移动端的UI框架 其实还有很多的框架 xff0c 各个大厂都有UI框架 目前 xff0c 找来找去 xff0c 只有腾讯的移动端是setup语法写的TDesign xff0c 其他大厂 xff0c 虽然都是VUE3写的 x
  • 使用uniapp创建小程序和H5界面

    uniapp的介绍可以看官网 xff0c 接下来我们使用uniapp创建小程序和H5界面 xff0c 其他小程序也是可以的 xff0c 只演示创建这2个 xff0c 其实都是一套代码 xff0c 只是生成的方式不一样而已 uni app官网
  • 使用NutUI创建小程序和H5界面

    做开发的时间长了 xff0c 技术都是通用的 xff0c 创建小程序和H5界面有很多的UI xff0c 本章节演示使用NutUI来创建 xff0c 官网 xff0c NutUI 移动端 Vue3 小程序组件库 1 使用HBuilder X创
  • 如何开发微信小程序呢

    也许很多人对小程序 xff0c H5程序 xff0c Vue xff0c 网页程序 xff0c PC端程序认识比较模糊 xff0c 因为这些跨度非常的大 xff0c 很少人会一次性全部接触 xff0c 甚至只是听说过 xff0c 并不了解其
  • .NET6中使用GRPC详细描述

    Supported languages gRPC xff0c 官网 至于原理就不说了 xff0c 可以百度原理之后 xff0c 然后再结合代码 xff0c 事半功倍 xff0c 就能很好理解GRPC了 目录 一 简单使用 二 实际应用 一
  • spss统计分析基础教程(下)--自学

    目录 xff09 第十二章分布类型的检验12 1假设检验的基本思想12 2正态分布检验K S检验的原理 12 3二项分布检验12 4游程检验12 5蒙特卡罗方法 第十三章连续变量的统计推断 xff08 一 xff09 t检验13 1t检验概
  • uniapp学习记录

    目录 1 布局使用flex布局 2 rpx和界面自适应 xff0c 设计稿是750rpx 3 首页不显示tabBar 4 跳转页面 启动跳转页面 5 uniapp中页面生命周期 传值 6 颜色使用 7 字体使用 8 SCSS CSS中获取j
  • uniapp中调用.net6 webapi

    使用uniapp开发程序时 xff0c 不管是小程序 xff0c 还是H5界面 xff0c 它们只是一个显示界面 xff0c 也就是只充当前台界面 xff0c 那么我们后台使用 net6 webapi写业务逻辑 xff0c 然后前端访问后端
  • vue3中前端处理不同数据结构的JSON

    有时候 xff0c 后端返回的JSON数据格式 xff0c 是前端不需要的格式类型 xff0c 这时 xff0c 要么让后端修改 xff0c 你要什么格式 xff0c 那么让后端大哥哥给你返回什么格式 但是有时候不尽人意 xff0c 后端大
  • 在vue3中Element Plus切换主题

    一共2种方法 目录 第一种 第二种 第一种 暗黑模式 xff0c 使用useDark xff0c 可以不用安装Element Plus xff0c 只切换页面的背景颜色 xff0c 不改变Element Plus控件的颜色 xff0c 本案
  • IIS发布.net6 api+微信小程序/H5真机调试接口的流程

    我们创建 net6 api程序 xff0c 然后使用SqlSugar连接MySQL数据库 xff0c 再使用iis发布 xff0c 当然使用其他的也行 再开发一个微信小程序 xff0c 手机运行小程序 xff0c 手机运行H5 xff0c
  • 全栈开发小作品展示(有声音)

    不积跬步 xff0c 无以至千里 xff1b 不积小流 xff0c 无以成江海 目录 1 客户端 2 网站 3 小程序 4 H5演示 1 客户端 PC桌面 xff0c WPF 43 prism框架 xff0c 前后端分离 xff0c 主要是
  • rust学习

    Installation The Rust Programming Language https doc rust lang org book ch01 01 installation html 一 安装 安装了几个组件 curl prot
  • visual studio 2017出现MSB8020,MSB8036等SDK版本选择的错误

    1 xff0c 严重性 代码 说明 项目 文件 行 禁止显示状态 错误 MSB8020 无法找到 v140 的生成 xff1b 2 xff0c 严重性 代码 说明 项目 文件 行 禁止显示状态 错误 MSB8036 找不到 Windows
  • C++中循环include问题的讨论

    问题 C语言中未避免头文件的重复引用 xff0c 一般都会使用include guard 如pragma once或 ifndef等 xff0c 但这样做以后并不是万事大吉了 循环使用include可能会出现一些意想不到的错误 如果代码较为
  • flask+gevent+gunicorn+supervisor+nginx异步高并发部署

    背景 flask是一款同步阻塞框架 xff0c 在调用外部http服务时 xff0c 当前进程将阻塞 多进程模式下 xff0c 无法响应其他用户的请求 xff0c 本文则是研究的是如何利用gevent提升flask的并发能力 xff0c 以
  • 小白学SAS--自学笔记

    64 TOC 目录 xff09 第一章初识SAS 数据集的命名 数据导入 建立永久数据集 用菜单新建文件夹 xff0c 并与电脑上已有文件夹关联 用libname语句指定文件夹名 xff0c 并与电脑上已有文件夹关联 用data语句直接指定

随机推荐

  • http协议常用请求头与响应头

    请求头 xff1a Accept 用于告诉服务器 xff0c 客户机支持的数据类型 Accept Charset xff1a 用于告诉服务器 xff0c 客户机所采用的编码 Accept Encoding xff1a 用于告诉服务器 xff
  • mysqlId 不能自启的问题(错误代号2003)

    计算机服务里看下有没有mysql的服务 xff0c 如果有 xff0c 把服务的启动类型改为自动 xff1b 如果没有 xff0c 则要进入安装目录的bin文件夹双击mysqld exe启动mysql 然后 cmd到 Mysql的安装目录的
  • RabbitMQ学习笔记1-"Hello World!"simple模型

    simple模型是RabbitMQ队列模型中最简单的一个模型 如图 xff1a P 是我们的生产者 xff08 producer xff09 xff0c C 是我们的消费者 xff08 consumer xff09 中间的红色框是队列 xf
  • RabbitMQ学习笔记2-Work queues

    接下来学习第二种模型 xff0c Work queues模型 xff0c 如图所示 xff1a 该模型描述的是 xff1a 一个生产者 xff08 P xff09 向队列发送一个消息 xff0c 然后多个消费者 xff08 P xff09
  • RabbitMQ学习笔记3-Publish/Subscribe

    在这部分中 xff0c 我们会做一些完全不同的事情 我们会向多个消费者传递相同的信息 这种模式就是 发布 订阅 该模型中生产者从不将任何消息直接发送到队列 xff0c 生产者通常甚至不知道要将消息发送到哪个队列 xff0c 通常是 xff0
  • ssh -X 使用遇到的问题

    ssh可以在登录时通过 X选项开启远程服务的X服务 xff0c 这样服务器端的需要调用X服务的程序就能开启了 最简单的例子就是可以使用服务器段的gedit程序编译文件了 问题是这样的 xff1a 在IDL程序中有个write gif程序 x
  • Docker部署Gitlab和gitlab-runner,搭建一站式DevOps平台

    一 首次安装Gitlab并配置Gitlab runner CI CD Gitlab Docker 官方安装文档 xff1a https docs gitlab cn jh install docker html 设置Gitlab数据和配置挂
  • 打开 word 显示内存或磁盘空间不足 ,Word 无法显示所请求的字体

    打开word显示内存或磁盘空间不足 xff0c Word无法显示所请求的字体 使用360加速球优化一下 xff0c 恢复正常
  • 使用win10工具远程连接树莓派

    win10远程连接树莓派 1 使用ssh远程连接1 1系统烧录1 2 SSH登录 2 使用win10的mstsc工具远程连接2 1进入远程ssh后 xff0c 修改软件源 xff0c 否则太慢 xff08 清华软件源地址 https mir
  • 网络 - 笔记本无线转为有线

    工具 路由器一个 计算机两台C1和C2 场景说明 xff1a 将C1计算机的无线网络通过路由器转为有线网络 xff0c 并提供给C2计算机使用 第一步 xff1a 硬件搭建 连接C1和路由器 xff1a 将网线的A端插入路由器的WAN口 x
  • 医学案例统计分析与SAS应用--自学笔记

    目录 第二章医学研究设计与SAS实现科研设计思路样本含量估计实验设计 科研设计的sas实现完全随机设计随机区组设计析因设计关系型研究 第三章 统计描述与SAS分析统计描述及sas命令简介定量资料的统计描述分类资料的统计描述 第四章 定量资料
  • 计算机基础 - 左移、右移和计算逻辑

    左移 指的是位移动 xff0c 左移就是将数据位向左移动 xff0c 例如十进制10 二进制为0000 1010 左移4位后得到1010 0000 xff0c 转为十进制后为160 如果是左移5位 xff0c 那么超出部分被丢弃得到的就是0
  • C++ 二叉树实现词频分析

    通过二叉树存单词 xff0c 并且对总共的单词数量进行计数 xff0c 二叉树自适应的将出现频率高的单词往上移动以减少二叉树的搜索时间 代码如下 span class hljs comment genSplay h span span cl
  • C++ cout输出字符

    cout输出字符时 xff0c 可以使用单引号 xff1a cout lt lt span class hljs string 39 39 span lt lt endl span class hljs regexp span 输出分号 s
  • Linux 多进程多线程编程

    一 创建进程 1 进程号 进程号的类型是pid t xff08 typedef unsigned int pid t xff09 获得进程和父进程ID的API如下 xff1a include lt sys types h gt includ
  • dpdk探究1-理解dpdk的运行逻辑

    DPDK介绍 DPDK主要功能 xff1a 利用IA xff08 intel architecture xff09 多核处理器进行高性能数据包处理 Linux下传统的网络设备驱动包处理的动作可以概括如下 xff1a 数据包到达网卡设备网卡设
  • C++11多线程实现的一道面试题

    题目 xff1a 子线程循环 10 次 xff0c 接着主线程循环 100 次 xff0c 接着又回到子线程循环 10 次 xff0c 接着再回到主线程又循环 100 次 xff0c 如此循环50次 xff0c 试写出代码 这里涉及到的问题
  • 第四章 智能指针

    裸指针问题如下 xff1a 裸指针在声明中并未指出 xff0c 裸指针指涉到的是单个对象还是一个数组 裸指针在声明中也没有提示是不是要对其进行虚构 换言之 xff0c 无法得知指针是否拥有其指涉的对象 或者是否空悬指针的析构是不是拥有重载的
  • dpdk无锁队列

    这篇博客是从网上博客整理摘抄而来 xff0c 具体参考的博客内容在文末给出 Linux无锁队列 kfifo概述 Linux内核中有一个先进先出的数据结构 xff0c 采用环形队列的数据结构来实现 xff0c 提供一个无边界的字节流服务 最重
  • C++虚函数和虚函数表原理

    虚函数的地址存放于虚函数表之中 运行期多态就是通过虚函数和虚函数表实现的 类的对象内部会有指向类内部的虚表地址的指针 通过这个指针调用虚函数 虚函数的调用会被编译器转换为对虚函数表的访问 xff1a ptr gt span class hl