C++学习笔记总结练习:多态与虚函数
1 多态
多态分类
- 静态多态,是只在编译期间确定的多态。静态多态在编译期间,根据函数参数的个数和类型推断出调用的函数。静态多态有两种实现的方式
- 重载。(函数重载)
- 模板。
- 动态多态,是运行时多态。通过虚函数机制实现(也称为重写override),使用父类的指针或者是引用,调用一个虚函数时,会根据其指向的具体对象确定调用的函数。基类和子类维护一个虚函数表,对象当中包含的虚指针,指向基类或子类的虚函数表。如果子类没有重写父类的虚函数则会直接调用父类的方法,否则调用子类重写的方法。
多态原理
- (对象的多态性)使用基类的引用或指针调用一个函数时。无法确定该函数作用的对象是什么类型。因为它可能是一个基类的对象,也可能是一个派生类的对象。
- (函数的多态性)如果该函数是虚函数,则直到运行时才会决定执行哪个版本。判断的依据是引用或指针所绑定的对象真实类型。
- 函数绑定。对非虚函数的调用在编译时进行绑定。我们通过对象进行的函数调用也在编译时绑定。对象的类型是确定不变的。
也就是说多态性体现在指针和引用的不确实能够性上。但对象在内存中的状态是确定的。当且晋档通过指针或引用调用虚函数是,才会在运行时解析该调用,也只有在这种情况下对动态类型才有可能与静态类型不同。
Bulk_quote a();//定义了对象a。这时候,无法触发多态。Bulk_quote* b = new Bulk_quote();//指针可以指向不同的类型的对象。Bulk_quote &b = a;//引用可以指向不同类型的对象。
重写与重定义对比
- 重定义:基类中没有声明函数是虚函数。派生类中对普通函数进行了重定义。只是作用域上的覆盖,没有触发多态和动态绑定。
- 重定义不能触发动态多态。无论指针或引用绑定的是什么对象,都会根据指针或引用的类型,调用该类型的函数。而不是使用虚指针查找虚函数表。只有调用虚函数的时候,才会去根据对象的虚函数指针,查找类中的虚函数表。
class A{
public:int a;A():a(10){};int real_ex(){return a;}virtual int virtual_ex(){return a;}
};class B:public A{
public:int b;B():b(20){};int real_ex(){//重定义A的函数return b;}virtual int virtual_ex(){//重写A的函数return b;}
};
int main(){Quote p(Bulk_quote());//直接初始化,拷贝构造函数Quote q = Bulk_quote();//赋值初始化,拷贝构造函数// B test_b;// A* test = &test_b;A* test=new B();cout<<test->real_ex()<<endl;//B重定义了函数。但是A类型的指针,调用基类的函数。cout<<test->virtual_ex()<<endl;//B重写类函数。B类型的对象,动态绑定,调用了派生类的函数。return 0;
}
实现条件
运行时多态的条件:
- 必须是继承关系
- 基类中必须包含虚函数,并且派生类中一定要对基类中的虚函数进行重写。
- 通过基类对象的指针或者引用调用虚函数。
注意事项
以下函数不能作为虚函数
- 友元函数,它不是类的成员函数
- 全局函数
- 静态成员函数,它没有this指针
- 构造函数,拷贝构造函数,以及赋值运算符重载(可以但是一般不建议作为虚函数)
实现原理
//参考虚函数。
2 虚函数
虚函数的定义
- 虚函数:基类希望它的派生类自定义适合自身的版本。为了实现多态
class Animal{
public:virtual double price(int n)const;
}
虚函数的原理
- 除了构造函数的非静态函数都可以是虚函数。
- 关键字virtual只能出现在类内部的声明语句之前。不能出现在类外部的函数定义。
- 如果把一个函数声明成虚函数,则该函数在派生类中也是隐式的虚函数。(即派生类的派生类,也需要重写次函数)
- 派生类可以不用重写虚函数。
- 派生类可以在它重写的虚函数前使用virtual关键字
回避虚函数的机制
- 类中的数据成员和成员函数是相互独立的。两者没有必然的联系。
- 成员函数通过this指针访问对象的数据成员。在继承体系中,this指针的指向是可以改变。即可以用派生类对象的this指针传递给基类的函数,从而实现派生类调用基类函数的方法。
- 调用是不进行动态绑定,而是强迫执行虚函数的某个特定版本。通过域作用运算符实现。
//强制调用基类中定义的函数
Bulk_quote *baseP = Bulk_quote();
double u = baseP->Quote::net_price();
纯虚函数和抽象基类
virtual void Eat() = 0;
- 一个纯虚函数无须定义,在函数体的位置书写=0,就可以讲一个虚函数说明为纯虚函数。只能出现在类内部的函数声明语句出。在类的内部必须没有定义,在类的外部可以定义纯虚函数。
- 含有纯虚函数的类是抽象基类。纯虚函数相当于接口,不能创建抽象基类的对象。
- 派生类构造函数只初始化它的直接基类。
虚函数表和虚指针原理
class A
{
public:virtual void f();virtual void g();
private:int a
};class B : public A
{
public:void g();
private:int b;
};//A、B实现省略
- 因为A有virtual void f()和g(),所以编译器为A类准备了一个虚函数表vtableA,内容如下:
A::f 的地址
A::g 的地址
- B因为继承了A,所以编译器也为B准备了一个虚函数表vtableB,内容如下:
A::f 的地址
B::g 的地址
注意:因为B::g是重写了的,所以B的虚表的g放的是B::g的入口地址,但是f是从上面的A继承下来的,所以f的地址是A::f的入口地址。
- 某处有语句 B bB;的时候,编译器分配空间时,除了A的int a,B的成员int b;以外,还分配了一个虚指针vptr,指向B的虚函数表vtableB,bB的布局如下:
vptr : 指向B的虚表vtableB
int a: 继承A的成员
int b: B成员
3 虚继承和虚基类
多继承
-
多继承(Multiple Inheritance)是指从多个直接基类中产生派生类的能力,多继承的派生类继承了所有父类的成员。尽管概念上非常简单,但是多个基类的相互交织可能会带来错综复杂的设计问题,命名冲突就是不可回避的一个。
-
多继承时很容易产生命名冲突,即使我们很小心地将所有类中的成员变量和成员函数都命名为不同的名字,命名冲突依然有可能发生,比如典型的是菱形继承,如下图所示:
菱形继承

-
类 A 派生出类 B 和类 C,类 D 继承自类 B 和类 C,这个时候类 A 中的成员变量和成员函数继承到类 D 中变成了两份,一份来自 A–>B–>D 这条路径,另一份来自 A–>C–>D 这条路径。
-
下面是菱形继承的具体实现:
//间接基类A
class A{
protected:int m_a;
};
//直接基类B
class B: public A{
protected:int m_b;
};
//直接基类C
class C: public A{
protected:int m_c;
};
//派生类D
class D: public B, public C{
public:void seta(int a){ m_a = a; } //命名冲突void setb(int b){ m_b = b; } //正确void setc(int c){ m_c = c; } //正确void setd(int d){ m_d = d; } //正确
private:int m_d;
};
int main(){D d;return 0;
}
-
这段代码实现了上图所示的菱形继承,第 25 行代码试图直接访问成员变量 m_a,结果发生了错误,因为类 B 和类 C 中都有成员变量 m_a(从 A 类继承而来),编译器不知道选用哪一个,所以产生了歧义。
-
为了消除歧义,我们可以在 m_a 的前面指明它具体来自哪个类:
void seta(int a){ B::m_a = a; }
- 这样表示使用 B 类的 m_a。当然也可以使用 C 类的:
void seta(int a){ C::m_a = a; }
虚继承(Virtual Inheritance)
-
为了解决多继承时的命名冲突和冗余数据问题,C++ 提出了虚继承,使得在派生类中只保留一份间接基类的成员。
-
在继承方式前面加上 virtual 关键字就是虚继承,请看下面的例子:
//间接基类A
class A{
protected:int m_a;
};
//直接基类B
class B: virtual public A{ //虚继承
protected:int m_b;
};
//直接基类C
class C: virtual public A{ //虚继承
protected:int m_c;
};
//派生类D
class D: public B, public C{
public:void seta(int a){ m_a = a; } //正确void setb(int b){ m_b = b; } //正确void setc(int c){ m_c = c; } //正确void setd(int d){ m_d = d; } //正确
private:int m_d;
};
int main(){D d;return 0;
}
-
这段代码使用虚继承重新实现了上图所示的菱形继承,这样在派生类 D 中就只保留了一份成员变量 m_a,直接访问就不会再有歧义了。
-
虚继承的目的是让某个类做出声明,承诺愿意共享它的基类。其中,这个被共享的基类就称为虚基类(Virtual Base Class),本例中的 A 就是一个虚基类。在这种机制下,不论虚基类在继承体系中出现了多少次,在派生类中都只包含一份虚基类的成员。
-
现在让我们重新梳理一下本例的继承关系,如下图所示:

-
观察这个新的继承体系,我们会发现虚继承的一个不太直观的特征:必须在虚派生的真实需求出现前就已经完成虚派生的操作。在上图中,当定义 D 类时才出现了对虚派生的需求,但是如果 B 类和 C 类不是从 A 类虚派生得到的,那么 D 类还是会保留 A 类的两份成员。
-
换个角度讲,虚派生只影响从指定了虚基类的派生类中进一步派生出来的类,它不会影响派生类本身。
虚继承在C++标准库中的实际应用
-
在实际开发中,位于中间层次的基类将其继承声明为虚继承一般不会带来什么问题。通常情况下,使用虚继承的类层次是由一个人或者一个项目组一次性设计完成的。对于一个独立开发的类来说,很少需要基类中的某一个类是虚基类,况且新类的开发者也无法改变已经存在的类体系。
-
C++标准库中的 iostream 类就是一个虚继承的实际应用案例。iostream 从 istream 和 ostream 直接继承而来,而 istream 和 ostream 又都继承自一个共同的名为 base_ios 的类,是典型的菱形继承。此时 istream 和 ostream 必须采用虚继承,否则将导致 iostream 类中保留两份 base_ios 类的成员。

虚基类成员的可见性
-
因为在虚继承的最终派生类中只保留了一份虚基类的成员,所以该成员可以被直接访问,不会产生二义性。此外,如果虚基类的成员只被一条派生路径覆盖,那么仍然可以直接访问这个被覆盖的成员。但是如果该成员被两条或多条路径覆盖了,那就不能直接访问了,此时必须指明该成员属于哪个类。
-
以图2中的菱形继承为例,假设 A 定义了一个名为 x 的成员变量,当我们在 D 中直接访问 x 时,会有三种可能性:
- 如果 B 和 C 中都没有 x 的定义,那么 x 将被解析为 A 的成员,此时不存在二义性。
- 如果 B 或 C 其中的一个类定义了 x,也不会有二义性,派生类的 x 比虚基类的 x 优先级更高。
- 如果 B 和 C 中都定义了 x,那么直接访问 x 将产生二义性问题。
相关文章:
C++学习笔记总结练习:多态与虚函数
1 多态 多态分类 静态多态,是只在编译期间确定的多态。静态多态在编译期间,根据函数参数的个数和类型推断出调用的函数。静态多态有两种实现的方式 重载。(函数重载)模板。 动态多态,是运行时多态。通过虚函数机制实…...
linux 批量更改指定后辍文件的可执行权限
要在Linux上批量更改指定后缀文件的可执行权限,您可以使用find命令来查找这些文件,然后使用chmod命令来更改它们的权限。以下是一些步骤: 1. 打开终端。 2. 使用 find 命令查找要更改权限的文件,例如,如果您想要更…...
数据结构(Java实现)-Map和Set
搜索树 概念 二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树: 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值 它的左右子树也…...
C++进程、线程、内存管理
目录 进程和线程区别 进程和线程切换的区别 系统调用流程 系统调用是否会引起线程切换 为什么需要使用虚拟内存 进程和线程区别 本质区别: 进程是资源分配的基本单元。 线程是操作系统调度的基本单元。 地址空间: 进程具有独立的虚拟地址空间。 线程…...
打车系统网约车系统开发支持APP公众号H5小程序版本源码
一、操作流程 二、业务模式 三、用户端 用户注册登录:未注册的手机号将自动创建账号 通过好友的邀请链接进行注册,将会绑定上下级关系 也可以注册的时候输入好友的邀请码,也可以绑定关系 用户充值: 用户下单支付时,可以…...
HTTP/1.1协议的状态码
2023年8月30日,周三下午 HTTP/1.1协议定义了一组状态码,用于表示请求的处理结果。 每个状态码都有特定的含义,它们以三位数字的形式出现在响应的状态行中。 下面是一些常见的HTTP/1.1协议的状态码及其含义: 1xx(信息…...
SpringCloud(十)——ElasticSearch简单了解(一)初识ElasticSearch和RestClient
文章目录 1. 初始ElasticSearch1.1 ElasticSearch介绍1.2 安装并运行ElasticSearch1.3 运行kibana1.4 安装IK分词器 2. 操作索引库和文档2.1 mapping属性2.2 创建索引库2.3 对索引库的查、删、改2.4 操作文档 3. RestClient3.1 初始化RestClient3.2 操作索引库3.3 操作文档 1. …...
CAD文字显示?问号解决
背景 从别人哪儿发过来的CAD文件,打开后发现有些文字显示为? 问题排查 通过点击文字特性得知 该文字的样式是 SF和仿宋通过命令行执行st 得知,两种样式关联的字体都是仿宋GB_2312,但当前操作系统无此字体,故显示为&…...
Calico切换网络模式无效
Calico切换网络模式无效 Calico由原先的BGP模式切换为IP IP模式发现未生效,使用的模式还是BGP模式,Calico卸载后查询Etcd发现存在很多Calico数据 [rootk8s-master-1 ~]# etcdctl get / --prefix --keys-only | grep calico /calico/ipam/v2/assignment…...
数据生成 | MATLAB实现GAN生成对抗网络结合SVM支持向量机的数据生成
数据生成 | MATLAB实现GAN生成对抗网络结合SVM支持向量机的数据生成 目录 数据生成 | MATLAB实现GAN生成对抗网络结合SVM支持向量机的数据生成生成效果基本描述程序设计参考资料 生成效果 基本描述 数据生成 | MATLAB实现GAN生成对抗网络结合SVM支持向量机的数据生成。 生成对抗…...
iOS - 资源按需加载 - ODR
一、瘦身技术大图 二、On-Demand Resources 简介 将其保存管理在苹果的服务器,按需使用资源、优化包体积,实现更小的应用程序。ODR 的好处: 应用体积更小,下载更快,提升初次启动速度资源会在后台下载操作系统将会在磁…...
arduino仿真 SimulIDE1.0仿真器
SimulIDE 是一个开源的电子电路模拟器,支持模拟各种电子元器件的行为,可以帮助电子工程师和爱好者进行电路设计和测试。以下是 SimulIDE 的安装和使用说明: 安装 SimulIDE SimulIDE 可以在 Windows、Linux 和 Mac OS X 等操作系统上安装。您…...
vue实现导出excel的多种方式
在Vue中实现导出Excel有多种方式,可以通过前端实现,也可以通过前后端配合实现。下面将详细介绍几种常用的实现方式。 1. 前端实现方式: 使用xlsx库:使用xlsx库可以在前端将数据导出为Excel文件。首先需要安装xlsx库,…...
redis实战-实现优惠券秒杀解决超卖问题
全局唯一ID 唯一ID的必要性 每个店铺都可以发布优惠券: 当用户抢购时,就会生成订单并保存到tb_voucher_order这张表中,而订单表如果使用数据库自增ID就存在一些问题: id的规律性太明显,容易被用户根据id的间隔来猜测…...
C语言:截断+整型提升+算数转换练习
详情关于整型提升、算数转换与截断见文章: 《C语言:整型提升》 《C语言:算数转换》 一、代码一 int main() { char a -1; signed char b -1; unsigned char c -1; printf("%d %d %d", a, b, c); return 0; } 求…...
Java后端开发面试题——多线程
创建线程的方式有哪些? 继承Thread类 public class MyThread extends Thread {Overridepublic void run() {System.out.println("MyThread...run...");}public static void main(String[] args) {// 创建MyThread对象MyThread t1 new MyThread() ;MyTh…...
Redis 学习笔记
文章目录 一、简介二、下载三、安装四、启动和关闭五、配置文件六、常用指令七、安全加固 版权声明:本文为CSDN博主「杨群」的原创文章,遵循 CC 4.0 BY-SA版权协议,于2023年9月3日首发于CSDN,转载请附上原文出处链接及本声明。 原…...
华为云新生代开发者招募
开发者您好,我们是华为2012UCD的研究团队 为了解年轻开发者的开发现状和趋势 正在邀请各位先锋开发者,与我们进行2小时的线上交流(江浙沪附近可线下交流) 聊聊您日常开发工作中的产品使用需求 成功参与访谈者将获得至少300元京…...
DockerFile简明教程
需求 由于在测试环境中使用了docker官网的centos 镜像,但是该镜像里面默认没有安装ssh服务,在做测试时又需要开启ssh。所以上网也查了查资料。下面详细的纪录下。在centos 容器内安装ssh后,转成新的镜像用于后期测试使用。 镜像定制 第一种…...
Cygwin是什么?是Windows还是Linux?
原文作者:gentle_zhou 原文链接:https://bbs.huaweicloud.com/blogs/408674 最近在和客户交流的时候,一直以为客户的研发环境就是windows 7,直到和对面的研发团队交流的时候,得到的反馈是在windows 7系统上安装了Cygw…...
大话软工笔记—需求分析概述
需求分析,就是要对需求调研收集到的资料信息逐个地进行拆分、研究,从大量的不确定“需求”中确定出哪些需求最终要转换为确定的“功能需求”。 需求分析的作用非常重要,后续设计的依据主要来自于需求分析的成果,包括: 项目的目的…...
练习(含atoi的模拟实现,自定义类型等练习)
一、结构体大小的计算及位段 (结构体大小计算及位段 详解请看:自定义类型:结构体进阶-CSDN博客) 1.在32位系统环境,编译选项为4字节对齐,那么sizeof(A)和sizeof(B)是多少? #pragma pack(4)st…...
系统设计 --- MongoDB亿级数据查询优化策略
系统设计 --- MongoDB亿级数据查询分表策略 背景Solution --- 分表 背景 使用audit log实现Audi Trail功能 Audit Trail范围: 六个月数据量: 每秒5-7条audi log,共计7千万 – 1亿条数据需要实现全文检索按照时间倒序因为license问题,不能使用ELK只能使用…...
华为云Flexus+DeepSeek征文|DeepSeek-V3/R1 商用服务开通全流程与本地部署搭建
华为云FlexusDeepSeek征文|DeepSeek-V3/R1 商用服务开通全流程与本地部署搭建 前言 如今大模型其性能出色,华为云 ModelArts Studio_MaaS大模型即服务平台华为云内置了大模型,能助力我们轻松驾驭 DeepSeek-V3/R1,本文中将分享如何…...
Spring AI与Spring Modulith核心技术解析
Spring AI核心架构解析 Spring AI(https://spring.io/projects/spring-ai)作为Spring生态中的AI集成框架,其核心设计理念是通过模块化架构降低AI应用的开发复杂度。与Python生态中的LangChain/LlamaIndex等工具类似,但特别为多语…...
学校时钟系统,标准考场时钟系统,AI亮相2025高考,赛思时钟系统为教育公平筑起“精准防线”
2025年#高考 将在近日拉开帷幕,#AI 监考一度冲上热搜。当AI深度融入高考,#时间同步 不再是辅助功能,而是决定AI监考系统成败的“生命线”。 AI亮相2025高考,40种异常行为0.5秒精准识别 2025年高考即将拉开帷幕,江西、…...
Aspose.PDF 限制绕过方案:Java 字节码技术实战分享(仅供学习)
Aspose.PDF 限制绕过方案:Java 字节码技术实战分享(仅供学习) 一、Aspose.PDF 简介二、说明(⚠️仅供学习与研究使用)三、技术流程总览四、准备工作1. 下载 Jar 包2. Maven 项目依赖配置 五、字节码修改实现代码&#…...
LRU 缓存机制详解与实现(Java版) + 力扣解决
📌 LRU 缓存机制详解与实现(Java版) 一、📖 问题背景 在日常开发中,我们经常会使用 缓存(Cache) 来提升性能。但由于内存有限,缓存不可能无限增长,于是需要策略决定&am…...
day36-多路IO复用
一、基本概念 (服务器多客户端模型) 定义:单线程或单进程同时监测若干个文件描述符是否可以执行IO操作的能力 作用:应用程序通常需要处理来自多条事件流中的事件,比如我现在用的电脑,需要同时处理键盘鼠标…...
苹果AI眼镜:从“工具”到“社交姿态”的范式革命——重新定义AI交互入口的未来机会
在2025年的AI硬件浪潮中,苹果AI眼镜(Apple Glasses)正在引发一场关于“人机交互形态”的深度思考。它并非简单地替代AirPods或Apple Watch,而是开辟了一个全新的、日常可接受的AI入口。其核心价值不在于功能的堆叠,而在于如何通过形态设计打破社交壁垒,成为用户“全天佩戴…...
