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

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再输出

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

数据结构入门-10-AVL

文章目录 一、AVL的性质1.2 平衡二叉树定义 二、添加需达到平衡2.1 平衡因子2.1.2 平衡因子的实现 2.2 判断该二叉树是否为平衡二叉树2.3 左旋右旋2.3.1 左旋LL右旋RR基本原理2.3.2 LR RLLRRL 三、AVL中删除 一、AVL的性质 平衡二叉树 AVL树得名于它的俄罗斯发明者G. M. Adels…

被裁员了,要求公司足额补缴全部公积金,一次补了二十多万!网友兴奋了,该怎么操作?...

被裁员后&#xff0c;能要求公司补缴公积金吗&#xff1f; 一位网友问&#xff1a; 被裁员了&#xff0c;要求公司把历史公积金全部足额缴纳&#xff0c;现在月薪2.3万&#xff0c;但公司每个月只给自己缴纳300元公积金&#xff0c;结果一次补了二十多万&#xff0c;一次性取出…

Node 【Buffer 与 Stream】

文章目录 &#x1f31f;前言&#x1f31f;Buffer&#x1f31f; Buffer结构&#x1f31f; 什么时候用Buffer&#x1f31f; Buffer的转换&#x1f31f; Buffer使用&#x1f31f; 创建Buffer&#x1f31f; 字符串转Buffer&#x1f31f; Buffer转字符串&#x1f31f; 拼接Buffer&am…

Java每日一练(20230417)

目录 1. N 皇后 &#x1f31f;&#x1f31f;&#x1f31f; 2. 搜索二维矩阵 &#x1f31f;&#x1f31f; 3. 发奖金问题 &#x1f31f; &#x1f31f; 每日一练刷题专栏 &#x1f31f; Golang每日一练 专栏 Python每日一练 专栏 C/C每日一练 专栏 Java每日一练 专栏 …

权限控制_SpringSecurity

认证-授权 认证&#xff1a;系统提供的用于识别用户身份的功能&#xff0c;通常提供用户名和密码进行登录其实就是在进行认证&#xff0c;认证的目的是让系统知道你是谁。 授权&#xff1a;用户认证成功后&#xff0c;需要为用户授权&#xff0c;其实就是指定当前用户可以操作…

Node 内置模块 【fs模块】

文章目录 &#x1f31f;前言&#x1f31f;fs模块&#x1f31f; 使用fs模块&#x1f31f; 异步编程和同步编程&#x1f31f; 异步编程&#x1f31f; 同步编程 &#x1f31f;常用操作&#x1f31f; 文件操作&#x1f31f; readFile异步读取文件&#x1f31f; readFileSync同步读取…

YOLOv8 更换主干网络之 GhostNetV2

《GhostNetV2:Enhance Cheap Operation with Long-Range Attention》 轻量级卷积神经网络(CNN)是专门为在移动设备上具有更快推理速度的应用而设计的。卷积操作只能捕捉窗口区域内的局部信息,这防止了性能的进一步提高。将自注意力引入卷积可以很好地捕捉全局信息,但这将大…

【系统集成项目管理工程师】项目进度管理

&#x1f4a5;十大知识领域&#xff1a;项目进度管理 主要考计算题 项目进度管理包括以下 7 个过程: 规划进度管理过程定义活动过程排列活动顺序过程估算活动资源过程估算活动持续时间过程制定进度计划过程控制进度过程 一、规划进度管理过程 制定政策、程序和文档以管理项目进…

JeecgBoot 3.5.1 版本发布,开源的企业级低代码平台

项目介绍 JeecgBoot是一款企业级的低代码平台&#xff01;前后端分离架构 SpringBoot2.x&#xff0c;SpringCloud&#xff0c;Ant Design&Vue3&#xff0c;Mybatis-plus&#xff0c;Shiro&#xff0c;JWT 支持微服务。强大的代码生成器让前后端代码一键生成! JeecgBoot引领…

苹果电容笔值得买吗?ipad电容笔推荐平价

在当今时代&#xff0c;高科技已经成为推动数字产品发展的重要推动力。无论是在工作上&#xff0c;还是在学习上&#xff0c;大屏幕都能起到很好的作用。IPAD将会更好地融入我们的生活&#xff0c;不管是现在还是未来。而ipad配上一支简单的电容笔&#xff0c;不仅可以提高工作…

