我的编程空间,编程开发者的网络收藏夹
学习永远不晚

C++超全面讲解多态

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

C++超全面讲解多态

多态的概念

概念:通俗的来说就是多种形态,具体就是去完成某个行为,当不同类型的对象去完成同一件事时,产生的动作是不一样的,结果也是不一样的。

举一个现实中的例子:买票这个行为,当普通人买票时是全价;学生是半价;军人是不需要排队。

多态也分为两种:

  • 静态的多态:函数调用
  • 动态的多态:父类指针或引用调用重写虚函数。

这里的静态是指在编译时实现多态的,而动态是在运行时完成的。

多态的定义及实现

构成条件

多态一定是建立在继承上的,那么除了继承还要两个条件:

  • 必须通过基类(父类)的指针或引用调用函数
  • 被调用的函数必须是虚函数,且派生类(子类)必须对积累的虚函数进行重写。

虚函数

概念:被virtual修饰的类成员函数称为虚函数

class Person
{
public:
    virtual void BuyTicket()
    {
        cout<<"全价票"<<endl;
    }
};

注意:

  • 只有类的非静态成员函数可以是虚函数
  • 虚函数这里virtual和虚继承中用的是同一个关键字,但是他们之间没有关系;虚函数这里是为了实现多态;虚继承是为了解决菱形继承的数据冗余和二义性,它们没有关联

虚函数的重写

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

例:

class Person
{
public:
    virtual void BuyTicket()
    {
        cout<<"全价票"<<endl;
    }
};
​
class Student :public Person
{
public:
    //子类的虚函数重写了父类的虚函数
    virtual void BuyTicket()
    {
        cout<<"半价票"<<endl;
    }
};
​
class Soldier : public Person
{
public:
    //子类的虚函数重写了父类的虚函数
    virtual void BuyTicket()
    {
        cout<<"优先买票"<<endl;
    }
};
//多态的实现
void f(Person& p)//这块的参数必须是引用或者指针
{
    p.BuyTicket();
}
​
int main()
{
    Person p;
    Student st;
    Soldier so;
    
    f(p);
    f(st);
    f(so);
    
    return 0;
}

注意:这里子函数的虚函数可以不加virtual,也算完成了重写,但是父类的虚函数必须要加,因为子类是先继承父类的虚函数,继承下来后就有了virtual属性了,子类只是重写这个virtual函数;除了这个原因之外,还有一个原因,如果父类的析构函数加了virtual,子类加不加都一定完成了重写,就保证了delete时一定能实现多态的正确调用析构函数。

虚函数重写的两个例外

1、协变

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

例:

class A{};
class B : public A{};
​
class Person
{
public:
    virtual A* f()
    {
        return new A;
    }
};
​
class Student : public Person
{
public:
    virtual B* f()           //返回值不同但是构成虚函数重写
    {
        return new B;
    }
};

2、析构函数的重写

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

例:

class Person {
public:
    //建议把父类析构函数定义为虚函数,这样方便子类的虚函数重写父类的虚函数
    virtual ~Person() {cout << "~Person()" << endl;}
};
​
class Student : public Person {
public:
    virtual ~Student() { cout << "~Student()" << endl; }
};
// 只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用析构函数,才能构成多态,才能保证p1和p2指向的对象正确的调用析构函数。
int main()
{
    Person* p1 = new Person;
   //这里p2指向的子类对象,应该调用子类析构函数,如果没有调用的话,就可能内存泄漏
    Person* p2 = new Student;
    //多态行为
    delete p1;
    delete p2;
    //只有析构函数重写了那么这里delete父类指针调用析构函数才能实现多态。
    return 0;
}

C++11 override和finel

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

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

class Car
{
public:
    virtual void Drive() final {}
};
class Benz :public Car
{
public:
    //会在这块报错,因为基类的虚函数已经被final修饰,不能被重写了
    virtual void Drive() {cout << "Benz-舒适" << endl;}
};  

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

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

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

抽象类

抽象类的概念

纯虚函数:在虚函数的后面加上=0就是纯虚函数,有纯虚函数的类就是抽象类,也叫接口类,抽象类无法实例化对象。抽象类的子类不重写父类的虚函数的话,也是一个抽象类。

//抽象类的定义
class Car
{
public:
    virtual void run()=0;   //不用实现只写接口就行。   
};

纯虚函数不写函数体,并不意味着不能实现,只是我们不写。因为写出来也没有人用。

虚函数的作用

  • 强制子类重写虚函数,完成多态。
  • 表示抽象类。

接口继承和实现继承

普通函数的继承就是实现继承,虚函数的继承就是接口继承。子类继承了函数的实现,可以直接使用。虚函数重写后只会继承接口,重写实现。所以如果不用多态,就不要把函数写为虚函数。

纯虚函数就体现了接口函数。下面我们来实现一道题,展现一下接口继承。

class A
{
public:
    virtual void fun(int val=0) 
    {
        cout<<"A->val = "<<val <<endl;
    }
    void Fun()
    {
        fun();
    }
};
​
class B:public A
{
public:
    virtual void fun(int val=1)
    {
        cout<<"B->val"<<val<<endl;
    }
};
​
int main()
{
    B b;
    A* a=&b;
    a->Fun();
    return 0;
}

结果打印为 :B->val=0

子类对象切片给父类指针,传给Fun函数,满足多态,会去调用子类的fun函数,但是子类的虚函数继承了父类的接口,所以val是父类的0。

多态的原理

虚函数表

class A
{
public:
    virtual void fun()
    {
        
    }
    protected:
    int _a;
};

sizeof(A)是多少?

打印出来是8。

