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

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++进阶之路上提供一些启发和帮助,祝愿你在编程的世界中不断进步,享受编程的乐趣!

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • CSS原子化
  • 2024年7月1日,公布的OpenSSH的漏洞【CVE-2024-6387】
  • 刷算法Leetcode---9(二叉树篇Ⅲ)
  • Vue.js 中属性绑定的详细解析:冒号 `:` 和非冒号的区别
  • 1125 子串与子列
  • 【3】迁移学习模型
  • 软件开发面试题(C#语言,.NET框架)
  • 【Linux进阶】Linux目录配置,FHS
  • MyBatis学习笔记-参数转义处理
  • App UI性能测试 - PerfDog使用全教程
  • 如何实现一套键盘鼠标控制两台计算机(Mouse Without Borders快速上手教程)
  • Docker搭建MySQL双主复制详细教程
  • 【前端】从零开始学习编写HTML
  • HNU小学期BSP软件编程基础十道测试题
  • Java学习
  • 9月CHINA-PUB-OPENDAY技术沙龙——IPHONE
  • CSS相对定位
  • GraphQL学习过程应该是这样的
  • Hibernate最全面试题
  • Vue全家桶实现一个Web App
  • 阿里云Kubernetes容器服务上体验Knative
  • 阿里云ubuntu14.04 Nginx反向代理Nodejs
  • 从零开始的无人驾驶 1
  • 分享一个自己写的基于canvas的原生js图片爆炸插件
  • 关于字符编码你应该知道的事情
  • 每个JavaScript开发人员应阅读的书【1】 - JavaScript: The Good Parts
  • 少走弯路,给Java 1~5 年程序员的建议
  • 适配iPhoneX、iPhoneXs、iPhoneXs Max、iPhoneXr 屏幕尺寸及安全区域
  • 小程序、APP Store 需要的 SSL 证书是个什么东西?
  • 你学不懂C语言,是因为不懂编写C程序的7个步骤 ...
  • 昨天1024程序员节,我故意写了个死循环~
  • # Swust 12th acm 邀请赛# [ A ] A+B problem [题解]
  • #pragam once 和 #ifndef 预编译头
  • $$$$GB2312-80区位编码表$$$$
  • (1)无线电失控保护(二)
  • (14)学习笔记:动手深度学习(Pytorch神经网络基础)
  • (20050108)又读《平凡的世界》
  • (39)STM32——FLASH闪存
  • (solr系列:一)使用tomcat部署solr服务
  • (五) 一起学 Unix 环境高级编程 (APUE) 之 进程环境
  • (一)eclipse Dynamic web project 工程目录以及文件路径问题
  • (转载)VS2010/MFC编程入门之三十四(菜单:VS2010菜单资源详解)
  • .net 前台table如何加一列下拉框_如何用Word编辑参考文献
  • .net 提取注释生成API文档 帮助文档
  • .NET的微型Web框架 Nancy
  • .NET面试题解析(11)-SQL语言基础及数据库基本原理
  • .NET与 java通用的3DES加密解密方法
  • /usr/local/nginx/logs/nginx.pid failed (2: No such file or directory)
  • /var/lib/dpkg/lock 锁定问题
  • @configuration注解_2w字长文给你讲透了配置类为什么要添加 @Configuration注解
  • [ CTF ] WriteUp- 2022年第三届“网鼎杯”网络安全大赛(朱雀组)
  • [ IO.File ] FileSystemWatcher
  • [ vulhub漏洞复现篇 ] Grafana任意文件读取漏洞CVE-2021-43798
  • [ vulhub漏洞复现篇 ] Hadoop-yarn-RPC 未授权访问漏洞复现
  • [C#]将opencvsharp的Mat对象转成onnxruntime的inputtensor的3种方法