一文搞懂C++多态的用法

目录
  • 前言
  • 1.多态的概念
  • 2.C++中多态的分类
    • (1)静态多态
    • (2)动态多态
  • 3.多态的构成条件
    • (1)举例
    • (2)两个概念
    • (3)多态的构成条件
  • 4.虚函数重写的两个例外
    • (1)协变
  • 5.final与override
    • (1)final
    • (2)override
  • 6.抽象类
  • 7.总结

前言

C++多态是在继承的基础上实现的,了解多态之前我们需要掌握一定的C++继承的知识,本文将介绍C++中多态的概念,构成条件以及用法。

1.多态的概念

多态,通俗来讲就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。比如,在买票这一行为,普通人买票是全价买票,学生买票是半价买票,而军人买票是优先买票;再比如动物园的动物叫这个行为,不同的动物叫声是不一样的。这些都是生活中多态的例子。

2.C++中多态的分类

(1)静态多态

静态多态是指在编译时实现的多态,比如函数重载,看似是调用同一个函数其实是在调用不同的。
比如我们使用cout这个对象来调用<<时:

int i=1;
double d=2.2;
cout<<i<<endl;
cout<<d<<endl;

虽然调用的都是<<,但其实调用的是不同的操作符重载之后的函数。

函数重载在之前的文章中详细讲解过,这里就不再赘述。

(2)动态多态

动态多态也就是我们通常所说的多态,本文以下内容均为动态多态内容。动态多态是在运行中实现的,

当一个父类对象的引用或者指针接收不同的对象(父类对象or子类对象)后,调用相同的函数会调用不同的函数。

这段话也许比较绕,这里只是给出一个概念,可以结合下面的例子来进行理解。

3.多态的构成条件

(1)举例

我们先根据一个构成例子来理解多态构成的条件:

#include<iostream>
#include<string>
using namespace std;
class Person
{
public:
    virtual void BuyTicket()
    {
        cout << "全价买票" << endl;
    }
};
class Student :public Person
{
public:
    virtual void BuyTicket()
    {
        cout << "半价买票" << endl;
    }
};
void Func(Person& p)
{
    p.BuyTicket();
}
int main()
{
    Person p1;
    Student p2;
    Func(p1);
    Func(p2);
}

我们先来看这样一段代码,其中子类Student继承了父类。运行起来打印的结果是:

我们在反观上述中动态多态的定义,用父类的引用或者指针(这里使用的是Person& p)来接收不同类型的对象(p1和p2),该引用或指针调用相同的函数(都调用了p.BuyTicket()),都调用了各自类中不同的函数(打印的结果不同)。我们将这一过程称为动态多态。

如果我们不传指针或者引用,那么将不构成多态(原理会在多态原理中详细解读)。

(2)两个概念

在解释多态的构成条件之前我们还需要了解两个概念。

虚函数

虚函数,即被virtual修饰的类成员函数称为虚函数。

比如上面代码中父类和子类的成员函数就是虚函数。

virtual void BuyTicket()
    {
        cout << "全价买票" << endl;
    }

关于虚函数还需要注意几点:

1.普通的函数不能是虚函数,只能是类中的函数。

2.静态成员函数不能加virtual

总结起来就是只能是类的非静态成员函数才能去形成虚函数。

虚函数的重写

虚函数的重写又称为虚函数的覆盖(重写是表面意义上的,覆盖是指原理上的):派生类中有一个根基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型,函数名,参数列表完全相同),称子类的虚函数重写了父类的虚函数。

//父类中的虚函数
virtual void BuyTicket()
    {
        cout << "全价买票" << endl;
    }
//子类中的虚函数
virtual void BuyTicket()
    {
        cout << "半价买票" << endl;
    }

两个虚函数满足返回值类型相同,函数名相同,参数列表相同。因此子类的虚函数重写了父类的虚函数。

注意,只有虚函数才能构成重写。

(3)多态的构成条件

多态的构成满足两个条件:

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

2.被调用的虚函数的派生类必须完成了对基类虚函数的重写。

我们在来看上面的代码,确实满足该条件:

1.使用了父类引用p来调用虚函数。

2.派生类的虚函数完成了对基类的虚函数的重写。

我们首先要明确使用多态的目的,就是使用不同的对象去完成同一个任务的时候会产生不同的结果。
如果我们拿掉以上任何一个条件都不会再构成多态,比如我们不使用指针或者引用去接收对象从而调用虚函数,而是使用对象呢?

void Func(Person p)
{
    p.BuyTicket();
}

此时我们会发现,打印的结果发生了变化:

这是不满足我们的预期的,因为不同的对象传给了p,p调用相同的函数却打印了相同的结果。

我们还可以将更改参数列表或者将父类的virtual拿掉,发现依然不是我们想要的结果。

但是有两个特殊的情况除外:

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

(1)协变

如果我们将父类和子类中的虚函数的返回值设为不同,可能会发生如下报错:

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

简单来说就是两者的返回值是父子关系的指针或者引用。

举一个例子:

class A{};
class B:public A
    {};
class Person
{
public:
    virtual A* BuyTicket()
    {
        A a;
        cout << "全价买票" << endl;
        return &a;
    }
};
class Student :public Person
{
public:
    virtual B* BuyTicket()
    {
        B b;
        cout << "半价买票" << endl;
        return &b;
    }
};

我们将上一段代码进行了改写,定义了B继承A,而在Person和Student两个类中的虚函数中将返回值分别置为A和B,由于A和B是继承关系,所以仍然可以构成多态,我们称派生类的虚函数为基类的虚函数的协变。

注意返回值必须是指针或者引用,对象不会构成协变。

(2)析构函数的重写

首先我们先回顾一下没有构成多态的析构函数调用:只需要子类对象销毁时无需手动销毁父类对象,会自动调用父类对象的析构函数。

1.如果基类的析构函数为虚函数,此时子类的析构函数无论加不加virtual,都是对父类的析构函数的重写。

2.虽然子类和父类的析构函数的函数名不同,但其实编译器对析构函数的名称进行了特殊的处理,都处理成了destructor。

下面举例说明,将Person和Student写入析构函数:

//父类中的析构函数
   virtual ~Person()
   {
        cout << "~Person" << endl;
    }
//子类中的析构函数
    virtual ~Student()
    {
        cout << " ~Student" << endl;
    }
//主函数
    Person* p1 = new Person;
    Person* p2 = new Student;
    delete p1;
    delete p2;

构成多态的结果是,Person*类型的p1和p2,接收两个不同类型的对象即Person类型和Student类型,在调用析构函数的时候可以分开调用(子类对象调用子类的析构函数,父类对象调用父类的析构函数。)

我们将上述代码运行一下,会发现:

结果的确是如此,当析构父类对象时,调用父类的析构函数,当析构子类对象时,调用的是子类的析构函数和父类的析构函数。

如果我们不使用父类指针进行管理,而是使用对象来接收子类对象呢?

    Student p2;
    Person p3 = p2;

此时我们发现打印的结果是:

在析构p3的时候,并没有根据按Student类的规则来进行析构。

同时,当我们将派生类的virtual去掉的时候,仍然可以构成多态,这与底层原理有关,在下面的介绍中会提及。为了统一性,不建议将virtual拿掉,C++大佬为了防止发生不必要的内存泄漏,所以设置了这一规则。这就导致所有的其实派生类的所有虚函数virtual都可以省略。这是由于其继承了基类的virtual属性,具体的还要在底层去理解,再强调一遍,尽量不要在派生类中省略virtual。

5.final与override

(1)final

限制类不被继承

但我们想要设计一个不被继承的类时,目前我们知道的有一种方法:就是将父类的构造函数设为私有(这是因为子类需要调用父类的构造函数来进行初始化)。如果使用这种方式,定义父类对象的话需要使用单例模式。

final提供了另一种方式来限制一个类不会被继承。

只需要在不想被继承的类后加final即可:

class Person final
{
public:
    virtual A* BuyTicket()
    {
        A a;
        cout << "全价买票" << endl;
        return &a;
    }
    virtual ~Person()
   {
        cout << "~Person" << endl;
    }
};

