【c++ 之 多态】

news/2025/2/19 14:27:35/

目录:

  • 前言
  • 多态
    • 认识多态
    • 多态的定义与实现
      • 构成多态的条件
      • 虚函数
        • 1.协变(基类与派生类虚函数返回值不同)
        • 2.析构函数的重写
        • c++11.两个虚函数修饰关键字:final & override
    • 重载、重写、重定义再理解
  • 抽象类
    • 抽象类的概念
    • 接口继承与实现继承
  • 多态的原理
    • 虚函数表
    • 打印虚函数表
    • 原理剖析
  • 经典例题
  • 总结

前言

打怪升级:第61天
在这里插入图片描述

多态

认识多态

所谓多态,通俗来讲就是多种形态,也就是当一件事情由不同的人去完成会表现出不同的形态,例如买车票:成人全价,学生半价,军人优先等;
再例如测量体重,不同的人去测量,体重仪的表现也会不同。

多态的定义与实现

构成多态的条件

下面我们先来“见一见猪跑”:

class Person
{
public:virtual void BuyTicket(){cout << "成人,全价" << endl;}
};class Student : public Person
{
public:virtual void BuyTicket(){cout << "学生,半价" << endl;}
};void Buy(Person& p)
{p.BuyTicket();
}void Test_p2()
{Person p1;Student t1;Buy(p1);Buy(t1);
}

在这里插入图片描述

虚函数

  • 虚函数的定义
    虚函数就是使用 virtual 修饰的成员函数
class Person
{
public:virtual void BuyTicket(){cout << "成人,全价" << endl;}
};
  • 虚函数的重写
    **重写(覆盖)**的条件:
    在子类中存在与父类完全相同的虚函数(三同:函数名、参数、返回值都必须相同),我们称为子类对父类的虚函数进行了重写。
class Person
{
public:virtual void BuyTicket(){cout << "成人,全价" << endl;}
};class Student : public Person
{
public:virtual void BuyTicket(){cout << "学生,半价" << endl;}
};

这里是引用

上面,我们说的十分肯定 – 必须由三同才可构成重写,
然而,其实是有两个特例存在的 – 1.子类的重写返回值在特殊情况下可以不同;2.析构函数的重写

1.协变(基类与派生类虚函数返回值不同)

当子类和父类的虚函数返回值为有父子关系的类对象时,返回值也可以不同。

class A
{};class B :public A
{};class Person
{
public:virtual Person& BuyTicket(){cout << "成人,全价" << endl;return *this;}
};class Student : public Person
{
public:virtual Student& BuyTicket(){cout << "学生,半价" << endl;return *this;}
};

在这里插入图片描述

可以让父类虚函数返回子类引用,子类虚函数返回父类引用吗?
不可,虚函数的重写实际上是对 从父类继承下来的虚函数的实现进行重写,声明部分是完全继承的,因此,
如果父类虚函数返回子类引用,就会使得子类中的虚函数使用父类对象初始化子类对象,(我们可以使用子类对象初始化父类对象 – 会进行切片,但是父类中不一定拥有子类的全部成员,无法完成对子类的初识化)。

2.析构函数的重写

如果基类的析构函数是虚函数,此时派生类的析构函数无论是否加 virtual,都与基类的析构函数构成重写。
这里虽然基类与派生类的析构函数函数名不同,看起来好像违反了 三同 的规则,
其实不然,这里是编译器在底层做了特殊处理:编译之后所有析构函数的名称都会被处理为destruction

这里是引用在这里插入图片描述

c++11.两个虚函数修饰关键字:final & override

final修饰父类虚函数:该虚函数不可再在子类中进行重写了。
override修饰子类虚函数:该虚函数必须是父类的虚函数的重写。

在这里插入图片描述

重载、重写、重定义再理解

也就是说:两个基类和派生类中的同名函数 不构成重写 就是重定义。


抽象类

抽象类的概念

虚函数后面写上 = 0 ,这个虚函数就变成了纯虚函数, 包含纯虚函数的类称为抽象类(也叫接口类),包含纯虚函数的类无法实例化出对象,抽象类的派生类想要实例化出对象必须对纯虚函数实现重写,否则派生类也是抽象类。

接口继承与实现继承

普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。
虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。
所以如果不实现多态,不要把函数定义成虚函数。

这里是引用


多态的原理

多态,如果只看表面应用 – 不同的对象调用"同一个函数"表现也不一样,看起来感觉好像很神奇、很厉害,居然可以“进行判断?”,
那么到底是不是这样呢,让我们去底层一探究竟吧~。
(注:以下数据测试环境为 vs2022,x86)

虚函数表

class Base
{
public:virtual void Print(){cout << "Base::Print" << endl;}int _bval;
};void Test_p3()
{Base b1;cout << sizeof(b1) << endl;}

我们来计算一下Base类的大小:
按照我们以前的知识:成员函数是放在代码段,对象中只有普通成员变量, 因此,Base的大小应该是4;

这里是引用

在这里插入图片描述在这里插入图片描述

class Base
{
public:virtual void Print1() {}virtual void Print2() {}int _bval = 1;
};class Derive :public Base
{
public:virtual void Print1() {}virtual void Print3() {}int _dval = 10;
};void Test_p4()
{Base b1;Derive d1;
}

这里是引用

