C++进阶:继承和多态

文章目录

  • ❤️继承
    • 🩷继承与友元
    • 🧡继承和静态成员
    • 💛菱形继承及菱形虚拟继承
    • 💚继承和组合
  • ❤️多态
    • 🩷什么是多态?
    • 🧡多态的定义以及实现
    • 💛虚函数
    • 💚虚函数的重写
    • 💙override和final
    • 🩵抽象类
  • ❤️总结

在这里插入图片描述

上一个C++继承的文章讲到了继承中的默认成员函数。
本篇文章接着上次的继续讲解。
上一篇文章:C++进阶:继承

❤️继承

🩷继承与友元

友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员

class Student;
class Person
{
public:
	friend void Display(const Person& p, const Student& s);
protected:
	string _name; // 姓名
};
class Student : public Person
{
protected:
	int _stuNum; // 学号
};
void Display(const Person& p, const Student& s)
{
	cout << p._name << endl;
	//cout << s._stuNum << endl;
}

例如上面这串代码,注释掉的那部分 就是错的,因为基类的友元函数无法访问到派生类保护或者私有成员。

🧡继承和静态成员

普通的成员变量在继承中,会重新生成一份在派生类中,但是静态成员还是会这样吗?首先对于普通成员变量来说,我们举个例子:

class Student;
class Person
{
public:
	string _name; // 姓名
};
class Student : public Person
{
public:
	int _stuNum; // 学号
};
int main()
{
	Student s;Person p;
	s._name = "li";p._name = "ll";
	cout << s._name << endl;
	cout << p._name << endl;
	return 0;
}

上面这个例子输出是:
在这里插入图片描述
很显然,这两个_name不是同一个_name,是两份_name,我们再来讨论静态成员的继承关系:
在这里插入图片描述

可以看见,如果将普通成员 变量改成静态成员变量之后 ,改变一个类的内容,另一个也会跟着改变,从中可以看出其实静态 成员变量只有一份,而且两个类中的静态成员变量是相同的。

💛菱形继承及菱形虚拟继承

什么是菱形继承?要了解菱形继承这个概念首先我们要了解什么是单继承什么是多继承?
单继承:一个子类继承一个父类的属性和方法
多继承:一个子类可以继承多个父类的属性和方法

什么是菱形继承呢?

在这里插入图片描述
类似于下图的关系就是菱形继承,菱形继承有什么特点呢?菱形继承继承了B但是B继承了A,C也同理,所以在D中有两份A,所以这里就导致了一些问题。二义性和冗余性

什么是二义性?
由于多个基类中存在同名成员,导致编译器无法确定应该调用哪个基类的成员,从而引发的不确定性和编译错误。

什么是冗余性?
由于多继承导致的重复继承同一个基类,使得相同的数据成员或方法被继承多次,从而产生多余的拷贝和不必要的资源占用。这种冗余性会导致效率低下和维护困难。

由于D中有两份A所以这里产生了重复,所以存在冗余性,在调用的时候,具体不知道调用哪个,所以这里产生了二义性,如何解决二义性呢?我们可以指定类域,就可以解决二义性,但是冗余性用当前所学的知识是解决不了的,所以C++引入了一个概念,叫做虚继承。

虚继承是 C++ 中的一种继承机制,旨在解决多继承中的菱形继承(钻石继承)问题。菱形继承问题发生在一个类通过多个路径继承同一个基类,从而导致重复继承基类的成员。

虚拟继承可以解决菱形继承的二义性和数据冗余的问题。如上面的继承关系,在B和C的继承A时使用虚拟继承,即可解决问题。需要注意的是,虚拟继承不要在其他地方去使用。

语法书写:

class Person
{
public :
 string _name ; // 姓名
};
class Student : virtual public Person
{
protected :
 int _num ; //学号
};
class Teacher : virtual public Person
{
protected :
 int _id ; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected :
 string _majorCourse ; // 主修课程
};

当继承将要产生过冗余信息的时候的类就用虚拟继承。
在这里插入图片描述

💚继承和组合

  • public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。
  • 组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。
  • 优先使用对象组合而不是继承。
  • 继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称
    为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的
    内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很
    大的影响。派生类和基类间的依赖关系很强,耦合度高。
  • 对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象
    来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复
    用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。
    组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被
    封装。
  • 实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有
    些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用
    继承,可以用组合,就用组合。