此时如果子类去继承Person的话会报错。

限制虚函数不被重写

当我们在函数后加上final的时候,该虚函数将不能被子类中的虚函数重写,否则会发生报错。

    virtual A* BuyTicket() final
    {
        A a;
        cout << "全价买票" << endl;
        return &a;
    }

(2)override

将override放在子类的重写的虚函数后,判断是否完成重写(重写的是否正确)

     virtual B* BuyTicket(int i=10) override
    {
        B b;
        cout << "半价买票" << endl;
        return &b;
    }

注意:final关键字放在父类的位置,override关键字放在子类的位置。

6.抽象类

在虚函数的后面加上=0,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫做接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象。**只有重写虚函数,派生类才能实例化出对象。**注意虽然不能实例化出对象,但是可以定义指针。

抽象类的存在本质上来说就是希望我们在派生类中重写父类的虚函数。抽象类中的虚函数一般只声明,不实现,因为没有意义。我们可以搭配override来使用。

//将父类中写入纯虚函数,父类变成抽象类
class Person
{
public:
    virtual A* BuyTicket() =0//纯虚函数
    {
        A a;
        cout << "全价买票" << endl;
        return &a;
    }
    virtual ~Person()
   {
        cout << "~Person" << endl;
    }
};

此时子类必须只有重写虚函数才能定义对象。通常情况下现实中没有的事物,定义成抽象类会比较合适。

虽然我们不能使用抽象类来定义对象,但是我们可以使用抽象类来定义指针。

class Car
{
public:
    virtual void Drive() = 0
    {
        cout << " Car" << endl;
    }
};
class Benz :public Car
{
public:
    virtual void Drive()
    {
        cout << "Benz" << endl;
    }
    void f()
    {
        cout << "f()" << endl;
    }
};
int main()
{
    //Car* p = nullptr;
    //p->Drive();//程序会崩溃
    Car* a = new Benz;
    a->Drive();
}

我们可以使用父类指针去接收子类对象,同时调用函数。但是不能使用父类去创建对象。

7.总结

C++多态的目的在于当我们使用父类的指针或者引用去接收子类的对象后,接收不同的子类对象的父类指针或者引用调用的相同的函数产生的结果不同。

重点在于实现多态的几个条件:

一是用父类的指针或者引用来接收。

二是子类必须对父类的虚函数进行重写。

