【C++】封装、继承和多态

引言

在现代软件开发中,面向对象编程(Object Oriented Programming)已经成为一种广泛应用的编程范式。C++作为一种支持面向对象编程的语言,在封装、继承和多态方面提供了强大的特性。本文将介绍C++中的封装、继承和多态概念,并通过简单示例来说明它们的使用方法。

一、封装

  • 封装可以隐藏实现细节,使得代码模块化,使代码和功能独立
  • 封装是把函数和数据包围起来,对数据的访问只能通过可信任的对象和类进行访问,对不可信的进行信息隐藏。
  • 在面向对象编程上可理解为:把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。

二、继承

继承的概念和定义

  • 继承是指这样一种能力:它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。其继承的过程,就是从一般到特殊的过程。
  • 通过继承创建的新类称为“子类”或“派生类”。被继承的类称为“基类”、“父类”或“超类”。要实现继承,可以通过“继承”(Inheritance)和“组合”(Composition)来实现。在C++当中,一个子类可以继承多个基类。

继承实现的三种方式

  • 继承概念的实现方式有三类:实现继承接口继承可视继承
  • 实现继承:指使用基类的属性和方法而无需额外编码的能力;
  • 接口继承:指仅使用属性和方法的名称、但是子类必须提供实现的能力;
  • 可视继承:指子窗体(类)使用基窗体(类)的外观和实现代码的能力。

继承基类成员访问方式的变化

类成员/继承方式

public继承

protected继承private继承
父类public成员子类public成员子类protected成员子类private成员
父类protected成员子类protected成员子类protected成员子类private成员
父类private成员子类中不可见子类中不可见子类中不可见

由上表可知:

  • 基类private成员在子类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。但如果是派生类调用基类当中访问了private成员的函数,那么也相当于间接访问。
  • 基类成员在被继承的时候,在派生类当中的访问方式会取该成员在基类当中的访问限定符和继承方式当中权限较小的一个,public > protected > private。
  • class默认的继承方式是private,而struct的默认继承方式是public。但在实际运用过程当中一般是采用public继承。
  • public成员是被公开的,在类里或者类外都能被直接访问;protected成员是能在派生类当中可见,类外不可见;private成员只能在类里被访问,外界和派生类都不能访问。

赋值转换

派生类对象可以赋值给基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去。注:切片不存在类型转换时构造临时对象,而是直接赋值。

  • 子类对象可以赋值给父类对象、指针和引用。
  • 基类对象不能赋值给派生类对象。
  • 基类的指针可以通过强制类型转换赋值给派生类的指针。注:如果该基类指针本身是指向派生类对象,访问派生类成员时,不存在越界问题,但是如果该指针是指向基类对象,访问派生类成员会存在越界问题。

函数隐藏

  • 在继承体系中基类和派生类都有独立的作用域。所以即使基类和派生类存在同名函数,不会构成函数重载,因为函数重载要求在同一作用域。
  • 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏(优先访问子类同名成员),也叫重定义。可以加类域限定符,来指定访问的是父类还是子类中的同名成员。
  • 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏

派生类默认成员函数

在派生类中,如果不自己写成员函数,那么编译器会自动生成默认成员函数。

  • 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
  • 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
  • 派生类的operator=必须要调用基类的operator=完成基类的赋值。
  • 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
  • 派生类对象初始化先调用基类构造再调派生类构造。
  • 派生类对象析构清理先调用派生类析构再调基类的析构。
  • 因为后续一些场景析构函数需要构成重写,重写的条件之一是函数名相同。那么编译器会对析构函数名进行特殊处理,处理成destrutor(),所以父类析构函数不加virtual的情况下,子类析构函数和父类析构函数构成隐藏关系。

注:子类成员会自动去调用父类的构造函数,不能自己去访问父类的私有成员进行初始化。拷贝构造也是如此,因为拷贝构造中是父类引用,就对应了赋值兼容规则,传过来子类对象,对其进行切片,得到父类那一部分的引用,从而进行拷贝构造。赋值也是如此。析构函数建议设置为虚函数让子类进行重写。

Student(const Student& s): Person(s), _num(s._num)f{}Student& operator=( const Student& s)
{if (this != &s){operator=(s);_num = s._num;}return *this;
}

但此处的operator=和父类中的operator=构成了隐藏,所以会自己调用自己,死循环,导致栈溢出。所以要加类域限制符,指定调用父类里面的=。

总结

  • 子类拷贝构造和赋值可以不写,会自动去调用父类中的拷贝构造和赋值,为了避免深拷贝的情况出现,才会有自己写子类的拷贝构造和赋值的过程。
  • 子类的析构函数不能显式去调用父类的析构函数,因为在子类对象中先构造父类部分再构造子类部分,为了保证子类部分先析构,父类部分后析构,所以不能显式调用,打破顺序。
  • 友元关系不能继承。
  • 静态成员变量即使被继承,也只存在这一个变量,但是子类和父类都可以访问。

