C++多态

在这里插入图片描述

文章目录

  • 🐵1. 什么是多态
  • 🐶2. 构成多态的条件
    • 🐩2.1 虚函数
    • 🐩2.2 虚函数的重写
    • 🐩2.3 final 和 override关键字
    • 🐩2.4 重载、重写、重定义对比
  • 🐱3. 虚函数表
  • 🐯4. 多态的原理
  • 🐎5. 多继承的虚表关系
  • 🦬6. 抽象类

🐵1. 什么是多态

当下网络有个热门词汇叫“双标”,意思就是用不同的标准来衡量人或事,这是一个贬义词。而在编程世界中,这种“双标”,我们称之为多态,当然了这里的多态并不是贬义词,而是一种技术实现。

比如说某种商城有会员机制,将用户分为普通用户、普通会员、尊贵会员等

那买同种东西的时候,不同的用户等级会有着不同的价格,这就是一种多态行为

🐶2. 构成多态的条件

实现多态性的主要构成条件是使用虚函数继承

  • 必须通过基类的指针或引用调用虚函数
  • 被调用的函数必须是虚函数,且派生类必须对虚函数进行重写

🐩2.1 虚函数

只有类的成员函数才能被定义为虚函数,格式如下:

class A
{//函数前面加上virtual 表面该成员函数为虚函数virtual void func() {}
};

🐩2.2 虚函数的重写

当派生类中有一个和基类完全相同的虚函数时,我们称这为虚函数的重写/覆盖

重写有三同,即:返回值类型、函数名、参数列表完全相同

class A
{
public://虚函数virtual void func() const{cout << "A->func()" << endl;}
};
class B :public A
{
public://虚函数重写virtual void func() const{cout << "B->func()" << endl;}
};
//多态调用传引用过去
void Print(const A& p)
{p.func();
}
int main()
{Print(A());	//A->func()Print(B());	//B->func()return 0;
}

多态调用中,看的是指向的对象;而普通的函数调用,看的是当前的类型

image-20230814125151703

虚函数的重写,还需注意几点:

  1. 虚函数父类必须加上virtual修饰,子类虚函数重写前面可以不加virtual,但在实际中,还是建议加上

    image-20230814125537847

  2. 对于虚函数的重写,我们规定三同,但是有例外——协变

    即基类与虚函数返回值类型不同,但是返回值类型必须是构成父子关系指针或者引用(同时是指针 或 同时是引用)

    class A
    {
    public://虚函数virtual A* func() const{cout << "A->func()" << endl;return 0;}
    };
    class B :public A
    {
    public://虚函数重写 B和A是父子关系virtual B* func() const{cout << "B->func()" << endl;return 0;}
    };
    void Print(const A& p)
    {p.func();
    }
    int main()
    {Print(A());Print(B());return 0;
    }
    
  3. 析构函数的重写,基类和派生类的析构函数名不同

    class A
    {
    public://虚函数virtual ~A(){cout << "~A()" << endl;}
    };
    class B :public A
    {
    public://虚函数重写virtual ~B(){cout << "~B()" << endl;}
    };
    int main()
    {A* a1 = new A;A* a2 = new B;delete a1;delete a2;return 0;
    }
    

    输出:image-20230814130832375

    这里的原因是因为编译器对析构函数的名字做了处理,编译后名称统一处理为destructor,那为什么要将析构函数统一处理称destructor呢?因为这里要让他们构成重写。如果不构成重写,就好出现类似这样的情况:

    class A
    {
    public:~A(){cout << "~A()" << endl;}
    };
    class B :public A
    {
    public:~B(){delete ptr;cout << "~B()" << endl;}
    protected:int* ptr;
    };
    int main()
    {A* a1 = new A;delete a1;a1 = new B;delete a1;return 0;
    }
    

    输出发现,我们这里new了一个B对象,但是每次都是调用A的析构函数,这显然与我们的意愿不符,我们期望的是这个a1->destructor形成的是多态调用,所以这样统一处理之后,就可以让他们构成重写

