浅谈C++的继承与多态(静态绑定、动态绑定和虚函数等)
今天我们来谈谈C++的继承与多态😊😊😊,本篇的关键内容如下:
- 继承的本质及其原理
- 派生类的构造和析构过程
- 重载、隐藏和覆盖
- 类的向下或向上转型
- 静态绑定与动态绑定
- 虚函数对类的影响
- 虚析构函数
下面,我们将对这几个有关于C++继承与多态的关键内容进行详述的论述
浅谈C++的继承与多态
- 一、继承的本质及其原理
- 二、派生类的构造和析构过程
- 三、重载、隐藏和覆盖
- 四、类的向下或向上转型
- 五、静态绑定与动态绑定
- **一个问题:thinking::thinking::thinking::一个类声明了虚函数后,当调用该虚函数时,一定是动态绑定吗?** **不一定 !**
- 六、虚析构函数
- **什么时候把基类的析构函数声明为虚函数呢?**
- 七、虚函数对类的影响
一、继承的本质及其原理
在C++中,继承是一种面向对象编程思想的概念,允许一个类继承另一个类的属性和方法,同时也可以在此基础上添加新的属性和方法。究其本质,即:
- 代码的复用
- 在基类中给所有派生类提供一个统一的接口,让派生类重写,满足开闭原则
二、派生类的构造和析构过程
在定义的派生类中,由于派生类是从基类的基础上继承过来的,对应继承而来的成员的初始化和清理是由基类的构造函数和析构函数负责,而派生类的构造和析构函数则负责初始化和清理派生类中特定的部分,所以在派生类调用构造函数需要初始化基类里的成员变量时,不能直接指定,而是需要通过基类的构造函数来进行初始化,如下:
class Base
{
public:Base(int d) :ma(d) { cout << "Base()" << endl; }~Base() { cout << "~Base()" << endl; }
protected:int ma;
};class Derive :public Base
{
public://Derive(int d = 20) :ma(d), mb(d) {cout << "Derive()" << endl;}//此句会出现报错:"ma" 不是类 "Derive" 的非静态数据成员或基类Derive(int d = 20) :Base(d), mb(d) {cout << "Derive()" << endl;}~Derive() { cout << "~Derive()" << endl; }
private:int mb;
};
这里我们给出一段关于派生类构造和析构过程的主要描述:
- 派生类调用基类的构造函数,初始化从基类继承而来的成员
- 调用派生类自己的构造函数,初始化派生类自己特有的成员
- 调用派生类的析构函数,释放派生类成员可能占用的外部资源(堆空间、文件等)
- 调用基类的构造函数,释放派生类内存中从基类继承而来的成员可以占用的外部资源(堆空间、文件等)
int main()
{Derive m(20); //可以自己允许一遍,查看程序允许结果return 0;
}
三、重载、隐藏和覆盖
重载: 一组函数重载,必须处在同一个作用域中,且函数名相同,参数列表不同
隐藏: 隐藏其实就是隐藏作用域,即,在继承结构中,派生类的同名成员,把基类的同名成员隐藏掉了
覆盖: 如果派生类中定义的方法与在基类继承而来的方法,在返回值、函数名、参数列表都相同的前提下,且基类的实现方法为virtual
虚函数,那么派生类的这个方法就会被自动的处理成虚函数,本质上是虚函数表上的虚函数地址的覆盖,详见后文!
若我们在上面的Base
类和Derive
中添加方法 void show()
,在派生类调用show
函数时会优先在自己类中寻找对应的函数方法,即会打印Derive::show()
,基类的show
方法被隐藏了
class Base
{...void show() { cout << "Base::show()" << endl; }...
};
class Derive :public Base
{void show() { cout << "Derive::show()" << endl; }...
};int main()
{Derive m(20);m.show(); //优先找派生类自己show成员return 0;
}
四、类的向下或向上转型
在C++中,派生类对象转化为基类对象、基类对象转化为派生类对象的情形可以描述为:
- 向上转型: 派生类对象(指针) -> 基类对象(指针) YES
- 向下转型: 基类对象(指针) -> 派生类对象(指针) NO
由于派生类是由基类继承而来的,其给它分配地址空间大于基类的地址空间,故可以将派生类对象(指针)转化为基类对象(指针),即Base* pb = &m
,而不可以可以将基类类对象(指针)转化为派生类对象(指针),即Derive* pd = &m;
,如下图所示:
对于 派生类 -》 基类: 相当于切片,红色部分内存在基类指针访问不了,也不会被访问
对于 基类 -》 派生类: 派生类申请的地址空间大于基类,一旦派生类对象/指针访问红色部分时,由于基类对象是没有这块空间的,直接会发生非法访问问题
五、静态绑定与动态绑定
静态绑定和动态绑定是C++中两种不同的绑定方式
- 静态绑定: 在编译时进行的绑定。它根据函数调用时的静态类型来确定要调用的函数。静态绑定适用于非虚函数,它会默认绑定到函数定义中的相应代码。
- 动态绑定: 在运行时进行的绑定。它根据函数调用时的动态类型来确定要调用的函数。动态绑定适用于虚函数,它允许在运行时根据实际对象类型来调用相应的函数。这样可以实现多态性,即不同对象调用同名函数时可以执行不同的操作。
#include <iostream>class Base {
public:void print() {std::cout << "Base class" << std::endl;}virtual void display() {std::cout << "Base class" << std::endl;}
};class Derived : public Base {
public:void print() {std::cout << "Derived class" << std::endl;}void display() {std::cout << "Derived class" << std::endl;}
};int main() {Base* d = new Derived();d->print(); // 静态绑定,调用Base类的print函数/*mov eax, dword ptr[pb]mov ecx, dword ptr[eax]call ecx;(虚函数地址) 动态(运行时期)的绑定(函数的调用)*/d->display(); // 动态绑定,调用Derived类的display函数return 0;
}
d->print()
调用的是静态绑定,因为 print() 函数在基类中不是虚函数。即使 d 指向的对象实际类型是 Derived 类,编译器也会根据指针类型(Base*)来静态绑定调用基类的 print() 函数。所以输出为 Base class
d->display()
这里调用的是动态绑定,因为 display() 函数是虚函数。在运行时,实际调用的是指向的对象的类型的版本,即RTTI (run-time type information)
,即 Derived 类中的 display() 函数。所以输出为 Derived class
一个问题🤔🤔🤔:一个类声明了虚函数后,当调用该虚函数时,一定是动态绑定吗? 不一定 !
动态绑定的实现通常涉及虚函数表(vtable)的使用。每个包含虚函数的类都会有一个虚函数表,其中包含了指向虚函数的指针。子类会继承父类的虚函数表,并在其自己的虚函数表中重写父类的虚函数指针。这样,当通过指针或引用调用虚函数时,会根据对象的实际类型找到对应的虚函数指针,并调用正确的函数。
然而,如果直接通过对象调用虚函数,编译器会根据对象的静态类型来进行绑定,即根据对象的声明类型来调用虚函数,而这就是静态绑定,因为编译器在编译时已经确定了应该调用哪个函数,不需要在运行时根据对象的实际类型进行判断。因此,在通过指针或引用调用虚函数时,会进行动态绑定,根据对象的实际类型确定调用哪个函数;而在直接通过对象调用虚函数时,会进行静态绑定,根据对象的声明类型确定调用哪个函数
六、虚析构函数
虚析构函数是一种特殊的析构函数,用于实现多态性。它允许在基类指针指向派生类对象时,使用基类指针来调用派生类对象的析构函数,从而正确地释放派生类对象的资源,我们先来看看下面几个问题
我们来看看虚函数所依赖的条件:
- 虚函数能产生地址,存储在vftable虚函数表中
- 对象必须存在(vfptr -> vftable -> 虚函数地址)
哪些函数不能声明为虚函数?
- 构造函数:构造函数在对象创建和销毁的过程中是特殊的,在构造函数中的所有函数都是静态绑定的,不能声明为virtual
- 静态成员函数static:虚函数是通过对象访问的,而静态成员函数没有 this 指针,是直接通过类名访问的,不能被继承和覆盖
- 内联函数:内联函数在编译时会直接将函数体嵌入到函数调用点,没有函数调用的开销。而虚函数是通过虚函数表来确定的,无法进行内联
什么时候把基类的析构函数声明为虚函数呢?
当基类指针(或引用)指向在堆上通过new创建的派生类对象时,派生类对象也会分配一份外部空间。如果基类的析构函数没有声明为虚函数,在准备使用delete删除基类指针时,会发生静态绑定。这样,基类对象会调用基类的析构函数,而派生类对象则无法调用其自己的析构函数,导致内存泄漏。
class Base { public:Base(int d) :ma(d) { cout << "Base()" << endl; }~Base() { cout << "~Base()" << endl; }virtual void show() { cout << "Base::show()" << >endl; } protected:int ma; };class Derive :public Base { public:Derive(int d) :Base(d), mb(d), ptr(new int(d)){cout << "Derive()" << endl;}// 基类的析构函数是virtual,那么派生类的析构>函数自动变成virtual~Derive(){delete ptr;cout << "~Derive()" << endl;}void show() { cout << "Derive::show()" << endl; }private:int mb;int* ptr; };int main() {Base* pb = new Derive(10);pb->show();delete pb;return 0; }
在将基类的析构函数声明为虚函数后,当使用delete删除基类指针时,由于基类的析构函数是虚函数,会发生动态绑定。这样,派生类的析构函数会自动成为虚析构函数。在执行delete时,通过虚函数表指针(vfptr)找到虚函数表(vftable),将基类在虚函数表上的虚析构函数覆盖为派生类的虚析构函数。这样就会先调用派生类的析构函数,再调用基类的虚构函数进行释放。
七、虚函数对类的影响
-
一个类里面定义了虚函数,那么编译阶段,编译器会给这个类生成唯一的虚函数表
vftable
主要存放的是RTTI指针
和虚函数地址
,当程序运行时,每一张虚函数表都会加载到内存的.rodate
区(只读,不能写) -
一个类中定义了虚函数,那么这个类定义的对象在程序运行时,内存中开始的部分多存储一个
vfptr虚函数指针
,指向相同类型的虚函数表vftable
,一个类型定义的n个对象,它们的vfptr
指向的都是同一张虚函数表 -
一个类里的虚函数个数,不影响对象内存的大小(vfptr),影响的是虚函数表的大小
-
如果派生类中的方法和基类继承而来的某个方法的返回值、函数名、参数列表相同,而基类的方法是virtual虚函数,那么派生类的这个方法会自动被处理成虚函数,将虚函数表中的原来的虚函数地址覆盖成派生类的虚函数地址
🌻🌻🌻以上就是有关浅浅谈C++的继承与多态(静态绑定、动态绑定和虚函数等)的内容,如果聪明的你浏览到这篇文章并觉得文章内容对你有帮助,请不吝动动手指,给博主一个小小的赞和收藏 🌻🌻🌻
相关文章:

浅谈C++的继承与多态(静态绑定、动态绑定和虚函数等)
今天我们来谈谈C的继承与多态😊😊😊,本篇的关键内容如下: 继承的本质及其原理派生类的构造和析构过程重载、隐藏和覆盖类的向下或向上转型静态绑定与动态绑定虚函数对类的影响虚析构函数 下面,我们将对这…...
【无人机综合考试题】
1.请选择出哪一个功能选项,在手动遥控飞行时,可以改变各通道的操作灵敏度? 行程比例在手动遥控飞行时,可以改变各通道的操作灵敏度 用于起降的遥控器中 THR、ELE 通道分别控制多旋翼无人机的什么运动? AIL(左、右移动)RUD(左、右水平旋转…...

JS精度计算的几种解决方法,1、转换成整数计算后再转换成小数,2、toFixed,3、math.js,4、bignumber.js,5、big.js
提示:学习express,搭建管理系统 文章目录 前言一、转换成整数计算后再转换成小数二、toFixed三、math.js四、bignumber.js五、big.js总结 前言 原始计算 let aNum 6.6 0.3;let bNum 6.6 - 0.2;let cNum 6.6 * 0.3;let dNum 6.6 / 0.2;console.log(…...

v77.递归
理解: 函数直接或者间接地调用自身;并且有边界条件。 1: #include <stdio.h> int main() {int result fun(3);printf("%d",result);return 0 ; } int fun(int num) {if(num 1)return num;return num fun(num-1); }思路…...
Spring Cloud微服务功能及其组件详细讲解
Spring Cloud微服务功能及其组件详细讲解 文章目录 Spring Cloud微服务功能及其组件详细讲解前言:什么是微服务?一、Spring Cloud原理简述二、核心组件1、服务发现——Nacos/Eureka/Consul1)Nacos服务发现2)Eureka服务发现3&#…...

(三维重建学习)已有位姿放入colmap和3D Gaussian Splatting训练
这里写目录标题 一、colmap解算数据放入高斯1. 将稀疏重建的文件放入高斯2. 将稠密重建的文件放入高斯 二、vkitti数据放入高斯 一、colmap解算数据放入高斯 运行Colmap.bat文件之后,进行稀疏重建和稠密重建之后可以得到如下文件结构。 1. 将稀疏重建的文件放入高…...
4635: 【搜索】【广度优先】回家
题目描述 小 H 在一个划分成了nm 个方格的长方形封锁线上。 每次他能向上下左右四个方向移动一格(当然小 H 不可以静止不动), 但不能离开封锁线,否则就被打死了。 刚开始时他有满血 6 点,每移动一格他要消耗 1 点血量…...

Uibot6.0 (RPA财务机器人师资培训第1天 )RPA+AI、RPA基础语法
训练网站:泓江科技 (lessonplan.cn)https://laiye.lessonplan.cn/list/ec0f5080-e1de-11ee-a1d8-3f479df4d981(本博客中会有部分课程ppt截屏,如有侵权请及请及时与小北我取得联系~) 紧接着小北之前的几篇博客,友友们我们即将开展新课的学习~…...

【吊打面试官系列】Redis篇 -Redis集群的主从复制模型是怎样的?
大家好,我是锋哥。今天分享关于 【Redis集群的主从复制模型是怎样的?】 面试题,希望对大家有帮助; Redis集群的主从复制模型是怎样的? 为了使在部分节点失败或者大部分节点无法通信的情况下集群仍然可用,所…...
高效的二进制列化格式 MessagePack 详解
目录 MessagePack 序列化原理 MessagePack 数据类型及编码方式 MessagePack 序列化与反序列化过程 MessagePack 的优势 应用场景 注意事项 小结 MessagePack (简称 msgPack)是一种高效的二进制序列化格式,可以将各种数据类型ÿ…...

鸿蒙Harmony应用开发—ArkTS-if/else:条件渲染
ArkTS提供了渲染控制的能力。条件渲染可根据应用的不同状态,使用if、else和else if渲染对应状态下的UI内容。 说明: 从API version 9开始,该接口支持在ArkTS卡片中使用。 使用规则 支持if、else和else if语句。 if、else if后跟随的条件语句…...
JAVA 100道题(14)
14.使用LinkedList实现一个简单的堆栈(Stack)数据结构。 下面是一个简单的Java程序,使用LinkedList来实现一个堆栈(Stack)数据结构。在这个程序中,我们定义了一个MyStack类,它包含了一些基本的堆…...

STM32+ESP8266水墨屏天气时钟:简易多级菜单(数组查表法)
项目背景 本次的水墨屏幕项目需要做一个多级菜单的显示,所以写出来一起学习,本篇文章不单单适合于水墨屏,像0.96OLED屏幕也适用,区别就是修改显示函数。 设计思路 多级菜单的实现,一般有两种实现的方法 1.通过双向…...

数学建模综合评价模型与决策方法
评价方法主要分为两类,其主要区别在确定权重的方法上 一类是主观赋权法,多次采取综合资讯评分确定权重,如综合指数法,模糊综合评判法,层次评判法,功效系数法等 另一类是客观赋权法,根据各指标…...

window下安装并使用nvm(含卸载node、卸载nvm、全局安装npm)
window下安装并使用nvm(含卸载node、卸载nvm、全局安装npm) 一、卸载node二、安装nvm三、配置路径和下载源四、使用nvm安装node五、nvm常用命令六、卸载nvm七、全局安装npm、cnpm八、遇到的问题 nvm 全名 node.js version management,顾名思义…...

Mysql——基础命令集合
目录 前期准备 先登录数据库 一、管理数据库 1.数据表结构解析 2.常用数据类型 3.适用所有类型的修饰符 4.使用数值型的修饰符 二、SQL语句 1.SQL语言分类 三、Mysql——Create,Show,Describe,Drop 1.创建数据库 2.查看数据库 3.切换数据库 4.创建数据表 5.查看…...
记录一次流相关故障
记录一次流相关故障 1、项目中有个JSON字典文件,通过流的方式加载进来,写了个输入流转字符串的方法,idea开发环境下运行一切正常,打成jar或者war包运行时,只能加载出部分数据,一开始怀疑过运行内存分配过小…...

linux源配置:ubuntu、centos;lspci与lsmod命令区别
1、ubuntu源配置 1)先查电脑版本型号: lsb_release -c2)再编辑源更新,源要与上面型号对应 参考:https://midoq.github.io/2022/05/30/Ubuntu20-04%E6%9B%B4%E6%8D%A2%E5%9B%BD%E5%86%85%E9%95%9C%E5%83%8F%E6%BA%90/ /etc/apt/…...

