首页 > 编程语言 >【C++】14.多态

【C++】14.多态

时间:2024-07-10 18:27:04浏览次数:11  
标签:虚表 函数 派生类 多态 C++ 基类 重写 14

一、多态的概念

在编程与现实的映射中就是,不同的对象完成相同的行为而产生的不同状态。

举个栗子:比如买票这个行为,当普通人买票时,是全价买票;学生买票时,是半价买票;军人买票时是优先买票。
再举个栗子:动物的叫声,猫的叫声是“喵喵”;狗的叫声是“汪汪”;老虎的叫声是“劳资蜀道山”。

多态又分为 静态多态(编译时多态)动态多态(运行时多态)
静态多态如:函数重载,函数模板,类模板……(在前面的博客中)
本博客主要内容就是 动态多态

二、多态的定义及实现

2.1 多态的定义

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

在继承中要构成多态的两个条件

  1. 必须通过基类的指针或者引用调用虚函数
  2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
    在这里插入图片描述

2.2 虚函数与纯虚函数

虚函数:被virtual修饰的类成员函数称为虚函数。
纯虚函数:在虚函数的后面写上 = 0 ,则这个函数为纯虚函数。

2.3 虚函数的重写

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

注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用)

虚函数重写的两个例外

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

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

只有派生类的析构函数重写了基类的析构函数,才能在消除由基类指针指向派生类对象(基类引用派生类对象)delete时的清理顺序是先清理派生类对象,然后再清理基类对象。

2.4 C++11 override和final

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

  • final:修饰虚函数,表示该虚函数不能再被重写
    class Animal
    {
    public:
    	 virtual void Scream() final {}//final表示不能再被重写
    };
    
    class Cat :public Animal
    {
    public:
    	 virtual void Scream() 
    	 {
    	 	cout << "喵喵" << endl;
    	 }
    };
    
  • override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
    class Animal
    {
    public:
    	 virtual void Scream() {}//final表示不能再被重写
    };
    
    class Cat :public Animal
    {
    public:
    	 virtual void Scream() override //检查是否重写
    	 {
    	 	cout << "喵喵" << endl;
    	 }
    };
    

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

在这里插入图片描述

三、抽象类

3.1 抽象类的概念

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

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

class Cat :public Animal
{
public:
	virtual void Scream() 
	{
		cout << "喵喵" << endl;
	}
};

3.2 接口继承和实现继承

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

3.3 一道变态的题

在这里插入图片描述

答案:B
p是一个B的指针类型,指向一块新开的B的空间。通过p来调用test()函数。
B类是A类的派生类,可见p调用的是基类A类中的test(),test()函数中的func()即*this->func(),这里的*this是A的this指针,满足多态的条件,对func()完成重写,但是重写的是func()的实现部分,因此输出的结果为B->1,答案为B。

四、多态的原理

在了解原理之前我们看这样一段代码:

#include<iostream>
using namespace std;

class Base
{
public:
	 virtual void Func1()
	 {
		 cout << "Func1()" << endl;
	 }
private:
	 int _b = 1;
};

int main()
{
    Base b;
	cout<<sizeof(b)<<endl;
	return 0;
}

程序的运行结果是8(x86环境)。
那么为什么运行的结果是8呢?我们的私有成员不是只有一个int类型吗?
这就牵扯到了多态的原理,也就是我们接下来的内容。

4.1 虚函数表

对上面的代码调试我们可以观察到如下信息:除了_b成员,还多一个__vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。
在这里插入图片描述
一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。那么派生类中这个表放了些什么呢?我们接着往下分析……

// 针对上面的代码我们做出以下改造
// 1.我们增加一个派生类Derive去继承Base
// 2.Derive中重写Func1
// 3.Base再增加一个虚函数Func2和一个普通函数Func3
#include<iostream>
using namespace std;

class Base
{
public:
	 virtual void Func1()
	 {
		 cout << "Base::Func1()" << endl;
	 }
	 virtual void Func2()
	 {
		 cout << "Base::Func2()" << endl;
	 }
	 void Func3()
	 {
		 cout << "Base::Func3()" << endl;
	 }
private:
	 int _b = 1;
};

class Derive : public Base
{
public:
	 virtual void Func1()
	 {
		cout << "Derive::Func1()" << endl;
	 }
private:
	 int _d = 2;
};

int main()
{
	 Base b;
	 Derive d;
 return 0;
}