image-20230814134515099

🐩2.3 final 和 override关键字

如果不想让这个虚函数被重写,可加上final关键字修饰

image-20230814141838376

当然了,final也可以修饰类,让这个类不被继承,一般用于最终的类

如果要检查某个派生类是否重写了基类的某个虚函数,可用override关键字修饰,如果没有重写,则编译报错

🐩2.4 重载、重写、重定义对比

image-20230814143143672

🐱3. 虚函数表

class A
{
public:virtual void func(){cout << "func()" << endl;}
protected:int _a;
};
int main()
{cout << sizeof(A) << endl;
}

这段代码如果不加上virtual,则输出的是4;但是加上virtual之后,输出的是16(64位下,指针是8字节,然后内存对齐)image-20230814144214403

这是因为有了虚函数,这个类里面会多一个虚函数表的指针,这些表里面存的是虚函数的地址

image-20230814144727594

但如果将这个虚函数没有被重写,那么派生类的虚函数表还是指向基类的虚函数;如果重写了,则指向重写的虚函数。

image-20230814145808846

所以多态调用的时候,不管我们传的是基类和派生类,在内存里看到的都是父类;普通调用是在编译的时候就确定了地址,而多态调用时,运行时会到指向对象的虚表找函数的地址

动态绑定与静态绑定:

  • 静态绑定:在编译时确定调用哪个函数或方法。这是在编译器根据变量的静态类型(声明类型)来决定调用哪个函数
  • 动态绑定:在运行时根据对象的实际类型来确定调用哪个函数或方法。这是通过虚函数(在基类中声明为虚函数,子类进行重写)实现的。动态绑定适用于通过基类指针或引用调用虚函数的情况,确保调用正确的派生类函数

image-20230814154255881

在这里虚表的地址,是存储在哪里的呢?我们通过这段代码来验证

class A
{
public:virtual void func(){cout << "A->func()" << endl;}virtual void Func(){cout << "A->Func()" << endl;}int _a;
};
class B :public A
{ 
public:virtual void func(){cout << "B->func()" << endl;}
};
void Print(A a)
{a.func();
}
int main()
{A aa;B bb;int a = 0;printf("栈:%p\n", &a);static int b = 0;printf("静态区:%p\n", &b);int* p = new int;printf("堆:%p\n", p);const char* str = "hello";printf("常量区:%p\n", str);//前四个字节,一定是虚表的地址printf("虚表a:%p\n", *((int*)&aa));printf("虚表b:%p\n", *((int*)&bb));
}

输出发现虚表的地址和常量区的地址隔的较近,所以我们可以得出结论:虚表的地址存储在常量区

image-20230814223647957

另外,我们在Vs的监视窗口只能查看3个虚函数的地址,但这不代表这,内存里面只有三个虚函数的地址,我们可通过这段代码进行验证:

class A
{
public:virtual void func1(){cout << "A->func1()" << endl;}virtual void func2(){cout << "A->func2()" << endl;}virtual void func3(){cout << "A->func3()" << endl;}
};
class B :public A
{virtual void func3(){cout << "B->func3()" << endl;}virtual void func4(){cout << "B->func4()" << endl;}
};
//函数指针命名
typedef void (*Func_Ptr)();
//打印函数指针数组
void PrintVFT(Func_Ptr table[])
{for (size_t i= 0; table[i]!=nullptr ; i++){printf("[%d]:%p->", i, table[i]);Func_Ptr f = table[i];f();}printf("\n");
}
int main()
{A a;B b;int vft1 = *((int*)&a);PrintVFT((Func_Ptr*)vft1);int vft2 = *((int*)&b);PrintVFT((Func_Ptr*)vft2);return 0;
}

🐯4. 多态的原理

有了虚表的概念,这我们就能理解,为什么构成多必须是通过基类的指针或引用调用虚函数。因为只有父类的虚表才能既能指向父类,又能指向子类。

