C++继承与多态—多重继承的那些坑该怎么填
课程总目录
文章目录
- 一、虚基类和虚继承
- 二、菱形继承的问题
一、虚基类和虚继承
虚基类:被虚继承的类,就称为虚基类
virtual作用:
- virtual修饰成员方法是虚函数
- 可以修饰继承方式,是虚继承,被虚继承的类就称为虚基类
注意与抽象类(有纯虚函数的类)区分开来
来看这段代码:
class A
{
public:
private:int ma;
};class B : public A
{
public:
private:int mb;
};
//A a; 4个字节
//B b; 8个字节
使用指令cl xxx.cpp -d1reportSingleClassLayoutA和cl xxx.cpp -d1reportSingleClassLayoutB看一下

如果采用虚继承
class B : virtual public A
再来看一下,B从8字节变为了12字节了

分析:当我们遇到虚继承时候,要考虑派生类B的内存布局时,首先我们先不考虑虚继承。类B继承了基类A的ma,还有自己的mb;当我们基类A被虚继承后,基类A变为虚基类,虚基类的数据一定要被挪到派生类数据的最后面,再在最前面添加一个vbptr

来看一些例题
class A {};
sizeof(A)=1 //空类大小是1class B : public A {};
sizeof(B) = 1
class A
{virtual void fun() {}
};
sizeof(A)=1class B : public A {};
sizeof(B) = 4 //B的内存里有vfptr
class A
{virtual void fun() {}
};
sizeof(A)=1class B : virtual public A {};
sizeof(B) = 8 //B的内存里有vfptr和vbptr
总结:
vfptr:一个类有虚函数,这个类生成的对象就有vfptr,指向vftablevbptr:派生类中虚继承基类,会有vbptrvftable:存放RTTI指针(指向运行时RTTI信息)、虚函数地址。vbtable:第一行为向上偏移量,第二行为vbptr离虚基类数据在派生类内存中的偏移量。
接下来再来看,当虚基类指针与虚函数指针在一起出现的时候会发生什么呢?
class A
{
public:virtual void func() { cout << "call A::func" << endl; }
private:int ma;
};class B : virtual public A
{
public:void func() { cout << "call B::func()" << endl; }
private:int mb;
};int main()
{// 基类指针指向派生类对象,永远指向的是派生类中基类部分数据的起始地址A* p = new B();p->func();delete p;return 0;
}

可以看到,调用是没有被影响到的,但是delete会出错
分析:
B的内存布局:B首先从A中获取vfptr与ma,B中还有自己的mb
此时A被虚继承,从A中继承来的所有的东西都移动到派生类的最后面,然后在最前面补一个vbptr,vbptr指向vbtable,vfptr指向vftable

基类指针指向派生类对象,永远指向的是派生类基类部分数据的起始地址
普通情况下,派生类内存布局先是基类数据,再是派生类自己的数据,基类指针指向派生类对象时,基类指针指向的就是派生类内存的起始部分。
但是在虚继承下,基类为虚基类,虚基类的数据被挪到派生类最后面,最前面补上vbptr,此时再用基类指针指向派生类对象时候,基类指针还是指向派生类基类部分数据的起始地址,也即指向vfptr,这也是能正常调用p->func();的原因
那么在释放内存的时候呢?现在p指向的是vfptr,从vfptr开始释放内存,,而对象内存现在是从vbptr开始,这就出错了
验证一下:
class A
{
public:virtual void func() { cout << "call A::func" << endl; }void operator delete(void* p){cout << "operator delete p:" << p << endl;free(p);}
private:int ma;
};class B : virtual public A
{
public:void func() { cout << "call B::func()" << endl; }void* operator new(size_t size){void* p = malloc(size);cout << "operator new p:" << p << endl;return p;}
private:int mb;
};int main()
{// 基类指针指向派生类对象,永远指向的是派生类中基类部分数据的起始地址。A* p = new B();cout << "main p:" << p << endl;p->func();delete p;return 0;
}
operator new p:00D316A0
main p:00D316A8
call B::func()
operator delete p:00D316A8
可以看到,从A0开始new的,返回给p的是A8,delete的时候也是A8,也就是从vfptr开始释放的,这是不对的
但是,这段代码也能说是错的,这和编译器有关,在Windows的vs中,是从vfptr开始释放的,但是在linux的g++下,会自动偏移到new出来的内存的起始部分来进行释放
如果在栈上开辟内存,基类指针指向派生类对象,出了作用域自己进行析构,不涉及内存的释放,这样是没有问题的,正常运行不会报错
B b;
A *p = &b;
cout << "main p:" << p << endl;
p->func();
运行结果:
main p:010FFE04
call B::func()
使用命令cl xxx.cpp -d1reportSingleClassLayoutB查看一下

