当前位置: 首页 > news >正文

C++继承与多态—多重继承的那些坑该怎么填


课程总目录


文章目录

  • 一、虚基类和虚继承
  • 二、菱形继承的问题


一、虚基类和虚继承

虚基类:被虚继承的类,就称为虚基类

virtual作用:

  1. virtual修饰成员方法是虚函数
  2. 可以修饰继承方式,是虚继承,被虚继承的类就称为虚基类

注意与抽象类(有纯虚函数的类)区分开来

来看这段代码:

class A
{
public:
private:int ma;
};class B : public A
{
public:
private:int mb;
};
//A a; 4个字节
//B b; 8个字节

使用指令cl xxx.cpp -d1reportSingleClassLayoutAcl 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,指向vftable
  • vbptr:派生类中虚继承基类,会有vbptr
  • vftable:存放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中获取vfptrma,B中还有自己的mb

此时A被虚继承,从A中继承来的所有的东西都移动到派生类的最后面,然后在最前面补一个vbptrvbptr指向vbtablevfptr指向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的是A8delete的时候也是A8,也就是从vfptr开始释放的,这是不对的

但是,这段代码也能说是错的,这和编译器有关,在Windowsvs中,是从vfptr开始释放的,但是在linuxg++下,会自动偏移到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++继承与多态—多重继承的那些坑该怎么填

课程总目录 文章目录 一、虚基类和虚继承二、菱形继承的问题 一、虚基类和虚继承 虚基类&#xff1a;被虚继承的类&#xff0c;就称为虚基类 virtual作用&#xff1a; virtual修饰成员方法是虚函数可以修饰继承方式&#xff0c;是虚继承&#xff0c;被虚继承的类就称为虚基类…...

论文阅读:基于谱分析的全新早停策略

来自JMLR的一篇论文&#xff0c;https://www.jmlr.org/papers/volume24/21-1441/21-1441.pdf 这篇文章试图通过分析模型权重矩阵的频谱来解释模型&#xff0c;并在此基础上提出了一种用于早停的频谱标准。 1&#xff0c;分类难度对权重矩阵谱的影响 1.1 相关研究 在最近针对…...

1.接口测试-postman学习

目录 1.接口相关概念2.接口测试流程3.postman基本使用-创建请求&#xff08;1&#xff09;环境&#xff08;2&#xff09;新建项目集合Collections&#xff08;3&#xff09;新建collection&#xff08;4&#xff09;新建模块&#xff08;5&#xff09;构建请求请求URLheader设…...

2024年码蹄杯本科院校赛道初赛(省赛)

赛时所写题&#xff0c;简单写一下思路&#xff0c;qwq 第一题&#xff1a; 输出严格次小值&#xff0c; //#pragma GCC optimize(2)#include <iostream> #include <cstring> #include <algorithm> #include <vector> #include <queue> #incl…...

PHP蜜语翻译器在线文字转码解码源码

源码介绍 PHP蜜语翻译器在线文字转码解码源码 文字加密通话、一键转换、蜜语密码 无需数据库,可以将文字、字母、数字、代码、表情、标点符号等内容转换成新的文字形式&#xff0c;通过简单的文字以不同的排列顺序来表达不同的内容&#xff01;支持在线加密解密 有多种加密展示…...

安卓浏览器区分启动、打开、分享

搞了几个钟头&#xff0c;终于全兼容了&#xff0c;分享有2种类型&#xff01; 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 中&#xff0c;数组是一块连续的内存空间&#xff0c;数组的下标通常用来定位这段内存中的特定元素。下标通常从 0 开始&#xff0c;最大到数组长度减 1。例如&#xff0c;一个有 10 个元素的数组&#xff0c;其有效下标范围是从 0 到 9。 当你尝试使用负数下标来…...

钓鱼网站开发原理(社会工程学)