那这里还有一个问题就是,为什么必须是指针或引用呢?

class A
{
public:virtual void func(){cout << "A->func()" << endl;}virtual void Func(){cout << "A->Func()" << endl;}int _a;
};
class B :public A
{
public:virtual void func(){cout << "B->func()" << endl;}
};
void Print(A a)
{a.func();
}
int main()
{A a;a._a = 1;B b;b._a = 10;a = b;A* pa = &b;A& ref = b;
}

这段代码调试发现,子类赋值给父类,父类会进行切片,这里值会拷贝过去,但是虚表并不会拷贝;因为如果拷贝了虚表的话,这样父类对象中的虚表指向的是父类还是子类就混淆了

🐎5. 多继承的虚表关系

上面讲的内容,包括举得例子都是单继承的,所以就不再赘述。这里我们看一下多继承里面的虚表是怎样的

class A
{
public:virtual void func1(){cout << "A->func1()" << endl;}virtual void func2(){cout << "A->func2()" << endl;}
protected:int _a;
};
class B
{
public:virtual void func1(){cout << "B->func1()" << endl;}virtual void func2(){cout << "B->func2()" << endl;}
protected:int _b;
};
class C :public A, public B
{
public:virtual void func1(){cout << "C->func1()" << endl;}virtual void funcC(){cout << "C->funcC()" << endl;}
protected:int _c;
};
typedef void (*Func_Ptr)();
//打印函数指针数组
void PrintVFT(Func_Ptr table[])
{for (size_t i= 0; table[i]!=nullptr ; i++){printf("[%d]:%p->", i, table[i]);Func_Ptr f = table[i];f();}printf("\n");
}
int main()
{C c;cout<<sizeof(c)<<endl;int vft1 = *((int*)&c);//int vft2 = *((int*)(char*)&c + sizeof(A));B* ptr = &c;int vft2 = *((int*)ptr);PrintVFT((Func_Ptr*)vft1);PrintVFT((Func_Ptr*)vft2);
}

通过验证,我们可以发现,C类里面有两张虚表,一张是A的,一张是B的。而C里面的虚函数funcC()的虚表,是存放在第一张虚表里面

image-20230815003928251

但是,我们这里发现,重写的func1()函数,明明是一样的,但是地址却不一样,我们这段代码转到汇编代码查看

int main()
{C c;A* ptr1 = &c;B* ptr2 = &c;ptr1->func1();ptr2->func1();return 0;
}

我们发现,ptr1是直接调用找个func1(),而ptr2最终调用的地址和ptr1是一样的,但是在jump的,寄存器减了一个8,这个减8正好是c的地址。ptr1不用修改是因为正好指向了c的起始地址,内存不看类型,只看地址

image-20230815010438247

菱形继承这里就不讲了,很混乱~

🦬6. 抽象类

虚函数后面加上=0,则这个函数为纯虚函数,包含了纯虚函数的类,叫做抽象类

抽象类不能实例化出对象,之后继承的派生类也不能实例化对象,只能重写虚函数,派生类才能实例化出对象。这里规定了派生类必须重新虚函数,所以抽象类也叫接口类

class A
{
public:virtual void func() = 0;
};
class B :public A
{
public:virtual void func(){cout << "B->func()" << endl;}
};
class C :public A
{
public:virtual void func(){cout << "C->func()" << endl;}
};
void Func(A*a)
{a->func();
}
int main()
{Func(new B);Func(new C);return 0;
}

那么本期的分享就到这里咯,我们下期再见,如果还有下期的话。

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

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

相关文章

【变形金刚01】attention和transformer所有信息

图1.来源&#xff1a;Arseny Togulev在Unsplash上的照片 一、说明 这是一篇 长文 &#xff0c;几乎讨论了人们需要了解的有关注意力机制的所有信息&#xff0c;包括自我注意、查询、键、值、多头注意力、屏蔽多头注意力和转换器&#xff0c;包括有关 BERT 和 GPT 的一些细节。因…