几分种学会React Router v6使用

React路由可以实现页面间的切换。 传送门&#xff1a;英文文档 中文教程&#xff1a; https://www.reactrouter.cn/docs/getting-started/tutorial 1.基础使用 1.安装react-router npm i react-router-dom62.配置根组件app.js import { React, lazy, Suspense } from "…

C++ -3- 类和对象 (中) | 构造函数与析构函数(一)

文章目录 1.类的6个默认成员函数2.构造函数3.析构函数构造函数与析构函数应用场景缺省值初始化 1.类的6个默认成员函数 如果一个类中什么成员都没有&#xff0c;简称为空类。 空类中真的什么都没有吗&#xff1f;并不是&#xff0c;任何类在什么都不写时&#xff0c;编译器会自…

【文章学习系列之模型】FEDformer

本章内容 文章概况模型流程主要结构Frequency Enhanced Decomposition Architecture&#xff08;频率增强分解结构&#xff09;Fourier enhanced blocks and Wavelet enhanced blocks&#xff08;傅里叶增强模块和小波增强模块&#xff09;Fourier Enhanced Structure&#xff…

【Java 数据结构】优先级队列 (堆)

&#x1f389;&#x1f389;&#x1f389;点进来你就是我的人了 博主主页&#xff1a;&#x1f648;&#x1f648;&#x1f648;戳一戳,欢迎大佬指点!人生格言&#xff1a;当你的才华撑不起你的野心的时候,你就应该静下心来学习! 欢迎志同道合的朋友一起加油喔&#x1f9be;&am…

快速精简软件,如何让软件缩小到原来的5%大小,从删除文件入手,到修改C++引用库,合规解决存储问题

Hi~大家好&#xff0c;今天制作一个简单的精简软件的教学~ 事先说明下&#xff0c;精简软件并不违反任何规定&#xff0c;尤其是开源软件&#xff0c;这里也仅讨论开源软件的修改&#xff0c;根据几乎所有开源软件的开源规则&#xff0c;精简软件&#xff0c;本质也就是修改软件…

戴尔G3 Ubuntu18.04双系统安装

ROS学习需要使用Linux系统&#xff0c;首先就是Ubuntu&#xff0c;我选择的是18.04.6这个版本&#xff0c;因为后面我要使用以Jetson Nano为主控的Jetbot进行ROS编程&#xff0c;Jetbot所带的出厂镜像就是18.04&#xff0c;为了方便程序移植&#xff0c;以及减少不必要的麻烦。…

【消息队列】聊一下Kafka副本机制

副本机制的好处 副本在分布式系统下&#xff0c;不同的网络互联的机器保存同一份数据。我们知道在分布式系统中&#xff0c;都会通过数据镜像、数据冗余的方式来提升高可用性。 提供数据冗余&#xff1a;这点比较好理解&#xff0c;说白了就是通过数据冗余在不同的服务器上&a…

大家副业都在做什么?csgo搬砖靠谱的副业推荐给你

从来没想过&#xff0c;以前只会玩CSGO的男孩子&#xff0c;现在居然能借助游戏赚到钱了&#xff01;甚至不需要什么专业的技巧&#xff0c;简简单单 在steam平台选择有利润的道具后&#xff0c;再上架到国内网易BUFF平台&#xff0c;赚取“信息差”差价而已&#xff01; 谁大…

SpringCloud学习(六)——Feign的简单使用

文章目录 1. Feign 的使用1.1 引入依赖1.2 添加注解1.3 编写Feign客户端1.4 测试 2. Feign中的自定义配置2.1.配置文件方式2.2.Java代码方式 3. Feign 性能优化4. Feign的抽取式使用4.1 抽取配置4.2 引入依赖4.3 指明Client 在此之前&#xff0c;我们服务之间需要进行调用的时候…

读懂MAC地址

MAC地址是一种用于标识计算机网络设备的唯一地址。它是由48个二进制数字组成的&#xff0c;通常表示为12个十六进制数字&#xff0c;每两个数字之间用冒号或连字符分隔开。MAC地址由设备制造商在生产过程中分配&#xff0c;以确保网络上每个设备都有唯一的标识符。 MAC地址的规…