C++进阶篇2---多态

1.多态的概念

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

举个例子:同样是吃饭,狗吃狗粮,猫吃猫粮,不同的对象,对于同一个行为会有不同的状态

2.多态的定义和实现

2.1虚函数

虚函数:即被virtual修饰的类成员函数,注意是类成员函数,其他函数不能被virtual修饰

class A {
public:virtual void func() {cout << "hello C++" << endl;}
};

2.2虚函数的重写

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

class A {
public:virtual void func() {cout << "hello C++" << endl;}
};class B : public A{
public://虚函数重写virtual void func() {cout << "hello ZXWS" << endl;}
};

2.3多态的构成条件

1.必须通过基类的指针或者引用调用虚函数

2.被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

class A {
public:virtual void func() {cout << "hello C++" << endl;}
};class B : public A{
public:virtual void func() {cout << "hello ZXWS" << endl;}
};void test(A& a)//必须是基类的引用/指针
{a.func();//该函数是虚函数且被重写
}int main()
{A a;B b;test(a);test(b);return 0;
}

 总结:多态的实现本质就是由虚函数的重写实现的,再次强调一下

虚函数重写规则的总结和补充

1.virtual关键字

2.三同(返回值类型/函数参数/函数名相同)

注意:

1.返回值不同也能构成虚函数重写,但是返回值的类型必须是引用/指针,称为协变,(返回值得全是指针/全是引用,且得是父子关系---父对父,子对子【基类中的虚函数返回值为父类,派生类中虚函数返回值为子类】,这里的父子关系可以是任意一对父子关系)

【注释】:基类、派生类和父类、子类是一个意思,上面的表述是为了表示两对父子关系

class A {};
class B : public A {};class Person {
public://父对父,子对子,对virtual A* func() {cout << "A* func()" << endl;return nullptr;}//父对子,子对父,错//virtual B* func() {//	cout << "A* func()" << endl;//	return nullptr;//}
};class Student : public Person {
public:virtual B* func() {cout << "B* func()" << endl;return nullptr;}//virtual A* func() {//	cout << "B* func()" << endl;//	return nullptr;//}
};

2.析构函数的重写(正常来看基类和派生类的名字不同)

如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor
class Person {
public:virtual ~Person(){cout << "~Person()" << endl;}
};class Student : public Person {
public:virtual ~Student() {cout << "~Student()" << endl;}
};int main()
{Person* p = new Person;Person* s = new Student;//如果析构函数不能实现虚函数重写,这里的空间释放就会出现问题//所以编辑器将析构函数的名字做了特殊处理delete p;delete s;return 0;
}
3.只要基类中的虚函数加了关键字virtual,派生类中可以不加,(推荐都加上)

2.4 C++11 override 和 final 

从上面的虚函数重写的规则,我们不难看出C++对虚函数的重写比较严格,这就导致我们在写的时候容易出错,且一些种错误在编译期间不会报错,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此:C++11提供了override和final两个关键字,可以帮助用户检测是否重写

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

 2.override: 检查派生类虚函数是否重写了基类某个虚函数。

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

 3.抽象类

3.1概念

在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口 类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生 类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承
class A {
public:virtual void func() = 0;
};class B : public A {
public:virtual void func() {cout << "hello C++" << endl;};
};class C : public A {
public:virtual void func() {cout << "hello ZXWS" << endl;};
};int main()
{//A a;//抽象类不能实例化对象B b;C c;A* pb = &b;A* pc = &c;pb->func();pc->func();return 0;
}

 3.2接口继承和实现继承

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

大家来猜猜下面代码的打印结果

class A {
public:virtual void test() {func();}virtual void func(int x = 0) {cout << "A->" << x << endl;}
};
class B : public A {
public:virtual void func(int x = 1) {cout << "B->" << x << endl;}
};int main()
{B b;A* pb = &b;pb->test();return 0;
}

代码分析:

首先pb调用A类中的test函数,因为B类中没有重写该虚函数,所以还是调用A类中的test。然后test函数里调用func函数,func函数实现了重写,所以调用B类中的,但是注意,这里的重写只是重写函数的内部实现,函数接口(函数的声明)还是A类的函数接口,所以x的缺省值为0,而不是1

大家要清楚普通继承和虚函数继承的差别!!!

4.多态的原理

在讲多态的原理之前,我们先来看看下面代码的运行结果

class A {
public:virtual void func(int x = 0) {cout << "A->" << x << endl;}
private:int _a;char ch;
};
int main()
{cout << sizeof(A) << endl;return 0;
}

 哎?为什么这是12呢,正常我们用内存对齐来算的结果应该是8才对呀,这个类和之前的类的唯一区别在于它多了一个虚函数,我们就猜测类里面来应该存了func这个函数的相关信息。

下面我们来调试看看

显然,a对象中确实多了一个指针,所以对象a的大小为12(用的是32位的编译环境,地址是4字节),那么这个指针有什么用呢?

这个指针指向的空间里存放了虚函数的地址,我们称这块空间为虚函数表,简称虚表,所以这个指针叫做虚函数表指针 (一个含有虚函数的类中都至少有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中)

那么多态是如何通过虚函数实现的呢?

通过上面的图,我们就应该能理解多态的实现原理了,本质就是在虚表里查找函数,里面放的啥函数就调用啥函数,而虚表里存放的虚函数,根据子类对父类的虚函数有没有重写,来决定存放哪个函数的地址

同时这里也能解释为什么多态的实现需要调用父类的引用/指针,因为父类对子类对象的引用并没有生成临时变量,而是直接引用的子类继承来的父类部分,那么当然也能通过虚函数表指针调用对应的函数实现多态,指针同理

(注意:有些平台可能会将虚函数表指针放到对象的最后面,这个跟平台有关)

 还有一点:同一个类的对象公用同一张虚函数表

 4.3动态绑定和静态绑定

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

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

虚函数表存在哪个区域?栈?堆?还是什么?

class A {
public:virtual void func() { cout << "A" << endl; }
};
void test()
{A aa;int a=0;int* p = new int;const char* str = "hello ZXWS";static int c = 0;printf("代码区:%p\n",str);printf("静态区:%p\n", &c);printf("堆区:%p\n", p);printf("栈区:%p\n", &a);printf("虚表:%p\n", *((int*)&aa));//VS中虚函数表指针的地址一般在对象的起始位置printf("虚函数:%p\n", &A::func);return 0;
}

很显然,虚表的地址离代码区比较近,其实虚表就在代码区,这也符合我们的认识,因为虚表的内容是不能被修改的,当然虚函数其实和正常函数没什么区别,都在代码区

5.1单继承

class A {
public:virtual void func1() { cout << "A::func1" << endl; }virtual void func2() { cout << "A::func2" << endl; }
private:int _a;
};class B {
public:virtual void func1() { cout << "B::func1" << endl; }virtual void func3() { cout << "B::func3" << endl; }virtual void func4() { cout << "B::func4" << endl; }
private:int _b;
};int main()
{A a;B b;return 0;
}

当我们看到调试窗口,就会发现B类的虚函数表中只有两个虚函数,而B类中应该有4个虚函数才对,那么会为什么呢?

这里解释一下,这是编辑器自己做了处理,我们要想看清,还是得看地址空间,如下

 (VS编辑器会在虚函数表的结尾放一个空指针)从内存窗口来看,b的虚函数表确实是存了4个地址,但是我们不能确定,下面我们来验证一下

