C++语法(16)---- 多态

news2024/11/20 14:15:59

https://blog.csdn.net/m0_63488627/article/details/130106690?spm=1001.2014.3001.5501https://blog.csdn.net/m0_63488627/article/details/130106690?spm=1001.2014.3001.5501

目录

1. 多态的概念

2.多态的实现

1.虚函数

2.多态条件

得到的多态条件

特殊条件

3.虚函数析构函数

4.override和final

 3.抽象类

1.概念

2.接口继承和实现继承

4.多态的原理

1.虚函数表

2.父子类虚表关系

3.多态的实现原理

1.静态绑定和态绑定

2.父子在多态调用的原理

3.虚表重要记忆点

5.单继承和多继承的多态

单继承

多继承

菱形继承

菱形虚拟继承


1. 多态的概念

1.概念

多态:就是多种形态,传不同的对象处理,会出现不同的状态

例子如,学生买票半价,普通人买票全价,军人优先买票

辨析:函数重载是通过传入不同的参数,随后能调用不同的同名函数;而多态是面对不同的对象出现不同的行为进行处理

2.多态的实现

1.虚函数

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

class Student :public Person
{
public:
    //虚函数
	virtual void BuyTicket() { cout << "买票-半价" << endl; }
};

虚函数构成重写,也叫做覆盖;

三同:函数名相同,参数相同,返回值相同。

如果不是虚函数,则两个函数构成隐藏;

2.多态条件

得到的多态条件

1.类继承

2.虚函数重写

3.父类指针或者引用调用虚函数

void Func(Person& p)
{
	p.BuyTicket();
}

void Func(Person* p)
{
	p->BuyTicket();
}

比较

普通调用:跟调用对象类型有关,传入对象,运行的是对象中的函数

多态调用:与指向对象有关,指向父类调用父类虚函数,指向子类调用子类虚函数

void Func(Person p)
{
	p.BuyTicket();
}

特殊条件

1.子类可以不加virtual关键字,只要父类有virtual那依然是虚函数

2.协变:三同中,返回值可以不同,都是要求返回值为一个父子类关系的指针或者引用

 此外,传出其他类的父子关系的指针或者引用也可以。

破坏多态条件:

1.父类没有virtual那就破坏了虚函数,调用为普通调用

2.取消引用和指针,就算是虚函数也会变为普通调用

3.虚函数析构函数

推荐:在继承中,将析构函数作为虚函数

原因:

class Person 
{
public:
	~Person()
	{
		cout << "Person delete:" << _p << endl;
		delete _p;
	}
protected:
	int* _p = new int[10];
};

class Student :public Person
{
public:
	~Student()
	{
		cout << "Student delete:" << _s << endl;
		delete _s;
	}
protected:
	int* _s = new int[10];
};

 1.如果是传统的,对象的作用域结束调用析构,父子类都没什么问题。

2.但是如果我们用到了指针,那么会出现问题,其调用就是普通调用,不会指定全部删除;因为析构函数父子同名重定义,那么当传入指针进行析构,那自然是普通调用,父类普通调用还好,可能能清除;但是子类使用父类的指针或者引用则一定只调用了父类的析构。

3.所以,析构函数作为虚函数,那么指针析构,指向的是父类就父类析构,子类就子类析构

修改:

class Person 
{
public:
	virtual ~Person()
	{
		cout << "Person delete:" << _p << endl;
		delete _p;
	}
protected:
	int* _p = new int[10];
};

class Student :public Person
{
public:
	virtual ~Student()
	{
		cout << "Student delete:" << _s << endl;
		delete _s;
	}
protected:
	int* _s = new int[10];
};

满足三同原则,为重写,指向父类调父类,指向子类调子类 

结论:写继承,无脑给父类的析构写virtual关键字

4.override和final

如何实现出一个不被继承的类

1.构造函数设为私有;原因是子类需要父类的构造但是父类的构造设为私有无法访问;那么就无法被继承了 -- 此外析构私有不太行,虽然不能直接构造,但是可以new出来指针,绕过了判断

2.类定义时加final关键字,该类称为最终类

class A final
{ };

此外,final也可以修饰虚函数,这样函数就不能被重写。

class A final
{ 
    virtual func final();
};

override:检查函数是否完成了重写

class A
{ 
    virtual func final(int);
};

class B : public A
{ 
    virtual func final() override;
};

 3.抽象类

1.概念

