C++ 多态详解附图与代码

news2024/11/16 13:40:22

一、多态

1.1 什么是多态

多态是面向对象编程中的一个重要概念,它允许在不同类型的对象上执行相同的操作,并根据对象的实际类型来决定具体执行哪个操作。通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态

1.2 多态构成的条件

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

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

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

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

二、虚函数

2.1 什么是虚函数

虚函数是在基类中声明为虚拟(virtual)的成员函数,其目的是通过动态绑定来在面向对象编程中实现多态性。多态性允许我们在编写代码时,使用基类指针或引用来处理各种不同的派生类对象,而不需要考虑对象的具体类型。

只有非静态的成员函数前加virtual才能是虚函数。

class Person
{
public:
    virtual void BuyTicket() { cout << "买票-全价" << endl; }
    // 这里BuyTicket就是虚函数了
private:
    int _a;
    char _ch;
};

2.2 虚函数的重写

2.2.1 如何实现重写

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

注意

在重写基类虚函数时,派生类的虚函数在不加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();
}

int main()
{
    Person ps;
    Student st;

    Func(ps); //  "买票-全价"
    Func(st); //  "买票-半价"

    return 0;
}

满足多态时:函数调用跟对象有关,指向哪个对象就调用他的虚函数。

不满足多态时:函数调用对类型有关,调用的类型是谁,调用就是谁的。

2.2.2 被重写的部分是什么

继承下来的是函数名、类型、返回值,重写的是函数体。

class A
{
public:
    virtual void func(int val = 1)
    {
        cout << "A->" << val << endl;
    }
    virtual void test() { func(); }
};

class B :public A
{
public:
    void func(int val = 0)
    {
        cout << "B->" << val << endl;
        //继承下来的是函数名、类型、返回值
        //重写的是函数体
    }
};

int main()
{
    B* p = new B;
    p->test();
    //继承
    return 0;
}

2.2.3 重载、重写、重定义的辨析

  1. 重载:

  • 定义:重载指的是在同一个作用域内使用相同的函数名,但参数列表不同的情况。

  • 特点:

(1)函数名称相同,但参数列表不同(数量、顺序、类型)。

(2)返回类型可以相同也可以不同。

(3)可以发生在同一个类中,也可以是类的不同成员函数。

  • 作用:通过提供不同的参数列表,可以实现对不同类型或数量的参数进行处理。

  1. 重写:

  • 定义:重写是指在派生类中重新定义(覆盖)基类的虚函数

  • 特点:

(1)函数名称、参数列表和返回类型都必须与基类中的虚函数完全相同。

(2)在派生类中使用 override 关键字显式声明重写。

(3)基类的函数必须被声明为虚函数。

  • 作用:通过重写基类的虚函数,可以实现对派生类特有行为的定制化,从而实现多态

  1. 重定义:

  • 定义:重定义指的是在派生类中定义一个与基类中的函数同名但不是虚函数的函数。

  • 特点:

(1)函数名称和参数列表必须与基类中的函数相同,但不能使用 override 关键字。

(2)返回类型可以相同也可以不同。

(3)不需要在基类中将该函数声明为虚函数。

  • 作用:重定义是一种在派生类中重新实现基类函数的方式,但它不支持多态性

总结:

  1. 重载是指在同一个作用域内使用相同函数名但参数列表不同的情况,用于处理不同类型或数量的参数。

  1. 重写是指在派生类中重新定义(覆盖)基类的虚函数,用于定制派生类特有行为。

  1. 重定义是指在派生类中定义一个与基类中函数同名但不是虚函数的函数,不支持多态性。

2.3 virtual使用情况的辨析

1.在继承中

可以在菱形继承中,去完成虚继承,解决数据冗余和二义性。(下面附之前博客)

C++ 继承详解

2.在多态中

修饰原函数,为了完成虚函数的重写,使其满足多态的条件之一。

两个地方使用了同一个关键字,但他们互相之间没有关联。

三、C++11的override和final

