[C++进阶]多态的概念、定义与实现

多态,顾名思义,即多种形态。具体来说,就是不同对象执行同一行为而产生不同的结果

一、多态的概念

多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。

比如说买票:普通人买的就是成人票,全价,学生呢?一般买的是学生票这个就是半价,二种票价的体现,就是说为的多态。

二、多态的定义与实现

多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如Student继承了Person。Person对象买票全价,Student对象买票半价。
那么在继承中要构成多态还有两个条件:

必须通过基类的指针或者引用调用虚函数
被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写。

1.虚函数

虚函数:即被virtual修饰类成员函数称为虚函数

如下:

class A
{
public:virtual viod func(){}
};

2.虚函数重写


虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。
注意:这里是重写/覆盖,不是重定义/隐藏,重定义/隐藏是继承中的,子类的成员名与父类的成员名相同的时候,子类会覆盖掉父类,只需要名字相同即可而这里是都要相同如下所示,就是重写

class Person {
public:virtual void BuyTicket() const { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:virtual void BuyTicket() const { cout << "买票-半价" << endl; }
};
void Func(const Person& p)
{p.BuyTicket();
}
int main()
{Student s;Person p;Func(p);Func(s);return 0;
}

运行结果:

这就是多态,不同的对象执行的结果不一样

但看代码,还是会觉得很神奇的,因为毕竟形参是一个Person类型的对象,居然能调用Student里面的函数。

其实虽然形参是Person类型的,这里的引用切割以后,只是让p指向了s中的Person的那一部分
多态调用看的是指向的对象的类型,普通对象看的是当前者的类型

3.多态的两个条件

多态有两个很重要的条件

  1. 调用函数是重写的虚函数(注意重写的条件是虚函数+三同)
  2. 基类指针或者引用

当我们将引用去掉以后,这里其实就变成了一个对象的切割/切片了。那么此时这个p就只能调用自己里面的函数了,不构成多态了。多态的两个重要条件里面的第二个就不满足了

class Person {
public:virtual void BuyTicket() const { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:virtual void BuyTicket() const { cout << "买票-半价" << endl; }
};
void Func(const Person p)
{p.BuyTicket();
}
int main()
{Student s;Person p;Func(p);Func(s);return 0;
}

运行结果:

如果将父类的virtual给去掉了,那么最终也是不构成多态的,不满足第一个多态的条件

class Person {
public:void BuyTicket() const { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:virtual void BuyTicket() const { cout << "买票-半价" << endl; }
};
void Func(const Person& p)
{p.BuyTicket();
}
int main()
{Student s;Person p;Func(p);Func(s);return 0;
}

运行结果:

总之必须得遵循以上两个条件

除此之外,我们调用的时候是正常的调用,不能是指定调用。如果是指定调用的话,那当然不会触发多态了

上面的例子都是引用的例子,下面来一个指针的例子

如下所示,指针或引用都是可以触发多态的

class Person {
public:virtual void BuyTicket() const { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:virtual void BuyTicket() const { cout << "买票-半价" << endl; }
};
void Func(const Person* p)
{p->BuyTicket();
}
int main()
{Student s;Person p;Func(&p);Func(&s);return 0;
}

运行结果:

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

重写的条件本来是虚函数加三同,但是有一些例外

  1. 子类可以不加virtual,但是父类必须加上去

如下所示,是第一种情况的例子,子类可以不加virtual。但是父类必须加上去。

class Person {
public:virtual void BuyTicket() const { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:void BuyTicket() const { cout << "买票-半价" << endl; }
};
void Func(const Person* p)
{p->BuyTicket();
}
int main()
{Student s;Person p;Func(&p);Func(&s);return 0;
}

运行结果:

原因其实也是比较好想的,因为父类已经加上了virtual,这里的派生类继承了父类以后,里面是有virtual的,这里的多态其实只是改变的函数的实现。

关于这一个例外,建议全部加上virtual,但是实际上子类不加上virtual也是可以的。

2.协变(返回的值可以不同,但是要求返回值必须是父子关系的指针或者引用)
注意,这一点例外,是C++的大坑之一。

首先正常的返回值不同是会报错的。如下所示,显示重写的虚函数返回值有差异,且不是协变

也就是说,返回值不同的时候,必须满足协变,否则报错

如下就是满足了协变的条件,可以正常实现多态

class Person {
public:virtual Person* BuyTicket() const { cout << "买票-全价" << endl; return 0; }
};
class Student : public Person {
public:virtual Student* BuyTicket() const { cout << "买票-半价" << endl; return 0;}
};
void Func(const Person* p)
{p->BuyTicket();
}
int main()
{Student s;Person p;Func(&p);Func(&s);return 0;
}

运行结果:

注意,这里的父子类关系的指针,不是必须是自己的,也可以是其他类的,只要满足是父子类关系的指针或引用都是可以的。还有一点要注意的是,不可以一个是指针,一个是引用,必须同时是指针,或者同时是引用。以及,必须父类中的返回值是父类的指针或者引用,子类的返回值是子类的指针或者引用。不可以反过来

5.总结

多态的实现有两个条件

