秒懂C++之多态

devtools/2024/9/20 1:23:25/ 标签: c++, 开发语言

fe594ea5bf754ddbb223a54d8fb1e7bc.gif

目录

一. 多态的概念

二. 多态的定义及实现

多态的构成条件

虚函数重写的例外

协变(基类与派生类虚函数返回值类型不同)

析构函数的重写(基类与派生类析构函数的名字不同)

练习例题

final

override

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

三. 抽象类

四. 多态的原理

虚函数表

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


一. 多态的概念

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

就比如拿我们的支付宝红包举例子,老用户往往会扫出很小的红包,而新用户却往往能扫到大额红包~

二. 多态的定义及实现

多态的构成条件

多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如Student继承了 Person。Person对象买票全价,Student对象买票半价。
那么在继承中要构成多态还有两个条件
  • 必须通过父类的指针或者引用调用虚函数
  • 被调用的函数必须是虚函数(加上virtual),且派生类必须对基类的虚函数进行重写(函数名相同,参数相同,返回值相同)
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 ps;Student st;Func(ps);Func(st);return 0;
}

正常情况下调用函数是通过其指针或引用类型然后去调该类型的类成员函数~而在多态中是看父类指针或引用的对象,通过对象去调用该对象的类成员函数~

虚函数重写的例外

协变(基类与派生类虚函数返回值类型不同)

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

重写是必须要求要相同返回值的,但也可以有例外,就是返回值可以不同,但是两个返回值必须得是指针或引用,同时得保证这两个返回值构成继承关系。即父类虚函数返回父类对象的指针或者引用,子类虚函数返回子类对象的指针或者引用时称为协变~

ps:不常出现,了解即可~

析构函数的重写(基类与派生类析构函数的名字不同)

class Person {
public:~Person() { cout << "~Person()" << endl;}
};// 重写实现
class Student : public Person {
public:~Student(){ // delete _ptr;cout << "~Student()" << endl;}
};int main()
{Person* p1 = new Person;delete p1;Person* p2 = new Student;delete p2;return 0;
}

如果我们没有用虚函数,那么在代码执行中p1去调用父类的析构函数,p2也会去调用父类的析构函数~

可是这样忽略了一个问题:如果我们new出来的子类当中有额外的资源呢?本来子类的析构函数可以去清理这个额外资源,但由于正常调用而去调用父类的析构导致内存泄漏~

所以为了能让析构函数也形成多态的效果,我们选择使用虚函数进行重写~

而且在同名函数这块也进行了特殊处理,一律把析构的函数名当作destructor来看~使重写的条件名字,最后在父类指针的调用下根据指向对象来调用对象的成员函数~

练习例题

ps小知识点:
  • 如果父类的virtual去掉,那就是与子类构成隐藏关系了,就不再是虚函数无法构成重写,也就没有多态只能是普通的调用(看类型)。
  • 如果子类的virtual去掉,那仍是虚函数,仍符合多态并且为多态调用(看对象)
// 以下程序输出结果是什么()A: A->0 B: B->1 C: A->1 D: B->0 E: 编译出错 F: 以上都不正确
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;
}
本题答案选B~ 接下来我来为大家分析解答~
首先我们发现子类func没有virtual而父类有,那么这也是能构成虚函数重写的~
然后我们可以得出在父类中的隐藏this指针类型是父类的,而父类指针去调用func函数是满足多态条件的,那么就要去找到所调用的对象而非类型,而test函数是被指向B对象的指针所调用的~那么说明多态作用下func调用的是B类的成员函数func~
又因为B类中的func函数中virtual是没有的,那么它只能去用父类的函数声明然后再结合自己的定义作组合,所以在这个过程中子类的func函数是用了父类func中的val再加上实现的B->最终形成答案B~
如果是普通对象调用或者非父类指针去调用那也就没那么多事了~

final

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

如果我们想要让子类无法继承父类有两种方法:

法一:让父类构造私有

class A 
{
public:
protected:int _a;
private:A(){}
};class B : public A
{};int main()
{B bb;return 0;
}

这样子类就无法实例化出对象了,因为子类构造必须调用父类构造~

法二:使用final

class A final
{
public:
protected:int _a;
private:A(){}
};class B : public A
{};int main()
{B bb;return 0;
}

override

override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。

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

三. 抽象类

