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://xiahunao.cn/news/3015400.html

如若内容造成侵权/违法违规/事实不符,请联系瞎胡闹网进行投诉反馈,一经查实,立即删除!

相关文章

Elsevier旗下双1区TOP刊,8.8分影响因子加上超低自引率,各指标领跑计算机类SCI

【SciencePub学术】 今天小编给大家带来了一本计算机类的高分优刊解读&#xff0c;隶属于Elsevier出版社&#xff0c;JCR1区&#xff0c;中科院1区TOP&#xff0c;影响因子高达8.7&#xff0c;领域相符的学者可考虑&#xff01; APPLIED SOFT COMPUTING 1 期刊概况 【期刊简…

SD-WAN对云服务的优化

在云服务日益普及的当下&#xff0c;SD-WAN技术正成为众多企业优化网络连接的首选方案。其通过优化云集成和连接&#xff0c;以及增强应用程序性能&#xff0c;为企业带来了前所未有的业务效益。这种革新性的云连接方式极大地促进了企业对全球劳动力和潜在客户的触达能力。 软件…

【设计模式实战】用三种设计模式去优化if-else屎山代码!!!

优化前提 【设计模式】之策略模式【设计模式】之工厂模式&#xff08;三种&#xff09;【设计模式】之模板方法模式 前言 我们之前也学习了不少设计模式&#xff0c;今天给大家介绍一个案例&#xff0c;帮助大家更加熟悉设计模式&#xff0c;并能够在自己写项目的时候能够下意…

PXE远程部署CentOS系统

文章目录 在局域网内搭建PXE服务器PXE 启动组件PXE的优点实验一、搭建PXE服务器&#xff0c;实现远程部署CentOS系统环境准备server关闭防火墙安装组件准备 Linux 内核、初始化镜像文件及PXE引导文件配置启用TFTP 服务配置启动DHCP服务准备CentOS 7 安装源配置启动菜单文件 Cli…

centos7.9系统rabbitmq3.8.5升级为3.8.35版本

说明 本文仅适用rabbitmq为RPM安装方式。 升级准备 查看环境当前版本&#xff1a; # cat /etc/redhat-release CentOS Linux release 7.9.2009 (Core) # rabbitmqctl status Status of node rabbitmq01 ... RuntimeOS PID: 19333 OS: Linux Uptime (seconds): 58 Is under …

解决本地启动项目,用IP地址访问失败问题

解决方法&#xff1a;看看index.html页面有没有 这个标签&#xff0c;将它注释掉

Go 语言基础之指针、复合类型【数组、切片、指针、map、struct】

1、数组 特别需要注意的是&#xff1a;在 Go 语言中&#xff0c;数组长度也是数组类型的一部分&#xff01;所以尽管元素类型相同但是长度不同的两个数组&#xff0c;它们的类型并不相同。 1.1、数组的初始化 1.1.1、通过初始化列表{}来设置值 var arr [3]int // int类型的数…

获取波形极值与间距并显示

获取并显示波形的极值与极值间距 1、流程 1、通过signal.find_peaks获取极大值 2、获取极大值下标 3、获取极大值对应的值 4、获取极大值的下标间距(就是隔多远有一个极大值) 5、获取极大值间距的标准差、方差、均值、最大值 6、图形展示波形图并标记极大值2、效果图 3、示…

专项技能训练五《云计算网络技术与应用》实训8-1:建立基于OpenvSwitch的GRE隧道

文章目录 建立基于OpenvSwitch的GRE隧道1. 使用VMware安装2个CentOS 7虚拟机&#xff0c;安装时记得都开启CPU虚拟化&#xff0c;第一台命名为“Docker”&#xff0c;第二台命名为“KVM”。2. 安装完虚拟机后&#xff0c;进入虚拟机&#xff0c;修改网络配置&#xff08;onboot…

软件架构的艺术:探索演化之路上的18大黄金原则

实际工作表明&#xff0c;一步到位的设计往往不切实际&#xff0c;而演化原则指导我们逐步优化架构&#xff0c;以灵活响应业务和技术的变化。这不仅降低了技术债务和重构风险&#xff0c;还确保了软件的稳定性和可扩展性。同时&#xff0c;架构的持续演进促进了团队协作&#…

