[C++#28][多态] 两个条件 | 虚函数表 | 抽象类 | override 和 final | 重载 重写 重定义
目录
0.引入
1.虚函数
1. 虚函数的重写/覆盖
2. 特例1:不加 virtual 关键字的重写
3. 特例2:协变(了解)
2.多态的构成和细节
1. C++11 的 override 和 final
1. final 不可重写
2. override 报错检查
⭕2. 重载、覆盖(重写)和隐藏(重定义)的对比
3. 多态的使用
⭕多态的两个条件:
4.多态的原理
1. 虚函数表(vtable)
2. 动态绑定与静态绑定
5. 抽象类
1. 概念
2. 接口继承和实现继承
6. 单继承和多继承中的虚函数表
1. 单继承中的虚函数表
2. 多继承中的虚函数表
3. 菱形继承和菱形虚拟继承
0.引入
通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。如:比如买票这个行为
- 当普通人买票时,是全价买票;
- 学生买票时,是半价买票;
- 军人买票时是优先买票
1.虚函数
- 即被virtual修饰的类成员函数称为虚函数
class Person{public:virtual void BuyTicket() { cout << "买票-全价" << endl; }};
1. 虚函数的重写/覆盖
重写是指在派生类中对基类的虚函数重新实现,需满足以下“三同”条件:
- 函数名相同
- 参数相同
- 返回值相同
满足以上条件时,子类的虚函数重写了基类的虚函数,体现了接口继承与实现继承的关系。
- 虚函数重写是接口继承:即子类提供了基类虚函数的不同实现。
- 普通函数继承是实现继承:即子类直接使用基类中的普通函数。
若子类函数不符合重写的要求,则会形成隐藏关系,而非重写。
2. 特例1:不加 virtual
关键字的重写
在子类中,即便不加 virtual
关键字,虚函数依然构成重写关系。这是因为派生类继承了基类的虚函数属性,但从规范性和可读性考虑,建议显式加上 virtual
关键字。
析构函数的特殊处理:
- 如果基类的析构函数是虚函数,则派生类的析构函数只要定义,无论是否加
virtual
关键字,都会与基类析构函数构成重写。虽然析构函数的名称不同,但编译器会将其统一处理为destructor
。 - 建议:在继承中,将析构函数定义为虚函数,以保证正确的析构行为。
示例:
class Person
{
public:virtual void BuyTicket() { cout << "买票-全价" << endl; }
};class Student : public Person
{
public:virtual void BuyTicket() { cout << "买票-半价" << endl; }
/*注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用*//*void BuyTicket() { cout << "买票-半价" << endl; }*/
};
虽然可以不加 virtual
,但规范上建议加上,以保持代码的可读性和一致性。
3. 特例2:协变(了解)
协变指的是当派生类重写基类的虚函数时,返回值类型可以不同,但要求遵守以下规则:
- 基类的虚函数返回基类对象的指针或引用。
- 派生类的虚函数返回派生类对象的指针或引用。
这种情况下,尽管返回值类型不同,依然构成重写。
示例:
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; }
};
这种形式允许返回值类型在派生类中进行“协变”,即派生类返回派生类类型的指针或引用。
2.多态的构成和细节
构成条件
- 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
- 必须通过基类的指针或者引用调用虚函数
1. C++11 的 override
和 final
1. final
不可重写
- 用途:用于修饰虚函数,表示该虚函数不能再被派生类重写。
- 示例:
class Car
{
public:virtual void Drive() final {}
};
使继承自 Car
的派生类都不能重写 Drive
函数。
2. override
报错检查
- 用途:用于检查派生类的虚函数是否正确地重写了基类中的某个虚函数。如果派生类的虚函数没有重写基类的虚函数(即函数签名不匹配),编译器会报错。
- 示例:
class Car
{
public:virtual void Drive() {}
};class Benz : public Car
{
public:virtual void Drive() override { cout << "Benz" << endl; }
};
如果 Drive
函数的签名与 Car
中的 Drive
函数不一致,编译器会报错。
⭕2. 重载、覆盖(重写)和隐藏(重定义)的对比
- 重载:函数名相同,参数列表不同,通常在同一个类中或在派生类中定义多个同名但参数不同的函数。重载与继承无关,可以在同一个作用域中实现。
- 覆盖(重写):派生类中重写基类的虚函数,要求(三同)函数名、参数列表、返回值类型必须与基类中的虚函数完全一致。覆盖是实现多态的关键。
- 隐藏(重定义):派生类定义了一个与基类中同名但非虚函数的函数。此时基类的函数在派生类中被隐藏,不能通过基类指针或引用调用。隐藏通常发生在非虚函数或静态成员函数中。
3. 多态的使用
代码示例:
#include <iostream>class Person {
public:
virtual void BuyTicket() {std::cout << "买票全价" << std::endl;
}
};class Student : public Person {
public:
void BuyTicket() override {std::cout << "买票半价" << std::endl;
}
};void Func(Person &people) {people.BuyTicket();
}void Test() {Person Mike;Func(Mike);Student Johnson;Func(Johnson);
}int main() {Test();return 0;
}
代码解释
- 定义了基类
Person
,其中有一个虚函数BuyTicket()
,输出 "买票全价"。 - 定义了派生类
Student
,继承自Person
并重写了BuyTicket()
函数,输出 "买票半价"。 - 定义了一个函数
Func()
,接收一个Person
类型的引用作为参数,并调用该参数对象的BuyTicket()
函数。 - 在
Test()
函数中,创建了一个Person
类的对象Mike
和一个Student
类的对象Johnson
,分别调用Func()
并传入对象。
- 观察下图的红色箭头我们看到,p是指向mike对象时,p->BuyTicket在mike 对象指向的类的虚表中找到虚 函数是Person::BuyTicket
- 观察下图的蓝色箭头我们看到,p是指向johnson对象时,p->BuyTicket在johson 对象指向的类的虚表中 找到虚函数是Student::BuyTicket
这样就实现出了不同对象去完成同一行为时,展现出不同的形态
⭕多态的两个条件:
- 虚函数覆盖:基类的虚函数被派生类重写。
- 对象的指针或引用调用虚函数:通过基类的指针或引用来调用派生类中重写的虚函数。
两种调用:
- 多态调用:在程序运行时,通过对象的虚表查找对应的虚函数地址并进行调用。虚表(vtable)是由虚函数指针组成的指针数组,每个对象都有一个指向虚表的指针(vptr)。
- 普通函数调用:在编译期间确定函数的地址,运行时直接调用,效率高于多态调用。
4.多态的原理
1. 虚函数表(vtable)
- 虚函数表:每个含有虚函数的类都有一个虚函数表(vtable),其中存储了该类的所有虚函数的指针。每个对象都有一个指向虚函数表的指针(vptr)。
- 虚函数表的生成:
-
- 派生类的虚表首先拷贝基类的虚表。
- 如果派生类重写了基类的虚函数,虚表中相应的指针会被替换为派生类的函数指针。
- 派生类新增的虚函数会按声明顺序添加到虚表的末尾。
- 虚函数表存储位置:虚表指针存在对象中,而虚表本身存储在代码段(常量区)。
- 示例:
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 = 1;
};class Derive : public Base
{
public:virtual void Func1() override{cout << "Derive::Func1()" << endl;}
private:int _d = 2;
};int main()
{Base b;Derive d;return 0;
}
在上述示例中,Base
类和 Derive
类分别有自己的虚表,其中 Derive
类重写了 Func1
,所以它的虚表中存储的是 Derive::Func1()
的地址,而 Base
类的虚表中则存储 Base::Func1()
的地址。
测试看内存
发现:满足多态以后的函数调用,不是在编译时确定的,是运行 起来以后到对象的中取找的。不满足多态的函数调用时编译时确认好的。
深入理解
在C++中,虚函数是用于实现多态的机制。每个包含虚函数的类都有一个虚函数表(虚表),它是一个包含了所有虚函数地址的数组。这个表存储在类的实例中,而不是函数体中。
- 虚函数和普通函数一样,它们的代码都存在于代码段中。虚表中的每个条目都是一个指向虚函数的指针。当通过基类指针或引用调用虚函数时,编译器会使用这个指针来找到实际的函数地址。
- 对象中存储的是指向虚表的指针,而不是虚表本身。这个指针通常称为“虚表指针”或“vptr”。虚表对象指针是类的一个特殊成员,它指向该类的虚表。
- 在Visual Studio 中,虚表是存储在代码段中的,而不是对象中。这是因为虚表的地址是静态的,对于一个给定的类,它不会随着对象实例的变化而变化。
- 同一个类型的所有对象共享同一个虚表。这意味着对于同一个类的所有实例,它们指向同一个虚表。
- 在Visual Studio中,子类的虚表和父类的虚表不是同一个。这是因为子类可能会添加新的虚函数,或者覆盖父类的虚函数,这些都会导致子类的虚表与父类的虚表不同。
总结:
- 虚函数存在于代码段中。
- 虚表存放在对象中,每个对象有一个指向虚表的指针。
- 虚表中存储的是虚函数的指针。再根据指针来找到地址,获取空间
- 同一个类型的所有对象共享同一个虚表。
- Visual Studio中,子类的虚表和父类的虚表是不同的。(拷贝使用)
- 对象指针-->虚表 ->中存的函数地址 ->调用函数
2. 动态绑定与静态绑定
- 静态绑定(早绑定):在编译期间确定函数调用,主要用于普通函数或函数重载,效率较高。
- 动态绑定(晚绑定):在运行期间根据对象的实际类型决定函数调用,主要用于虚函数,实现了多态,灵活性高。
5. 抽象类
1. 概念
- 抽象类:包含至少一个纯虚函数(即在函数后面写
= 0
的虚函数)的类称为抽象类,不能实例化对象。 - 派生类继承抽象类:派生类必须重写抽象类中的所有纯虚函数,才能实例化对象。如果没有重写纯虚函数,派生类也会成为抽象类。
- 示例:
class Car
{
public:virtual void Drive() = 0; // 纯虚函数
};class Benz : public Car
{
public:virtual void Drive() override{cout << "Benz-舒适" << endl;}
};class BMW : public Car
{
public:virtual void Drive() override{cout << "BMW-操控" << endl;}
};void Test()
{Car *pBenz = new Benz;pBenz->Drive(); // 输出: Benz-舒适Car *pBMW = new BMW;pBMW->Drive(); // 输出: BMW-操控
}
2. 接口继承和实现继承
- 接口继承:虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,主要目的是为了重写虚函数并实现多态。
- 实现继承:普通函数的继承是一种实现继承,派生类继承了基类的函数实现,可以直接使用这些函数。
- 建议:如果不需要实现多态,不要将函数定义为虚函数。
6. 单继承和多继承中的虚函数表
1. 单继承中的虚函数表
派生类的虚表继承自基类,派生类如果重写了基类的虚函数
- 虚表中的相应指针会被派生类的函数指针覆盖。
- 派生类新增的虚函数会添加到虚表的末尾。
class Base
{
public:virtual void func1() { cout << "Base::func1" << endl; }virtual void func2() { cout << "Base::func2" << endl; }
private:int a;
};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 b;
};
可以发现 fun2()地址一样,fun1()实现了重写
实现如下:
2. 多继承中的虚函数表
- 多继承时,派生类会有多个虚表,分别对应继承的多个基类的虚函数。派生类会继承多个基类的虚表,并将未重写的虚函数保留在对应基类的虚表中。
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 (*VFPTR)();
void PrintVTable(VFPTR vTable[])
{cout << " 虚表地址>" << vTable << endl;for (int i = 0; vTable[i] != nullptr; ++i){printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);VFPTR f = vTable[i];f();}cout << endl;
}
int main()
{Derive d;VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);PrintVTable(vTableb1);VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d + sizeof(Base1)));PrintVTable(vTableb2);return 0;
}
多继承原理:继承了两张虚函数表
观察下图可以看出:多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中
在两张虚函数表中都对 fun1 进行了重写
3. 菱形继承和菱形虚拟继承
- 菱形继承:由多重继承引发的继承关系,其中基类被多个派生类继承,而这些派生类又被另一个类继承,形成菱形结构。菱形继承容易引发复杂性和性能问题,因此不推荐使用。
- 菱形虚拟继承:通过虚拟继承解决菱形继承中的重复继承问题,虽然避免了多次继承同一基类的副本,但仍可能引发复杂性和性能问题。
相关文章:

[C++#28][多态] 两个条件 | 虚函数表 | 抽象类 | override 和 final | 重载 重写 重定义
目录 0.引入 1.虚函数 1. 虚函数的重写/覆盖 2. 特例1:不加 virtual 关键字的重写 3. 特例2:协变(了解) 2.多态的构成和细节 1. C11 的 override 和 final 1. final 不可重写 2. override 报错检查 ⭕2. 重载、覆盖&…...
List 集合指定值升序降序排列Comparator实现
升序排序 升序排序通常是指从小到大的排序。对于数值类型来说,可以直接使用 compareTo 方法,而对于其他类型,可以根据实际需求实现比较逻辑。 示例代码 import java.util.Comparator; import java.util.List; import java.util.ArrayList;cl…...

【Day07】
目录 MySQL-DQL- 基本查询 MySQL-DQL- 条件查询 MySQL-DQL- 聚合函数 MySQL-DQL- 分组查询 MySQL-DQL- 排序查询 MySQL-DQL- 分页查询 MySQL-DQL- 案例 MySQL-多表设计-一对多 MySQL-多表设计-一对多-外键约束 MySQL-多表设计-一对一&多对多 MySQL-多表设计-案例…...

shell 控制台显示彩色文字的方法
在shell脚本中,如果我们希望在控制台能显示带颜色的文字, 那就需要使用shell中的色彩专用变量代码来进行. shell中的各种颜色代码定义 # 颜色定义 BLACK"\033[0;30m" DARK_GRAY"\033[1;30m" BLUE"\033[0;34m" LIGHT_BLUE"\033[1;3…...

Nginx: 缓存, 不缓存特定内容和缓存失效降低上游压力策略及其配置示例
概述 在负载均衡的过程中,有一个比较重要的概念,就是缓存利用缓存可以很好协调Nginx在客户端和上游服务器之间的速度不匹配的矛盾从而很好的解决整体系统的响应速度 如果用户需要通过Nginx获取某一些内容的时候,发起一个request请求这个请求…...
Python 全栈系列266 Kafka服务的Docker搭建
说明 在大量数据处理任务下的缓存与分发 这个算是来自顾同学的助攻1,我有点java绝缘体的体质,碰到和java相关的安装部署总会碰到点奇怪的问题,不过现在已经搞定了。测试也接近了kafka官方标称的性能。考虑到网络、消息的大小等因素࿰…...

集合框架,List常用API,栈和队列初识
回顾 集合框架 两个重点——ArrayList和HashSet. Vector/ArraysList/LinkedList区别 VectorArraysListLinkedList底层实现数组数组链表线程安全安全不安全不安全增删效率较低较低高扩容*2*1.5-------- (>>)运算级最低,记得加括号。 常…...

构建全景式智慧文旅生态:EasyCVR视频汇聚平台与AR/VR技术的深度融合实践
在科技日新月异的今天,AR(增强现实)和VR(虚拟现实)技术正以前所未有的速度改变着我们的生活方式和工作模式。而EasyCVR视频汇聚平台,作为一款基于云-边-端一体化架构的视频融合AI智能分析平台,可…...
C++结构体声明时初始化
提示:文章 文章目录 前言一、背景二、 2.1 2.2 总结 前言 前期疑问: 本文目标: 一、背景 最近 二、 2.1 c 结构体默认初始化 在C中,结构体的默认成员初始化可以通过构造函数来完成。如果没有为结构体提供构造函数&#x…...

基于微信的热门景点推荐小程序的设计与实现(论文+源码)_kaic
摘 要 近些年来互联网迅速发展人们生活水平也稳步提升,人们也越来越热衷于旅游来提高生活品质。互联网的应用与发展也使得人们获取旅游信息的方法也更加丰富,以前的景点推荐系统现在已经不足以满足用户的要求了,也不能满足不同用户自身的个…...
9、设计模式
设计模式 1、工厂模式 在工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。工厂模式作为一种创建模式,一般在创建复杂对象时,考虑使用;在创建简单对象时&…...

数学专题.
数论 1.判断质数 定义:在大于1的整数中,如果只包含1和本身这两个约数,就称为质数or素数 Acwing 866.试除法判断质数 2.预处理质数(筛质数) Acwing 868.筛质数 3.质因数分解 Acwing 867.分解质因数 4.阶乘分解 5.因…...

如何提升网站的收录率?
要提升网站的收录率,其中一个特别有效的工具就是GPC爬虫池,这个工具通过深度研究谷歌SEO算法,吸引谷歌爬虫。 GPC爬虫池的基本原理是构建一个庞大的站群系统,并创建复杂的内链和外链结构,以吸引并留住谷歌蜘蛛 使用GP…...
HALCON根据需要创建自定义函数
在HALCON中,根据需要创建自定义函数是扩展其图像处理和分析功能的有效方式。HALCON支持通过其高级编程接口(HDevelop和C/C、C#、Python等)来创建自定义函数。这里将主要讨论在HDevelop环境中如何创建自定义函数,因为HDevelop是HAL…...
力扣SQL仅数据库(196~569)
196. 删除重复的电子邮箱 题目:编写解决方案 删除 所有重复的电子邮件,只保留一个具有最小 id 的唯一电子邮件。 (对于 SQL 用户,请注意你应该编写一个 DELETE 语句而不是 SELECT 语句。) (对于 Pandas …...

网络基础:理解IP地址、默认网关与网段(IP地址是什么,默认网关是什么,网段是什么,IP地址、默认网关与网段)
前言 在计算机网络中,IP地址、默认网关和网段(也称为子网)之间有着密切的关系。它们是网络通信中的至关重要的概念,但它们并不相同。这里来介绍一下它们之间的关系,简单记录一下 一. IP地址 1. 介绍 IP 地址…...

windows安装php7.4
windows安装php7.4 1.通过官网下载所需的php版本 首先从PHP官网(https://www.php.net/downloads.php)或者Windows下的PHP官网(http://windows.php.net/download/)下载Windows版本的PHP安装包。下载后解压到一个路径下。 2.配…...
【代码随想录|图论part03之后】
代码随想录|数组 704. 二分查找,27. 移除元素 一、part031、101. 孤岛的总面积1.1 dfs版本1.2 BFS版本2.102. 沉没孤岛3、103. 水流问题4、104.建造最大岛屿二、part041、110. 字符串接龙2、105.有向图的完全可达性3、106. 岛屿的周长三、part05-06 并查集理论1、107. 寻找存在…...
【项目一】基于pytest的自动化测试框架day1
day1不涉及编写代码,只简单梳理接口测试相关的概念。 day1接口测试的本质:功能测试的一部分测试用例的设计与实现接口调试与自动化:从postman到持续集成补充概念 day1 接口测试的本质:功能测试的一部分 接口测试是功能测试的一部…...
如何下载和安装 Notepad++
Notepad 是一款功能强大的开源文本编辑器,广泛用于代码编写和文本编辑。以下是 Notepad 的下载安装教程: 下载 Notepad 访问官方网站 打开你的网络浏览器,访问 Notepad 的官方网站:https://notepad-plus-plus.org/ 选择下载选项…...

未来机器人的大脑:如何用神经网络模拟器实现更智能的决策?
编辑:陈萍萍的公主一点人工一点智能 未来机器人的大脑:如何用神经网络模拟器实现更智能的决策?RWM通过双自回归机制有效解决了复合误差、部分可观测性和随机动力学等关键挑战,在不依赖领域特定归纳偏见的条件下实现了卓越的预测准…...

《从零掌握MIPI CSI-2: 协议精解与FPGA摄像头开发实战》-- CSI-2 协议详细解析 (一)
CSI-2 协议详细解析 (一) 1. CSI-2层定义(CSI-2 Layer Definitions) 分层结构 :CSI-2协议分为6层: 物理层(PHY Layer) : 定义电气特性、时钟机制和传输介质(导线&#…...

Springcloud:Eureka 高可用集群搭建实战(服务注册与发现的底层原理与避坑指南)
引言:为什么 Eureka 依然是存量系统的核心? 尽管 Nacos 等新注册中心崛起,但金融、电力等保守行业仍有大量系统运行在 Eureka 上。理解其高可用设计与自我保护机制,是保障分布式系统稳定的必修课。本文将手把手带你搭建生产级 Eur…...
LLM基础1_语言模型如何处理文本
基于GitHub项目:https://github.com/datawhalechina/llms-from-scratch-cn 工具介绍 tiktoken:OpenAI开发的专业"分词器" torch:Facebook开发的强力计算引擎,相当于超级计算器 理解词嵌入:给词语画"…...
06 Deep learning神经网络编程基础 激活函数 --吴恩达
深度学习激活函数详解 一、核心作用 引入非线性:使神经网络可学习复杂模式控制输出范围:如Sigmoid将输出限制在(0,1)梯度传递:影响反向传播的稳定性二、常见类型及数学表达 Sigmoid σ ( x ) = 1 1 +...

mysql已经安装,但是通过rpm -q 没有找mysql相关的已安装包
文章目录 现象:mysql已经安装,但是通过rpm -q 没有找mysql相关的已安装包遇到 rpm 命令找不到已经安装的 MySQL 包时,可能是因为以下几个原因:1.MySQL 不是通过 RPM 包安装的2.RPM 数据库损坏3.使用了不同的包名或路径4.使用其他包…...
docker 部署发现spring.profiles.active 问题
报错: org.springframework.boot.context.config.InvalidConfigDataPropertyException: Property spring.profiles.active imported from location class path resource [application-test.yml] is invalid in a profile specific resource [origin: class path re…...

HarmonyOS运动开发:如何用mpchart绘制运动配速图表
##鸿蒙核心技术##运动开发##Sensor Service Kit(传感器服务)# 前言 在运动类应用中,运动数据的可视化是提升用户体验的重要环节。通过直观的图表展示运动过程中的关键数据,如配速、距离、卡路里消耗等,用户可以更清晰…...

如何更改默认 Crontab 编辑器 ?
在 Linux 领域中,crontab 是您可能经常遇到的一个术语。这个实用程序在类 unix 操作系统上可用,用于调度在预定义时间和间隔自动执行的任务。这对管理员和高级用户非常有益,允许他们自动执行各种系统任务。 编辑 Crontab 文件通常使用文本编…...
LRU 缓存机制详解与实现(Java版) + 力扣解决
📌 LRU 缓存机制详解与实现(Java版) 一、📖 问题背景 在日常开发中,我们经常会使用 缓存(Cache) 来提升性能。但由于内存有限,缓存不可能无限增长,于是需要策略决定&am…...