override 和 final 是 C++11 中引入的两个关键字,用于在派生类中对基类的虚函数进行重写或者限制。

3.1 override

override 关键字用于指示派生类中的函数是对基类中的虚函数进行重写(override)。它可以帮助编译器检查派生类中的函数是否正确地重写了基类中的虚函数。如果使用了 override 关键字但实际上没有重写成功(比如你重写了虚函数但是函数名写错了你却没发现,导致实际上并没有完成虚函数的重写),则编译器会给出错误提示

简单说它就是用来检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。

class Base 
{
public:
    virtual void f() 
    {
        // ...
    }
};

class Derived : public Base 
{
public:
    void f() override // 使用 override 关键字检查
    { 
        // ...
    }
};

3.2 finnal

final 关键字用于限制派生类继承或者覆盖某个函数。当我们将一个函数声明为 final 时,它将无法被派生类再次重写或者覆盖。这样可以确保某个函数在派生类中保持不变,提高程序的安全性和稳定性。

final 关键字只能用于修饰虚函数和派生类

(1)final:修饰虚函数,表示该虚函数不能再被继承。

(2)final修饰派生类,表示派生类类不再能被继承。

class Base
{
public:
    virtual void f() final 
    {  // 使用 final 关键字限制虚函数不可重写
        // ...
    }
};

class Derived : public Base
{
public:
    // 下面的代码将导致编译错误,因为派生类无法重写被声明为 final 的函数
    void f() override 
    {
        // ...
    }
};

在C++中,final 关键字不能直接用于修饰基类。final 关键字的作用是防止派生类进一步继承或者覆盖某个函数。它用于在派生类中声明一个函数是最终版本,不允许再次重写或者覆盖。这样可以提高代码的安全性和稳定性,确保某个函数或者类不会被修改或扩展。

对于基类来说,并没有特定的关键字用于标记基类为最终类基类本身就具有被继承的特性,其他类可以通过继承基类来创建新的子类。如果不希望基类被继承,可以采用以下两种方式之一:

1.私有构造函数,禁止直接继承

class Base 
{
private:
    Base() { } 
    // ...
};

2.将基类声明为抽象类。抽象类不能直接实例化,只能作为接口供其他类进行继承和实现。

class Base 
{
public:
    virtual void foo() = 0; // 纯虚函数,使基类成为抽象类
    // ...
};

四、抽象类

4.1 什么是抽象类

在虚函数的后面写上=0,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类的特点是不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。

纯虚函数规范了派生类必须重写,因为派生类只有重写后才能实例化出对象,另外纯虚函数更体现出了接口继承。

所以在基类中使用纯虚函数的作用

  1. 强制继承它的子类必须去完成对虚函数的重写。

  1. 表示抽象的类型,即在现实中没有对应的实体。

#include <iostream>
using namespace std;

// 抽象类
class AbstractClass 
{
public:
    // 纯虚函数
    virtual void f() = 0;

    // 普通成员函数
    void Print() 
    {
        cout << "抽象类-P" << endl;
    }
};

// 抽象类的派生类
class ConcreteClass : public AbstractClass 
{
public:
    // 实现纯虚函数
    virtual void f() override 
    {
        cout << "具体类-f" << endl;
    }
};

int main() 
{
    // 抽象类不能被实例化
    // AbstractClass a;  // 错误

    // 使用派生类创建对象
    ConcreteClass concrete;

    // 调用纯虚函数和普通成员函数
    concrete.f();       // 输出:"具体类-f"
    concrete.Print();   // 输出:"抽象类-P"

    return 0;
}

4.2 接口继承和实现继承

在面向对象编程中,接口继承和实现继承是两种不同的继承方式。

1.接口继承

接口继承是指一个类继承另一个类的接口,它主要关注类之间的接口兼容性。继承下来的是函数名、类型、返回值,通过接口继承,子类可以拥有与父类相同的接口,从而实现多态性和代码的灵活性。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。