我们定义了一个A类型的对象a,打开调试窗口,发现a的内容如下

我们发现出了成员变量_a以外,还多了一个指针,这个指针是不准确的,实际上应该是 _vftptr(virtual function table pointer),虚函数表指针。在计算类大小的时候要加上这个指针的大小。虚表就是存放虚函数的地址地方,当我们去调用虚函数,编译器就会通过虚表指针去虚表里查找。

class A
{
public:
    void fun1()
    {
        
    }
    virtual void fun2()
    {}
};
​
int main()
{
    A* a=nullptr;
    a->fun1();//调用函数,因为这是普通函数的调用
    a->fun2();//调用失败,虚函数需要对指针操作,无法操作空指针。
    return 0;
}

实现一个继承

class A
{
    public:
    virtual void fun1()
    {}
    virtual void fun2()
    {}
};
class B : public A
{
    public:
    virtual void fun1()
    {}
    virtual void fun2()
    {}
};
​
int main()
{
    A a;
    B b;
    return 0;
}

子类与父类一样有一个虚表指针。

子类的虚函数表一部分继承自父类。如果重写了虚函数,那么子类的虚函数会在虚表上覆盖父类的虚函数。

本质上虚函数表是一个虚函数指针数组,最后一个元素是nullptr,代表虚表的结束。所以,如果继承了虚函数,那么

  • 子类先拷贝一份父类虚表,然后用一个虚表指针指向这个虚表。
  • 如果有虚函数重写,那么在子类的虚表上用子类的虚函数覆盖。
  • 子类新增的虚函数按其在子类中的声明次序增加到子类虚表的最后。

虚函数表放在内存的那个区,虚函数又放在哪?

虚函数与虚函数表都放在代码段。

多态的原理

我们现在来看多态的原理

class person
{
public:
    virtual void fun()
    {
        cout<<"全价票"<<endl;
    }
};
class student : public person
{
public:
    virtual void fun()
    {
        cout<<"半价票"<<endl;
    }
};
void buyticket(person* p)
{
    p->fun();
}

这样就实现了不同对象去调用同一函数,展现出不同的形态。 满足多态的函数调用是程序运行是去对象的虚表查找的,而虚表是在编译时确定的。 普通函数的调用是编译时就确定的。

动态绑定与静态绑定

1.静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载

2.动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。我们说的多态一般是指动态多态。

这里我附上一个有意思的问题:

就是在子类已经覆盖了父类的虚函数的情况下,为什么子类还是可以调用“被覆盖”的父类的虚函数呢?

#include <iostream>
using namespace std;
​
class Base {
public:
    virtual void func() {
        cout << "Base func\n";
    }
};
​
class Son : public Base {
public:
    void func() {
        Base::func();
        cout << "Son func\n";
    }
};
​
int main()
{
    Son b;
    b.func();
    return 0;
}

输出:

Base func

Son func

这是C++提供的一个回避虚函数的机制

通过加作用域(正如你所尝试的),使得函数在编译时就绑定。

到此这篇关于C++ 超全面讲解多态的文章就介绍到这了,更多相关C++ 多态内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

C++超全面讲解多态

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

C++BoostMetaStateMachine定义状态机超详细讲解

Boost是为C++语言标准库提供扩展的一些C++程序库的总称。Boost库是一个可移植、提供源代码的C++库,作为标准库的后备,是C++标准化进程的开发引擎之一,是为C++语言标准库提供扩展的一些C++程序库的总称
2022-12-08

JavaScript防抖与节流超详细全面讲解

在开发中我们经常会遇到一些高频操作,比如:鼠标移动,滑动窗口,键盘输入等等,节流和防抖就是对此类事件进行优化,降低触发的频率,以达到提高性能的目的。本文就教大家如何实现一个让面试官拍大腿的防抖节流函数,需要的可以参考一下
2022-11-13

C++BoostUtility超详细讲解

Boost是为C++语言标准库提供扩展的一些C++程序库的总称。Boost库是一个可移植、提供源代码的C++库,作为标准库的后备,是C++标准化进程的开发引擎之一,是为C++语言标准库提供扩展的一些C++程序库的总称
2022-12-08

C++BoostUuid超详细讲解

Boost是为C++语言标准库提供扩展的一些C++程序库的总称。Boost库是一个可移植、提供源代码的C++库,作为标准库的后备,是C++标准化进程的开发引擎之一,是为C++语言标准库提供扩展的一些C++程序库的总称
2022-12-08

全面讲解RAID

RAID,为RedundantArraysofIndependentDisks的简称,中文为廉价冗余磁盘阵列。RAID磁盘阵列简单的解释,就是将多台硬盘透过RAIDController(分Hardware,Software)结合成虚拟单台大容量的硬盘使用。编程学习网教育
全面讲解RAID
2024-04-23

Mybatis动态sql超详细讲解

动态SQL是MyBatis的强大特性之一,顾名思义就是会动的SQL,即是能够灵活的根据某种条件拼接出完整的SQL语句,下面这篇文章主要给大家介绍了关于Mybatis动态sql的相关资料,需要的朋友可以参考下
2023-05-17

Java依赖注入容器超详细全面讲解

依赖注入(DependencyInjection)和控制反转(InversionofControl)是同一个概念。具体含义是:当某个角色(可能是一个Java实例,调用者)需要另一个角色(另一个Java实例,被调用者)的协助时,在传统的程序设计过程中,通常由调用者来创建被调用者的实例
2023-01-12

Android全面屏适配与判断超详细讲解

这篇文章主要介绍了Android全面屏适配及判断是否为全面屏,全面屏手势和虚拟导航栏的判断,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习吧
2023-01-28

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录