【C++知识点】多态

news2024/10/5 17:24:00

✍个人博客:https://blog.csdn.net/Newin2020?spm=1011.2415.3001.5343
📚专栏地址:C/C++知识点
📣专栏定位:整理一下 C++ 相关的知识点,供大家学习参考~
❤️如果有收获的话,欢迎点赞👍收藏📁,您的支持就是我创作的最大动力💪
🎏唠叨唠叨:在这个专栏里我会整理一些琐碎的 C++ 知识点,方便大家作为字典查询~

多态

构成多态的条件

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

案例

定义了一个 Person 类和一个 Student 类,Student 类继承自 Person 类,接着,在 main 函数里面,我们分别实例化了一个 Person 对象和一个 Student 对象。

最后,分别调用了 Person 类对象的 info 方法和 Student 类对象的 info 方法,这时发现,它们各自调用了自己的 info 函数,现在,用 Student 来实例化 Person 类。

//Person类
class Person{
public:
    Person(string name, int age):name(name),age(age){}
    void info(){
        cout << "Call Person info, Name = " << this->name << " Age = " << this->age << endl;
    }
protected:
    string name;
    int age;
};
class Student:public Person{
public:
    Student(string name, int age, float score):Person(name, age),score(score){}
    void info(){
        cout << "Call Student info, Name = " << this->name << " Age = " << this->age << " Score = " << score << endl;
    }
protected:
    float score;
};
int main(){
    Person *person = new Person("zs", 20);
    person->info();
    Student *student = new Student("ww", 21, 90);
    student->info();
    return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-20BIXZYA-1678064942578)(C++笔记.assets/image-20221207101742012.png)]

如果用 Student 类实例化了 Person 类,最终调用 info 函数,此时的 info 函数还是调用的 Person 类的,这不是想要的效果,我们期望的是还是调用 Student 类的 info 函数。

int main(){
    Person *person = new Person("zs", 20);
    person->info();
    person = new Student("ww", 21, 90);
    person->info();
    return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EkkulIRA-1678064942580)(C++笔记.assets/image-20221207101756171.png)]

通过基类指针只能访问派生类的成员变量,但是不能访问派生类的成员函数。为了能让基类指针访问派生类的成员函数,C++ 增加了虚函数(Virtual Function),现在,将 info 函数声明为虚函数。

在Person类和Student类中的void info()前都加上virtual关键字:

virtual void info().....

修改代码之后,再次运行,这次使用 person 对象可以调用到 Student 实例的 info 方法了,即,通过虚函数实现了多态

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6mibbVs7-1678064942581)(C++笔记.assets/image-20221207101805915.png)]

引用实现多态

指针和引用很像,在 C++ 中,多态 的实现,除了可以使用子类的指针指向父类的对象之外,还可以通过引用来实现多态,不过引用不像 指针 灵活,指针可以随时改变指向,而引用只能指代固定的对象,在多态性方面缺乏表现力。

Person p1("xiaoming",100);
Student s1("xiaozhang",120,99);
Person &rPerson = p1;
Person &rStuent = s1;
rPerson.info();
rStuent.info();

引用类似于常量,只能在定义的同时初始化,并且以后也要从一而终,不能再引用其他数据,所以本例中必须要定义两个引用变量,一个用来引用基类对象,一个用来引用派生类对象。从运行结果可以看出,当基类的引用指代基类对象时,调用的是基类的成员,而指代派生类对象时,调用的是派生类的成员。

即,我们使用了引用实现了多态的功能

虚函数

在 C++ 中,使用 virtual 关键字 修饰的 函数 被称为虚函数,虚函数对于 多态 具有决定性的作用,有虚函数才能构成多态

什么时候需要将函数声明为虚函数,首先看 成员函数 所在的类是否会作为基类。然后看成员函数在类的继承后有无可能被更改功能,如果希望更改其功能的,一般应该将它声明为虚函数。

如果成员函数在类被继承后功能不需修改,或派生类用不到该函数,则不要把它声明为虚函数。

上个案例中的Person类中和Student类中的的info函数,就是虚函数。

必须将 info 函数声明为虚函数,不然,没办法通过子类对象指向父类成员时,访问子类对象的 info 方法。

//语法
virtual type funcName(plist){}

案例

定义一个Animal类,在定义一个虚函数eat;在定义一个dog类和cat类继承Animal,通过多态调用不同的eat函数。

