C++多态的一些理解

news/2024/6/15 16:27:47/文章来源:https://blog.csdn.net/KK_2018/article/details/136943302

C++多态的一些理解

多态的概念

什么是多态?多态是C++的三大特性之一。简单来说就是用一种接口(函数)来实现多种不同的功能。当我们调用同一个函数的时候,根据业务需要,会执行不同的功能,产生不同的效果。

为什么要用多态?多态性是一种强大的面向对象编程的特性,它提供了灵活性、可扩展性和可维护性。通过多态性,可以编写通用的代码,减少重复编码,并实现对象的替换和扩展。这样可以使代码更加灵活、可维护和可扩展,并提高程序的可读性和可靠性。试想这样一个场景,你需要实现一个加法器,但是参数类型是有整数和浮点数两种,函数的返回值也是有两种,如果不使用多态,那么就要分别考虑参数类型,针对不同的参数类型来写不同的函数,但是这样做就显得很繁琐,毕竟都是加法,直接用一个函数不好吗?

多态分为静态多态和动态多态,静态多态是在编译时确定要调用的函数版本,通过函数重载实现,适用于非虚函数和静态成员函数。动态多态是在运行时确定要调用的函数版本,通过虚函数和继承实现,适用于虚函数和纯虚函数,并且需要使用指针或引用来实现多态性。静态多态的绑定是在编译时完成的,而动态多态的绑定是在运行时完成的。
在这里插入图片描述

静态多态

(静态绑定,早绑定,编译时多态):
静态多态通过函数重载或者模板(包括模板函数和模板类)实现。编译器在编译时期决定调用哪个函数,这是通过参数的数量、类型或者两者来决定的。这种决定过程称为静态绑定或早期绑定。

静态多态的优点

  • 性能:由于函数调用是在编译时解析的,静态多态避免了运行时的虚函数查找开销,提供了更好的性能。
  • 类型安全:编译器在编译时检查类型,确保类型的正确性,减少运行时错误。
  • 明确性:通过函数重载和模板的明确性,代码可以更清晰地表达其意图,提高了代码的可读性和可维护性。

但是,如果使用大量模板,会导致代码膨胀(编译后,程序体积显著增大)。

代码膨胀的影响

  • 增加了可执行文件的大小:这可能影响程序的加载时间和内存使用,尤其是在内存受限的环境下。
  • 可能降低运行时性能:更大的可执行文件可能导致更频繁的缓存失效和页换入换出,从而降低程序的运行效率。
  • 编译时间增加:生成大量代码会延长编译时间,影响开发效率。

动态多态

(动态绑定,晚绑定,运行时多态):

  • 动态多态是通过虚函数实现的,这允许在运行时决定调用哪个函数。这是通过派生类重写基类中声明为虚的函数来实现的。这种决定过程称为动态绑定或晚期绑定。
  • 当通过基类指针或引用调用虚函数时,实际调用的是根据对象的动态类型确定的函数版本。
  • 动态多态适用于虚函数和纯虚函数,并且需要使用指针或引用来实现多态性。

动态多态的优点

  1. 增强了软件的可维护性。动态多态使得代码更加模块化,通过基类接口可以操作所有派生类对象。这样,当需要修改或扩展程序功能时,只需添加或修改派生类,而无需修改使用基类引用或指针的代码。这大大减少了维护和升级软件所需的工作量。
  2. 提高了代码复用性。通过继承和多态,可以创建通用的代码来操作所有派生自同一基类的对象。这意味着相同的代码可以用于操作不同类型的对象,从而提高了代码的复用性。
  3. 支持动态绑定。动态多态允许程序在运行时动态地绑定对象和方法,而不是在编译时。这种能力使得程序可以更灵活地处理不同类型的对象,即使在编写代码时并不知道具体将操作哪些对象类型。
  4. 促进了接口与实现的分离。动态多态鼓励设计者将接口(通过基类定义)与实现(通过派生类完成)分离。这种分离不仅使得代码更加清晰,也更易于管理和扩展。
  5. 便于创建更复杂的行为。使用动态多态,可以在基类中定义接口和部分实现,在派生类中根据需要添加或修改特定行为。这种方法便于创建能表现出复杂行为的对象,同时保持代码的组织和简洁。
  6. 支持后期绑定。后期绑定(或运行时绑定)是动态多态的核心,它使得对象方法的调用可以在运行时确定,为创建可扩展和易于管理的大型系统提供了基础。
  7. 促进了设计模式的应用。许多设计模式,如策略模式、观察者模式和工厂模式,都依赖于动态多态来实现其灵活性和解耦。通过使用这些模式,可以提高代码的灵活性和可复用性。