再来看,这时有人会问了,派生类为啥不像下面这样画呢?

如果是这样画,也就是vfptr属于B的作用域,这是不对的,因为A中有虚函数,vfptr是从A中继承而来的
如果真的这样画的话,那就是基类中没有虚函数,从派生类中才有的虚函数
二、菱形继承的问题
多重继承:可以复用多个基类的代码到派生类中
但是多重继承中也会出现问题:菱形继承、半圆形继承等

这些都会导致派生类有多份间接基类的数据,此时可以采用虚继承来解决
菱形继承代码:
class A
{
public:A(int data) : ma(data) { cout << "A()" << endl; }~A() { cout << "~A()" << endl; }
protected:int ma;
};
//==========================================================
class B : public A
{
public:B(int data) : A(data), mb(data) { cout << "B()" << endl; }~B() { cout << "~B()" << endl; }
protected:int mb;
};class C : public A
{
public:C(int data) : A(data), mc(data) { cout << "C()" << endl; }~C() { cout << "~C()" << endl; }
protected:int mc;
};
//==========================================================
class D : public B, public C
{
public:D(int data) : B(data), C(data), md(data) { cout << "D()" << endl; }~D() { cout << "~D()" << endl; }
protected:int md;
};int main()
{D d(10);return 0;
}
运行结果:
A()
B()
A()
C()
D()
~D()
~C()
~A()
~B()
~A()
来看一下D的内存布局

用指令cl xxx.cpp -d1reportSingleClassLayoutD看看

可以看到调用了两次A的构造,同时数据重复了
怎么解决呢?虚继承
class A { ... };
//==========================================================
class B : virtual public A { ... };
class C : virtual public A { ... };
//==========================================================
class D : public B, public C { ... };
此时内存布局变了,解决了多份数据的问题

用指令cl xxx.cpp -d1reportSingleClassLayoutD看看

