C++知识点总结:5.多态和虚函数(自用)

news2024/9/28 23:38:09

多态和虚函数

  • 1. 多态和虚函数
  • 2. 引用形式的多态
  • 3. 虚函数注意事项
  • 4. 构成多态的条件
  • 5. 为什么构造函数不能是虚函数
  • 6. 虚析构函数的必要性
  • 7. 纯虚函数
  • 8. 抽象类
  • 9. 虚函数表
  • 10. typeid运算符:获取类型信息
  • 11. RTTI机制(C++运行时类型识别机制)
  • 12. 静态绑定和动态绑定


引用:
[1]C语言中文网


1. 多态和虚函数

前提:
类型转化(向上转型):当把派生类对象指针赋值给基类对象指针后,基类对象指针只能访问派生类的成员变量,但是不能访问派生类的成员函数。因为,编译器会根据指针类型来调用对应类型的成员函数。

引入问题:
这样就会限制灵活性,我们想要向上转型后,基类对象指针既可以调用派生类的成员变量,又可以调用派生类的成员函数。此时就可以通过虚函数来实现多态。

虚函数就是在需要多态的函数前加上virtual声明。只有虚函数声明后的成员函数(同名)才能多态。

多态:基类对象指针既可以调用自己的成员(成员变量和成员函数),又可以调用直接派生类或间接派生类的成员(成员变量和成员函数)。这种灵活的调用的形式就是多态

C++中,虚函数就是为了多态而存在的。

class A{
public:
    int m_a;
    virtual void display(){cout<<"class A: m_a="<<m_a<<endl;}
    A(int a);
};
A::A(int a):m_a(a){}

class B: public A{
public:
    int m_b;
    void display(){cout<<"class B: m_a="<<m_a<<", m_b="<<m_b<<endl;};
    void show(){cout<<"class B"<<endl;};
    B(int a, int b);
};
B::B(int a, int b):A(a), m_b(b){}

int main() {
    A *pa = new A(10);
    B *pb = new B(1, 2);
    pa = pb;
    pa->display();
    return 0;
}

输出:
在这里插入图片描述
可以看到基类对象指针可以调用派生类的成员函数了,也可以使用派生类的成员变量。但是要注意的是:只有virtual修饰的函数可以调用,派生类中没有遮蔽的同名虚函数不能调用,例如下例中的show函数。派生类自己新增的成员变量也不可调用,下例中的m_b
在这里插入图片描述
在这里插入图片描述

2. 引用形式的多态

引用因为本身就是指针的封装,因此引用也可以实现多态。但是,引用一经赋值就无法改变指向,所以并没有指针灵活。所以一般多态都是通过指针来实现的。

class A{
public:
    int m_a;
    virtual void display(){cout<<"class A: m_a="<<m_a<<endl;}
    A(int a);
};
A::A(int a):m_a(a){}

class B: public A{
public:
    int m_b;
    void display(){cout<<"class B: m_a="<<m_a<<", m_b="<<m_b<<endl;}
    void show(){cout<<"class B"<<endl;};
    B(int a, int b);
};
B::B(int a, int b):A(a), m_b(b){}

int main() {
    A a = A(10);
    B b = B(1, 2);
    A &ra = b;
    ra.display();
    return 0;
}

在这里插入图片描述

3. 虚函数注意事项

  1. 只需要在函数声明之前加上virtual关键字,函数定义处可以不用加
  2. 可以只将基类中的函数变成虚函数,此时派生类中的具有遮蔽关系的同名函数也会自动也变成虚函数类型。
  3. 在基类中定义虚函数,如果派生类中没有同名的函数,那么将使用基类的虚函数
  4. 只有派生类的虚函数覆盖基类的虚函数(函数名称和参数都相同)才能构成多态。
  5. 构造函数不能是虚函数。对于基类的构造函数,它仅仅是在派生类构造函数中被调用,这种机制不同于继承。也就是说,派生类不继承基类的构造函数,将构造函数声明为虚函数没有什么意义。
  6. 析构函数可以声明为虚函数,而且有时候必须要声明为虚函数。