如何实现多态

通过函数重载实现(静态)

函数重载是指在同一个作用域内(即全局函数或者在同一个类内)定义多个具有相同名称但参数列表不同的函数。

函数重载允许使用相同的函数名来执行不同的操作,根据传递给函数的参数来选择适当的函数进行调用,提供了代码简洁性、灵活性和函数命名一致性的好处,使得代码更加清晰、易于理解和可扩展。

#include <iostream>using namespace std;class FunctionOverloading
{
public:int add(int a, int b){return a + b;}double add(double a, double b){return a + b;}int add(int a, int b, int c){return a + b + c;}
};int main()
{FunctionOverloading fol;cout << fol.add(1, 2) << endl;cout << fol.add(1.5, 2.5) << endl;cout << fol.add(1, 2, 3) << endl;return 0;
}

看下输出结果
在这里插入图片描述
可以看到不同的参数类型调用了不同的函数,这就是函数重载。

通过函数模板实现(静态)

什么是函数模板?函数模板是一种通用的函数定义,可以用于生成多个具有相似功能但类型不同的函数。它允许在代码中编写一次函数定义,然后可以根据需要使用不同的数据类型进行实例化和调用。函数模板的优势在于可以提供通用的代码,避免了重复编写相似功能的函数。它可以根据不同的类型参数自动生成不同的函数定义,提高代码的灵活性和重用性。函数模板常用于容器类、算法库和泛型编程等场景。

函数模板的定义使用关键字 template,后面跟着模板参数列表,其中包含一个或多个类型参数或非类型参数。类型参数用于指定函数中的参数类型,非类型参数用于指定函数中的常量值或枚举值。

利用函数模板也可以实现多态,我们修改刚才的代码,添加一个函数模板

#include <iostream>using namespace std;class FunctionOverloading
{
public:int add(int a, int b){return a + b;}double add(double a, double b){return a + b;}int add(int a, int b, int c){return a + b + c;}
};template <typename T>
T add(T a, T b)
{return a + b;
}int main()
{FunctionOverloading fol;cout << fol.add(1, 2) << endl;cout << fol.add(1.5, 2.5) << endl;cout << fol.add(1, 2, 3) << endl;cout << add(1, 2) << endl;cout << add(3, 4) << endl;cout << add(1.5, 2.5) << endl;return 0;
}

运行结果如下
在这里插入图片描述

通过虚函数和纯虚函数实现(动态)

虚函数的特点包括:

  • 基类中用virtual关键字声明的函数。派生类可以重写这个函数来提供特定的实现。如果有一个基类指针或引用指向派生类的对象,那么调用虚函数时,会根据对象的实际类型来调用相应的函数。
  • 可被派生类重写:派生类可以通过在自己的类定义中使用相同的函数签名来重写(覆盖)基类中的虚函数。重写的函数必须具有相同的返回类型、参数列表和常量属性。
  • 动态绑定:当通过基类指针或引用调用虚函数时,实际调用的是根据对象的动态类型来确定的函数版本。这种绑定是在运行时完成的,称为动态绑定。它允许在运行时根据对象的实际类型来调用适当的函数,实现多态性。
  • 默认实现:虚函数可以在基类中提供默认的实现,如果派生类没有重写该函数,将使用基类中的默认实现。派生类可以选择性地重写虚函数,以便修改或扩展其行为。

纯虚函数的特点:
纯虚函数在基类中没有实现,必须在非抽象派生类中被重写。纯虚函数的声明方式是在函数原型的结尾加上 = 0