实现一个不能被继承的类? 

把构造函数和析构函数私有化,那么子类就不能构造或者析构对象了。

菱形继承

菱形继承是多继承的一种特殊情况,由图可知,b类和c类继承了a类,而d类又同时继承了b类和c类,这种情况一般就被称为菱形继承。

数据冗余和二义性

b类和c类分别继承了一份a类中的成员,而d类又继承了b类和c类,那么d一定存在两份a类的成员,这就产生了数据冗余,因为根本不需要多一份的a类成员,同时,当对这个成员进行访问的时候,也会会产生到底是访问b类中的a类成员,还是c类中的a类成员,指向不明确,这就是二义性。

如何解决?——虚继承

所谓虚继承(virtual)就是子类中只有一份间接父类的数据。该技术用于解决多继承中的父类为非虚基类时出现的数据冗余问题,即菱形继承问题。

在继承时,在继承方式的前面加上一个virtual关键字即可。

由上图可知:在d类对象当中,只存在了一份a类成员,而在b类(8c cd aa 00)和c类(ac cb aa 00)对象的起始位置存放的是一个虚基表指针,而虚基表当中存放就是当前成员到a类成员的偏移量。

虚基表:每个虚继承的子类都有一个虚基表指针,虚基表里面存放偏移量,通过偏移量找到唯一的成员,从而解决了数据冗余和二义性。

对于菱形虚拟继承:先继承的类,先进行初始化,且被多继承的类,只会进行一次初始化。

继承和组合

继承是派生类继承子类,组合则是一个类中包含另一个类的对象。

  • 继承:is-a 学生是人 采用继承的方式
  • 组合:has-a 车有轮胎 采用组合的方式

实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有些关系就适合继承那就用继承,另外要实现多态,也必须要继承。可以继承,可以组合,优先使用组合。

三、多态

多态的概念

  • 多态是同一个行为具有多个不同表现形式或形态的能力。
  • 多态就是同一个接口,使用不同的实例而执行不同操作
  • 允许派生类类型指针或引用赋值给基类类型的指针或引用

动态绑定和静态绑定

  • 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载
  • 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。

多态的实现——虚函数

虚函数:被virtual修饰的类成员函数可称为虚函数。
虚函数的作用:允许在派生类中重新定义与基类同名的函数,并且可以通过基类指针或引用来访问基类和派生类中的同名函数。

虚函数的重写:派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),在派生类当中重写了该同名函数,称为虚函数重写。(函数重写\覆盖)

特例:

  • 协变:允许虚函数的返回值不同,但即使不同,两个返回值类型的关系必须构成派生类和基类的关系。
  • 析构函数的重写:如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor

接口继承和实现继承

普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。

纯虚函数

在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口
类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生
类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

虚函数实现多态的条件

  1. 虚函数的重写:父类中的函数加了virtual,子类对该函数的重写就可以不加virtual。继承的是接口,即包括了函数当中的缺省参数,重写的只是函数实现。
  2. 通过基类的指针或者引用调用:传递派生类对象时,基类的指针或者引用可以进行切片,无论派生类对象还是基类对象,其处理过程都一样。

函数重载、覆盖、隐藏的对比

final和override

  • final:修饰虚函数,表示该虚函数不能被重写。
  • override:检查派生类虚函数是否重写了基类的虚函数,如果没有则编译报错。

虚函数表

每个包含了虚函数的类都包含一个虚表。 
我们知道,当一个类(A)继承另一个类(B)时,类A会继承类B的函数的调用权。所以如果一个基类包含了虚函数,那么其继承类也可调用这些虚函数,换句话说,一个类继承了包含虚函数的基类,那么这个类也拥有自己的虚表。

class A {
public:virtual void vfunc1();virtual void vfunc2();void func1();void func2();
private:int m_data1, m_data2;
};

因为类A存在vfunc1和vfunc2两个虚函数,所以存在一个虚表,虚表里面存在两个虚函数指针。

虚表是一个指针数组,存放的是虚函数指针,并且数组最后一般放的nullptr。普通的成员函数即非虚函数,其调用并不需要经过虚表,所以虚表的元素并不包括普通成员函数的函数指针。 
虚函数指针的赋值发生在编译器的编译阶段,也就是说在代码的编译阶段,虚表就可以构造出来了。

虚表指针

虚表是属于类的,而不是属于某个具体的对象,一个类只需要一个虚表即可。同一个类的所有对象都使用同一个虚表。 为了指定对象的虚表,对象内部包含一个虚表的指针,来指向自己所使用的虚表。为了让每个包含虚表的类的对象都拥有一个虚表指针,编译器在类中添加了一个指针,*__vptr,用来指向虚表。这样,当类的对象在创建时便拥有了这个指针,且这个指针的值会自动被设置为指向类的虚表。