新华三VRRP配置

新华三VRRP配置 配置步骤 (1).基础配置&#xff1a; CORE1&#xff1a; [CORE1]vlan 10 //创建vlan10 [CORE1-vlan10]int vlan 10 //进入vlanif 10 [CORE1-Vlan-interface10]ip add 192.168.10.1 24 //配置ip [CORE1-Vlan-interface10]int g1/0/2 //进入接口 [C…

常见错误以及如何纠正它们

团队和关键结果目标 (OKR) 之间的关系是深刻且至关重要的。总而言之&#xff0c;一切都应该是相互关联的。正如《团队的智慧》一书中所强调的&#xff1a; 在团队中&#xff0c;没有什么比每个成员对共同目标和一组相关绩效目标的承诺更重要的了&#xff0c;而团队对此负有共同…

通过管理系统进行升级怎么选?

现在通过系统来做办公效率提升的又很多&#xff0c;但怎么选&#xff0c;确实很关键。 我们是在去年年初的时候进行企业系统化的。当时刚摘下口罩&#xff0c;领导也是意识到团队办公的不便&#xff0c;数据管理的混乱&#xff0c;业务流转的低效等原因&#xff0c;开始寻找各…

CUDA、CUDNN、Pytorch三者之间的关系

这个东西嘛&#xff0c;我一开始真的是一头雾水&#xff0c;安装起来真是麻烦死了。但是随着要复现的项目越来越多&#xff0c;我也不得不去学会他们是什么&#xff0c;以及他们之间的关系。 首先&#xff0c;一台电脑里面允许有多种版本的cuda存在&#xff0c;然后cuda分为run…

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

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

从简单逻辑到复杂计算:感知机的进化与其在现代深度学习和人工智能中的应用(下)

文章目录 第一章&#xff1a;感知机的局限性1.1 异或门的挑战1.2 线性与非线性问题 第二章&#xff1a;多层感知机2.1 已有门电路的组合2.2 实现异或门 第三章&#xff1a;从与非门到计算机 文章文上下两节 从简单逻辑到复杂计算&#xff1a;感知机的进化与其在现代深度学习和人…

DigitalOcean 应用托管平台级更新:应用端到端运行时性能大幅改进

DigitalOcean 希望可以为企业提供所需的工具和基础设施&#xff0c;以帮助企业客户加速云端的开发&#xff0c;实现业务的指数级增长。为此 DigitalOcean 在 2020 年就推出了App Platform。 App Platform&#xff08;应用托管&#xff09; 是一个完全托管的 PaaS 解决方案&…

代码随想录Day 40|Leetcode|Python|139.单词拆分 ● 关于多重背包,你该了解这些! ● 背包问题总结篇!

139.单词拆分 给你一个字符串 s 和一个字符串列表 wordDict 作为字典。如果可以利用字典中出现的一个或多个单词拼接出 s 则返回 true。 注意&#xff1a;不要求字典中出现的单词全部都使用&#xff0c;并且字典中的单词可以重复使用。 解题思路&#xff1a; 确定dp数组含义…

火山引擎数据飞轮携手美宜佳 探索拓店营销新思路

在刚刚过去的 3 月&#xff0c;美宜佳又交出了门店增长的高分答卷。 最新数据显示&#xff0c;美宜佳在全国的连锁店数已经超过 35000 家&#xff0c;每年净增 3000-4000 家店&#xff0c;月均服务顾客超 2 亿人次&#xff1b;同时&#xff0c;在中国连锁经营协会(CCFA)近日发布…

有哪些方式可以有效地评估精益生产咨询公司的能力?

在寻求精益生产咨询服务的过程中&#xff0c;评估咨询公司的能力至关重要。这不仅关乎企业精益生产转型的成功与否&#xff0c;更直接影响到企业未来的竞争力和发展。那么&#xff0c;有哪些方式可以有效地评估精益生产咨询公司的能力呢&#xff1f; 首先&#xff0c;了解咨询公…