在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口
类),抽象类不能实例化出对象
class Car
{
public:virtual void Drive() = 0;
};
class Benz :public Car
{
public:virtual void Drive(){cout << "Benz-舒适" << endl;}
};
class BMW :public Car
{
public:virtual void Drive(){cout << "BMW-操控" << endl;}
};
int main()
{Car c;//无法实例化出对象Car* pBenz = new Benz;pBenz->Drive();Car* pBMW = new BMW;pBMW->Drive();return 0;
}

子类只有进行虚函数重写避开去调用父类的纯虚函数就可以实例化了~从另一种角度上看抽象类也有强迫子类进行多态调用的提醒作用~

四. 多态的原理

虚函数表

//sizeof(Base)是多少?
class Base
{
public:virtual void Func1(){cout << "Func1()" << endl;}
private:int _b = 1;char _ch;
};int main()
{cout << sizeof(Base) << endl;Base  bb;return 0;
}

很多人会说是8,但实际上却为12。因为在有了虚函数后对象里面还会多出一个指针~

该指针内放置虚函数Func1的地址~
我们再来修改一下代码~
class Base
{
public:void Func1(){cout << "Base::Func1()" << endl;}virtual void Func2(){cout << "Base::Func2()" << endl;}void Func3(){cout << "Base::Func3()" << endl;}
private:int _b = 1;char _ch = 'a';
};class Derive : public Base
{
public:virtual void Func1(){cout << "Derive::Func1()" << endl;}
private:int _d = 2;
};int main()
{cout << sizeof(Base) << endl;Base  bb;cout << sizeof(Derive) << endl;Derive dd;return 0;
}

我们在父类多添加了一个虚函数和普通函数,在子类添加了一个虚函数

不出我们意外,都多出了一个指针~
其实虚函数建立的时候在内存中会有虚函数表这种对象~而虚函数表也就是函数指针的数组~
当有多个虚函数的时候就会出现一个指针(_vfptr)去指向虚函数数组(虚函数表),然后访问数组里面各个虚函数的地址~
子类中是没有func2函数的,那么就会去拿父类中func2的地址,而子类是有自己的func1函数的,那么在虚函数表中就会让子类的func1地址去覆盖父类的func1地址~
ps:虚函数表中先声明的放在前面~

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

我们再来对虚函数表做一些补充~
  • 首先如果是普通调用,那么函数地址是在编译时就确定的。在编译链接的时候从符号表中找到函数地址,然后去调用它。
  • 而在多态调用中是通过指针进入对应对象虚函数表,在里面找到虚函数地址并成功调用它~

 