面试算法-88-反转链表
题目 给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。 示例 1: 输入:head [1,2,3,4,5] 输出:[5,4,3,2,1] 解 class Solution {public ListNode reverseList(ListNode head) {if(head null || hea…...

如何在个人Windows电脑搭建Cloudreve云盘并实现无公网IP远程访问
文章目录 1、前言2、本地网站搭建2.1 环境使用2.2 支持组件选择2.3 网页安装2.4 测试和使用2.5 问题解决 3、本地网页发布3.1 cpolar云端设置3.2 cpolar本地设置 4、公网访问测试5、结语 1、前言 自云存储概念兴起已经有段时间了,各互联网大厂也纷纷加入战局&#…...

Docker 离线安装指南
参考文章 1、确认操作系统类型及内核版本 Docker依赖于Linux内核的一些特性,不同版本的Docker对内核版本有不同要求。例如,Docker 17.06及之后的版本通常需要Linux内核3.10及以上版本,Docker17.09及更高版本对应Linux内核4.9.x及更高版本。…...

YSYX学习记录(八)
C语言,练习0: 先创建一个文件夹,我用的是物理机: 安装build-essential 练习1: 我注释掉了 #include <stdio.h> 出现下面错误 在你的文本编辑器中打开ex1文件,随机修改或删除一部分,之后…...
解决本地部署 SmolVLM2 大语言模型运行 flash-attn 报错
出现的问题 安装 flash-attn 会一直卡在 build 那一步或者运行报错 解决办法 是因为你安装的 flash-attn 版本没有对应上,所以报错,到 https://github.com/Dao-AILab/flash-attention/releases 下载对应版本,cu、torch、cp 的版本一定要对…...
C++.OpenGL (10/64)基础光照(Basic Lighting)
基础光照(Basic Lighting) 冯氏光照模型(Phong Lighting Model) #mermaid-svg-GLdskXwWINxNGHso {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-GLdskXwWINxNGHso .error-icon{fill:#552222;}#mermaid-svg-GLd…...
Spring Boot+Neo4j知识图谱实战:3步搭建智能关系网络!
一、引言 在数据驱动的背景下,知识图谱凭借其高效的信息组织能力,正逐步成为各行业应用的关键技术。本文聚焦 Spring Boot与Neo4j图数据库的技术结合,探讨知识图谱开发的实现细节,帮助读者掌握该技术栈在实际项目中的落地方法。 …...

【数据分析】R版IntelliGenes用于生物标志物发现的可解释机器学习
禁止商业或二改转载,仅供自学使用,侵权必究,如需截取部分内容请后台联系作者! 文章目录 介绍流程步骤1. 输入数据2. 特征选择3. 模型训练4. I-Genes 评分计算5. 输出结果 IntelliGenesR 安装包1. 特征选择2. 模型训练和评估3. I-Genes 评分计…...

STM32---外部32.768K晶振(LSE)无法起振问题
晶振是否起振主要就检查两个1、晶振与MCU是否兼容;2、晶振的负载电容是否匹配 目录 一、判断晶振与MCU是否兼容 二、判断负载电容是否匹配 1. 晶振负载电容(CL)与匹配电容(CL1、CL2)的关系 2. 如何选择 CL1 和 CL…...
Python实现简单音频数据压缩与解压算法
Python实现简单音频数据压缩与解压算法 引言 在音频数据处理中,压缩算法是降低存储成本和传输效率的关键技术。Python作为一门灵活且功能强大的编程语言,提供了丰富的库和工具来实现音频数据的压缩与解压。本文将通过一个简单的音频数据压缩与解压算法…...

aardio 自动识别验证码输入
技术尝试 上周在发学习日志时有网友提议“在网页上识别验证码”,于是尝试整合图像识别与网页自动化技术,完成了这套模拟登录流程。核心思路是:截图验证码→OCR识别→自动填充表单→提交并验证结果。 代码在这里 import soImage; import we…...

车载诊断架构 --- ZEVonUDS(J1979-3)简介第一篇
我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 做到欲望极简,了解自己的真实欲望,不受外在潮流的影响,不盲从,不跟风。把自己的精力全部用在自己。一是去掉多余,凡事找规律,基础是诚信;二是…...