  1. 必须通过基类的指针或者引用调用虚函数
  2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写(也就是要调用重写的虚函数)

6.析构函数的重写(虚函数重写的第三个例外)

  首先我们思考以下问题:

析构函数可以是虚函数吗?为什么要是虚函数?

析构函数加上virtual是不是虚函数重写呢?

其实类析构函数都被处理成了destructor这个统一的名字
那么为什么要这么处理呢?其实也很简单,因为要让他们构成重写(重写的条件无论是三同还是例外,都需要让函数名相同的)
那么为什么要让他们构成重写呢?我们看到如下代码:

class Person {
public:virtual ~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:virtual ~Student() { cout << "~Student()" << endl; }
};int main()
{Person p;Student s;return 0;
}

运行结果如下所示,符合我们的预期(子类里面有一个父类,它会像栈一样,先析构后面生成的,在析构前面的。所以先析构子类,然后析构子类中的父类,最后析构父类)

但是如果我们不加virtual

class Person {
public:~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:~Student() { cout << "~Student()" << endl; }
};int main()
{Person p;Student s;return 0;
}

运行结果:

我们不加virtual好像也没什么问题,

好像似乎我们不需要让析构函数进行重写也是没有任何问题的?编译器还让这个函数名处理成了destructor,同名了,似乎也还构成了隐藏/重定义的关系。因为隐藏/重定义只需要在继承关系中,成员名相同即可。就可以使得子类可以隐藏父类的成员。

所以C++即便不重写析构函数也是没有任何问题的吧?

其实如果不重写的话,有一个场景是过不去的,这属于一个特例:

class Person {
public:~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:~Student() { cout << "~Student()" << endl;delete[] _ptr;}
protected:int* _ptr = new int[10];
};int main()
{Person* p = new Person;delete p;p = new Student;delete p;return 0;
}

运行结果:

诶,我们析构的两次都是person,这会导致什么问题呢?对,聪明的同学肯定猜到了,没错就是内存泄漏,student中开辟的空间并没有被释放。Student中int*所指向的那块空间已经被泄露出去了。

那么为什么会发生内存泄漏呢?其实是因为没有调用到派生类的析构函数。

那么为什么又调不到派生类的析构函数呢?

我们在前面说过,类的析构函数都被处理成了destructor这个函数。

而delete p的本质其实也可以分为两部分,一部分是调用析构函数,即p->destructor(),另外一部分是调用operator delete§。

而这里就恰好编译器处理成一样的函数名了,加之这里并没有virtual构成重写/覆盖,反而是构成了隐藏/重定义了。而且p刚好是Person类型的指针,是一个普通的调用,不是多态调用。而在前文中也说过:普通调用,看的是当前者的类型。多态调用,看的是其指向的类型,编译器就以为调用的是Person的析构函数了。

但是我们肯定调用的不是Person的析构啊。虽然p这个指针的类型是Person*,但是它是一个基类,它有可能指向一个基类,也有可能指向一个派生类对象。我们希望它指向什么类型,就调用什么类型的析构函数,这样才可以保证不会产生内存泄漏。

而指向什么类型,调用什么类型,这不就正好符合多态调用吗?

那么我们就得知了,加上一个virtual进行修饰,刚好就是满足虚函数+三同,正好满足了成为多态的第一个条件,并且这里的p就是基类的指针,这也满足了成为多态的第二个条件,必须是基类的指针或者引用去调用这个重写的虚函数。刚好满足多态的条件。

满足了多态的条件,就可以解决这里的问题了。

class Person {
public:virtual ~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:virtual ~Student(){cout << "~Student()" << endl;delete[] _ptr;}
protected:int* _ptr = new int[10];
};int main()
{Person* p = new Person;delete p;p = new Student;delete p;return 0;
}

运行结果:

其实一切的源头还是在于,C++允许了切片的行为。才导致父类指针可以指向一个子类对象,造成的一系列问题。而C++允许切片的行为,也是由于要实现多态,必须要使用切片。如下所示:

class Person {
public:virtual ~Person() { cout << "~Person()" << endl; }virtual Person* BuyTicket() const { cout << "买票-全价" << endl; return 0; }};
class Student : public Person {
public:virtual ~Student(){cout << "~Student()" << endl;delete[] _ptr;}virtual Student* BuyTicket() const { cout << "买票-半价" << endl; return 0; }protected:int* _ptr = new int[10];
};int main()
{Person* p = new Person;p->BuyTicket();delete p;p = new Student;p->BuyTicket();delete p;return 0;
}

运行结果:

这样一来,我们知道了,为了实现多态搞出来的切片和虚函数,但又由于切片导致了析构函数存在内存泄漏的问题,所以便通过多态去解决了这个内存泄漏问题。

这里我们也可以称之为虚函数重写的第三个例外,即析构函数的重写。从形式上来看他们的函数名是不一样的,不满足三同,但是由于编译器对函数名进行了处理,导致满足了三同,构成了虚函数的重写。从而解决了一类问题。

而虚函数重写的第一个例外是,基类必须加上virtual,但是子类可以不用加。我们可以这样理解,这个例外正好可以为析构函数服务。因为析构函数正常来说是没有返回值的,加上一个virtual其实挺奇怪的。于是我们可以默认说,基类的析构函数最好加上virtual,其他的照常即可。这样也正好就没有这个问题了。

加上了virtual的话, 会建立一张虚表,它可能会造成一定的伤害,但是从总体上来看,还是很有必要的,利远大于弊!如果我们对库进行查看,有不少析构函数都加上了virtual。

7. C++11之override 和 final

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

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

final:可以修饰变量、函数和类。
对于变量,确保初始化后不能被修改
对于函数,确保不能被子类重写
对于类,确保不能被继承

class Car
{
public:virtual void Drive() final {};
};
class Benz : public Car
{
public:virtual void Drive(){cout << "123456" << endl;}
};

如上所示,当我们想要运行的时候,会报错的,因为声明为final的函数是无法被重写的。​​​​​​​需要注意的是虚函数重写的第一个例外,子类可以不加virtual,因为下面也是构成重写的,所以也会报错。

但是如果我们直接改变了虚函数使它不会构成重写了,自然也不会报错了,注意这,我们加上了virtual,但是并不满足多态,不构成重写,但是满足函数名相同,满足了隐藏/重定义。这里的virtual的功能虽然不构成重写,但是万一以后有其他类要继承它的时候,就有可能会存在这两个构成重写的。

#include<iostream>
using namespace std;
class Car
{
public:virtual void Drive() final {};
};
class Benz : public Car
{
public:virtual void Drive(int i){cout << "123456" << endl;}
};

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

class Car
{
public:virtual void Drive() {}
};class Benz :public Car
{
public:virtual void Drive() override { cout << "Benz-舒适" << endl; }
};
int main()
{return 0;
}
class Car
{
public:void Drive() {};
};
class Benz : public Car
{
public:virtual void Drive() override{cout << "123456" << endl;}
};int main()
{return 0;
}

比较我们的上面的两段代码,第一段构成重写,我们运行最后会返回0,第二段我们基类没有加上virtual,就会报错了。这里只是构成了隐藏/重定义。

这些都讲完了现在我来问问大家如何设计一个类,使得这个类不会被继承

这个很简单吧!

肯定有人会这么想把基类构造函数私有化

class A
{
private:A() {};
};
class B : public A
{};
int main()
{B b;return 0;
}

我们将基类的构造函数给私有化,B自然就无法继承A了,B在定义对象的时候,会有一个构造函数,这个构造函数无论如何都会默认先走A的构造函数,但是这里我们将构造函数给屏蔽了。没法继承了。所以我们定义的对象b也就无法通过编译了

但是如果我们这么做了A类型的对象不是也创建不了吗?

为了解决这个问题,我们可以使用一个函数来返回这个A的匿名对象,这个是传值返回,所以A的匿名对象会先拷贝构造给临时变量,这个临时变量具有常性,然后我们这里看似是一个=,实际上是一个拷贝构造。利用这个临时对象去拷贝构造给这个a对象。不过上面只是我们的分析过程,我们讲过这种情况编译器会对这个过程进行优化处理,即拷贝构造+拷贝构造->拷贝构造。

这里我们还需要加上一个static将其变为静态的成员函数,这是为了避免调用这个函数还需要先创建一个对象去调用,而这个对象本身就是为了创建一个对象的,陷入了先有鸡还是先有蛋的纠缠。而我们加上了static以后,就没有了this指针,自然就可以直接使用类域去访问这个函数了。

class A
{
public :static A Creatobj(){return A();}
private:A() {};
};
class B : public A
{};
int main()
{A a = A::Creatobj();return 0;
}

第二个方案是析构函数私有化

class A
{
public:
private:~A() {};
};
class B : public A
{};
int main()
{A a;B b;return 0;
}

这个代码也是会报错的,我们使用的方案是析构函数私有化。这样一来也是可以的。但是我们又创建不了A类型的对象了

我们可以使用new来解决这个问题,我们的问题就是这个对象在栈区,它生命周期结束后会自动调用析构函数,但是析构函数被封锁了,无法调用,那我们直接将这个对象放在堆区的话,不就可以解决这个问题了吗。不过这样,我们释放不了这个内存了。所以我们可以和构造函数一样的方法,写一个静态的函数destroy去释放这些资源即可。

class A
{
public:
private:~A() {};
};
class B : public A
{};
int main()
{A *pa=new A;return 0;
}

第三种方案是基类使用final

前两种方案能用打架闹事未免显得太过于繁琐了,上面我们说过C++11的final的作用,所以我们可以直接用final修饰

class A final
{};
class B : public A
{};
int main()
{return 0;
}

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

​​​​​​​

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

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

相关文章

神经网络与注意力机制的权重学习对比:公式探索

神经网络与注意力机制的权重学习对比&#xff1a;公式探索 注意力机制与神经网络权重学习的核心差异 在探讨神经网络与注意力机制的权重学习时&#xff0c;一个核心差异在于它们如何处理输入数据的权重。神经网络通常通过反向传播算法学习权重&#xff0c;而注意力机制则通过学…

LLMs之Llama 3.1:Llama 3.1的简介、安装和使用方法、案例应用之详细攻略

LLMs之Llama 3.1&#xff1a;Llama 3.1的简介、安装和使用方法、案例应用之详细攻略 导读&#xff1a;2024年7月23日&#xff0c;Meta重磅推出Llama 3.1。本篇文章主要提到了Meta推出的Llama 3.1自然语言生成模型。 背景和痛点 >> 过去开源的大型语言模型在能力和性能上一…

Kylin Cube构建日志分析:洞察大数据构建过程的窗口

Kylin Cube构建日志分析&#xff1a;洞察大数据构建过程的窗口 Apache Kylin是一款为Hadoop优化的开源分布式分析引擎&#xff0c;它通过构建数据立方体&#xff08;Cube&#xff09;来实现对大数据的快速查询。在维护和优化Cube的过程中&#xff0c;构建日志分析是一个重要的…

Docker 常用命令详解

目录 Docker 简介安装 DockerDocker 基本命令 镜像命令容器命令网络命令 Docker 高级命令 数据卷Docker Compose 实战案例 部署一个简单的 Web 应用使用 Docker Compose 管理多容器应用 总结 Docker 简介 Docker 是一个开源的容器化平台&#xff0c;提供了简化应用程序开发、…

OCC 创建方管(拉伸操作)

目录 一、OCC 拉伸操作 二、例子 1、使BRepBuilderAPI_MakeFace 2、使用BRepPrimAPI_MakeRevol 3、垂直路径扫掠 一、OCC 拉伸操作 BRepPrimAPI_MakeSweep Class Reference - Open CASCADE Technology Documentation OCC提供几种图形的构建是由基本图形的旋转,拉伸等方…

C++进程遍历的几种方法

在应用层下&#xff0c;进程遍历有多种方式&#xff0c;这里介绍几种常用的方式&#xff1a;进程快照、NtQuerySystemInformation、EnumProcesses函数、WMI等。 在C#中Process类提供了一个GetProcesses()函数&#xff0c;这个函数内部就是调用的NtQuerySystemInformation进行获…

基于STM32瑞士军刀--【FreeRTOS开发】学习笔记(二)|| 堆 / 栈

堆和栈 1. 堆 堆就是空闲的一块内存&#xff0c;可以通过malloc申请一小块内存&#xff0c;用完之后使用再free释放回去。管理堆需要用到链表操作。 比如需要分配100字节&#xff0c;实际所占108字节&#xff0c;因为为了方便后期的free&#xff0c;这一小块需要有个头部记录…

电子加密狗的定义与功能

电子加密狗&#xff0c;也称为加密锁、硬件锁或USB密钥&#xff0c;是一种用于软件保护和授权管理的硬件设备。它通常是一个外部设备&#xff0c;插入到计算机的USB接口上&#xff0c;通过加密算法和技术来确保软件的安全性和防止非法复制、盗版以及未经授权的使用。以下是关于…

软件测试面试准备工作

1、 什么是数据库? 答&#xff1a;数据库是按照某种数据模型组织起来的并存放二级存储器中的数据集合。 2、 什么是关系型数据库? 答&#xff1a;关系型数据库是建立在关系数据库模型基础上的数据库&#xff0c; 借助集合代数等概念和方法处理数据库中的数据。目前主流的关…

AR 眼镜之-蓝牙电话-实现方案

目录 &#x1f4c2; 前言 AR 眼镜系统版本 蓝牙电话 来电铃声 1. &#x1f531; 技术方案 1.1 结构框图 1.2 方案介绍 1.3 实现方案 步骤一&#xff1a;屏蔽原生蓝牙电话相关功能 步骤二&#xff1a;自定义蓝牙电话实现 2. &#x1f4a0; 屏蔽原生蓝牙电话相关功能 …

[linux] seqeval安装报错

新建一个新的环境 然后安装&#xff1a; # 不能拷贝别人的环境再安mebert_wash的环境。有冲突。我需要重新安一个空的conda环境&#xff0c;再安装。 # conda create -n wash python3.10 ipykernel python -m pip install --upgrade setuptools python -m pip install --upgr…

【Unity】关于Luban的简单使用

最近看了下Luban导出Excel数据的方式&#xff0c;来记录下 【Unity】关于Luban的简单使用 安装Luban开始使用UnityLubanC# 扩展 安装Luban Luban文档&#xff1a;https://luban.doc.code-philosophy.com/docs/beginner/quickstart 1.安装dotnet sdk 8.0或更高版本sdk 2.githu…

ViewPager2实现原理分析

ViewPager2 是 Android 开发中用于实现水平滑动视图的组件&#xff0c;它是 ViewPager 的一个改进版&#xff0c;提供了更多的功能和更好的性能。下面&#xff0c;我们将结合源码来简要分析 ViewPager2 的实现原理。 1. 基本架构 ViewPager2 的主要架构基于 RecyclerView&…

Activiti 6 兼容openGauss数据库bytes类型不匹配

当前有个项目需要做国产调研&#xff0c;需要适配高斯数据库&#xff0c;项目启动的时候&#xff0c;提示column "bytes_" is type bytea but expression is of type blob byte_字段是act_ge_bytearray表的&#xff0c;openGauss里的类型是bytea&#xff0c;类型是匹…

Mysql或MariaDB数据库的用户与授权操作——实操保姆级教程

一、问题描述 在日常的工作中,我们需要给不同角色的人员创建不同的账号,他们各自可访问的数据库或权限不一样,这时就需要创建用户和赋予不同的权限内容了。 二、问题分析 1、创建不同的角色账号; 2、给这些账号授予各自可访问数据库的权限。 三、实现方法 Centos8安装…

房子装修完显得大的一些

雅静说房子装修完怎么让它显得大一些      说七点,给大家总结装修三十年的经验      1,把阳台纳入大厅里来,拆掉开发商给的推拉门,换个大点但不影响通风的窗户      视觉上的通透感就会显得空间更大      2,全屋通铺,在瓦工阶段跟师父交代好,      直接通铺…

Java Generic练习(2024.7.25)

GenericExercise1 package GenericExercise20240725;import java.util.ArrayList; import java.util.List;public class GenericExercise1 {public static void main(String[] args) {// 泛型是JDK5以后引入的新的特性&#xff0c;主要目的是为了提供编译时的类型安全检测机制…

STM32——GPIO(LED闪烁)

一、什么是GPIO&#xff1f; GPIO&#xff08;通用输入输出接口&#xff09;&#xff1a; 1.GPIO 功能概述 GPIO 是通用输入/输出&#xff08;General Purpose I/O&#xff09;的简称&#xff0c;既能当输入口使用&#xff0c;又能当输出口使用。端口&#xff0c;就是元器件…

Java 代码规范if嵌套

在Java编程中&#xff0c;过度的if嵌套会使代码难以阅读和维护。为了遵循良好的代码规范&#xff0c;我们应尽量减少嵌套的深度。这通常可以通过重新组织代码或使用其他结构&#xff08;如switch语句&#xff0c;或者将逻辑封装到单独的方法中&#xff09;来实现。 以下是一个…

android settings提示音开关状态与修改(一)

android系统&#xff0c;settings提示音类型&#xff1a; 提示音开关默认状态&#xff0c;定义文件&#xff1a; frameworks/base/packages/SettingsProvider/res/values/defaults.xml 提示音默认定义&#xff1a; // 锁屏提示音 <integer name"def_lockscreen_sounds_…