在虚函数的后面写上 =0 ,则这个函数为纯虚函数;包含纯虚函数的类叫做抽象类。

1.抽象类不能实例化出对象

2.派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。

在我看来,抽象类算是一种模板,如果想继承该模板,必须重写虚函数后可以实例化。那么现实中一个不存在的概念性的对象,可以用抽象类定义。

2.接口继承和实现继承

1.普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。

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

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

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

p调用的test()是B从A中继承的,那么test中调用func函数,是this指针调用的,this是test的指针即A的指针,但是A指针是p切割后得到的,那么func是满足虚函数要求,调用func时是B的函数是多态情况,但是要知道虚函数是接口继承,可认为A中func函数被替换为B中的,但是val=1没有变,所以答案选B

4.多态的原理

1.虚函数表

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

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

虚表指针指向虚表,虚表存放虚函数的地址,有几个虚函数就有几个虚函数地址存储

2.父子类虚表关系

1.父类结构其实跟上面说的一样,一个虚表指针指向虚表,虚表里有虚函数地址

2.子类继承了父类,那子类中的父类有虚指针,但是虚表的内容发生变化;拷贝父类,将完成重写的虚函数进行覆盖,覆盖原来指向

3.虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。

3.多态的实现原理

1.静态绑定和态绑定

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

静态调用:就是编译器能确定调用的函数,以至于它call的是函数的地址

动态调用:编译器看不出来,需要算出函数的地址,再调用之

2.父子在多态调用的原理

1.对于一个指针指向无论是父类还是子类,指针能读到的都是父类的那一部分

2.父类的虚表就是上面说过的那样的,子类是覆盖了虚表

3.调用方式都是取虚表指针,在虚表中找到虚函数的地址取实现调用

4.也就是说,多态的实现取决于虚表

3.虚表重要记忆点

1.虚表放在常量区/代码段

2.同一个类型的虚表是一样,不同类型的虚表是不一样的

5.单继承和多继承的多态

单继承

class Base {
public:
	virtual void func1() { cout << "Base::func1" << endl; }
	virtual void func2() { cout << "Base::func2" << endl; }
private:
	int _a;
};

class Derive :public Base {
public:
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func3() { cout << "Derive::func3" << endl; }
private:
	int _b;
};

typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[],int num)
{
	cout << " 虚表地址>" << vTable << endl;
	for (int i = 0; i<num; ++i)
	{
		printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);
		VFPTR f = vTable[i];
		f();
	}
	cout << endl;
}

int main()
{
	Base b;
	Derive d;
	VFPTR * vTableb = (VFPTR*)(*(void**)&b);
	PrintVTable(vTableb,2);
	VFPTR* vTabled = (VFPTR*)(*(void**)&d);
	PrintVTable(vTabled,3);
	return 0;
}

此时我们发现:子类没有构成虚函数的函数也在虚表中 

多继承

class Base1 {
public:
	virtual void func1() { cout << "Base1::func1" << endl; }
	virtual void func2() { cout << "Base1::func2" << endl; }
private:
	int b1;
};

class Base2 {
public:
	virtual void func1() { cout << "Base2::func1" << endl; }
	virtual void func2() { cout << "Base2::func2" << endl; }
private:
		int b2;
};

class Derive : public Base1, public Base2 {
public:
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func3() { cout << "Derive::func3" << endl; }
private:
	int d1;
};
typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
	cout << " 虚表地址>" << vTable << endl;
	for (int i = 0; vTable[i] != nullptr; ++i)
	{
		printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);
		VFPTR f = vTable[i];
		f();
	}
	cout << endl;
}
int main()
{
	Derive d;
	VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);
	PrintVTable(vTableb1);
	VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d + sizeof(Base1)));
	PrintVTable(vTableb2);
	return 0;
}

1.多继承,只有有虚函数的父类,才会生成虚表 

2.子类自己的函数,存在第一个虚表中

菱形继承

其模型就是菱形继承的虚函数,其实就是D中有BC;而BC还要A的虚表,所以BC自带两个虚表

菱形虚拟继承

1.如果是菱形虚拟继承,则D一定要写虚函数;因为如果不写,A虚函数不知道写在B表还是C表,其存在二义性编译器看不懂。

2.D类会多一个虚表

3.B的第一个表是虚表,第二个是虚基表,C亦是如此

4.虚基表的开头找的是虚函数的偏移量。

class A{
public:
    A(char *s) { cout<<s<<endl; }
    ~A(){}
};

