C++学习笔记总结练习:多态与虚函数

1 多态

多态分类

  1. 静态多态,是只在编译期间确定的多态。静态多态在编译期间,根据函数参数的个数和类型推断出调用的函数。静态多态有两种实现的方式
    1. 重载。(函数重载)
    2. 模板。
  2. 动态多态,是运行时多态。通过虚函数机制实现(也称为重写override),使用父类的指针或者是引用,调用一个虚函数时,会根据其指向的具体对象确定调用的函数。基类和子类维护一个虚函数表,对象当中包含的虚指针,指向基类或子类的虚函数表。如果子类没有重写父类的虚函数则会直接调用父类的方法,否则调用子类重写的方法。

多态原理

  • (对象的多态性)使用基类的引用或指针调用一个函数时。无法确定该函数作用的对象是什么类型。因为它可能是一个基类的对象,也可能是一个派生类的对象。
  • (函数的多态性)如果该函数是虚函数,则直到运行时才会决定执行哪个版本。判断的依据是引用或指针所绑定的对象真实类型。
  • 函数绑定。对非虚函数的调用在编译时进行绑定。我们通过对象进行的函数调用也在编译时绑定。对象的类型是确定不变的。

也就是说多态性体现在指针和引用的不确实能够性上。但对象在内存中的状态是确定的。当且晋档通过指针或引用调用虚函数是,才会在运行时解析该调用,也只有在这种情况下对动态类型才有可能与静态类型不同。

    Bulk_quote a();//定义了对象a。这时候,无法触发多态。
    Bulk_quote* b = new Bulk_quote();//指针可以指向不同的类型的对象。
    Bulk_quote &b = a;//引用可以指向不同类型的对象。

重写与重定义对比

  • 重定义:基类中没有声明函数是虚函数。派生类中对普通函数进行了重定义。只是作用域上的覆盖,没有触发多态和动态绑定。
  • 重定义不能触发动态多态。无论指针或引用绑定的是什么对象,都会根据指针或引用的类型,调用该类型的函数。而不是使用虚指针查找虚函数表。只有调用虚函数的时候,才会去根据对象的虚函数指针,查找类中的虚函数表。
class A{
public:
    int a;
    A():a(10){};
    int real_ex(){
        return a;
    }
    virtual int virtual_ex(){
        return a;
    }
};

class B:public A{
public:
    int b;
    B():b(20){};
    int real_ex(){//重定义A的函数
        return b;
    }
    virtual int virtual_ex(){//重写A的函数
        return b;
    }
};
int main(){

    Quote p(Bulk_quote());//直接初始化,拷贝构造函数
    Quote q = Bulk_quote();//赋值初始化,拷贝构造函数
    
    // B test_b;
    // A* test = &test_b;
    A* test=new B();
    cout<<test->real_ex()<<endl;//B重定义了函数。但是A类型的指针,调用基类的函数。
    cout<<test->virtual_ex()<<endl;//B重写类函数。B类型的对象,动态绑定,调用了派生类的函数。
    return 0;
}

实现条件

运行时多态的条件:

  • 必须是继承关系
  • 基类中必须包含虚函数,并且派生类中一定要对基类中的虚函数进行重写。
  • 通过基类对象的指针或者引用调用虚函数。

注意事项

以下函数不能作为虚函数

  1. 友元函数,它不是类的成员函数
  2. 全局函数
  3. 静态成员函数,它没有this指针
  4. 构造函数,拷贝构造函数,以及赋值运算符重载(可以但是一般不建议作为虚函数)

实现原理

//参考虚函数。

2 虚函数

虚函数的定义

  • 虚函数:基类希望它的派生类自定义适合自身的版本。为了实现多态
class Animal{
public:
    virtual double price(int n)const;
}

