IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> 【C++】多态 — 认识多态 + 多态的条件及其性质(上篇) -> 正文阅读

[C++知识库]【C++】多态 — 认识多态 + 多态的条件及其性质(上篇)

📖 前言

C++是一门面向对象的语言,其三大特性,封装、继承、多态。
本文将介绍来好好讲讲多态…


1. 多态的概念

多态的概念:

通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。


2. 多态的定义及实现

2.1 虚函数:

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

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

2.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;
}

上述代码总结:

  • 多态是同样是买票,不同的人买票,结果不一样
  • 父类的指针,父类的引用,可以指向子类的对象,也可以指向父类的对象

在这里插入图片描述

2.2 - 1 多态的两个条件(重点)

  • 必须是父类的指针或者引用去调用虚函数
  • 子类虚函数重写父类的虚函数(重写:三同 【函数名/参数/返回值】 + 虚函数)

1.为什么一定是父类的指针和引用去调用虚函数,父类的对象可以吗?

在这里插入图片描述

先看运行结果:

在这里插入图片描述

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

2.如果有一个条件不满足多态的话(当参数不满足相同时):

在这里插入图片描述
我们来看运行结果:

在这里插入图片描述

  • 我们看到结果是有问题的,并没有达到我们想要的情况,军人的虚函数不符合重写的规则
  • 到讲解多态的原理时,我们再来深入学习
  • 现在只需要记住多态的两个条件(重点)

3. 虚函数重写的两个例外

3.1 协变 - 基类与派生类虚函数返回值类型不同:

1.协变的概念:

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

  • 虚函数重写对返回值的要求有个例外,叫作:协变
  • 协变的返回值类型也不是随便的,必须是(父子关系)的指针和引用

2.父子关系的指针:

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;
}

在这里插入图片描述

3.父子关系的引用:

在这里插入图片描述

在这里插入图片描述
4.父子关系的对象:

在这里插入图片描述
在这里插入图片描述
可见父子关系的对象是不行的。

C++在这里搞麻烦了,意义不是很大。

5.如果同为父类:

在这里插入图片描述
在这里插入图片描述

这里则是继承中的隐藏。


补充:

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

建议:

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

3.1 - 1 一道非常考验基础知识的笔试题

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.看下面代码的运行结果是什么?
在这里插入图片描述
运行结果:
在这里插入图片描述
这里就是类成员的普通调用,并不是多态的调用。

3.再来看一组:
在这里插入图片描述
这才是多态的调用,时刻记住多态的两个条件!

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


3.2 析构函数的重写 - 基类与派生类析构函数的名字不同:

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

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

在这里插入图片描述
在这里插入图片描述

普通调用看的指针类型,指针类型是Person * ,就去调用Person的析构函数去了

  • 如果不构成多态这是一个普通调用
  • 普通调用都是看调用变量或者指针的类型

只要完成重写(覆盖),指向子类调子类,指向父类调父类,构成了多态。

问题:

  • 这里调用就会出现调用不到子类的析构,会存在内存泄露的风险。

2.解决办法 - 多态:

class Person 
{
public:
	virtual ~Person() 
	{ 
		cout << "~Person()" << endl; 
	}
};

class Student : public Person 
{
public:
	virtual ~Student() 
	{ 
		cout << "~Student()" << endl; 
	}
};

int main()
{
	Person p;
	Student s;

	return 0;
}

(1) 对于普通对象没有影响:

在这里插入图片描述

  • 子类对象析构的时候是先子后父,初始化是先父后子
  • 调用子类的析构函数会自动调用父类的析构函数

析构函数默认是隐藏关系,如果要实现多态 – 析构函数的函数名都要被处理成destructor,才能满足多态的条件。

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

(2) 但是指针的切片调用就会有不同,触发了多态:

在这里插入图片描述
在这里插入图片描述
建议:

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

4. 新增的两个关键字

  • C++11提供了override和final两个关键字,可以帮助用户检测是否重写。

4.1 final的使用:

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

class Car
{
public:
	virtual void Drive() final {}
};

class Benz :public Car
{
public:
	//加上final这个关键字之后是无法被 重写/覆盖 的
	virtual void Drive() { cout << "Benz" << endl; }
};

int main()
{
	return 0;
}

在这里插入图片描述
2.在继承那一章节我们讲到,如何实现一个不能被继承的类:

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

(1) C++98 – 间接的方式(通过语法的规则走的间接方式)

在这里插入图片描述
在这里插入图片描述
正常继承并没有报错。

  • 这里不是说子类继承不了,而是直接继承不了,而是通过间接实现的

而是通过间接的方式:

在这里插入图片描述

  • 因为子类的构造函数必须要去调用父类的构造函数
  • 而父类的构造函数私有子类不可见
  • 是通过联合的方式来调用的
  • 通过语法规则走了一层间接

(2) C++11 – 直接的方式

在这里插入图片描述
final的类不能被继承 – 最终类,不能继承,更直观一些。

3.final的两个作用:

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

4.2 override的使用:

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

class Car
{
public:
	virtual void Drive() {}
};

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

int main()
{

	return 0;
}

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

这里并没有完成重写,所以报错了:
在这里插入图片描述


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

一张图讲清楚这三者各自的特点:

在这里插入图片描述
补充:

  • 同名的成员变量也是隐藏的关系

6. 抽象类概念 + 使用

概念:

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

代码演示:

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

int main()
{
	Car c;
	
	return 0;
}

在这里插入图片描述
包含纯虚函数的类都叫做抽象类。

抽象就是不具体(虚拟层面),不具象化。

在现实中一般没有具体对应的实体,不想让类实例化出对象

看下述代码:
在这里插入图片描述
为什么BMW不能实例化出对象?

  • 这是因为BMW也是一个抽象类
  • 因为BMW类从Car类中继承了纯虚函数
  • 根据定义,有纯虚函数的类叫做抽象类,抽象类是不能实例化对象的。

正确的写法:

在这里插入图片描述
这时候就可以实例化出对象了。

从另一个角度分析:

  • 纯虚函数除了让基类不能实例化出对象
  • 同时又做了另一件事情
  • 那就是间接的让子类强制重写
  • 而override是放在子类中查重写,没重写就检查报错

基类不能定义对象,但是基类可以定义指针:
在这里插入图片描述

  • 但是这里可以new出子类对象。
  • 这里指向谁调用谁,构成了多态。

同样的指针可以进行如上操作:

  • 引用当然也可以进行以上操作
  • 因为它们俩本质在底层是一样的
  • 如果不是纯虚函数:
  • 那么实现起来还是有意义的
  • 因为父类的指针指向父类对象(多态的场景或普通场景)或者是父类的指针指向子类对象(非多态场景)
  • 父类的对象可以调用的到
  • 如果是纯虚函数:
  • 那么就是没有人能调用
  • 因为父类没有对象,子类对象直接去调用的是自己的函数
  • 如果是多态的场景,调用的也是自己的
  • 因为父类的指针不会指向父类的对象,父类对象new不出来

综上所述:

  • 纯虚函数的函数体实现没有任何意义,因为没人能用到它,因为纯虚函数没人能调用得到。
  • 所以我们一般情况下,纯虚函数不回去实现,直接给一个声明就可以了
  • 纯虚函数本身也就是一个接口继承
  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2022-10-31 11:35:34  更:2022-10-31 11:39:06 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 5:58:16-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码