文章目录

  • 前言
  • 多态的概念
  • 多态的定义和实现
    • 虚函数
    • 虚函数的重写(覆盖)
  • 多态的构成条件
  • override 和 final(C++11提出)
    • final
    • override
  • 重载、覆盖(重写)、隐藏(重定义)的对比
  • 抽象类
    • 接口继承和实现继承
  • 多态的原理
    • 虚函数表(也叫做虚表)
      • 引申:虚表的打印
    • 多态的原理
    • 静态多态和动态多态
  • 多继承中的虚函数表
  • 作业部分

前言

多态是面向对象编程的三大核心特性(封装、继承、多态)之一,它使得同一接口可以呈现出不同的行为,极大地提升了代码的灵活性和可扩展性。在 C++ 中,多态的实现与虚函数、虚表等机制紧密相关,其底层逻辑涉及编译期与运行期的不同处理方式。
本文将系统梳理 C++ 多态的概念、实现条件、核心机制(虚函数与虚表),并深入解析多态在继承场景下的表现,同时结合典型问题与示例代码,帮助读者全面理解多态的本质与应用。无论是基础的虚函数重写,还是复杂的多继承虚表结构,本文都将逐一剖析,为开发者在实际编程中合理运用多态提供清晰指引。

多态的概念

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

为了更方便和灵活的实现多种形态的调用

多态的定义和实现

虚函数

概念:被virtual修饰的类成员函数称为虚函数(和前面的虚继承区分)

eg:class Person {
public:virtual void text() {};

虚函数的重写(覆盖)

概念:派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写或者覆盖了基类的虚函数。

省流:虚函数+三同

虚构函数重写的两个例外情况:

1.协变:

此时基类与派生类虚函数返回值类型可以不同,但是返回值必须是父子关系的指针和引用

一个虚函数返回值是指针,一个是引用这样也不行
但是一个返回的是父的,一个返回的是子的没关系

2.派生类重写虚函数可以不加virtual(但是建议加上)

总问题: 析构函数可以是虚函数吗?为什么需要是虚函数?

析构函数加virtual,是不是虚函数重写?
是,因为类析构函数都被处理成destructor这个统一的名字

为什么要这么处理呢?

因为要让他们构成重写

那为什么要让他们构成重写呢?

因为下面的场景

(Person是基类,Student是派生类)
Person* p = new Person;p->text();delete p;p = new Student;//注意:这里的p还是Person类的p->text();delete p; // p->destructor() + operator delete(p)// 这里我们期望p->destructor()是一个多态调用,而不是普通调用

多态的构成条件

1.必须通过基类的指针或者引用调用虚函数(注意是基类!!!)

2.被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

注意:多态调用看的是指向的对象,普通的调用看的是当前的类型
eg: class Person{
public:
virtual void text(){}
};class Student : public Person{
public:virtual void text(){}//--a
};void func(const Person& p)
{
p.text();
}main函数里面func(Student());是调用的a的

问题:

1.为什么必须要是父类的指针或引用,而不是父类对象或者子类的指针或引用

(编译器把这几种行为ban了的原因)

原因:

1.不能是父类对象的原因:

不会拷贝子类的虚表和其他特有的,所以这个父类对象根本不知道子类的存在(指针和引用就可以避开这一点)

编译器选择不拷贝子类的虚表指针的原因:

害怕别人不知道父类对象虚表中是父类的还是子类的

2.不能是子类指针或引用:

怕去访问到父类中没有的成员

引申:

1.子类虚表的构建:

子类继承父类时,会先复制一份父类的虚表。如果子类没有重写父类的虚函数,那么虚表中对应函数指针就指向父类虚函数实现;若子类重写了某个虚函数,就会用子类自己的虚函数地址覆盖虚表中从父类继承来的对应函数指针。

2.子类赋值给父类对象切片,不会拷贝虚表,父类还是会要自己的虚表

override 和 final(C++11提出)

final

作用:1.修饰虚函数,表示该虚函数不能再被重写

2.使用final关键字修饰类,直接禁止任何类继承它

eg: class Person final{};
用法:eg:virtual void text() final {}(前有无virtual不重要哈)
引申:一个有final一个无final也能构成重载和隐藏

override

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

class Person{
public:virtual void text(){}
};class Student :public Person {
public:virtual void text() override {}
};

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

在这里插入图片描述

抽象类

概念:

在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。

作用:强制要求派生类重写虚函数,另外抽象类体现出了接口继承的关系

比如:class Car
{
public:
virtual void Drive() = 0;
};

接口继承和实现继承

普通函数的继承是一种实现继承,继承的是函数的实现。

虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。

多态的原理

虚函数表(也叫做虚表)

包含虚函数的类会有虚函数表指针,虚函数表指针指向的是虚函数表的地址

虚函数表里面存了虚函数的指针

引申:函数不符合多态,编译时就确定地址了
符合多态,运行时到指向对象的虚函数表中找调用函数的地址

注意:同一个类的所有实例对象共享同一个虚函数表

比较特殊的是:VS编译器的虚表指向的地址后面会有0作为结束(可以用内存窗口看)

比如:在这里插入图片描述

但是在进行增量编译之后,可能这个0就没了,这时候需要清理一下解决方案或者重新生成解决方案才行

引申:虚表的打印

虚表本质上是函数指针数组

typedef void(*FUNC_PTR) ();
//这里就是将   一个void(*)()的函数指针类型取别名为FUNC_PTR// 打印函数指针数组的方法
void PrintVFT(FUNC_PTR* table)
{for (size_t i = 0; table[i] != nullptr; i++){printf("[%d]:%p->", i, table[i]);FUNC_PTR f = table[i];f();}printf("\n");
}int main()
{Student st;int vft2 = *((int*)&st);
//这个强转之后就一次++指++(int)个字节的东西了;而且这个32位上正好一个指针4个字节,正好读完
//注意:Linux是64位的!!!PrintVFT((FUNC_PTR*)vft2);
//发现隐式类型转换会报错,就改成强转了return 0;
}

注意:成员变量的变化会导致虚表的打印出错–因为可能会影响到内存布局

虚表和虚基表都是在编译阶段生成的

对象实例化之后,才会与虚表有联系(通过虚表指针)

多态的原理

核心的实现机制就是虚函数表和虚指针

满足多态的话,子类的虚指针指向的虚表中的虚函数就会覆盖父类的虚函数的地址,然后调用的就是子类的虚函数了

静态多态和动态多态

静态多态,又叫静态绑定,前期绑定(早绑定),在程序编译期间就确定了程序的行为

比如:函数重载

动态多态又称为动态绑定,后期绑定(晚绑定),是在程序运行期间才确定调用什么函数的

也就是继承+虚函数重写实现的多态

在默认情况下,多态一般指的是动态多态

多继承中的虚函数表

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;
};int main()
{Derive d;cout << sizeof(d) << endl;
//X86环境下,这个占20个字节,组成:两个基类(都是一个虚表指针加一个成员变量)加一个成员变量Base1* ptr1 = &d;ptr1->func1();Base2* ptr2 = &d;ptr2->func1();//通过修正this指针,来让this指针指向派生类的头return 0;
}
问题:为什么重写func1,Base1和Base2的虚表中func1的地址不一样?
Base2中func1的地址不一样是为了jmp去修正this指针的位置