钓鱼网站开发原理&#xff08;社会工程学&#xff09; 一、课程简介1、课程大纲2、课程目标3、知识储备 二、钓鱼网站简介1、什么是钓鱼网站2、开发&原理 三、PHP环境搭建1、简介2、自动安装MySQL/apache/PHP3、安装navicat 四、PDO表单入库案例1、语法2、显示登录表单3、入…...

如何优雅地使用 console.log 打印数组或对象

一、背景 使用 console.log 在控制台中打印数组或者对象时&#xff0c;很多时候它们的字段都是默认关闭的&#xff0c;需要手动一个个的点开&#xff0c;非常不直观且麻烦。 二、解决方案 使用 JSON.stringify() 的第三个参数 我们来看一下官方对于 JSON.stringify 的介绍 三、…...

模式分解的概念(下)-无损连接分解的与保持函数依赖分解的定义和判断、损失分解

一、无损连接分解 1、定义 2、检验一个分解是否是无损连接分解的算法 输入与输出 输入&#xff1a; 关系模式R&#xff08;U&#xff0c;F&#xff09;&#xff0c;F是最小函数依赖集 R上的一个分解 输出&#xff1a; 判断分解是否为无损连接分解 &#xff08;1&#x…...

vue3父组件获取子组件的实例对象

一&#xff0c;ref 在父组件的模板里&#xff0c;对子组件的标签定义ref属性&#xff0c;并且设置属性值&#xff0c;在方法里获取ref()获取实例对象。 父组件&#xff1a; <template><div ><div>我是父组件</div><<SonCom ref"sonComRe…...

主流框架选择:React、Angular、Vue的详细比较

目前前端小伙伴经常使用三种广泛使用的开发框架&#xff1a;React、Angular、Vue - 来设计网站 Reactjs&#xff1a;效率和多功能性而闻名 Angularjs&#xff1a;创建复杂的应用程序提供了完整的解决方案&#xff0c;紧凑且易于使用的框架 Vuejs&#xff1a;注重灵活性和可重用…...

交易者的意义是什么?

按照阿德勒的说法&#xff1a;人生的意义就是为社会创造价值&#xff0c;推动整个人类社会的发展进步。 我认同且秉持这种观点。 而在交易中&#xff0c;你是否直接或者间接为社会做贡献了呢&#xff1f;这个还真不好说。 但是做为职业交易者&#xff0c;你的存在价值&#…...

io_uring

转&#xff1a;[译] Linux 异步 I_O 框架 io_uring&#xff1a;基本原理、程序示例与性能压测&#xff08;2020&#xff09; 新一代异步IO框架 io_uring &#xff5c; 得物技术 干翻 nio &#xff0c;王炸 io_uring 来了 &#xff01;&#xff01;&#xff08;图解史上最全&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…...

怎样去掉卷子上的答案并打印

当面对试卷答案的问题时&#xff0c;一个高效而简单的方法是利用图片编辑软件中的“消除笔”功能。这种方法要求我们首先将试卷拍摄成照片&#xff0c;然后利用该功能轻松擦除答案。尽管这一方法可能需要些许时间和耐心&#xff0c;但它确实为我们提供了一个可行的解决途径。 然…...

海思SS928/SD3403开发笔记1——使用串口调试开发板

该板子使用串口可以调试&#xff0c;下面是win11 调试 该板子步骤 1、给板子接入鼠标、键盘、usb转串口 2、下载SecureCRT&#xff0c;并科学使用 下载地址&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/11dIkZVstvHQUhE8uS1YO0Q 提取码&#xff1a;vinv 3、安装c…...

JSON数据操作艺术

在现代Web开发和数据交换场景中&#xff0c;JSON&#xff08;JavaScript Object Notation&#xff09;作为一种轻量级的数据交换格式&#xff0c;扮演着至关重要的角色。它以易于阅读的文本形式存储和传输数据对象&#xff0c;而这些对象的核心便是由属性名&#xff08;键&…...

iOS 26 携众系统重磅更新,但“苹果智能”仍与国行无缘

