C++之多态的深度剖析

news2024/10/30 7:24:26

目录

前言

1.多态的概念

2.多态的定义及实现

2.1多态的构成条件

2.1.1重要条件

2.1.2 虚函数

2.1.3 虚函数的重写/覆盖

2.1.4 选择题

2.1.5 虚函数其他知识

协变(了解)

 析构函数的重写

override 和 final关键字

3. 重载,重写,隐藏的对比

 4.纯虚函数和抽象类

结束语


前言

在前面我们对C++的封装,继承等特性都有了了解和学习,接下来我们将对C++的第三大特性-多态进行认识和掌握。内容分为来两大部分,第一个是对多态的认识和运用,第二大部分是对多态原理的了解和扩展。

1.多态的概念

多态(Polymorphism)是面向对象编程(OOP)中的一个核心概念,它指的是同一个行为具有多个不同表现形式或形态的能力。在编程中,多态通常通过继承(inheritance)和接(interfaces来实现。

以下是多态的几个主要方面:

  1. 编译时多态(静态多态):这是在编译时确定的多态性,通常通过函数重载(function overloading)和模板(templates)来实现。编译器根据函数的参数类型或数量来决定调用哪个函数。

  2. 运行时多态(动态多态):这是在程序运行时确定的多态性,主要通过虚函数(virtual functions)和继承来实现。在运行时,根据对象的实际类型来调用相应的成员函数。

之所以叫编译时多态,是 因为他们实参传给形参的参数匹配是在编译时完成的,我们把编译时一般归为静态,运行时归为动态。
运行时多态,具体点就是去完成某个行为(函数),可以传不同的对象就会完成不同的行为,就达到多种 形态。比如买票这个行为,当普通人买票时,是全价买票;学生买票时,是优惠买票(5折或75折);军人买票时是优先买票。再比如,同样是动物叫的一个行为(函数),传猫对象过去,就是”(>^ω^<)喵“,传狗对象过去,就是"汪汪"。

多态的关键特性包括:

  • 继承:子类继承父类的属性和行为,可以对这些行为进行重写(override)。
  • 虚函数:在基类中声明为虚的成员函数,可以在派生类中被重写,使得通过基类指针或引用调用函数时,能够根据对象的实际类型来调用相应的函数版本。
  • 虚函数表:用于实现运行时多态的数据结构,它存储了虚函数的地址,使得程序能够在运行时确定调用哪个函数。
  • 向上转型:将派生类对象的引用或指针转换为基类类型的引用或指针,这是多态实现的基础。

2.多态的定义及实现

2.1多态的构成条件

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

2.1.1重要条件

被调用的函数必须是虚函数
指针或者引用调用虚函数
说明:要实现多态效果,第一必须是基类的指针或引用,因为只有基类的指针或引用才能既指向派生 类对象;第二派生类必须对基类的虚函数重写/覆盖,重写或者覆盖了,派生类才能有不同的函数,多 态的不同形态效果才能达到。
ac30984662c6415b9d1e6519bc6a0a3c.png

2.1.2 虚函数

类成员函数前面加virtual修饰,那么这个成员函数被称为虚函数。注意⾮成员函数不能加virtual修
饰。
class Person {
public:
	virtual void BuyTicket() {
		cout << "买票全额" << endl;
	}
};

2.1.3 虚函数的重写/覆盖

虚函数的重写/覆盖: 派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表(类型,数量)完全相同),称派生类的虚函数重写了基类的虚函数。
注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承 后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样 使用,不过在考试选择题中,经常会故意买这个坑,让判断是否构成多态

#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class Person {
public:
	virtual void BuyTicket() {
		cout << "买票全额" << endl;
	}
};
class Student : public Person {
public:
	virtual void BuyTicket() {
		cout << "学生票半价" << endl;
	}
};
//引用调用
void func(Person& p) {
	p.BuyTicket();
}
//指针调用
void func1(Person* p) {
	p->BuyTicket();
	// 这⾥可以看到虽然都是Person指针Ptr在调⽤BuyTicket
    // 但是跟ptr没关系,⽽是由ptr指向的对象决定的。
}
int main() {
	Person p1;
	Student s1;
	Person* p2 = new Person();
	Student* s2 = new Student();
	func(p1);
	func(s1);
	p1.BuyTicket();
	s1.BuyTicket();
	func1(&p1);
	func1(&s1);
	p2->BuyTicket();
	s2->BuyTicket();

	return 0;
}

 0a05807e075c4caca7e97d76458d4218.png