4. 构成多态的条件

  1. 必须是继承关系。
  2. 继承关系中必须有同名的虚函数,并且是覆盖关系。(函数原型相同,包括名称和参数)。
  3. 存在基类指针,通过该指针调用虚函数。

例子:

class Base{
public:
    virtual void func(){cout<<"Base:func()"<<endl;}
    virtual void func(int){cout<<"Base:func(int)"<<endl;}
};

class Derived: public Base{
public:
    void func(){cout<<"Derived:func()"<<endl;}
    void func(char*){cout<<"Derived:func(char*)"<<endl;}
};

int main() {
    Base *p = new Derived();
    p->func(); // 调用派生类中的虚函数func()
    p->func(10);//由于派生类没有遮蔽的同名虚函数,则调用基类的虚函数func(int)
    p->func("1111"); // 编译错误 ,因为通过基类的指针只能访问从基类继承过去的成员,不能访问派生类新增的成员。
    return 0;
}

5. 为什么构造函数不能是虚函数

  1. 派生类不会继承基类的构造函数,因此,这是虚函数没有意义。
  2. 构造函数是完成对象初始化工作的,会在构造函数中初始化虚函数表和虚函数指针。如果把构造函数设置为虚函数,那么就不会存在虚函数表,因此无法查询虚函数表,也就不知道要调用哪一个构造函数。

6. 虚析构函数的必要性

析构函数用于在销毁对象时进行清理工作,可以声明为虚函数,而且有时候必须要声明为虚函数。

举例说明:

class Base{
public:
    char * a;
    Base(){a = new char[100]; cout << "BASE construct!"<<endl;}
    ~Base(){delete a; cout << "BASE destruct!"<<endl;}
};

class Derived: public Base{
public:
    char * b;
    Derived(){b = new char[100]; cout << "Derived construct!"<<endl;}
    ~Derived(){delete b; cout << "Derived destruct!"<<endl;}
};

int main() {
    Base *p = new Derived();
    delete p;
    return 0;
}

在这里插入图片描述
从输出结果就可以看到,Derived没有调用析构函数,因此,析构函数中的b的内存就没有被释放,因此会造成内存泄漏。

原因就是:多态只能调用虚函数。对于非虚函数,就回到了上篇"继承和派生14.2章节"中说的那样,根据指针类型调用对应类型的类成员函数。在该例子中,析构函数不是虚函数,因此就会根据指针类型Base,来调用Base类中的析构函数,没有调用Derived类的析构函数。

一般析构函数就是用来释放内存的。为了避免内存泄漏,一定要将基类的析构函数声明为虚函数。(这里强调的是基类,如果一个类是最终的类,那就没必要再声明为虚函数了)

修改为下例子:

class Base{
public:
    char * a;
    Base(){a = new char[100]; cout << "BASE construct!"<<endl;}
    virtual ~Base(){delete a; cout << "BASE destruct!"<<endl;} //把基类的析构函数声明为虚函数
};

class Derived: public Base{
public:
    char * b;
    Derived(){b = new char[100]; cout << "Derived construct!"<<endl;}
    ~Derived(){delete b; cout << "Derived destruct!"<<endl;}
};

int main() {
    Base *p = new Derived();
    delete p;
    return 0;
}

在这里插入图片描述
将基类的析构函数声明为虚函数后,派生类的析构函数也会自动成为虚函数。这个时候编译器会忽略指针的类型,而根据指针的指向来选择函数;当我们调用派生类的析构函数后,系统还会默认的调用基类的析构函数。

7. 纯虚函数

纯虚函数只有函数声明,没有函数体。具体形式如下:
在这里插入图片描述

此处=0不代表返回值为0,而是声明该函数是纯虚函数。

8. 抽象类

