C++基础/C++中的多态(关于虚...)

news/2024/9/21 3:32:53/文章来源:https://www.cnblogs.com/cha9/p/18385205

C++中的多态(关于虚...)

1.前置基础知识

1.1对象是如何存储在内存中的

#include <iostream>
#include <string>class Animal {
private:string name;int age;public:Animal(std::string name, int age) : name(name), age(age) {};~Animal();virtual void eat() {std::cout << "Animal在吃东西" << std::endl;}
};class Cat : public Animal {
private:int lives = 0;public:Cat(int lives) : Animal("cat", 18), lives(lives) {}virtual void eat() {std::cout << "cat在吃鱼" << std::endl;}
};int main() {Animal a("dog", 18);   // 属性会单独的存储在一个连续的内存空间中,但是函数大家都是一样的,所以不会重复拷贝一份函数和属性存储在一起,是放在另一快内存空间,供大家一起调用Animal b("cat", 12);   Cat c(8);  // 会申请一片内存空间,从父类继承过来的属性以及虚函数表指针存储在子类的属性和虚函数表指针前,是连续的return 0;
}

1.2虚函数

#include <iostream>
#include <string>class Animal {
public:std::string name;int age;public:Animal(std::string name, int age) : name(name), age(age) {};~Animal();virtual void eat() {std::cout << "Animal在吃东西" << std::endl;}
};class Cat : public Animal {
private:int lives = 0;public:Cat(int lives) : Animal("cat", 18), lives(lives) {}virtual void eat() {std::cout << "cat在吃鱼" << std::endl;}
};int main() {Animal* c = new Cat(8);c->eat();return 0;
}

在这里插入图片描述

  • 因为是Animal类型,所以调用的eat方法也就是Animal的eat方法

在这里插入图片描述

  • 给函数加上virtual关键字就代表是虚函数,这个时候调用会根据子类的虚函数表指针去查,根据对象去判断使用父类的方法还是子类重写后的方法

1.3虚函数表

在这里插入图片描述

  • 加上virtual关键字变为虚函数的函数都会存在虚函数表中,父类子类都有,一开始父类子类的虚函数表都是一样的,子类重写后,就会覆盖子类修函数表对应的位置上的内容

1.4虚函数表指针

在这里插入图片描述

  • 虚函数表指针顾名思义就是一个指针,指向于虚函数表,与对象的属性存储在一起

  • 虚函数 虚函数表 虚函数表指针关系

    在这里插入图片描述

1.5动态绑定vs静态绑定

  • Animal* c = new Cat("helloKitty", 18);上述这一行代码,为什么必须是指针或者引用?

    • 静态绑定+对象切片

      • 因为如果Animal c = new Cat("helloKitty", 18);这样写,就声明了c是Animal类型的,但是Cat构造法方法中多了一个属性,且重写了,会直接截取对象,使得c对象缺少lives这个属性和重写后的方法·。此时也为静态绑定,在编译的时候就根据对象类型已经确定了对应方法和属性,、。

        对象切片:当你将子类对象赋值给父类对象时,只有父类部分被复制,子类特有的部分被"切掉"了。 - 这意味着你失去了子类的特定信息和行为。

        静态绑定: - 使用对象(而不是指针或引用)调用方法时,编译器会在编译时决定调用哪个函数版本。 - 这种绑定是基于变量的声明类型,而不是实际对象的类型。

    • 动态绑定

      • 使用父类对象的指针或引用指向子类对象,并不会发生对象切片。因为只是把子类对象的地址赋值给父类指针(把地址传给指向父类指针,由于是虚函数,此时编译器不知道是什么类型),后在运行的时候确定类型,并根据虚函数指针查找虚函数表确认函数地址。
  • 总结

  1. 静态绑定(Static Binding):
    • 发生时间:编译时
    • 决定因素:变量的声明类型
    • 工作原理:编译器根据变量的声明类型直接决定调用哪个函数
    • 适用情况:非虚函数调用,或通过对象(非指针/引用)调用函数
    • 优点:运行效率高,没有额外开销
    • 缺点:不支持多态
  2. 动态绑定(Dynamic Binding):
    • 发生时间:运行时
    • 决定因素:指针或引用所指对象的实际类型
    • 工作原理:通过虚函数表查找并调用正确的函数
    • 适用情况:通过基类的指针或引用调用虚函数
    • 优点:支持多态,提高代码的灵活性和可扩展性
    • 缺点:相比静态绑定有轻微的性能开销

关键区别:

  1. 确定时机:
    • 静态绑定在编译时确定调用哪个函数。
    • 动态绑定在运行时确定调用哪个函数。
  2. 类型判断:
    • 静态绑定基于编译时已知的类型信息。
    • 动态绑定基于运行时对象的实际类型。
  3. 灵活性:
    • 静态绑定对于编译器来说更直接,但缺乏运行时的灵活性。
    • 动态绑定允许在运行时根据实际情况选择正确的函数版本。
  • 一句话总结区别
  • 静态绑定在编译的时候就确定了,效率较高、内存占用少(编译器默认都是静态绑定)。动态绑定是只有是虚函数才会发生,不然就根据声明类型编译时就确定了。