注意:

1.多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中(其实是末尾)

作业部分

设计不想被继承类,如何设计?方法1:基类构造函数私有   (C++98)方法2:基类加一个final  (C++11)方法1:eg:
class A
{
public:static A CreateObj()//这个static不能去掉,不然就不能通过域名去调用了{return A();}
private:A(){}
};//当然,用析构函数这么搞也行哈
int main()
{A::CreateObj();return 0;
}方法2:
class A final
{}
 这里常考一道笔试题:sizeof(Base)是多少?(X86环境下的话)
答案:8个字节//不是一个字节,也不是四个字节
要注意的是:类里面还有一个虚函数表指针(_vfptr)
class Base
{
public:virtual void Func1(){cout << "Func1()" << endl;}
private:char _b = 1;
};int main()
{cout << sizeof(Base) << endl;return 0;
}
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[])//相当于int main()
{B* p = new B;p->test();return 0;
}
结果:输出B->1引申:如果把test()放在了B里面的话,就应该输出B->0了
因为此时this->func()的this不是父类指针,不构成多态

派生类那里不用加virtual的原因:

本质上只重写了实现

面试常考题:

1.什么是多态?–静态多态和动态多态都要答

2.inline函数可以是虚函数吗?答:可以,不过编译器就忽略inline属性,这个函数就不再是

inline,因为虚函数要放到虚表中去。

3.静态成员可以是虚函数吗?答:不能,因为静态成员函数没有this指针,使用类型::成员函数

的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。(语法也会强制检查这个,会报错)

4.构造函数可以是虚函数吗?答:不能,因为对象中的虚函数表指针是在构造函数初始化列表

阶段才初始化的。