以下代码能更好诠释通过虚函数来实现多态的过程:

class A {
public:virtual void vfunc1();virtual void vfunc2();void func1();void func2();
private:int m_data1, m_data2;
};class B : public A {
public:virtual void vfunc1();void func1();
private:int m_data3;
};class C: public B {
public:virtual void vfunc2();void func2();
private:int m_data1, m_data4;
};

A类存在两个虚函数vfunc1和vfunc2,B类继承A类之后,对vfun1进行了重写,C类继承B类,对vfunc2进行了重写,所以会得到下面的关系:



总结一下派生类的虚表生成:

a.先将基类中的虚表内容拷贝一份到派生类虚表中

b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数

c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。

注:在继承两个父类之后,子类中如果有虚函数存在,那么这个虚函数会放在第一个继承的父类的虚表指针当中,且子类对父类的虚函数重写之后,继承的两个虚表里面的虚函数都会被覆盖,但在监视窗口中,地址居然不一样。是因为调用的子类的成员函数,必须要this指针来调用,那么this指针的位置必须指向对象开头的位置,为了修正this指针,可能会多一些操作,ecx里面一般存放的就是this指针。

通过代码验证也可以得知,虚表放在虚基表的前面,为了方便找到虚表,虚基表中还存放了距离虚表的偏移量,例如,全f是-1,那fc就是-4,可以理解为是找到虚表位置。

虚函数相关问题

  • inline函数可以是虚函数吗?可以,但是编译器就会直接忽略掉inline属性,这个函数不再是inline,因为虚函数要放到虚表当中去。
  • 静态成员函数可以是虚函数吗?不可以,因为虚函数还是成员函数,需要通过this指针调用,但静态成员函数不需要this指针进行调用,所以静态成员函数无法放进虚表当中。
  • 构造函数可以是虚函数吗?不可以,对象中的虚表指针是在构造函数的初始化列表时被初始化的。
  • 对象访问普通函数快还是虚函数更快?如果是对象去调用,是一样快,但如果是指针或者引用去调用,则调用普通函数更快,因为构成了多态,运行时需要到虚表当中去查找。
  • 虚表存在哪儿?虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。那么虚表存在哪的呢? vs下是存在代码段的
  • 虚表是什么时候生成的?对象中虚表指针什么时候初始化?虚表在编译阶段生成,而虚表指针在构造函数的初始化列表进行初始化。

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

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

相关文章

element plus:tree拖动节点交换位置和改变层级

图层list里有各种组件,用element plus的tree来渲染,可以把图片等组件到面板里,面板是容器,非容器组件,比如图片、文本等,就不能让其他组件拖进来。 主要在于allow-drop属性的回调函数编写,要理清…

数字逻辑电路基础-有限状态机

文章目录 一、有限状态机基本结构二、verilog写一个基础有限状态机(moore型状态机)三、完整代码一、有限状态机基本结构 本文主要介绍使用verilog编写有限状态机FSM(finite state machine),它主要由三部分组成,下一状态逻辑电路,当前状态时序逻辑电路和输出逻辑电路。 有…

ZYNQ之嵌入式开发04——自定义IP核实现呼吸灯、固化程序

文章目录 自定义IP核——呼吸灯实验固化程序 自定义IP核——呼吸灯实验 Xilinx官方提供了很多IP核,在Vivado的IP Catalog中可以查看这些IP核,在构建自己复杂的系统时,只使用Xilinx官方的免费IP核一般满足不了设计的要求,因此很多…

NOIP,CSP-J,CSP-S——高精度加减乘除

一、高精度加法 1、大整数的输入 int的范围,正负上下限大约为2.1*10^9; long long的范围,正负上下限大约为9.2*10^18; 如果整数成千上万位,那么这么大的整数我们如何处理? 方法:先用字符串输入,然后把每一个字符转换成为数字,存到一个int数组里 int数组中的一个位…

揭秘Faiss:大规模相似性搜索与聚类的技术神器深度解析!

Faiss(由Facebook AI Research开发)是一个用于高效相似性搜索和密集向量聚类的库。它用C编写,并提供Python绑定,旨在帮助研究人员和工程师在大规模数据集上进行快速的相似性搜索和聚类操作。 一、介绍: Faiss的核心功…

OSPF认证方式,ISIS简介,ISIS路由器类型

OSPF:转发,泛洪,丢弃

ROS 2边学边练(33)-- 写一个静态广播(C++)

