当前位置: 首页 > news >正文

多态的原理与实现机制

目录

一、多态的基本原理

二、虚函数表机制

三、多态的实现条件分析

1、为什么需要虚函数重写?

2、为什么必须使用父类指针或引用?

3、为什么直接使用父类对象无法实现多态?(重点!!!)

四、总结

五、综上的问题解惑

1、核心原因:对象的类型决定了它的虚表

2、Person p2 = Johnson; 背后发生了什么?

总结与对比

一句话总结:


一、多态的基本原理

        多态是面向对象编程的核心特性之一,它允许不同类的对象对同一消息做出不同的响应。在C++中,多态主要通过虚函数机制实现。

        以下面的代码为例,分析多态的工作原理:思考一下为什么当父类Person指针指向的是父类对象Mike时,调用的就是父类的BuyTicket,当父类Person指针指向的是子类对象Johnson时,调用的就是子类的BuyTicket?

#include <iostream>
using namespace std;// 父类
class Person
{
public:virtual void BuyTicket() // 虚函数声明{cout << "买票-全价" << endl;}int _p = 1; // 成员变量
};// 子类
class Student : public Person
{
public:virtual void BuyTicket() // 重写虚函数{cout << "买票-半价" << endl;}int _s = 2; // 子类特有成员变量
};int main()
{Person Mike;Student Johnson;Johnson._p = 3; // 修改父类成员变量值,便于观察切片效果Person* p1 = &Mike;    // 父类指针指向父类对象Person* p2 = &Johnson; // 父类指针指向子类对象(切片)p1->BuyTicket(); // 输出:买票-全价p2->BuyTicket(); // 输出:买票-半价return 0;
}


二、虚函数表机制

通过调试分析可以发现:(之前好像讲解过,忘了,再写一遍也无妨)

  • 对象Mike中包含一个成员变量_p和一个虚表指针

  • 对象Johnson中包含两个成员变量_p_s以及一个虚表指针

  • 这两个对象的虚表指针分别指向各自的虚函数表,而通过虚函数表我们能找到表里面保存的虚函数的地址

多态的实现原理如下:

  • 父类指针p1指向Mike对象,p1->BuyTicket()Mike的虚表中找到的虚函数是Person::BuyTicket

  • 父类指针p2指向Johnson对象,p2->BuyTicket()Johnson的虚表中找到的虚函数是Student::BuyTicket

这样就实现了不同对象执行同一行为时,展现出不同形态的多态特性。


三、多态的实现条件分析

多态的实现需要满足两个条件:

  • 完成虚函数的重写(覆盖)

  • 必须使用父类的指针或引用调用虚函数

1、为什么需要虚函数重写?

        虚函数重写是为了实现子类虚表中虚函数地址的覆盖。当子类重写父类的虚函数时,子类的虚表会使用子类重写后的函数地址覆盖父类对应的虚函数地址。

2、为什么必须使用父类指针或引用?

        使用父类指针或引用时,实际上发生的是切片行为:只会获取父类对象或子类对象中属于父类的那一部分。这样,通过父类指针或引用调用虚函数时,能够根据实际指向的对象类型找到正确的虚表,从而调用正确的虚函数。

Person* p1 = &Mike;    // 切片:获取Mike中Person部分
Person* p2 = &Johnson; // 切片:获取Johnson中Person部分

因此,我们后序用p1和p2调用虚函数时,p1和p2通过虚表指针找到的虚表是不一样的,最终调用的函数也是不一样的。

3、为什么直接使用父类对象无法实现多态?(重点!!!)

Person p1 = Mike;      // 调用拷贝构造函数
Person p2 = Johnson;   // 切片后调用拷贝构造函数

使用父类对象时:

  • 切片操作会获取子类对象中属于父类的部分成员变量

  • 调用父类的拷贝构造函数进行拷贝构造

  • 关键点拷贝构造出来的父类对象p1p2中的虚表指针指向的都是父类的虚表(同类型的对象共享一张虚表)

因此,通过p1p2调用虚函数时,都会通过父类的虚表找到父类的虚函数,无法实现多态效果。


四、总结

  • 构成多态时:指向哪个类的对象就调用哪个类的虚函数(与对象实际类型相关)

  • 不构成多态时:对象类型是什么就调用哪个类的虚函数(与指针/引用类型相关)

多态机制通过虚函数表和动态绑定实现了运行时的多态性,是面向对象编程中实现代码扩展性和灵活性的重要基础。


五、综上的问题解惑

        在“Person p2 = Johnson;   // 切片后调用拷贝构造函数”中调用拷贝构造函数,Johnson的父类成员变量被拷贝过去了,那为什么那个虚表指针却不是Johnson重写的虚函数表呢?而是Mike中的虚函数表。为什么?

回答:

        这个问题是理解C++对象模型的一个关键点!Johnson确实有它自己的虚表指针,但拷贝后p2的虚表指针却变了。原因在于:拷贝构造函数的行为是“按类型构造”,而不是“按原样复制”。