在这里插入图片描述
通过观察和测试,我们发现了以下几点问题:

  1. 派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就是存在部分的另一部分是自己的成员。
  2. 基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
  3. 另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函数,所以不会放进虚表。
  4. 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。
  5. 总结一下派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
  6. 这里还有一个很容易混淆的问题:虚函数存在哪的?虚表存在哪的?
    答:**虚函数和普通函数一样都是存在代码段(常量区)的。虚表中存的是虚函数指针,虚表也是存放在代码段(常量区)的。**至于对象中存的不过是虚表指针罢了。下面我们来简单的证明一下:
    在这里插入图片描述

4.2 多态的原理

满足多态以后的函数调用,不是在编译时确定的,是运行起来以后到对象的中取找的。不满足多态的函数调用时编译时确认好的。

4.3 静态绑定与动态绑定

  1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载
  2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态

标签:虚表,函数,派生类,多态,C++,基类,重写,14
From: https://blog.csdn.net/2301_80258336/article/details/140317986

相关文章

  • 研0 冲刺算法竞赛 day14 P1957 口算练习题
    思路:分别考虑以运算符或数字开头,为运算符,直接读入后面两个数字;为数字,在读入一个数字即可代码:#include<iostream>#include<cstring>#include<cstdio>usingnamespacestd;intmain(){ intN; cin>>N; charc[10],str[55],f; while(N--) { cin>>c; int......
  • 【NOI】C++算法设计入门之贪心
    文章目录前言一、概念1.导入2.概念2.1贪心算法的核心思想2.2贪心算法的步骤2.3贪心算法的应用场景二、例题讲解问题:1372.活动选择问题:1456.淘淘捡西瓜问题:1551-任务调度问题:1561.买木头三、总结五、感谢前言贪心算法,如同成语"得陇望蜀"所描述的那样,总是......
  • 题解:P8144 [JRKSJ R4] BBWWBB
    思路分析题意可得,白方必定不会胜利,只能尽量让游戏无限进行下去。那么我们只考虑黑方能否胜利。若想让戏能无限进行下去,必须满足以下条件。白方先手。若黑方先手必然可以吃掉一个白方,白方仅有一个棋子,必输。白方第一轮可以吃掉一颗黑方。因为只有\(3,4\)是白方,所以......
  • 深入剖析C++的 “属性“(Attribute specifier sequence)
    引言在阅读开源项目源代码是,发现了一个有趣且特殊的C++特性:属性。属性(attributespecifiersequences)是在C++11标准引入的。在C++11之前,编译器特有的扩展被广泛用来提供额外的代码信息。例如,GNU编译器(GCC)使用__attribute__来控制函数的行为。但是缺点也很明显,那就是这种方......
  • c++新特性
    1.c++17新特性[[fallthrough]] 属性:这个属性用于在switch语句中。通常,当switch语句的一个case执行完毕后,会自动跳转到switch语句的末尾,除非存在break语句。如果你想要故意从一个case“落入”(fallthrough)到下一个case,可以使用[[fallthrough]]属性来告诉编译器这是故意的行为,以......
  • Matebook14 2020款 更换固态(全流程)
    Matebook142020款更换固态全流程因为工作的原因需要升级存储,我的老款的Matebook14只有512G。网络上的中文教程普遍比较古老。特此写下这篇笔记希望能帮助到有需要的朋友。工具螺丝刀(四花00和六花T4)新的固态硬盘U盘(容量不小于1G)移动硬盘(容量不能小于你的系统镜像)操作流......
  • JavaSE学习笔记第二弹——对象和多态(上)
    目录面向对象基础面向对象程序设计的定义类的基本结构成员变量成员方法方法定义与使用设计练习方法重载构造方法静态变量和静态方法String和StringBuilder基本含义区别总结今天我们继续来学习JavaSE,扩展和Java相关的知识,与大家共勉。今天的主题是对象和多态,希......
  • 141. 环形链表
    给你一个链表的头节点 head ,判断链表中是否有环。如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从0开始)。注意:pos 不作为参数进行传递 。仅仅是为......
  • POJ 3414 Pots
    题目链接:POJ3414【Pots】思路    对于每个A、B瓶的每个状态,使用结构体存储,同时pre存储操作前的状态的下标,方便回溯查询正确路径的操作,oper存储使用什么操作得到当前状态,operNumber存储到达当前状态需要几步。由于需要求的是最少的操作次数,所以使用BFS,依次增加操作次......
  • UE C++获取本机IP地址
    UEC++获取本机IP地址开发环境UE5.2.1VisualStudio201916.11.19代码部分XXX.Build.cs文件添加以下依赖:PrivateDependencyModuleNames.AddRange(newString[]{"Sockets"}).cpp文件:#include"SockerSubsystem.h"voidGetIp(){ boolbBind=false; TSharedRef......