这里有一点我们需要注意:虚函数表存在哪里?虚函数又存在哪里?
虚函数表存在对象中,虚函数存在虚函数表中,吗?
不是的,对象中存的是一个虚函数表指针,虚函数表中存的也只是虚函数的指针,
至于虚函数表和虚函数,其实都存在于内存中的代码段

打印虚函数表

typedef void(*VFPTR)();  //  定义 VFPTR为  void(*)() -- 函数指针类型void VFTable(VFPTR*table)
{/*while (*table){(*table)();++table;}*/for (int i = 0; table[i]; ++i){printf("[%d]->", i);table[i](); // 函数调用}cout << endl;
}void Test_p4()
{Base b1;Derive d1;//  要打印虚函数表,我就要先获取虚函数表的地址 -- 通过上面几次的查看我们可以看到 -- 虚函数表地址存放在对象的最前面VFTable((VFPTR*)(*(int*)&b1));VFTable((VFPTR*)(*(int*)&d1));VFTable(*(VFPTR**)&b1); VFTable(*(VFPTR**)&d1);
}

这里是引用在这里插入图片描述


原理剖析

在这里插入图片描述在这里插入图片描述

补充:

  1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载
  2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。

经典例题

  • 1
class Base1 { public: int _b1; };
class Base2 { public: int _b2; };
class Derive : public Base1, public Base2 { public: int _d; };int main() 
{Derive d;Base1* p1 = &d;Base2* p2 = &d;Derive* p3 = &d;return 0;
}

在这里插入图片描述

  • 2
class AA
{
public:virtual void Print(int a = 1){cout << "a = " << a << endl;}virtual void Call() { Print(); }
};class BB :public AA
{
public:virtual void Print(int b = 0) { cout << "b = " << b << endl; }
};void Test_p1()
{BB p;p.Call();
}

在这里插入图片描述

总结

多态的重点

  1. 就是要了解多态构成的条件:父类的指针或引用;虚函数重写。
  2. 就是知道了解虚函数表的原理:存的是虚函数地址。
  3. 清楚多态实现的原理。



http://www.ppmy.cn/news/54121.html

相关文章

Node【Node.js 20】新特性

文章目录 &#x1f31f;前言&#x1f31f;Node.js 20: 一次重要的升级和改进&#x1f31f;Internationalization API Update&#x1f31f;端口管理器&#x1f31f;字符串处理&#x1f31f; 更好的调试工具&#x1f31f; Crypto模块的更新&#x1f31f;总结&#x1f31f;写在最后…

React函数组件语法(N)

文章目录 react学习的说明新的官网全力投入现代React与Hooks React简介概述官网特点生态 React脚手架create-react-app官网创建和启动项目项目结构sass支持 Vite创建和启动项目项目结构常见配置 虚拟DOM什么是虚拟DOM虚拟DOM优缺点优点&#xff1a;缺点&#xff1a; 虚拟DOM实现…

7.0、Java继承与多态 - 多态的特性

7.0、Java继承与多态 - 多态的特性 面向对象的三大特征&#xff1a;封装性、继承性、多态性&#xff1b; extends继承 或者 implements实现&#xff0c;是多态性的前提&#xff1b; 用学生类创建一个对象 - 小明&#xff0c;他是一个 学生&#xff08;学生形态&#xff09;&…

从0~1落地接口自动化测试,让你不再手忙脚乱丨轻松入门

接口自动化测试入门教程地址&#xff1a;https://www.bilibili.com/video/BV1914y1F7Bv/? 目录&#xff1a;导读 前言 为什么要做接口测试 理解接口和接口测试 如何落地接口自动化测试 结语 前言 对于初学者而言&#xff0c;如何从零开始落地接口自动化测试是一个难点&am…

【随笔】转发/转向(服务器重定向,服务器转发,服务器跳转)和重定向(客户端重定向,客户端转发,客户端跳转)

文章目录 1.转发&#xff08;转向&#xff09;和重定向图解2.例子3.区别 1.转发&#xff08;转向&#xff09;和重定向图解 图&#xff1a;转发&#xff08;转向&#xff09; 图&#xff1a;重定向 2.例子 转发&#xff1a;A找B要钱&#xff0c;B没钱&#xff0c;于是B向C…

【牛客刷题专栏】0x24:JZ23 链表中环的入口结点(C语言编程题)

前言 个人推荐在牛客网刷题(点击可以跳转)&#xff0c;它登陆后会保存刷题记录进度&#xff0c;重新登录时写过的题目代码不会丢失。个人刷题练习系列专栏&#xff1a;个人CSDN牛客刷题专栏。 题目来自&#xff1a;牛客/题库 / 在线编程 / 剑指offer&#xff1a; 目录 前言问…

Lesson13 IP协议

IP: 提供一种能力,将数据从A主机送到B主机的能力,但不一定会成功 主机 : 配有 IP 地址 , 但是不进行路由控制的设备 ; 路由器: 即配有 IP 地址 , 又能进行路由控制 ; 节点 : 主机和路由器的统称; 协议头格式 如何封装和解包: 定长报头 自描述字段 如何交付(分用) : 8…

ArrayList的扩容机制

前置知识 ArrayList的底层实现是一个Object[]&#xff0c;而LinkedList的底层实现是一个链表 ArrayList与LinkedList相对比&#xff1a; ArrayList在随机访问时可以做到O(1)&#xff0c;但是LinkedList的随机访问就是遍历链表&#xff0c;所以时间复杂度是O(N)ArrayList在插入…