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.rhkb.cn/news/90168.html

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

相关文章

深入探究QCheckBox的三种状态及其用法

文章目录 引言&#xff1a;三种状态一、未选中状态&#xff08;0&#xff09;&#xff1a;二、选中状态&#xff08;2&#xff09;&#xff1a;三、部分选中状态&#xff08;1&#xff09;&#xff1a; 判断方法结论&#xff1a; 引言&#xff1a; QCheckBox是Qt框架中常用的复…

分支语句与循环语句(2)

3.3 do...while()循环 3.3.1 do语句的语法&#xff1a; do 循环语句; while(表达式); 3.3.2执行流程图&#xff1a; 3.3.3 do语句的特点 循环至少执行一次&#xff0c;使用的场景有限&#xff0c;所以不是经常使用。 打印1-10的整数&#xff1a; #define _CRT_SECURE_NO_WA…

网页显示摄像头数据的方法---基于web video server

1. 背景&#xff1a; 在ros系统中有发布摄像头的相关驱动rgb数据&#xff0c;需求端需要将rgb数据可以直接在网页上去显示。 问题解决&#xff1a; web_video_server功能包&#xff0c;相关链接&#xff1a; web_video_server - ROS Wiki 2. 下载&#xff0c;安装和编译&a…

ios swift5 collectionView 瀑布流(两列)

文章目录 1.瀑布流1.1 demo地址1.2 记得把部署的最低版本由8改成11,13甚至更高。不然编译会报错 2.动态计算图片和文字的高度 1.瀑布流 1.1 demo地址 CollectionViewWaterfallLayout - github 1.2 记得把部署的最低版本由8改成11,13甚至更高。不然编译会报错 2.动态计算图片和…

【Android Framework系列】第10章 PMS之Hook实现广播的调用

1 前言 前面章节我们学习了【Android Framework系列】第4章 PMS原理我们了解了PMS原理&#xff0c;【Android Framework系列】第9章 AMS之Hook实现登录页跳转我们知道AMS可以Hook拦截下来实现未注册Activity页面的跳转&#xff0c;本章节我们来尝试一下HookPMS实现广播的发送。…

Stable Diffusion + Deform制作指南

1.安装sd以及deform插件,更新后记得重启 需要安装ffmpeg https://ffmpeg.org/download.html 选择对应版本然后安装 如果是windows需要解压后将ffmpeg的bin目录配置在电脑的环境变量里面。 2.准备一张初始开始图片 3.填写参数,这里面参数要注意,宽高一定是32的倍数。如果填写…

matplotlib绘制位置-时序甘特图

文章目录 1 前言2 知识点2.1 matplotlib.pyplot.barh2.2 matplotlib.legend的handles参数 3 代码实现4 绘制效果5 总结参考 1 前言 这篇文章的目的是&#xff0c;总结记录一次使用matplotlib绘制时序甘特图的经历。之所以要绘制这个时序甘特图&#xff0c;是因为22年数模研赛C…

电力能源管理系统在生物制药行业的应用

安科瑞 华楠 摘要&#xff1a;根据生物制品类企业的电力能源使用特点&#xff0c;制定了符合公司实际情况的能源管理系统&#xff0c;介绍了该系统的架构及其在企业的应用情况&#xff0c;提升了公司能源数据的实时监控能力&#xff0c;优化了公司能源分配&#xff0c;降低了公…

【数据结构】八大排序详解

&#x1f680; 作者简介&#xff1a;一名在后端领域学习&#xff0c;并渴望能够学有所成的追梦人。 &#x1f40c; 个人主页&#xff1a;蜗牛牛啊 &#x1f525; 系列专栏&#xff1a;&#x1f6f9;数据结构、&#x1f6f4;C &#x1f4d5; 学习格言&#xff1a;博观而约取&…

Python实现透明隧道爬虫ip:不影响现有网络结构

作为一名专业爬虫程序员&#xff0c;我们常常需要使用隧道代理来保护个人隐私和访问互联网资源。本文将分享如何使用Python实现透明隧道代理&#xff0c;以便在保护隐私的同时不影响现有网络结构。通过实际操作示例和专业的解析&#xff0c;我们将带您深入了解透明隧道代理的工…