2.实现多态

2.1何为多态(分为静态多态和动态多态)

  • 动态多态

    • 动态多态:它允许我们通过基类的引用或指针来调用派生类的重写方法,这样就能在不修改代码的情况下实现多态行为,提高代码的复用性和扩展性。

      我理解的多态是,在编写任何相关代码时,如果声明传入的对象是父类的,可以根据虚函数,传入子类从而调用子类重写的方法,达到极高的代码复用性。

  • 静态多态

    • 静态多态:在编译时就确定使用的方法,可以通过重载和或者模板

2.2动态多态必须具备的条件

  1. 首先必须具备一个具有虚函数的父类和重写虚函数的子类
  2. 父类指针或引用指向(绑定)子类对象
  3. 通过动态绑定实现多态
#include <iostream>
#include <string>class Animal {
public:std::string name;int age;public:Animal(std::string name, int age) : name(name), age(age) {};virtual void eat() {           // 1.父类虚函数std::cout << "Animal在吃东西" << std::endl;}virtual ~Animal() {delete this;}
};class Cat : public Animal {
private:int lives = 0;public:Cat(int lives) : Animal("cat", 18), lives(lives) {}virtual void eat() {          // 1.子类重写父类虚函数std::cout << "cat在吃鱼" << std::endl;}
};void sleep(Animal& a) {           // 2.父类引用指向子类对象a.eat();                      // 3.进行动态绑定实现多态
}int main() {Animal* c = new Cat(8);c->eat();sleep(*c);Cat* ca = new Cat(9);         // 2.父类指针指向子类对象,这个例子可能要难理解些,因为嵌套了一层,显示一个指针再传为引用,引用依靠着这里的指针,如果指针是不满足条件的那么第二次引用也会出问题,类似于高楼大厦特别依靠地基sleep(*ca);return 0;
}

2.3静态多态

  • 这里不过多介绍,仅介绍部分概念
  • 静态多态通过函数重载和模板,函数重载常见的有运算符重载。模板可以自定义数据类型。

3.多态的注意点

3.1纯虚函数

  • 虚函数=0

    virtual ~Base() = 0

3.2虚析构函数

  • 概念

    • 虚基类: 指的是一个类由被其它类虚继承,那么该类就为虚基类。

    • 虚基类派生的子类的子类只能接受一份关于虚基类的属性与方法

  • 当父类指针或引用指向(绑定)子类对象,若虚基类中没有析构函数不是虚函数,那么只会调用父类的析构,不会调用子类的析构函数,可能会造成内存泄漏

    #include <iostream>class Base {
    public:Base() { std::cout << "Base constructor\n"; }virtual ~Base() { std::cout << "Base destructor\n"; }  // 虚析构函数
    };class Derived : public Base {
    public:Derived() { std::cout << "Derived constructor\n"; }~Derived() { std::cout << "Derived destructor\n"; }
    };int main() {Base* obj = new Derived();delete obj; // 正确:会调用Derived的析构函数,然后再调用Base的析构函数return 0;
    }//输出结果
    //Base constructor
    //Derived constructor
    //Derived destructor
    //Base destructor
    

3.3抽象类和纯析构函数

  • 抽象类就是虚函数=0,抽象类必须被继承

    #include <iostream>class Base {
    public:Base() { std::cout << "Base constructor\n"; }virtual ~Base() = 0 // 虚析构函数
    };Base::virtual ~Base() {std::cout << "Base析构执行了" << std::endl;
    }class Derived : public Base {
    public:Derived() { std::cout << "Derived constructor\n"; }~Derived() { std::cout << "Derived destructor\n"; }
    };int main() {Derived* obj = new Derived();delete obj;return 0;
    }
    
  • 纯析构是实现抽象类的一个方法,但是要注意也要实现一下纯析构函数,原因是纯虚析构函数确保基类的析构过程能被正确调用,以便释放派生类对象的资源。没有实现,析构过程会出现未定义行为。这个时候有人会问可是我加了个函数函数体但是是空的却不会有问题,因为纯虚析构和函数体为空是不同的

