C++中的多态

news2024/11/19 3:45:45

目录

多态的定义及实现

多态的构成条件

虚函数 

虚函数重写

虚函数重写的两个例外:

override 和 final(C++11)

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

抽象类 

多态的原理 

虚函数表


多态的概念:字面的意思就是多种形态,完成某个行为,当不同的对象去完成时会产生出不同的状态。当类之间存在层次结构,并且类之间是通过继承关联时,就会用到多态。C++ 多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数。

多态的定义及实现

多态分为编译时多态和运行时多态,也叫早期绑定和晚期绑定。编译时多态是早期绑定,主要通过重载实现。

多态的构成条件

多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如Student继承了 Person。Person对象买票全价,Student对象买票半价。

在继承中要构成多态还有两个条件:

1. 必须通过基类的指针或者引用调用虚函数

2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

虚函数 

虚函数是在基类中使用关键字 virtual 声明的函数。在派生类中重新定义基类中定义的虚函数时,会告诉编译器不要静态链接到该函数。

static和virtual不能同时使用。静态成员函数就不能设置为虚函数,静态成员函数与具体对象无关,属于整个类,核心关键是没有隐藏的this指针,可以通过类名::成员函数名直接调用,此时没有this无法拿到虚表,就无法实现多态,因此不能设置为虚函数。实现多态是要付出代价的,如虚表,虚表指针等,所以不实现多态就不要有虚函数了。

虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的 返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。

虚函数重写

虚函数的重写时,继承父类虚函数的接口,重写的是虚函数的实现。

class Person {
public:
    virtual void BuyTicket() 
    {
        cout << "买票-全价" << endl;
    }
};

class Student : public Person {
public:
    // 子类中满足三同(函数名、参数、返回值)的虚函数,叫做重写(覆盖)
    virtual void BuyTicket() 
    {
        cout << "买票-半价" << endl;
    }

   /*void BuyTicket()

    {
        cout << "买票-半价" << endl; 
     }*/
};

虚函数的重写允许,两个都是虚函数或者父类是虚函数,再满足虚函数返回值类型、函数名字、参数列表,就构成重写。在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议使用。

 构成多态,跟p的类型没有关系,传的哪个类型的对象,调用的就是这个类型的虚函数 -- 跟对象有关。Person对象调用Person类的虚函数,Student对象调用Student类的虚函数。

不构成多态,调用就是p类型的函数 -- 跟类型有关。 Person对象和Student对象调用的都是Person类的虚函数,因为Func()参数类型是Person。

虚函数重写的两个例外

1. 协变(基类与派生类虚函数返回值类型不同)

派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。

2. 析构函数的重写(基类与派生类析构函数的名字不同)

如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字, 都与基类的析构函数构成重写,虽然基类与派生类析构函数名不相同, 看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。 

动态申请的对象,如果给了父类指针管理,那么需要析构函数是虚函数。 

只有派生类Student的析构函数重写了Person的析构函数,delete对象调用析构函数,才能构成多态,才能保证p1和p2指向的对象正确的调用析构函数。 

override 和 final(C++11)

如何设计一个不能被继承的类?

C++98中可以间接限制,子类构成函数无法调用父类构造函数初始化成员,没办法实例化对象 

class A
    {
    private:
        A(int a = 0)
            :_a(a)
        {}
    
    public:
        static A CreateOBj(int a = 0)
        {
            //new A;
            return A(a);
        }
    protected:
        int _a;
    };
class B : public A
    {
    
    };
int main()
{
    A aa = A::CreateOBj(10);
    //B bb;  报错
    return 0;
}

在C++11 可以使用final关键字直接限制

1. final:修饰虚函数,表示该虚函数不能再被重写

2. override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。

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

重载:两个同名函数在同一作用域,同名函数的形式参数(指参数的个数、类型或者顺序)必须不同(返回值没有要求)。

覆盖(重写):两个同名函数分别在基类和派生类的作用域,同名函数的参数和返回值必须相同(协变例外),两个同名函数必须都是虚函数

隐藏(重定义):两个同名函数分别在基类和派生类的作用域,两个同名函数不构成重写就是重定义。

抽象类 

一个类型,如果一般在现实世界中,没有具体的对应实物就定义成抽象类比较好。

在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口 类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生 类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

父类中纯虚函数一般只声明不实现,实现没有意义。调用的时候调用的是子类的函数。 

纯虚函数的类本质上强制子类完成虚函数的重写。

接口继承和实现继承

普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实 现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成 多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。

多态的原理 

虚函数表

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

    virtual void Func2()
    {
        cout << "Func2()" << endl;
    }

private:
    int _b = 1;
    char _ch = 'A';
};

int main()
{
    cout << sizeof(Base) << endl; //16 
    Base bb;

    return 0;
}

除了_b和_ch成员,还多一个__vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。

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 = 1;
};
class Derive : public Base
{
public:
    virtual void Func1()
    {
        cout << "Derive::Func1()" << endl;
    }
private:
    int _d = 2;
};
int main()
{
    Base b;
    Derive d;
    return 0;
}