void func(Student& p) {
    p.BuyTicket();
}
//指针调用
void func1(Student* p) {
    p->BuyTicket();
}

如果改成Student,就会出问题,就不是多态了,也就不能传Person对象了。 

#include <iostream>
using namespace std;

class Pet {
public:
	virtual void eat() const{
		cout << "Eat food" << endl;
	}
};
class Dog : public Pet{
public:
	virtual void eat() const {
		cout << "Dog eats meat!" << endl;
	}
};
class Cat :public Pet {
public:
	virtual void eat()const {
		cout << "Cat eats fish!" << endl;
	}
};
void func(const Pet& p) {
	p.eat();
}
int main() {
	Pet p;
	Dog g;
	Cat c;
	func(p);
	func(g);
	func(c);
	return 0;
}

 上述是宠物的一个多态实现。

这里我们测试一下,基类函数不加virtual会怎样,

class Pet {
public:
     void eat() const{
        cout << "Eat food" << endl;
    }
};

da905b0d2aef46c3b394655b042a457f.png

我们会发现多态效果没有实现,所以一定要加上virtual. 

2.1.4 选择题

下面程序输出结果是什么?(B)

A: A->0 B: B->1 C: A->1 D: B->0 E: 编译出错 F: 以上都不正确

class A {
public:
virtual void func(int val = 1){ std::cout<<"A->"<< val <<std::endl;}
virtual void test(){ func();}
};
class B : public A {
public:
void func(int val = 0){ std::cout<<"B->"<< val <<std::endl; }
};
int main(int argc ,char* argv[]) {
 B*p = new B;
 p->test();
return 0; }

  • B* p = new B; 创建了一个 B 类型的对象,并通过基类指针 p 指向它。
  • p->test(); 调用了 A 类的 test 方法(因为 B 类没有重写 test 方法)。
  • 在 A 类的 test 方法中,func(val) 被调用,没有指定 val 的值,因此它使用 A 类 func 方法的默认参数 1
  • 由于 func 是虚函数,并且 p 指向一个 B 类型的对象,所以 B 类的 func 方法被调用,接收到的参数是 1

2.1.5 虚函数其他知识

协变(了解)
派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。

#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;

class A {};
class B : public A {};
class Person {
public:
	virtual A* BuyTicket()
	{
		cout << "买票-全价" << endl;
		return nullptr;
	}
};
class Student : public Person {
public:
	virtual B* BuyTicket()
	{
		cout << "买票-打折" << endl;
		return nullptr;
	}
};
void Func(Person* ptr)
{
	ptr->BuyTicket();
}
int main()
{
	Person ps;
	Student st;
	Func(&ps);
	Func(&st);
	return 0;
}
 析构函数的重写
基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同看起来不符合重写的规则,实际上编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor, 所以基类的析构函数加了vialtual修饰,派生类的析构函数就构成重写。

故在C++中,当一个基类的析构函数被声明为虚函数时,它确保了当通过基类指针或引用删除派生类对象时,会调用正确的析构函数,即派生类的析构函数,然后再调用基类的析构函数。这是因为虚析构函数允许动态绑定,确保了派生类对象被正确地销毁。

#include <iostream>
using namespace std;
class A {
public:
	virtual ~A() {
		cout << "delete A" << endl;
	}
}; 
class B :public A {
	public:
		~B() {
			cout << "~B()->delete:" << _p << endl;
			delete _p;
		}
protected:
	int* _p = new int[10];
};
int main() {
	A* a = new A;
	A* b = new B;
	delete a;
	delete b;
	return 0;
}

2c9ec88991b54a408d740498d87f66fc.png

当我们不把基类析构函数设置成virtual时, 会发现没有调用B的析构,该释放的资源没有释放掉。

public:
	~A() {
		cout << "delete A" << endl;
	}
}; 

e451daade64e44c1a890b5c6dbc382e7.png

故基类的析构函数我们要设置成虚函数。