到此这篇关于一文搞懂C++多态的用法的文章就介绍到这了,更多相关C++ 多态内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 详解C++ 多态的两种形式(静态、动态)

    1.多态的概念与分类 多态(Polymorphisn)是面向对象程序设计(OOP)的一个重要特征.多态字面意思为多种状态.在面向对象语言中,一个接口,多种实现即为多态.C++中的多态性具体体现在编译和运行两个阶段.编译时多态是静态多态,在编译时就可以确定使用的接口.运行时多态是动态多态,具体引用的接口在运行时才能确定. 静态多态和动态多态的区别其实只是在什么时候将函数实现和函数调用关联起来,是在编译时期还是运行时期,即函数地址是早绑定还是晚绑定的.静态多态是指在编译期间就可以确定函数的调用地址,

  • C++ 超详细分析多态的原理与实现

    目录 多态的定义及实现 多态的构成条件 虚函数重写 C++11的override和final 抽象类 多态的原理 虚函数表 动态绑定与静态绑定 单继承和多继承关系的虚函数表 单继承中的虚函数表 多继承中的虚函数表 常见问题 多态的定义及实现 多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态. 比如买票这个行为,当普通人买票时,是全价买票:学生买票时,是半价买票:军人买票时是优先买票. 多态的构成条件 多态是在不同继承关系的类对象,去调用同一函数

  • C++中的多态详谈

    1. 多态概念 1.1 概念 多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态. 举个栗子:比如买票,当普通人买票时,是全价买票:学生买票时,是半价买票:军人买票时是优先买票.同一个事情针对不同的人或情况有不同的结果或形态. 2. 多态的定义及实现 2.1 多态的构成条件 多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为.比如Student继承了Person. Person对象买票全价,Student对象买票半价. 注意:那么在继

  • C++的多态与虚函数你了解吗

    目录 多态性 虚函数 总结 多态性 多态性是面向对象程序设计的关键技术之一,若程序设计语言不支持多态性,不能称为面向对象的语言,利用多态性技术,可以调用同一个函数名的函数,实现完全不同的功能 在C++中有两种多态性: 编译时的多态 通过函数的重载和运算符的重载来实现的 运行时的多态性 运行时的多态性是指在程序执行前,无法根据函数名和参数来确定该调用哪一个函数,必须在程序执行过程中,根据执行的具体情况来动态地确定:它是通过类继承关系public和虚函数来实现的,目的也是建立一种通用的程序:通用性是

  • C++多态实现方式详情

    注:文章转自公众号:Coder梁(ID:Coder_LT) 在我们之前介绍的继承的情况当中,派生类调用基类的方法都是不做任何改动的调用. 但有的时候会有一些特殊的情况,我们会希望同一个方法在不同的派生类当中的行为是不同的.举个简单的例子,比如speak方法,在不同的类当中的实现肯定是不同的.如果是Human类,就是正常的说话,如果是Dog类可能是汪汪,而Cat类则是喵喵. 在这种情况下只是简单地使用继承是无法满足我们的要求的,最好能够有一个机制可以让方法有多种形态,不同的对象去调用的逻辑不同.这

  • C++ 超全面讲解多态

    目录 多态的概念 多态的定义及实现 构成条件 虚函数 虚函数的重写 虚函数重写的两个例外 抽象类 抽象类的概念 接口继承和实现继承 多态的原理 虚函数表 多态的原理 多态的概念 概念:通俗的来说就是多种形态,具体就是去完成某个行为,当不同类型的对象去完成同一件事时,产生的动作是不一样的,结果也是不一样的. 举一个现实中的例子:买票这个行为,当普通人买票时是全价:学生是半价:军人是不需要排队. 多态也分为两种: 静态的多态:函数调用 动态的多态:父类指针或引用调用重写虚函数. 这里的静态是指在编译

  • 一文搞懂C++多态的用法

    目录 前言 1.多态的概念 2.C++中多态的分类 (1)静态多态 (2)动态多态 3.多态的构成条件 (1)举例 (2)两个概念 (3)多态的构成条件 4.虚函数重写的两个例外 (1)协变 5.final与override (1)final (2)override 6.抽象类 7.总结 前言 C++多态是在继承的基础上实现的,了解多态之前我们需要掌握一定的C++继承的知识,本文将介绍C++中多态的概念,构成条件以及用法. 1.多态的概念 多态,通俗来讲就是多种形态,具体点就是去完成某个行为,当

  • 一文搞懂Python的hasattr()、getattr()、setattr() 函数用法

    目录 hasattr() getattr() setattr() hasattr() hasattr() 函数用来判断某个类实例对象是否包含指定名称的属性或方法.该函数的语法格式如下: hasattr(obj, name) 其中 obj 指的是某个类的实例对象,name 表示指定的属性名或方法名,返回BOOL值,有name特性返回True, 否则返回False. 例子: class demo: def __init__ (self): self.name = "lily" def sa

  • 一文搞懂Vue3中的异步组件defineAsyncComponentAPI的用法

    目录 前言 传递工厂函数作为参数 传递对象类型作为参数 总结 前言 当我们的项目达到一定的规模时,对于某些组件来说,我们并不希望一开始全部加载,而是需要的时候进行加载:这样的做得目的可以很好的提高用户体验. 为了实现这个功能,Vue3中为我们提供了一个方法,即defineAsyncComponent,这个方法可以传递两种类型的参数,分别是函数类型和对象类型,接下来我们分别学习. 传递工厂函数作为参数 defineAsyncComponent方法接收一个工厂函数是它的基本用法,这个工厂函数必须返回

  • 一文搞懂JMeter engine中HashTree的配置问题

    目录 一.前言 二.HashTree的用法 三.JMeter源码导出jmx脚本文件介绍 四.自定义HashTree生成JMeter脚本 一.前言 之前介绍了JMeter engine启动原理,但是里面涉及到HashTree这个类结构没有给大家详细介绍,这边文章就详细介绍JMeter engine里面的HashTree结构具体用来做什么 大家看到下面是JMeter控制台配置截图,是一个标准的菜单形式:菜单形式其实就类似于"树型"的数据结构,而HashTree其实就是一个树型数据结构 我们

  • 一文搞懂如何避免JavaScript内存泄漏

    目录 一.什么是内存泄漏 二.常见的内存泄漏 1.意外的全局变量 2. 计时器 3. 闭包 4. 事件监听器 5.缓存 6.分离的DOM元素 三.识别内存泄漏 1.使用性能分析器可视化内存消耗 2. 识别分离的 DOM 节点 大家好,我是CUGGZ.SPA(单页应用程序)的兴起,促使我们更加关注与内存相关的 JavaScript 编码实践.如果应用使用的内存越来越多,就会严重影响性能,甚至导致浏览器的崩溃.下面就来看看JavaScript中常见的内存泄漏以及如何避免内存泄漏. 一.什么是内存泄漏

  • 一文搞懂Map与Set的用法和区别解析

    目录 前言 1.基本概念 1.1 Map(字典) 1.2 Set(集合) 2.基本使用 2.1 Map 基本使用 2.2 Set 基本使用 3.Map和Set区别 4.使用场景介绍 4.1 Set对象使用场景 4.2 Map对象使用场景 5.思考点 总结 前言 作为前端开发人员,我们最常用的一些数据结构就是 Object.Array 之类的,毕竟它们使用起来非常的方便.往往有些刚入门的同学都会忽视 Set 和 Map 这两种数据结构的存在,因为能用 set 和 map 实现的,基本上也可以使用对

  • 一文搞懂JavaScript中bind,apply,call的实现

    目录 bind.call和apply的用法 bind call&apply 实现bind 实现call和apply 总结 bind.call和apply都是Function原型链上面的方法,因此不管是使用function声明的函数,还是箭头函数都可以直接调用.这三个函数在使用时都可以改变this指向,本文就带你看看如何实现bind.call和apply. bind.call和apply的用法 bind bind()方法可以被函数对象调用,并返回一个新创建的函数. 语法: function.bin

  • 一文搞懂C++中继承的概念与使用

    目录 前言 继承概念及定义 继承概念 继承定义 继承方式 父类和子类对象赋值转换 继承中的作用域 派生类的默认成员函数 派生类的友元与静态成员 继承关系 单继承 多继承 菱形继承 前言 我们都知道面向对象语言的三大特点是:**封装,继承,多态.**之前在类和对象部分,我们提到了C++中的封装,那么今天呢,我们来学习一下C++中的继承. 继承概念及定义 继承概念 继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能

  • 一文搞懂Spring中@Autowired和@Resource的区别

    目录 1.来源不同 2.依赖查找顺序不同 2.1 @Autowired 查找顺序 2.2 @Resource 查找顺序 2.3 查找顺序小结 3.支持的参数不同 4.依赖注入的支持不同 5.编译器提示不同 总结 @Autowired 和 @Resource 都是 Spring/Spring Boot 项目中,用来进行依赖注入的注解.它们都提供了将依赖对象注入到当前对象的功能,但二者却有众多不同,并且这也是常见的面试题之一,所以我们今天就来盘它. @Autowired 和 @Resource 的区

  • 一文搞懂Golang中iota的用法和原理

    目录 前言 iota的使用 iota在const关键字出现时将被重置为0 按行计数 所有注释行和空行全部忽略 跳值占位 多个iota 一行多个iota 首行插队 中间插队 没有表达式的常量定义复用上一行的表达式 实现原理 iota定义 const 前言 我们知道iota是go语言的常量计数器,只能在常量的const表达式中使用,在const关键字出现的时将被重置为0,const中每新增一行常量声明iota值自增1(iota可以理解为const语句块中的行索引),使用iota可以简化常量的定义,但

随机推荐