下面我们来了解一些多继承关系中的虚函数表~
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(*VF_PTR)();
void PrintVFT(VF_PTR* vft)
{for (size_t i = 0; vft[i] != nullptr; i++){printf("[%d]:%p->", i, vft[i]);VF_PTR f = vft[i];f();//(*f)();}cout << endl << endl;
}int main()
{Derive d;cout << sizeof(d) << endl;Base1* ptr1 = &d;Base2* ptr2 = &d;PrintVFT((VF_PTR*)(*(int*)ptr1));PrintVFT((VF_PTR*)(*(int*)ptr2));return 0;
}

由于vs的监视窗口无法查看2个以上的虚函数,所以我们这里选择人工打印出所属对象的虚函数表里面的内容~

  • 首先我们需要取得类对象中的前4个字节,因为那里代表指向虚函数表的指针~所以我们对切片过的指针ptr1进行强转为int*类型再解引用就可以拿到其指针地址了~
  • 然后我们再通过对指针强转为(VF_PTR*类型)因为我们的打印函数中打印的是函数指针,所以需要实参与形参类型相同以便接收~
  • 最后我们再把得到的函数指针去回调func函数形成多态的效果~

在多继承中有两张虚函数表,一张代表Base1,一张代表Base2~
而在Base1表中原有的虚函数会被子类独有的虚函数覆盖,例如子类只复写了func1与func3虚函数,那么在Base1中就会被子类的func1与func3虚函数覆盖~
而在Base2表中同样如此,不过需要注意的是func3最后只放在了先声明的那个父类中,Base2是不放func3的~
总结:对于func3这种在父类没有的虚函数,一般会放在最先声明的类中~至于其他的该拷贝拷贝,该替换替换~
最后是其他知识点的总结:
  1. inline函数可以是虚函数吗?答:可以,不过编译器就忽略inline属性,这个函数就不再是 inline,因为虚函数要放到虚表中去。
  2. 静态成员可以是虚函数吗?答:不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。
  3. 构造函数可以是虚函数吗?答:不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。
  4. 对象访问普通函数快还是虚函数更快?答:首先如果是普通对象,是一样快的。如果是指针 对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函 数表中去查找。
  5. 虚函数表是在什么阶段生成的,存在哪的?答:虚函数表是在编译阶段就生成的,一般情况 下存在代码段(常量区)的。          

http://www.ppmy.cn/devtools/92213.html

相关文章

haproxy基础

目录 1 HAProxy介绍 1.1 版本对比 1.2 HAProxy功能 2 参数介绍与实践 2.1 global参数说明 2.2 真实代码格式实例 2.3 常用全局参数 2.3.1 nbproc -- 开启几个进程 2.3.2 cpu-map(CUP绑定) 2.3.3 nbthread 2 --开启2个线程 3 Proxies配置 3.1 Proxies配置-defaults 3.2 Proxi…

C语言项目——贪吃蛇,为什么用curses,定义上下左右

在Linux系统中&#xff0c;使用ncurses在程序编译时还要加上 -lcurses 即&#xff1a;gcc cursedemo.c -lcurses #include<curses.h> int main() {initscr(); //ncurse界面的初始化函数printw("This is a curses window.\n");//再ncurse模式下的printfgetc…

solidity 以太坊(Ether) 单位(很基础)

一个字面常数可以带一个后缀 wei&#xff0c; gwei 或 ether 来指定一个以太坊的数量&#xff0c; 其中没有后缀的以太数字被认为单位是wei。 在以太坊和许多其他基于以太坊的区块链系统中&#xff0c;以太币&#xff08;Ether&#xff09;是网络中的主要加密货币。 以太可以被…

Stable Diffusion绘画 | 图生图-涂鸦重绘

涂鸦重绘的整体参数配置&#xff0c;与局部重绘基本一致&#xff0c;仅多了一个蒙版透明度的参数。 都是对局部区域进行重新绘制&#xff0c;但它不仅能识别蒙版的区域&#xff0c;同时还能识别画笔颜色。 例如&#xff0c;对图片中的人物脸部&#xff0c;使用蓝色的画笔&…

JDK8优化JVM总结

JDK8优化JVM总结 默认垃圾回收器&#xff1a; 当前大多数应用仍然使用JDK 1.8&#xff0c;并且默认的年轻代垃圾回收器是Parallel Scavenge。Parallel Scavenge回收器旨在最大化吞吐量&#xff0c;适合对CPU使用率有较高要求的应用场景。 AdaptiveSizePolicy&#xff1a; 默认…

如何在linux系统上部署Redis

<1>简介 Redis 全称 Remote Dictionary Server&#xff08;远程字典服务器&#xff09;&#xff0c;是一个高性能的(key/value)分布式内存数据库&#xff0c;基于内存运行并支持持久化的NoSQL数据库&#xff0c;是当前最热门的NoSql数据库之一,也被人们称为数据结构服务…

SpringBoot依赖之H2 Database(一)

基本概念介绍 H2 Database 依赖名称: H2 Database 功能描述: Provides a fast in-memory database that supports JDBC API and R2DBC access, with a small (2mb) footprint. Supports embedded and server modes as well as a browser based console application. 提供支持…

ptrade排坑笔记——get_price函数在早上8点10分获取不了上一交易日的数据

前言 今天想要和大家分享的是使用get_price函数&#xff0c;但是却没有办法获取上一个交易日的一个交易数据&#xff0c;发生时间点是在早上的8点10分&#xff01; 一、问题描述 就和前言中的一样&#xff0c;在8点10分的时候&#xff0c;使用get_price函数&#xff0c;但是…

Spring配置

1.Spring的两大核心思想IOC和AOP思想 1.1类注解 1.Controller, Service, Configuration, Component, Repository 1.2方法注解 bean&#xff08;这个方法搭配上面的五大注解进行使用&#xff09; 2.Bean的名称 2.1.类注解名称 &#xff08;1&#xff09;默认首字母小写驼…

演示:基于WPF的DrawingVisual开发GS(2019)1822号矢量中国地图一

一、目的&#xff1a;基于WPF的DrawingVisual开发的矢量地图 二、预览 默认样式 深黑样式 深蓝色样式 深蓝色透明样式 演示&#xff1a;基于WPF的DrawingVisual开发GS(2019)1822号矢量中国地图二-CSDN博客VS2022&#xff0c;net7演示&#xff1a;基于WPF的DrawingVisual开发GS…

Codeforces Round 964 (Div. 4) (A~G1)

文章目录 题目链接写在前面A- AB Again?思路code B- Card Game思路code C- Showering思路code D- Slavics Exam思路 E- Triple Operations思路code F- Expected Median思路code G1- Ruler (easy version)思路code 题目链接 点击这里 写在前面 昨天晚上打的这场cf打的跟坨⑩…

C语言经典编程题——基础版1.13

【程序14】 题目&#xff1a;输入某年某月某日&#xff0c;判断这一天是这一年地第几天&#xff1f; #include<stdio.h> int main(){int year,m1,d1,num0;scanf("%d,%d,%d",&year,&m1,&d1);int m1,d1;while(m<m1||d<d1){if(m1||m3||m5||m7…

ASP.Net Core设置接口根路径的方法

使用asp.net core开发微服务项目&#xff0c;需要给每个服务设置不同的根路径&#xff0c;这样既能使用网关转发请求&#xff0c;又方便对单个服务进行测试&#xff0c;保证请求路径的统一。 设置方法需要使用中间件&#xff0c;在Program.cs添加如下代码 app.UsePathBase(&qu…

Using Embeddings API in Azure OpenAI

题意&#xff1a;当我在 Azure OpenAI 中使用嵌入功能时&#xff0c;我遇到了 404 错误&#xff08;资源未找到&#xff09; 问题背景&#xff1a; When I use embeddings with Azure OpenAI I am getting 404 (resource not found): 当我在 Azure OpenAI 中使用嵌入功能时&a…

kafka下载|安装

1、下载kafka https://kafka.apache.org/downloads 2、安装kafka 解压下载的kafka安装包即可 tar -xvf kafka_2.13-3.7.0.tgz -C /usr/local/3、查看kafka目录 bin目录&#xff1a;存放了脚本 config目录&#xff1a;主要存放了配置文件

Java语言程序设计——篇十三(1)

&#x1f33f;&#x1f33f;&#x1f33f;跟随博主脚步&#xff0c;从这里开始→博主主页&#x1f33f;&#x1f33f;&#x1f33f; 欢迎大家&#xff1a;这里是我的学习笔记、总结知识的地方&#xff0c;喜欢的话请三连&#xff0c;有问题可以私信&#x1f333;&#x1f333;&…

Docker快速入门指南

&#x1f6e0;️ Docker 应用场景 Docker 是一个开源的平台&#xff0c;旨在简化应用程序的开发、部署和管理。它通过容器技术&#xff0c;将应用及其所有依赖打包在一个标准化的环境中&#xff0c;从而确保应用在不同环境中的一致性和可移植性。在 Python 爬虫的场景中&#…

高职院校云计算人才培养成果导向系统构建、实施要点与评量方法

一、引言 随着“十四五”规划的深入实施&#xff0c;云计算作为新一代信息技术的关键组成部分&#xff0c;已成为推动各行业数字化转型的重要驱动力。高职院校作为技术技能人才培养的重要阵地&#xff0c;如何根据云计算产业的发展需求&#xff0c;培养具备云计算技术应用与运…

面试笔记--(正在整理版)

面试常见: Jvm&#xff0c;高并发&#xff0c;多线程&#xff0c;数据库&#xff0c;redis&#xff0c;框架 多线程 1.线程和进程是什么?如何保证线程安全性? 进程线程&#xff08;一&#xff09;——基础知识&#xff0c;什么是进程&#xff1f;什么是线程&#xff1f;_…

【视频监控国标GB/T28181】 如何支持TCP和UDP接入

视频监控国标GB/T28181支持TCP和UDP接入的方式主要依赖于该标准中定义的通信协议和传输机制。以下是对这两种接入方式的详细解释&#xff1a; 一、TCP接入方式 1.1 TCP接入的特点 可靠性&#xff1a;TCP协议提供面向连接的、可靠的字节流服务。在数据传输过程中&#xff0c;…