override 和 final关键字
从上面可以看出,C++对函数重写的要求比较严格,但是有些情况下由于疏忽,比如函数名写错参数写错等导致无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失。
如果不想让派生类重写这个虚函数,那么可以用final去修饰。
在C++中,override 和 final 关键字是C++11标准引入的,用于增强类继承和虚函数的声明。

override 关键字用于明确指出一个成员函数旨在重写(覆盖)其基类中的一个虚函数。如果该函数没有正确地重写基类中的任何虚函数,编译器将报错。这有助于避免因拼写错误或参数列表不匹配而意外地没有重写虚函数的情况。
 

	class Car {
	public:
		virtual void Dirve()
		{}
	};
	class Benz :public Car {
	public:
		virtual void Drive() override { cout << "Benz-舒适" << endl; }
	};

比如上面这个例子,函数名写错了,重写失败,编译报错。

3c24128dc270456985512ecfe80ec180.png


final 关键字用于防止类被进一步派生,或者防止虚函数被重写。当应用于类时,它表示这个类不能被继承。当应用于虚函数时,它表示这个虚函数不能在派生类中被重写。

class Car {
public:
	virtual void Dirve() final
	{}
};
class Benz :public Car {
public:
	virtual void Dirve(){ cout << "Benz-舒适" << endl; }
};

 992bd20cde2c433b998df1a03c050a49.png



class Base final { // 不能从这个类派生其他类
public:
    virtual void doSomething() const final {} // 这个虚函数不能被重写
};
// 下面的类声明会导致编译错误,因为 Base 是 final 的
// class Derived : public Base {};
// 下面的函数声明也会导致编译错误,因为 doSomething 是 final 的
// class Derived : public Base {
// public:
//     void doSomething() const override {} // 错误:不能重写 final 函数
// };

使用 final 关键字可以确保类或虚函数的行为不会被意外的继承或重写改变,这对于设计那些不打算被扩展的类或函数非常有用。 

3. 重载,重写,隐藏的对比

重载(Overloading)

  • 定义:在同一作用域内,可以定义多个同名函数,只要它们的参数列表(参数的数量、类型或顺序)不同。
  • 特点
    • 发生在同一类中。
    • 参数列表必须不同。
    • 返回类型可以不同,但不是区分重载的主要因素。

重写(Overriding)

  • 定义:在派生类中提供一个与基类中虚函数同名、参数列表和返回类型相同的函数,以实现多态。
  • 特点
    • 发生在基类和派生类之间。
    • 参数列表和返回类型必须相同。
    • 基类函数必须是虚函数。
    • 使用 override 关键字可以明确指出重写意图。

 隐藏(Hiding)

  • 定义:在派生类中定义一个与基类中成员(非虚函数或非静态成员变量)同名的成员,导致基类中的同名成员在派生类中不可见。
  • 特点
    • 发生在基类和派生类之间。
    • 可以是函数或变量。
    • 如果是函数,参数列表不必相同。
    • 如果派生类中的成员与基类中的成员具有相同的名称,但不同的参数列表,则基类成员被隐藏,而不是重载或重写。

 4.纯虚函数和抽象类

在虚函数的后面写上 =0 ,则这个函数为纯虚函数,纯虚函数不需要定义实现(实现没啥意义因为要被派生类重写,但是语法上可以实现),只要声明即可。
包含纯虚函数的类叫做抽象类,抽象类不能实例化出对象,如果派生类继承后不重写纯虚函数,那么派生类也是抽象类。纯虚函数某种程度上强制了派生类重写虚函数,因为不重写实例化不出对象。
#include <iostream>
using namespace std;
class Car {
public:
	virtual void Drive() = 0;

};
class Benchi :public Car {
public:
	virtual void Drive() {
		cout << "Benchi-舒适" << endl;
	}
};

class Baoma :public Car {
public:
	virtual void Drive() {
		cout << "Baoma-上手" << endl;
	}
};
int main() {
	Car car;
	Car* b = new Benchi();
	b->Drive();
	Car* m = new Baoma();
	m->Drive();
	return 0;
}

337ab3ba1fc946b4ba2ffe85b6eb524f.png

 这里Car是抽象类,所以无法实例化对象。