举个例子

#include <iostream>
using namespace std;class Base {
public:virtual void show() { cout << "Base show" << endl; }virtual ~Base() {}
};class Derived : public Base {
public:void show() override { cout << "Derived show" << endl; }
};void display(Base* b) {b->show();
}int main() {Base* b = new Base();Base* d = new Derived();display(b); // 输出 Base showdisplay(d); // 输出 Derived showdelete b;delete d;return 0;
}

在上述示例中,display函数接受一个Base类的指针。虽然display函数调用的是Base类的show方法,但是由于show方法被声明为虚函数,并且在Derived类中被重写,所以当display函数传入Derived类的对象时,调用的是Derived类中的show方法。这就是动态多态的体现。

静态多态和动态多态的原理

静态多态(函数重载和模板)

静态多态主要通过函数重载和模板实现。编译器在编译时进行函数调用的解析(即所谓的静态绑定),决定调用哪个函数。因此,在生成的汇编代码中,你会看到对具体函数的直接调用。没有额外的运行时开销,因为所有的决策都在编译时完成了。

  • 函数重载:编译器根据函数的参数类型和数量,在编译时决定调用哪个重载版本。在汇编代码中,每个重载函数都有其唯一的标签,调用时会直接跳转到相应的标签。
  • 模板:模板实例化也发生在编译时。每个具体化的模板实例会生成一套独立的函数或类代码。在汇编代码中,你会看到每个模板实例化生成的函数都有其独立的地址,调用时直接跳转到这个地址。

对于这段代码

#include <iostream>
class Print {
public:void show(int i) {std::cout << "Integer: " << i << std::endl;}void show(double f) {std::cout << "Float: " << f << std::endl;}
};int main() {Print obj;obj.show(5);    // Calls the show(int) functionobj.show(3.14); // Calls the show(double) functionreturn 0;
}

在编译后的汇编代码中,会看到对Print::show(int)和Print::show(double)的直接调用。编译器在编译时已经解析了哪个函数应该被调用,因此生成的代码中会有两个分别对应这两个函数的直接跳转或函数调用指令。

也就是说,在静态多态的情况下,编译器在编译时就已经决定了将调用哪个函数。因此,生成的汇编代码中会直接包含对特定函数版本的调用指令。这些调用是直接的,意味着没有运行时查找或决策过程。

动态多态(虚函数)

当通过基类的指针或引用调用虚函数时,具体调用哪个函数(基类还是某个派生类中的重写版本)是在运行时通过虚表(vtable)来决定的。因此,在生成的汇编代码中,调用虚函数的过程包括了查找虚表的步骤,这是一个运行时的开销。

  • 在汇编代码中,每个含有虚函数的类都会有一个虚表。类的对象中包含了一个指向这个虚表的指针,也叫虚指针(通常是对象内存布局的第一个成员);
  • 调用虚函数时,汇编指令首先会通过对象的虚表指针访问虚表,然后根据函数在虚表中的偏移找到具体的函数地址,最后跳转到这个地址执行函数;
  • 这意味着,与静态多态相比,动态多态的函数调用包含了额外的间接寻址步骤,这是实现运行时多态性的开销。
  • 这个过程涉及到间接跳转,因为实际的函数地址在程序运行前是不确定的,只有在运行时,通过对象的实际类型确定。

举个例子

#include <iostream>
class Base {
public:virtual void show() {std::cout << "Base show" << std::endl;}
};class Derived : public Base {
public:void show() override {std::cout << "Derived show" << std::endl;}
};void display(Base* b) {b->show(); // Dynamic polymorphism
}int main() {Base b;Derived d;display(&b);display(&d);return 0;
}

对于含虚函数的类,会生成一个虚表(vtable)。在汇编代码中,通过基类指针调用虚函数show时,会首先从对象的内存布局中取出虚表指针,然后通过这个虚表指针加上show函数的偏移量找到实际应该调用的函数地址,最后通过这个地址跳转到函数实现。这个过程涉及几个间接访问步骤,体现了运行时多态性的实现。

虚函数表

什么是虚函数表