typedef void(*VF)();//重定义函数指针
void PrintVF(VF v[]) {for (int i = 0; v[i]; i++) {printf("[%d]:%p-> ", i, v[i]);v[i]();//用函数指针调用函数printf("\n");}
}
int main()
{A a;B b;PrintVF((VF*)(*(int*)&b));//这里传的是虚函数表指针//VS中虚函数表指针的地址一般在对象的起始位置,这里注意32位地址4字节,64位地址8字节//这里是32位下,用int*,64位下要用long long*return 0;
}

通过验证,单继承中虚函数确实都在虚函数表中(验证时要注意,编辑器有时候对虚表处理不干净,需要我们重新生成解决方案,重新编译一下)

5.2多继承中的虚函数表

class A1 {
public:virtual void func1() { cout << "A1::func1" << endl; }virtual void func2() { cout << "A1::func2" << endl; }
private:int a1;
};class A2 {
public:virtual void func1() { cout << "A2::func1" << endl; }virtual void func2() { cout << "A2::func2" << endl; }
private:int a2;
};class B : public A1, public A2 {
public:virtual void func1() { cout << "B::func1" << endl; }virtual void func3() { cout << "B::func3" << endl; }
private:int b;
};int main()
{B b;printf("虚表地址:%p\n",* ((int*)&b));PrintVF((VF*)(*((int*)&b)));A2* p = &b;printf("虚表地址:%p\n", *((int*)p));PrintVF((VF*)(*((int*)p)));return 0;
}

可以看出:多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中,且子类会继承父类的虚函数表

这里简单说明一下,为什么两个虚表中B::func1函数的地址不同的问题,其实这是为了修正this指针,我们知道多态的实现离不开指针(引用),当我们用p去调用func1函数时,显然传过去的this指针是不对的,p仅仅指向A2的部分,所以编辑器会在调用B::func1这个函数之前,将this指针修正成b对象的起始地址,而继承来的A1成员的起始地址恰好和b对象的起始地址相同,不需要修正,所以直接填了函数地址,所以两者地址不同,但是最终调用的函数一样

5.3菱形继承、菱形虚拟继承

这里只要了解即可

菱形进程和多继承一样,无非是消耗了空间,这里就不多讲了

我们来看看菱形虚拟继承

class A {
public:virtual void func1() { cout << "A1::func1" << endl; }
private:int a=1;
};class A1 : virtual public A{
public:virtual void func1() { cout << "A1::func1" << endl; }virtual void func2() { cout << "A1::func2" << endl; }
private:int a1=2;
};class A2 : virtual public A {
public:virtual void func1() { cout << "A2::func1" << endl; }virtual void func2() { cout << "A2::func2" << endl; }
private:int a2=3;
};class B : public A1, public A2 {
public:virtual void func1() { cout << "B::func1" << endl; }virtual void func3() { cout << "B::func3" << endl; }
private:int b=4;
};

(上面两个图是分开调试的,需要单独分析,仅仅当长个见识,不会也没关系)

六、一些小点

1.inline函数可以是虚函数吗?

可以,不过编译器就忽略inline属性,这个函数就不再是 inline,因为虚函数要放到虚表中
class A {
public:inline virtual void func() { cout << "A::func()"; }
};
int main()
{A a;A* p = new A;a.func();p->func();return 0;
}

(上面的调试内容需要将相关优化打开,正常调试看不到,这里也就是给大家看看)

 2.静态成员可以是虚函数吗?

不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。
3.构造函数可以是虚函数吗?
不可以,因为虚表指针是在构造函数初始化列表阶段才初始化的
4.对象访问普通函数快还是虚函数快?
首先如果是普通对象,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。

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

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

相关文章

nodejs+vue大学生社团管理系统

通过软件的需求分析已经获得了系统的基本功能需求&#xff0c;根据需求&#xff0c;将大学生社团管理系统平台功能模块主要分为管理员模块。管理员添加社团成员管理、社团信息管理&#xff0c;社长管理、用户注册管理等操作。 目 录 摘 要 I ABSTRACT II 目 录 II 第1章 绪论 1…

asp.net社区医疗辅助诊断网站系统VS开发sqlserver数据库web结构c#编程

一、源码特点 asp.net社区医疗辅助诊断网站系统 是一套完善的web设计管理系统&#xff0c;系统采用mvc模式&#xff08;BLLDALENTITY&#xff09;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S模式开发。开发环境为vs2010&#xff0c;数据库为sqlserver200…

spring懒加载

简介 Spring默认会在容器初始化的过程中&#xff0c;解析xml或注解&#xff0c;创建配置为单例的bean并保存到一个map中&#xff0c;这样的机制在bean比较少时问题不大&#xff0c;但一旦bean非常多时&#xff0c;spring需要在启动的过程中花费大量的时间来创建bean &#xff0…

Flink学习笔记(四):Flink 四大基石之 Window 和 Time

文章目录 1、 概述2、 Flink 的 Window 和 Time2.1、Window API2.1.1、WindowAssigner2.1.2、Trigger2.1.3、Evictor 2.2、窗口类型2.2.1、Tumbling Windows2.2.2、Sliding Windows2.2.3、Session Windows2.2.4、Global Windows 2.3、Time 时间语义2.4、乱序和延迟数据处理2.5、…

Git(一)Windows下安装及使用Git Bash

目录 一、简介1.1 什么是Git&#xff1f;1.2 Git 的主要特点1.3 什么是 Git Bash&#xff1f; 二、下载三、安装3.1 同意协议3.2 选择安装位置3.3 其他配置&#xff08;【Next】 即可&#xff09;3.4 安装完毕3.5 打开 Git Bash 官网地址&#xff1a; https://www.git-scm.com/…

视频去噪网络BSVD的实现

前些天写了视频去噪网络BSVD论文的理解&#xff0c;详情请点击这里&#xff0c;这两个星期动手实践了一下&#xff0c;本篇就来记录一下这个模型的实现。 这个网络的独特之处在于&#xff0c;它的训练和推理在实现上有所差别。在训练阶段&#xff0c;其使用了TSM&#xff08;T…

基于斑马优化的BP神经网络(分类应用) - 附代码

基于斑马优化的BP神经网络&#xff08;分类应用&#xff09; - 附代码 文章目录 基于斑马优化的BP神经网络&#xff08;分类应用&#xff09; - 附代码1.鸢尾花iris数据介绍2.数据集整理3.斑马优化BP神经网络3.1 BP神经网络参数设置3.2 斑马算法应用 4.测试结果&#xff1a;5.M…

虚拟机安装centos系统后配置桥接网络

一.桥接网络和nat网络的区别 桥接模式 通过使用物理机网卡 具有单独ip,但是需要手动配置。 在bridged模式下&#xff0c;VMWare虚拟出来的操作系统就像是局域网中的一台独立的主机&#xff0c;它可以访问网内任何一台机器。主机网卡和虚拟网卡的IP地址处于同一个网段&#xff…

Mybatis的SqlRunner执行流程

Mybatis的SqlRunner执行流程 SqlRunner exec new SqlRunner(connection); Map<String, Object> row exec.selectOne("SELECT * FROM PRODUCT WHERE PRODUCTID ?", "FI-SW-01");connection.close();assertEquals("FI-SW-01", row.ge…

【QT开发(10)】QT 进程

文章目录 1.1 运行一个新进程1.2 QProcess 还可以对一些信号进行关联2 进程间通信2.1 使用共享内存实现进程通信2.2 演示 代码仓库参考 1.1 运行一个新进程 使用类 QProcess&#xff0c;允许将一个进程堪称一个顺序IO设备。 在Qt中&#xff0c;QProcess类是用于启动外部进程的…

大模型与知识图谱如何相互助力

目前各行各业在数字化、智能化发展的大势所趋下&#xff0c;信息新技术不断涌现&#xff0c;也在加快深入融合到传统实体行业应用中&#xff0c;比如知识图谱、人工智能、数字孪生等等&#xff0c;特别是基于人工智能的大模型在去年底被chatgpt的带领下涌现出一波又一波的浪潮&…

驱动开发1 概念、内核模块编程、内核消息打印函数printk函数的使用、内核模块传参、内核导出符号

1 驱动相关概念 2 内核模块编程 内核模块编写实例代码注释 #include <linux/init.h> #include <linux/module.h>//入口函数&#xff0c;安装内核模块时执行 static int __init mycdev_init(void) {//static 修饰当前函数只能在本文件使用//int 函数的返回值类型&a…

【Leetcode】【中等】1726.同积元组

力扣&#xff08;LeetCode&#xff09;官网 - 全球极客挚爱的技术成长平台备战技术面试&#xff1f;力扣提供海量技术面试资源&#xff0c;帮助你高效提升编程技能&#xff0c;轻松拿下世界 IT 名企 Dream Offer。https://leetcode.cn/problems/tuple-with-same-product/ 给你…

适用于 Mac 电脑的 10 款最佳数据恢复工具集

无论是个人照片还是重要的商业文档&#xff0c;对于那些依赖计算机获取重要文件的人来说&#xff0c;数据丢失都是一场噩梦。 值得庆幸的是&#xff0c;Mac用户可以使用各种数据恢复工具&#xff0c;可以帮助您恢复丢失或意外删除的文件。 在本文中&#xff0c;我们将采用适用于…

input框输入中文时,输入未完成触发事件。Vue中文输入法不触发input事件?

前言 在做搜索输入框时&#xff0c;产品期待实时搜索&#xff0c;就是边输入边搜索&#xff0c;然而对于中文输入法出现的效果&#xff0c;不同的产品可能有不同的意见&#xff0c;有的觉得输入未完成也应该触发搜索。但有的却认为应该在中文输入完成后再触发搜索。我发现在vu…

Docker Swarm 集群搭建

Docker Swarm Mode Docker Swarm 集群搭建 Docker Swarm 节点维护 Docker Service 创建 1.准备主机 搭建一个 docker swarm 集群&#xff0c;包含 5 个 swarm 节点。这 5 个 swarm 节点的 IP 与暂 时的角色分配如下&#xff08;注意&#xff0c;搭建完成后会切换角色&#xff…

23年上半年上午题复习

敏捷方法 耦合 软件维护 消息 面向对象测试 面向对象设计原则 包图 原型模式 数据库三级模型 数据库函数依赖 哈夫曼树 左0右1 折半查找 画一个折半查找树&#xff0c;这个树只会往一个方向查找&#xff0c;一个节点不会同时出现左右子树&#xff0c;较小的作为左子树&#…

通义大模型使用指南之通义千问

一、注册 我们可以打开以下网站&#xff0c;用手机号注册一个账号即可。 通义大模型 (aliyun.com) 二、使用介绍 如图&#xff0c;我们可以看到有三个大项功能&#xff0c;通义千问、通义万相、通义听悟。下来我们体验一下通义千问的功能。 1、通义千问 通义千问主要有两个功能…

如何使用VSCode将iPad Pro转化为功能强大的开发工具?

文章目录 前言1. 本地环境配置2. 内网穿透2.1 安装cpolar内网穿透(支持一键自动安装脚本)2.2 创建HTTP隧道 3. 测试远程访问4. 配置固定二级子域名4.1 保留二级子域名4.2 配置二级子域名 5. 测试使用固定二级子域名远程访问6. iPad通过软件远程vscode6.1 创建TCP隧道 7. ipad远…

# 开发趋势 Java Lambda 表达式 第三篇

开发趋势 Java Lambda 表达式 第三篇 一&#xff0c;Lambda 整合集合常规操作 List Java Lambda 表达式可以与List集合和常规操作进行整合&#xff0c;以提供一种更简洁、更可读的代码编写方式。以下是几个示例&#xff1a; 集合遍历操作&#xff1a; List<String> n…