包含纯虚函数的类为抽象类。

  • 抽象类不能实例化。
  • 抽象类通常都是基类,由派生类进行实现纯虚函数。
  • 派生类必须实现纯虚函数才能实例化。
  • 抽象基类除了约束派生类的功能,还可以实现多态。
  • 只有类中的虚函数才能被声明为纯虚函数,普通成员函数和顶层函数均不能声明为纯虚函数。

9. 虚函数表

前提总结,当通过指针访问成员函数时,有以下两个状况。

  • 成员函数是非虚函数,则编译器根据指针的类型,找到对应类中的成员函数。
  • 成员函数是虚函数,且该虚函数有相同的虚函数遮蔽,那么指针指向哪一类,编译器就在该类中调用对应的虚函数。

指针之所以能找到对应的虚函数,就是通过虚函数表实现的。

如何一个类中有含有虚函数的话,在构造函数中,就会创建一个数组以及指向这个数组的指针(简称vfptr)。这个数组中每个元素都存有虚函数的入口地址,这个数组就是虚函数表,简称vtable。虚函数表指针存在对象内存模型中

在这里插入图片描述
(此图来自C语言中文网)
可以看出一个包含虚函数的类,实例化后会生成两个数组,一个是对象,一个是虚函数表。虚函数指针存在对象的第一个元素中,并指向虚函数表。虚函数表的顺序是先基类虚函数,然后派生类虚函数。当派生类中存在遮挡的同名虚函数时,则会替换基类中的对应虚函数,例如图中间,Student::display替换了People::display。

10. typeid运算符:获取类型信息

typeid是一个运算符,引用自#include <typeinfo>。可以获取一个表达式的类型信息(包含基本类型, 例如int,float等,和类类型信息,例如对象)。最终把结果存储到tpye_info对象中

具体操作方式为:

typeid(p)

type_info对象的成员:
在这里插入图片描述
在这里插入图片描述

11. RTTI机制(C++运行时类型识别机制)

有的数据类型在编译器阶段就可以确定。但是有时候数据类型在编译期间无法确定,需要在程序执行到该表达式时才能确定,例如,多态时需要根据用户输入信息,来判断指针指向。

针对这种情形,C++会在虚函数表的开头增加一个额外的type_info对象指针,指向一个数组,该数组中存放所有对象的类型信息(type_info对象)。这样,当运行时,通过对象指针p找到虚函数指针vfptr,在通过vfptr找到type_info对象的指针,从而获取类型数据。具体形式见下图:
(此图来自C语言中文网)
在这里插入图片描述
在程序运行后确定对象的类型信息的机制称为运行时类型识别(Run-Time Type Identification,RTTI)。

12. 静态绑定和动态绑定

背景:
在编译器看来,代码中的函数名和变量名其实都是地址符号,它们本身代表的是地址,因为CPU是通过地址来取值的,并不是通过函数名或变量名。编译和链接的操作其实就是在找变量和函数对应的地址,并替换成对应的地址

  • 函数绑定:找到函数名对应的地址,并将函数调用处用该地址替换,这称为函数绑定。
  • 静态绑定:在编译期间(包括链接)就能找到函数名对应的地址,并完成函数绑定的,就被称为静态绑定。
  • 动态绑定:在编译期间(包括链接)无法确定函数名对应的地址,必须等到程序运行后根据具体环境或者用户操作来进行函数绑定的,则被称为动态绑定。

通过函数重载实现多态的就是静态绑定。因为重载的函数名在编译阶段就能确定其地址,并完成函数绑定。
通过基类对象指针的指向实现多态的则是动态绑定,因为只有当运行对应指针赋值表达式时才能确认是执行那个对象的函数。例如,都是p->display(),你无法判断display()是那个地址,需要借助之前的指针指向来确定。

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

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

相关文章

加速开发利器:代码生成器如何快速生成后端接口?

最新技术资源&#xff08;建议收藏&#xff09; https://www.grapecity.com.cn/resources/ 前言 在现代软件开发中&#xff0c;重复性的增删改查逻辑代码的编写往往非常耗时且容易出错。为了提高开发效率&#xff0c;减少手动维护的成本&#xff0c;代码生成器就成为了一个非常…