class Animal{
public:
    virtual void eat(){
        cout << "Animal eat" << endl;
    }
};
class Dog:public Animal{
public:
    virtual void eat(){
        cout << "Dog eat" << endl;
    }
};
class Cat: public Animal{
    virtual void eat(){
        cout << "Cat eat" << endl;
    }
};
//指针实现
int main(){
    Animal *a1 = new Animal();
    a1->eat();
    a1 = new Dog();
    a1->eat();
    a1 = new Cat();
    a1->eat();
    return 0;
}
//引用实现
int main(){
    Animal a0;
    Animal &a1 = a0;
    a1.eat();
    Dog d1;
    Animal &a2 = d1;
    a2.eat();
    Cat c1;
    Animal &a3 = c1;
    a3.eat();
    return 0;
}

虚析构

在 C++ 中,使用 virtual 关键字 修饰的 函数 被称为 虚函数,C++ 的构造函数不可以被声明为虚函数,但析构函数可以被声明为虚函数,并且有时候必须将析构声明为虚函数

用 C++ 开发的时候,用来做基类的类的析构函数一般都是虚函数

虚析构函数的作用

虚析构函数是为了避免内存泄露,而且是当子类中会有指针 成员变量 时才会使用得到的。也就说虚析构函数使得在删除指向子类对象的基类指针时可以调用子类的析构函数达到释放子类中堆内存的目的,而防止内存泄露的。

当父类的析构函数不声明成虚析构函数的时候,当子类继承父类,父类的指针指向子类时,delete掉父类的指针,只调动父类的析构函数,而不调动子类的析构函数。

当父类的析构函数声明成虚析构函数的时候,当子类继承父类,父类的指针指向子类时,delete掉父类的指针,先调动子类的析构函数,再调动父类的析构函数。

//语法
virtual ~FuncName(){}

案例

在 main 函数里面,使用了父类指向了子类对象,最终,释放子类对象时,调用了父类的析构函数,这样会导致,子类对象的一些数据成员没发得到释放,会造成内存泄露。

class Person{
public:
    Person(string name, int age):name(name),age(age){}
    ~Person(){
        cout << "call ~Person" << endl;
    }
    virtual void info(){
        cout << "Call Person info, Name = " << this->name << " Age = " << this->age << endl;
    }
protected:
    string name;
    int age;
};
class Student:public Person{
public:
    Student(string name, int age, float score):Person(name, age),score(score){}
    ~Student(){
        cout << "Call ~Student" << endl;
    }
    virtual void info(){
        cout << "Call Student info, Name = " << this->name << " Age = " << this->age << " Score = " << score << endl;
    }
protected:
    float score;
};
int main(){
    Person *p1 = new Student("zs",20,90);
    p1->info();
    delete p1;
    return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TsuMhisc-1678064942584)(C++笔记.assets/image-20221207103135517.png)]

将父类的析构函数声明为了虚析构,再次运行程序。

virtual ~Person(){
    cout << "call ~Person" << endl;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-N3R7F43C-1678064942587)(C++笔记.assets/image-20221207103352158.png)]

虚函数表

在 C++ 中,多态 是由 虚函数 实现的,而虚函数主要是通过虚函数表(V-Table)来实现的。对象不包含虚函数表,只有虚指针,类 才包含虚函数表,派生类会生成一个兼容基类的虚函数表。

如果一个类中包含虚函数(virtual 修饰的函数),那么这个类就会包含一张虚函数表,虚函数表存储的每一项是一个虚函数的地址。

C++ 的虚函数表如下图所示:

class A {
public:
    virtual void vfunc1();
    virtual void vfunc2(); 
    void func1(); 
    void func2();
private: 
    int m_data1, m_data2;
};

虚表是一个指针数组,其元素是虚函数的指针,每个元素对应一个虚函数的函数指针。需要指出的是,普通的函数即非虚函数,其调用并不需要经过虚表,所以虚表的元素并不包括普通函数的函数指针。

虚表内的条目,即虚函数指针的赋值发生在编译器的编译阶段,也就是说在代码的编译阶段,虚表就可以构造出来了。

虚表是属于类的,而不是属于某个具体的对象,一个类只需要一个虚表即可。同一个类的所有对象都使用同一个虚表。

C++如何实现动态绑定

动态绑定什么时候发生?所有的工作都是由编译器在幕后完成。当我们告诉通过创建一个virtual函数来告诉编译器要进行动态绑定,那么编译器就会根据动态绑定机制来实现我们的要求, 不会再执行早绑定。

编译器如何处理虚函数。当编译器发现类中有虚函数的时候,编译器会创建一张虚函数表,把虚函数的函数入口地址放到虚函数表中,并且在类中秘密增加一个指针,这个指针就是vpointer(缩写vptr),这个指针是指向对象的虚函数表。在多态调用的时候,根据vptr指针,找到虚函数表来实现动态绑定