美国西海岸的夏天&#xff0c;再次被苹果点燃。一年一度的全球开发者大会 WWDC25 如期而至&#xff0c;这不仅是开发者的盛宴&#xff0c;更是全球数亿苹果用户翘首以盼的科技春晚。今年&#xff0c;苹果依旧为我们带来了全家桶式的系统更新&#xff0c;包括 iOS 26、iPadOS 26…...

label-studio的使用教程(导入本地路径)

文章目录 1. 准备环境2. 脚本启动2.1 Windows2.2 Linux 3. 安装label-studio机器学习后端3.1 pip安装(推荐)3.2 GitHub仓库安装 4. 后端配置4.1 yolo环境4.2 引入后端模型4.3 修改脚本4.4 启动后端 5. 标注工程5.1 创建工程5.2 配置图片路径5.3 配置工程类型标签5.4 配置模型5.…...

【Linux】C语言执行shell指令

在C语言中执行Shell指令 在C语言中&#xff0c;有几种方法可以执行Shell指令&#xff1a; 1. 使用system()函数 这是最简单的方法&#xff0c;包含在stdlib.h头文件中&#xff1a; #include <stdlib.h>int main() {system("ls -l"); // 执行ls -l命令retu…...

Debian系统简介

目录 Debian系统介绍 Debian版本介绍 Debian软件源介绍 软件包管理工具dpkg dpkg核心指令详解 安装软件包 卸载软件包 查询软件包状态 验证软件包完整性 手动处理依赖关系 dpkg vs apt Debian系统介绍 Debian 和 Ubuntu 都是基于 Debian内核 的 Linux 发行版&#xff…...

《Playwright:微软的自动化测试工具详解》

Playwright 简介:声明内容来自网络&#xff0c;将内容拼接整理出来的文档 Playwright 是微软开发的自动化测试工具&#xff0c;支持 Chrome、Firefox、Safari 等主流浏览器&#xff0c;提供多语言 API&#xff08;Python、JavaScript、Java、.NET&#xff09;。它的特点包括&a…...

对WWDC 2025 Keynote 内容的预测

借助我们以往对苹果公司发展路径的深入研究经验&#xff0c;以及大语言模型的分析能力&#xff0c;我们系统梳理了多年来苹果 WWDC 主题演讲的规律。在 WWDC 2025 即将揭幕之际&#xff0c;我们让 ChatGPT 对今年的 Keynote 内容进行了一个初步预测&#xff0c;聊作存档。等到明…...

OkHttp 中实现断点续传 demo

在 OkHttp 中实现断点续传主要通过以下步骤完成&#xff0c;核心是利用 HTTP 协议的 Range 请求头指定下载范围&#xff1a; 实现原理 Range 请求头&#xff1a;向服务器请求文件的特定字节范围&#xff08;如 Range: bytes1024-&#xff09; 本地文件记录&#xff1a;保存已…...

【论文笔记】若干矿井粉尘检测算法概述

总的来说&#xff0c;传统机器学习、传统机器学习与深度学习的结合、LSTM等算法所需要的数据集来源于矿井传感器测量的粉尘浓度&#xff0c;通过建立回归模型来预测未来矿井的粉尘浓度。传统机器学习算法性能易受数据中极端值的影响。YOLO等计算机视觉算法所需要的数据集来源于…...

PL0语法,分析器实现!

简介 PL/0 是一种简单的编程语言,通常用于教学编译原理。它的语法结构清晰,功能包括常量定义、变量声明、过程(子程序)定义以及基本的控制结构(如条件语句和循环语句)。 PL/0 语法规范 PL/0 是一种教学用的小型编程语言,由 Niklaus Wirth 设计,用于展示编译原理的核…...

vue3+vite项目中使用.env文件环境变量方法

vue3vite项目中使用.env文件环境变量方法 .env文件作用命名规则常用的配置项示例使用方法注意事项在vite.config.js文件中读取环境变量方法 .env文件作用 .env 文件用于定义环境变量&#xff0c;这些变量可以在项目中通过 import.meta.env 进行访问。Vite 会自动加载这些环境变…...