【C++】多态学习

多态

  • 多态的概念与定义
    • 多态的概念
    • 构成多态的两个条件
      • 虚函数与重写
        • 重写的两个特例
  • final 和 override
  • 重载、重写(覆盖)、重定义(隐藏)的对比
  • 抽象类
  • 多态的原理
    • 静态绑定与动态绑定
  • 单继承与多继承关系下的虚函数表(派生类)
    • 单继承中的虚函数表查看
    • 多继承中的虚函数表查看
  • 菱形继承与菱形虚拟继承
    • 菱形继承
    • 菱形虚拟继承
  • 继承与多态一些常见问题

多态的概念与定义

多态的概念

多态就是多种形态,简单理解就是不同的对象去执行某个行为时会产生出不同的状态表现。
多态表现在继承关系中,继承关系的类对象去调用同一函数,会产生不同的状态行为表现。
例如,在买票体系中,普通人(Person)买票是全价,学生(Student)买票是半价。

构成多态的两个条件

  1. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数重写。
  2. 必须是通过基类的指针或者引用调用虚函数。

虚函数与重写

虚函数:被virtual关键字修饰的类成员函数
虚函数的重写:
重写也叫覆盖。重写要满足三同条件,三同条件也是建立在虚函数的基础上。
三同条件要求派生类虚函数与基类虚函数的返回值类型、函数名、参数列表完全相同。

class Person
{
public:virtual void BuyTicket(){cout << "买票-全价" << endl;}
};class Student : public Person
{
public:/* * 注意:子类虚函数不加virtual,依旧构成重写* 因为继承后基类的虚函数被继承下来在派生类依旧保持虚函数属性* 但实际最好加上virtual,否则写法不是很规范*/void BuyTicket(){cout << "买票-半价" << endl;}
};void Func(Person& p)
{p.BuyTicket();
}
void Test1()
{Person p;Student st;Func(p);Func(st);
}

要实现多态,那多态的两个条件必须严格遵守,任何一个条件不符合规则,或任何一个条件下的小条件不满足,都无法成功实现多态。

重写的两个特例

  1. 协变
    派生类重写基类虚函数时,基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用。
class A
{};class B : public A
{};class Person
{
public://virtual Person* BuyTicket()virtual A* BuyTicket(){cout << "买票-全价" << endl;//return this;return nullptr;}
};class Student : public Person
{
public:// 重写的协变:返回值可以不同,要求必须是父子关系的指针或者引用// 这里满足父子关系即可,不一定非要某类父子关系virtual B* BuyTicket(){cout << "买票-半价" << endl;//return this;return nullptr;}
};void Func(Person& p)
{p.BuyTicket();
}
void Test2()
{Person p;Student st;Func(p);Func(st);
}
  1. 析构函数的重写
    一眼看去,基类与派生类中析构函数的重写似乎不满足三同中的函数名相同,其实不然,可以理解为编译器对析构函数的名称做了特殊处理,在程序编译后析构函数的名称统一处理成了destructor
    所以,只要基类的析构函数是虚函数,此时派生类的析构函数只要定义,都与基类的析构函数构成重写。
    而且一般建议,将继承体系中析构函数定义成虚函数。下面的例子可以帮助参考。
class Person
{
public://~Person()virtual ~Person(){cout << "~Person()" << endl;}
};class Student : public Person
{
public://~Student()virtual ~Student(){cout << "~Student()" << endl;}
};void Test3()
{Person* p1 = new Person;delete p1;Person* p2 = new Student;delete p2;
}

在这里插入图片描述

final 和 override

  1. final
    修饰虚函数,表示该虚函数不能被重写。
class Car
{
public:virtual void Drive() final{}
};class Benz : public Car
{
public:// 无法实现重写virtual void Drive(){cout << "Benz" << endl;}
};

final也可以修饰类,表示该类不能被继承。

  1. override
    用于检查派生类虚函数是否重写了基类某个虚函数,如果没有重写会编译报错。
class Car
{
public:virtual void Drive(){}
};class Benz : public Car
{
public:// override 检查子类虚函数是否完成重写virtual void Drive() override{cout << "Benz" << endl;}
};

重载、重写(覆盖)、重定义(隐藏)的对比

在这里插入图片描述
两个基类和派生类的同名函数不构成重写,就是构成重定义。

抽象类

虚函数的后面加上=0,则表示这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(接口类)。
抽象类无法直接实例化出对象。抽象类被派生类继承后,派生类如果不重写纯虚函数,派生类也不能实例化出对象。
纯虚函数规范了派生类必须进行重写,体现了接口继承。