class B:virtual public A
{
public:
    B(char *s1,char*s2):A(s1) { cout<<s2<<endl; }
};

class C:virtual public A
{
public:
    C(char *s1,char*s2):A(s1) { cout<<s2<<endl; }
};

class D:public B,public C
{
public:
    D(char *s1,char *s2,char *s3,char *s4):B(s1,s2),C(s1,s3),A(s1)
    { cout<<s4<<endl;}
};

int main() {
    D *p=new D("class A","class B","class C","class D");
    delete p;
    return 0;
}

A:class A class B class C class D 
B:class D class B class C class A
C:class D class C class B class A 
D:class A class C class B class D

1.A只有一个,所以A只构造一次

2.优先A构造

3.初始化列表优先于括号内,所以D最后

4.构造按继承顺序,即使先C(s1,s3),B(s1,s2) 也是B先输出,C再输出

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

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

相关文章

Socks5代理和IP代理

Socks5代理和IP代理是常用的网络代理服务&#xff0c;它们为用户提供了匿名访问和保护隐私的功能。在本文中&#xff0c;我们将介绍这两种代理的基本概念和工作原理&#xff0c;并展示如何编写一个简单的代理服务器。 一、什么是Socks5代理和IP代理&#xff1f; Socks5代理…

[操作系统安全]SetUID与Capability权能

问题一、解释“passwd”, “sudo” , “ping”等命令为什么需要 setuid位&#xff0c;去掉s位试运行&#xff0c;添加权能试运行。 1、为什么需要setuid位&#xff1a; 首先明确setuid的作用是&#xff1a;执行该设置后&#xff0c;文件执行时将以文件拥有者的身份执行&#xf…

C++【栈队列(3种)反向迭代器】

文章目录一、容器适配器二、栈&#xff08;一&#xff09;栈定义&#xff08;二&#xff09;栈使用接口&#xff08;三&#xff09;栈模拟实现(1) 栈模拟实现解析(2) 栈模拟实现代码(3) 栈模拟结果三、队列&#xff08;一&#xff09;普通队列&#xff08;1&#xff09;普通队列…

4、浅谈Makefile文件及其简单的使用知识

文章目录1、什么是Makefile&#xff1f;&#xff08;1&#xff09;makefile关系到了整个工程的编译规则。&#xff08;2&#xff09;makefile带来的好处就是——“自动化编译”&#xff08;3&#xff09;make是一个命令工具&#xff0c;是一个解释makefile中指令的命令工具2、为…

[FREERTOS]队列

1.什么是队列 队列也称消息队列&#xff0c;是一种常用于任务间通信的数据结构&#xff0c;队列可以在任务之间&#xff0c;中断和任务之间传递信息 2.传递信息为什么不用全局变量呢&#xff1f; 确实全局变量依然可以传递信息&#xff0c;但是如果全局变量改变的很频繁&#x…

网工必知—什么是堡垒机?-CCIE

什么是堡垒机&#xff1f; 网络工程师一定听过或用过所谓的“堡垒机”&#xff0c;那么堡垒机到底是什么呢&#xff1f; 堡垒机是一种跳板机制&#xff08;Jump Server&#xff09;&#xff0c;在一个特定的网络环境下&#xff0c;为了保障网络和数据不受来自外部和内部用户的…

flink 1.16 在centos安装 部署踩的坑

报错: 1 RESOURCES_DOWNLOAD_DIR : 这个错误是修改了 conf目录下 的 master 或 workers 等信息造成的. 2 修改了这个信息可能会造成输入密码的问题. 3 Could not connect to BlobServer at address localhost/127.0.0.1:39203 这个端口还会变化,这种问题可能是因为conf下的…

Python将Word文件中的内容写入Excel文件

在日常办公中我们经常需要将word文件中的数据写入Excel中&#xff0c;如果是手动一个一个进行复制粘贴&#xff0c;那将会非常的耗时且繁琐&#xff01; 遇到这种问题我们首先想到就是利用编程解决&#xff0c;今天我分享一个word转excel的小方法&#xff01; 首先我有一个wo…

常见漏洞扫描工具AWVS、AppScan、Nessus的使用

HVV笔记——常见漏洞扫描工具AWVS、AppScan、Nessus的使用1 AWVS1.1 安装部署1.2 激活1.3 登录1.4 扫描web应用程序1.4.1 需要账户密码登录的扫描1.4.2 利用录制登录序列脚本扫描1.4.3 利用定制cookie扫描1.5 扫描报告分析1.5.1 AWVS报告类型1.5.2 最常用的报告类型&#xff1a…