虚函数表(Virtual Function Table,通常缩写为vtable)是C++用来支持动态多态性的一种机制。每个含有虚函数的类都会有一个虚函数表,这个表在编译时被创建,用于运行时解析虚函数的调用。

虚函数表用于存储指向类的虚函数实现的指针。当一个类的对象需要调用虚函数时,程序会通过这个对象的虚函数表来确定具体应该调用哪个函数实现。这个机制允许在运行时根据对象的实际类型来动态绑定函数调用,而不是在编译时静态确定。

当类中声明了虚函数时,编译器会为该类生成一个虚函数表。如果派生类覆盖了基类中的虚函数,派生类的虚函数表中会放置指向派生类函数实现的指针。

当创建一个对象时,编译器会在对象内存布局中设置一个指针(vptr),指向该对象类的虚函数表。这样,每个对象都能通过vptr来访问其虚函数表。

当通过基类的指针或引用调用虚函数时,程序会使用指针/引用所指对象的vptr来访问虚函数表,然后通过表中的地址找到并调用正确的函数实现。

在下面这个例子中,Base类有一个虚函数func(),而Derived类重写了这个函数。编译器为Base和Derived各自生成一个虚函数表。Derived对象的vptr指向Derived的虚函数表,因此当调用func()时,即使通过Base类型的指针进行调用,也会执行Derived中的实现。

class Base {
public:virtual void func() { std::cout << "Base::func()" << std::endl; }
};class Derived : public Base {
public:void func() override { std::cout << "Derived::func()" << std::endl; }
};Base* obj = new Derived();
obj->func();  // Calls Derived::func() at runtime
虚函数表的数量
  • 在C++中,每个含有虚函数的类(包含虚函数的类以及其派生类)都会有一个虚函数表。这个表是类级别的,不是对象级别的,意味着同一个类的所有对象共享同一个虚函数表;
  • 如果一个类没有虚函数,那么它就没有虚函数表;
  • 虽然虚函数表是类级别的,但是每个含有虚函数的类的对象都会存储一个指向其类虚函数表的指针(通常称为vptr)。这个指针是对象级别的,是对象内存布局的一部分,通常位于对象数据的开始位置;
  • 并不是每个对象都有一个完整的虚函数表,而是每个对象有一个指向其类虚函数表的指针;
  • 派生类对象同样包含一个指向其虚函数表的指针。如果派生类没有新增虚函数并且没有覆盖基类的虚函数,它可能会使用与基类相同的虚函数表。但通常情况下,派生类至少会覆盖一些基类的虚函数,因此它会有自己的虚函数表,其中包含指向派生类特定实现的指针;
  • 如果派生类新增了虚函数,这些函数也会被加入到派生类的虚函数表中;
  • 当派生类覆盖了基类中的虚函数,派生类的虚函数表会更新为指向派生类中新的函数实现;
  • 当派生类继承自一个含有虚函数的基类时,基类的虚函数(不论是否被派生类重写)在派生类中仍然是虚函数。如果派生类没有显式重写这些虚函数,它们会保留基类的行为;
  • 即便派生类没有新增或重写任何虚函数,派生类的对象仍然会包含一个虚表(vtable),这个虚表中会包含指向基类虚函数实现的指针,除非派生类提供了自己的实现来覆盖这些函数;
  • 派生类的虚表(如果派生类提供了至少一个新的虚函数或重写了基类的虚函数)可能会与基类的虚表不同。如果派生类没有提供任何新的虚函数实现,它的虚表会与基类相同或非常类似,但仍然会因为继承了虚函数而被视为含有虚函数的类;
  • 当通过基类的指针或引用调用虚函数时,如果操作的对象实际上是派生类的实例,如果派生类没有重写那个虚函数,程序运行时也会调用基类中定义的虚函数实现;
  • 如果未来派生类被修改以重写这些虚函数,不需要改动使用基类指针或引用的代码,就可以实现不同的行为,这正体现了多态性的强大之处。