class Car
{
public:virtual void Drive() = 0;
};class Benz : public Car
{
public:virtual void Drive(){cout << "Benz" << endl;}
};void Test4()
{Benz b;b.Drive();
}

多态的原理

class Base
{
public:virtual void Func1(){cout << "Func1()" << endl;}
private:int _b = 0;
};void Test5()
{cout << "sizeof Base: " << sizeof Base << endl;Base b;
}

在这里插入图片描述
在这里插入图片描述
从上面结果可以看出,b对象中,除了_b成员,还多了一个_vfptr的指针(虚函数表指针,v代表virtual,f代表function)。
一个含有虚函数的类中都至少有一个虚函数表指针,而虚函数的地址被放到虚函数表(简称虚表)中。
Test5的代码改造一下,进一步观察。

class Base
{
public:virtual void Func1(){cout << "Base::Func1()" << endl;}virtual void Func2(){cout << "Base::Func2()" << endl;}void Func3(){cout << "Base::Func3()" << endl;}
private:int _b = 0;
};class Derive : public Base
{
public:virtual void Func1(){cout << "Derive::Func1()" << endl;}
private:int _d = 0;
};void Test6()
{Base b;Derive d;
}

在这里插入图片描述
通过观察,可以知道基类b对象和派生类d对象的虚表是不一样的。因为Func1完成了重写,所以d对象的虚表中存的是重写的Derive::Func1(),这也是重写被叫做覆盖的道理,即覆盖就是虚表中虚函数的覆盖。(重写是语法层的叫法,覆盖是原理层的叫法)
其实,派生类虚表是从基类虚表拷贝过来的,如果派生类重写了基类的某个虚函数,就用派生类自己的虚函数覆盖虚表中基类的虚函数,派生类自己新增加的虚函数按其在派生类中的声明次序,依次增加到派生类虚表的最后。
下面再通过之前买票的例子Test1帮助阐述多态的原理。

class Person
{
public:virtual void BuyTicket(){cout << "买票-全价" << endl;}
};class Student : public Person
{
public:virtual void BuyTicket(){cout << "买票-半价" << endl;}
};void Func(Person& p)
{p.BuyTicket();
}
void Test7()
{Person Mike;Student Allen;Func(Mike);Func(Allen);
}

MikeAllen通过Func传给p
p指向Mike时,就是在Mike的虚表中找到虚函数Person::BuyTicket
p指向Allen时,就是在Allen的虚表中找到虚函数Student::BuyTicket
这样就实现了不同对象去执行同一行为时,展现出不同形态的情况。
多态的本质总结:
对象多态成员函数调用时,会到对象的虚表中找到对应的虚函数地址,进行调用。

静态绑定与动态绑定

  1. 静态绑定又称前期绑定/早绑定。
    是指在程序编译期间就确定了程序的行为。也称静态/编译时多态。
    像重载,或是普通类成员函数的调用(直接call函数地址)。
    在这里插入图片描述
  2. 动态绑定又称后期绑定/晚绑定。
    是指在程序运行过程中,需要根据具体情况确定程序的具体行为。也称动态/运行时多态。
    在这里插入图片描述

单继承与多继承关系下的虚函数表(派生类)

单继承中的虚函数表查看

class Base
{
public:virtual void func1(){cout << "Base:func1" << endl;}virtual void func2(){cout << "Base:func2" << endl;}private:int _b = 0;
};class Derive : public Base
{
public:virtual void func1(){cout << "Derive::func1" << endl;}virtual void func3(){cout << "Derive::func3" << endl;}virtual void func4(){cout << "Derive::func4" << endl;}private:int _d = 1;
};void Test8()
{Base b;Derive d;
}

Test8测试代码调试时看到的虚表可能不完整,可以通过下面函数对虚表进行打印。

