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

server/2024/9/20 3:56:05/ 标签: 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.ppmy.cn/server/39487.html

相关文章

linux命令——硬链接

在软链接中&#xff0c;创建的快捷方式&#xff0c;我们可以改变原文件内容&#xff0c;这时再打开快捷方式&#xff0c;会发现对应内容也随之改变&#xff0c;那么我们想到&#xff0c;能否通过改变快捷方式内容&#xff0c;改变原文件 可以看到&#xff0c;也是可以的 在linu…

金蝶BI应收分析报表:关于应收,这样分析

这是一张出自奥威-金蝶BI方案的BI应收分析报表&#xff0c;是一张综合运用了筛选、内存计算等智能分析功能以及数据可视化图表打造而成的BI数据可视化分析报表&#xff0c;可以让企业运用决策层快速知道应收账款有多少&#xff1f;账龄如何&#xff1f;周转情况如何&#xff1f…

二叉树及其遍历

一、二叉树 二叉树是一种树形结构&#xff0c;每个节点最多有两个子节点&#xff0c;分别称为左子节点和右子节点。如果一个节点没有子节点&#xff0c;则称为叶子节点。二叉树常用于实现搜索算法和排序算法。 二叉树的特点包括&#xff1a; 每个节点最多有两个子节点。左子节…

Logstash分析MySQL慢查询日志实践

删除匹配到的行&#xff0c;当前行信息不记录到message中

【论文阅读笔记】MAS-SAM: Segment Any Marine Animal with Aggregated Features

1.论文介绍 MAS-SAM: Segment Any Marine Animal with Aggregated Features MAS-SAM&#xff1a;利用聚合特征分割任何海洋动物 Paper Code(空的) 2.摘要 最近&#xff0c;分割任何模型&#xff08;SAM&#xff09;在生成高质量的对象掩模和实现零拍摄图像分割方面表现出卓越…

算法:动态规划

开始前先用两个小问题开开思路&#xff1a; 寻找多数元素&#xff1a; 链接&#xff1a;题目1 class Solution { public:int majorityElement(vector<int>& nums) {int x 0, votes 0;for (int num : nums){if (votes 0) x num;votes num x ? 1 : -1;}retur…

流畅的Python阅读笔记

五一快乐的时光总是飞快了&#xff0c;不知多久没有拿起键盘写文章了&#xff0c;最近公司有Python的需求&#xff0c;想着复习下Python吧&#xff0c;然后就买了本Python的书籍 书名&#xff1a; 《流畅的Python》 下面是整理的一个阅读笔记&#xff0c;大家自行查阅&#xf…

cookie,session,token

目的&#xff1a;解决用户登录状态 从一个简单的登录开始说起&#xff0c; 在我们访问bilibili的时候&#xff0c;第一次需要登录&#xff0c;但后续就不需要登录了&#xff0c;可以直接访问bilibili。 而且每次在页面请求服务器的资源都需要维持登录状态&#xff0c;如果没…

STM32单片机实战开发笔记-PWM波输出频率及占空比配置【wulianjishu666】

单片机物联网开发资料&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/1XzodQuML7CqZ4ZKinDGKkg?pwdbgep 提取码&#xff1a;bgep PWM模块测试 功能描述 脉冲宽度调制模式&#xff1a; PWM边沿对齐模式&#xff1a; 向上计数配置 当TIMX_CR1寄存器中的DIR为低的时…

SinoDB数据库导入导出工具External table

External table又叫SinoDB外部表&#xff0c;外部表采用多线程机制&#xff0c;支持多线程读取、写入数据文件以及多线程数据转换、插入操作。多线程机制只需要消耗相对较少的系统资源&#xff0c;但是能提供高速数据导入、导出&#xff0c;可以应用在数据采集、表重建、数据库…

界面组件Kendo UI for Angular教程 - 构建强大的PDF阅读器(一)

如今当用户需要处理PDF文件时&#xff0c;通常不得不下载应用程序或者浏览器插件&#xff0c;控制用户如何与PDF交互并不是一件容易的事。如果我们提供PDF作为内容&#xff0c;用户可以下载它并使用浏览器或PDF本身提供的控件进行交互。然而&#xff0c;一些企业可能希望控制用…

layui的treeTable组件,多层级上传按钮失效的问题解决

现象描述: layui的treeTable 的上传按钮在一层能用&#xff0c;展开后其他按钮正常点击&#xff0c;上传按钮无效。 具体原因没有深究&#xff0c;大概率是展开的子菜单没有被渲染treeTable的done管理到&#xff0c;导致没有重绘上传按钮。 解决方案: 不使用layu的上传组件方法…

基于Springboot的校园健康驿站管理系统(有报告)。Javaee项目,springboot项目。

演示视频&#xff1a; 基于Springboot的校园健康驿站管理系统&#xff08;有报告&#xff09;。Javaee项目&#xff0c;springboot项目。 项目介绍&#xff1a; 采用M&#xff08;model&#xff09;V&#xff08;view&#xff09;C&#xff08;controller&#xff09;三层体系…

kubernate 基本概念

一 K8S 是什么&#xff1f; K8S 全称&#xff1a;Kubernetes 1 kubernate基本概念 作用&#xff1a; 用于自动部署、扩展和管理“容器化&#xff08;containerized&#xff09;应用程序”的开源系统。 可以理解成 K8S 是负责自动化运维管理多个容器化程序&#xff08;比如…

随便写点东西

1 react的高阶组件 1.1 操纵组件的props、对组件的props进行增删&#xff1b; 1.2 复用组件逻辑 服用的组件逻辑&#xff0c;互不影响&#xff1b;比如高阶组件中复用了input框&#xff0c;输入内容是互不影响的&#xff1b; 1.3 可以通过配置装饰器来实现高阶组件&#xff08…

速盾高防CDN的防御能力如何?

速盾高防CDN是一种网络安全解决方案&#xff0c;旨在保护网站免受各种网络攻击&#xff0c;如分布式拒绝服务&#xff08;DDoS&#xff09;攻击、恶意爬虫、SQL注入等。它通过使用先进的防御技术和强大的基础设施来提供出色的防御能力。 首先&#xff0c;速盾高防CDN具备强大的…

英语学习笔记8——What‘s your job?

What’s your job? 你是做什么工作的&#xff1f; 词汇 Vocabulary policeman 男警察 policewoman 女警察 police n. 警力 集合名词&#xff0c;永表复数 西方国家警察管的事很多。交警&#xff0c;刑警&#xff0c;武警一般不分开。 taxi driver 出租车司机 taxi / cab n.…

基于WPF的DynamicDataDisplay曲线显示

一、DynamicDataDisplay下载和引用 1.新建项目,下载DynamicDataDisplay引用: 如下图: 二、前端开发: <Border Grid.Row="0" Grid.Column="2" BorderBrush="Purple" BorderThickness="1" Margin="2"><Grid>…

系统Cpu利用率降低改造之路

系统Cpu利用率降低改造之路 一.背景 1.1 系统背景 该系统是一个专门爬取第三方数据的高并发系统&#xff0c;该系统单台机器以每分钟400万的频次查询第三方数据&#xff0c;并回推给内部第三方系统。从应用类型上看属于IO密集型应用,为了提高系统的吞吐量和并发&#xff0c;…