C++笔记:类与对象(三)->多态

多态

虚函数

先来看一段代码:

#include<iostream>
using namespace std;class Animal {
public :void run() {cout << "I don't know how to run" << endl;}
};class Cat : public Animal{
public :void run() {cout << "I can run with four legs" << endl;}
};class Bat : public Animal{
public :void run() {cout << "I can fly" << endl;}
};#define P(func) {\printf("%s : ", #func);\func;\
}int main() {Cat c;//子类的对象可以隐式的转换为父类的类型Animal &a = c;Animal *p = &c;P(c.run());P(a.run());P(p->run());return 0;
}

        a是Animal的引用类行而指向的是c对象,p是Animal的指针类型指向对象c的地址。

那么我们认为的结果应该都是调用Cat类型中的run方法,但是结果却是调用的Animal中的run方法

        那么如何完成我们想要的效果呢,把基类中也就是Animal类中的run()方法变为虚函数,在父类中对应函数前加上virtual关键字,再子类中对应的函数后加上override关键字。

class Animal {
public :virtual void run() {cout << "I don't know how to run" << endl;}
};class Cat : public Animal{
public :void run() override{cout << "I can run with four legs" << endl;}
};class Bat : public Animal{
public :void run() override{cout << "I can fly" << endl;}
};

这样过后执行整个代码,就可以得到我们想要的效果了。

这里对应的知识点:
        普通成员函数是跟着类型的,比开始时,run方法不是虚函数只是普通的成员函数,那么在对应类类型前调用该函数,执行的就是对应类型中的函数方法,那么Animal &a = c,调用的就是Animal中的run方法,Animal *p  = c调用的run方法也是Animal中的run方法。

        虚函数是跟着对象的,比如在对父类中(Animal类)run方法加上virtual关键字之后,让run方法变为了虚函数,而子类中的run方法也变为了虚函数,执行时调用的对应函数就是对象对应的函数。Animal &a = c对应调用就是对象c的类型中的run方法,同理Animal *p  = c调用的run方法也是对象c的类中的run方法。

编译期和运行期

来一段简单的代码理解运行期和编译期

#include<iostream>
using namespace std;int main() {int n;scanf("%d", &n);//只有再代码执行时,读入n//才能知道m具体等于几//所以这是运行期int m = 2 * n;//在读代码时我们就能准确的知道c等于3//所以这是编译期int c = 1 + 2;return 0;
}

理解完后再看下面的代码:


#include<iostream>
using namespace std;class Animal {
public :void say() {cout << " Class Animal" << endl;}
};class Cat : public Animal {
public :void say() {cout << " Class Cat" << endl;}
};class Dog : public Animal {
public :void say() {cout << " Class Dog" << endl;}
};class Bat : public Animal {
public :void say() {cout << " Class Bat" << endl;}
};
int main() {#define MAX_N 10srand(time(0));Animal *arr[MAX_N + 5];for (int i = 0; i < MAX_N; i++) {switch (rand() % 3) {case 0: arr[i] = new Cat(); break;case 1: arr[i] = new Dog(); break;case 2: arr[i] = new Bat(); break;}}for (int i = 0; i < MAX_N; i++) {arr[i]->say();}return 0;
}

        在不运行这段代码,我们都知道最后的结果会是,调用了10次Animal中的say方法,因为指针数组是Animal类型的,并且say方法是普通的成员方法,所以指针数组不管指向什么子类对象都只会调用Animal中的say方法,所以这段代码是编译器的状态。

        执行结果:

那么我们将say方法改为虚函数:

#include<iostream>
using namespace std;class Animal {
public :virtual void say() {cout << " Class Animal" << endl;}
};class Cat : public Animal {
public :void say() override {cout << " Class Cat" << endl;}
};class Dog : public Animal {
public :void say() override {cout << " Class Dog" << endl;}
};class Bat : public Animal {
public :void say() override {cout << " Class Bat" << endl;}
};
int main() {#define MAX_N 10srand(time(0));Animal *arr[MAX_N + 5];for (int i = 0; i < MAX_N; i++) {switch (rand() % 3) {case 0: arr[i] = new Cat(); break;case 1: arr[i] = new Dog(); break;case 2: arr[i] = new Bat(); break;}}for (int i = 0; i < MAX_N; i++) {arr[i]->say();}return 0;
}

        那么现在,输出的结果我们是不确定的了,现在调用的say方法,就应该是每个arr[i]中对应的对象中类型的say方法。所这段代码需要在运行后才知道结果。

        那么执行结果是不确定了,我执行两次的结果可以看一下:

        可以发现是不同的结果,这段代码只有在执行后才能知道结果,这就是运行期。

        那么我们就可以理解图中的解释。

