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

『 C++ 入门到放弃 』- 多态

一、多态的概念

  • 多态顾名思义就是多种形态

  • 多态的分类

    • 编译时多态 => 静态多态

      静态多态的例子:函数模版、函数重载

      它们的特点是可以根据我们传入不同的参数,达成调用不同的函数的效果。之所以称为「静态」,是因为实参传给形参的参数匹配是在编译阶段完成的

    • 运行时多态 => 动态多态 ( 本篇重点说明的目标 )

      动态多态就是根据传入不同的对象来达到调用不同的行为 ( 相同函数不同效果 ) 以达成多态的效果

      哪尼?!

      举例来说:高铁定票 => 学生票 75 折,全票不打折

      如何达到例子中的效果?就是借由传入不同的对象,调用不同行为( 打折或不打折等 )

所以接下来我们要来正式说明面向对象编程三大特性 ( 封装、继承、多态 ) 之一的多态

1.1 多态的定义与条件

多态就是有着继承关系的类对象,调用相同的函数但形成不同的行为。如上面的例子 Student 继承 Person

假设我们有个函数 Buyticket()

Student 调用 => 票价打75折

Person 调用 => 不打折

多态的条件有:

  1. 需要有继承关系

  2. 要是基类的指针或引用调用虚函数

    => 因为只有基类的指针或引用才能既指向基类对象又指向派生类对象

  3. 被调用的函数一定要是虚函数,并且是完成了虚函数的重写 ( 覆盖 )

    => 重写或者覆盖了,基类和派生类之间才能有不同的函数,多态的不同形态效果才能达到
    在这里插入图片描述

  4. 静态绑定 & 动态绑定

    • 静态绑定:对不满足多态条件 (指针或者引用+调用虚函数函数) 的函数调用是在编译时绑定的也就是编译时就确定了调用函数的地址,叫做静态绑定
    • 动态绑定:满足多态条件的函数调用是在运行时绑定,也就是在运行时到指向对象的虚函数表中找到调用函数的地址,也就做动态绑定。而多态是在运行时才确定要调用函数的地址,所以多态属于动态绑定

1.2 虚函数

  • 虚函数就是在类的「成员函数」前加上关键字 virtual 那么这个函数就会被定义成了虚函数
  • 不是成员函数不能加 virtual 修饰
class Person
{
public:
};
virtual void BuyTicket() { cout << "买票-全价" << endl;}
1.2.1 虚函数的重写

虚函数重写 / 覆盖的定义:

  • 派生类和基类有个完全相同的函数 ( 包含:函数名、返回值类型、参数列表 ) 且要在基类要定义为虚函数的函数前加上 virtual

    • 派生类的虚函数可以不加virtual => 因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性

      但这种比较不规范,建议还是加上

    • virtual关键字只在声明时加上,在类外实现时不能加

#include <iostream>
using namespace std;
class Person
{
public:virtual void Buyticket(){cout << "全票" << endl;}
};class Student : public Person
{
public:virtual void Buyticket(){cout << "七五折" << endl;}
};
void func(Person & p){p.Buyticket();
}
int main()
{Person p;Student st;func(p); // 全票func(st); // 七五折return 0;
}
  • 静态成员函数不能设为虚函数

    理由:静态成员函数与具体对象无关,属于整个类,核心关键是没有隐藏的this指针,可以通过类名::成员函数名直接调用,此时没有this无法拿到虚表,就无法实现多态,因此不能设置为虚函数

  • 析构函数重写

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

    • Q1 : 为什么要把基类的析构函数变成虚函数?

      class A {
      public:~A() {cout << "A 析构" << endl;}
      };class B : public A {
      public:~B() {cout << "B 析构" << endl;}
      };int main() {A* p = new B;delete p; // 只会呼叫 A 的析构函数 /*p 是 A*,但实际上它指向 B。delete p; 时,只会呼叫 ~A(),不会呼叫 ~B()!如果 ~B() 有需要释放的资源,这些资源就不会被释放,会「造成内存泄漏*/
      }
1.2.2 override & final 关键字
  • override

    因为虚函数的条件较严苛,如果在设计虚函数时不免有因为疏忽而少写或写错的时候,因此C++11提供了关键字override 帮助用户检测是否重写
    在这里插入图片描述
    在这里插入图片描述

  • final

    如果我们不想让派生类重写这个虚函数,那么可以用final去修饰
    在这里插入图片描述
    在这里插入图片描述

1.2.3 纯虚函数 & 抽象类

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