什么是组合呢?
class A{}; class B { private: int _b; A _a; };
上面就是一个对象的组合

❤️多态

🩷什么是多态?

面向对象编程中的一个核心概念,它允许对象以多种形式出现,并通过相同的接口来调用不同的实现。在 C++ 中,多态通常通过函数重载、运算符重载和虚函数来实现。多态的主要类型包括编译时多态(静态多态)和运行时多态(动态多态)。

简单来说多态就是:就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。
例如:每个动物都有叫声,我们将叫声写为一个函数,然后每个动物去继承这个 动物类,每个动物去调用叫声这个函数都会得到不同的结果,这就是多态。

多态分为两种:

  1. 静态多态:编译时多态是通过函数重载和运算符重载在编译时实现的。它允许同一个函数或运算符根据参数的不同执行不同的操作。
  2. 动态多态:运行时多态是通过虚函数和基类指针或引用在运行时实现的。它允许基类指针或引用调用派生类的重载方法。

本篇主要以动态多态为主。

🧡多态的定义以及实现

多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如Student继承了
Person。Person对象买票全价,Student对象买票半价。

在继承中要构成多态的必要条件(缺一不可):

  1. 虚函数重写
  2. 父类指针或者引用进行调用虚函数

💛虚函数

class Person {
public:
 virtual void BuyTicket() { cout << "买票-全价" << endl;}
};

在Person类中形如BuyTicket一类的函数都是虚函数。

💚虚函数的重写

虚函数的重写的概念:

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

class Person {
public:
 virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
 virtual void BuyTicket() { cout << "买票-半价" << endl; }

函数BuyTicket()是一个虚函数,Student继承了Person类,BuyTicket()在Student中重写。

void Func(Person& p)
{ p.BuyTicket(); }
int main()
{
Person ps;
Student st;
Func(ps);
Func(st);
 return 0;
}

函数Func是用父类的指针进行调用,很显然这构成了多态了,所以这里会根据实例化出来的对象的指向来调用具体函数Person调用的就是Person的BuyTicket,而Student调用的就是自己的BuyTicket。
在这里插入图片描述

虚函数重写的两个例外

  1. 协变(基类与派生类虚函数返回值类型不同)
    派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指
    针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。
class A{};
class B : public A {};
class Person {
public:
 virtual A* f() {return new A;}
};
class Student : public Person {
public:
 virtual B* f() {return new B;}
};
  1. 析构函数的重写(基类与派生类析构函数的名字不同)
    如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,
    都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,
    看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处
    理,编译后析构函数的名称统一处理成destructor。
class Person {
public:
 virtual ~Person() {cout << "~Person()" << endl;}
};
class Student : public Person {
public:
 virtual ~Student() { cout << "~Student()" << endl; }
};
// 只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用析构函数,才能构成多态,才能保证p1和p2指向的对象正确的调用析构函数。
int main()
{
 Person* p1 = new Person;
 Person* p2 = new Student;
 delete p1;
 delete p2;
 return 0;
}

如果上面基类不写virtual则会报错,将析构函数写成虚函数保证正常的析构的过程。

💙override和final

这是两个关键字。
override:

用于表示一个虚函数(virtual function)在派生类中重写了基类中的虚函数。使用override关键字可以帮助编译器检测是否正确地重写了基类的虚函数。如果函数签名不匹配,编译器会报错,这有助于避免一些常见的编程错误。

override用于检查一个类中是否构成虚函数重写。

final:

  1. 用于类:阻止该类被进一步继承。
  2. 用于虚函数:阻止该虚函数在派生类中被重写。
class Car
{
public:
 virtual void Drive() final {}
};
class Benz :public Car
{
public:
 virtual void Drive() {cout << "Benz-舒适" << endl;}
};

如图所示,上图就表示Drive函数不能被重写。

重载、重写、隐藏对比:
在这里插入图片描述

🩵抽象类

要讲什么是抽象类,我们先讲什么是纯虚函数。
纯虚函数:在虚函数的后面写上 =0 ,则这个函数为纯虚函数。
那什么是抽象类呢?
抽象类:包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。

派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

class Car
{
public:
virtual void Drive() = 0;
};
class Benz :public Car
{
public:
 virtual void Drive() { cout << "Benz-舒适" << endl; }
};
class BMW :public Car
{
public:
 virtual void Drive() { cout << "BMW-操控" << endl; }
};

注意:Drive不能实例化,如果在Benz中重写出Drive并且前面不写成虚函数,那么这个类也不能实例化。

❤️总结

当你掌握了C++中的继承和多态,你就迈出了成为真正优秀C++程序员的第一步。继承让你可以构建出层次化的类结构,通过代码复用和扩展实现高效的软件设计。多态则使得你的代码可以根据对象的实际类型动态地选择合适的函数实现,提升了程序的灵活性和可维护性。

通过继承和多态,你可以更加清晰地组织你的代码,将复杂的问题分解为简单的对象和行为,使得代码更易于理解和修改。同时,这些特性也是C++等面向对象编程语言的核心所在,掌握它们不仅仅是技术层面的提升,更是编程思维和设计能力的深化。

在你的学习之旅中,不断练习和实践是掌握继承和多态的关键。通过编写更复杂的项目和解决实际的编程问题,你将逐渐发现如何更好地利用这些特性来构建高效、可扩展的软件系统。

希望本文能为你在C++进阶之路上提供一些启发和帮助,祝愿你在编程的世界中不断进步,享受编程的乐趣!

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

图论·Day01

P3371 P4779 P3371 【模板】单源最短路径&#xff08;弱化版&#xff09; 注意的点&#xff1a; 边有重复&#xff0c;选择最小边&#xff01;对于SPFA算法容易出现重大BUG&#xff0c;没有负权值的边时不要使用&#xff01;&#xff01;&#xff01; 70分代码 朴素板dijsk…

打卡第7天-----哈希表

继续坚持✊,我现在看到leetcode上的题不再没有思路了,真的是思路决定出路,在做题之前一定要把思路梳理清楚。 一、四数相加 leetcode题目编号:第454题.四数相加II 题目描述: 给定四个包含整数的数组列表 A , B , C , D ,计算有多少个元组 (i, j, k, l) ,使得 A[i] + B[j…

蚁群算法(Ant Colony Optimization,ACO)讲解+代码实现

1.蚁群算法来源 蚁群算法&#xff08;Ant Colony Optimization&#xff0c;简称ACO&#xff09;是一种模拟自然界中蚂蚁寻找食物路径行为的优化算法&#xff0c;主要用于解决组合优化问题。它的灵感来源于意大利学者Marco Dorigo在1992年提出的蚂蚁系统模型。 蚁群算法的灵感来…

应急响应——勒索病毒

先上搜索引擎上搜 也可以用360来杀 但是都无法解密 可以解密的&#xff1a; linux

LeNet原理及代码实现

目录 1.原理及介绍 2.代码实现 2.1model.py 2.2model_train.py 2.3model.test.py 1.原理及介绍 2.代码实现 2.1model.py import torch from torch import nn from torchsummary import summaryclass LeNet(nn.Module):def __init__(self):super(LeNet, self).__init__…

uniapp 去掉小数末尾多余的0

文章目录 在uniapp或者一般的JavaScript环境中&#xff0c;要去掉小数末尾的0&#xff0c;可以使用以下几种方法&#xff1a; 使用parseFloat()函数 let num 123.4500; let result parseFloat(num); console.log(result); // 输出: 123.45字符串处理 将数字转换为字符串&am…

02day-C++学习(const 指针与引用的关系 inline nullptr)

02day-C学习 1. 使用const注意事项 注意事项 • 可以引⽤⼀个const对象&#xff0c;但是必须⽤const引⽤。const引⽤也可以引⽤普通对象&#xff0c;因为对象的访 问权限在引⽤过程中可以缩⼩&#xff0c;但是不能放⼤。 • 不需要注意的是类似 int& rb a3; double d 1…

代码随想录——合并区间(Leecode LCR74)

题目链接 贪心 排序 class Solution {public int[][] merge(int[][] intervals) {ArrayList<int[]> res new ArrayList<>();// 先将数组按照左区间排序Arrays.sort(intervals, new Comparator<int[]>() {public int compare(int[] intervals1, int[] in…

模板语法之插值语法{{}}——01

<主要研究&#xff1a;{{ 这里可以写什么}} 1.在data中声明的变量函数都可以 2.常量 3.只要是合法的JavaScript的表达式&#xff0c;都可以 4. 模板表达式都被放在沙盒中&#xff0c;只能访问全局变量的一个白名单&#xff0c;如 Math 和 Date <body> <div i…

STM32智能仓库管理系统教程

目录 引言环境准备智能仓库管理系统基础代码实现&#xff1a;实现智能仓库管理系统 4.1 数据采集模块 4.2 数据处理与控制算法 4.3 通信与网络系统实现 4.4 用户界面与数据可视化应用场景&#xff1a;仓库管理与优化问题解决方案与优化收尾与总结 1. 引言 智能仓库管理系统通…

深入理解循环神经网络(RNN)

深入理解循环神经网络&#xff08;RNN&#xff09; 循环神经网络&#xff08;Recurrent Neural Network, RNN&#xff09;是一类专门处理序列数据的神经网络&#xff0c;广泛应用于自然语言处理、时间序列预测、语音识别等领域。本文将详细解释RNN的基本结构、工作原理以及其优…

国际网课平台Udemy上的亚马逊云科技AWS免费高分课程和创建、维护EC2动手实践

亚马逊云科技(AWS)是全球云行业最&#x1f525;火的云平台&#xff0c;在全球经济形势不好的大背景下&#xff0c;通过网课学习亚马逊云科技AWS基础备考亚马逊云科技AWS证书&#xff0c;对于找工作或者无背景转行做AWS帮助巨大。欢迎大家关注小李哥&#xff0c;及时了解世界最前…

香橙派AIpro初体验:搭建无线随身NAS

文章目录 1.引言2. 香橙派 AIPro概述3. 开发准备3.0 烧录镜像3.1 需要准备的硬件3.2 需要准备的软件3.3 启动并连接香橙派 AIPro3.3.1 初始化启动香橙派 AIPro3.3.2 无线连接香橙派 AIPro3.3.3.3 VNC连接香橙派 AIPro 3.4 设置固定ip3.4.1 设置开机自动连接WIFI3.4.1 设置香橙派…

遍历请求后端数据引出的数组forEach异步操作的坑

有一个列表数据&#xff0c;每项数据里有一个额外的字段需要去调另外一个接口才能拿到&#xff0c;后端有现有的这2个接口&#xff0c;现在临时需要前端显示出来&#xff0c;所以这里需要前端先去调列表数据的接口拿到列表数据&#xff0c;然后再遍历请求另外一个接口去拿到对应…

springboot封装请求参数json的源码解析

源码位置&#xff1a; org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodArgumentResolver#readWithMessageConverters(org.springframework.http.HttpInputMessage, org.springframework.core.MethodParameter, java.lang.reflect.Type…

Java PKI Programmer‘s Guide

一、PKI程序员指南概述 PKI Programmer’s Guide Overview Java认证路径API由一系列类和接口组成&#xff0c;用于创建、构建和验证认证路径。这些路径也被称作认证链。实现可以通过基于提供者的接口插入。 这个API基于密码服务提供者架构&#xff0c;这在《Java密码架构参考指…

c++入门基础篇(上)

目录 前言&#xff1a; 1.c&#xff0b;&#xff0b;的第一个程序 2.命名空间 2.1 namespace的定义 2.2 命名空间使用 3.c&#xff0b;&#xff0b;输入&输出 4.缺省参数 5.函数重载 前言&#xff1a; 我们在之前学完了c语言的大部分语法知识&#xff0c;是不是意…

springboot驾校管理系统-计算机毕业设计源码49777

驾校管理系统 摘 要 驾校管理系统是一个基于Spring Boot框架开发的系统&#xff0c;旨在帮助驾校提高管理效率和服务水平。该系统主要实现了用户管理、年月类型管理、区域信息管理、驾校信息管理、车辆信息管理、报名信息管理、缴费信息管理、财务信息管理、教练分配管理、更换…

微搭低代码从入门到实战01创建数据源

目录 1 创建数据源2 创建字段总结 很多零基础的想学习低代码开发&#xff0c;苦于没有编程的经验感觉入门困难。本次教程就按照我们日常开发的思路&#xff0c;从浅入深逐步拆解一下低代码该如何学习。 开发软件&#xff0c;不管是管理后台还是小程序&#xff0c;先需要规划好数…

忘记Apple ID密码怎么退出苹果ID账号?

忘记Apple ID密码怎么退出账号&#xff1f;Apple ID对每个苹果用户来说都是必不可少的&#xff0c;没有它&#xff0c;用户就不能享受iCloud、App Store、iTunes等服务。苹果手机软件下载、丢失解锁、恢复出厂设置等都需要使用Apple ID。如果忘记Apple ID 密码&#xff0c;这会…