C++ 多态和虚函数详解

本文章内容来源于C++课堂上的听课笔记

多态基础

多态(Polymorphism)是面向对象编程中的一个重要概念,它允许使用统一的接口来表示不同的对象和操作。多态性有两种主要形式:静态多态性(编译时多态性)和动态多态性(运行时多态性)

多态性分为两类: 静态多态性和动态多态性

静态多态性(编译时多态性):
定义: 在编译时确定方法的调用,通常与函数重载(overloading)相关。
例子: 方法重载是一种静态多态性的体现,编译器在编译时能够根据方法的参数类型或个数来选择正确的方法

class StaticPolymorphism {
public:void display(int value) {// ...}void display(double value) {// ...}
};

注意:重载(overload)和重写(override)的区别,下面是个重写的例子:

#include <iostream>
using namespace std;
class CA 
{  public:void f(int){ cout << "CA::f(int)"<< endl; }}; 
class CB : public CA 
{   public:void f(int) {cout << "CB::f(int) "<< endl;} void f(int,int) {cout << "CB::f(int,int)" << endl; } int f(int,int,int) {cout << "CB::f(int,int,int)"<< endl; }void test() {f(1);  f(1,1); f(1,1,1); }};int main() {  CB B;B.test();} 

如果把上面的void f(int) {cout << "CB::f(int) "<< endl;} 注释掉,会发生什么错误?

原因:在调用一个类的成员函数时,编译器会沿着类的继承链逐级的向上查找函数的定义,如果找到了则停止查找 如果派生类CB和基类CA都有同一个同名函数f(不论参数是否相同),编译器最终将选择派生类中的函数,即派生类的成员函数“隐藏”了基类的成员函数, 它阻止了编译器继续向上查找函数的定义

所以如果修改成下面这样就可以运行了:

#include <iostream>
using namespace std;
class CA 
{  public:void f(int){ cout << "CA::f(int)"<< endl; }}; 
class CB : public CA 
{   public:void test() {f(1);  }};int main() {  CB B;B.test();} 

动态多态性(运行时多态性):
定义: 在运行时确定方法的调用,通常与虚函数(virtual function)和继承相关。
例子: 通过虚函数和基类指针实现动态多态性,可以在运行时选择调用合适的函数。 

#include<iostream>
using namespace std;class Base {
public:virtual void display() {// ...cout<<1<<endl;}
};class Derived : public Base {
public:void display()  override{// ...cout<<2<<endl;}
};int main() {Base* ptr = new Derived();ptr->display(); // 在运行时调用 Derived 类的 display 方法delete ptr;return 0;
}

静态多态性和动态多态性的区别:
时机不同: 静态多态性在编译时确定,动态多态性在运行时确定。

实现机制不同: 静态多态性通常与函数重载等相关,而动态多态性通常与虚函数和继承相关。

使用场景不同: 静态多态性适用于编译时能够确定的情况,而动态多态性适用于在运行时确定的情况。

总的来说,多态性是面向对象编程的一个强大特性,它允许代码更加灵活、可扩展和易维护。

虚函数

在面向对象编程中,我们有时候会有一系列的类,它们可能会有一些相同的函数名,但是具体的实现可能会因为类的不同而有所不同。虚函数就是为了解决这个问题而设计的。

想象一下,你有一群动物,比如猫、狗、鸟等,它们都能发出声音。你可能会定义一个名为 makeSound 的函数来表示这个动作。但是,猫“喵喵”叫,狗“汪汪”叫,鸟“啾啾”叫,它们的叫声不同。这时候,你就可以使用虚函数了

#include<iostream>
using namespace std;
class Animal {
public:virtual void makeSound() {// 这里可以是一个默认的实现,也可以是空的}
};
class Cat : public Animal {
public:void makeSound() override {std::cout << "喵喵" << std::endl;}
};class Dog : public Animal {
public:void makeSound() override {std::cout << "汪汪" << std::endl;}
};class Bird : public Animal {
public:void makeSound() override {std::cout << "啾啾" << std::endl;}
};
int main()
{Animal* myPet = new Dog();myPet->makeSound();  // 输出:汪汪Cat cat;Bird bird;myPet = &cat;myPet->makeSound();myPet = &bird;myPet->makeSound();return 0;
}

 如果基类的函数不加关键字virtual会发生什么?

1.所有派生类的相关函数不能添加override关键字(以为实际上没有重写)

2.函数最终调用的是基类的函数,但可能使用对应派生类对象的数据

举个例子展示不用virtual时的情况

#include <iostream>
#include <string>
using namespace std;
class Student
{public:Student(int, string,float);void display( );                                            protected:int num;string name;float score;};
Student::Student(int n, string nam,float s) {num=n;name=nam;score=s;}void Student::display( ) 
{cout<<"num:"<<num<<"\nname:"<<
name<<"\nscore:"<<score<<"\n\n";}
class Graduate: public Student
{public:Graduate(int, string, float, float);                          void display( );                                             
private:float pay;
};void Graduate::display( ) {cout<<"num:"<<num<<"\nname:"<<name<<"\nscore:"<<score<<"\npay="<<pay<<endl;}Graduate::Graduate(int n, string nam,float s,float p):Student(n,nam,s),pay(p){ }
int main(){ Student stud1(1001,"Li",87.5);                   Graduate grad1(2001,"Wang",98.5,563.5); Student *pt=&stud1;                            pt->display( );pt=&grad1;pt->display( );return 0;}

如果修改成虚函数,即Student类中函数声明变为:

virtual void display( );  

运行结果

在派生类中重新定义此函数,要求函数名、函数类型、函数参数个数和类型全部与基类的虚函数相同,当一个成员函数被声明为虚函数后,其派生类中的同名函数都自动成为虚函数。派生类重新声明该虚函数时,可以加virtual,也可以不加

下面我们通过修改上述代码来证明这一点 

添加类Test

class Test: public Graduate
{public:Test(int ,string,float,float,int);void display();private:int others;
};
void Test::display( ) {cout<<"\n\nnum:"<<num<<"\nname:"<<name<<"\nscore:"<<score<<"\nothers="<<others<<endl;}Test::Test(int n, string nam,float s,float p,int o):Graduate(n,nam,s,p),others(o){ }

main中添加:

Test t(9999,"s",11,22,33);pt=&t;pt->display();

结果:

说明Graduate派生类Test中的同名函数display已经成为虚函数,如果不是虚函数,应该输出下面的结果:
num:1001
name:Li
score:87.5

num:2001
name:Wang
score:98.5
pay=563.5


num:9999
name:s
score:11
pay=22

静态关联和动态关联

在C++中,静态关联和动态关联是面向对象编程中两个关键的概念,通常与继承和多态性相关

静态关联(Static Binding):
概念: 静态关联发生在编译时,编译器在编译阶段就能够确定程序中各个函数或方法的调用关系。
实现: 在静态关联中,编译器根据函数或方法的声明类型来确定调用哪个函数或方法,这种绑定在编译时期就已经确定,因此称为静态关联。
优点: 效率高,因为在编译时已经确定了函数调用关系,不需要在运行时进行额外的查找。
例子: C++中的函数重载是一种静态关联的例子,编译器在编译时根据参数的类型和数量确定调用哪个重载版本。

动态关联(Dynamic Binding):
概念: 动态关联发生在运行时,程序在执行过程中才能够确定调用哪个函数或方法。
实现: 在动态关联中,通常通过使用虚函数和指针(或引用)来实现。这种绑定在运行时根据实际对象的类型确定,因此称为动态关联。
优点: 提供了更高的灵活性和可扩展性,允许在运行时根据实际情况改变调用关系。
例子: C++中的虚函数和纯虚函数是动态关联的例子。当基类指针或引用指向派生类对象,并调用虚函数时,根据实际对象的类型来确定调用哪个版本的函数

什么情况下适合声明虚函数?
1.基类预期被继承: 如果你设计一个基类,并且希望它能够作为其他类的基础,支持多态性,那么你应该在基类中声明虚函数。这样,派生类就有机会覆盖这些虚函数,实现自己的版本
2.需要运行时动态绑定: 如果你希望在运行时根据对象的实际类型来调用函数,而不是根据指针或引用的静态类型,那么你应该使用虚函数。这种动态绑定提供了多态性的特性
3.需要覆盖基类函数: 如果你希望派生类能够覆盖基类中的同名函数,以提供特定于派生类的实现,那么这个函数应该声明为虚函数
4.实现抽象类和接口: 如果你想要创建一个抽象类(包含至少一个纯虚函数)或者接口,那么你应该声明虚函数。这样的类不能被实例化,但可以作为基类供其他类继承并实现虚函数

虚析构函数

在C++中,析构函数是用来在对象生命周期结束时进行清理工作的函数。而当我们在面向对象的程序设计中使用继承时,有时候我们会遇到这样的情况:基类指针指向派生类对象。

如果你使用了继承,并且在基类和派生类中都定义了析构函数,那么就有可能发生一个问题。当你使用基类指针指向派生类对象时,如果你删除这个指针,只会调用基类的析构函数,而不会调用派生类的析构函数。这可能导致一些资源(比如内存)没有得到正确释放,从而产生问题。

这时候,虚析构函数就发挥作用了。在基类的析构函数前面加上virtual关键字,就可以将它声明为虚析构函数。这样,当你通过基类指针删除派生类对象时,会正确地调用派生类的析构函数。

#include<iostream>
using namespace std;
class Base {
public:virtual ~Base() {// 基类的清理工作cout<<"Base Destruction"<<endl;}
};class Derived : public Base {
public:~Derived()  {// 派生类的清理工作cout<<"Derived Destruction"<<endl;}
};
int main()
{Base *pt=new Derived;delete pt;return 0;
}

这里,~Base()是虚析构函数,而~Derived()覆盖了基类的虚析构函数。当你使用基类指针指向派生类对象时,通过这个基类指针删除对象时,会正确地调用~Derived()来完成清理工作,确保所有相关资源都被正确释放。

总的来说,虚析构函数是为了在继承层次结构中正确地释放资源而引入的。通过使用虚析构函数,可以确保在删除基类指针时正确调用派生类的析构函数,从而实现正确的资源清理。

纯虚函数和抽象类

抽象类:
首先,想象一下你要画一只动物的图,但是你不知道具体是什么动物,只知道它是动物。你可能会画一些共性的特征,比如四条腿、有尾巴等。这个画得很模糊的图就好比一个抽象类。
在编程中,抽象类也是一种类,但是它是一种不完整的类,不能被实例化(也就是不能创建对象)。抽象类里面可能包含了一些方法(函数),但是这些方法没有具体的实现,只是一个声明。抽象类的存在是为了给其他类提供一种共同的接口,让这些类去继承它并实现这些方法。
纯虚函数:
再想象一下,你画的那只动物,有一些特征是必须由具体的动物来定义的,比如各种动物的叫声。这时,你可以把叫声这个特征标记为一个“待定”项,告诉其他人,这个特征必须由实际的动物类来具体实现。
在编程中,这个“待定”项就是纯虚函数。一个纯虚函数是在抽象类中声明的虚函数,但是没有具体的实现。它的目的是让派生类强制实现这个方法,以使抽象类变得更加具体。
综合起来,抽象类是一种不完整的类,包含一些没有具体实现的方法,其中可能包含了纯虚函数。而纯虚函数是为了强制派生类实现某些方法,使得抽象类可以更具体、更有实际意义。在C++中,含有纯虚函数的类就被称为抽象类

注意!
1.凡是包含纯虚函数的类都是抽象类,包含纯虚函数的类是无法建立对象的
2.如果在抽象类所派生出的新类中对基类的所有纯虚函数进行了定义,那么这些函数就被赋予了功能,可以被调用。这个派生类就不是抽象类,而是可以用来定义对象的具体类。如果在派生类中没有对所有纯虚函数进行定义,则此派生类仍然是抽象类,不能用来定义对象
3.虽然抽象类不能定义对象(或者说抽象类不能实例化),但是可以定义指向抽象类数据的指针变量
下面举一个综合性例子体现上面所说的一切:

#include <iostream>// 抽象类 Animal
class Animal {
public:// 纯虚函数,表示动物的叫声virtual void makeSound() const = 0;// 普通方法,表示动物的一般特征void sleep() const {std::cout << "Zzz..." << std::endl;}
};// 具体的动物类 Lion(狮子)
class Lion : public Animal {
public:// 实现了纯虚函数,给出了狮子的叫声void makeSound() const override {std::cout << "Roar!" << std::endl;}
};// 具体的动物类 Elephant(大象)
class Elephant : public Animal {
public:// 实现了纯虚函数,给出了大象的叫声void makeSound() const override {std::cout << "Trumpet!" << std::endl;}
};// 具体的动物类 Monkey(猴子)
class Monkey : public Animal {
public:// 猴子并没有实现纯虚函数 makeSound// 因此 Monkey 仍然是抽象类
};int main() {// 1. 尝试创建抽象类的对象,编译错误// Animal animal;  // 编译错误,抽象类不能实例化// 2. 使用抽象类指针,指向派生类对象Animal* lion = new Lion();Animal* elephant = new Elephant();// Animal* monkey = new Monkey();  // 编译错误,抽象类不能实例化// 调用纯虚函数,实际上会调用相应派生类的实现lion->makeSound();     // 输出:Roar!elephant->makeSound(); // 输出:Trumpet!// 调用抽象类的普通方法lion->sleep();elephant->sleep();// 3. 定义指向抽象类的指针变量Animal* abstractAnimal = nullptr;  // 可以定义指针变量// 4. 尝试使用派生类 Monkey,编译错误// Monkey monkeyObj;  // 编译错误,抽象类不能实例化delete lion;delete elephant;return 0;
}

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

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

相关文章

华为无线ac+fit三层组网,每个ap发射不同的业务vlan

ap管理dhcp在ac控制器上&#xff0c;业务dhcp在汇聚上 配置WLAN业务 &#xff08;1&#xff09;配置VAP模板 • 配置员工网络的VAP模板&#xff08;employee&#xff09; [AC-wlan-view] security-profile name employee //创建名为“employee”的安全模板 [AC-wlan-sec-prof-…

Git面经

Git八股文 第一章 git基础 1.1 什么是git git是一款免费的开源的分布式版本控制系统 1.2 为什么要使用git 为了保留之前的所有版本&#xff0c;方便回滚或修改 1.3 集中化版本控制系统和分布式版本控制系统的区别 集中化版本控制系统如svn&#xff0c;客户端连接到中央服…

C语言——用递归函数计算n!

归纳编程学习的感悟&#xff0c; 记录奋斗路上的点滴&#xff0c; 希望能帮到一样刻苦的你&#xff01; 如有不足欢迎指正&#xff01; 共同学习交流&#xff01; &#x1f30e;欢迎各位→点赞 &#x1f44d; 收藏⭐ 留言​&#x1f4dd; 比别人多一点努力&#xff0c;你…

国产化区块链平台-FISCO BCOS 区块链

目录 FISCO BCOS 版本信息 系统概述 关键特性 组件服务 开发运维工具 FISCO BCOS作为一种企业级区块链平台&#xff0c;为企业和组织提供了高性能、隐私保护和可定制的区块链解决方案。其强大的架构和丰富的功能使得企业能够在安全可信的环境中开展区块链应用&#xff0…

基数排序详解(LSD方法+MSD方法+思路+图解+代码)

文章目录 基数排序一、基数排序概念1.LSD排序法&#xff08;最低位优先法&#xff09;2.MSD排序法&#xff08;最高位优先法&#xff09; 基数排序 一、基数排序 概念 基数排序是一种非比较型整数排序算法 将整数按位数切割成不同的数字&#xff0c;然后按每个位数分别比较 …

小程序开发平台源码系统 各行各业都可使用 功能强大 附带完整的搭建教程

当前&#xff0c;数字化转型已经成为各行各业的重要趋势&#xff0c;而小程序作为数字化转型的重要工具之一&#xff0c;具有广泛的应用前景。因此&#xff0c;我们开发了这个源码系统&#xff0c;以帮助各行各业快速开发出符合需求的小程序。 以下是部分代码示例&#xff1a;…

设计模式—结构型模式之享元模式

设计模式—结构型模式之享元模式 享元模式(Flyweight Pattern)&#xff0c;运用共享技术有效地支持大量细粒度对象的复用。系统只使用少量的对象&#xff0c;而这些对象都很相似&#xff0c;状态变化很小&#xff0c;可以实现对象的多次复用。对象结构型。 在享元模式中可以共…

机器学习实战 ——《跟着迪哥学Python数据分析与机器学习实战》(2)

机器学习实战 ——《跟着迪哥学Python数据分析与机器学习实战》&#xff08;2&#xff09; 七、贝叶斯算法7.1 新闻分类任务实战7.1.1 结巴分词7.1.2 词云表示工具包wordcloud7.1.3 TF-IDF特征 八、聚类算法K-means K-均值聚类算法评估指标优缺点 DBSCAN&#xff08;Density-Ba…

ChatGPT/GPT4科研实践应用与AI绘图技术及论文高效写作

2023年随着OpenAI开发者大会的召开&#xff0c;最重磅更新当属GPTs&#xff0c;多模态API&#xff0c;未来自定义专属的GPT。微软创始人比尔盖茨称ChatGPT的出现有着重大历史意义&#xff0c;不亚于互联网和个人电脑的问世。360创始人周鸿祎认为未来各行各业如果不能搭上这班车…

VS搭建QT环境失败1

在VS中做一个简单QT控制台程序; 包含目录已经添加如下图;可以找到QCoreApplication头文件;可以找到QCoreApplication类,把鼠标放上去会有提示;但是由QCoreApplication类生成对象会出错;app这个变量提示出错; 同时显示200多个错误;这是VS2015; 看一下我的QT安装有没有缺…

【PostgreSQL】解决PostgreSQL时区(TimeZone)问题

问题描述 最近在使用PostgreSQL中&#xff0c;对行记录进行设置创建时间&#xff08;created_time&#xff09;时&#xff0c;出现了设置了now()时间而数据库中写入的数据是不一致的数据。 eg&#xff1a; insert into dept ( created_at, updated_at) VALUES (now(),now())…

算法分析与设计课后练习22

设W(5,7,10,12,15,18,20)和M35&#xff0c;使用过程SUMOFSUB找出W种使得和数等于M的全部子集并画出所生成的部分状态空间树

国产压力测试工具的主要作用

国产压力测试工具可以帮助软件开发和维护团队对系统进行全面的性能测试&#xff0c;以评估系统在高负载下的性能表现。以下是国产压力测试工具的主要作用&#xff1a; 性能评估&#xff1a;国产压力测试工具可以模拟多用户同时对系统进行访问和操作&#xff0c;通过对系统的响应…

Vite - 静态资源处理 - json文件导入

直接就说明白了 vite 中对json 文件直接当作一个模块来解析。 可以直接导入使用&#xff01; 可以直接导入使用&#xff01; 可以直接导入使用&#xff01;json文件中的key&#xff0c;直接被作为一个属性&#xff0c;可以单独被导入。 因此&#xff0c;导入json文件有两种方式…

金山云2023年Q3财报:持续向好!

11月21日&#xff0c;金山云公布了2023年第三季度业绩。 财报显示&#xff0c;金山云Q3营收16.3亿元&#xff0c;调整后毛利率达12.1%再创历史新高&#xff0c;调整后毛利额同比上涨57.5%。今年第三季度&#xff0c;公有云实现收入10.2亿元&#xff0c;毛利率达到4.7%&#xf…

OpenAI GPT-4 Turbo发布:开创AI新时代

&#x1f3a5; 屿小夏 &#xff1a; 个人主页 &#x1f525;个人专栏 &#xff1a; IT杂谈 &#x1f304; 莫道桑榆晚&#xff0c;为霞尚满天&#xff01; 文章目录 &#x1f4d1;前言一. GPT-4 Turbo的突破1.1上下文长度和控制手段的加强&#xff1a;1.2多模态支持&#xff1a…

Linux 网络:PMTUD 简介

文章目录 1. 前言2. Path MTU Discovery(PMTUD) 协议2.1 PMTUD 发现最小 MTU 的过程 3. Linux 的 PMTUD 简析3.1 创建 socket 时初始化 PMTUD 模式3.2 数据发送时 PMTUD 相关处理3.2.1 源头主机发送过程中 PMTUD 处理3.2.2 转发过程中 PMTUD 处理 4. PMTUD 观察5. 参考链接 1. …

使用Python实现几种底层技术的数据结构

使用Python实现几种底层技术的数据结构 数据结构(data structure)是带有结构特性的数据元素的集合&#xff0c;它研究的是数据的逻辑结构和数据的物理结构以及它们之间的相互关系&#xff0c;并对这种结构定义相适应的运算&#xff0c;设计出相应的算法&#xff0c;并确保经过这…

千字文||无聊又数了一下千字文字数

千字文的字数去除标点符号真的整整一千个不多的不少 该文章无技术含量&#xff0c;仅三字经原文&#xff0c;学技术的同学可以止步了 千字文&#xff08;原文&#xff09; 【作者】周兴嗣 【朝代】南北朝 天地玄黄&#xff0c;宇宙洪荒。 日月盈昃&#xff0c;辰宿列张。 寒来…

Python 进程和线程详解(multiprocessing、threading)

文章目录 1 概述1.1 进程 VS 线程1.2 优缺点 2 进程2.1 三个步骤2.2 多进程2.3 带参数2.3.1 元组参数 args2.3.2 字典参数 kwargs 2.4 获取进程编号2.5 设置进程守护 3 线程3.1 三个步骤3.2 多线程3.3 带参数2.3.1 元组参数 args2.3.2 字典参数 kwargs 2.4 获取线程编号2.5 设置…