综上所述,即使派生类没有重写基类的任何虚函数,它仍然是含有虚函数的类,并且通过继承维持了与基类相同的多态性。这种设计允许派生类在不修改基类接口的前提下,增加或改变功能,提供了极大的灵活性和扩展性。

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

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

相关文章

大创项目推荐 深度学习 opencv python 实现中国交通标志识别

文章目录 0 前言1 yolov5实现中国交通标志检测2.算法原理2.1 算法简介2.2网络架构2.3 关键代码 3 数据集处理3.1 VOC格式介绍3.2 将中国交通标志检测数据集CCTSDB数据转换成VOC数据格式3.3 手动标注数据集 4 模型训练5 实现效果5.1 视频效果 6 最后 0 前言 &#x1f525; 优质…

VSCode在文件生成添加作者,创建时间、最后编辑人和最后编辑时间等信息

一、安装插件 我使用的是 korofileheader 二、配置文件 左下角点击设置图标—设置—输入"ext:obkoro1.korofileheader"—点击"在setting.json中编辑" 进入后会自动定位到你添加信息的地方 "Author": "tom", "Date": "…

PS从入门到精通视频各类教程整理全集,包含素材、作业等(5)

PS从入门到精通视频各类教程整理全集&#xff0c;包含素材、作业等 最新PS以及插件合集&#xff0c;可在我以往文章中找到 由于阿里云盘有分享次受限制和文件大小限制&#xff0c;今天先分享到这里&#xff0c;后续持续更新 初寒调色案例及练习图 等文件 https://www.alipan.…

JimuReport积木报表 v1.7.4 正式版本发布,免费的JAVA报表工具

项目介绍 一款免费的数据可视化报表&#xff0c;含报表和大屏设计&#xff0c;像搭建积木一样在线设计报表&#xff01;功能涵盖&#xff0c;数据报表、打印设计、图表报表、大屏设计等&#xff01; Web 版报表设计器&#xff0c;类似于excel操作风格&#xff0c;通过拖拽完成报…

大数据量查询语句优化

测试单表模糊查询&#xff0c;符合条件的数量为&#xff1a; -- 查看总共有多少条数据 select count(0) from "REGISTER_HOUSE_INFO" where SEAT_NAME like %1% ;未优化&#xff1a;测试单表模糊查询分页&#xff0c;符合条件的数据为&#xff1a; select * from …

【Linux2】Linux的权限

思维导图 学习内容 在介绍完一些基本指令后&#xff0c;我们需要进行对权限以后一个全新的认识&#xff0c;比如文件的权限、目录的权限等等…… 学习内容 通过上面的学习目标&#xff0c;我们可以列出要学习的内容&#xff1a; shell命令以及运行原理Linux权限的概念Linux权…

【Frida】【Android】06_夜神模拟器中间人抓包

&#x1f6eb; 系列文章导航 【Frida】【Android】01_手把手教你环境搭建 https://blog.csdn.net/kinghzking/article/details/136986950【Frida】【Android】02_JAVA层HOOK https://blog.csdn.net/kinghzking/article/details/137008446【Frida】【Android】03_RPC https://bl…

EasyExcel 复杂表头的导出(动态表头和静态表头)

问题&#xff1a;如图&#xff0c;1部分的表头是动态的根据日期变化&#xff0c;2部分是数据库对应的字段&#xff0c;静态不变的&#xff1b; 解决方案&#xff1a;如果不看1的部分&#xff0c;2部分内容可以根据实体类注解的方式导出&#xff0c;那么我们是不是可以先将动态表…

vivado 配置存储器器件编程2

为双 QSPI (x8) 器件创建配置存储器文件 您可使用 write_cfgmem Tcl 命令来为双 QSPI (x8) 器件生成 .mcs 镜像。此命令会将配置数据自动拆分为 2 个独立 的 .mcs 文件。 注释 &#xff1a; 为 SPIx8 生成 .mcs 时指定的大小即为这 2 个四通道闪存器件的总大小。…

[leetcode]剑指 Offer 29. 顺时针打印矩阵