Microchip的10M以太网解决方案

&#xff08;以下所有图片均来源于Microchip官网&#xff09; 一 为什么需要10M车载以太网 目前车载百兆以太网&#xff08;100Base-T1&#xff09;和千兆以太网&#xff08;1000Base-T1&#xff09;技术较为成熟&#xff0c;但如果直接用100Base-T1/1000Base-T1来替代目前被广…

anaconda 创建虚拟环境 基本命令操作

下载好之后直接打开 anaconda prpmpt : 此时直接输入 &#xff1a;activate 不加环境名是直接进入到base环境中的 必须先进入到base环境中再使用后边的命令 &#xff1a; activate 直接进入base环境&#xff1a;如图&#xff1a;conda create -n 名字 python3.7 创建虚拟…

善用Embedding,我们来给文本分分类

你好&#xff0c;我是徐文浩。 上一讲里我们看到大模型的确有效。在进行情感分析的时候&#xff0c;我们通过OpenAI的API拿到的Embedding&#xff0c;比T5-base这样单机可以运行的小模型&#xff0c;效果还是好很多的。 不过&#xff0c;我们之前选用的问题的确有点太简单了。…

springboot服务端接口外网远程调试,并实现HTTP服务监听 - 内网穿透

文章目录前言1. 本地环境搭建1.1 环境参数1.2 搭建springboot服务项目2. 内网穿透2.1 安装配置cpolar内网穿透2.1.1 windows系统2.1.2 linux系统2.2 创建隧道映射本地端口2.3 测试公网地址3. 固定公网地址3.1 保留一个二级子域名3.2 配置二级子域名3.2 测试使用固定公网地址4. …

segment anything paper笔记

demo主页&#xff08;包含paper, demo, dataset&#xff09; 通过demo可以看到一个酷炫的效果&#xff0c;鼠标放在任何物体上都能实时分割出来。 segment anything宣传的是一个类似BERT的基础类模型&#xff0c;可以在下游任务中不需要再训练&#xff0c;直接用的效果。 而且…

增强领域的知识图谱

以下是一些近两年基于知识图谱做知识增强的顶会论文&#xff1a; "knowledge-enhanced hierarchical graph convolutional networks for intent detection" (acl 2021) "kg-bert: bert for knowledge graph completion" (emnlp 2019) "k-adapter: i…

C语言刷题--内存存储、操作符

魔王的介绍&#xff1a;&#x1f636;‍&#x1f32b;️一名双非本科大一小白。魔王的目标&#xff1a;&#x1f92f;努力赶上周围卷王的脚步。魔王的主页&#xff1a;&#x1f525;&#x1f525;&#x1f525;大魔王.&#x1f525;&#x1f525;&#x1f525; ❤️‍&#x1…

04-Mysql常用操作

1. DDL 常见数据库操作 # 查询所有数据库 show databases; # 查询当前数据库 select databases();# 使用数据库 use 数据库名;# 创建数据库 create database [if not exits] 数据库名; # []代表可选可不选# 删除数据库 drop database [if exits] 数据库名; 常见表操作 创建…

Nestjs实战干货-概况-中间件-Middleware

中间件 中间件是一个在路由处理程序之前被调用的函数。中间件函数可以访问请求和响应对象&#xff0c;以及应用程序的请求-响应周期中的next()中间件函数。下一个中间件函数通常由一个名为next的变量来表示。 Nest 中间件在默认情况下等同于Express中间件。下面是来自官方 exp…

MBD—模型的回调函数

目录 前面 如何设置&#xff1f; 应用 简单的提示 数据的初始化 前面 常用的回调函数有三类&#xff1a;模型的回调函数、模块的回调函数、信号的回调函数。这里分享一下模型的回调函数。 回调函数就是CallBack. 如何设置&#xff1f; 打开一个模型&#xff0c;在空白…

【计算机网络】TCP拥塞控制、丢包重传机制与滑动窗口机制

文章目录TCP 拥塞控制原理为什么拥塞期间&#xff0c;发送方会收到接收方的重复确认&#xff1f;TCP 滑动窗口机制原理TCP 丢包重传机制TCP 拥塞控制原理 TCP拥塞控制是指在网络拥塞的情况下&#xff0c;TCP协议通过调整发送数据的速率来避免网络拥塞的一种机制。TCP拥塞控制的…