5.对象访问普通函数快还是虚函数更快?答:首先如果是普通对象,是一样快的。如果是指针

对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函

数表中去查找。

6.虚函数表是在什么阶段生成的,存在哪的?答:虚函数表是在编译阶段就生成的,一般情况

下存在代码段(常量区)的。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/news/914435.shtml
繁体地址,请注明出处:http://hk.pswp.cn/news/914435.shtml
英文地址,请注明出处:http://en.pswp.cn/news/914435.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Node.js + Express的数据库AB View切换方案设计

方案总览数据导入过程&#xff1a; - 根据控制表判断当前活跃组&#xff08;假设当前活跃的是a&#xff0c;那么接下来要导入到b&#xff09;。 - 清空非活跃表&#xff08;即b表&#xff09;的数据&#xff0c;然后将新数据导入到b表。 - 切换控制表&#xff0c;将活…

C++_编程提升_temaplate模板_案例

类模板案例案例描述: 实现一个通用的数组类&#xff0c;要求如下&#xff1a;可以对内置数据类型以及自定义数据类型的数据进行存储将数组中的数据存储到堆区构造函数中可以传入数组的容量提供对应的拷贝构造函数以及operator防止浅拷贝问题提供尾插法和尾删法对数组中的数据进…

Win11系统安装Anaconda环境极简教程

Win11系统安装Anaconda环境极简教程 &#x1f4e5; 第一步&#xff1a;下载 Anaconda 安装包 打开浏览器&#xff0c;访问 Anaconda 官网&#xff0c;选择View All Installers 选择所需版本&#xff08;此文以2024.02-1为例&#xff09;&#xff0c;点击进行下载&#xff08;…

Datawhale AI夏令营-基于带货视频评论的用户洞察挑战赛

一.赛事目标基于星火大模型Spark 4.0 Ultra&#xff0c;对视频和评论的数据进行商品识别&#xff0c;情感分析&#xff0c;归类分析&#xff0c;最终为带货效果进行评价。并通过优化模型来提高评价准确度二.赛事环境1.基础平台&#xff1a;星火大模型Spark 4.0 Ultra2.赛事数据…

如何基于FFMPEG 实现视频推拉流

文章目录 前言环境准备为什么选择 FFmpeg什么是nginx 1.7.11.3 GryphonNginx的conf配置启动nginx推流命令接收视频流Untiy播放视频流最后前言 我们经常会有在电脑上实现推拉流的需求,Unity 和Unreal 都提供了基于WebRTC 的视频流方案,效果还不错,但是当我们需要推拉整个电脑…

飞算JavaAI:从情绪价值到代码革命,智能合并项目与定制化开发新范式

目录一、飞算 JavaAI 是什么&#xff1f;二、飞算JavaAI&#xff1a;安装登录2.1 IDEA插件市场安装&#xff08;推荐&#xff09;2.2 离线安装包三、飞算JavaAI核心功能&#xff1a;一键生成完整工程代码功能背景3.1 理解需求3.2 设计接口3.3 表结构自动设计3.4 处理逻辑&#…

Python 基础语法与数据类型(十一) - 类 (class) 与对象 (实例)

文章目录1. 什么是类 (Class)&#xff1f;1.1 定义一个类2. 什么是对象 (Object) 或实例 (Instance)&#xff1f;2.1 创建对象&#xff08;实例化&#xff09;3. 访问属性和调用方法4. 类属性 vs 实例属性5. self 的重要性总结练习题练习题答案前几篇文章我们学习了变量、数据类…

精准数据检索+数据飞轮自驱优化,彩讯AI知识库助力企业知识赋能和效率创新

近两年&#xff0c;人工智能技术的精细化发展&#xff0c;让知识库概念重新成为“热门词汇”&#xff0c;腾讯ima等智能工作台产品为个人用户打造专属知识库&#xff0c;而面向B端市场&#xff0c;企业AI知识库也逐步成为企业集中存储与管理核心文档、数据、经验和流程的知识中…

打破空间边界!Nas-Cab用模块化设计重构个人存储逻辑

文章目录前言1. Windows安装Nas-Cab2. 本地局域网连接Nas-Cab3. 安装Cpolar内网穿透4. 固定Nas-Cab 公网地址"数据管理不该受制于硬件形态或地理边界。这个开源方案证明&#xff1a;当功能模块化且可扩展时&#xff0c;私有云可以像水一样渗透进所有设备——现在就去Git仓…

Sigma-Aldrich细胞培养基础知识:细胞培养的安全注意事项