1. 派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚 表指针也就是存在部分的另一部分是自己的成员。

2. 基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表 中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。

3. Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函数,所以不会放进虚表。

4. 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。

5. 派生类的虚表生成:

      a.先将基类中的虚表内容拷贝一份到派生类虚表中

      b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数

      c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。

6. 虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。

出满足多态以后的函数调用,不是在编译时确定的,是运行起来以后到对象的中取找的。不满足多态的函数调用时编译时确认好的。 

动态绑定与静态绑定

1.静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态, 比如:函数重载

2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体 行为,调用具体的函数,也称为动态多态。

编译期间,编译器主要检测代码是否违反语法规则,此时无法知道基类的指针或者引用到底引用那个类的对象,也就无法知道调用那个类的虚函数。在程序运行时,才知道具体指向那个类的对象,然后通过虚表调用对应的虚函数,从而实现多态。

常见面试题

1. 什么是多态?

2. 什么是重载、重写(覆盖)、重定义(隐藏)?

3. 多态的实现原理?

4.inline函数可以是虚函数吗?

        可以,调用时,如果不构成多态,这个函数保持inline属性;如果构成多态,编译器就忽略inline属性,这个函数就不再是 inline,因为虚函数要放到虚表中去,调用是去对象的虚函数表中找到虚函数地址,实现调用时无法使用inline属性。

5. 静态成员可以是虚函数吗?

        不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。

6. 构造函数可以是虚函数吗?

        不能,虚函数的意义是构成多态调用,多态调用要去虚函数表中查找虚函数,对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。

7. 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?

        可以,并且最好把基类的析构函数定义成虚函数。

8. 对象访问普通函数快还是虚函数更快?

        如果是普通对象,不构成多态,都是编译器确定调用函数的地址,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快,普通函数编译时直接确定地址,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。

9. 虚函数表是在什么阶段生成的,存在哪的?

        虚函数表是在编译阶段就生成的,一般情况下存在代码段(常量区)的。

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

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

相关文章

利用MDK的FLM文件生成通用flash驱动

文章目录前言一、FLM文件是什么&#xff1f;二、FLM文件结构1.FlashPrg.c2.FlashPrg.c三、解析FLM文件1.解析flm文件四、设计flash驱动抽象层五、快速使用前言 在进行Flash操作时&#xff0c;一般我们需要设计一套Flash抽象层&#xff0c;至少要包括flash的init&#xff0c;re…

C++STL之stack容器和优先级队列底层详解

一&#xff1a;stack容器1.1&#xff1a;容器适配器概念&#xff1a;容器适配器是用特定类封装作为其底层的容器&#xff0c;并提供一组特定的成员函数来访问元素&#xff0c;stack的底层容器可以是任意的类模板&#xff0c;或者一些其他的容器类&#xff0c;这些容器类应该支持…

JavaScript详解

目录 1.JavaScript基础知识 1.1 JavaScript概述 1.1.1 JavaScript历史 1.1.2 JavaScript是什么 1.1.3 JavaScript的作用 1.1.4 HTML/CSS/JS的关系 1.1.5 基本特点 1.1.6 浏览器执行JS简介 1.1.7 JavaScript脚本语言的组成 1.2 JavaScript使用方法 1.3 JavaScript输…

论文创新及观点2

题目 Zero-Shot Visual Recognition using Semantics-Preserving Adversarial Embedding Networks 摘要 基于visual-semantic embedding&#xff0c;的ZSL方法存在信息损失(semantic loss),的问题&#xff0c;在训练过程中&#xff0c;如果某些语义信息对分类的区分性不大&a…

「项目管理」如何做好研发FO角色?

角色定位 FO &#xff08;Feature Owner&#xff09;&#xff0c;项目某一阶段/版本迭代生命周期的总负责人。基于从需求发起、研发接入、上线等项目过程阶段&#xff0c;可以根据职责本位不同来推荐具体项目成员、干系人担任FO角色&#xff0c;前端、 客户端 、服务端、测试、…

Spring事务和事务传播机制

⭐️前言⭐️ 事务是作为一名后端程序员&#xff0c;必须去要了解清楚的东西&#xff0c;因为它决定了程序的正常运行以及与程序运行效率之间的权衡&#xff0c;这篇文章我们就来了解一下Spring事务和事务传播机制。 &#x1f349;欢迎点赞 &#x1f44d; 收藏 ⭐留言评论 &am…

【阶段三】Python机器学习08篇:机器学习项目实战:决策树分类模型

本篇的思维导图: 决策树模型简介 决策树模型的基本原理是通过对一系列问题进行if/else的推导,最终实现相关决策。 下图所示为一个典型的决策树模型——员工离职预测模型的简单演示。该决策树首先判断员工满意度是否小于5,若答案为“是”,则认为该员工会离…

07、ThingsBoard本地打镜像上传到harbor

1、Harbor是什么? Harbor是为企业用户设计的容器镜像仓库开源项目,包括了权限管理(RBAC)、LDAP、审计、安全漏洞扫描、镜像验真、管理界面、自我注册、HA等企业必需的功能,同时针对中国用户的特点,设计镜像复制和中文支持等功能。 2、安装Harbor 2.1、下载地址 Tags g…

