C++多态、虚函数、纯虚函数、抽象类


多态的概念
        通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。
        举个简单的例子:抢红包,我们每个人都只需要点击一下红包,就会抢到金额。有些人能抢到几十元,而有些人只能抢到几元甚至几毛。也正说明了不同的人做相同的事,结果却不同,这就是多态。

        在C++中有两种多态性,一种是静态的多态、一种是动态的多态;

静态的多态:函数重载,看起来调用同一个函数却有不同的行为。静态:原理是编译时实现。

动态的多态:一个父类的引用或指针去调用同一个函数,传递不同的对象,会调用不同的函数。动态:原理是运行时实现。


一、前言

        多态按字面的意思就是多种形态。当类之间存在层次结构,并且类之间是通过继承关联时,就会用到多态。C++ 多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数。下面的实例中,基类 Shape 被派生为两个类,如下所示:

#include <iostream> 
using namespace std;class Shape {
public:void area(){cout << "Parent class area :" << endl;}
};
class Rectangle : public Shape {
public:void area(){cout << "Rectangle class area :" << endl;}
};
class Triangle : public Shape {
public:void area(){cout << "Triangle class area :" << endl;}
};void func(Shape& p) {p.area();
}
// 程序的主函数
int main()
{Rectangle Rec;// 调用矩形的求面积函数 areafunc(Rec);Triangle Tri;// 调用三角形的求面积函数 areafunc(Tri);return 0;
}

当上面的代码被编译和执行时,它会产生下列结果:

Parent class area :
Parent class area :

        导致错误输出的原因是,调用函数 area() 被编译器设置为基类中的版本,这就是所谓的静态多态,或静态链接 - 函数调用在程序执行前就准备好了。有时候这也被称为早绑定,因为 area() 函数在程序编译期间就已经设置好了。

        但现在,让我们对程序稍作修改,在 Shape 类中,area() 的声明前放置关键字 virtual,其余不变,如下所示:

#include <iostream> 
using namespace std;class Shape {
public:virtual void area(){cout << "Parent class area :" << endl;}
};
class Rectangle : public Shape {
public:void area(){cout << "Rectangle class area :" << endl;}
};
class Triangle : public Shape {
public:void area(){cout << "Triangle class area :" << endl;}
};void func(Shape& p) {p.area();
}
// 程序的主函数
int main()
{Rectangle Rec;// 调用矩形的求面积函数 areafunc(Rec);Triangle Tri;// 调用三角形的求面积函数 areafunc(Tri);return 0;
}

修改后,当编译和执行前面的实例代码时,它会产生以下结果:

Rectangle class area :
Triangle class area :

        此时,编译器看的是指针的内容,而不是它的类型。因此,由于 tri 和 rec 类的对象的地址存储在 *shape 中,所以会调用各自的 area() 函数。

        正如您所看到的,每个子类都有一个函数 area() 的独立实现。这就是多态的一般使用方式。有了多态,您可以有多个不同的类,都带有同一个名称但具有不同实现的函数,函数的参数甚至可以是相同的。

二、多态的定义及实现

1.多态的构成条件 

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

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

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

2.虚函数

        虚函数 是在基类中使用关键字 virtual 声明的函数。在派生类中重新定义基类中定义的虚函数时,会告诉编译器不要静态链接到该函数。
        我们想要的是在程序中任意点可以根据所调用的对象类型来选择调用的函数,这种操作被称为动态链接,或后期绑定

        一旦定义了虚函数,该基类的派生类中同名函数也自动成为了虚函数。也就是说在派生类中有一个和基类同名的函数,只要基类加了virtual修饰,派生类不加virtual修饰也是虚函数。虚函数只能是类中的一个成员函数,不能是静态成员或普通函数。

        注意:我们在继承中为了解决数据冗余和二义性的问题,需要用到虚拟继承,关键字也是virtual,和多态中的virtual是没有关系的。

3.虚函数的重写

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

        通过对虚函数的重写,就能够实现多态:

#include<iostream>
using namespace std;//买票
class Person
{
public:virtual void BuyTicket() { cout << "买票-全价" << endl; }
};//学生买票
class Student : public Person
{
public:virtual void BuyTicket() { cout << "买票-半价" << endl; }
};//军人买票
class Soldier : public Person
{
public:void BuyTicket() { cout << "优先-买票-半价" << endl; }};//构成多态,传的哪个类型的对象,调用的就是这个类型的虚函数 --- 跟对象有关
//不构成多态,调用就是P的类型 --- 跟类型有关
void Func(Person& p) //或void Func(Person* p)
{p.BuyTicket();   //p->BuyTicket(); 
}int main()
{Person ps;Func(ps);   //没有任何身份去买票,一定是全价Student st;Func(st);   //以学生的身份去买票,是半价Soldier so;Func(so);   //以军人的身份去买票,是优先并且半价return 0;
}