1、核心原因:对象的类型决定了它的虚表

        在C++中,每个对象的虚表指针是在构造函数中初始化的。一个Person对象的虚表指针永远指向Person的虚表,一个Student对象的虚表指针永远指向Student的虚表。这是铁律。

2、Person p2 = Johnson; 背后发生了什么?

        这行代码的准确说法是:Johnson对象中属于Person的部分作为参数,调用Person类的拷贝构造函数,构造了一个全新的、完整的Person类对象p2

这个过程可以分解为:(重点理解!!!)

  1. 切片(Slice):编译器从Johnson对象中提取出属于Person类的成员(也就是_p和虚表指针__vptr)。

  2. 构造新对象:调用Person类的拷贝构造函数 Person(const Person&)

  3. 关键步骤:在Person的拷贝构造函数内部(无论是编译器生成的还是自定义的),它要做的就是构造一个Person对象。因此,它会按照构造Person对象的规则来:

    • 初始化p2的成员变量 _p:将切片得到的值(Johnson._p = 3)拷贝过来。

    • 初始化p2的虚表指针 __vptr绝不会拷贝Johnson的虚表指针,而是将其设置为Person类的虚表地址。因为现在是在构建一个Person对象,它的虚表理所当然应该是Person的虚表。

总结与对比

操作代码结果对象类型虚表指针来源是否多态
指针/引用Person* p2 = &Johnson;Student (实际)Johnson 对象的 (Student虚表)
对象切片Person p2 = Johnson;Person (编译时确定)Person 类的拷贝构造函数 (Person虚表)

一句话总结

        对象切片 Person p2 = Johnson; 创建的是一个全新的、纯粹的Person对象,它继承的是Johnson数据,但它的身份(虚表指针) 由它自己的类型(Person)决定。而指针/引用是直接指向原有对象,不会创建新对象,因此身份保持不变。


文章转载自:

http://ANytk5GS.pdwzr.cn
http://xKBQL5VO.pdwzr.cn
http://qBXTTwa7.pdwzr.cn
http://YTDXIVoE.pdwzr.cn
http://CWeyJikA.pdwzr.cn
http://gUHl64wU.pdwzr.cn
http://rOU53zbz.pdwzr.cn
http://mH4pewgI.pdwzr.cn
http://GNYBoBNr.pdwzr.cn
http://ehHaFRO4.pdwzr.cn
http://j27LJxPk.pdwzr.cn
http://PnWjsDpy.pdwzr.cn
http://jBUYutoq.pdwzr.cn
http://VeduOIIg.pdwzr.cn
http://3fn1ScHf.pdwzr.cn
http://bOHMAjDa.pdwzr.cn
http://nH88XKw3.pdwzr.cn
http://zOOPprYT.pdwzr.cn
http://euhZzVjR.pdwzr.cn
http://yuvTt506.pdwzr.cn
http://AuDHakke.pdwzr.cn
http://pCpgvoQi.pdwzr.cn
http://m9CDcafO.pdwzr.cn
http://IifJmFEW.pdwzr.cn
http://1nz1TcfO.pdwzr.cn
http://hSCR45hh.pdwzr.cn
http://ma4ekMEq.pdwzr.cn
http://DsRkXXU6.pdwzr.cn
http://Gm1ii54U.pdwzr.cn
http://Si3eZtWO.pdwzr.cn
http://www.dtcms.com/a/388287.html

相关文章:

  • [C++]异常
  • Windows PE 文件结构详解:从入口到执行的旅程
  • LLM 处理 PDF 表格的最佳方法:从解析到高效利用
  • 自动驾驶中的传感器技术50——Radar(11)
  • WALL-OSS--自变量机器人--2025.9.8--开源
  • GJOI 9.11/9.13 题解
  • 基于Spark的用户实时分析
  • 什么是 Conda 环境?
  • RK3506开发板QT Creator开发手册,交叉编译工具链与QT应用示例,入门必备
  • 颠覆3D生成,李飞飞团队新研究实现3D场景「无限探索」,AI构建世界模型能力跨越式进化
  • 3D 大模型生成虚拟世界
  • AI技术全景图:从大模型到3D生成,探索人工智能的无限可能
  • 一天认识一种模型方法--3D人体建模 SMPL
  • World Labs 的核心技术介绍:生成持久、可导航的 3D 世界
  • websocket如何推送最新日志
  • 使用Docker部署bewCloud轻量级Web云存储服务
  • web Service介绍
  • Web 架构中的共享存储:NFS 部署与用户压缩
  • RuoYi整合ZLM4j+WVP
  • @CrossOrigin的作用
  • Tree-shaking【前端优化】
  • Scikit-learn Python机器学习 - 分类算法 - 随机森林
  • 深入浅出Java中的Happens-Before原则!
  • centos7更换yum源
  • [特殊字符] 认识用户手册用户手册(也称用户指南、产品手册)是通过对产品功能的清
  • Codex 在 VS Code/Cursor 的插件基础配置
  • 前端Web案例-登录退出
  • Redis学习------------缓存优化
  • openfeigin 跨服务调用流程 源码阅读
  • 运动手环心率监测:原理、可靠性与市场顶尖之选全解析​​