C++:多态讲解

news2024/11/19 0:37:08

多态

    • 1.多态的概念
    • 2.多态的定义和实现
      • 2.1多态构成条件
      • 2.2虚函数
      • 2.3虚函数的重写(覆盖)
      • 2.4 C++11 override 和 final
      • 2.5重载、重写(覆盖)、隐藏(重定义)的对比
    • 3.抽象类
    • 4.多态的原理
    • 5.单继承和多继承关系的虚函数表
      • 5.1单继承
      • 5.2多继承
      • 5.3菱形继承和多态

1.多态的概念

多态的概念:同样的一个行为不同的对象去完成时会产生不同的状态

例子:拿买票举例,军人、学生、普通人(子类)都是(父类),但军人买票可以优选选票,学生买票可以半价,普通人买票就要全价了。因此要实现多态必先继承



2.多态的定义和实现

2.1多态构成条件

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

已经形成了继承关系,实现多态还需要以下两个条件

  1. 必须通过基类的指针或者引用调用虚函数
  2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

大家先记住这两个条件,后面讲解虚函数和多态原理后大家就明白了。


2.2虚函数

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

class Person {
public:
	//这里和菱形虚拟继承公用了关键字,但两者是没有关联的
	virtual void BuyTicket() { cout << "买票-全价" << endl;}
};

2.3虚函数的重写(覆盖)

虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。
重写比较好理解,覆盖有点原理层面的意思。

class Person {
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
	virtual void BuyTicket() { cout << "买票-半价" << endl; }
	/*注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因
	为继承后基类的虚函数被继承下来了所以认为派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议
	这样使用*/
	/*void BuyTicket() { cout << "买票-半价" << endl; }*/  //这个写法是可以的
};
void Func(Person& p)  
{
	p.BuyTicket();  //因为是父类引用去调用,所以构成多态
}
int main()
{
	Person ps;
	Student st;
	Func(ps);
	Func(st);
	return 0;
}

谈一谈实现继承和接口继承:

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

虚函数重写的例外:

  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. 析构函数的重写(基类与派生类析构函数的名字不同)
    析构函数看似不同名,违背了虚函数重写的规则,但为了保证资源的正确释放,编译器会对析构函数做特殊处理,编译后会统一处理为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;
}

2.4 C++11 override 和 final

C++对虚函数重写的要求比较严格,有时会因为字母次序不同等导致无法构成重写,但这种情况编译器不会报错,等发现运行结果不对再来矫正就太麻烦了。故C++11引入了override和final两个关键字,可以帮助用户检测是否重写。

  1. final:修饰虚函数,表示该虚函数不能再被重写。
// final用来修饰虚函数用处不大,设计虚函数本就是为了让子类重写实现多态
class Car
{
public:
	virtual void Drive() final {}
};
class Benz :public Car
{
public:
	virtual void Drive() { cout << "Benz-舒适" << endl; }   //这里会报错
};

//final还可以用来修饰类,被修饰的类不能被继承
class A final  
{
	//……
};

class B : public A  //这里会报错
{};
  1. override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
class Car {
public:
	virtual void Drive() {}
};
class Benz :public Car {
public:
	virtual void Drive() override { cout << "Benz-舒适" << endl; }
};

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

  • 重载:(1)两个函数在同一作用域内。  (2)函数名相同,参数列表必须有区别。
  • 隐藏:(1)两个函数分别在基类和派生类的作用域。  (2)函数名相同即可。
  • 重写:(1)两个函数分别在基类和派生类的作用域。  (2)函数名、参数、返回值都必须相同(除开例外)  (3)两个函数必须都是虚函数。

上面的关系中,隐藏的条件比重写简单,也就是说两个基类和派生类的同名函数不构成重写就构成隐藏

//重载
//在同一作用域并且参数列表有区别,属于重载
void fun(int a)
{}

void fun(double a)
{}