4多继承和菱形继承

  • 多继承:

    就是一个子类继承了多个类,注意要避免二义性,就是多个父类含有相同的命名的属性,这个时候需要通过作用域运算符进行访问"::"

  • 菱形继承

    在这里插入图片描述

    可与看出子类齐天大圣的两个父类都继承了同一个父类,那么齐天大圣就会有两个关于地球的属性,这就重复了,所以就有了虚继承

  • 虚继承: 如果多个基类共享一个公共基类,可以使用虚继承来避免菱形继承问题,确保公共基类只存在一个实例。

  • 虚基类: 指的是一个类由被其它类虚继承,那么该类就为虚基类。

  • 虚基类派生的子类的子类只能接受一份关于虚基类的属性与方法

  • 如何使用

    class Base {
    public:int base;Base() : base(0) {}
    };class Derived1 : virtual public Base {// 通过虚继承继承 Base
    };class Derived2 : virtual public Base {// 通过虚继承继承 Base
    };class Final : public Derived1, public Derived2 {
    public:void show() {std::cout << "Base::base = " << base << std::endl; // 这里访问 Base::base}
    };int main() {Final f;f.show(); // 正常输出 Base::basereturn 0;
    }
    

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

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

相关文章

算法与数据结构——哈希算法

哈希算法 前面介绍了哈希表的工作原理和哈希冲突的处理方法。然而无论是开放寻址还是链式地址,它们只能保证可以在发生冲突时正常工作,而无法减少哈希冲突的发生。 如果哈希冲突过于频繁,哈希表的性能则会急剧劣化。如下图所示,对于链式哈希表,理想情况下键值对均匀分布在…

Proxyless的多活流量和微服务治理

1. 引言 1.1 项目的背景及意义 在当今的微服务架构中,应用程序通常被拆分成多个独立的服务,这些服务通过网络进行通信。这种架构的优势在于可以提高系统的可扩展性和灵活性,但也带来了新的挑战,比如:服务间通信的复杂性:不同服务之间需要进行可靠的通信,处理失败重试、负…

报表融合大屏,做不一样的财务分析!

冷冰冰的数据如何让人眼前一亮? 千篇一律的表格如何让数据可视化? ...... 赶快丢掉那些传统的表格工具吧!!!现在我们都用更智能的工具来做报表了!财务报表是什么? 财务报表是企业财务状况、经营成果及现金流量的综合反映,通过表格、图表等形式,系统地展示了企业在一定…

Cloudflare Workers 每日免费限制 超出流量自动关闭 - 失败模式 改为 失败时自动关闭(阻止)

cloudflare workers 每日免费限制 超出流量自动关闭 - 失败模式 改为 失败时自动关闭(阻止) 位置在 Workers 和 Pages - 相应的workers - 设置 - 函数 - 更改失败模式 改为 失败时自动关闭(阻止) 这个设置,网上竟然没有人说,这么重要的事情,应该要设置,必须要设置!!…

为什么上海市的跨江大桥两边上没有设置非机动车道 All In One

为什么上海市的跨江大桥两边上没有设置非机动车道 All In One 处于设计、安全和成本考虑,因噎废食,懒得搞为什么上海市的跨江大桥两边上没有设置非机动车道 All In One处于设计、安全和成本考虑,因噎废食,懒得搞FQA 行人如何跨越黄浦江?上海15座跨江大桥,为何只见车流不见…

vue3的 状态管理库

1.vue 的状态管理库 vue 常用的状态管理库有 vuex 和 Pinia,两者的区别如下:架构设计‌:Vuex 采用全局单例模式,通过一个store对象来管理所有的状态;而 Pinia 采用了分离模式,每个组件都拥有自己的store实例。 模块设计:Vuex 包含 states、mutations、getters、actions、…

南沙信息学家教陈老师: 1349:【例4-10】最优布线问题

​ 【题目描述】学校有nn台计算机,为了方便数据传输,现要将它们用数据线连接起来。两台计算机被连接是指它们有数据线连接。由于计算机所处的位置不同,因此不同的两台计算机的连接费用往往是不同的。 当然,如果将任意两台计算机都用数据线连接,费用将是相当庞大的。为了节…

基于springboot的grpc服务端demo

一个springboot 实现grpc 服务端demo的简单配置,包含java配置 和 代理配置1. Javamaven配置点击查看代码 <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www…

博客园美化系列第一弹

博客园美化系列第一弹 首先要确保你已经申请开通博客「理由随便写,积极向上即可」,且已通过审核。 然后进入【设置】,申请 js 权限。 申请理由举例「从网上找的,当时直接复制上就通过了」: 尊敬的博客园管理员: 您好,我想通过 js 定制化我的博客,麻烦通过下我的申请。 …

高通ramdump

背景 高通平台下提供了一个工具,专门用来抓取内核死机以后的dump信息。如果只是非系统层面的crash(例如底层应用,安卓程序),则不能抓取dump信息。 在阅读一些文档的时候知道有这个功能,但是一直没时间尝试。 介绍 流程为: 1、进入dump模式:系统需要触发crash, 同时机器…

QL5010-16-ASEMI逆变焊机专用整流桥QL5010

QL5010-16-ASEMI逆变焊机专用整流桥QL5010编辑:ll QL5010-16-ASEMI逆变焊机专用整流桥QL5010 型号:QL5010 品牌:ASEMI 封装:KBPC-4 批号:2024+ 类型:整流模块 电流:50A 电压:1600V 安装方式:直插式封装 特性:大功率、整流桥 产品引线数量:4 产品内部芯片个数:4 产品…

解决方案 | IrfanView如何滑动滚轮图像缩放?

这是个bug,已经很多人反映了。目前没有比较好的解决方法,还是使用ctrl+滚轮最好。如果需要设置滚轮放大的话,按照下图即可,但是带来一个bug,你无法通过方向键或者菜单的箭头浏览 下一张图片。综上所述,你有3个选择,1 接受使用 ctrl+滚轮进行放大2 设置--关闭”显示所有支…