C++之多态

C++之多态

多态

在这里插入图片描述

什么是多态?

多态(Polymorphism)是面向对象编程的三大核心特性之一(封装、继承、多态),源于希腊语"多种形态"。在C++中,它允许我们使用统一的接口处理不同类型的对象,显著提高了代码的灵活性和可扩展性。

核心概念

  1. 同一接口,多种形态
    不同的对象可以通过相同的方法名调用,但实际执行的逻辑由对象自身的类决定。
  2. 解耦调用与实现
    调用者只需关注接口(方法名和参数),无需关心具体实现,提高代码的可扩展性和可维护性。

多态的定义及实现

多态的构成条件

多态是⼀个继承关系的下的类对象,去调⽤同⼀函数,产⽣了不同的⾏为。
实现多态还有两个必须重要条件:

  1. 必须是基类的指针或者引⽤调⽤虚函数
  2. 被调⽤的函数必须是虚函数,并且完成了虚函数重写/覆盖。
    说明:要实现多态效果,第⼀必须是基类的指针或引⽤,因为只有基类的指针或引⽤才能既指向基类 对象⼜指向派⽣类对象;第⼆派⽣类必须对基类的虚函数完成重写/覆盖,重写或者覆盖了,基类和派 ⽣类之间才能有不同的函数,多态的不同形态效果才能达到。
虚函数

类成员函数前⾯加virtual修饰,那么这个成员函数被称为虚函数。注意⾮成员函数不能加virtual修饰。

class A { public: virtual void(){}};
虚函数的重写/覆盖