class Car
{public:virtual void Derive() = 0;
};class BMW : public Car
{public:virtual void Derive() override{cout << "BMW" << endl;}
};class Benz : public Car
{public:virtual void Derive() override{cout << "Benz" << endl;}
};int main()
{Car car; // error: variable type 'Car' is an abstract classCar *pBenz = new Benz;Car *pBMW = new BMW;pBenz->Derive();pBMW->Derive();return 0;
}

二、重写 vs. 隐藏 vs. 重载

在这里插入图片描述

三、多态的底层原理

首先我们要先认识两个新名词 : 虚函数表、虚函数指针

  • 虚函数表:每个定义了虚函数的类,编译器会为它产生一个表,里面放的是函数指针,指向该类的虚函数实现

    基类对象的虚函数表中存放基类所有虚函数的地址。同类型的对象共用同一张虚表不同类型的对象有各自独立的虚表,所以基类和派生类有各自独立的虚表

  • 虚函数指针 ( vfptr ) : 每个对象中会有一个隐藏的指针,指向该类的虚函数表,称为 vfptr

3.1 多态底层运作流程

class Base {
public:virtual void speak() { std::cout << "Base speaking\n"; }
};class Derived : public Base {
public:void speak() override { std::cout << "Derived speaking\n"; }
};int main() {Base* obj = new Derived();obj->speak(); 
}
  1. Derived 是 Base 的子类,且 speak() 是虚函数,编译器会为每个类生成对应的 vtable。
  2. 建立 Derived 对象时,编译器会:
    • 在对象中加入一个 vfptr
    • 将这个指针设为指向 Derived 的 vtable
  3. 执行 obj->speak(); 时:
    • 透过 vfptr 找到 Derived 的 vtable
    • 从 vtable 中找到对应的 speak() 函数指针
    • 呼叫 Derived::speak()

3.2 虚函数表长什么样

"*虚函数表是在编译期间就确定的

/*vtable for Derived:Index 0: &Derived::speak
Index 1: ...(其他虚函数)*/// 每个 vtable 本质上是「函数指针数组」 类似下面
typedef void(*FuncPtr)();FuncPtr vtable_Derived[] = {(FuncPtr)&Derived::speak,...
};

3.3 vfptr 是怎么储存在对象中

Derived obj;obj 内存:
+-------------------+
| vfptr --------+   |
|               |   |
| 其他资料成员    |   |
+-------------------+|v+------------------+| vtable_Derived   |+------------------+
http://www.lryc.cn/news/585122.html

相关文章:

  • MyBatis-Plus通用中等、大量数据分批查询和处理
  • c语言中的数组IV
  • 卸载软件总留一堆“垃圾”?这款免费神器,一键扫清注册表和文件残留!
  • Python shutil模块详解
  • GPT3/chatGPT/T5/PaLM/LLaMA/GLM主流大语言模型的原理和差异
  • 从零实现一个GPT 【React + Express】--- 【3】解析markdown,处理模型记忆
  • 【LeetCode 热题 100】146. LRU 缓存——哈希表+双向链表
  • 0102基础补充_交易演示-区块链-web3
  • Django母婴商城项目实践(二)
  • 机器学习数据集划分全指南:train_test_split详解与实践
  • 基于相似性引导的多视角功能性脑网络融合|文献速递-最新论文分享
  • 【科研绘图系列】R语言绘制系统发育树和柱状图
  • 思维链革命:让大模型突破“机器思考”的边界
  • UniHttp中HttpApiProcessor生命周期钩子介绍以及公共参数填充-以百度天气接口为例
  • Grid网格布局完整功能介绍和示例演示
  • hive/spark sql中unix_timestamp 函数的坑以及时间戳相关的转换
  • php中调用对象的方法可以使用array($object, ‘methodName‘)?
  • 【JMeter】接口加密
  • 【JMeter】数据驱动测试
  • 预防DNS 解析器安全威胁
  • flutter redux状态管理
  • 【unitrix】 4.21 类型级二进制数基本结构体(types.rs)
  • JavaScript加强篇——第五章 DOM节点(加强)与BOM
  • 【驱动】移植CH340驱动,设置 udev 规则,解决和 BRLTTY 的冲突
  • 容器管理: 单机用Docker Compose,多机用Kubernetes
  • 用 React Three Fiber 实现 3D 城市模型的扩散光圈特效
  • 保安员从业资格证历年考试真题
  • Debian:从GNOME切换到Xfce
  • 【音视频】HLS拉流抓包分析
  • 物联网与互联网融合生态