【C++】多态学习
多态
- 多态的概念与定义
- 多态的概念
- 构成多态的两个条件
- 虚函数与重写
- 重写的两个特例
- final 和 override
- 重载、重写(覆盖)、重定义(隐藏)的对比
- 抽象类
- 多态的原理
- 静态绑定与动态绑定
- 单继承与多继承关系下的虚函数表(派生类)
- 单继承中的虚函数表查看
- 多继承中的虚函数表查看
- 菱形继承与菱形虚拟继承
- 菱形继承
- 菱形虚拟继承
- 继承与多态一些常见问题
多态的概念与定义
多态的概念
多态就是多种形态,简单理解就是不同的对象去执行某个行为时会产生出不同的状态表现。
多态表现在继承关系中,继承关系的类对象去调用同一函数,会产生不同的状态行为表现。
例如,在买票体系中,普通人(Person)买票是全价,学生(Student)买票是半价。
构成多态的两个条件
- 被调用的函数必须是虚函数,且派生类必须对基类的虚函数重写。
- 必须是通过基类的指针或者引用调用虚函数。
虚函数与重写
虚函数:被virtual
关键字修饰的类成员函数。
虚函数的重写:
重写也叫覆盖。重写要满足三同条件,三同条件也是建立在虚函数的基础上。
三同条件要求派生类虚函数与基类虚函数的返回值类型、函数名、参数列表完全相同。
class Person
{
public:virtual void BuyTicket(){cout << "买票-全价" << endl;}
};class Student : public Person
{
public:/* * 注意:子类虚函数不加virtual,依旧构成重写* 因为继承后基类的虚函数被继承下来在派生类依旧保持虚函数属性* 但实际最好加上virtual,否则写法不是很规范*/void BuyTicket(){cout << "买票-半价" << endl;}
};void Func(Person& p)
{p.BuyTicket();
}
void Test1()
{Person p;Student st;Func(p);Func(st);
}
要实现多态,那多态的两个条件必须严格遵守,任何一个条件不符合规则,或任何一个条件下的小条件不满足,都无法成功实现多态。
重写的两个特例
- 协变
派生类重写基类虚函数时,基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用。
class A
{};class B : public A
{};class Person
{
public://virtual Person* BuyTicket()virtual A* BuyTicket(){cout << "买票-全价" << endl;//return this;return nullptr;}
};class Student : public Person
{
public:// 重写的协变:返回值可以不同,要求必须是父子关系的指针或者引用// 这里满足父子关系即可,不一定非要某类父子关系virtual B* BuyTicket(){cout << "买票-半价" << endl;//return this;return nullptr;}
};void Func(Person& p)
{p.BuyTicket();
}
void Test2()
{Person p;Student st;Func(p);Func(st);
}
- 析构函数的重写
一眼看去,基类与派生类中析构函数的重写似乎不满足三同中的函数名相同,其实不然,可以理解为编译器对析构函数的名称做了特殊处理,在程序编译后析构函数的名称统一处理成了destructor
。
所以,只要基类的析构函数是虚函数,此时派生类的析构函数只要定义,都与基类的析构函数构成重写。
而且一般建议,将继承体系中析构函数定义成虚函数。下面的例子可以帮助参考。
class Person
{
public://~Person()virtual ~Person(){cout << "~Person()" << endl;}
};class Student : public Person
{
public://~Student()virtual ~Student(){cout << "~Student()" << endl;}
};void Test3()
{Person* p1 = new Person;delete p1;Person* p2 = new Student;delete p2;
}
final 和 override
- final
修饰虚函数,表示该虚函数不能被重写。
class Car
{
public:virtual void Drive() final{}
};class Benz : public Car
{
public:// 无法实现重写virtual void Drive(){cout << "Benz" << endl;}
};
final
也可以修饰类,表示该类不能被继承。
- override
用于检查派生类虚函数是否重写了基类某个虚函数,如果没有重写会编译报错。
class Car
{
public:virtual void Drive(){}
};class Benz : public Car
{
public:// override 检查子类虚函数是否完成重写virtual void Drive() override{cout << "Benz" << endl;}
};
重载、重写(覆盖)、重定义(隐藏)的对比
两个基类和派生类的同名函数不构成重写,就是构成重定义。
抽象类
虚函数的后面加上=0
,则表示这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(接口类)。
抽象类无法直接实例化出对象。抽象类被派生类继承后,派生类如果不重写纯虚函数,派生类也不能实例化出对象。
纯虚函数规范了派生类必须进行重写,体现了接口继承。
class Car
{
public:virtual void Drive() = 0;
};class Benz : public Car
{
public:virtual void Drive(){cout << "Benz" << endl;}
};void Test4()
{Benz b;b.Drive();
}
多态的原理
class Base
{
public:virtual void Func1(){cout << "Func1()" << endl;}
private:int _b = 0;
};void Test5()
{cout << "sizeof Base: " << sizeof Base << endl;Base b;
}
从上面结果可以看出,b
对象中,除了_b
成员,还多了一个_vfptr
的指针(虚函数表指针,v代表virtual,f代表function)。
一个含有虚函数的类中都至少有一个虚函数表指针,而虚函数的地址被放到虚函数表(简称虚表)中。
将Test5的代码改造一下,进一步观察。
class Base
{
public:virtual void Func1(){cout << "Base::Func1()" << endl;}virtual void Func2(){cout << "Base::Func2()" << endl;}void Func3(){cout << "Base::Func3()" << endl;}
private:int _b = 0;
};class Derive : public Base
{
public:virtual void Func1(){cout << "Derive::Func1()" << endl;}
private:int _d = 0;
};void Test6()
{Base b;Derive d;
}
通过观察,可以知道基类b对象和派生类d对象的虚表是不一样的。因为Func1
完成了重写,所以d对象的虚表中存的是重写的Derive::Func1()
,这也是重写被叫做覆盖的道理,即覆盖就是虚表中虚函数的覆盖。(重写是语法层的叫法,覆盖是原理层的叫法)
其实,派生类虚表是从基类虚表拷贝过来的,如果派生类重写了基类的某个虚函数,就用派生类自己的虚函数覆盖虚表中基类的虚函数,派生类自己新增加的虚函数按其在派生类中的声明次序,依次增加到派生类虚表的最后。
下面再通过之前买票的例子Test1帮助阐述多态的原理。
class Person
{
public:virtual void BuyTicket(){cout << "买票-全价" << endl;}
};class Student : public Person
{
public:virtual void BuyTicket(){cout << "买票-半价" << endl;}
};void Func(Person& p)
{p.BuyTicket();
}
void Test7()
{Person Mike;Student Allen;Func(Mike);Func(Allen);
}
Mike
或Allen
通过Func
传给p
。
当p
指向Mike
时,就是在Mike
的虚表中找到虚函数Person::BuyTicket
。
当p
指向Allen
时,就是在Allen
的虚表中找到虚函数Student::BuyTicket
。
这样就实现了不同对象去执行同一行为时,展现出不同形态的情况。
多态的本质总结:
对象多态成员函数调用时,会到对象的虚表中找到对应的虚函数地址,进行调用。
静态绑定与动态绑定
- 静态绑定又称前期绑定/早绑定。
是指在程序编译期间就确定了程序的行为。也称静态/编译时多态。
像重载,或是普通类成员函数的调用(直接call
函数地址)。
- 动态绑定又称后期绑定/晚绑定。
是指在程序运行过程中,需要根据具体情况确定程序的具体行为。也称动态/运行时多态。
单继承与多继承关系下的虚函数表(派生类)
单继承中的虚函数表查看
class Base
{
public:virtual void func1(){cout << "Base:func1" << endl;}virtual void func2(){cout << "Base:func2" << endl;}private:int _b = 0;
};class Derive : public Base
{
public:virtual void func1(){cout << "Derive::func1" << endl;}virtual void func3(){cout << "Derive::func3" << endl;}virtual void func4(){cout << "Derive::func4" << endl;}private:int _d = 1;
};void Test8()
{Base b;Derive d;
}
Test8测试代码调试时看到的虚表可能不完整,可以通过下面函数对虚表进行打印。
// VFPTR是一个函数指针,指向的函数参数为void,返回值为void
typedef void(*VFPTR)();void PrintVFTable(VFPTR table[])
{for (size_t i = 0; table[i] != nullptr; ++i){printf("vft[%d]:%p\n", i, table[i]);table[i](); // 函数回调}
}
对于PrintVFTable
函数的调用如下。
/*
* 1.先取对象的地址,强转成int*,可以拿到头四个字节的地址
* 2. 在解引用取到的是虚函数表的指针,强转成VFPTR*,就可以进行传递
*/
PrintVFTable((VFPTR*)(*(int*)&b));
cout << endl;
PrintVFTable((VFPTR*)(*(int*)&d));
多继承中的虚函数表查看
class Base1
{
public:virtual void Func1() { cout << "Base1::Func1" << endl; }virtual void Func2() { cout << "Base1::Func2" << endl; }private:int _b1 = 1;
};class Base2
{
public:virtual void Func1() { cout << "Base2::Func1" << endl; }virtual void Func2() { cout << "Base2::Func2" << endl; }private:int _b2 = 2;
};class Derive : public Base1, public Base2
{
public:virtual void Func1() { cout << "Derive::Func1" << endl; }virtual void Func3() { cout << "Derive::Func3" << endl; }private:int _d = 3;
};void Test9()
{cout << "sizeof Derive: " << sizeof Derive << endl;Derive d;
}
对内存的查看:
d对象继承自两个父类,具有两张虚表。
下面通过PrintVFTable
对两张表中的内容进行查看。
// 第一个虚表的查看
PrintVFTable((VFPTR*)(*(int*)&d));
cout << endl;
// 第二个虚表的查看 - 方法一
PrintVFTable((VFPTR*)(*(int*)((char*)&d + sizeof(Base1))));
// 第二个虚表的查看 - 方法二
Base2* pb = &d;
PrintVFTable((VFPTR*)(*(int*)(pb)));
可以看到多继承派生类的未重写的虚函数放在第一个所继承基类部分的虚函数表中。
其实子类有几个父类,如果父类有虚函数,则就会有几张虚表,子类自己的虚函数只会放到第一个父类的虚表后面。
这里深入查看,发现两张虚表中虽然存的都是Derive::Func1
,但调用时所用的地址却是不一样的,这是如何做到的?下面通过查看汇编来看看。
Derive d;Base1* pb1 = &d;
Base2* pb2 = &d;d.Func1(); // 普通函数调用pb1->Func1(); // 多态调用
pb2->Func1(); // 多态调用
通过汇编的查看可以发现,虽然最初的地址不同,但最后都能跳到同一处进行函数调用,即Deriver::Func1
。
菱形继承与菱形虚拟继承
菱形继承
class A
{
public:virtual void func1() { cout << "A::func1" << endl; };
public:int _a;
};
class B : public A
{
public:virtual void func1() { cout << "B::func1" << endl; };virtual void func2() { cout << "B::func2" << endl; };
public:int _b;
};
class C : public A
{
public:virtual void func1() { cout << "C::func1" << endl; };virtual void func2() { cout << "C::func2" << endl; };
public:int _c;
};
class D : public B, public C
{
public:int _d;
};void Test10()
{D d;d.B::_a = 1;d.C::_a = 2;d._b = 3;d._c = 4;d._d = 5;
}
菱形虚拟继承
class A
{
public:virtual void func1() { cout << "A::func1" << endl; };
public:int _a;
};
class B : virtual public A
{
public:virtual void func1() { cout << "B::func1" << endl; };virtual void func2() { cout << "B::func2" << endl; };
public:int _b;
};
class C : virtual public A
{
public:virtual void func1() { cout << "C::func1" << endl; };virtual void func2() { cout << "C::func2" << endl; };
public:int _c;
};
class D : public B, public C
{
public:// 此时D必须对func1进行重写virtual void func1() { cout << "D::func1" << endl; };
public:int _d;
};void Test11()
{D d;d.B::_a = 1;d.C::_a = 2;d._b = 3;d._c = 4;d._d = 5;
}
D必须对func1进行重写,因为B和C都有fun1,虚拟继承为了解决数据冗余和二义性,D的虚表里面只能存放一个,就无法确定存哪一个。
通过内存查看,菱形虚拟继承的关系可以如下表示。
继承与多态一些常见问题
- inline函数可以是虚函数吗?
可以,当一个函数是虚函数,在多态调用中,inline就失效了。 - static函数可以是虚函数吗?
不可以,static成员函数都是在编译时进行地址确定。虚函数是为了实现多态,需要运行时去虚表进行地址确定,static函数是virtual的话没有意义,因为本来就不会去虚表。 - 析构函数可以是虚函数吗?
不可以,对象中的虚表指针都是构造函数初始化列表阶段才进行初始化的,所以构造函数是虚函数是没有意义的。 - 析构函数可以是虚函数吗?
可以,并且建议基类的析构函数定义成虚函数。 - 拷贝构造函数可以是虚函数吗?
不可以,拷贝构造函数也是构造函数。 - 赋值函数可以是虚函数吗?
语法上可以,但是没有什么实际价值。 - 对象访问普通函数快还是虚函数快?
虚函数不构成多态,是一样快;
虚函数构成多态调用,普通函数更快。因为多态调用是运行时去虚函数表中找虚函数地址。 - 虚函数表是什么时候生成的?存在哪的?
虚函数表是编译阶段就生成好的,存在于代码段(常量区)。所以一个类的不同对象共享该类的虚表。
(构造函数初始化列表阶段初始化的是虚函数表指针,对象中存的也是虚函数表指针)
相关文章:

【C++】多态学习
多态 多态的概念与定义多态的概念构成多态的两个条件虚函数与重写重写的两个特例 final 和 override重载、重写(覆盖)、重定义(隐藏)的对比抽象类多态的原理静态绑定与动态绑定 单继承与多继承关系下的虚函数表(派生类)单继承中的虚函数表查看多继承中的虚函数表查看 菱形继承与…...

大数据之Maven
一、Maven的作用 作用一:下载对应的jar包 避免jar包重复下载配置,保证多个工程共用一份jar包。Maven有一个本地仓库,可以通过pom.xml文件来记录jar所在的位置。Maven会自动从远程仓库下载jar包,并且会下载所依赖的其他jar包&…...
自制centos7.9的wsl发行版
自制centos7.9的wsl发行版 参考:https://zhuanlan.zhihu.com/p/482538727 Windows10提供了一个wsl工具用于直接在windows上运行Linux子系统。 CentOS国内镜像下载:https://mirrors.aliyun.com/centos/ 这里选择了7.9.2009版本:https://mirr…...
使用VisualStudio制作上位机(五)
文章目录 使用VisualStudio制作上位机(五)第四部分:GUI界面数据显示使用VisualStudio制作上位机(五) Author:YAL 第四部分:GUI界面数据显示 这一部分,主要实现GUI的界面显示。 上一文已经实现了CAN数据的接收,并将数据更新到数组里。所以在做界面的显示时,只需要在…...

ChatGPT在医疗领域可应用于改善与患者的沟通
注意:本信息仅供参考,发布该内容旨在传递更多信息的目的,并不意味着赞同其观点或证实其说法。 自从ChatGPT在2022年末对公众开放以来,OpenAI的这款生成式AI聊天机器人在医疗领域展示出了巨大潜力。它已经通过了美国医学执照考试&a…...

直播预告|博睿学院第四季即将开讲:博睿数据资深运维团队现身说法!
博睿学院第四季开讲啦!本季博睿学院的课程将于本周四(8月31日)16点正式启动。本季我们邀请到了博睿数据平台支撑中心的四位资深运维专家现身说法,来为我们分享一体化智能可观测平台Bonree ONE的实践干货。 他们,见多识…...

端到端自动驾驶综述
End-to-end Autonomous Driving: Challenges and Frontiers 文章脉路 Introduction 从经典的模块化的方法到端到端方法的一个对比, 讲了各自的优缺点, 模块化的好处是各个模块都有自己明确的优化的目标, 可解释性较强, 且容易debug, 缺点是各个模块优化的目标并不是最终的驾…...
mysql索引、事务、存储引擎
一、索引 索引的概念: 索引是一个排序的列表,在这个列表中存储着索引的值和包含这个值的数据所在行的物理地址(类似于C语言的链表通过指针指向数据记录的内存地址)。使用索引后可以不用扫描全表来定位某行的数据,而是…...
【CMU15445】Fall 2019, Project 2: Hash Table 实验记录
目录 实验准备实验测试 实验准备 官方说明:https://15445.courses.cs.cmu.edu/fall2019/project2/ 实验测试 Task 1: mkdir build cd build make hash_table_page_test ./test/hash_table_page_testTask 2: make hash_table_test ./test…...
PMP证书是不是烂大街了?
大家都知道,PMP证书是项目管理领域的金字招牌。近年来,随着项目管理的重要性日益凸显,越来越多的人开始关注和学习PMP证书。无论是企业招聘还是个人职业发展,PMP证书都成为了一张炙手可热的敲门砖。 那么,PMP证书到底…...

Mac下Docker Desktop安装命令行工具、开启本地远程访问
Mac系统下,为了方便在terminal和idea里使用docker,需要安装docker命令行工具,和开启Docker Desktop本地远程访问。 具体方法是在设置-高级下, 1.将勾选的User调整为System,这样不用手动配置PATH即可使用docker命令 …...

Java实现根据商品ID获取京东商品详情数据,1688商品详情接口,1688API接口封装方法
要通过京东的API获取商品详情数据,您可以使用京东开放平台提供的接口来实现。以下是一种使用Java编程语言实现的示例,展示如何通过京东开放平台API获取商品详情: 首先,确保您已注册成为京东开放平台的开发者,并创建一…...

element-plus指定el-date-picker的弹出框位置
此处记录一下,通过popper-options指定popper出现的位置...

游戏陪玩语音聊天系统3.0商业升级独立版本源码
首发价值29800元的最新商业版游戏陪玩语音聊天系统3.0商业升级独立版本源码 1、增加人气店员轮播 2、优化ui界面丨优化游戏图标展示丨优化分类展示 3、增加动态礼物打赏功能 4、增加礼物墙功能 增加店员满足业绩,才能升级功能 5、增加店员等级不同,…...
TCP/IP网络江湖武艺传承:物理层与通信江湖的幕后(物理层中篇:物理层与现代通信技术)
目录 〇、引言:进入现代通信技术的江湖 一、数字信号与模拟信号:传承与差异...

Nuxt 菜鸟入门学习笔记三:视图
文章目录 入口文件组件 Components页面 Pages布局 Layouts Nuxt 官网地址: https://nuxt.com/ Nuxt 提供多个组件层来实现应用程序的用户界面。 入口文件 App.vue组件 Components页面 Pages布局 Layouts 下面逐一进行介绍。 入口文件 默认情况下,Nu…...

Python Opencv实践 - 霍夫线检测(Hough Lines)
import cv2 as cv import numpy as np import matplotlib.pyplot as plt import randomimg cv.imread("../SampleImages/GreenBoard.jpg") print(img.shape) plt.imshow(img[:,:,::-1])#将图像转为二值图 gray cv.cvtColor(img, cv.COLOR_BGR2GRAY) plt.imshow(gra…...

Weblogic漏洞(四)之 CVE-2018-2894 任意文件上传漏洞
CVE-2018-2894 任意文件上传漏洞 漏洞影响 Weblogic受影响的版本: 10.3.6.012.1.3.012.2.1.212.2.1.3 漏洞环境 此次我们使用的是vnlhub靶场搭建的环境,是vnlhub中的Weblogic漏洞中的CVE-2018-2894靶场,我们 cd 到 CVE-2018-2894&#x…...
C++:string的[ ],at,push_back
1.[ ]运算符和at函数 返回的是string的当前字符串的合法的索引位置的引用,所谓的合法是指小于size的索引 #include <string> #include <iostream>using namespace std;int main() {string str = "hello";cout<<"str:"<<str<…...
C语言(第三十六天)
4. 位操作符:&、|、^ 位操作符有: & //按位与 | //按位或 ^ //按位异或 注:他们的操作数必须是整数。 直接上代码: #include <stdio.h> int main() { int num1 -3; int num2 5; num1 & num2; num1 | num2; nu…...

SpringBoot-17-MyBatis动态SQL标签之常用标签
文章目录 1 代码1.1 实体User.java1.2 接口UserMapper.java1.3 映射UserMapper.xml1.3.1 标签if1.3.2 标签if和where1.3.3 标签choose和when和otherwise1.4 UserController.java2 常用动态SQL标签2.1 标签set2.1.1 UserMapper.java2.1.2 UserMapper.xml2.1.3 UserController.ja…...

springboot 百货中心供应链管理系统小程序
一、前言 随着我国经济迅速发展,人们对手机的需求越来越大,各种手机软件也都在被广泛应用,但是对于手机进行数据信息管理,对于手机的各种软件也是备受用户的喜爱,百货中心供应链管理系统被用户普遍使用,为方…...

.Net框架,除了EF还有很多很多......
文章目录 1. 引言2. Dapper2.1 概述与设计原理2.2 核心功能与代码示例基本查询多映射查询存储过程调用 2.3 性能优化原理2.4 适用场景 3. NHibernate3.1 概述与架构设计3.2 映射配置示例Fluent映射XML映射 3.3 查询示例HQL查询Criteria APILINQ提供程序 3.4 高级特性3.5 适用场…...

大型活动交通拥堵治理的视觉算法应用
大型活动下智慧交通的视觉分析应用 一、背景与挑战 大型活动(如演唱会、马拉松赛事、高考中考等)期间,城市交通面临瞬时人流车流激增、传统摄像头模糊、交通拥堵识别滞后等问题。以演唱会为例,暖城商圈曾因观众集中离场导致周边…...
C#中的CLR属性、依赖属性与附加属性
CLR属性的主要特征 封装性: 隐藏字段的实现细节 提供对字段的受控访问 访问控制: 可单独设置get/set访问器的可见性 可创建只读或只写属性 计算属性: 可以在getter中执行计算逻辑 不需要直接对应一个字段 验证逻辑: 可以…...
根目录0xa0属性对应的Ntfs!_SCB中的FileObject是什么时候被建立的----NTFS源代码分析--重要
根目录0xa0属性对应的Ntfs!_SCB中的FileObject是什么时候被建立的 第一部分: 0: kd> g Breakpoint 9 hit Ntfs!ReadIndexBuffer: f7173886 55 push ebp 0: kd> kc # 00 Ntfs!ReadIndexBuffer 01 Ntfs!FindFirstIndexEntry 02 Ntfs!NtfsUpda…...

wpf在image控件上快速显示内存图像
wpf在image控件上快速显示内存图像https://www.cnblogs.com/haodafeng/p/10431387.html 如果你在寻找能够快速在image控件刷新大图像(比如分辨率3000*3000的图像)的办法,尤其是想把内存中的裸数据(只有图像的数据,不包…...
OCR MLLM Evaluation
为什么需要评测体系?——背景与矛盾 能干的事: 看清楚发票、身份证上的字(准确率>90%),速度飞快(眨眼间完成)。干不了的事: 碰到复杂表格(合并单元…...

2025年- H71-Lc179--39.组合总和(回溯,组合)--Java版
1.题目描述 2.思路 当前的元素可以重复使用。 (1)确定回溯算法函数的参数和返回值(一般是void类型) (2)因为是用递归实现的,所以我们要确定终止条件 (3)单层搜索逻辑 二…...

门静脉高压——表现
一、门静脉高压表现 00:01 1. 门静脉构成 00:13 组成结构:由肠系膜上静脉和脾静脉汇合构成,是肝脏血液供应的主要来源。淤血后果:门静脉淤血会同时导致脾静脉和肠系膜上静脉淤血,引发后续系列症状。 2. 脾大和脾功能亢进 00:46 …...