详解多态、虚继承、多重继承内存布局及虚表(C++)

本篇文章深入分析多态、虚继承、多重继承的内存布局及虚函数表以及实现原理。编译器使用VS 2022,直接放结论,代码及内存调试信息在后文。

结论

内存布局

一个没有虚函数的类,它的大小其实就是所有成员变量的大小,此时它就是一个由诸多成员变量组成的结构体,计算大小时同样要按照字节对齐去计算。

一个没有虚函数的类派生出一个没有虚函数的派生类,那么这个派生类的内存布局就是先基类成员变量,然后派生类成员变量组成的结构体,各成员变量在内存中存储顺序按照声明时的顺序来存放。

一个有虚函数的类,类本身会生成一份虚函数表,这个虚函数表是所有类对象共享的,每个类对象都会在构造时首先生成一个虚表指针,指向这个虚函数表,然后才是各个成员变量,所以有虚函数的类对象会比没有虚函数的类多一个虚表指针。

一个派生类非虚继承于一个有虚函数的类,不论派生类是否有同样的虚函数,它的内存布局都只是在有虚函数的基类基础上增加派生类的成员变量,虚表指针是直接继承基类的,指向基类虚表指针,如果派生类有同样的虚函数,那就覆盖基类虚表中同名函数。如果是派生类独有的虚函数,那就追加在基类虚函数表后面。

一个派生类虚继承于一个有虚函数且有成员变量的基类,此时派生类会重新生成它自己的虚表指针和虚函数表,内存布局则是派生类的虚表指针和成员变量在前,基类的虚表指针和成员变量在后;

虚函数表

每个含有虚函数的类都会有一个虚函数表: 如果类定义或继承了虚函数,编译器会为该类生成一个虚函数表。这个表包含了指向类虚函数实现的指针。

派生类覆写基类的虚函数: 它会在自己的虚函数表中更新该函数的入口。这确保了使用基类指针或引用调用虚函数时,执行的是派生类中最新的函数实现。

当一个类有多个基类时,且每个基类都有自己的虚函数表: 派生类将继承所有这些虚函数表。如果派生类覆写了任何继承的虚函数,它会在继承来的表上进行修改。如果派生类增加了新的虚函数,则会追加到现有的虚函数表。

虚基类表

只有在使用虚继承时,类才会有虚基类表: 虚基类表用于存储从派生类到虚基类的偏移量信息,这样无论虚基类在继承层次结构中被继承多少次,派生类中都只有一个实例。

每个使用虚继承的类(直接或间接继承虚基类的类)会有自己的虚基类表: 用于正确定位其虚基类的实例。

如果一个类继承多个类,且这些类通过虚继承自同一个基类,派生类会有一个虚基类表来管理对那个共享基类的访问。

没有虚函数

单一类

一个类没有虚函数的时候,其实就是结构体,它的内存布局就是按照成员变量的顺序来的。

#include <iostream>
using namespace std;
class Base
{
    double x;
    int y;
    char z;
public:
    Base() {}
    ~Base() {}
};
int main()
{
    Base test;
    return 0;
}

内存布局如下所示:
在这里插入图片描述
此时没有虚函数,类就是一个结构体,计算大小按照8个字节对齐。

派生类

相当于结构体的嵌套,内存布局按照声明顺序来。

#include <iostream>
using namespace std;
class Base1
{
    double x;
    int y;
    char z;
public: 
    Base1() {}
    ~Base1() {}
};
class Base2
{
    int x2;
    int y2;
public:
    Base2() {};
    ~Base2() {};
};
class Derive:public Base1,Base2
{
private:
    int x1;
public:
    Derive() {};
    ~Derive() {};
};
int main()
{
    Derive test;
    return 0;
}

在这里插入图片描述

存在虚函数

单一类

先看一个包含虚函数的单类,代码如下:

#include <iostream>
using namespace std;
class Base
{
    double x;
    int y;
    char z;
public:
    virtual void print() {}; //增加的虚函数
    Base() {}
    ~Base() {}
};
int main()
{
    Base test;
    return 0;
}

在这里插入图片描述

可以看到,有了虚函数以后,在之前基础上增加了vfptr,大小为8字节,正好是一个指针的大小(64位系统)。所以有了虚函数,单一的类就会相应的增加一个虚指针。

凡是存在虚函数的类,生成的对象都会生成一个虚表指针,并且这个虚表指针存储于对象所占用内存的最开始,也就是首先生成了虚表指针,然后再给成员变量分配的空间,虚表指针占用大小与操作系统有关。

不实现虚函数的派生类

代码如下:

#include <iostream>
using namespace std;
class Base
{
    double x;
    int y;
    char z;
public: 
    virtual void print() {};
    Base() {}
    ~Base() {}
};
class Derive:public Base
{
private:
    int x1;
public:
    Derive() {};
    ~Derive() {};
};
int main()
{
    Base test;
    return 0;
}

在这里插入图片描述
对于派生类对象而言,跟之前没有虚函数的时候没啥区别,一样的只是在基类基础上增加了派生类的成员变量而已,直接使用的是父类的虚表指针,虚函数表中也是父类的函数。

实现虚函数的派生类

派生类中实现基类同样的虚函数,其实就是多态的基本操作。代码如下:

#include <iostream>
using namespace std;
class Base
{
    double x;
    int y;
    char z;
public: 
    virtual void print() { cout << "Base\n"; };
    Base() {}
    ~Base() {}
};
class Derive:public Base
{
private:
    int x1;
public:
    virtual void print() { cout << "Derive\n"; };
    Derive() {};
    ~Derive() {};
};
int main()
{
    Derive test;
    return 0;
}

在这里插入图片描述
看起来内存布局其实跟之前没有区别,派生类并没有重新生成虚表指针,直接继承了基类的虚表指针,但是虚表中的函数变成了派生类实现的函数。

其实在普通继承(非虚继承)的时候派生类并不会重新生成虚表指针,只是会使用它自身的虚函数地址去覆盖基类的相同虚函数,如果是派生类独有的虚函数,则直接追加到虚函数表的最后面

继承多个基类并实现虚函数的派生类

如果有一个类继承了两个基类的虚函数,并实现呢?

#include <iostream>
using namespace std;
class Base1
{
private:
    int x1;
public:
    virtual void print() { cout << "Base1\n"; };
    Base1() {}
    ~Base1() {}
};
class Base2
{
private:
    int x2;
public:
    virtual void print() { cout << "Base2\n"; };
    Base2() {}
    ~Base2() {}
};

class Derive :public Base1,Base2
{
private:
    int x1;
public:
    virtual void print() { cout << "Derive\n"; };
    Derive() {};
    ~Derive() {};
};
int main()
{
    Derive test;
    return 0;
}

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述Derive::$vftable@Base1@:这是派生自Base1的虚函数表。它包含了指向Derive::print函数的指针,意味着Derive重写了Base1的虚函数print。

Derive::$vftable@Base2@:这是派生自Base2的虚函数表。由于Derive::print也应用于Base2,表中包含了一个特殊的条目&thunk: this-=16; goto Derive::print。

这是一个调整器(thunk),用于调整this指针,以便Derive::print函数能够正确地访问Base2的成员。-16意味着在调用print前,需要将this指针向后调整16字节,这是因为Base2在Derive对象中的起始位置偏移了16字节。

由此可得:你的父类如果存在虚函数,会一并继承其虚函数表,有几个父类,就有几个虚函数表。

虚继承

单继承

在没有虚函数的时候是不是虚继承影响不大,但存在虚函数的时候虚继承和非虚继承是不一样的。

#include <iostream>
using namespace std;
class Base
{
    double x;
    int y;
    char z;
public: 
    virtual void print() { cout << "Base\n"; };
    Base() {}
    ~Base() {}
};
class Derive:virtual public Base
{
private:
    int x1;
public:
    virtual void print() { cout << "Derive\n"; };
    Derive() {};
    ~Derive() {};
};
int main()
{
    Derive test;
    return 0;
}

在这里插入图片描述
vbtable: 存储有关虚基类Base在派生类对象中偏移量的信息。它表明基类Base位于派生类对象起始地址之后的24字节处。

vftable: 包含了虚函数print的地址。vtordisp用于在调用虚函数时调整this指针,以便正确访问虚基类Base的成员。vtordisp的值为-24,表明需要调整的偏移量。

虚继承不只是实现了派生类自己的虚表指针,还重新生成了属于它自己的虚函数表,等于虚继承就比非虚继承多了很多开销。

再说回内存布局,在非虚继承的时候是按照顺序存储,但虚继承情况下,派生类的虚表指针和成员变量在前面,基类的虚表指针和成员变量在后面。

多重继承和二义性问题

在多重继承的情境下,如果两个或多个基类继承自同一个更远的基类,而一个派生类又从这些基类继承,则最远处的基类会在派生类中有多个实例。这会导致访问最远处基类的成员时出现二义性,因为编译器无法确定使用哪个实例。

虚继承通过确保在继承层次结构中只创建基类的单一实例来解决这个问题。当一个类通过虚继承继承另一个类时,它不会创建基类的新实例,而是使用现有的实例(如果已经存在)。这意味着无论基类被继承多少次,派生类中都只会有一个共享的基类实例。

