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

【C++闯关笔记】详解多态

系列文章目录

上一篇笔记:【C++闯关笔记】map与set底层:二叉搜索树-CSDN博客


文章目录

目录

系列文章目录

文章目录

前言

一、多态是什么?

1.多态的概念

2.多态实现条件

二、多态的一些细节

1.虚函数的传染性与 override

2.虚函数重写的一些其他问题

1)多态特例:协变

2)析构函数的重写

3)重载、重写、重定义的细节对比

3.纯虚函数与抽象类

二、多态的原理

1.虚函数指针

2.虚函数表

本文总结



前言

        作为C++面对对象编程三架马车的最后一架,多态因为站在前两者的肩膀上,所以比之前的内容知识都略显晦涩。本文将深入讨论多态,从知晓多态的概念到深入理解原理再到最后的实践运用。


一、多态是什么?

1.多态的概念

多态的概念:简单来说,就是多种形态,分为编译时能明确的称为为静态,运行时才能明确的角动态。即多态分为编译时多态(静态多态)和运行时多态(动态多态)

编译时多态(静态多态):函数重载和函数模板,他们传不同类型的参数就可以调用不同的函数,通过参数不同达到多种形态,之所以叫编译时多态。

运行时多态(动态多态):执行某个函数(行为)时,不同的对象会有不同的结果发生。比如买票这个行为:同样是买票这个行为,成人买成人票,儿童买儿童票,学生买学生票。

        静态多态函数重载和函数模板在之前的笔记中已经介绍过,这里就不再赘述,本文主要介绍动态多态。

2.多态实现条件

        多态在C++中是一个继承关系的特殊情况,当父类对象与子类对象去调用同一函数,会产生不同的行为。

实现多态有三个必要条件

①类与类之间满足继承关系;

②被调用的函数必须是虚函数,并且完成了虚函数重写/覆盖;

③必须是基类的指针或者引用调用虚函数。

 让我们来解释一下:

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

class A
{
public: virtual void test1(){cout << "A: test1" << endl;}
private:int _a;char _b;
};

②重写/覆盖:派生类中有一个跟基类完全相同的虚函数(即函数的返回值类型、函数名字、参数类型与个数完全相同)但函数体实现的行为不同,从而隐藏了基类的版本,称派生类的虚函数重写了基类的虚函数。

class A
{
public: virtual void test1(){cout << "A: test1" << endl;}
private:int _a;char _b;
};class C :public A
{
public:virtual void test1(){cout << "C: test1" << endl;}
private:double _c;
};

二、多态的一些细节

1.虚函数的传染性与 override

        在C++中,一旦一个函数在基类中被声明为virtual,它在所有派生类中都将保持虚函数的特性,无论你是否在子类中显式地写上virtual关键字。

        也就是说在派生类中可以不用每次都频繁费事的写virtual关键字,但这带来了一些潜在问题:

class Base 
{
public:virtual void test(int x) {cout << "Base::test(int)" << endl;}
};class Derived : public Base 
{
public:// 程序员本意是想重写覆盖,但写错了函数名!void tset(intx) {  cout << "Derived::test(int)" << endl;}// 这实际上没有覆盖Base::test(int),而是创建了一个新函数!
};

        像上述写错函数名/变量名这样的小错误,在项目中却容易引起极大的问题。于是自C++11引入了override 关键字。

class Derived : public Base 
{
public://使用override,让编译器帮我们检查void tset(int x) override //// 编译器会报错{  cout << "Derived::test(int)" << endl;}
};

所以派生类中

  • 可以省略virtual关键字(因为会自动继承虚特性)

  • 但一定要使用override关键字

  • 对于不希望被进一步重写的函数,可以在基类中可以添加final关键字。

class A
{
public: virtual void test2()final{cout << "A: test2" << endl;}
private:int _a;char _b;
};

2.虚函数重写的一些其他问题

1)多态特例:协变

        原本多态要求重写虚函数时,虚函数的返回值类型要一样,但却有个例外:协变。

        协变,即派生类重写基类虚函数时,与基类虚函数返回值类型是不同的:基类虚函数返回基类类型(或其指针/引用),派生类虚函数返回派生类类型(或其指针/引用),并且这些类型之间存在继承关系,这就构成了协变。

        关键限制:必须是指针或引用类型,不能是按值返回。

使用场景与用法如下

引入协变前:

class Animal 
{
public:virtual Animal* create() {return new Animal();}
};class Dog : public Animal 
{
public:// 如果不支持协变,我们只能这样写:Animal* create() override {return new Dog();  // 必须返回Animal*,即使创建的是Dog}
};void example()
{Dog dog;Animal* animal = dog.create();  // 返回的是Animal*// 想要调用Dog特有方法,必须进行向下转型Dog* dogPtr = dynamic_cast<Dog*>(animal);
}

引入协变后:

class Animal 
{
public:virtual Animal* create() {return new Animal();}
};class Dog : public Animal 
{
public:// 支持协变,可以直接返回Dog*Dog* create() override {return new Dog();}
};void example() 
{Dog dog;Dog* dogPtr = dog.create();  // 直接得到Dog*,不需要转型Animal* animal = dog.create();  // 也可以赋值给基类指针
}