细胞培养实验室风险评估风险评估的主要目的是防止人员受伤&#xff0c;保护财产&#xff0c;并避免对个人和环境的伤害。在许多国家&#xff0c;法律要求进行风险评估。例如&#xff0c;英国的《英国职业健康与安全法案&#xff08;1974年&#xff09;》就是一个例子。欧洲共同…

Imx6ull用网线与电脑连接

理解工作方式没有路由器时&#xff0c;可以使用&#xff0c;只要保持虚拟机的两个网卡一个与电脑在同一网,一个与板子在同一网段(保持通信)就可以从虚拟机往板子下载第一步&#xff1a;查看电脑连接的网络这一步是在找到主机ip地址这两步在其他同类教程里一样的第二步:设置以太…

力扣454.四数相加Ⅱ

给你四个整数数组 nums1、nums2、nums3 和 nums4 &#xff0c;数组长度都是 n &#xff0c;请你计算有多少个元组 (i, j, k, l) 能满足&#xff1a;0 < i, j, k, l < nnums1[i] nums2[j] nums3[k] nums4[l] 0示例 1&#xff1a;输入&#xff1a;nums1 [1,2], nums2 …

Joplin:一款免费开源、功能强大且注重隐私的笔记软件

Joplin 是一款免费开源、功能强大且注重隐私的笔记和待办事项应用程序&#xff0c;它的设计目标是成为 Evernote 等流行笔记应用的强大替代品&#xff0c;尤其适合重视数据所有权和隐私的用户。 功能特性 Joplin 的核心定位与优势如下&#xff1a; 完全开源&#xff1a;代码公…

渗透前四天总结

目录 一.DNS DNS 基本概述 DNS解析过程 二.HTTPS TLS握手过程 RSA加密 对称加密&#xff1a; 非对称加密&#xff1a; RSA加密过程 三.使用xdebug调试php 四.信息收集 一.DNS DNS 基本概述 DNS&#xff1a;域名系统(DomainNameSystem)因特网的一项核心服务&#xf…

Python----NLP自然语言处理(中文分词器--jieba分词器)

一、介绍文本语料在输送给NLP模型前&#xff0c;需要一系列的预处理工作&#xff0c;才能符合模型输入的要求。对于NLP来说&#xff0c;他学习一篇人类书写的文章不是整体的来学习&#xff0c;而是一个词一个词的来学习。所以文本预处理的第一个步骤就是对文本进行分词处理。&a…

深入了解linux系统—— 进程信号的保存

信号 信号&#xff0c;什么是信号&#xff1f; 在现实生活中&#xff0c;闹钟&#xff0c;红绿灯&#xff0c;电话铃声等等&#xff1b;这些都是现实生活中的信号&#xff0c;当闹钟想起时&#xff0c;我就要起床&#xff1b;当电话铃声想起时&#xff0c;我就知道有人给我打电…

Redis 事务错误处理机制与开发应对策略

&#x1f4d8; Redis 事务错误处理机制与开发应对策略一、Redis 事务基础回顾 Redis 中的事务由以下三组命令构成&#xff1a;命令作用说明MULTI开始一个事务&#xff0c;进入命令入队模式命令集所有后续命令不会立即执行&#xff0c;而是入队等待提交EXEC提交事务&#xff0c;…

信息学奥赛一本通 1549:最大数 | 洛谷 P1198 [JSOI2008] 最大数

【题目链接】 ybt 1549&#xff1a;最大数 洛谷 P1198 [JSOI2008] 最大数 【题目考点】 1. 线段树&#xff1a;单点修改 区间查询 知识点讲解见&#xff1a;洛谷 P3374 【模板】树状数组 1&#xff08;线段树解法&#xff09; 【解题思路】 本题为设线段树维护区间最值&a…

【STM32】什么在使能寄存器或外设之前必须先打开时钟?

这篇文章解释一个非常基础但是重要的问题&#xff1a; 为什么在使能寄存器或外设之前必须先打开时钟&#xff1f; 我们会发现&#xff0c;如果不开时钟就访问寄存器 ⇒ 会“写不进去”或“读取错误”。 因此&#xff0c;我们在写代码时&#xff0c;总是需要 先开时钟&#xff0…

Go·并发处理http请求实现

一、Goroutine介绍 基本原理 goroutine 是 Go 运行时(Runtime)管理的​​用户态线程。与线程相比,其初始栈空间仅约 2KB,创建和切换的开销更低,能够同时运行大量并发任务。 创建goroutine的方法非常简单,在将要调用的函数前加入go关键字即可。 func hello() {fmt.Pri…