以下代码为例,查看内存布局:

#include <iostream>
using namespace std;

class A
{
public:
    int a;
    A() {}
    virtual ~A() {}
};

class B : virtual public A
{
public:
    int b;
    B() {}
    ~B() {}
};

class C : virtual public A
{
public:
    int c;
    C() {}
    ~C() {}
};

class D :public B, public C
{
public:
    int d;
};

int main()
{
    D d;
    return 0;
}

在这里插入图片描述
总共有三个虚表:两个是虚基类表(每个基类B和C各一个),用于虚继承的偏移量管理;一个是虚函数表,用于支持类D的虚函数,如其析构函数的动态绑定。

对于类B、类C、类D这三个,它是按照顺序来存储的,对于类A与上一节虚继承得出的结果一样,虚基类的虚表指针和成员变量是放在一块内存的最后面的。

个人理解: 虚基类之所以放在对象所属内存的后面,跟虚继承的机制有关,用了虚继承以后,能保证虚基类在对象内存中永远只有一份拷贝,如果还是按照顺序存储,虚基类只有一份,但是派生类却有多个,那编译器到底该把虚基类放在哪个派生类前面呢,那干脆放在最后面,让大家共享,这样就不存在冲突行为,同时这也解释了为什么虚继承能解决二义性问题。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/523025.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

手机如何在线制作gif?轻松一键在线操作

现在大家都喜欢使用手机来拍摄记录有趣的事物&#xff0c;但是时间长了手机里的视频越来越多导致手机存储空间不够了&#xff0c;这些视频又不想删除时应该怎么办呢&#xff1f;这个很简单&#xff0c;下面就给大家分享一款不用下载手机就能操作的视频转gif网站-GIF中文网&…

【海思SS528 | VDEC】查看VDEC的proc调试信息 | cat /proc/umap/vdec

&#x1f601;博客主页&#x1f601;&#xff1a;&#x1f680;https://blog.csdn.net/wkd_007&#x1f680; &#x1f911;博客内容&#x1f911;&#xff1a;&#x1f36d;嵌入式开发、Linux、C语言、C、数据结构、音视频&#x1f36d; &#x1f923;本文内容&#x1f923;&a…

稀碎从零算法笔记Day38-LeetCode:除自身以外数组的乘积

题型&#xff1a;数组、前缀、分治、 链接&#xff1a;238. 除自身以外数组的乘积 - 力扣&#xff08;LeetCode&#xff09; 来源&#xff1a;LeetCode 题目描述 给你一个整数数组 nums&#xff0c;返回 数组 answer &#xff0c;其中 answer[i] 等于 nums 中除 nums[i] 之…

Linux:安装zabbix-agent被监控端(2)

本章是结合着上一篇文章的续作 Linux&#xff1a;部署搭建zabbix6&#xff08;1&#xff09;-CSDN博客https://blog.csdn.net/w14768855/article/details/137426966?spm1001.2014.3001.5501本章将在两台centos部署agent端&#xff0c;然后使用server进行连接监控 agent1 在1…

突破编程_前端_SVG(基础元素介绍)

1 rect 矩形 在 SVG 中&#xff0c;<rect> 元素用于创建圆形。 &#xff08;1&#xff09;基本语法 <rectx"x坐标"y"y坐标"width"宽度"height"高度"rx"可选&#xff1a;圆角x半径"ry"可选&#xff1a;圆角…

达梦数据库记录

1.计算日期差 SELECT DATEDIFF(day,sysdate(), 2024-06-01) 2.出现HJ_BUF_GLOBAL_SIZE设置不当造成应用报错的问题&#xff0c;详细信息如下&#xff1a; dm.jdbc.driver.DMException: 超出全局hash join空间,适当增加HJ_BUF_GLOBAL_SIZEat dm.jdbc.driver.DBError.throwExce…

Java设计模式:桥接模式实现灵活组合,超越单一继承的设计之道(十)

码到三十五 &#xff1a; 个人主页 心中有诗画&#xff0c;指尖舞代码&#xff0c;目光览世界&#xff0c;步履越千山&#xff0c;人间尽值得 ! 目录 一、引言二、什么是桥接设计模式三、桥接设计模式的核心思想四、桥接设计模式的角色五、桥接设计模式的工作流程和实现实现方…

如何设置win10系统不更新,win10怎么设置系统不更新系统

Win10频繁地更新让很多用户感到困扰,虽然我们都知道,更新系统有利于提高系统的安全性,同时还能获得功能更新以及一些bug修复,但是过于频繁的更新会让人感到疲惫,也有不少用户对此举表示反感。一般情况下,win10系统是默认自动更新的,如何阻止系统自动更新呢?下面,小编跟…