物联网和不断发展的ITSM

物联网将改变社会&#xff0c;整个技术行业关于对机器连接都通过嵌入式传感器、软件和收集和交换数据的电子设备每天都在更新中。Gartner 预测&#xff0c;全球将有4亿台互联设备投入使用。 无论企业采用物联网的速度如何&#xff0c;连接设备都将成为新常态&#xff0c;IT服务…

Dubbo1-架构的演变

分布式系统上的相关概念 项目&#xff1a;传统项目、互联网项目 传统项目&#xff1a; 一般为公司内部使用&#xff0c;或者小群体小范围的使用&#xff0c;一般不要求性能&#xff0c;美观&#xff0c;并发等 互联网项目的特点&#xff1a; 1.用户多 2.流量大&#xff0c;并…

【CSS】透明背景的圆角渐变边框实现方案

css的渐变边框可以用下面方式实现 border-image: linear-gradient(rgb(89, 0, 255),pink) 30 30; css的圆角边框可以用下面方式实现 border-radius: 20px; 那想要实现一个圆角的渐变边框呢&#xff0c;可能会以为&#xff0c;两个都用上不就可以了&#xff0c;但事实是&…

JSON字符串转换

大家好 , 我是苏麟 , 今天带来一个JSON序列化库 Gson . GitHub 地址 : GitHub - google/gson: A Java serialization/deserialization library to convert Java Objects into JSON and back java 中 json 序列化库有很多&#xff1a; gson (谷歌的) fastjson (阿里的) jack…

Python——添加照片边框

原图&#xff1a; 添加边框后&#xff1a; 添加边框会读取照片的exif信息如时间、相机型号、品牌以及快门焦段等信息&#xff0c;将他们显示在下面的边框中。 获取当前py文件路径 import os #get path that py file located def Get_Currentpath():file_path os.path.abspa…

软考:中级软件设计师:文件管理,索引文件结构,树型文件结构,位示图,数据传输方式,微内核

软考&#xff1a;中级软件设计师: 提示&#xff1a;系列被面试官问的问题&#xff0c;我自己当时不会&#xff0c;所以下来自己复盘一下&#xff0c;认真学习和总结&#xff0c;以应对未来更多的可能性 关于互联网大厂的笔试面试&#xff0c;都是需要细心准备的 &#xff08;1…

sudo免密码设置以及设置失败解决方法

使用sudo visudo修改\etc\sudoers文件 打开后有很多已有的设置大致格式username ALL(ALL:ALL) ALL&#xff0c;都不要动&#xff01; 在文件结尾加上一句话&#xff1a; username ALL(ALL:ALL) NOPASSWD: ALLusername就是目前你这个账户的名字&#xff0c;开机时会输密码登录…

数据库内日期类型数据大于小于条件查找注意事项

只传date格式的日期取查datetime的字段的话默认是 00:00:00 日期类型字符串需要使用 ’ ’ 单引号括住 使用大于小于条件查询某一天的日期数据 前后判断条件不能是同一天 一个例子 数据库内数据&#xff1a; 查询2023-08-14之后的数据&#xff1a; select * from tetst…

互联网发展历程:从中继器口不够到集线器的引入

互联网的发展&#xff0c;就像一场不断演进的技术盛宴&#xff0c;每一步的变革都在推动着我们的世界向前。然而&#xff0c;在网络的早期&#xff0c;一项重要的技术问题曾困扰着人们&#xff1a;当中继器的接口数量不足时&#xff0c;如何连接更多的设备&#xff1f;这时&…

移动端预览指定链接的pdf文件流

场景 直接展示外部系统返回的获取文件流时出现了跨域问题&#xff1a; 解决办法 1. 外部系统返回的请求头中调整&#xff08;但是其他系统不会给你改的&#xff09; 2. 我们系统后台获取文件流并转为新的文件流提供给前端 /** 获取传入url文件流 */ GetMapping("/get…