在C++中,接口继承通常通过抽象类来实现。父类是一个抽象类,其中包含纯虚函数作为接口,子类继承该抽象类并实现其纯虚函数。通过这种方式,子类可以共享一个公共的接口,并根据自身的需求来实现具体的功能。接口继承使得子类能够以相同的方式被使用,提高了代码的可重用性和可扩展性。

2.实现继承

普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。

五、多态的原理

5.1 虚函数表

当基类中的成员函数被声明为虚函数时,它们的调用就可以在运行时根据对象的动态类型来确定。这个过程称为动态绑定或后期绑定,它能够保证在运行时选择正确的函数实现。

C++ 中实现多态的关键机制是虚表(vftable)和虚表指针(vfptr)。当一个类声明了虚函数时,编译器会为该类生成一个虚表,其中存储了指向虚函数地址的指针。同时,编译器还会在每个对象中添加一个虚表指针,指向该对象对应的虚表。

规定:由于虚表中可能存有多个虚函数的地址,所以规定了虚表结束后会加空指针标记结束。

验证一下虚表指针的存在:

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

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

private:
    int _a = 1;
};

int main()
{
    Base b;
    cout << sizeof(b) << endl; // 8(32位下)

    return 0;
}

为什么b的大小是8呢?按理说函数最终都编译成了指令放到了代码段,所以说只需要计算a的大小,再考虑内存对齐就可以了。

实际原因是b中多了一个虚表指针,这个指针指向了一个表,虚表实际就是一个存储了虚函数地址的指针数组。在这里虚函数是F1和F2,那么虚表中也就会存储了它们的地址。

5.2 多态的实现

#include <iostream>
using namespace std;

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

    int _p = 1;
};
class Student :public Person
{
public:
    virtual void BuyTicket() { cout << "买票-半价" << endl; }
    
    int _s = 2;
};

int main()
{
    Person p;
    Student st;

    return 0;
}

上面代码Student继承了Person,Person中声明了买票的虚函数,当Student完成了对它的重写之后,子类中存储的虚表就发生了变化。

当满足了多态的两个条件后:

1.虚函数的重写

2.父类的指针或者引用调用

程序在运行时就会到指向的对象的虚表中查找要调用的虚函数的地址来进行调用。

注意,必须要满足这两个条件才能构成多态。

下面我们来实现一下:

#include <iostream>
using namespace std;

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

    int _p = 1;
};
class Student :public Person
{
public:
    virtual void BuyTicket() { cout << "买票-半价" << endl; }
    
    int _s = 2;
};

void f(Person& p)
{
    p.BuyTicket();
}

int main()
{
    Person p;
    Student st;
    
    f(p);  // 买票-全价
    f(st); // 买票-全价

    return 0;
}

p指向person类型的对象就调用person虚函数, p指向student类型的对象调用的就是student虚函数

p如果指向父类,就会到父类的虚函数表中去找,找到的就是父类的虚函数。p如果指向子类,会发生切片(将子类中属于父类的一部分赋给父类),那么对于该引用看到的还是一个父类对象,只不过这个对象是子类切出来的。但是此时虚表指针指向指向的虚表里面已经是子类的虚函数了,因此会调用子类的虚函数。

因此指向谁就调用谁的真正原理就是指向谁就到谁的虚表里去找对应的虚函数, 而这个虚函数已经是写好了的,重写了之后其实就是虚表里面会变成子类虚函数的地址。

因此重写还有另一个概念是覆盖,重写意为只继承了父类的函数名参数返回值,然后把函数的实现给重写了, 而覆盖在原理层指明了是将虚函数表给覆盖了,没有完成重写也就不会覆盖。

5.3 静态绑定与动态绑定

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

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

一般我们说的多态指的是动态多态。

5.4 虚基表与虚表的区别

  • 虚表(vtable)用于实现单一继承中的动态绑定,存储了虚函数地址,通过虚指针找到对应的虚表来调用虚函数。

  • 虚基表(virtual base table)用于解决多重继承中虚基类的共享问题菱形继承问题),存储了虚基类的偏移量和其他相关信息,通过虚基表指针来处理虚基类的访问和布局。

