【C++】多态---上( 概念、条件及性质)

news2024/12/27 12:35:07

来前言:

       我们之前提到过,C++是一门面向对象的语言,它有三大特性——封装、继承、多态

封装和继承我们已经详细学习过了,本章将进入多态的学习。

   

目录

(一)多态的概念

(二)多态的定义和实现

(1)多态的构成条件

1、虚函数

2、虚函数的重写(覆盖)

3、多态的条件(重点)

(2)虚函数重写的两个例外

1、协变(基类与派生类虚函数返回值类型不同)

2、析构函数的重写(基类与派生类析构函数的名字不同)

(三)C++11---两个关键字之override和final

(1)final的用法

(2)override的用法

(四)重载、覆盖(重写)、隐藏(重定义)的对比

 (五)抽象类的概念+使用

(1)概念

(2)接口继承和实现继承


(一)多态的概念

通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会
产生出不同的状态
举个栗子:比如买票这个行为,当普通人买票时,是全价买票;学生买票时,是半价买票;军人
买票时是优先买票。各个群体买的票形态是不一样的。

(二)多态的定义和实现

(1)多态的构成条件

1、虚函数

在定义多态前,我们先要了解虚函数。

虚函数:即被virtual修饰的类成员函数称为虚函数

2、虚函数的重写(覆盖)

虚函数的重写(覆盖):

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

例如:

class Person
{
public:
	Person(const char* name)
		:_name(name)
	{}
	//虚函数
	virtual void BuyTicket() { cout << _name << " Person: 买票-全价 100¥" << endl; }

protected:
	string _name;
};

class Student : public Person
{
public:
	Student(const char* name)
		:Person(name)
	{}
	//虚函数 + 函数名/参数/返回值 -> 重写/覆盖
	virtual void BuyTicket() { cout << _name << " Student: 买票-半价 50¥" << endl; }
};
这里构成了虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),所以称子类的虚函数重写了基类的虚函数。

