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

news2024/12/25 9:32:52

本篇文章深入分析多态、虚继承、多重继承的内存布局及虚函数表以及实现原理。编译器使用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与上一节虚继承得出的结果一样,虚基类的虚表指针和成员变量是放在一块内存的最后面的。

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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

13 指针(上)

指针是 C 语言最重要的概念之一&#xff0c;也是最难理解的概念之一。 指针是C语言的精髓&#xff0c;要想掌握C语言就需要深入地了解指针。 指针类型在考研中用得最多的地方&#xff0c;就是和结构体结合起来构造结点(如链表的结点、二叉树的结点等)。 本章专题脉络 1、指针…

redis string底层为什么使用sds, sds好处?redis 的动态字符串优点?

1. redis 的键值对&#xff0c;都是由对象组成的&#xff0c; 其中键总是一个字符串对象&#xff08;string object&#xff09; 而键的value则可以是&#xff1a;“字符串对象”&#xff0c; “列表对象 &#xff08;list object&#xff09;”&#xff0c;“哈希对象 (hash o…

《由浅入深学习SAP财务》:第2章 总账模块 - 2.6 定期处理 - 2.6.3 月末操作:外币评估

2.6.3 月末操作&#xff1a;外币评估 企业的外币业务在记账时一般使用期初的汇率或者即时汇率&#xff0c;但在月末&#xff0c;需要按照月末汇率对外币的余额或者未清项进行重估&#xff08;revaluation&#xff09;。 企业在资产负债表日&#xff0c;应当按照下列规…

nandgame中的Code generation(代码生成)

题目说明&#xff1a; 代码生成为语言的语法规则定义代码生成&#xff0c;以支持加法和减法。 您可以使用在前面级别中定义的堆栈操作&#xff08;如ADD和SUB&#xff09;。代码生成模板通常需要包含规则中其他符号的代码。 这些可以通过方括号中的符号名称插入。例如&#xf…

【初中生讲机器学习】15. EM 算法一万字详解!一起来学!

创建时间&#xff1a;2024-04-08 最后编辑时间&#xff1a;2024-04-10 作者&#xff1a;Geeker_LStar 你好呀~这里是 Geeker_LStar 的人工智能学习专栏&#xff0c;很高兴遇见你~ 我是 Geeker_LStar&#xff0c;一名初三学生&#xff0c;热爱计算机和数学&#xff0c;我们一起加…

西门子PLC(S7-200 SMART)学习笔记1:初识PLC可编程逻辑器件

今日开始我的西门子PLC学习之路&#xff0c;学习的型号以S7-200 SMART为主 主要认识一下PLC是什么、型号怎么看、 通信相关、编程软件、构造及工作原理 目录 西门子官方PLC手册获取&#xff1a; 1、PLC可编程逻辑器件的基本认识&#xff1a; PLC的结构及各部分的作用&#xff…

Kali安装黑屏与进入系统后不显示中文的解决办法

使用镜像版本kali-linux-2024.1-installer-amd64.iso 一.创建虚拟机安装Kali镜像时&#xff0c;安装后要重启时发现左上角有个— 闪动并黑屏&#xff0c;启动不成功 上述办法也很简单&#xff0c;可以试试再windows中的CMD终端输入netsh winsock reset&#xff0c;重启电脑如果…

嵌入式Linux:Linux库函数

目录 1、Linux库函数简介 2、标准C语言库函数 1、Linux库函数简介 Linux 提供了丰富的库函数&#xff0c;涵盖了各种领域&#xff0c;从文件操作到网络编程、图形界面、数学运算等。这些库函数大多数都是标准的 C 库函数&#xff0c;同时也包括一些特定于 Linux 系统的库。 …

【Linux】初识Linux,虚拟机安装Linux系统,配置网卡

前言 VMware软件&#xff1a;首先&#xff0c;确保您已经下载了VMware Workstation软件并安装在电脑上。VMware Workstation是一款功能强大的虚拟化软件&#xff0c;它允许在单一物理机上运行多个操作系统。 Linux镜像文件&#xff1a;需要准备一个Linux操作系统的镜像文件。…

初学python记录:力扣1702. 修改后的最大二进制字符串