结束语

本期内容就到此结束了,内容有点多,下节我们将对多态的原理进行补充讲解。

最后感谢各位友友的支持!!!

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

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

相关文章

信息安全数学基础(34)正规子群和商群

正规子群详述 定义&#xff1a; 设G是一个群&#xff0c;H是G的子群。若H的左陪集与右陪集总是相等&#xff08;即对任何的a∈G&#xff0c;都有aHHa&#xff09;&#xff0c;则称H是G的正规子群或不变子群&#xff0c;记为H⊴G。 性质&#xff1a; 平凡性&#xff1a;任何群G…

日期选择简化版今日、本周、本月、本季度、本年

function 未来之窗_时间_现在() {let date new Date(),year date.getFullYear(), //获取完整的年份(4位)month date.getMonth() 1, //获取当前月份(0-11,0代表1月)strDate date.getDate() // 获取当前日(1-31),小时 date.getHours(),分钟 date.getMinutes();if (month &…

C语言笔记(指针题目)例题+图解

本文分为两部分 &#xff0c;第一部分为数组、字符串、字符指针在sizeof和strlen中的辨析&#xff0c;第二部分是一些笔试题目。若有错误&#xff0c;请批评指正。 目录 1.第一部分 1.1.数组名的使用 1.1.1一维整型数组在sizeof中的使用 1.1.2一维字符数组在sizeof中的使用…

Android Input的流程和原理

Android Input事件机制 Android系统是由事件驱动的&#xff0c;而Input是最常见的事件之一&#xff0c;用户的点击、滑动、长按等操作&#xff0c;都属于Input事件驱动&#xff0c;其中的核心就是InputReader和InputDispatcher。InputReader和InputDispatcher是跑在system_serv…

Manus在虚拟现实仿真模拟中的应用案例分享

Manus虚拟现实手套作为一种高精度的人机交互设备&#xff0c;在仿真模拟领域展现出了巨大的应用潜力。通过提供实时、准确的手指动作捕捉数据&#xff0c;Manus手套为多个行业带来了前所未有的仿真体验&#xff0c;推动了技术发展和应用创新。 技术特点 1. 高精度手指跟踪 Manu…

windows 驱动实例分析系列: NDIS 6.0的Filter 驱动改造(四)

驱动的测试代码解读 1. 打开设备对象: // 1. 打开底层设备对象HANDLE hDevice CreateFile(L"\\\\.\\NDISFilter", // 设备名称GENERIC_READ | GENERIC_WRITE, // 访问权限0, // 不共享NULL, // 默认安全属性OPEN_EXISTING, // 打开现有设备0, // 无特殊标志NULL /…

加强版 第四节联通组件分析与演示

得到二值图像的目的是为了后面的分析 基本概念解释 -图像联通组件 -四邻域与八邻域联通 ccl联通组件标记 通过四邻域和八邻域来对图像进行边缘寻找与噪声处理的功能 常见算法来寻找联通组件 -基于像素扫描方法 -基于块扫描的方法 -两步法扫描 API: OpenCV中支持连通组件…

闯关leetcode——232. Implement Queue using Stacks

大纲 题目地址内容 解题代码地址 题目 地址 https://leetcode.com/problems/implement-queue-using-stacks/description/ 内容 Implement a first in first out (FIFO) queue using only two stacks. The implemented queue should support all the functions of a normal …

【Unity基础】初识UI Toolkit - 编辑器UI

&#xff08;本文所需图片在文章上面的资源中&#xff0c;点击“立即下载”。&#xff09; 本文介绍了如何通过UI工具包&#xff08;UI Toolkit&#xff09;来创建一个编辑器UI。 一、创建项目 1. 打开Unity创建一个空项目&#xff08;任意模板&#xff09;&#xff0c;这里我…

什么?Flutter 又要凉了? Flock 是什么东西?

今天突然看到这个消息&#xff0c;突然又有一种熟悉的味道&#xff0c;看来这个月 Flutter “又要凉一次了”&#xff1a; 起因 flutter foundation 决定 fork Flutter 并推出 Flock 分支用于自建维护&#xff0c;理由是&#xff1a; foundation 推测 Flutter 团队的劳动力短缺…

windows文件实时同步

在现今这个高效运转的工作时代&#xff0c;数据同步与备份的重要性愈发凸显。特别是对于Windows用户&#xff0c;实现文件在不同设备间的实时流转&#xff0c;不仅能够显著提升工作效率&#xff0c;更是数据安全的一道重要防线。接下来&#xff0c;我们将深入剖析Windows文件实…

全桥PFC电路及MATLAB仿真

一、PFC电路原理概述 PFC全称“Power Factor Correction”&#xff08;功率因数校正&#xff09;&#xff0c;PFC电路即能对功率因数进行校正&#xff0c;或者说是能提高功率因数的电路。是开关电源中很常见的电路。功率因数是用来描述电力系统中有功功率&#xff08;实际使用…

rom定制系列------红米note8_miui14安卓13定制修改固件 带面具root权限 刷写以及界面预览

&#x1f49d;&#x1f49d;&#x1f49d;红米note8机型代码&#xff1a;ginkgo。高通芯片。此固件官方最终版为稳定版12.5.5安卓11的版本。目前很多工作室需要高安卓版本的固件来适应他们的软件。并且需要root权限。根据客户要求。修改固件为完全root。并且修改为可批量刷写的…

【传知代码】图像处理解决种子计数方法

文章目录 一、背景及意义介绍研究背景农业考种需求传统计数方法的局限性人工计数仪器设备计数 研究意义提高育种效率提高计数准确性广泛的适用性数据存档与分析便利 二、概述三、材料与数据准备以及方法介绍整体流程图像采集图像预处理形态学操作腐蚀运算开运算 图像二值化种子…

【鸿蒙HarmonyOS实战:通过华为应用市场上架测试版App实现HBuilder X打包的UniApp项目的app转hap教程(邀请码)方式教程详解】

鸿蒙HarmonyOS实战&#xff1a;通过华为应用市场上架测试版App实现HBuilder X打包的UniApp项目的app转hap教程&#xff08;邀请码&#xff09;方式详解 在使用uniapp打包的鸿蒙项目的过程中&#xff0c;由于生成的是app文件&#xff0c;而hdc传给鸿蒙HarmonyOS系统需要的是hap文…

2025选题|基于协同推荐的黔醉酒业白酒销售系统

作者主页&#xff1a;编程指南针 作者简介&#xff1a;Java领域优质创作者、CSDN博客专家 、CSDN内容合伙人、掘金特邀作者、阿里云博客专家、51CTO特邀作者、多年架构师设计经验、多年校企合作经验&#xff0c;被多个学校常年聘为校外企业导师&#xff0c;指导学生毕业设计并参…

从0开始学python-day17-数据结构2

2.3 队列 队列(Queue)&#xff0c;它是一种运算受限的线性表,先进先出(FIFO First In First Out) 队列是一种受限的线性结构 受限之处在于它只允许在表的前端&#xff08;front&#xff09;进行删除操作&#xff0c;而在表的后端&#xff08;rear&#xff09;进行插入操作 P…

Element Plus在Vue3的安装

本来想下载element ui 的&#xff0c;但是不支持报错了 所以我们使用element plus 在终端使用下命令下载 npm install element-plus --save 然后就可以在package.json里面看到所下载的版本号了 之后在main.js中根据自己的的情况配置Element-Plus // main.ts import { create…

App Inventor 2 列表显示框能否实现多选?

Q&#xff1a;列表显示框有没有办法做到多选的功能&#xff1f; // 问题分析 // AppInventor2列表显示框原生并没有多选功能&#xff0c;只能点击其中一项&#xff0c;然后触发“选择完成时”这个事件&#xff0c;那么有没有办法做到多选呢&#xff1f; // 问题思路 // 经过…

Topaz Video AI for Mac 视频无损放大软件安装教程【保姆级,操作简单轻松上手】

Mac分享吧 文章目录 Topaz Video AI for Mac 视频无损放大软件 安装完成&#xff0c;软件打开效果一、Topaz Video AI 视频无损放大软件 Mac电脑版——v5.3.5⚠️注意事项&#xff1a;1️⃣&#xff1a;下载软件2️⃣&#xff1a;安装软件&#xff0c;将安装包从左侧拖入右侧文…