【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…...
C++初阶-list的底层
目录 1.std::list实现的所有代码 2.list的简单介绍 2.1实现list的类 2.2_list_iterator的实现 2.2.1_list_iterator实现的原因和好处 2.2.2_list_iterator实现 2.3_list_node的实现 2.3.1. 避免递归的模板依赖 2.3.2. 内存布局一致性 2.3.3. 类型安全的替代方案 2.3.…...
脑机新手指南(八):OpenBCI_GUI:从环境搭建到数据可视化(下)
一、数据处理与分析实战 (一)实时滤波与参数调整 基础滤波操作 60Hz 工频滤波:勾选界面右侧 “60Hz” 复选框,可有效抑制电网干扰(适用于北美地区,欧洲用户可调整为 50Hz)。 平滑处理&…...
【力扣数据库知识手册笔记】索引
索引 索引的优缺点 优点1. 通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。2. 可以加快数据的检索速度(创建索引的主要原因)。3. 可以加速表和表之间的连接,实现数据的参考完整性。4. 可以在查询过程中,…...
FFmpeg 低延迟同屏方案
引言 在实时互动需求激增的当下,无论是在线教育中的师生同屏演示、远程办公的屏幕共享协作,还是游戏直播的画面实时传输,低延迟同屏已成为保障用户体验的核心指标。FFmpeg 作为一款功能强大的多媒体框架,凭借其灵活的编解码、数据…...
深入理解JavaScript设计模式之单例模式
目录 什么是单例模式为什么需要单例模式常见应用场景包括 单例模式实现透明单例模式实现不透明单例模式用代理实现单例模式javaScript中的单例模式使用命名空间使用闭包封装私有变量 惰性单例通用的惰性单例 结语 什么是单例模式 单例模式(Singleton Pattern&#…...
srs linux
下载编译运行 git clone https:///ossrs/srs.git ./configure --h265on make 编译完成后即可启动SRS # 启动 ./objs/srs -c conf/srs.conf # 查看日志 tail -n 30 -f ./objs/srs.log 开放端口 默认RTMP接收推流端口是1935,SRS管理页面端口是8080,可…...
ServerTrust 并非唯一
NSURLAuthenticationMethodServerTrust 只是 authenticationMethod 的冰山一角 要理解 NSURLAuthenticationMethodServerTrust, 首先要明白它只是 authenticationMethod 的选项之一, 并非唯一 1 先厘清概念 点说明authenticationMethodURLAuthenticationChallenge.protectionS…...
C++ 求圆面积的程序(Program to find area of a circle)
给定半径r,求圆的面积。圆的面积应精确到小数点后5位。 例子: 输入:r 5 输出:78.53982 解释:由于面积 PI * r * r 3.14159265358979323846 * 5 * 5 78.53982,因为我们只保留小数点后 5 位数字。 输…...
全面解析各类VPN技术:GRE、IPsec、L2TP、SSL与MPLS VPN对比
目录 引言 VPN技术概述 GRE VPN 3.1 GRE封装结构 3.2 GRE的应用场景 GRE over IPsec 4.1 GRE over IPsec封装结构 4.2 为什么使用GRE over IPsec? IPsec VPN 5.1 IPsec传输模式(Transport Mode) 5.2 IPsec隧道模式(Tunne…...
python报错No module named ‘tensorflow.keras‘
是由于不同版本的tensorflow下的keras所在的路径不同,结合所安装的tensorflow的目录结构修改from语句即可。 原语句: from tensorflow.keras.layers import Conv1D, MaxPooling1D, LSTM, Dense 修改后: from tensorflow.python.keras.lay…...