2)析构函数的重写

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

        为什么要这样做?

        当一个基类的指针或者引用指向一个派生类对象,若派生类准备析构时会调谁的析构函数?基类,还是派生类?实际上如果基类没有将析构函数函数设置为虚函数,那么调用的是基类的析构函数,此时若派生类对象中有额外资源申请,由于没有调用派生类析构函数就造成了资源泄漏

        对构函数的名称统一处理的目的,正是为了在多态情况下能正常析构派生类对象。

3)重载、重写、重定义的细节对比

3.纯虚函数与抽象类

        在虚函数的后面写上=0,则这个函数为纯虚函数,纯虚函数可以有定义实现(实现没啥意义因为要被派生类重写),但一般只要声明即可。包含纯虚函数的类叫做抽象类,抽象类不能实例化出对象,如果派生类继承后不重写纯虚函数,那么派生类也是抽象类。

class A
{
public: virtual void test() = 0;
public:char _a;int _b;
};

二、多态的原理

1.虚函数指针

下面代码的输出结果是?

class A
{
public:virtual void test(){std::cout << "A" << std::endl;}
public:char _a;int _b;
};int main()
{A a;std::cout << sizeof(a) << std::endl;return 0;
}

        由于内存对齐,在32位环境下打印出来应该是8字节(1Byte的_a,填充3字节,4Byte的_b,可点击前方蓝字查看内存对齐规则)。可实际打印出的结果是?

虚函数表指针

        除了_a和_b成员,还多一个__vfptr放在对象的前面(有些平台可能会放到对象的最后面),对象中的这个指针我们叫做虚函数表指针。一个含有虚函数的类中都至少都有一个虚函数表指针。

        所以a对象的大小打印出来是12字节(4Byte的指针,1Byte的_a,填充3字节,4Byte的_b)。

2.虚函数表

        虚函数表指针指向的自然就是虚函数表了,虚函数表中存放着一个类所有虚函数的地址,包括从基类继承的,和本类中函数前加virtual的,虚函数表也简称虚表。

这个虚函数表有什么作用呢?

        当满足多态条件后,编译器不再是编译时就确定函数的地址,而是在运行时到指向的对象的虚函数表中查找对应的虚函数的地址。若派生类不重写虚函数,那么程序运行时派生类对象与基类对象调用的就是同一个函数地址;若派生类重写虚函数,那么新的虚函数地址就会覆盖继承下来的虚函数地址(这也是为什么叫覆盖的原因),达到同样的函数实现不同行为(因为实际上执行的函数体不同)。

        这样就实现了指针或引用指向基类就调用基类的虚函数,指向派生类就调用派生类对应的虚函数。

虚函数表的有关细节

①同类型的对象共用同一张虚表,不同类型的对象各自有独立的虚表,所以可以理解为每个类都有自己的虚函数表;

②派生类继承下来的基类中有虚函数表指针,自己就不会再生成虚函数表指针。但是当派生类构造完成后,这个继承下来的虚函数表指针就不再指向基类的虚函数表,而是派生类自己的虚函数表。

③虚函数和普通函数一样的,都是存在代码段的;

④虚函数表存放在代码段,也就是常量区中。


本文总结

本文深入探讨C++多态机制,重点分析动态多态的实现原理。

本文先介绍了多态的概念与实现条件,之后介绍了多态的一些细节如协变、析构函数重写、纯虚函数等,最后通过虚函数表解开多态的原理。

读完点赞,手留余香~

http://www.dtcms.com/a/524836.html

相关文章:

  • 数据库技术指南(二):MySQL CURD 与高级查询实战
  • 用mvc做网站报告做做做网站
  • 设置一个自定义名称的密钥,用于 git 仓库上下传使用
  • MAC Flood与ARP Flood攻击区别详解
  • 高兼容与超低延迟:互联网直播点播平台EasyDSS直播服务如何成为直播点播应用的“技术底座”?
  • MongoDB 集群优化实战指南
  • wordpress网站速度检测医院做网站需要多少钱
  • iOS 26 查看电池容量与健康状态 多工具组合的工程实践
  • 机器学习(10)L1 与 L2 正则化详解
  • 保险网站建设平台与别人相比自己网站建设优势
  • vscode中好用的插件
  • PCB过电流能力
  • 【数据库】KingbaseES数据库:首个多院区异构多活容灾架构,浙人医创新开新篇
  • 嵌入式软件算法之PID闭环控制原理
  • 性价比高seo网站优化免费下载模板的网站有哪些
  • 无棣网站制作襄樊网站制作公司
  • AI服务器工作之电源测试
  • 《Muduo网络库:实现Acceptor类》
  • 第十三篇《TCP的可靠性:三次握手与四次挥手全解析》
  • SSE 流式响应实战:如何在 JavaScript 中处理 DeepSeek 流式 API
  • 在线阅读网站开发教程品牌建设促进会是什么工作
  • 一站式服务门户网站充值支付宝收款怎么做
  • 网站建设超速云免费小程序源码php
  • 如何裁剪u-boot,保留其必要功能,使体积尽可能小
  • 借助智能 GitHub Copilot 副驾驶 的 Agent Mode 升级 Java 项目
  • 广州市网站建设 乾图信息科技在哪里建网站
  • Flutter---自定义日期选择对话框
  • 怎么代码放到网站上网站建设需要的公司
  • k8s部署容器化应用-tomcat
  • AI开发工具实战解析:如何实现企业数据处理流程自动化