【看表情包学Linux】探讨项目构建问题 | Makefile | 依赖关系与依赖方法 | 伪目标 PHONY

&#x1f923; 爆笑教程 &#x1f449; 《看表情包学Linux》&#x1f448; 猛戳订阅 &#x1f525; &#x1f4ad; 写在前面&#xff1a;本章我们要学习的是 makefile。会不会写 makefile&#xff0c;从一个侧面说明一个人是否具备完成大型工程的能力。一个工程中的源文件不计…

学计算机专业的你后悔了吗?

先说结论&#xff1a;不后悔。当年高考的时候&#xff0c;老师和我们说&#xff0c;将来环境、船舶、园林专业肯定特别吃香&#xff0c;填志愿的时候记得都选上。现在来看&#xff0c;这几个专业妥妥的天坑专业&#xff0c;前段时候还认识一个船舶专业的博士报培训班转计算机&a…

【GNN】图基本知识代码、nxworkx包的基本使用

一个写得很好的博客&#xff1a; 图或网络中的中心性&#xff1a;点度中心性、中介中心性、接近中心性、特征向量中心性、PageRank 特征向量中心性&#xff08;eigenvector centrality&#xff09; 特征向量中心性的基本思想是&#xff0c;一个节点的中心性是相邻节点中心性的函…

Kali Linux渗透测试小实践——Metasploit与后门木马

一、环境和工具准备 1.Metasploit Metasploit是一款开源的安全漏洞检测工具&#xff0c;可以帮助安全和IT专业人士识别安全性问题&#xff0c;验证漏洞的缓解措施&#xff0c;并管理专家驱动的安全性进行评估&#xff0c;提供真正的安全风险情报。这些功能包括智能开发&#…

MySQL常用基础 - 小白必看

MySQL数据库基本操作 一、DDL 概念&#xff1a;是一个数据定义语言 该语言部分包括&#xff1a; 1、对数据库的常用操作 创建数据库&#xff1a; 1、create database 数据库名 (直接删除) 2、create database if not exists 数据库名 &#xff08;判断数据库是否存在&…

Allegro174版本新功能介绍之去掉拷贝自动偏移功能

Allegro174版本新功能介绍之去掉拷贝自动偏移功能 Allegro升级到了174版本的时候,在用拷贝功能的时候,不管怎么拷贝都会出现偏差,即便是拷贝坐标的方式,如下图 那是因为174新增了一个自动粘贴的功能,按照下面的步骤可以去除自动粘贴的功能,具体操作如下 选择Setup选择Use…

1.《计算机组成原理》之初识计算机系统

一、基本内容&#xff1a;基本部件的结构和组织方式。基本运算的操作原理。基本部件和单元的设计思想。二、特色&#xff1a;计算机组成的一般原理&#xff0c;不以具体机型为依托。采用自顶向下的方式、层层细化。三、补充&#xff1a;一、基本内容&#xff1a;1.这门课讲什么…

【JavaScript】JavaScript中的时间函数

JavaScript中的时间函数 关于JavaScript里面时间的函数&#xff1a; 方法描述Date()返回当日的日期和时间。getDate()从Date对象返回一个月中的某一天&#xff08;1 ~ 31&#xff09;。getDay()从Date对象返回一周中的某一天&#xff08;0 ~ 6&#xff09;。getMonth()从Date…

LeetCode 热题HOT100-两数之和(C语言)

LeetCode 热题HOT100-两数之和&#xff08;C语言&#xff09; 作为一名程序语言的学习者&#xff0c;刷力扣我想是必要经历的一条路&#xff0c;所以我也在这里分享刷题后所得知识&#xff0c;也可以帮助更多人理解题意。 给定一个整数数组 nums 和一个整数目标值 target&#…

Zynq PS之UART调试

在创建应用工程时&#xff0c;选择Hello World模板即可&#xff0c;以XCZU21DR开发。 硬件原理图如下&#xff1a; Vivado中添加&配置IP 添加Zynq UltraScale MPSoc IP。 UART设置 DDR配置 硬件设计中选择的DDR4型号是MT40A2G8VA-062E IT&#xff0c;DDR配置如下图&…

6、SySeVR复现——Data preprocess(下)

紧接着上篇SySeVR复现——Data preprocess&#xff08;上&#xff09;5、SySeVR复现——Data preprocess&#xff08;上&#xff09;_sliver呀的博客-CSDN博客 目录 5、训练Word2vec模型 6、token向量化且划分数据集 7、固定每一个切片的长度 5、训练Word2vec模型 对应crea…

Jetpack Compose中使用Notification

发送通知相关的主要有两个关键的类 NotificationCompat.Builder 和 NotificationManagerCompat 为方便使用&#xff0c;首先定义一个扩展工具类来管理通知 const val MAIN_CHANNEL_ID "MainChannel ID" const val MAIN_CHANNEL "MainChannel"fun Contex…