在编译阶段,编译器秘密增加了一个vptr指针,但是此时vptr指针并没有初始化指向虚函数表(vtable),什么时候vptr才会指向虚函数表?在对象构建的时候,也就是在对象初始化调用构造函数的时候。编译器首先默认会在我们所编写的每一个构造函数中,增加一些vptr指针初始化的代码。如果没有提供构造函数,编译器会提供默认的构造函数,那么就会在默认构造函数里做此项工作,初始化vptr指针,使之指向本对象的虚函数表。

起初,子类继承基类,子类继承了基类的vptr指针,这个vptr指针是指向基类虚函数表,当子类调用构造函数,使得子类的vptr指针指向了子类的虚函数表。

当子类无重写基类虚函数时

当程序执行到这里,会去animal指向的空间中寻找vptr指针,通过vptr指针找到func1函数,此时由于子类并没有重写也就是覆盖基类的func1函数,所以调用func1时,仍然调用的是基类的func1。

Animal *a = new Dog();
a->func1();

代码验证:

class Animal{
public:
    virtual void func1(){
        cout << "Animal func1" << endl;
    }
    virtual void func2(){
        cout << "Animal func1" << endl;
    }
};
class Dog:public Animal{
public:
    virtual void func3(){
        cout << "Dog func3" << endl;
    }
    virtual void func4(){
        cout << "Dog func4" << endl;
    }
};
int main(){
    Animal *a=new Dog;
    a->func1();
    return 0;
}

当子类重写基类虚函数时

当程序执行到这里,会去animal指向的空间中寻找vptr指针,通过vptr指针找到func1函数,由于子类重写基类的func1函数,所以调用func1时,调用的是子类的func1。

Animal *a = new Dog();
a->func1();

代码验证:

class Animal{
public:
    virtual void func1(){
        cout << "Animal func1" << endl;
    }
    virtual void func2(){
        cout << "Animal func1" << endl;
    }
};
class Dog:public Animal{
public:
    virtual void func1(){
        cout << "Dog func1" << endl;
    }
    virtual void func4(){
        cout << "Dog func4" << endl;
    }
};
int main(){
    Animal *a=new Dog;
    a->func1();
    return 0;
}

抽象基类和纯虚函数

在设计时,常常希望基类仅仅作为其派生类的一个接口。这就是说,仅想对基类进行向上类型转换,使用它的接口,而不希望用户实际地创建一个基类的对象。要做到这点,可以在基类中加入至少一个纯虚函数,来使基类成为抽象类。纯虚函数使用关键字virtual,并且在其后面加上=0。如果某人试着生成一个抽象类的对象,编译器会制止他,这个工具允许生成特定的设计。

当继承一个抽象类时,必须实现所有的纯虚函数,否则继承出的类也将是一个抽象类。创建一个纯虚函数允许在接口中放置成员函数,而不一定要提供一段可能对这个函数毫无意义的代码。同时,纯虚函数要求出的类对它提供一个定义。纯虚函数总是变成“哑”函数。

建立公共接口,也就是纯虚函数抽象类。它能对于每个不同的子类有不同的表示,它建立一个基本的格式。

纯虚函数

虚函数:virtual type funcName(plist) {}
纯虚函数:virtual type funcName(plist) = 0;

抽象基类

基类中添加了至少一个纯虚函数的基类称为抽象基类。

案例

抽象基类是泡饮品的抽象操作,子类 泡咖啡是 泡饮品的具体操作,子类 泡茶是 泡饮品的具体操作,泡茶和泡咖啡通过继承泡饮品的共用操作,完成实现。

//抽象基类,制作饮品
class AbstractDrinking{
public:
    //烧水
    virtual void Boil() = 0;
    //泡
    virtual void Brow() = 0;
    //倒入杯子
    virtual void PourInCup() = 0;
    //辅料
    virtual void PutSomething() = 0;
    //规定流程
    void MakeDrink(){
        Boil();
        Brow();
        PourInCup();
        PutSomething();
    }
};
//子类 制作咖啡
class Coffee : public AbstractDrinking{
public:
    //烧水
    virtual void Boil(){
        cout << "煮山泉" << endl;
    }
    //泡
    virtual void Brow(){
        cout << "泡咖啡" << endl;
    }
    //倒入杯子
    virtual void PourInCup(){
        cout << "咖啡倒入杯子" << endl;
    }
    //辅料
    virtual void PutSomething(){
        cout << "加牛奶" << endl;
    }
};
//子类 泡茶
class Tea : public AbstractDrinking{
public:
    //烧水
    virtual void Boil(){
        cout << "煮白开水" << endl;
    }
    //泡
    virtual void Brow(){
        cout << "泡茶" << endl;
    }
    //倒入杯子
    virtual void PourInCup(){
        cout << "茶倒入杯子" << endl;
    }
    //辅料
    virtual void PutSomething(){
        cout << "加盐" << endl;
    }
};
//业务函数
void DoBussiness(AbstractDrinking* drink){
    drink->MakeDrink();
    delete drink;
}
int main(){
    DoBussiness(new Coffee);
    cout << endl;
    DoBussiness(new Tea);
    return 0;
}

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

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