4.虚函数重写的两个例外

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

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

另一种解释:

        C++中的协变(Covariance)指的是派生类的返回类型可以是基类函数的返回类型的子类型。当一个派生类继承了一个基类,并且覆盖(override)了基类中的虚函数时,可以使用协变来改变返回类型。

        具体而言,如果基类函数的返回类型是指针或引用,那么派生类中覆盖该函数时,返回类型可以是基类返回类型所指向或引用的类型的派生类型。

实现协变需满足以下条件:

  • 基类中的函数必须是虚函数(使用 virtual 关键字声明)。
  • 派生类中重写的函数必须具有相同的函数签名(函数名、参数列表和常量性)。
  • 派生类中重写的函数的返回类型必须是基类函数返回类型的子类型。

示例:

引用自:C++协变(covariant)-CSDN博客

        假设有一个基类 Animal 和两个派生类 Dog 和 Cat。Animal 类中有一个虚函数 makeSound(),它返回一个指向 Animal 对象的指针。在派生类 Dog 中,可以重写 makeSound() 函数并返回一个指向 Dog 对象的指针。同样,在派生类 Cat 中也可以重写 makeSound() 函数并返回一个指向 Cat 对象的指针。

#include <iostream>
class Animal {
public:virtual Animal* makeSound() {std::cout << "Animal makes a sound." << std::endl;return this;}
};
class Dog : public Animal {
public:virtual Dog* makeSound() {std::cout << "Dog barks." << std::endl;return this;}
};
class Cat : public Animal {
public:virtual Cat* makeSound() {std::cout << "Cat meows." << std::endl;return this;}
};
int main() {Animal* animal;Dog dog;Cat cat;animal = &dog;animal->makeSound();  // Output: "Dog barks."animal = &cat;animal->makeSound();  // Output: "Cat meows."return 0;
}

协变与多态的区别: 

        C++中协变和多态是密切相关的。多态(Polymorphism)指的是同一个函数在不同的对象上被调用时,可以表现出不同的行为方式。

        在C++中,通过使用虚函数(virtual function),实现了运行时多态的语法机制。基类中的虚函数可以在派生类中被重写(覆盖),当派生类对象调用该虚函数时,会根据对象的实际类型来确定调用哪个虚函数。

        而协变则指的是派生类可以改变继承自基类函数的返回类型,使得返回类型成为基类返回类型所指向或引用的类型的派生类型。

        通过将协变和多态结合起来,我们可以在派生类中覆盖基类的虚函数,并且返回派生类特有的类型。这就允许我们在多态的情况下,在派生类中使用更具体的返回类型。

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

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

        在C++中,析构函数是一种特殊的成员函数,用于在对象销毁时执行清理工作。通常情况下,析构函数会自动由编译器生成,默认执行对象的成员变量和基类的析构函数。

        当需要对派生类进行额外的清理工作或资源释放时,可以通过重写(override)基类的析构函数来实现。

        在派生类中重写析构函数需要遵循以下规则:

  1. 函数名与基类的析构函数完全相同。
  2. 参数列表为空。
  3. 返回类型为空(void)。
  4. 可以添加override关键字(可选),以显式地说明正在重写基类的析构函数。

        以下是一个示例代码:

class Base {
public:virtual ~Base() {// 基类的析构函数}
};class Derived : public Base {
public:~Derived() override {// 派生类的析构函数,重写了基类的析构函数}
};

        在上述代码中,基类Base定义了一个虚析构函数,派生类Derived通过重写基类的析构函数,实现了自己的清理逻辑。

        需要注意的是,在继承关系中,如果基类的析构函数是一个虚函数,则派生类中的析构函数也应该声明为虚函数。这样,在使用基类指针或引用指向派生类对象,并通过该指针或引用调用析构函数时,能够正确地调用到派生类的析构函数。

        总之,通过在派生类中重写基类的析构函数,可以实现额外的清理工作或资源释放。重写析构函数需要遵循特定的规则,并且建议将基类的析构函数声明为虚函数。

 

另一种解释:

(引用自:C++ 多态(一) : 多态的构成条件、final、override、协变、析构函数的重写、抽象类_c++ 多态 override-CSDN博客)

        析构函数虽然函数名不同,但是也能构成重写,因为站在编译器的视角,他所调用的析构函数名称都叫做destructor。

为什么编译器要通过这种方式让析构函数也能构成重写呢?

假设我用一个基类指针或者引用指向派生类对象,如果不构成多态会怎样?

class Human
{
public:~Human(){cout << "~Human()" << endl;}
};class Student : public Human
{
public:~Student(){cout << "~Student()" << endl;}
};int main()
{Human* h = new Student;delete h;return 0;
}

输出结果: 

~Human()

分析:

        上述代码只会调用类Human的析构函数,即如果不构成多态,那么指针是什么类型的,就会调用什么类型的析构函数,这也就导致了一种情况,如果派生类的析构函数中有资源释放,而这里却没有释放掉那些资源,就会导致内存泄漏的问题。

        所以为了防止这种情况,必须要将析构函数定义为虚函数。这也就是编译器将析构函数重命名为destructor的原因。

class Human
{
public:virtual ~Human(){cout << "~Human()" << endl;}
};class Student : public Human
{
public:virtual ~Student() // 该virtual关键字可省略{cout << "~Student()" << endl;}
};int main()
{Human* h = new Student;delete h;return 0;
}

输出结果: 

~Student()
~Human() 

5.C++11 override和final

        从上面可以看出,C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此:C++11提供了 override 和 final 两个关键字,可以帮助用户检测是否重写。

(1) final

        在C++11标准中,final是一个关键字,用于禁止继承和覆盖类的虚函数。当一个类或者一个类的成员函数被声明为final时,意味着它不能再被其他类继承或者它的虚函数不能被派生类覆盖。

使用final关键字的好处是:

  1. 可以增强代码的安全性:使用final关键字可以防止不恰当的继承和覆盖。
  2. 可读性:使用final关键字可以增强代码的可读性和可维护性,明确了类或函数的意图。

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


#include<iostream>
class Car
{
public:virtual void Drive() final{}
};class Benz :public Car
{
public:virtual void Drive() override //检查是否完成重写{std::cout << "Benz-舒适" << std::endl;}
};
int main() {Benz benz;benz.Drive(); 
}

 上述程序因为final关键字的存在会报错,报错原因是:

final:修饰类,表示该类不能再被继承。

示例:


#include<iostream>
class Car final
{
public:virtual void Drive() {}
};class Benz :public Car 
{
public:virtual void Drive()  //检查是否完成重写{std::cout << "Benz-舒适" << std::endl;}
};
int main() {Benz benz;benz.Drive(); 
}

上述程序报错: 

不能将final用于基类,否则程序报错! 

(2) override

        在C++中,override是一个特殊的关键字,用于显式地标识派生类中的函数是覆盖(override)基类中的虚函数。

        当派生类中的函数与基类中的虚函数具有相同的名称、参数列表和返回类型时,可以使用override关键字来明确指示该函数是对基类函数的覆盖。

使用override关键字的好处是:

  1. 错误检查:编译器会在编译时检查是否存在函数覆盖错误。如果派生类中使用了override关键字,但没有正确地覆盖基类中的虚函数,编译器将报错。
  2. 可读性:使用override关键字可以增强代码的可读性和可维护性,明确了派生类函数的意图。

下面是一个示例代码:


#include<iostream>
class Car
{
public:virtual void Drive(){}
};class Benz :public Car
{
public:virtual void Drive() override //检查是否完成重写{std::cout << "Benz-舒适" << std::endl;}
};
int main() {Benz benz;benz.Drive(); // Benz-舒适
}

三、抽象类

纯虚函数

        您可能想要在基类中定义虚函数,以便在派生类中重新定义该函数更好地适用于对象,但是您在基类中又不能对虚函数给出有意义的实现,这个时候就会用到纯虚函数。

        在虚函数的后面写上 = 0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。

class Shape {public:// pure virtual functionvirtual int area() = 0;
};

   = 0 告诉编译器,函数没有主体,上面的虚函数是纯虚函数

包括纯虚函数的类叫做抽象类,也叫接口类,抽象类不能实例化出对象。

示例:

#include<iostream>
//抽象类
class Car
{
public:virtual void Drive() = 0;//纯虚函数 
};int main()
{Car c;//抽象类不能实例化出对象return 0;
}

上述程序运行报错: 

 派生类继承后也不能实例化出对象。

示例:

#include<iostream>
class Car
{
public:virtual void Drive() = 0; // 纯虚函数
};
class Benz :public Car{};
int main()
{Benz b1;
}

上述程序运行出错: 

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

示例:

#include<iostream>
class Car
{
public://纯虚函数一般只声明,不实现(可以实现,但没有价值,因为不能实例化出对象,可以定义指针或引用)virtual void Drive() = 0;
};class Benz :public Car
{
public:virtual void Drive(){std::cout << "Benz-舒适" << std::endl;}
};class BMW :public Car
{
public:virtual void Drive(){std::cout << "BMW-操控" << std::endl;}
};int main()
{//派生类只有重写了纯虚函数才能实例化出对象Benz b1;BMW b2;//通过基类的指针去调用不同对象的函数Car* pBenz = new Benz;pBenz->Drive();Car* pBMW = new BMW;pBMW->Drive();
}

输出结果: 

Benz-舒适
BMW-操控

接口继承和实现继承

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

参考自(很值得学习):

【精选】【C++】—— 多态_c++多态_霄沫凡的博客-CSDN博客

注:参考内容只是为了自身学习,并无其他想法!!!

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

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

相关文章

5G学习笔记之5G频谱

参考&#xff1a;《5G NR通信标准》1. 5G频谱 1G和2G移动业务的频段主要在800MHz~900MHz&#xff0c;存在少数在更高或者更低频段&#xff1b;3G和4G的频段主要在450MHz ~ 6GHz&#xff1b;5G主要是410MHz ~ 6GHz&#xff0c;以及24GHz ~ 52GHz。 5G频谱跨度较大&#xff0c;可…

开源贡献难吗?

本文整理自字节跳动 Flink SQL 技术负责人李本超在 CommunityOverCode Asia 2023 上的 Keynote 演讲&#xff0c;李本超根据自己在开源社区的贡献经历&#xff0c;基于他在贡献开源社区过程中的一些小故事和思考&#xff0c;如何克服困难&#xff0c;在开源社区取得突破&#x…

Spark--经典SQL50题

目录 连接数据库准备工作 1、查询"01"课程比"02"课程成绩高的学生的信息及课程分数 2、查询"01"课程比"02"课程成绩低的学生的信息及课程分数 3、查询平均成绩大于等于60分的同学的学生编号和学生姓名和平均成绩 4、查询平均成绩…

在Kubernetes(k8s)上部署整个SpringCloud微服务应用

视频教程地址&#xff1a;https://www.bilibili.com/video/BV1Xh4y1q7aW/ 文章目录 项目准备打成使用Docker打成镜像准备Docker仓库打包项目为Docker镜像 部署应用到k8s创建nfs挂载目录创建一些基本资源创建命名空间创建拉取镜像的secret创建java运行环境的profile 部署mysql创…

Unity中Shader的XRay透视效果

文章目录 前言一、模拟菲涅尔效果1、获取 V 向量2、获取 N 向量3、点积输出效果4、模拟出菲涅尔效果(中间暗&#xff0c;周围亮) 二、实现 &#xff38;Ray 效果1、使用半透明排序、修改混合模式、加点颜色2、增加分层效果&#xff08;使用 frac 函数&#xff0c;只取小数部分&…

【深入探究Java集合框架】从List到Map的完整指南

文章目录 &#x1f31f; Java集合框架&#x1f34a; Collection&#x1f389; List&#x1f389; Set&#x1f389; Map &#x1f34a; 集合的选择&#x1f389; 1. 有序并允许重复元素的集合 List&#x1f389; 2. 无序并且不允许重复元素的集合 Set&#x1f389; 3. 维护映射…

【计网 EMail】计算机网络 EMail协议详解:中科大郑烇老师笔记 (五)

目录 0 引言1 电子邮件EMail1.1 组成1.2 SMTP协议1.3 案例&#xff1a;Alice给Bob发送报文1.4 SMTP总结1.5 邮件报文格式1.6 POP3协议和IMAP协议 &#x1f64b;‍♂️ 作者&#xff1a;海码007&#x1f4dc; 专栏&#xff1a;计算机四大基础专栏&#x1f4dc; 其他章节&#xf…

辅助驾驶功能开发-功能规范篇(16)-2-领航辅助系统NAP-安全接管策略

书接上回 2.3.6安全接管策略 为保障辅助驾驶车辆的安全性,在辅助驾驶系统运行过程中,对出现的影响系统稳定性的异常情况,制定对应的安全接管策略。 异常情况可能包括:系统传感器、控制器、执行器、电源、通讯、备份系统等的故障或失效,驾驶员异常行为(如不响应车辆的接管…

毅速丨3D打印结合拓扑优化 让轻量化制造更容易

制造轻量化对于提高能源利用效率、提高产品性能和减少环境影响&#xff0c;推动制造业的绿色化、高质量发展具有重要的促进作用。 轻量化设计对许多领域都有着重要影响&#xff0c;尤其是那些需要降低能源消耗、提高运输效率或减少对环境影响的领域。如航空航天&#xff0c;轻量…

【论文阅读笔记】 Curated Pacific Northwest AI-ready Seismic Dataset

Curated Pacific Northwest AI-ready Seismic Dataset 太平洋西北部人工智能地震数据集 摘要 描述了一个AI就绪地震数据集包括各种地震事件参数 仪器元数据 地震波行描述地震目录和事件属性&#xff08;事件震级类型&#xff0c;信道类型&#xff0c;波形极性&#xff0c;信…

网络协议--ICMP:Internet控制报文协议

6.1 引言 ICMP经常被认为是IP层的一个组成部分。它传递差错报文以及其他需要注意的信息。ICMP报文通常被IP层或更高层协议&#xff08;TCP或UDP&#xff09;使用。一些ICMP报文把差错报文返回给用户进程。 ICMP报文是在IP数据报内部被传输的&#xff0c;如图6-1所示。 ICMP…

【Overload游戏引擎细节分析】Lambert材质Shader分析

一、经典光照模型&#xff1a;Phong模型 现实世界的光照是极其复杂的&#xff0c;而且会受到诸多因素的影响&#xff0c;这是以目前我们所拥有的处理能力无法模拟的。经典光照模型冯氏光照模型(Phong Lighting Model)通过单独计算光源成分得到综合光照效果&#xff0c;然后添加…

08-React扩展

08-React扩展 1. setState的2种写法 案例&#xff1a; export default class Demo extends Component {state {count: 0}add () > {// 获取当前的值const { count } this.state// 更新状态this.setState({ count: count 1 })console.log(count);}render() {const { coun…

JavaScript从入门到精通系列第二十二篇:JavaScript中的toString方法和JavaScript中的垃圾回收

文章目录 一&#xff1a;toString方法 1&#xff1a;怪异的返回值[object Object] 2&#xff1a;打印对象成为一个JSON 二&#xff1a;垃圾回收&#xff08;GC&#xff09; 1&#xff1a;垃圾回收概念 2&#xff1a;JS当中的垃圾回收机制 3&#xff1a;JS中的垃圾回收算…

基于厨师优化的BP神经网络(分类应用) - 附代码

基于厨师优化的BP神经网络&#xff08;分类应用&#xff09; - 附代码 文章目录 基于厨师优化的BP神经网络&#xff08;分类应用&#xff09; - 附代码1.鸢尾花iris数据介绍2.数据集整理3.厨师优化BP神经网络3.1 BP神经网络参数设置3.2 厨师算法应用 4.测试结果&#xff1a;5.M…

基于ssm的旅游管理系统

功能如下图所示 摘要 基于SSM框架的旅游管理系统代表了信息技术在旅行业中的崭新机遇&#xff0c;为旅行企业提供了强大的工具&#xff0c;以应对现代旅游市场的复杂挑战。这个系统的研发和实施具有广泛的研究意义&#xff0c;它深刻影响了旅游业的发展&#xff0c;具体表现如下…

自然语言处理---Transformer机制详解之GPT模型介绍

1 GPT介绍 GPT是OpenAI公司提出的一种语言预训练模型.OpenAI在论文<< Improving Language Understanding by Generative Pre-Training >>中提出GPT模型.OpenAI后续又在论文<< Language Models are Unsupervised Multitask Learners >>中提出GPT2模型.…

【LeetCode刷题(数据结构与算法)】:数据结构中的常用排序实现数组的升序排列

现在我先将各大排序的动图和思路以及代码呈现给大家 插入排序 直接插入排序是一种简单的插入排序法&#xff0c;其基本思想是&#xff1a; 把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中&#xff0c;直到所有的记录插入完为 止&#xff0c;得到一个…

基于协作搜索优化的BP神经网络(分类应用) - 附代码

基于协作搜索优化的BP神经网络&#xff08;分类应用&#xff09; - 附代码 文章目录 基于协作搜索优化的BP神经网络&#xff08;分类应用&#xff09; - 附代码1.鸢尾花iris数据介绍2.数据集整理3.协作搜索优化BP神经网络3.1 BP神经网络参数设置3.2 协作搜索算法应用 4.测试结果…

图论04-【无权无向】-图的广度优先遍历

文章目录 1. 代码仓库2. 广度优先遍历图解3.主要代码4. 完整代码 1. 代码仓库 https://github.com/Chufeng-Jiang/Graph-Theory 2. 广度优先遍历图解 3.主要代码 原点入队列原点出队列的同时&#xff0c;将与其相邻的顶点全部入队列下一个顶点出队列出队列的同时&#xff0c;将…