// VFPTR是一个函数指针,指向的函数参数为void,返回值为void
typedef void(*VFPTR)();void PrintVFTable(VFPTR table[])
{for (size_t i = 0; table[i] != nullptr; ++i){printf("vft[%d]:%p\n", i, table[i]);table[i](); // 函数回调}
}

对于PrintVFTable函数的调用如下。

/*
* 1.先取对象的地址,强转成int*,可以拿到头四个字节的地址
* 2. 在解引用取到的是虚函数表的指针,强转成VFPTR*,就可以进行传递
*/
PrintVFTable((VFPTR*)(*(int*)&b));
cout << endl;
PrintVFTable((VFPTR*)(*(int*)&d));

在这里插入图片描述

多继承中的虚函数表查看

class Base1
{
public:virtual void Func1() { cout << "Base1::Func1" << endl; }virtual void Func2() { cout << "Base1::Func2" << endl; }private:int _b1 = 1;
};class Base2
{
public:virtual void Func1() { cout << "Base2::Func1" << endl; }virtual void Func2() { cout << "Base2::Func2" << endl; }private:int _b2 = 2;
};class Derive : public Base1, public Base2
{
public:virtual void Func1() { cout << "Derive::Func1" << endl; }virtual void Func3() { cout << "Derive::Func3" << endl; }private:int _d = 3;
};void Test9()
{cout << "sizeof Derive: " << sizeof Derive << endl;Derive d;
}

在这里插入图片描述
对内存的查看:
在这里插入图片描述
d对象继承自两个父类,具有两张虚表。
下面通过PrintVFTable对两张表中的内容进行查看。

// 第一个虚表的查看
PrintVFTable((VFPTR*)(*(int*)&d));
cout << endl;
// 第二个虚表的查看 - 方法一
PrintVFTable((VFPTR*)(*(int*)((char*)&d + sizeof(Base1))));
// 第二个虚表的查看 - 方法二
Base2* pb = &d;
PrintVFTable((VFPTR*)(*(int*)(pb)));

在这里插入图片描述
可以看到多继承派生类的未重写的虚函数放在第一个所继承基类部分的虚函数表中。
其实子类有几个父类,如果父类有虚函数,则就会有几张虚表,子类自己的虚函数只会放到第一个父类的虚表后面。
这里深入查看,发现两张虚表中虽然存的都是Derive::Func1,但调用时所用的地址却是不一样的,这是如何做到的?下面通过查看汇编来看看。

Derive d;Base1* pb1 = &d;
Base2* pb2 = &d;d.Func1(); // 普通函数调用pb1->Func1(); // 多态调用
pb2->Func1(); // 多态调用

在这里插入图片描述
通过汇编的查看可以发现,虽然最初的地址不同,但最后都能跳到同一处进行函数调用,即Deriver::Func1

菱形继承与菱形虚拟继承

菱形继承

class A
{
public:virtual void func1() { cout << "A::func1" << endl; };
public:int _a;
};
class B : public A
{
public:virtual void func1() { cout << "B::func1" << endl; };virtual void func2() { cout << "B::func2" << endl; };
public:int _b;
};
class C : public A
{
public:virtual void func1() { cout << "C::func1" << endl; };virtual void func2() { cout << "C::func2" << endl; };
public:int _c;
};
class D : public B, public C
{
public:int _d;
};void Test10()
{D d;d.B::_a = 1;d.C::_a = 2;d._b = 3;d._c = 4;d._d = 5;
}

在这里插入图片描述

菱形虚拟继承

class A
{
public:virtual void func1() { cout << "A::func1" << endl; };
public:int _a;
};
class B : virtual public A
{
public:virtual void func1() { cout << "B::func1" << endl; };virtual void func2() { cout << "B::func2" << endl; };
public:int _b;
};
class C : virtual public A
{
public:virtual void func1() { cout << "C::func1" << endl; };virtual void func2() { cout << "C::func2" << endl; };
public:int _c;
};
class D : public B, public C
{
public:// 此时D必须对func1进行重写virtual void func1() { cout << "D::func1" << endl; };
public:int _d;
};void Test11()
{D d;d.B::_a = 1;d.C::_a = 2;d._b = 3;d._c = 4;d._d = 5;
}

D必须对func1进行重写,因为B和C都有fun1,虚拟继承为了解决数据冗余和二义性,D的虚表里面只能存放一个,就无法确定存哪一个。
在这里插入图片描述
通过内存查看,菱形虚拟继承的关系可以如下表示。
在这里插入图片描述

继承与多态一些常见问题

  1. inline函数可以是虚函数吗?
    可以,当一个函数是虚函数,在多态调用中,inline就失效了。
  2. static函数可以是虚函数吗?
    不可以,static成员函数都是在编译时进行地址确定。虚函数是为了实现多态,需要运行时去虚表进行地址确定,static函数是virtual的话没有意义,因为本来就不会去虚表。
  3. 析构函数可以是虚函数吗?
    不可以,对象中的虚表指针都是构造函数初始化列表阶段才进行初始化的,所以构造函数是虚函数是没有意义的。
  4. 析构函数可以是虚函数吗?
    可以,并且建议基类的析构函数定义成虚函数。
  5. 拷贝构造函数可以是虚函数吗?
    不可以,拷贝构造函数也是构造函数。
  6. 赋值函数可以是虚函数吗?
    语法上可以,但是没有什么实际价值。
  7. 对象访问普通函数快还是虚函数快?
    虚函数不构成多态,是一样快;
    虚函数构成多态调用,普通函数更快。因为多态调用是运行时去虚函数表中找虚函数地址。
  8. 虚函数表是什么时候生成的?存在哪的?
    虚函数表是编译阶段就生成好的,存在于代码段(常量区)。所以一个类的不同对象共享该类的虚表。
    (构造函数初始化列表阶段初始化的是虚函数表指针,对象中存的也是虚函数表指针)

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

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

相关文章

关于一个git的更新使用流程

1.第一步使用git bash 使用git bash命令来进行操作&#xff08;当然我是个人比较喜欢用这种方法的&#xff09; 2. 第二步&#xff1a;连接 3.第三步&#xff1a;进入 4.第四步&#xff1a;查看分支 5.第五步&#xff1a;切换分支 将本地文件更新后之后进行提交 6.第六步&am…

【个人博客系统网站】框架升级 · 工程目录 · 数据库设计

【JavaEE】进阶 个人博客系统&#xff08;1&#xff09; 文章目录 【JavaEE】进阶 个人博客系统&#xff08;1&#xff09;1. 使用Spring全家桶 MyBatis框架进行开发2. 页面2.1 登录页2.2 注册页2.3 详情页2.4 我的博客列表页3.5 所有人的博客列表页3.6 添加博客页3.7 修改文…

华为云 sfs 服务浅谈

以root用户登录弹性云服务器。 以root用户登录弹性云服务器。 安装NFS客户端。 查看系统是否安装NFS软件包。 CentOS、Red Hat、Oracle Enterprise Linux、SUSE、Euler OS、Fedora或OpenSUSE系统下&#xff0c;执行如下命令&#xff1a; rpm -qa|grep nfs Debian或Ubuntu系统下…

设计模式—观察者模式(Observer)

目录 思维导图 一、什么是观察者模式&#xff1f; 二、有什么优点吗&#xff1f; 三、有什么缺点吗&#xff1f; 四、什么时候使用观察者模式&#xff1f; 五、代码展示 ①、双向耦合的代码 ②、解耦实践一 ③、解耦实践二 ④、观察者模式 六、这个模式涉及到了哪些…

开发一个npm包

1 注册一个npm账号 npm https://www.npmjs.com/ 2 初始化一个npm 项目 npm init -y3编写一段代码 function fn(){return 12 }exports.hellofn;4发布到全局node_module npm install . -g5测试代码 创建一个text文件 npm link heath_apisnode index.js6登录(我默认的 https…

docker,nvidia-docker安装

卸载先前的docker Docker 的旧版本被称为 docker&#xff0c;docker.io 或 docker-engine 。如果已安装&#xff0c;请卸载它们&#xff1a; sudo apt-get remove docker docker-engine docker.io containerd runc使用 Docker 仓库进行安装 设置仓库 更新 apt 包索引 sudo…

基于单片机教室人数实时检测系统

一、系统方案 主程序中main函数主要是引脚的初始化&#xff0c;给单片机引脚初始化&#xff0c;初始化LCD1602&#xff0c;初始化红外对管&#xff0c;通过对LCD1602赋值&#xff0c;采集进入教室的人数&#xff0c;显示在LCD1602上面进出人数我们采用按键的形式&#xff0c;检…

opencv鼠标事件函数setMouseCallback()详解

文章目录 opencv鼠标事件函数setMouseCallback()详解1、鼠标事件函数&#xff1a;&#xff08;1&#xff09;鼠标事件函数原型&#xff1a;setMouseCallback()&#xff0c;此函数会在调用之后不断查询回调函数onMouse()&#xff0c;直到窗口销毁&#xff08;2&#xff09;回调函…

肖sir__linux详解__001

linux详解: 1、ifconfig 查看ip地址 2、6版本&#xff1a;防火墙的命令&#xff1a; service iptables status 查看防火墙状态 service iptables statrt 开启防火墙 service iptables stop 关闭防火墙 service iptables restart 重启防火墙状态 7版本&#xff1a; systemctl s…

考前冲刺上岸浙工商MBA的备考经验分享

2023年对于许多人来说都是不平凡的一年&#xff0c;历经三年的抗争&#xff0c;我们终于成功结束了疫情。而我也很幸运的被浙工商MBA项目录取&#xff0c;即将开始全新的学习生活。身为一名已在职工作6年的人&#xff0c;能够重回校园真是一种特别令人激动的体验。今天&#xf…

域内密码喷洒

在Kerberos阶段认证的AS-REQ阶段&#xff0c;请求包cname对应的值是用户名&#xff0c;当用户名存在时候&#xff0c;密码正确和错误两种情况下&#xff0c;AS-REP返回包不一样&#xff0c;所以可以利用这一点对域用户名进行密码喷洒攻击 域内密码喷洒工具 Kerbrute kerbrut…

【Redis】redis入门+java操作redis

目录 一、Redis入门 1.1 Redis简介 1.2 Redis下载与安装 1.2.1 下载 1.2.2 linux安装 1.2.3 windows安装 1.3 Redis服务启动与停止 1.3.1 linux启动、停止Redis服务 1.3.2 windows启动、停止Redis服务 1.4 修改Redis启动密码 1.4.1 Linux修改设置 1.4.2 windows设…

使用HTTPS模式建立高效爬虫IP服务器详细步骤

嘿&#xff0c;各位爬虫小伙伴们&#xff01;想要自己建立一个高效的爬虫IP服务器吗&#xff1f;今天我就来分享一个简单而强大的解决方案——使用HTTPS模式建立工具&#xff01;本文将为你提供详细的操作步骤和代码示例&#xff0c;让你快速上手&#xff0c;轻松建立自己的爬虫…

【力扣每日一题】2023.8.29 带因子的二叉树

目录 题目&#xff1a; 示例&#xff1a; 分析&#xff1a; 代码&#xff1a; 题目&#xff1a; 示例&#xff1a; 分析&#xff1a; 题目给我们一些元素&#xff0c;让我们用这些元素连接形成特定的二叉树&#xff0c;每种元素可以使用任意次数&#xff0c;形成的二叉树要…

【云计算•云原生】5.云原生之初识DevOps

文章目录 1.DevOps背景2.DevOps概念3.DevOps工具链 1.DevOps背景 软件开发必须包含两个团队&#xff1a;开发团队和运维团队 开发团队负责开发项目&#xff0c;系统迭代更新运维团队负责项目测试以及部署上线&#xff0c;维持系统稳定运行 一个软件周期中是由这两个团队相互…

数据库备份

数据库备份&#xff0c;数据库为school&#xff0c;素材如下 1.创建student和score表 目录 数据库备份&#xff0c;数据库为school&#xff0c;素材如下 1.创建student和score表 2.为student表和score表增加记录 3.备份数据库school到/backup目录 4.备份MySQL数据库为带…

Windows10上使用llama-recipes(LoRA)来对llama-2-7b做fine-tune

刚刚在Windows10上搭建环境来对llama2做finetune&#xff0c;里面坑还是挺多的&#xff0c;这里把印象中的坑整理了一下以作备忘。 llama-recipes是meta的开源项目&#xff0c;Github地址为&#xff1a;GitHub - facebookresearch/llama-recipes: Examples and recipes for Ll…

Go几种读取配置文件的方式

比较有名的方案有 使用viper管理配置[1] 支持多种配置文件格式&#xff0c;包括 JSON,TOML,YAML,HECL,envfile&#xff0c;甚至还包括Java properties 支持为配置项设置默认值 可以通过命令行参数覆盖指定的配置项 支持参数别名 viper[2]按照这个优先级&#xff08;从高到低&am…

UE4 显示遮挡物体

SceneDepth是你相机能够看见的物体的深度距离 CustomDepth是你相机包括看不见被遮挡的物体的深度距离 如果CustemDepth比SceneDepth的距离相等&#xff0c;那么就是没有被遮挡的物体&#xff0c;如果被遮挡那么就是CustemDepth比SceneDepth深度距离远&#xff0c;然后再做对应…

【业务功能篇91】微服务-springcloud-多线程-线程池执行顺序

一、线程的实现方式 1. 线程的实现方式 1.1 继承Thread class ThreadDemo01 extends Thread{Overridepublic void run() {System.out.println("当前线程:" Thread.currentThread().getName());} }1.2 实现Runnable接口 class ThreadDemo02 implements Runnable{…