两者不能混为一谈。

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

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

相关文章

2019年全国硕士研究生入学统一考试管理类专业学位联考逻辑试题——纯享题目版

&#x1f3e0;个人主页&#xff1a;fo安方的博客✨ &#x1f482;个人简历&#xff1a;大家好&#xff0c;我是fo安方&#xff0c;考取过HCIE Cloud Computing、CCIE Security、CISP等证书。&#x1f433; &#x1f495;兴趣爱好&#xff1a;b站天天刷&#xff0c;题目常常看&a…

【排序算法】堆排序

堆与一维数组 建立堆与一维数组的联系 堆排序并不是直接对堆节点Node类型排序&#xff0c;而是通过建立索引之间的关系&#xff0c;对一维数组排序。 称之为堆排序&#xff0c;是因为节点索引值之间的关系与完全二叉树的非常类似&#xff0c;而树又称堆。 设根节点为i&#xff…

【C#】委托、匿名方法、Lambda表达式和事件

【C#】委托、匿名方法、Lambda表达式和事件 委托 什么是委托&#xff1f; 委托和类一样&#xff0c;是用户自定义类型&#xff0c;是方法&#xff08;函数&#xff09;的抽象。通俗讲&#xff0c;委托就是 自定义类型的方法&#xff08;函数&#xff09;的代表。 声明委托 …

HTML+CSS+JavaScript华为主页

样式&#xff1a; HTMLCSSJavaScript仿华为首页 HTML: <!DOCTYPE html> <html><head><meta charset"utf-8"><link rel"stylesheet" type"text/css" href"Homepage.css"/><script type"text/ja…

NextJs下浅尝Prisma+Sqlite+逆向生成数据模型

1.安装prisma npm install prisma/client 2.创建schema.prisma npx prisma init 执行完命令后创建文件目录如下&#xff1a; 3.配置数据库连接 generator client {provider "prisma-client-js" }datasource db {provider "sqlite" //数据库类型 这…

libevent实践07:监听服务器并管理客户端

简介 函数bufferevent_new struct bufferevent * bufferevent_new(evutil_socket_t fd,bufferevent_data_cb readcb, bufferevent_data_cb writecb,bufferevent_event_cb eventcb, void *cbarg) 参数说明&#xff1a; fd:新客户端的文件描述符 readcb&#xff1a;一个函数指…

【Redis的优化】

目录 一、Redis 高可用二、 Redis 持久化2.1、Redis 提供两种方式进行持久化2.2、RDB 持久化1. 触发条件&#xff08;1&#xff09;手动触发&#xff08;2&#xff09;自动触发 2. 执行流程3. 启动时加载 2.3、AOF 持久化1. 开启AOF2. 执行流程(1&#xff09;命令追加(append)(…

深入理解 Linux 物理内存分配全链路实现

目录 内核物理内存分配接口 物理内存分配内核源码实现 内存分配的心脏 __alloc_pages prepare_alloc_pages 内存慢速分配入口 alloc_pages_slowpath 总结 内核物理内存分配接口 在物理内存分配成功的情况下&#xff0c; alloc_pages&#xff0c;alloc_page 函数返回的都是指…

2022最常用密码公布,你的账户安全吗?

密码管理工具 NordPass 公布了 2022 年最常用密码列表&#xff0c;以及破解密码所需的时间。该研究基于对来自 30 个不同国家 / 地区的 3TB 数据库的分析。研究人员将数据分为不同的垂直领域&#xff0c;使得其能够根据国家和性别进行统计分析。今年的研究主要聚焦于文化如何影…

工业软件对于现代制造业的生产效率和质量有何影响?

工业软件在提高现代制造业的生产力和质量方面发挥着至关重要的作用。比如&#xff1a; 流程自动化&#xff1a;工业软件可以实现各种制造流程的自动化&#xff0c;消除手动任务并减少人为错误。自动化通过简化操作、缩短周期时间和提高整体效率来提高生产力。它还可以最大限度地…

vue3和element plus踩坑