相关文章

Java与Winform进行AES加解密数据传输的工具类与对应关系和示例

场景 AndroidJava中使用Aes对称加密的工具类与使用&#xff1a; AndroidJava中使用Aes对称加密的工具类与使用_霸道流氓气质的博客-CSDN博客 上面讲的Java与安卓进行数据传输时使用AES加解密的示例工具类。 如果Java需要与其他第三方平台比如Winform程序进行数据传递时也需…

一文吃透前端低代码的 “神仙生活”

今天来说说前端低代码有多幸福&#xff1f; 低代码是啥&#xff1f;顾名思义少写代码…… 这种情况下带来的幸福有&#xff1a;代码写得少&#xff0c;bug也就越少&#xff08;所谓“少做少错”&#xff09;&#xff0c;因此开发环节的两大支柱性工作“赶需求”和“修bug”就…

智能氮气柜相对于传统氮气柜的优点分析

智能氮气柜是利用氮气置换柜内湿气从而进行除湿&#xff0c;氮气经过流量计&#xff0c;进入智能氮气控制盒&#xff0c;氮气没有氧气和水分&#xff0c;与潮湿气体的比重不同&#xff0c;进入柜内排掉湿气降湿。智能氮气柜在传统氮气柜的基础上加上了智能控制系统&#xff0c;…

从传统数据库痛点看分布式数据库选型问题

本文转载自&#xff1a;OceanBase 社区 作者简介&#xff1a;蔡鹏&#xff0c;拥有十多年DBA工作经历&#xff0c;曾就职于饿了么、蚂蚁集团&#xff0c;现任货拉拉数据库部门负责人&#xff0c;负责数据库、缓存&#xff0c;消息队列的管理与平台研发工作。 引言 近年来&…

反诈老陈自曝2022年收入133万

我是卢松松&#xff0c;点点上面的头像&#xff0c;欢迎关注我哦&#xff01; 反诈老陈自曝2022年收入133万&#xff0c;“缴税近40万&#xff0c;捐赠84万”&#xff0c;曾称网络上得到的收入分文不取。老陈自曝引起了广泛热议。 100万的打赏让老陈离职做直播 相较于普通人…

统计学基础:置信区间和p值知识

引言小伙伴们&#xff0c;今天我们要来聊聊置信区间和p值这两个看起来超级相关的概念。咱们先来说说置信区间&#xff0c;它是一种区间估计&#xff0c;相当于给你的样本数据画了一个“框框”&#xff0c;告诉你总体数据的真实值很可能在这个框框里。就好像你买了一个锅&#x…

CS:GO头号特训添加bot + CS:GO控制台指令大全

CS:GO头号特训添加bot CS:GO控制台指令大全 我是艾西&#xff0c;在我们玩csgo时总是会有一些自己的下想法以及想和小伙伴们一起玩的快乐&#xff0c;今天我跟大家简单的说一下头号特训 头号特训模式下单人或多人跑图的相关指令&#xff0c;帮助玩家熟悉头号特训的玩法、特殊道…

写作利器之Markdown编辑套装

简述 作为一名习惯使用Markdown写作的重度用户&#xff0c;并且经常有发布文章需求的作者来说&#xff0c;一套完善的写作发布套装就显得的很重要了。 所以就有了以下的使用体验&#xff0c;本文大概能解决的问题&#xff1a; 1.由于网络问题或编辑器原因部分网站粘贴后的mar…

Docker【基本使用】

1&#xff1a;启动Docker1.1&#xff1a;操作systemctl start docker.service1.2&#xff1a;常见问题【第一步】启动docker&#xff0c;提示启动失败&#xff0c;查询运行状态systemctl start docker.service【第二步】查询docker运行状态&#xff0c;提示不支持SELinux【第三…

大数据flink框架入门分享(起源与发展、实时与离线计算、场景、处理流程、相关概念、特性普及、入门Demo)