前言&#xff1a;剑指offer刷题系列 问题&#xff1a; 输入一个矩阵&#xff0c;按照从外向里以顺时针的顺序依次打印出每一个数字。 示例&#xff1a; 输入&#xff1a;matrix [[1,2,3],[4,5,6],[7,8,9]] 输出&#xff1a;[1,2,3,6,9,8,7,4,5]思路&#xff1a; 看到这个…

标准版IP地址证书

IP地址证书是一种网络安全工具&#xff0c;用于确保互联网通信中IP地址的所有权和真实性。它类似于为网站颁发的SSL/TLS证书&#xff0c;但专门针对IP地址。这种证书由受信任的第三方机构&#xff08;如证书颁发机构&#xff09;签发&#xff0c;包含公钥、所有者信息和有效期。…

Php_Code_challenge18

题目&#xff1a; 答案&#xff1a; 解析&#xff1a; strcmp遇到特殊类型数据返回0&#xff0c;MD5函数遇到特殊数据类型会返回0&#xff0c;弱类型可用16进制绕过。

el-row一行放置3个el-col,有时出现空行现象

利用el-row的type属性&#xff0c;将type设置为flex&#xff0c;启用flex布局&#xff0c;再设置justify和align属性&#xff0c;即可避免该问题出现。 <el-row type"flex" class"row-bg"><el-col :span"6"><div class"gri…

数据结构之栈

栈的概念 栈是一种遵从后进先出原则的有序集合添加新元素的一端称为栈顶&#xff0c;另一端称为栈底操作栈的元素时&#xff0c;只能从栈顶操作&#xff08;添加、移除或取值&#xff09; 栈的实现 push() 入栈方法pop() 出栈方法top() 获取栈顶值size() 获取栈的元素个数cle…

3月27日 磁盘操作Linux下安装MySQL

3月27日 磁盘操作&&Linux下安装MySQL 一&#xff0e; 增加磁盘 增加了2个磁盘&#xff0c;然后重启系统。 #lsblk 可用于查看所有设备挂载情况 注意&#xff1a;增加了2块硬盘&#xff0c;分别是sdb和sdc,但是这2块硬盘没有分区也没有格式化&#xff0c;无法使用 Li…

使用pillow创建动态图形验证码

使用pillow创建动态图形验证码 #安装pillow模块&#xff1a;pip3 install pillow from PIL import Image,ImageDraw,ImageFont import random import stringdef id_code(width,height,bit,font_file,font_size):"""功能&#xff1a;生成随机图片验证码:param w…

Java毕业设计-基于Spring Boot的在线考试系统-毕业论文+答辩ppt(附源代码+演示视频)

文章目录 前言一、毕设成果演示&#xff08;源代码在文末&#xff09;二、毕设摘要展示1、开发说明2、需求分析3、系统功能结构 三、系统实现展示1、系统登录注册2、管理员功能模块3、用户功能模块 四、毕设内容和源代码获取总结 Java毕业设计-基于Spring Boot的在线考试系统-毕…

如何用Git在终端以可视化的方式查看提交情况

2024年4月2日&#xff0c;周二上午 要使用 Git 查看树状提交情况&#xff0c;可以使用 git log 命令的 --graph 选项。这个选项会以树状图的形式显示提交历史&#xff0c;清晰地展示每个提交的分支合并情况和分支间的关系。 git log --graph 除了 --graph 选项外&#xff0c;还…

如何将Maven与TestNG集成

我们已经讨论了如何在maven中执行单元测试用例&#xff0c;但那些是JUnit测试用例&#xff0c;而不是TestNG。当maven使用“mvn test”命令进入测试阶段时&#xff0c;这些用例被执行。 本文将介绍如何将Maven与TestNG集成&#xff0c;并在maven进入测试阶段时执行TestNG测试。…

【Linux】开始掌握进程控制吧!

送给大家一句话&#xff1a; 我并不期待人生可以一直过得很顺利&#xff0c;但我希望碰到人生难关的时候&#xff0c;自己可以是它的对手。—— 加缪 开始学习进程控制 1 前言2 进程创建2.1 fork函数初识2.2 fork函数返回值2.3 写时拷贝2.4 fork常规用法2.5 fork调用失败的原因…