多态程序设计中注意事项 

看下面代码来理解为什么要这样去设计:

#include<iostream>
using namespace std;class Base {
public :Base(){cout << "Base constructor" << endl;}virtual ~Base(){cout << "Base destructor" << endl;}
};class A : public Base {
public :A() : data(new int[0]) {cout << "A constructor" << endl;}~A() override {delete[] data;cout << "A destructor" << endl;}int *data;
};int main() {//这里Base类的指向A类型的对象//如果析构函数不是虚函数//在析构p时,那么会调用Base类的析构函数//而不是调用对象对应的A类中的析构函数//那么就会造成内存泄漏//比如这里data通过A类的构造函数中new关键字获取了内存//而没有通过A类中的析构函数将对应的内存给释放掉Base *p = new A();delete p;return 0;
}

        对应上面这份代码,可以尝试将父类中的析构函数,设置为普通函数,看打印结果,你会发现,它只调用了父类的析构函数没有调用A对象的析构。

纯虚函数

#include<iostream>
using namespace std;namespace test1 {
class Animal{
public :virtual void say() = 0;
};class Cat : public Animal{
public :void say() override {cout << "Class Cat" << endl;}
};class Dog : public Animal{
public :void say() override {cout << "Class Dog" << endl;}
};class Bat : public Animal{
public :void say() override {cout << "Class Bat" << endl;}
};int main() {#define MAX_N 5srand(time(0));Animal *arr[MAX_N + 5];for (int i = 0; i < MAX_N; i++) {switch (rand() % 3) {case 0 : arr[i] = new Dog(); break;case 1 : arr[i] = new Cat(); break;case 2 : arr[i] = new Bat(); break;}}for (int i = 0; i < MAX_N; i++) arr[i]->say();return 0;
}
}/*
namespace test2{
class A {
public :virtual void func() = 0;
};
class B : public A{
public :
};
int main() {B b;return 0;
}
}
*//*
namespace test3{
class A {
public :virtual void func() = 0;
};
class B : public A{
public :void func() override {};
};
int main() {A a;return 0;
}
}
*/int main() {//test1中展示了纯虚函数的使用方法test1::main();//在test2中可以发现B继承A//A中有一个纯虚函数func//而在B中没有重写func在定义B的对象d会发生报错//test2::main();//在test3中因为A类是一个抽象类//所以这个A类不能定义对象//test3::main();return 0;
}

通过抽象类如何去理解接口

假设现在我们实现USB接口,然后USB可以接键盘和鼠标:

#include <iostream>
#include <cstdlib>
#include <ctime>
using namespace std;class USB_interface {
public :virtual string get() = 0;virtual void set(string) = 0;string msg;
};class KeyBoard : public USB_interface {
public :string get() override {return "this messge come from key  board\n";}void set(string msg) {cout << "key borad receive msg : " << msg << endl;}
};
class Mouse : public USB_interface {
public :string get() override {return "mouse dida dida\n";}void set(string msg) {cout << "mouse receive msg" << msg << endl;}
};int main() {srand(time(0));USB_interface *usb[2];int ind = rand() % 2;usb[ind] = new KeyBoard();usb[1 - ind] = new Mouse();for (int i = 0; i < 2; i++) {cout << "USB #" << i << ":" << endl;cout << usb[i]->get() << endl;usb[i]->set("over done!");} return 0;
}

通过执行代码可以发现,USB接口它只是起到连接键盘或者鼠标,而具体的实现功能都在鼠标和键盘里面实现的。可以理解USB接口它里面有一些纯虚函数,而通过USB接口接上的东西他需要重写这些纯虚函数,并且还可以自己定义一些方法,这就是接口的作用。

虚函数的底层原理

来看一段代码:

#include<iostream>
using namespace std;class Base {
public :void say() {cout << "Class Base" << endl;}
};class A : public Base {
public :void say() {cout << "Class A" << endl;}int x;
};class B : public Base {
public :   void say() {cout << "Class B" << endl;}int x;
};int main() {A a;B b;cout << "sizeof(A) :" << sizeof(A) << endl;cout << "sizeof(B) :" << sizeof(B) << endl;return 0;
}

通过执行可以发现,a对象和b对象只有4字节。也就是他们包含的int x的字节大小。

再看下面一段代码,他们中的say方法是虚函数时。

#include<iostream>
using namespace std;class Base {
public :virtual void say() {cout << "Class Base" << endl;}
};class A : public Base {
public :void say() override {cout << "Class A" << endl;}int x;
};class B : public Base {
public :   void say() override {cout << "Class B" << endl;}int x;
};int main() {A a;B b;cout << "sizeof(A) :" << sizeof(A) << endl;cout << "sizeof(B) :" << sizeof(B) << endl;return 0;
}

a和b对象有16个字节了

 

这是为什么,这就要说到虚函数表。

虚函数表:

每个对象如果它的类型有虚函数,那么这个对象对应的存储区通常第一个元素就是一个地址,而这个地址就是指向虚函数表的首地址

而上面的16字节是怎么算来的,结构体内存对齐

来看下面一段代码:

#include<iostream>
using namespace std;class Base {
public :virtual void say() {cout << "Class Base" << endl;}
};class A : public Base {
public :void say() override {cout << "Class A" << endl;}int x;
};class B : public Base {
public :   void say() override {cout << "Class B" << endl;}int x;
};int main() {A a1, a2;B b;cout << "sizeof(A) :" << sizeof(A) << endl;cout << "sizeof(B) :" << sizeof(B) << endl;//((void **)(&a1))[0]获取a1对象的虚函数表的地址cout << "Class A(a1) virtual function address : " << ((void **)(&a1))[0] << endl;cout << "Class A(a2) virtual function address : " << ((void **)(&a2))[0] << endl;cout << "Class B virtual function address : " << ((void **)(&b))[0] << endl;return 0;
}

执行结果:

可以发现a1和a2的虚函数表地址是相同的,而b的虚函数表的地址和他们不同,那么就可以知道相同类型的虚函数表是相同的,而不同的类型虚函数表是不同的。

来看下面的图:

这样就可以理解为什么虚函数是跟着对象走的。

深入理解this指针:

来看下面一段代码:

#include<iostream>
using namespace std;class Base {
public :virtual void say(int x) {cout << this << endl;cout << "Class Base : " << x << endl;}
};class A : public Base {
public :void say(int x) override {cout << this << endl;cout << "Class A : " << x << endl;}int x;
};class B : public Base {
public :   void say(int x) override {cout << this << endl;cout << "Class B : " << x << endl;}int x;
};typedef void (*func_t)(int);int main() {A a1, a2;B b;Base *p1 = &a1, *p2 = &a2, *p3 = &b;cout << "sizeof(A) :" << sizeof(A) << endl;cout << "sizeof(B) :" << sizeof(B) << endl;//((void **)(&a1))[0]获取a1对象的虚函数表的地址cout << "Class A(a1) virtual function address : " << ((void **)(&a1))[0] << endl;cout << "Class A(a2) virtual function address : " << ((void **)(&a2))[0] << endl;cout << "Class B virtual function address : " << ((void **)(&b))[0] << endl;p1->say(1);cout << "================" << endl;p2->say(2);cout << "================" << endl;p3->say(3);cout << "================" << endl;//通过原生指针调用say()方法((func_t **)(&a2))[0][0](97);return 0;
}

执行结果:

        对于p1,p2,p3调用没有任何问题,都是准确调用传参,但是到了通过原生指针调用a2中的虚函数表的第一个函数也就是say方法,然后传入参数是97为什么输出的x的值是0,而this指针的值变为了16进制的61,转换为10进制的就是97,为什么this指针的值被赋值为97了。

       也就是当前say方法实际是这样的:

void say(A *this, int x) override {...}

        所以在使用C语言原生指针调用成员方法时是需要将this指针当作参数传入的,那么如和正确调用该函数呢,如下:

    //修改处//void *任何类型的指针typedef void (*func_t)(void *, int);//修改处使用原生指针调用say方法处((func_t **)(&a2))[0][0](&a2, 97);

        那么最终的执行结果就是和我们想要的结果是一样的:

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

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

相关文章

【毕业设计】基于微信小程序的校园快递平台系统设计与实现

1.项目介绍 如今社会上各行各业&#xff0c;都喜欢用自己行业的专属软件工作&#xff0c;互联网发展到这个时候&#xff0c;人们已经发现离不开了互联网。新技术的产生&#xff0c;往往能解决一些老技术的弊端问题。因为传统校园快递平台系统信息管理难度大&#xff0c;容错率…

让我们把Domino变成SFTP服务器

大家好&#xff0c;才是真的好。 远程共享文件有很多办法&#xff0c;其中值得注意的是SFTP方式。SFTP即SSH文件传输协议&#xff0c;通过使用SSH传输层&#xff0c;SFTP可以通过Internet连接安全地访问和移动大量数据文件。 今天我们就介绍使用Domino中的HTTP OSGI方式来实现…

如何自己快速的制作流程图?6个软件教你快速进行流程图制作

如何自己快速的制作流程图&#xff1f;6个软件教你快速进行流程图制作 自己制作流程图可以是项目管理、流程设计或教学展示中的重要环节。以下是六款常用的流程图制作软件&#xff0c;它们都提供了快速、简单的方式来制作流程图&#xff1a; 迅捷画图&#xff1a;这是一款非…

echarts学习笔记:柱状图+雷达图+双环形图+地图可视化+数据传递关系图+关键词条图+数据总览图+AntV/G2/DataV

GitHub - lgd8981289/imooc-visualization: https://www.bilibili.com/video/BV1yu411E7cm/?vd_source391a8dc379e0da60c77490e3221f097a 课程源码 国内echarts镜像站&#xff1a;ISQQW.COM x ECharts 文档&#xff08;国内同步镜像&#xff09; - 配置项 echarts图表集&…

618挑选家用洗地机,需要注意哪些事项?有哪些家用洗地机值得买?

近年来&#xff0c;智能清洁家电越来越受到消费者的欢迎&#xff0c;洗地机作为清洁家电的新宠&#xff0c;凭借其集扫地、拖地、杀菌清洗于一体的强大功能&#xff0c;成为市场上的热销产品。那么&#xff0c;这类洗地机真的好用吗&#xff1f;怎么挑选到好用的家用的洗地机呢…

win10下,svn上传.so文件失败

问题&#xff1a;win10下使用TortoiseSVN&#xff0c;svn上传.so文件失败 解决&#xff1a;右键&#xff0c;选择Settings&#xff0c;Global ignore pattern中删除*.so&#xff0c;保存即可。

设置多用户远程登录windows server服务器

##设置多用户远程登录windows server服务器 ###1、远程登录windows server 2016 运行—>mstsc—>远程IP地址—>用户和密码 2、远程windows服务器设置多用户策略 运行—>gpedit.msc->计算机配置—管理模板—windows组件—远程桌面服务—远程桌面会话主机----连…

阿里巴巴1688商品详情API返回值深度剖析:精准获取商品信息的关键

在电子商务日益繁荣的今天&#xff0c;阿里巴巴1688作为中国领先的B2B平台&#xff0c;汇聚了海量的供应商和商品信息。对于商家、开发者以及希望深入了解商品数据的用户来说&#xff0c;如何通过有效的方式获取这些商品信息成为了一个重要议题。阿里巴巴1688商品详情API的出现…

Semi-decentralized Federated Ego Graph Learning for Recommendation

论文概况 本文是2023年WWW的一篇联邦推荐论文&#xff0c;提出了一个半去中心化的联合自我图学习框架。 Introduction 作者提出问题 现有的推荐方法收集所有用户的自我图来组成一个全局图&#xff0c;导致隐私风险。联合推荐系统已被提出来缓解隐私问题&#xff0c;但在客户…

zabbix监控方式(zabbix-trapper)