虚函数的原理

  • 除了构造函数的非静态函数都可以是虚函数。
  • 关键字virtual只能出现在类内部的声明语句之前。不能出现在类外部的函数定义。
  • 如果把一个函数声明成虚函数,则该函数在派生类中也是隐式的虚函数。(即派生类的派生类,也需要重写次函数)
  • 派生类可以不用重写虚函数。
  • 派生类可以在它重写的虚函数前使用virtual关键字

回避虚函数的机制

  • 类中的数据成员和成员函数是相互独立的。两者没有必然的联系。
  • 成员函数通过this指针访问对象的数据成员。在继承体系中,this指针的指向是可以改变。即可以用派生类对象的this指针传递给基类的函数,从而实现派生类调用基类函数的方法。
  • 调用是不进行动态绑定,而是强迫执行虚函数的某个特定版本。通过域作用运算符实现。
//强制调用基类中定义的函数
Bulk_quote *baseP = Bulk_quote();
double u = baseP->Quote::net_price();

纯虚函数和抽象基类

 virtual void Eat() = 0;
  • 一个纯虚函数无须定义,在函数体的位置书写=0,就可以讲一个虚函数说明为纯虚函数。只能出现在类内部的函数声明语句出。在类的内部必须没有定义,在类的外部可以定义纯虚函数。
  • 含有纯虚函数的类是抽象基类。纯虚函数相当于接口,不能创建抽象基类的对象。
  • 派生类构造函数只初始化它的直接基类。

虚函数表和虚指针原理

class A
{
public:
    virtual void f();
    virtual void g();
private:
    int a
};
 
class B : public A
{
public:
    void g();
private:
    int b;
};//A、B实现省略
  • 因为A有virtual void f()和g(),所以编译器为A类准备了一个虚函数表vtableA,内容如下:
A::f 的地址
A::g 的地址
  • B因为继承了A,所以编译器也为B准备了一个虚函数表vtableB,内容如下:
A::f 的地址
B::g 的地址

注意:因为B::g是重写了的,所以B的虚表的g放的是B::g的入口地址,但是f是从上面的A继承下来的,所以f的地址是A::f的入口地址。

  • 某处有语句 B bB;的时候,编译器分配空间时,除了A的int a,B的成员int b;以外,还分配了一个虚指针vptr,指向B的虚函数表vtableB,bB的布局如下:
vptr : 指向B的虚表vtableB
int a: 继承A的成员
int b: B成员

3 虚继承和虚基类

多继承

  • 多继承(Multiple Inheritance)是指从多个直接基类中产生派生类的能力,多继承的派生类继承了所有父类的成员。尽管概念上非常简单,但是多个基类的相互交织可能会带来错综复杂的设计问题,命名冲突就是不可回避的一个。

  • 多继承时很容易产生命名冲突,即使我们很小心地将所有类中的成员变量和成员函数都命名为不同的名字,命名冲突依然有可能发生,比如典型的是菱形继承,如下图所示:

菱形继承

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • 类 A 派生出类 B 和类 C,类 D 继承自类 B 和类 C,这个时候类 A 中的成员变量和成员函数继承到类 D 中变成了两份,一份来自 A–>B–>D 这条路径,另一份来自 A–>C–>D 这条路径。

  • 下面是菱形继承的具体实现:

//间接基类A
class A{
protected:
    int m_a;
};
//直接基类B
class B: public A{
protected:
    int m_b;
};
//直接基类C
class C: public A{
protected:
    int m_c;
};
//派生类D
class D: public B, public C{
public:
    void seta(int a){ m_a = a; }  //命名冲突
    void setb(int b){ m_b = b; }  //正确
    void setc(int c){ m_c = c; }  //正确
    void setd(int d){ m_d = d; }  //正确
private:
    int m_d;
};
int main(){
    D d;
    return 0;
}
  • 这段代码实现了上图所示的菱形继承,第 25 行代码试图直接访问成员变量 m_a,结果发生了错误,因为类 B 和类 C 中都有成员变量 m_a(从 A 类继承而来),编译器不知道选用哪一个,所以产生了歧义。

  • 为了消除歧义,我们可以在 m_a 的前面指明它具体来自哪个类:

void seta(int a){ B::m_a = a; }
  • 这样表示使用 B 类的 m_a。当然也可以使用 C 类的:
void seta(int a){ C::m_a = a; }

虚继承(Virtual Inheritance)

  • 为了解决多继承时的命名冲突和冗余数据问题,C++ 提出了虚继承,使得在派生类中只保留一份间接基类的成员。

  • 在继承方式前面加上 virtual 关键字就是虚继承,请看下面的例子:

//间接基类A
class A{
protected:
    int m_a;
};
//直接基类B
class B: virtual public A{  //虚继承
protected:
    int m_b;
};
//直接基类C
class C: virtual public A{  //虚继承
protected:
    int m_c;
};
//派生类D
class D: public B, public C{
public:
    void seta(int a){ m_a = a; }  //正确
    void setb(int b){ m_b = b; }  //正确
    void setc(int c){ m_c = c; }  //正确
    void setd(int d){ m_d = d; }  //正确
private:
    int m_d;
};
int main(){
    D d;
    return 0;
}
  • 这段代码使用虚继承重新实现了上图所示的菱形继承,这样在派生类 D 中就只保留了一份成员变量 m_a,直接访问就不会再有歧义了。

  • 虚继承的目的是让某个类做出声明,承诺愿意共享它的基类。其中,这个被共享的基类就称为虚基类(Virtual Base Class),本例中的 A 就是一个虚基类。在这种机制下,不论虚基类在继承体系中出现了多少次,在派生类中都只包含一份虚基类的成员。

  • 现在让我们重新梳理一下本例的继承关系,如下图所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • 观察这个新的继承体系,我们会发现虚继承的一个不太直观的特征:必须在虚派生的真实需求出现前就已经完成虚派生的操作。在上图中,当定义 D 类时才出现了对虚派生的需求,但是如果 B 类和 C 类不是从 A 类虚派生得到的,那么 D 类还是会保留 A 类的两份成员。

  • 换个角度讲,虚派生只影响从指定了虚基类的派生类中进一步派生出来的类,它不会影响派生类本身。

虚继承在C++标准库中的实际应用

  • 在实际开发中,位于中间层次的基类将其继承声明为虚继承一般不会带来什么问题。通常情况下,使用虚继承的类层次是由一个人或者一个项目组一次性设计完成的。对于一个独立开发的类来说,很少需要基类中的某一个类是虚基类,况且新类的开发者也无法改变已经存在的类体系。

  • C++标准库中的 iostream 类就是一个虚继承的实际应用案例。iostream 从 istream 和 ostream 直接继承而来,而 istream 和 ostream 又都继承自一个共同的名为 base_ios 的类,是典型的菱形继承。此时 istream 和 ostream 必须采用虚继承,否则将导致 iostream 类中保留两份 base_ios 类的成员。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

虚基类成员的可见性

  • 因为在虚继承的最终派生类中只保留了一份虚基类的成员,所以该成员可以被直接访问,不会产生二义性。此外,如果虚基类的成员只被一条派生路径覆盖,那么仍然可以直接访问这个被覆盖的成员。但是如果该成员被两条或多条路径覆盖了,那就不能直接访问了,此时必须指明该成员属于哪个类。

  • 以图2中的菱形继承为例,假设 A 定义了一个名为 x 的成员变量,当我们在 D 中直接访问 x 时,会有三种可能性:

    • 如果 B 和 C 中都没有 x 的定义,那么 x 将被解析为 A 的成员,此时不存在二义性。
    • 如果 B 或 C 其中的一个类定义了 x,也不会有二义性,派生类的 x 比虚基类的 x 优先级更高。
    • 如果 B 和 C 中都定义了 x,那么直接访问 x 将产生二义性问题。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

C#,《小白学程序》第八课:列表(List)应用之二“编制高铁列车时刻表”