题目&#xff1a; 给你一个二进制字符串 binary &#xff0c;它仅有 0 或者 1 组成。你可以使用下面的操作任意次对它进行修改&#xff1a; 操作 1 &#xff1a;如果二进制串包含子字符串 "00" &#xff0c;你可以用 "10" 将其替换。 比方说&#xff0c;…

LWIP一探究竟

1.网卡接收数据的流程 我们网卡接收数据基本上就是开发板上eth接收完数据后产生一个中断,然后释放一个信号量通知网卡接收线程去处理这些接收的数据,然后将这些数据封装成信息,投递到tcpip_mbox邮箱中,LWIP内核线程得到这个消息,就对消息进行解析,根据消息中数据包类型进行处理…

docker使用canal

1. 准备MySql主库 1.1.在服务器新建文件夹 mysql/data&#xff0c;新建文件 mysql/conf.d/my.cnf 其中my.cnf 内容如下 [mysqld] log_timestampsSYSTEM default-time-zone8:00 server-id1 log-binmysql-bin binlog-do-db mall # 要监听的库 binlog_formatROW配置解读&#…

Harmony鸿蒙南向驱动开发-Regulator

Regulator模块用于控制系统中各类设备的电压/电流供应。在嵌入式系统&#xff08;尤其是手机&#xff09;中&#xff0c;控制耗电量很重要&#xff0c;直接影响到电池的续航时间。所以&#xff0c;如果系统中某一个模块暂时不需要使用&#xff0c;就可以通过Regulator关闭其电源…

网络基础三——其他周边问题

3.1ARP原理 ​ ARP不是一个单纯的数据链路层的协议&#xff0c;而是一个介于数据链路层和网络层之间的协议&#xff1b; ​ 以广播的形式(主机号填成全1)构建Mac帧&#xff0c;发送ARP请求包&#xff0c;告诉所有在局域网的主机我的IP地址和Mac帧&#xff0c;与目的IP相同的主…

意得辑意得辑

你是否也曾遇到过在发表论文时英语写作水平不尽如人意的困境&#xff1f;审稿意见总是指出语言表达不够好&#xff0c;需要找英语母语者修改&#xff1f;不用担心&#xff0c;我和你一样&#xff0c;也曾历经这样的挑战。但是&#xff0c;我找到了一家值得信赖的专业润色机构—…

深入分析Linux上下文与上下文切换

Linux 进程运行空间与特权等级 在 Linux 操作系统中&#xff0c;进程的运行空间被划分为内核空间和用户空间&#xff0c;这种划分是为了保护系统的稳定性和安全性。这两个空间对应着 CPU 的特权等级&#xff0c;分别为&#xff1a; Ring 0&#xff08;内核态&#xff09;Ring…

全国青少年劳动技能与智能设计大赛安徽省赛区赛前培训在肥东六中顺利举办

为推进我省青少年劳动教育和素质教育高质量发展&#xff0c;提升青少年家国情怀、多元思维、劳动技能、创新能力和综合素质&#xff0c;发现和培养创新后备人才。4月10日&#xff0c;肥东县教体局联合安徽省青少年劳动技能与智能设计大赛组委会在肥东六中创新科教基地成功举行全…

人工智能分类算法概述

文章目录 人工智能主要分类算法决策树随机森林逻辑回归K-均值 总结 人工智能主要分类算法 人工智能分类算法是用于将数据划分为不同类别的算法。这些算法通过学习数据的特征和模式&#xff0c;将输入数据映射到相应的类别。分类算法在人工智能中具有广泛的应用&#xff0c;如图…

目标检测——车牌图像数据集

一、重要性及意义 车牌图像识别的重要性及意义主要体现在以下几个方面&#xff1a; 智能交通管理&#xff1a;车牌图像识别技术是智能交通系统&#xff08;ITS&#xff09;的核心组成部分。通过自动识别车辆车牌&#xff0c;可以实现对交通违章行为的自动记录和处理&#xff…

SSD涨价停不下来!

随着HBM内存产能短缺问题的出现&#xff0c;存储市场正遭遇另一波供应短缺。在2021年存储市场陷入低迷后&#xff0c;SSD价格已连续下滑约两年。面对市场变化&#xff0c;存储厂商减少了NAND闪存的生产。随着减产策略的有效执行&#xff0c;需求部分回升&#xff0c;导致SSD供应…