中文&#xff1a;zabbix采集器&#xff0c;即zabbix sender 。 Zabbix-Trapper 监控方式可以一次批量发送数据给Zabbix Server&#xff0c;与主动模式不同&#xff0c;Zabbix-Trapper 可以让用户控制数据的发送&#xff0c;而不用Zabbix-Agent进程控制&#xff0c;这意味着可以…

PE文件(四)FileBuffer-ImageBuffer

文件执行的总过程 当文件从硬盘中读入虚拟内存&#xff08;FileBuffer&#xff09;中时&#xff0c;文件数据会被原封不动的复制一份到虚拟内存中&#xff0c;然后进行拉伸对齐。此时虚拟内存中文件数据叫做文件印象或者内存印象&#xff0c;即ImageBuffer。此时ImageBuffer中…

42.乐理基础-拍号-看懂拍号的意义

到这必然是已经知道 X、Y的意思了&#xff1a; 然后带入数字&#xff1a; 然后念拍号的时候&#xff0c;在国内&#xff0c;百分之九十的地方是从下往上念&#xff0c;念作四二拍&#xff0c;还有百分之十的地方是和国外一样&#xff0c;从上往下念&#xff0c;念作二四拍&…

跨境支付行业研究

1. 行业基本情况 随着全球人均购买力增强、互联网普及率提升、支付渠道的进一步成熟、物流等配套设施的完善&#xff0c;网络购物已经成为全球兴起的消费习惯。另一方面&#xff0c;跨境电商对传统贸易的替代已经成为趋势。跨境电商在交易成本和便利程度上都有明显的优势 图1 …

大数据API技术分享:使用API接口采集淘宝数据(商品详情丨关键词搜索丨店铺所有商品)

使用API接口采集淘宝数据&#xff08;商品详情、关键词搜索、店铺所有商品&#xff09;是大数据领域常见的应用场景。以下是一些关于如何使用API接口进行这些操作的技术分享&#xff1a; 1. 获取API权限 首先&#xff0c;你需要在淘宝开放平台注册成为开发者&#xff0c;并创建…

想做视频号小店,为何不建议开通个体店?开店步骤+做店思路如下

我是王路飞。 如果你想在视频号开通店铺的话&#xff0c;那么一定不要使用个体执照开通个体店&#xff1f; 这是为什么呢&#xff1f; 原因很简单&#xff0c;视频号个体店是无法入驻优选联盟的&#xff0c;只能企业店可以入驻。 因为现阶段视频号小店的自然流量很少&#…

五一 作业

#include <iostream>using namespace std; class Num { private:int a; public:Num() {}Num(int a):a(a){}//设置a的值void set(int a){this->aa;}//1-a的和void Sum(){if(a<1){cout<<"a<1"<<endl;return;}int sum0;for(int i1;i<a;i)…

开源模型应用落地-CodeQwen模型小试-探索更多使用场景(三)

一、前言 代码专家模型是基于人工智能的先进技术&#xff0c;它能够自动分析和理解大量的代码库&#xff0c;并从中学习常见的编码模式和最佳实践。这种模型可以提供准确而高效的代码建议&#xff0c;帮助开发人员在编写代码时避免常见的错误和陷阱。 通过学习代码专家模型&…

计算机视觉——OpenCV Otsu阈值法原理及实现

算法简介 Otsu阈值法&#xff0c;也被称为大津算法&#xff0c;是一种在图像处理中广泛使用的自动阈值分割技术。这种方法由日本学者大津展之于1979年提出&#xff0c;旨在根据图像的灰度直方图来自动选择最佳全局阈值。Otsu阈值法的核心思想是最小化类内方差或最大化类间方差…

《设计一款蓝牙热敏打印机》

主控芯片用易兆威蓝牙ic&#xff0c;通讯接口&#xff1a;蓝牙、串口、usb 安卓apk用java kotlin编写、上位机用Qt编写。

【微磁学】对于现阶段微磁学仿真发展的思考1-理论篇

系列文章目录 对于现阶段微磁学仿真发展的思考1-理论篇 对于现阶段微磁学仿真发展的思考2-工具篇 文章目录 系列文章目录前言一、微磁学的数学区二、微磁学的物理区三、微磁学仿真现存的一些问题四、微磁学代码区&#xff1a;上手操作&#xff0c;理解更深入栗子1: 能量最小化…