1 文本格式 /// <summary> /// 车站信息类 class /// </summary> public class Station { /// <summary> /// 编号 /// </summary> public int Id { get; set; } 0; /// <summary> /// 车站名 /// </summary&g…

在Ubuntu Linux系统上安装RabbitMQ服务并解决公网远程访问问题

文章目录 前言1.安装erlang 语言2.安装rabbitMQ3. 内网穿透3.1 安装cpolar内网穿透(支持一键自动安装脚本)3.2 创建HTTP隧道 4. 公网远程连接5.固定公网TCP地址5.1 保留一个固定的公网TCP端口地址5.2 配置固定公网TCP端口地址 前言 RabbitMQ是一个在 AMQP(高级消息队列协议)基…

系列五、Java操作RocketMQ简单消息之同步消息

一、概述 同步消息的特征是消息发出后会有一个返回值&#xff0c;即RocketMQ服务器收到消息后的一个确认&#xff0c;这种方式非常安全&#xff0c;但是性能上却没有那么高&#xff0c;而且在集群模式下&#xff0c;也是要等到所有的从机都复制了消息以后才会返回&#xff0c;适…

QT(8.30)常用类与组件,实现登录界面

1.作业&#xff1a; 完成一个登录界面(图片未附带): 头文件: #ifndef WIDGET_H #define WIDGET_H#include <QWidget>#include <QLineEdit>//行编辑器#include<QIcon>//图标#include<QLabel>//标签#include<QPushButton>//按钮#include<QIc…

GA遗传算法

储备知识 GA算法主要解决数学模型中最优化的搜索算法&#xff0c;是进化算法中的一种&#xff0c;基因算法借鉴了自然界基因的遗传的主要现象&#xff0c;分别为遗传&#xff0c;变异&#xff0c;自然选择&#xff0c;杂交等。 GA算法参数 GA算法的参数如下所示。 种群规模…

JAVA 求最小公因数

JAVA 求最小公因数 文章目录 JAVA 求最小公因数方法一&#xff1a;枚举法的第一种方法一&#xff1a;枚举法的第二种方法二&#xff1a;展转相除法(欧几里德算法)方法三&#xff1a;递归拓展 求最小公倍数公式为综合 辗转相除法递归 求n个数的最大公约数和最小公倍数 题目&…

一百六十九、Hadoop——Hadoop退出NameNode安全模式与查看磁盘空间详情(踩坑,附截图)

一、目的 在海豚跑定时跑kettle的从Kafka到HDFS的任务时&#xff0c;由于Linux服务器的某个文件磁盘空间满了&#xff0c;导致Hadoop的NodeName进入安全模式&#xff0c;此时光执行hdfs dfsadmin -safemode leave命令语句没有效果&#xff08;虽然显示Safe mode is OFF&#x…

EFLK日志平台(filebeat-->kafka-->logstash-->es-->kiabana)

ELK平台是一套完整的日志集中处理解决方案&#xff0c;将 ElasticSearch、Logstash 和 Kiabana 三个开源工具配合使用&#xff0c; 完成更强大的用户对日志的查询、排序、统计需求。 安装顺序 1.安装es 7.17.12 2.安装kibana 7.17.12 3.安装x-pack 保证以上调试成功后开始下面…

C语言:动态内存(一篇拿捏动态内存!)

目录 学习目标&#xff1a; 为什么存在动态内存分配 动态内存函数&#xff1a; 1. malloc 和 free 2. calloc 3. realloc 常见的动态内存错误&#xff1a; 1. 对NULL指针的解引用操作 2. 对动态开辟空间的越界访问 3. 对非动态开辟内存使用free释放 4. 使用free释…

汽车自适应巡航系统控制策略研究

目 录 第一章 绪论 .............................................................................................................................. 1 1.1 研究背景及意义 ..........................................................................................…

Spring Boot 中 Nacos 配置中心使用实战

官方参考文档 https://nacos.io/zh-cn/docs/quick-start-spring-boot.html 本人实践 1、新建一个spring boot项目 我的spirngboot版本为2.5.6 2、添加一下依赖 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-…