文章目录起源与发展flink在github上的现状实时计算VS离线计算实时计算离线计算实时计算常用的场景框架流处理流程flink电商场景下的业务图示例flink中一些重要特性有界数据和无界数据时间语义、水位线事件时间处理时间水位线flink窗口概念理想中的数据处理含有延迟数据的数据处…

基于BP神经网络的手部动作分类识别,BP神经网络详细原理

目标 背影 BP神经网络的原理 BP神经网络的定义 BP神经网络的基本结构 BP神经网络的神经元 BP神经网络的激活函数&#xff0c; BP神经网络的传递函数 数据 神经网络参数 基于BP神经网络手部动作识别的MATLAB代码 效果图 结果分析 展望 背影 随着人工智能的发展&#xff0c;智…

深入剖析 MVC 模式与三层架构

文章目录1. 前言2. MVC模式3. 三层架构4. MVC和三层架构5. 总结5.1 IDEA 小技巧1. 前言 前面我们探讨了 JSP 的使用&#xff0c;随着计算机技术的不断更新迭代&#xff0c;JSP 的技术由于存在很多的缺点&#xff0c;已经逐渐退出了历史的舞台&#xff0c;所以在学习时&#xf…

加密功能实现

文章目录1. 前言2. 密码加密1. 前言 本文 主要实现 对密码进行加密 &#xff0c;因为 使用 md5 容易被穷举 (彩虹表) 而破解 &#xff0c;使用 spring security 框架又太大了 (杀鸡用牛刀) 。   所以本文 就自己实现一个密码加密 . 2. 密码加密 这里我们通过 加盐是方式 来 对…

pytorch安装的超级详细教程(没有之一)

一、发展历程 &#xff08;简单介绍&#xff09; (15年)caffe --> (16年)tensorflow1.x --> (17年)keras --> (18年)Tensorflow2.x --> (19年)pytorch。 面向gihub开源项目编程。 向下支持比较好&#xff0c;各个版本之间支持比较好&#xff0c;兼容性强。 版本…

Android事件拦截(3)——系统拦截和应用拦截

本文主要分析触摸事件和按键事件在不同阶段被拦截的流程&#xff0c;总结在不同阶段不同方法中返回值的含义。 按键的拦截 &#xff08;1&#xff09;interceptKeyBeforeQueueing interceptKeyBeforeQueueing方法的意义就是在事件入队列前拦截按键事件&#xff0c;也就是如果…

想找工作,这一篇15w字数+的文章帮你解决

文章目录前言一 专业技能1. 熟悉GoLang语言1.1 Slice1.2 Map1.3 Channel1.4 Goroutine1.5 GMP调度1.6 垃圾回收机制1.7 其他知识点2. 掌握Web框架Gin和微服务框架Micro2.1 Gin框架2.2 Micro框架2.3 Viper2.4 Swagger2.5 Zap2.6 JWT3. 熟悉使用 MySQL 数据库3.1 索引3.2 事务3.3…

每日学术速递3.6

Subjects: cs.CV 1.Multi-Source Soft Pseudo-Label Learning with Domain Similarity-based Weighting for Semantic Segmentation 标题&#xff1a;用于语义分割的基于域相似性加权的多源软伪标签学习 作者&#xff1a;Shigemichi Matsuzaki, Hiroaki Masuzawa, Jun Miura …

2022掉队的“蔚小理”,按下了兔年加速键

配图来自Canva可画 进入2023年&#xff0c;各大车企又展开了新一轮的“竞速”。尽管1月份汽车整体销量出现了“阴跌”&#xff0c;但从各路车企发布的销量目标来看&#xff0c;车企对于2023依旧保持着较高的信心和预期。在一众车企中&#xff0c;以“蔚小理”为代表的新势力们…

基于quartz实现定时任务管理系统

基于quartz实现定时任务管理系统 背景 说起定时任务框架&#xff0c;首先想到的是Quartz。这是定时任务的老牌框架了&#xff0c;它的优缺点都很明显。借助PowerJob 的readme文档的内容简单带过一下这部分。 除了上面提到&#xff0c;还有elastic-job-lite、quartzui也是相当…

【C++】仿函数 -- priority_queue

文章目录一、priority_queue 的介绍和使用1、priority_queue 的介绍2、priority_queue 的使用3、priority_queue 相关 OJ 题二、仿函数1、什么是仿函数2、仿函数的作用三、priority_queue 的模拟实现一、priority_queue 的介绍和使用 1、priority_queue 的介绍 priority_queu…