【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…...
Lombok 的 @Data 注解失效,未生成 getter/setter 方法引发的HTTP 406 错误
HTTP 状态码 406 (Not Acceptable) 和 500 (Internal Server Error) 是两类完全不同的错误,它们的含义、原因和解决方法都有显著区别。以下是详细对比: 1. HTTP 406 (Not Acceptable) 含义: 客户端请求的内容类型与服务器支持的内容类型不匹…...
Unity3D中Gfx.WaitForPresent优化方案
前言 在Unity中,Gfx.WaitForPresent占用CPU过高通常表示主线程在等待GPU完成渲染(即CPU被阻塞),这表明存在GPU瓶颈或垂直同步/帧率设置问题。以下是系统的优化方案: 对惹,这里有一个游戏开发交流小组&…...
UE5 学习系列(三)创建和移动物体
这篇博客是该系列的第三篇,是在之前两篇博客的基础上展开,主要介绍如何在操作界面中创建和拖动物体,这篇博客跟随的视频链接如下: B 站视频:s03-创建和移动物体 如果你不打算开之前的博客并且对UE5 比较熟的话按照以…...
Go 语言接口详解
Go 语言接口详解 核心概念 接口定义 在 Go 语言中,接口是一种抽象类型,它定义了一组方法的集合: // 定义接口 type Shape interface {Area() float64Perimeter() float64 } 接口实现 Go 接口的实现是隐式的: // 矩形结构体…...
抖音增长新引擎:品融电商,一站式全案代运营领跑者
抖音增长新引擎:品融电商,一站式全案代运营领跑者 在抖音这个日活超7亿的流量汪洋中,品牌如何破浪前行?自建团队成本高、效果难控;碎片化运营又难成合力——这正是许多企业面临的增长困局。品融电商以「抖音全案代运营…...
在Ubuntu中设置开机自动运行(sudo)指令的指南
在Ubuntu系统中,有时需要在系统启动时自动执行某些命令,特别是需要 sudo权限的指令。为了实现这一功能,可以使用多种方法,包括编写Systemd服务、配置 rc.local文件或使用 cron任务计划。本文将详细介绍这些方法,并提供…...
vue3 定时器-定义全局方法 vue+ts
1.创建ts文件 路径:src/utils/timer.ts 完整代码: import { onUnmounted } from vuetype TimerCallback (...args: any[]) > voidexport function useGlobalTimer() {const timers: Map<number, NodeJS.Timeout> new Map()// 创建定时器con…...
mac:大模型系列测试
0 MAC 前几天经过学生优惠以及国补17K入手了mac studio,然后这两天亲自测试其模型行运用能力如何,是否支持微调、推理速度等能力。下面进入正文。 1 mac 与 unsloth 按照下面的进行安装以及测试,是可以跑通文章里面的代码。训练速度也是很快的。 注意…...
DAY 26 函数专题1
函数定义与参数知识点回顾:1. 函数的定义2. 变量作用域:局部变量和全局变量3. 函数的参数类型:位置参数、默认参数、不定参数4. 传递参数的手段:关键词参数5 题目1:计算圆的面积 任务: 编写一…...
OCR MLLM Evaluation
为什么需要评测体系?——背景与矛盾 能干的事: 看清楚发票、身份证上的字(准确率>90%),速度飞快(眨眼间完成)。干不了的事: 碰到复杂表格(合并单元…...