虚函数的重写/覆盖:派⽣类中有⼀个跟基类完全相同的虚函数(即派⽣类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称派⽣类的虚函数重写了基类的虚函数。

注意:在重写基类虚函数时,派⽣类的虚函数在不加virtual关键字时,也可以构成重写(因为继承后基类的虚函数被继承下来了在派⽣类依旧保持虚函数属性)

关键技术原理
  1. 虚函数表(vtable)
    • 每个包含虚函数的类自动生成虚函数表
    • 存储该类所有虚函数的地址
    • 创建对象时隐式添加vptr指针指向vtable
  2. 动态绑定过程基类指针查询对象vptr访问vtable调用实际函数地址
最佳实践指南
  1. 使用override关键字明确重写意图
  2. 基类析构函数必须声明为virtual
  3. 接口类使用纯虚函数
  4. 性能考量:虚函数调用比普通函数多一次指针解引用

虚函数重写

协变

派⽣类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引⽤,派⽣类虚函数返回派⽣类对象的指针或者引⽤时,称为协变。
核心概念

  1. 基类虚函数返回基类类型指针 / 引用。
  2. 派生类重写函数返回派生类类型指针 / 引用。
  3. 编译器会自动处理类型转换,确保调用的一致性。

协变的条件

  1. 基类函数必须为虚函数。
  2. 返回类型必须是指针或引用(不能是值类型)。
  3. 派生类返回类型必须是基类返回类型的公有派生类。
  4. 函数签名的其他部分(参数、常量性)必须完全相同。
析构函数的重写

基类的析构函数为虚函数,此时派⽣类析构函数只要定义,⽆论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派⽣类析构函数名字不同看起来不符合重写的规则,实际上编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统⼀处理成destructor,所以基类的析构函数加了vialtual修饰,派⽣类的析构函数就构成重写。

class A { public: virtual ~A(){ cout <<"~A()"<< endl;}}; class B : public A { public:~B(){ cout <<"~B()->delete:"<<_p<< endl; delete _p;} protected:int* _p = new int[10];};intmain(){ A* p1 = new A; A* p2 = new B; delete p1; delete p2;return0;}

如果~A(),不加virtual,那么delete p2时只调⽤的A的析构函数,没有调⽤B的析构函数,就会导致内存泄漏问题,因为 ~B()中在释放资源。

只有派⽣类Student的析构函数重写了Person的析构函数,下⾯的delete对象调⽤析构函数,才能构成多态,才能保证p1和p2指向的对象正确的调⽤析构函数。

override和final关键字
  1. override 关键字
    作用
    明确标记一个函数是重写基类的虚函数,编译器会检查以下条件:
    基类中是否存在同名虚函数。
    基类和派生类的函数签名(参数、常量性、引用限定符)是否严格一致。
    返回类型是否符合协变规则(如果适用)。
  2. final 关键字
    作用
    final 有两种用法:
    修饰虚函数:禁止派生类重写该虚函数。
    修饰类:禁止该类被继承。
纯虚函数和抽象类

在虚函数的后⾯写上=0,则这个函数为纯虚函数,纯虚函数不需要定义实现(实现没啥意义因为要被派⽣类重写,但是语法上可以实现),只要声明即可。包含纯虚函数的类叫做抽象类,抽象类不能实例化出对象,如果派⽣类继承后不重写纯虚函数,那么派⽣类也是抽象类。纯虚函数某种程度上强制了派⽣类重写虚函数,因为不重写实例化不出对象

  • 纯虚函数:在基类中声明为virtual,并且没有实现的函数,形式为virtual void func() = 0;
  • 抽象类:包含纯虚函数的类称为抽象类,不能实例化对象,只能作为基类被派生。
    • Shape是一个抽象类,Circle是派生类,重写了draw()函数。

多态的使用:

Shape* shape =newCircle(); shape->draw();// 输出:Drawing Circle

示例:

classShape{public:virtualvoiddraw()=0;// 纯虚函数};classCircle:publicShape{public:voiddraw()override{ cout <<"Drawing Circle"<< endl;}};

多态的原理

多态是如何实现的

在C++中,多态主要通过虚函数(virtual functions)和继承机制来实现。以下是多态实现的详细过程和原理:

1. 虚函数表(vtable)

  • 概念:每个包含虚函数的类都会有一个虚函数表(vtable),它是一个存储虚函数地址的数组。当创建一个类的对象时,对象会包含一个指向其类的虚函数表的指针(vptr)。
  • 作用:vtable是实现多态的关键。当通过基类指针或引用调用虚函数时,程序会通过vptr找到对应的vtable,然后通过vtable找到实际的虚函数地址。
虚函数表知识要点
  1. 基类对象的虚函数表中存放基类所有虚函数的地址。同类型的对象共⽤同⼀张虚表,不同类型的对象各⾃有独⽴的虚表,所以基类和派⽣类有各⾃独⽴的虚表。
  2. 派⽣类由两部分构成,继承下来的基类和⾃⼰的成员,⼀般情况下,继承下来的基类中有虚函数表指针,⾃⼰就不会再⽣成虚函数表指针。但是要注意的这⾥继承下来的基类部分虚函数表指针和基类对象的虚函数表指针不是同⼀个,就像基类对象的成员和派⽣对象中的基类对象成员也独⽴的。
  3. 派⽣类中重写的基类的虚函数,派⽣类的虚函数表中对应的虚函数就会被覆盖成派⽣类重写的虚函数地址。
  4. 派⽣类的虚函数表中包含,(1)基类的虚函数地址,(2)派⽣类重写的虚函数地址完成覆盖,派⽣类⾃⼰的虚函数地址三个部分。
  5. 虚函数表本质是⼀个存虚函数指针的指针数组,⼀般情况这个数组最后⾯放了⼀个0x00000000标记。(此条取决于编译器)
  6. 虚函数存储的位置(vs下为常量区)
    可以自行验证:
class Base { public: virtual voidfunc1(){} protected:int a =1;}; class Derive : public Base { public: virtual voidfunc1(){} protected:int b =2;};intmain(){int i =0;staticint j =1;int* p1 = new int;constchar* p2 ="xxxxxxxx";printf("栈:%p\n",&i);printf("静态区:%p\n",&j);printf("堆:%p\n", p1);printf("常量区:%p\n", p2); Base b; Base* p3 =&b;printf("虚表地址:%p\n",*(int*)p3);return0;}//通过比较虚表地址,找到其最近的内存片段来确定

2. 虚函数的声明

    • Base类的show()函数被声明为虚函数,Derived类重写了这个虚函数。

在类中使用virtual关键字声明函数为虚函数。
Base为基类,Derived为继承类。

classBase{public:virtualvoidshow(){ cout <<"Base show()"<< endl;}};classDerived:publicBase{public:voidshow()override{// override表示重写基类的虚函数 cout <<"Derived show()"<< endl;}};

3. 多态的实现过程

    • 当创建Derived类的对象d时,对象中会包含一个vptr,指向Derived类的vtable。
    • Derived类的vtable中存储了Derived类重写的虚函数show()的地址。
    • ptr是一个指向Base类的指针,但它指向的是Derived类的对象。
    • 当调用ptr->show()时,程序会通过ptr找到对象的vptr,然后通过vptr找到Derived类的vtable,最终调用Derived类的show()函数。
    • 这就是多态的运行时绑定(动态绑定)过程。

通过基类指针或引用调用虚函数

Base* ptr =&d; ptr->show();

创建对象

Derived d;
动态绑定与静态绑定

• 对不满⾜多态条件(指针或者引⽤+调⽤虚函数)的函数调⽤是在编译时绑定,也就是编译时确定调⽤函数的地址,叫做静态绑定。

• 满⾜多态条件的函数调⽤是在运⾏时绑定,也就是在运⾏时到指向对象的虚函数表中找到调⽤函数的地址,也就做动态绑定。

Read more

【 C/C++ 算法】入门动态规划-----路径问题(以练代学式)

【 C/C++ 算法】入门动态规划-----路径问题(以练代学式)

>每日激励:“不设限和自我肯定的心态:I can do all things。 — Stephen Curry” 绪论 : 本章是动态规划的第二篇,本章将开始二维的动态规划,在二维中的动态规划本质和一维的分析来说差不太多,只不过状态表示从一维变成了二维,而在二维上所能管理的状态就从一维的两个变成了二维的三个,也就是x轴,y轴,数组中的值。若没看了解过动规算法,我强烈建议先看第一篇blog,因为当你看完第一篇你就对动规基本认识了,其中也就能认识到它的五步骤分析法,这里也就不扩充说明而是直接使用了 ———————— 早关注不迷路,话不多说安全带系好,发车啦(建议电脑观看)。 路径问题🛣️ 本章主要还是在二维数组中的进行的动态规划: 同样还是五步走:状态表示、状态方程、初始化、移动方向、返回结果 1. 其中在二维中状态表示就会和一位略有不同,不同本质一样: 从以 i 结尾.,… ==》从左上角到达 i j 位置,… 1. 当然在最后一题中发现上面这种常规方法实现不通,因为状态方程会受后面状态影响 2.

By Ne0inhk

【python】一般python项目的目录结构

Python 项目标准目录结构(全场景完整版) 你想了解Python项目的通用目录结构,核心结论先说:Python项目没有「唯一绝对」的标准,但有「行业通用、约定俗成」的最佳实践结构,会根据「项目规模/用途」区分,从小型脚本项目 → 中大型工程化项目 → Web框架项目,结构逐步规范,所有规范都遵循 Python 社区的通用约定,兼顾可读性、可维护性、协作效率。 一、基础通用版(✅ 90%的中小项目首选,新手必学,最常用) 适用于:个人项目、工具类项目、业务逻辑不复杂的中小型项目、内部自用项目,结构简洁够用,无冗余,规范且易上手,是Python项目的「最小完美结构」。 your_project/ # 项目根目录(项目名,自定义,比如data_analysis/) ├── README.

By Ne0inhk

Python逆向工程实战:解密PyInstaller可执行文件的字节码恢复技术

Python逆向工程实战:解密PyInstaller可执行文件的字节码恢复技术 【免费下载链接】pyinstxtractorPyInstaller Extractor 项目地址: https://gitcode.com/gh_mirrors/py/pyinstxtractor 当你拿到一个加密的Python可执行文件,却需要分析其内部实现逻辑时,如何突破层层封装获取核心代码?当重要项目的源代码意外丢失,仅存一个打包后的可执行文件时,如何高效恢复开发资源?PyInstaller解包工具正是解决这些难题的专业利器,它能帮助开发者和安全研究员从PyInstaller打包的可执行文件中完整提取Python源代码和资源文件,实现Python可执行文件逆向与源代码提取的核心需求。 如何安全提取PyInstaller打包的可执行文件? 逆向环境搭建:从工具获取到环境配置 核心原理:PyInstaller解包工具通过解析可执行文件的归档结构,提取其中的Python字节码(Bytecode:Python解释器可执行的中间代码)和资源文件,并修复字节码文件头信息使其可被反编译工具识别。 工具

By Ne0inhk

C语言面试篇:深度解析C 与 C++ 的不同

C与C++的核心差异概述 C是面向过程的结构化语言,强调函数与模块化; C++在兼容C的基础上引入面向对象(OOP)、泛型编程等特性,更适合大型软件工程开发。 以下从语法特性、内存管理、编程范式等维度展开分析。 1.语法特性对比 C 语言特性:聚焦 “简洁的过程式逻辑” C 的语法设计以 “轻量、高效” 为核心,仅保留过程式编程的基础能力,缺乏复杂抽象支持: * 结构体仅作数据容器:C 的struct只能聚合数据成员,无法绑定操作数据的函数,数据与逻辑必须分离 —— 操作结构体的功能需通过独立函数实现,代码的 “数据 - 行为” 关联度低。 * 无函数重载与默认参数:C 中函数名是唯一标识,不同参数类型 / 个数的功能需用不同函数名区分(如add_int、add_float);同时不支持参数默认值,调用时必须传入所有参数,灵活性受限。 * 仅依赖指针操作内存:C

By Ne0inhk