MySQL常用的日期和时间函数

文章目录 概述日期和时间函数 概述 在 MySQL 中&#xff0c;有许多常用的日期和时间函数&#xff0c;可以帮助你处理和操作日期和时间字段。 日期和时间函数 获取当前日期和时间 NOW(): 返回当前的日期和时间。CURRENT_DATE() 或 CURDATE(): 返回当前的日期&#xff08;不包括…

3C产品手册制作7步骤:让消费者快速了解产品

引言 在这个信息爆炸的时代&#xff0c;如何让消费者在众多3C产品&#xff08;计算机、通讯、消费电子&#xff09;中快速了解并选择您的产品&#xff1f;一份精心制作的产品手册无疑是关键。它不仅是产品的“名片”&#xff0c;更是连接品牌与消费者的桥梁。接下来&#xff0…

2024年湖北省建筑施工特种作业人员证书延期申请/年审

2024年湖北省建筑施工特种作业人员证书延期申请/年审 建筑电工、建筑架子工、建筑起重机械司机、信号工、施工升降机等延期&#xff0c;要注意提前3个月内进行延期&#xff0c;2年1延期。 湖北特种作业考核管理系统跳转至延期申请申报页面&#xff0c;再点击“新增”按钮&…

Games101--shading 3

1.重心坐标 为什么要进行插值&#xff1f;&#xff08;因为有很多的计算实在三角形内部进行的&#xff0c;而我们需要形成一个平滑的过渡&#xff09; 插值需要插值什么内容&#xff1f;&#xff08;颜色&#xff0c;纹理映射&#xff0c;法线插值&#xff0c;可以对三角形的…

计算机毕业设计选题推荐-C语言学习辅导网站-Java/Python项目实战

✨作者主页&#xff1a;IT毕设梦工厂✨ 个人简介&#xff1a;曾从事计算机专业培训教学&#xff0c;擅长Java、Python、微信小程序、Golang、安卓Android等项目实战。接项目定制开发、代码讲解、答辩教学、文档编写、降重等。 ☑文末获取源码☑ 精彩专栏推荐⬇⬇⬇ Java项目 Py…

springboot工程,无法访问index.html主页

1、问题概述&#xff1f; 我们使用springboot开发了工程后&#xff0c;会将项目打包成jar包或者war包放到服务器端进行发布&#xff0c;但是在打包后&#xff0c;时长会出现index.html主页无法访问的情况。 先分析几种常见的主页无法访问的解决方案&#xff0c;助你解决问题&…

【wiki知识库】08.添加用户登录功能--后端SpringBoot部分

目录 一、今日目标 二、SpringBoot后端实现 2.1 新增UserLoginParam 2.2 修改UserController 2.3 UserServiceImpl代码 2.4 创建用户上下文工具类 2.5 通过token校验用户&#xff08;重要&#xff09; 2.6 创建WebMvcConfig 2.7 用户权限校验拦截器 一、今日目标 上篇…

职业本科大数据实训室

一、职业本科大数据实训室建设背景 在数字化浪潮汹涌澎湃的今天&#xff0c;大数据已跃升为引领社会进步和经济发展的新引擎。随着《中华人民共和国国民经济和社会发展第十四个五年规划和2035年远景目标纲要》的深入实施&#xff0c;数字化转型作为国家战略的重要组成部分&…

微信小程序登录获取 session_key 和 openid

一、申请测试小程序&#xff0c;只要微信扫码授权就可以申请了。 二、调用接口获取登录凭证&#xff08;code&#xff09;。通过凭证进而换取用户登录态信息&#xff0c;包括用户在当前小程序的唯一标识&#xff08;openid&#xff09;、微信开放平台账号下的唯一标识&#xf…

黔东南苗族文化展示小程序的设计与实现-计算机毕业设计源码85589