【Locomotor运动模块】瞬移

文章目录 一、原理二、两种类型1、Instant(立刻)2、Dash&#xff08;猛冲&#xff09; 三、瞬移区域、瞬移点1、瞬移区域2、瞬移点 一、原理 抛物线指针选择好目标位置&#xff0c;然后告诉瞬移预设体&#xff1a;你想法把游戏区域弄到目标位置来 解释&#xff1a;抛物线指针选…

uniapp 微信小程序仿抖音评论区功能,支持展开收起

最近需要写一个评论区功能&#xff0c;所以打算仿照抖音做一个评论功能&#xff0c;支持展开和收起&#xff0c; 首先我们需要对功能做一个拆解&#xff0c;评论区功能&#xff0c;两个模块&#xff0c;一个是发表评论模块&#xff0c;一个是评论展示区。接下来对这两个模块进行…

最新ChatGPT程序源码+AI系统+详细图文部署教程/支持GPT4.0/支持Midjourney绘画/Prompt知识库

一、AI系统 如何搭建部署人工智能源码、AI创作系统、ChatGPT系统呢&#xff1f;小编这里写一个详细图文教程吧&#xff01;SparkAi使用Nestjs和Vue3框架技术&#xff0c;持续集成AI能力到AIGC系统&#xff01; 1.1 程序核心功能 程序已支持ChatGPT3.5/GPT-4提问、AI绘画、Mi…

Kubernetes技术--使用kubeadm搭建高可用的K8s集群(贴近实际环境)

1.高可用k8s集群架构(多master) 2.安装硬件要求 一台或多台机器,操作系统 CentOS7.x-86_x64 硬件配置:2GB或更多RAM,2个CPU或更多CPU,硬盘30GB或更多 注: 这里属于教学环境,所以使用三台虚拟机模拟实现。 3.部署规划 4.部署前准备 (1).关闭防火墙 systemctl stop fi…

数据结构|栈和队列以及实现

栈和队列 一、栈1.1栈的概念及结构1.2栈的实现 二、队列2.1队列的概念及结构2.2队列的实现 一、栈 1.1栈的概念及结构 栈&#xff1a;一种特殊的线性表&#xff0c;其只允许在固定的一端进行插入和删除元素操作。进行数据插入和数据删除的一端称为栈顶&#xff0c;另一端称为栈…

深度学习入门(Python)学习笔记1

第1章 Python入门 1.1python是什么 Python是一个简单、易读、易记的编程语言&#xff0c;而且是开源的&#xff0c;可以免费地自由使用。 使用Python不仅可以写出可读性高的代码&#xff0c;还可以写出性能高&#xff08;处理速度快&#xff09;的代码。 再者&#xff0c;在…

7.2 项目2 学生通讯录管理:文本文件增删改查(C 版本)(自顶向下设计+断点调试) (A)

C自学精简教程 目录(必读) 该作业是 作业 学生通讯录管理&#xff1a;文本文件增删改查&#xff08;C版本&#xff09; 的C 语言版本。 具体的作业题目描述&#xff0c;要求&#xff0c;可以参考 学生通讯录管理&#xff1a;文本文件增删改查&#xff08;C版本&#xff09;。…

Windows下Redis的安装和配置

文章目录 一,Redis介绍二,Redis下载三,Redis安装-解压四,Redis配置五,Redis启动和关闭(通过terminal操作)六,Redis连接七,Redis使用 一,Redis介绍 远程字典服务,一个开源的,键值对形式的在线服务框架,值支持多数据结构,本文介绍windows下Redis的安装,配置相关,官网默认下载的是…

iTOP-RK3588开发板Android12 设置系统默认不休眠

修改文件&#xff1a; device/rockchip/rk3588/overlay/frameworks/base/packages/SettingsProvider/res/values/defaults. xml 文件&#xff0c;如下图所示&#xff1a; - <integer name"def_screen_off_timeout">60000</integer> <integer name&q…