3、多态的条件(重点

有了上面虚函数和覆盖的基础,我们给出下面继承构成多态的条件:

那么在继承中要构成多态还有两个条件
  • 1. 必须通过基类的指针或者引用调用虚函数
  • 2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

 我们实现一个简易的购票系统为例:


//多态只用的样例:
class Person
{
public:
	Person(const char* name)
		:_name(name)
	{}
	//虚函数
	virtual void BuyTicket() { cout << _name << " Person: 买票-全价 100¥" << endl; }

protected:
	string _name;
	//int _id;
};

class Student : public Person
{
public:
	Student(const char* name)
		:Person(name)
	{}
	//虚函数 + 函数名/参数/返回值 -> 重写/覆盖
	virtual void BuyTicket() { cout << _name << " Student: 买票-半价 50¥" << endl; }
};

class Soldier : public Person
{
public:
	Soldier(const char* name)
		:Person(name)
	{}
	//虚函数 + 函数名/参数/返回值 -> 重写/覆盖
	virtual void BuyTicket() { cout << _name << " Soldier: 优先买预留票-88折 100¥" << endl; }
};

void Pay(Person* ptr)
{
	ptr->BuyTicket();
	delete ptr;
}

//赋值兼容的转换,父类指针可以指向父类对象,也可以指向子类对象
void Pay(Person& ptr)
{
	ptr.BuyTicket();
}

//全部都去调用父类去了 -- 不构成多态
//void Pay(Person ptr)
//{
//	ptr.BuyTicket();
//}

int main()
{
	int option = 0;
	cout << "=========================================" << endl;
	do
	{
		cout << "请选择身份:";
		cout << "1、普通人 2、学生 3、军人" << endl;
		cin >> option;

		cout << "请输入名字:";
		string name;
		cin >> name;

		//switch case语句里面,是不能支持定义对象的,要加一个域{}
		//加完域之后就是局部域了
		switch (option)
		{
		case 1:
		{
			Person p(name.c_str());
			Pay(p);
			break;
		}
		case 2:
		{
			Student s(name.c_str());
			Pay(s);
			break;
		}
		case 3:
		{

			Soldier s(name.c_str());
			Pay(s);
			break;
		}
		default:
			cout << "输入错误,请从新输入" << endl;
			break;
		}
		cout << "=========================================" << endl;
	} while (option != -1);

	return 0;
}

这里我们满足形成多态的两个条件:

1、必须通过基类的指针或者引用调用虚函数:

错误范例:

  • 因为都调用到父类对象去了 —— 不构成多态
  • 原理要到多态的原理中才能理清楚

 2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

 我们实现多态必须严掐多态的条件!!!!(下面给出例外的情况,例外情况的次数出现较少)

(2)虚函数重写的两个例外

1、协变(基类与派生类虚函数返回值类型不同)

协变的概念:

派生类重写基类虚函数时, 与基类虚函数返回值类型不同 。即基类虚函数返回基类对象的指
针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。(了解)
  • 虚函数重写对返回值的要求有个例外,叫作:协变
  •  协变的返回值类型也不是随便的,必须是(父子关系)的指针和引用

例:

A和B构成父子关系,而Person和Student返回的正是这对父子关系的指针,此处就构成协变。

class A
{};

class B : public A
{};

class Person
{
public:
	virtual A* f()
	{
		cout << "virtual A* Person::f()" << endl;
		return nullptr;
	}
};

class Student : public Person
{
public:
	virtual B* f()
	{
		cout << "virtual B* Student::f()" << endl;
		return nullptr;
	}
};

int main()
{
	Person p;
	Student s;

	Person* ptr = &p;
	ptr->f();

	ptr = &s;
	ptr->f();

	return 0;
}

补充:

  • 子类的虚函数没有写virtual,f依旧是虚函数
  • 因为先继承了父类函数接口的声明
  • 重写的是父类虚函数的实现
  • 所以父类有virtual的属性子类也就有了
  • 这样写不太规范

建议:

  • 最好不要协变或者子类不加virtual
  • 我们自己写的时候子类虚函数也写上virtual

浅刷几道笔试题~

1、下面程序的输出结果是什么?

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()
{
	B* p = new B;
	p->test();

	return 0;
}

运行结果:

 详解:


再看一组: 

 这就是明显的多态调用了。

多态:拿一个类的指针去调用另一个类的函数。


2、析构函数的重写(基类与派生类析构函数的名字不同)

  • 重写又叫做覆盖
  •  隐藏又叫做重定义

析构函数不构成多态的情况:

  这样子调用析构函数,p1、p2一看自己的类型是Person,就自动调用了Person的析构函数,这样会导致内存泄漏!!

 


解决办法:

多态的实现。

如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,
都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,
看起来违背了重写的规则,其实不然, 这里可以理解为编译器对析构函数的名称做了特殊处
理,编译后析构函数的名称统一处理成destructor。

class Person {
public:
	virtual ~Person() { cout << "~Person()" << endl; }
	//~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:
	virtual ~Student() { cout << "~Student()" << endl; }
	//~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,才能满足多态的条件。

  • 如果Person析构函数加了virtual,关系就变了
  • 加上virtual之后就从隐藏关系变成了:重写关系 – 隐藏/重定义->重写/覆盖

建议:

  • 如果设计的一个类,可能作为基类。
  • 其析构函数最好定义为虚函数。

(三)C++11---两个关键字之override和final

引入:

  • 从上面可以看出,C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数 名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有 得到预期结果才来debug会得不偿失,因此:C++11提供了override和fifinal两个关键字,可以帮助用户检测是否重写。

(1)final的用法

1、final:修饰虚函数,表示该虚函数不能再被重写

 2、final:修饰类,表示该类不能被继承。

在继承那一章节我们讲到,如何实现一个不能被继承的类:

  • 当时我们讲到了一种方法,那就是将父类构造函数私有化
  • 但是这不是一种很好的方式

 final的类不能被继承 – 最终类,不能继承,更直观一些。


总结final的两个作用:

  • 修饰类 - 不能被继承
  • 修饰虚函数 - 不能被重写

(2)override的用法

override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。

override是写在子类中的,要求严格检查是否完成重写,如果没有就报错。、

这里完成了虚函数的重写,所以不报错:

这里把基类的virtual去掉,Drive就不是虚函数了,更别说完成了重写,所以报错: 

 

 

(四)重载、覆盖(重写)、隐藏(重定义)的对比

我们在刚步入C++时学过函数重载,继承时候学过隐藏(重定义),刚刚学虚函数时讲解到了覆盖(重写),那么他们三到底有什么区别呢?

一张图了解:

 (五)抽象类的概念+使用

(1)概念

概念:

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

代码演示:

//抽象类
class Car
{
public:
	//纯虚函数
	virtual void Drive() = 0;
};

int main()
{
	Car c;
	
	return 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;
	}
};
int main()
{
	Car* pBenz = new Benz;
	pBenz->Drive();
	Car* pBMW = new BMW;
	pBMW->Drive();
}

重写后,派生类才可以实例化出对象。 

(2)接口继承和实现继承

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

 我们通过上面的抽象类也可以看出,虚函数继承是一种接口继承。

综上所述:

  • 纯虚函数的函数体实现没有任何意义,因为没人能用到它,因为纯虚函数没人能调用得到。
  • 所以我们一般情况下,纯虚函数不会去实现,直接给一个声明就可以了
  • 纯虚函数本身也就是一个接口继承

本章基本讲解了多态的 概念、条件及其性质,下面一章我们会着重分析其底层实现和原理!

感谢您的阅读,祝您学业有成!!

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

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

相关文章

Vector - CAPL - Panel面板_02

Button 功能&#xff1a;触发指定的操作 说明&#xff1a;Button 是一个控件&#xff0c;使用它可以触发指定的操作。 适用场景&#xff1a; 1、按下启动按钮会使电机启动。 2、启动锁定开关时&#xff0c;所有车门都会自动锁定。 3、启动TestModule测试模块、回放模块等 设…

单机部署MongoDB

文章目录 一、Windows 环境1.1 安装1.2 启动和连接1.3 Compass 图形化客户端 二、Linux 环境2.1 安装2.2 启动和连接 提示&#xff1a;以下是本篇文章正文内容&#xff0c;MongoDB 系列学习将会持续更新 一、Windows 环境 1.1 安装 ①下载安装包&#xff0c;官方下载地址&am…

OldWang带你了解MySQL(七)

文章目录&#x1f525;多表查询&#x1f525;SQL92标准中的查询&#x1f525;非等值连接&#x1f525;自连接&#x1f525;SQL99标准中的查询&#x1f525;SQL99中的自然连接(NATURAL JOIN)&#x1f525;SQL99中的内连接(INNER JOIN)&#x1f525;外连接查询(OUTER JOIN)&#…

行业那么多,为什么计算机领域这么火?

行业那么多&#xff0c;为什么计算机领域这么火&#xff1f;计算机领域火已经不是一天两天了&#xff0c;从开始的进入互联网时代、到“互联网”、再到大数据、人工智能时代、数字化经济……计算机技术从行业内部的自我发展逐渐渗透到各行各业&#xff0c;甚至成为社会整体经济…

NLP深度网络中self.embedding(x)词嵌入后降维方法

在自然语言处理中的循环神经网络中&#xff0c;经常使用torch定义类&#xff0c;self.embedding(x&#xff09;中&#xff0c;x是输入&#xff0c;介绍self.embedding(x&#xff09;返回结果&#xff0c;以及结果的形状&#xff0c;并解释这个形状 在自然语言处理中的循环神经网…

P4158 [SCOI2009]粉刷匠(分组背包问题+前缀和优化)

[TOC](P4158 [SCOI2009]粉刷匠(分组背包问题)) 一、问题 [SCOI2009]粉刷匠 题目描述 windy有 N 条木板需要被粉刷。 每条木板被分为 M 个格子。 每个格子要被刷成红色或蓝色。 windy每次粉刷&#xff0c;只能选择一条木板上一段连续的格子&#xff0c;然后涂上一种颜色。 …

Spring Cloud Gateway: 网关

文章目录 网关Hello world路由: Route谓词: Predicate过滤器: FilterGateway实现限流: RequestRateLimiter过滤器使用Gateway实现服务降级 自定义全局过滤器GateWay中执行流程 网关 API网关就是实现了前端项目和服务端项目之间的统一入口 Nginx实现的是用户和前端项目之间调用…

【Linux】环境变量相关笔记

文章目录 echo $PATHexport和环境变量相关的命令main(int argc,char* argv[],char *env[])三个参数介绍getenv()通过系统调用获取su与su - 的区别&#xff1a;exportsource 和 .优先级 echo $PATH 是用来查环境变量的 export 从下面的图片当中可以看到&#xff0c;的确是将文…

ubuntu 20.04设置开机自启动脚本

1 建立开机启动服务 在 路径下 /lib/systemd/system/rc-local.service 的 rc-local.service 的脚本&#xff0c;内容规定了 rc.local 的启动顺序和行为 这行代码规定了这个service在开机启动时所执行的命令是&#xff1a;/etc/rc.local start。即运行 /etc/rc.local 脚本。不过…

《面试1v1》HashMap

没有人比中国人更懂 HashMap 我是 javapub&#xff0c;一名 Markdown 程序员从&#x1f468;‍&#x1f4bb;&#xff0c;八股文种子选手。 面试官&#xff1a;HashMap 是Java程序员用得最频繁的集合之一,可以给我简单介绍一下它的内部实现机制吗? 候选人&#xff1a; Hash…

C++ -3- 类和对象 (中) | 拷贝构造函数 赋值运算符重载

文章目录 4.拷贝构造函数什么是拷贝构造函数&#xff1f;应用——示例&#xff1a;日期计算器什么情况下需要自己实现拷贝构造函数&#xff1f; 5.赋值运算符重载运算符重载&#xff08;重要&#xff09;赋值运算符重载 拷贝构造函数和赋值重载函数 4.拷贝构造函数 什么是拷贝…

Baumer工业相机堡盟工业相机如何联合BGAPI SDK和OpenCV实现Mono12和Mono16格式位深度的图像保存(C++)

Baumer工业相机堡盟工业相机如何联合BGAPI SDK和OpenCV实现Mono12和Mono16位深度的图像保存&#xff08;C&#xff09; Baumer工业相机Baumer工业相机保存位深度12/16位图像的技术背景代码案例分享1&#xff1a;引用合适的类文件2&#xff1a;BGAPI SDK在图像回调中联合OpenCV保…

Effective C++条款条款42:了解typename的双重意义(Understand the two meanings of typename)

Effective C条款条款42&#xff1a;了解typename的双重意义&#xff08;Understand the two meanings of typename&#xff09; 条款42&#xff1a;了解typename的双重意义1、从属名称和非从属名称2、typename在traits机制中的运用3、牢记 总结 《Effective C》是一本轻薄短小的…

1.17 从0开始学习Unity游戏开发--场景切换

前面的所有文章我们都在一个固定的游戏场景内进行开发&#xff0c;在最开始介绍场景这个概念的时候就已经提及&#xff0c;这个场景可以是一张地图&#xff0c;或者是一个对战房间等等&#xff0c;所以显然这个场景可以有多个&#xff0c;并且可以从一个场景切换到另外一个场景…

Collection接口

文章目录 1. Java集合框架概述2. Collection接口中15个方法的使用3. Iterator(迭代器)接口4. Connection子接口一&#xff1a;List4.1 List的实现类4.2 源码分析4.2.1 ArrayList源码分析4.2.2 LinkedList源码分析4.2.3 Vector源码分析 4.3 List接口中的常用方法 5. Collection子…

死锁---银行家算法例题

1、知识点 1.银行家算法使用的四个必要的数据结构是: 可用资源向量Available&#xff0c;最大需求矩阵Max&#xff0c;分配矩阵Allocation&#xff0c;需求矩阵Need。 2.银行家算法是不是破坏了产生死锁的必要条件来达到避免死锁的目的&#xff1f;若是&#xff0c;请简述破…

【数字 IC / FPGA】 有关建立/保持时间计算的思考

引言 最近准备一些数字IC的机试&#xff0c;刷到了一些有关静态时序分析的题目。有一些比较经典的题目&#xff0c;在这里整理分享一下。 有什么疑问可以在评论区交流~互相进步 双D触发器典型电路 假设时钟周期为Tcycle,Tsetup,Thold分别为触发器建立保持时间&#xff0c;为…

Mac OS挂载ext4硬盘

一、安装macFUSE Home - macFUSE 如下载macfuse-4.4.3dmg安装 安装过程可能会遇到“若要要启用系统扩展,您需要在恢复环境中修改安全性设置”的提示&#xff0c;如下图&#xff1a; 解决&#xff1a; 关机&#xff0c;直到键盘灯全灭了&#xff01; 再按住开机键&#xff0c…

机器视觉技术分享-彩色图像处理 含c++ ,python代码说明

彩色图像处理是指对彩色图像进行数字处理和分析的过程&#xff0c;其目的是提取图像的有用信息&#xff0c;改善图像质量&#xff0c;实现图像的增强、复原、分割、匹配、识别等功能。 针对彩色图像处理&#xff0c;可以采用以下一些常见的方法&#xff1a; 1. 颜色空间转换&…

简简单单认识一下Inscode

CSDN最新推出的Inscode服务是一个在线编程工具&#xff0c;旨在为开发者提供一个便捷的编写、运行和分享代码的环境&#xff0c;让开发者无需在本地搭建编程环境&#xff0c;即可快速编写和运行代码。 Inscode支持多种编程语言&#xff0c;包括Java、Python、C等&#xff0c;同…