摘要 黔东南苗族文化作为中国传统文化的重要组成部分之一&#xff0c;具有悠久的历史和丰富的民俗传统。然而&#xff0c;随着社会的发展和现代化进程&#xff0c;苗族文化面临着传承和保护的挑战。为了更好地传播和展示黔东南苗族文化&#xff0c;本研究设计并实现了一款专注…

leetcode-119-杨辉三角II

原理&#xff1a; 1、初始化每行一维数组nums[1]&#xff1b; 2、从第2行开始&#xff0c;在nums的头插入0&#xff08;因为杨辉三角每行的第一个1相当于是上一行的1与其前面的0相加之和&#xff09;后进行相加操作。 代码&#xff1a;

MySQL——数据库的操作,数据类型,表的操作

MySQL——数据库的操作&#xff0c;数据类型&#xff0c;表的操作 1. 数据库的操作1.1 显示当前数据库1.2 创建数据库舍弃当前所写的SQL语句查看当前数据库服务全局的默认字符集 1.3 使用数据库1.4 查看当前操作的数据库查看MySQL的帮助 1.5 删除数据库 2. 常见数据类型2.1 数值…

Java生成Word->PDF->图片

文章目录 引言I Java生成Word、PDF、图片文档获取标签渲染数据生成文档案例II 工具类封装2.1 word 渲染和word 转 pfd2.2 pdf转成一张图片III poi-tl(word模板渲染) 标签简介文本标签{{var}}图片标签表格标签IV poi-tl提供了类 Configure 来配置常用的设置标签类型前后缀see al…

【Vue3】图片未加载成功前占位

背景 在写项目时&#xff0c;加载图片未成功前&#xff0c;会出现空白页面&#xff0c;太影响美观和体验感 解决方案 1. element ui通过slot占位符解决 2. 自定义指令 原生img标签可以通过自定义指令解决&#xff0c;img标签有onload和onerror事件&#xff0c;都是在渲染成…

svg封装使用

1、安装库 "vite-plugin-svg-icons": "^2.0.1" 2、配置svg vite.config中配置&#xff1a; 主要是配置createSvgIconsPlugin import react from vitejs/plugin-react import viteESLintPlugin from vite-plugin-eslint import { loadEnv } from vite im…

VLSI | 计算CMOS反相器的负载电容在BSIM4中的相关参数

ref. SPICE Model Parameters for BSIM4.5.0 (ubc.ca)PTM (umn.edu) 来自UMN的Microelectronics Co-design Research Group给出了晶体管PTM模型可以在SPICE仿真中使用&#xff1a;PTM (umn.edu)&#xff0c;但是由于使用Google才能下载&#xff0c;因此搬运到了这里&#xff…

电机制造业MES系统:直面行业痛点,引领智能化发展趋势

在电机制造业中&#xff0c;MES的应用具有重要意义。由于该行业的产品种类繁多&#xff0c;生产工艺复杂多变&#xff0c;生产现场的信息化管理难度较大。而通过引入MES&#xff0c;企业可以实现对生产现场的实时监控、生产进度的准确把握以及产品质量的有效控制。 电机行业信息…

什么是Docker | Docker入门及应用

1 Docker简介 1.1 什么是Docker Docker 是一个开源项目&#xff0c;诞生于 2013 年初&#xff0c;最初是 dotCloud 公司内部的一个业余项目。它基于 Google 公司推出的 Go 语言实现。 项目后来加入了 Linux 基金会&#xff0c;遵从了 Apache 2.0 协议&#xff0c;项目代码在 …

【ZooKeeper】ZooKeeper快速入门

1.ZooKeeper的概念 Zookeeper 是 Apache Hadoop 项目下的一个子项目&#xff0c;是一个树形目录服务。Zookeeper 翻译过来就是动物园管理员&#xff0c;它是用来管 Hadoop&#xff08;大象&#xff09;、Hive&#xff08;蜜蜂&#xff09;、Pig&#xff08;小猪&#xff09;的管…