文件夹类型变无?数据恢复有高招!

在日常使用电脑的过程中&#xff0c;我们有时会遇到一种奇怪的现象&#xff1a;原本整齐有序的文件夹突然变成了无类型的状态&#xff0c;即文件夹类型变无。这些文件夹失去了原有的图标和属性&#xff0c;变成了系统无法识别的空白图标&#xff0c;甚至无法打开。这种情况下&a…

甘特图/横道图制作技巧 - 任务组

在甘特图中通过合理的任务分组可以让项目更加清晰&#xff0c;修改也更方便。 列如下面的甘特图一眼不太容易看清楚整体的进度。或者需要把所有的任务整体的延迟或者提前只能这样一个一个的任务调整&#xff0c;就比较麻烦。 通过给任务分组&#xff0c;看这上面整体的进度就…

Redis安装及基本类型详解

目录 一、Redis概念 二、Redis的应用场景 三、Redis的特点 四、redis访问数据为什么快&#xff1f; 五、Ubuntu下安装redis 五、全局命令(针对任意类型value都可使用) 1、keys &#xff08;1&#xff09;keys * &#xff08;2&#xff09;keys pattern 2、exists 3、…

git Failed to connect to 你的网址 port 8282: Timed out

git Failed to connect to 你的网址 port 8282: Timed out 出现这个问题的原因是&#xff1a;原来的仓库换了网址&#xff0c;原版网址不可用了。 解决方法如下&#xff1a; 方法一&#xff1a;查看git用户配置是否有如下配置 http.proxyhttp://xxx https.proxyhttp://xxx如果…

《梦幻西游》迎来史上最大翻车,老玩家们为何纷纷揭竿而起?

因一次调整&#xff0c;21岁的《梦幻西游》迎来了自己有史以来最大的一波节奏。 玩家在微博上炮轰官方&#xff0c;称&#xff1a;“游戏借着打击工作室牟利的称号&#xff0c;砍副本活动产出&#xff0c;然后自己口袋无限卖”&#xff0c;要求改善游戏现状。 从3月29日起&am…

uniapp 密码框的眼睛

效果展示&#xff1a; uniapp input 官网链接&#xff1a;链接 按照官方文档&#xff0c;uni-icon出不来。 通过自己的方法解决了&#xff0c;解决方案如下&#xff1a; 代码&#xff1a; <uni-forms-item name"password"><inputclass"uni-input&quo…

background背景图参数边渐变CSS中创建背景图像的渐变效果

效果:可以看到灰色边边很难受,希望和背景融为一体 原理: 可以使用线性渐变&#xff08;linear-gradient&#xff09;或径向渐变&#xff08;radial-gradient&#xff09;。以下是一个使用线性渐变作为背景图像 代码: background: linear-gradient(to top, rgba(255,255,255,0)…

【Linux】指令

1. 简单指令 whoami 显示当前登入账号名 ls /home 现在有的用户名 adduser 用户名 新加用户&#xff08;必须在root目录下&#xff09; passwd 用户名 给这个用户设置密码 userdel -r 用户名 删除这个用户 pwd 显示当前所处路径 stat 文件名 / 文件夹名 显示文件状…

学习大数据之JDBC(使用JAVA语句进行SQL操作)(3)

文章目录 DBUtils工具包准备工作DBUtils的介绍QueryRunner空参的QueryRunner的介绍以及使用有参QueryRunner的介绍以及使用 ResultSetHandler结果集BeanHandler<T>BeanListHandler<T>ScalarHanderColumnListHander 事务事务事务_转账分析图实现转账&#xff08;不加…

CTF之GET和POST

学过php都知道就一个简单传参&#xff0c;构造payload:?whatflag得到 flag{3121064b1e9e27280f9f709144222429} 下面是POST那题 使用firefox浏览器的插件Hackbar勾选POST传入whatflag flag{828a91acc006990d74b0cb0c2f62b8d8}

【网站项目】鲜花销售微信小程序

&#x1f64a;作者简介&#xff1a;拥有多年开发工作经验&#xff0c;分享技术代码帮助学生学习&#xff0c;独立完成自己的项目或者毕业设计。 代码可以私聊博主获取。&#x1f339;赠送计算机毕业设计600个选题excel文件&#xff0c;帮助大学选题。赠送开题报告模板&#xff…

Dubbo 服务发现

Dubbo 服务发现 1、什么是服务发现 **服务发现&#xff08;Service discovery&#xff09;**是自动检测一个计算机网络内的设备及其提供的服务。 2、Dubbo 与 服务发现 Dubbo 提供的是一种 Client-Based 的服务发现机制&#xff0c;依赖第三方注册中心组件来协调服务发现过…