前言 通过这一篇我们将了解并学习到如何广播静态坐标变换到tf2(由tf2来转换这些坐标系)。 发布静态变换对于定义机器人底座与其传感器或非移动部件之间的关系非常有用。例如,在以激光扫描仪中心的坐标系中推理激光扫描测量数据是最简单的。 这…

C++学习进阶版(一):用C++写简单的状态机实现

目录 一、基础知识 1、状态机 2、四大要素 3、描述方式 4、设计步骤 5、实现过程中需注意 (1) 状态定义 (2) 状态转换规则 (3) 输入处理 (4) 状态机的封装 (5…

本地部署Docker容器可视化图形管理工具DockerUI并实现无公网IP远程访问——“cpolar内网穿透”

文章目录 前言1. 安装部署DockerUI2. 安装cpolar内网穿透3. 配置DockerUI公网访问地址4. 公网远程访问DockerUI5. 固定DockerUI公网地址 前言 DockerUI是一个docker容器镜像的可视化图形化管理工具。DockerUI可以用来轻松构建、管理和维护docker环境。它是完全开源且免费的。基…

路由过滤与引入

1、实验拓扑 2、实验要求 1、按照图示配置 IP 地址,R1,R3,R4 上使用 1oopback口模拟业务网段 2、运行 oSPF,各自协议内部互通 3、R1 和 R2 运行 RIPv2,R2,R3和R4在 RIP 和 oSPF 间配置双向路由引入,要求除 R4 上的业务…

基于51单片机的温度、烟雾、防盗、GSM上报智能家居系统

基于51单片机的智能家居系统 (仿真+程序+原理图+设计报告) 功能介绍 具体功能: 1.DS18B20检测温度,MQ-2检测烟雾、ADC0832实现模数转换; 2.按键可以设置温度、烟雾浓度阈值&#x…

【Java--数据结构】提升你的编程段位:泛型入门指南,一看就会!

前言 泛型是一种编程概念,它允许我们编写可以适用于多种数据类型的代码。通过使用泛型,我们可以在编译时期将具体的数据类型作为参数传递给代码,从而实现代码的复用和灵活性。 在传统的编程中,我们通常需要为不同的数据类型编写不…

10 JavaScript学习:函数

函数的概念 JavaScript中的函数是一段可重复使用的代码块,它接受输入(称为参数),执行特定的任务,并返回一个值。函数可以被调用(或者说被执行),并且可以接受不同的输入来产生不同的…

提升效率!微信自动统计数据报表,轻松实现!

在数字化时代,提高工作效率是每个人的追求。下面就给大家分享一个能够自动统计微信号运营数据的神器——个微管理系统,让大家无需手动整理和计算,提高工作效率! 1、好友统计报表 它分为通讯录好友统计、新增好友统计和删除好友统…

python创建线程和结束线程

👽发现宝藏 前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。【点击进入巨牛的人工智能学习网站】。 python创建线程和结束线程 在 Python 中,线程是一种轻量级的执行单元&#xff…

Mysql 存在多条数据,按时间取最新的那一组数据

1、数据如下,获取每个用户最近的一次登录数据 思路1:order by group by 先根据UserIdLogInTime排序,再利用Group分组,即可得到每个User_Id的最新数据。 1 SELECT * FROM login_db l ORDER BY l.user_id, l.login_time DESC; 排…

【Linux】实现一个进度条

我们之前也学了gcc/vim/make和makefile,那么我们就用它们实现一个进度条。 在实现这个进度条之前,我们要先简单了解一下缓冲区和回车和换行的区别 缓冲区其实就是一块内存空间,我们先看这样一段代码 它的现象是先立马打印,三秒后程…

使用表格法插入公式和编号

如何将公式和编号优雅地插入到论文当中呢? 首先插入一个1行2列的表格 调整一下 输入公式方法一:感觉墨迹公式挺好用的,word自带的 输入公式方法二:图片转LATEX代码 这个方法更快 分享一个公式识别网站 图片识别得到LATEX代码&…

惠海H6212L DCDC同步降压芯片IC 24V30V36V48V转3.3V5V12V3A大电流方案 带线损

同步降压芯片IC 24V30V36V48V转3.3V5V12V3A大电流方案是一种电源管理方案,它采用同步整流技术,将较高的输入电压(如24V、30V、36V、48V)转换为较低的输出电压(如3.3V、5V、12V),并提供高达3A的大…

代码随想录训练营Day 29|Python|Leetcode|● 860.柠檬水找零 ● 406.根据身高重建队列 ● 452. 用最少数量的箭引爆气球

860.柠檬水找零 在柠檬水摊上,每一杯柠檬水的售价为 5 美元。顾客排队购买你的产品,(按账单 bills 支付的顺序)一次购买一杯。 每位顾客只买一杯柠檬水,然后向你付 5 美元、10 美元或 20 美元。你必须给每个顾客正确…