但是注意,此时编译会报错,因为现在A::ma靠在了D的作用域上面,我们要在D里面给A初始化
class D : public B, public C
{
public:D(int data) : A(data), B(data), C(data), md(data) { cout << "D()" << endl; }~D() { cout << "~D()" << endl; }
protected:int md;
};
再运行看一看结果:
A()
B()
C()
D()
~D()
~C()
~B()
~A()
多重继承的好处:可以做更多代码的复用,比如上面的例子,D继承自B和C,那么就可以B* p = new D();或C* p = new D();,有两个基类,两个基类指针都可以指向派生类对象
相关文章:
C++继承与多态—多重继承的那些坑该怎么填
课程总目录 文章目录 一、虚基类和虚继承二、菱形继承的问题 一、虚基类和虚继承 虚基类:被虚继承的类,就称为虚基类 virtual作用: virtual修饰成员方法是虚函数可以修饰继承方式,是虚继承,被虚继承的类就称为虚基类…...
论文阅读:基于谱分析的全新早停策略
来自JMLR的一篇论文,https://www.jmlr.org/papers/volume24/21-1441/21-1441.pdf 这篇文章试图通过分析模型权重矩阵的频谱来解释模型,并在此基础上提出了一种用于早停的频谱标准。 1,分类难度对权重矩阵谱的影响 1.1 相关研究 在最近针对…...
1.接口测试-postman学习
目录 1.接口相关概念2.接口测试流程3.postman基本使用-创建请求(1)环境(2)新建项目集合Collections(3)新建collection(4)新建模块(5)构建请求请求URLheader设…...
2024年码蹄杯本科院校赛道初赛(省赛)
赛时所写题,简单写一下思路,qwq 第一题: 输出严格次小值, //#pragma GCC optimize(2)#include <iostream> #include <cstring> #include <algorithm> #include <vector> #include <queue> #incl…...
PHP蜜语翻译器在线文字转码解码源码
源码介绍 PHP蜜语翻译器在线文字转码解码源码 文字加密通话、一键转换、蜜语密码 无需数据库,可以将文字、字母、数字、代码、表情、标点符号等内容转换成新的文字形式,通过简单的文字以不同的排列顺序来表达不同的内容!支持在线加密解密 有多种加密展示…...
安卓浏览器区分启动、打开、分享
搞了几个钟头,终于全兼容了,分享有2种类型! void getDataFromIntent(Intent intent) {if (intent.getAction().equals(Intent.ACTION_VIEW)) {urln intent.getDataString();if (urln ! null) {if (urln.contains("\n"))urln url…...
C/C++ 数组负数下标
一 概述 在 C 中,数组是一块连续的内存空间,数组的下标通常用来定位这段内存中的特定元素。下标通常从 0 开始,最大到数组长度减 1。例如,一个有 10 个元素的数组,其有效下标范围是从 0 到 9。 当你尝试使用负数下标来…...
钓鱼网站开发原理(社会工程学)
钓鱼网站开发原理(社会工程学) 一、课程简介1、课程大纲2、课程目标3、知识储备 二、钓鱼网站简介1、什么是钓鱼网站2、开发&原理 三、PHP环境搭建1、简介2、自动安装MySQL/apache/PHP3、安装navicat 四、PDO表单入库案例1、语法2、显示登录表单3、入…...
如何优雅地使用 console.log 打印数组或对象
一、背景 使用 console.log 在控制台中打印数组或者对象时,很多时候它们的字段都是默认关闭的,需要手动一个个的点开,非常不直观且麻烦。 二、解决方案 使用 JSON.stringify() 的第三个参数 我们来看一下官方对于 JSON.stringify 的介绍 三、…...
模式分解的概念(下)-无损连接分解的与保持函数依赖分解的定义和判断、损失分解
一、无损连接分解 1、定义 2、检验一个分解是否是无损连接分解的算法 输入与输出 输入: 关系模式R(U,F),F是最小函数依赖集 R上的一个分解 输出: 判断分解是否为无损连接分解 (1&#x…...
vue3父组件获取子组件的实例对象
一,ref 在父组件的模板里,对子组件的标签定义ref属性,并且设置属性值,在方法里获取ref()获取实例对象。 父组件: <template><div ><div>我是父组件</div><<SonCom ref"sonComRe…...
主流框架选择:React、Angular、Vue的详细比较
目前前端小伙伴经常使用三种广泛使用的开发框架:React、Angular、Vue - 来设计网站 Reactjs:效率和多功能性而闻名 Angularjs:创建复杂的应用程序提供了完整的解决方案,紧凑且易于使用的框架 Vuejs:注重灵活性和可重用…...
交易者的意义是什么?
按照阿德勒的说法:人生的意义就是为社会创造价值,推动整个人类社会的发展进步。 我认同且秉持这种观点。 而在交易中,你是否直接或者间接为社会做贡献了呢?这个还真不好说。 但是做为职业交易者,你的存在价值&#…...
io_uring
转:[译] Linux 异步 I_O 框架 io_uring:基本原理、程序示例与性能压测(2020) 新一代异步IO框架 io_uring | 得物技术 干翻 nio ,王炸 io_uring 来了 !!(图解史上最全&a…...
构建高并发Web应用:基于Gunicorn、Flask和Docker的部署指南
目录 一 理解基础组件 什么是Flask? 什么是Gunicorn? 什么是Docker? 二 环境准备 三 构建Flask应用 创建项目结构 编写Flask应用 app/views.py 四 使用Gunicorn部署Flask应用 配置Gunicorn Gunicorn配置文件 五 使用Docker进行容器化部署 编写Dockerfile 构建…...
【Ruby简单脚本02】双色球系统
# frozen_string_literal: true require date # 生成中奖号码的工具 # 红球 1-32 篮球 1-15 def create_num nums [] 6.times do while true num rand(1..32) unless nums.include?(num) nums << num break end end end blue rand(1..15) nums…...
Netty ByteBuf 使用详解
文章目录 1.概述2. ByteBuf 分类3. 代码实例3.1 常用方法3.1.1 创建ByteBuf3.1.2 写入字节3.1.3 扩容3.1.2.1 扩容实例3.1.2.2 扩容计算新容量代码 3.1.4 读取字节3.1.5 标记回退3.1.6 slice3.1.7 duplicate3.1.8 CompositeByteBuf3.1.9 retain & release3.1.9.1 retain &a…...
怎样去掉卷子上的答案并打印
当面对试卷答案的问题时,一个高效而简单的方法是利用图片编辑软件中的“消除笔”功能。这种方法要求我们首先将试卷拍摄成照片,然后利用该功能轻松擦除答案。尽管这一方法可能需要些许时间和耐心,但它确实为我们提供了一个可行的解决途径。 然…...
海思SS928/SD3403开发笔记1——使用串口调试开发板
该板子使用串口可以调试,下面是win11 调试 该板子步骤 1、给板子接入鼠标、键盘、usb转串口 2、下载SecureCRT,并科学使用 下载地址: 链接:https://pan.baidu.com/s/11dIkZVstvHQUhE8uS1YO0Q 提取码:vinv 3、安装c…...
JSON数据操作艺术
在现代Web开发和数据交换场景中,JSON(JavaScript Object Notation)作为一种轻量级的数据交换格式,扮演着至关重要的角色。它以易于阅读的文本形式存储和传输数据对象,而这些对象的核心便是由属性名(键&…...
解锁B站视频下载:5个高效技巧让你轻松获取心仪内容
解锁B站视频下载:5个高效技巧让你轻松获取心仪内容 【免费下载链接】BilibiliDown (GUI-多平台支持) B站 哔哩哔哩 视频下载器。支持稍后再看、收藏夹、UP主视频批量下载|Bilibili Video Downloader 😳 项目地址: https://gitcode.com/gh_mirrors/bi/B…...
别再只用DoDragDrop了!手把手教你用WPF实现一个能拖拽合并数据的自定义控件(附完整源码)
WPF高级拖拽交互实战:从原生API局限到自定义控件设计 在构建现代桌面应用时,流畅自然的拖拽交互往往能极大提升用户体验。WPF虽然提供了基础的DoDragDrop API,但当我们需要实现复杂场景如卡片合并、动态数据交换时,原生方案就显得…...
如何绕过App Store限制:iOS第三方应用安装的终极指南
如何绕过App Store限制:iOS第三方应用安装的终极指南 【免费下载链接】AltStore AltStore is an alternative app store for non-jailbroken iOS devices. 项目地址: https://gitcode.com/gh_mirrors/al/AltStore 还在为苹果App Store的严格限制而烦恼吗&…...
RK3399pro固件逆向实战:3步提取文件系统(附完整命令)
RK3399pro固件逆向实战:从原理到实践的深度拆解 在嵌入式设备安全研究领域,固件逆向分析是获取设备内部运行机制的关键入口。作为Rockchip旗下的高性能处理器,RK3399pro广泛应用于智能硬件、边缘计算设备等领域。当我们拿到一个RK3399pro设备…...
Z-Image Turbo与Vue3前端框架集成实战
Z-Image Turbo与Vue3前端框架集成实战 本文详细介绍了如何在Vue3项目中集成Z-Image Turbo图像生成API,通过WebSocket实现实时图像生成功能,并提供完整的组件封装方案。 1. 引言 前端开发者经常面临一个挑战:如何在Web应用中集成强大的AI图像…...
RxDataSources编辑功能详解:如何实现TableView的增删改操作
RxDataSources编辑功能详解:如何实现TableView的增删改操作 【免费下载链接】RxDataSources UITableView and UICollectionView Data Sources for RxSwift (sections, animated updates, editing ...) 项目地址: https://gitcode.com/gh_mirrors/rx/RxDataSources…...
从标定板到生产线:OpenCV实战工业相机畸变校正全流程
1. 工业相机畸变:产线精度杀手的前世今生 第一次在产线上看到相机拍出来的零件尺寸和实物差了0.5毫米时,我盯着屏幕愣了三分钟——这个误差足以让整个自动化装配线变成废品生产线。工业相机的畸变就像近视眼没戴眼镜,看到的物体位置和形状都…...
如何快速掌握AI变声神器RVC:面向初学者的完整指南
如何快速掌握AI变声神器RVC:面向初学者的完整指南 【免费下载链接】Retrieval-based-Voice-Conversion-WebUI 语音数据小于等于10分钟也可以用来训练一个优秀的变声模型! 项目地址: https://gitcode.com/GitHub_Trending/re/Retrieval-based-Voice-Con…...
Emotion2Vec+语音情感识别系统:5分钟快速部署,9种情绪一键分析
Emotion2Vec语音情感识别系统:5分钟快速部署,9种情绪一键分析 1. 系统介绍与核心价值 1.1 什么是Emotion2Vec Emotion2Vec是一款基于深度学习的语音情感识别系统,能够自动分析语音中蕴含的情绪状态。这个由科哥二次开发构建的镜像版本&…...
实时口罩检测-通用部署教程:Windows WSL2环境下ModelScope模型本地加载
实时口罩检测-通用部署教程:Windows WSL2环境下ModelScope模型本地加载 1. 环境准备与WSL2配置 1.1 WSL2安装与设置 如果你使用的是Windows系统,首先需要安装WSL2(Windows Subsystem for Linux 2)。这是微软提供的Linux兼容层&…...