//隐藏
//两个同名函数在基类和派生类作用域内,不构成重写就属于隐藏
class A
{
	void fun(int a)
	{}
};

class B : public A
{
	void fun(int a)
	{}
};

//重写
//两个同名函数在基类和派生类作用域内,并且都是虚函数
//函数名、参数列表、返回值都必须一致(除去例外),才能构成重写
class C
{
	virtual void fun(int a)
	{}
};

class D : public C
{
	virtual void fun(int a)
	{}
};



3.抽象类

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

//抽象类的应用场景:比如图形就可以设计为抽象类
//三角形、正方形等继承了该抽象类后重写虚函数,就形成一个具体的类,可以实例化

class Graphics
{
public:
	virtual double GetArea() = 0
	{}//抽象类里面可以设计图形共有的接口,比如求面积等
};

class Square : public Graphics  //正方形
{
public:
	virtual double GetArea()
	{
		return side_length * side_length;
	}
private:
	int side_length = 5;
};

class rotundity : public Graphics  //圆形
{
public:
	virtual double GetArea()
	{
		return 3.14 * radius * radius;
	}
private:
	int radius = 5;
};

int main()
{
	Square sq;
	rotundity ro;
	cout << "正方形面积:" << sq.GetArea() << endl;
	cout << "圆形面积:" << ro.GetArea() << endl;
}



4.多态的原理

//sizeof(Base)是多少? --答案是8(我是32位程序),除了一个int还有一个指针变量
class Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
private:
	int _b = 1;
};

在这里插入图片描述

我们再看看这个指针指向的内容:
在这里插入图片描述
我就直接说了,这个指针指向的是一张表(指针数组),表里面存储的是虚函数的函数地址这个表称为虚函数表(简称虚表),这个对象中的指针称为虚表指针。至于为什么不直接在对象中存函数地址,主要是是节省空间(所有对象可公用虚表)。为什么这样设计呢?我们接着往下看。

//编译器是在编译阶段就确定了调用是否满足多态
//(1)对于满足多态的调用,编译器通过找虚表指针,接着找到虚表中函数地址进行调用
//     多态调用也只是傻傻的执行指令而已
//(2)对于不构成多态的普通调用,编译器通过函数名和参数类型就可以确定调用那个函数

class Person {
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
	virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
void Func(Person& p)  //满足多态条件
{
	p.BuyTicket();
}
int main()
{
	Person Mike;
	Func(Mike);
	Student Johnson;
	Func(Johnson);
	//这里不构成多态,对象调用而不是指针或引用,按正常调用规则即可
	Mike.BuyTicket();  
	return 0;
}

这里可以结合汇编代码来理解
在这里插入图片描述
虚函数重写其实就是继承父类后,拿自己的虚表去覆盖父类的虚表,因此重写也叫覆盖


5.单继承和多继承关系的虚函数表

PS:这一部分不是特别重要。

5.1单继承

有前面的分析,单继承只需要说一点即可:对于那些未重写的虚函数,也是存在虚表中的,至于存储在第一个位置还是最后,就看编译器的实现了。


5.2多继承

class A
{
public:
	virtual void fun(int a)
	{}
};

class B
{
public:
	virtual void fun(int b)
	{}
};

class C : public A, public B  //继承A、B两个类
{
public:
	virtual void fun(int c)
	{}
};

int main()
{
	C c;
	A* p1 = new C;
	B* p2 = new C;
	p1->fun();
	p2->fun();
	delete p1, p1 = nullptr;
	delete p2, p2 = nullptr;
	return 0;
}

在这里插入图片描述

上面的代码中C继承了A、B,并重写了fun(),覆盖两个位置,因此有两个虚表,这也是为了考虑A、B中不同名的虚函数,因此没有合并为一个虚表。但我们发现这两个虚表指向的内容是不一样的!!!
这实际是一种封装,代码中p1和p2最终调用的其实是同一个函数,p2在调用到真正的函数前做了一件事情,那就是调整指针变量指向
在这里插入图片描述


5.3菱形继承和多态

之前说过实际之中应该避免菱形继承,菱形虚拟继承加多态会让对象模型变得异常复杂,这里不细讲,感兴趣的可以看看下面文章:

  • C++虚函数表解析
  • C++ 对象的内存布局

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

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

相关文章

【Vue面试题二十三】、你了解vue的diff算法吗?说说看

文章底部有个人公众号&#xff1a;热爱技术的小郑。主要分享开发知识、学习资料、毕业设计指导等。有兴趣的可以关注一下。为何分享&#xff1f; 踩过的坑没必要让别人在再踩&#xff0c;自己复盘也能加深记忆。利己利人、所谓双赢。 面试官&#xff1a;你了解vue的diff算法吗&…

MFC-对话框

目录 1、模态和非模态对话框&#xff1a; &#xff08;1&#xff09;、对话框的创建 &#xff08;2&#xff09;、更改默认的对话框名称 &#xff08;3&#xff09;、创建模态对话框 1&#xff09;、创建按钮跳转的界面 2&#xff09;、在跳转的窗口添加类 3&#xff0…

树莓派:64位 RPI OS(Bookworm) 更换国内源

几天前新的RPI OS发布了。官方的发版说明里明确注明已经基于Debian Bookworm了。总的来说切到国内源&#xff08;清华&#xff09;跟Bullseye差不多&#xff0c;细节上只有一丢丢不同&#xff08;non-free变成了non-free-firmware&#xff09;。 老规矩&#xff0c;仍然是修改…

二、深度测试(Z Test)

1.是什么 ①从渲染管线出发 ②书面上理解 所谓深度测试&#xff0c;就是针对当前对象在屏幕上&#xff08;更准确的说是frame buffer&#xff09;对应的像素点&#xff0c;讲对象自身的深度值与当前该像素点缓存的深度值进行比较&#xff0c;如果通过了&#xff0c;本对象再改…

七、三层交换机不同网段通信实验

拓扑图&#xff1a; 首先将所有端口ip配置完毕&#xff0c;之后对SW1三层交换机进行配置 创建Vlan10 20网段 进入g0/0/1物理端口&#xff0c;只允许vlan10通过 再进入到g0/0/2端口配置允许通过vlan20的数据包 之后进入vlan的虚拟接口去配置网关ip&#xff0c;一定要先配置物理…

vue:diff库实现文本对比

官方文档 https://www.npmjs.com/package/diff 安装&#xff1a;npm install diff 内容 <template><div><div style"white-space: pre-line;display: flex;"><div><span class"default">{{oldStr}}</span></div&…

游游的字母串 (环形数组两点之间的位置)

题目链接&#xff1a;登录—专业IT笔试面试备考平台_牛客网 题目&#xff1a; 样例&#xff1a; 输入 yab 输出 3 思路&#xff1a; 暴力枚举&#xff0c;全部变成对应的26个字母字符需要的操作步数&#xff0c;取最少的一个操作步数&#xff0c; 这里的操作步数&#xff0…

2021年12月 Python(二级)真题解析#中国电子学会#全国青少年软件编程等级考试

Python编程&#xff08;1~6级&#xff09;全部真题・点这里 C/C编程&#xff08;1~8级&#xff09;全部真题・点这里 一、单选题&#xff08;共25题&#xff0c;每题2分&#xff0c;共50分&#xff09; 第1题 执行以下程序 a[33,55,22,77] a.sort() for i in a:print(i)运行…

Java字符串String

【char】类型代表字符类型&#xff0c;【String】类型代表字符串类型&#xff1b; 1.String类 1.1 声明字符串 在Java中字符串必须包含在一对双引号&#xff08;“ "&#xff09;之内。双引号包含的都是字符串。 声明字符串语法&#xff1a; String str; //声明字符串语…

产品经理如何有效跟进开发进度?

作为产品经理&#xff0c;很难跟进开发过程。随着软件开发的复杂性和不断变化的产品环境&#xff0c;产品经理必须保持在开发过程的顶端&#xff0c;并确保目标得到满足。产品经理如何跟进开发进度&#xff1f; 第一步是对开发过程本身有一个扎实的理解。产品经理必须熟悉开发过…

模型预测控制(MPC)中考虑约束中的不确定性(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

OpenAI开放gpt-3.5turbo微调fine-tuning测试教程

文章目录 openai微调 fine-tuning介绍openai微调地址jsonl格式数据集准备点击上传文件 openai微调 fine-tuning介绍 openai微调地址 网址&#xff1a;https://platform.openai.com/finetune jsonl格式数据集准备 使用Chinese-medical-dialogue-data数据集git clone进行下载 …

iOS 17请了一个免费的医生!它为你免费预诊断你的焦虑和抑郁

你的iPhone&#xff08;多亏了iOS 17&#xff09;现在有了心理健康问卷&#xff0c;可以帮助诊断焦虑和抑郁&#xff0c;并帮助你了解两者的风险。 心理健康问题通常是可怕的、复杂的和微妙的&#xff0c;你不能总是准确地说出你的感受属于哪一类。因此&#xff0c;如果你正在…

GB28181学习(八)——历史视音频的回放

要求 采用SIP协议实现会话&#xff1b;采用SIP扩展协议INFO方法的消息体携带视音频回放控制命令&#xff1b;采用RTP/RTCP实现媒体传输&#xff1b;媒体回放控制命令引用MANSRTSP协议中的PLAY、PAUSE、TEARDOWN的请求消息和应答消息&#xff1b;媒体流接收者可为SIP客户端、SI…

Windows系统上配置Python开发环境

目录 Python安装程序 运行安装程序 选择安装选项 完成安装 自动环境变量设置 手动环境变量设置 验证安装 Python安装程序 首先&#xff0c;您需要从Python官方网站&#xff08;https://www.python.org/downloads/&#xff09;下载Python的最新稳定版本。在页面上选择适用…

接口自动化测试_L3

目录&#xff1a; 整体结构响应断言 响应信息数据极为庞大&#xff0c;针对于“大响应数据”如何断言JSONSchema 简介JSONSchema 整体结构响应断言JSONSchema 的生成JSONSchema 的生成效果界面工具生成第三方库生成&#xff08;Python&#xff09;JSONSchema 验证&#xff08;…

scratch躲避陨石 2023年9月电子学会图形化编程scratch编程等级考试三级真题和答案解析

目录 scratch躲避陨石 一、题目要求 1、准备工作 2、功能实现 二、案例分析

【java学习—七】多态性(34)

文章目录 1. 概念2. 对象的多态2.1. 一2.2. 二 3. 虚拟方法的调用4. 总结 1. 概念 多态性&#xff0c;是面向对象中最重要的概念&#xff0c;在 java 中有两种体现&#xff1a; &#xff08;1&#xff09;方法的重载 (overload) 和重写 (overwrite) 。 重载&#xff1a;本类中…

Go TLS服务端绑定证书的几种方式

随着互联网的发展&#xff0c;网站提供的服务类型和规模不断扩大&#xff0c;同时也对Web服务的安全性提出了更高的要求。TLS(Transport Layer Security)[1]已然成为Web服务最重要的安全基础设施之一。默认情况下&#xff0c;一个TLS服务器通常只绑定一个证书[2]&#xff0c;但…

GEE 18:基于GEE平台的土地荒漠化监测与分析【论文复现】

Desertification 1. 研究背景1.1 参考论文1.2 参数获取1.2.1 NDVI1.2.2 Albedo1.2.3 Normalizing indices1.2.4 Calculating the quantitative relationship1.2.5 Calculating DDI2. GEE2.1 数据2.2 GEE code2.2.1 Study region2.2.2 Reomove cloud for Landsat-82.2.3 Calcula…