1.有说vue版本有两个&#xff0c;但检查之后发现只有一个&#xff0c;且为vue3的版本 2.也有说是因为命名的问题&#xff0c;组件名和页面名一致 最后发现是因为 在main.js里面引入element plus 使用这种use方式会报错&#xff0c;虽然也不知道为什么 import { createApp } …

《计算机系统与网络安全》第十一章 入侵检测与防御技术

&#x1f337;&#x1f341; 博主 libin9iOak带您 Go to New World.✨&#x1f341; &#x1f984; 个人主页——libin9iOak的博客&#x1f390; &#x1f433; 《面试题大全》 文章图文并茂&#x1f995;生动形象&#x1f996;简单易学&#xff01;欢迎大家来踩踩~&#x1f33…

Dell-Precision5520 电脑 Hackintosh 黑苹果efi引导文件

原文来源于黑果魏叔官网&#xff0c;转载需注明出处。&#xff08;下载请直接百度黑果魏叔&#xff09; 硬件配置 硬件型号驱动情况 主板Dell-Precision5520 处理器Intel Core i7-7820HQ已驱动 内存Micron 2400MHz DDR4 16GB x2已驱动 硬盘Samsung 970EVO 512GB已驱动 显…

Java中volatile的作用和原理

用法 volatile 是 Java 中的关键字&#xff0c;直接修饰成员变量&#xff0c;不能和 final 关键字同时使用。 private volatile boolean flag false;作用 当一个变量被声明为volatile时&#xff0c;它可以确保以下两点&#xff1a; 保证可见性&#xff1a;当一个线程修改了…

三维天地助力高校实验室数字化智能决策分析

近年来&#xff0c;随着检验检测行业技术的不断发展&#xff0c;高校实验室管理的复杂程度也在不断提高。由于传统的检测实验室日常工作任务繁重、费时费力&#xff0c;存在数据或信息的手动录入、人工计算&#xff0c;纸质文档资料的长期保存&#xff0c;数据快速汇总困难等诸…

大数据面试题:Kafka的Message包括哪些信息

面试题来源&#xff1a; 《大数据面试题 V4.0》 大数据面试题V3.0&#xff0c;523道题&#xff0c;679页&#xff0c;46w字 参考答案&#xff1a; 一个 Kafka 的 Message 由一个固定长度的 header 和一个变长的消息体 body 组成&#xff0c;header 部分由一个字节的 magic&…

Android 12 LED 定制灯效开发小结

文章目录 背景&#xff1a;Android 10 的设备上测试正常Android 12 中目前出现无法闪烁的问题电量变化广播监听总结参考 背景&#xff1a; 在定制的Android 10系统中&#xff0c;通过修改 Framwork 层的代码后&#xff0c;调用标准的接口后&#xff0c;能实现 LED 灯的闪烁灯效…

抖音旋转验证码分析

旋转验证码类型challenge_code为99996&#xff0c; 拿到的旋转验证码通常都是如下&#xff1a; 待旋转的图片&#xff1a; 旋转的背景图&#xff1a; 加密分析过程 可以参考&#xff1a;https://blog.csdn.net/weixin_38819889/article/details/129727564 旋转的难点在于如何…

英国 Tortoise Media发布2023年全球AI指数排名;美团宣布完成收购光年之外

&#x1f989; AI新闻 &#x1f680; 美团宣布完成收购光年之外&#xff0c;加强人工智能竞争力 摘要&#xff1a;美团在公告中宣布于2023年6月29日盘后收购光年之外的全部权益&#xff0c;以加强其在快速增长的人工智能行业中的竞争力。光年之外是中国领先的通用人工智能创新…

【ISO26262】汽车功能安全第一部分:术语

【tommi_wei@163.com】 故障响应时间 fault reaction time 从故障(2.42) 探测到进入安全状态(2.102) 的时间间隔。 故障容错时间间隔 fault tolerant time interval 在危害事件(2.59) 发生前, 系统(2.129) 中一个或多个故障(2.42) 可存在的时间间隔。 功能安全 functio…