OpenCV图像处理——轮廓检测

目录 图像的轮廓查找轮廓绘制轮廓 轮廓的特征轮廓面积轮廓周长轮廓近似凸包边界矩形最小外接圆椭圆拟合直线拟合 图像的矩特征矩的概念图像中的矩特征 图像的轮廓 查找轮廓 binary,contours,hierarchycv.findContours(img,mode,method)绘制轮廓 cv.drawContours(img,coutours…

印度货代专线【我国到印度专线有哪些方式】

随着全球贸易的不断发展&#xff0c;我国与印度之间的贸易往来也日益频繁。作为两个人口最多的国家之一&#xff0c;中国和印度之间的货物运输需求不断增长。为了满足这一需求&#xff0c;印度货代专线应运而生&#xff0c;为进出口商提供高效、可靠的货物运输服务。本文将探索…

939. 最小面积矩形;2166. 设计位集;2400. 恰好移动 k 步到达某一位置的方法数目

939. 最小面积矩形 核心思想&#xff1a;枚举矩形的右边那条边的两个点&#xff0c;并用一个哈希表存储相同纵坐标的最近出现的列的列数,不断更新最近出现的左边那条边。 2166. 设计位集 核心思想&#xff1a;这题主要是时间复杂度的优化&#xff0c;用一个flag来标记当前翻转…

CSS自学框架之表单

首先我们看一下表单样式&#xff0c;下面共有5张截图 一、CSS代码 /*表单*/fieldset{border: none;margin-bottom: 2em;}fieldset > *{ margin-bottom: 1em }fieldset:last-child{ margin-bottom: 0 }fieldset legend{ margin: 0 0 1em }/* legend标签是CSS中用于定义…

IOS开发-XCode14介绍与入门

IOS开发-XCode14介绍与入门 1. XCODE14的小吐槽2. XCODE的功能bar一览3. XCODE项目配置一览4. XCODE更改DEBUG/RELEASE模式5. XCODE单元测试 1. XCODE14的小吐槽 iOS开发工具一直有个毛病&#xff0c;就是新版本的开发工具的总会有一些奇奇怪怪的bug。比如在我的Mac-Pro&#…

Springboot 实践(3)配置DataSource及创建数据库

前文讲述了利用MyEclipse2019开发工具&#xff0c;创建maven工程、加载springboot、swagger-ui功能。本文讲述创建数据库&#xff0c;为项目配置数据源&#xff0c;实现数据的增删改查服务&#xff0c;并通过swagger-ui界面举例调试服务控制器 创建数据库 项目使用MySQL 8.0.…

vue基础知识四:Vue实例挂载的过程

一、思考 我们都听过知其然知其所以然这句话 那么不知道大家是否思考过new Vue()这个过程中究竟做了些什么&#xff1f; 过程中是如何完成数据的绑定&#xff0c;又是如何将数据渲染到视图的等等 一、分析 首先找到vue的构造函数 源码位置&#xff1a;src\core\instance\…

一生一芯4——使用星火应用商店在ubuntu下载QQ、微信、百度网盘

星火应用商店可以非常方便的完成一些应用的下载&#xff0c;下面是官方网址 http://spark-app.store/download 我使用的是intel处理器&#xff0c;无需下载依赖项&#xff0c;直接点击软件本体 我这里下载amd64,根据自己的处理器下载对应版本 sudo apt install ./spark-stor…

数据结构入门指南:二叉树

目录 文章目录 前言 1. 树的概念及结构 1.1 树的概念 1.2 树的基础概念 1.3 树的表示 1.4 树的应用 2. 二叉树 2.1 二叉树的概念 2.2 二叉树的遍历 前言 在计算机科学中&#xff0c;数据结构是解决问题的关键。而二叉树作为最基本、最常用的数据结构之一&#xff0c;不仅在算法…

java对大文件分片上传

这里记录一下&#xff0c;Java对大文件的切分&#xff0c;和后端接口分片上传的实现逻辑 正常&#xff0c;前后端分离的项目其实是前端去切分文件&#xff0c;后端接口接收到切分后的分片文件去合并&#xff0c;这里都用java来记录一下。特别说明&#xff1a;我这里用的是zip包…

ReactDOM模块react-dom/client没有默认导出报错解决办法

import ReactDOM 模块“"E:/Dpandata/Shbank/rt-pro/node_modules/.pnpm/registry.npmmirror.comtypesreact-dom18.2.7/node_modules/types/react-dom/client"”没有默认导出。 解决办法 只需要在tsconfig.json里面添加配置 "esModuleInterop": true 即…

【C++】queue容器

1.queue容器基本概念 2.queue常用接口 #include <iostream> using namespace std;//队列queue #include<queue>//创建Person类 class Person { public:Person(string name, int age){this->m_Name name;this->m_Age age;}string m_Name; //姓名int m_Age; …

优维低代码实践:自定义模板

优维低代码技术专栏&#xff0c;是一个全新的、技术为主的专栏&#xff0c;由优维技术委员会成员执笔&#xff0c;基于优维7年低代码技术研发及运维成果&#xff0c;主要介绍低代码相关的技术原理及架构逻辑&#xff0c;目的是给广大运维人提供一个技术交流与学习的平台。 优维…

禾赛科技Q2营收交付双新高,国产激光雷达从量变到质变

随着2022年激光雷达元年、2023年城市智能辅助驾驶&#xff08;NOA&#xff09;元年相继到来&#xff0c;激光雷达产业迎来爆发期。 今年以来&#xff0c;自动驾驶公司、汽车制造商以及移动出行公司等各路人马积极推动城市级别的智能辅助驾驶全面落地&#xff0c;北京、上海、深…

通过css设置filter 属性,使整个页面呈现灰度效果,让整个网页变灰

通过css设置filter 属性设置页面整体置灰 效果图: 通过设置 filter 属性为 grayscale(100%)&#xff0c;页面中的所有元素都会被应用灰色滤镜效果&#xff0c;使整个页面呈现灰度效果。 <style type"text/css"> html { filter: grayscale(100%); -webkit-f…

TB/TM-商品详情原数据(APP)

一、接口参数说明&#xff1a; item_get_app-获得TB/TMapp商品详情原数据&#xff0c;点击更多API调试&#xff0c;请移步注册API账号点击获取测试key和secret 公共参数 请求地址: https://api-gw.onebound.cn/taobao/item_get_app 名称类型必须描述keyString是调用key&…

考研 408 | 【计算机网络】 应用层

导图 网络应用模型 客户/服务器&#xff08;c/s&#xff09;模型 P2P模型 DNS 域名 域名服务器 域名解析过程 文件传输协议FTP FTP服务器和用户端 FTP工作原理 电子邮件 电子邮件的信息格式 组成结构 邮件服务器的功能&#xff1a; 1.发送&接收邮件 2.给发件人报告邮…

《游戏编程模式》学习笔记(四) 观察者模式 Observer Pattern

定义 观察者模式定义了对象间的一种一对多的依赖关系&#xff0c;当一个对象的状态发生改变时&#xff0c;所有依赖于它的对象都得到通知并被自动更新。 这是定义&#xff0c;看不懂就看不懂吧&#xff0c;我接下来举个例子慢慢说 为什么我们需要观察者模式 我们看一个很简…

如何在iPhone手机上修改手机定位和模拟导航?

如何在iPhone手机上修改手机定位和模拟导航&#xff1f; English Location Simulator&#xff08;定位模拟工具&#xff09; 是一款功能强大的 macOS 应用&#xff0c;专为 iPhone 用户设计&#xff0c;旨在修改手机定位并提供逼真的模拟导航体验。无论是为了保护隐私、测试位…