一篇文章带你看懂多态的本质

本文探讨了多态的本质,指出通过派生类隐藏基类函数虽然能实现不同对象结果,但与多态本质不同。重点讲解了虚函数表在多态中的作用,以及多态调用与普通函数调用的差异,包括虚函数的隐藏、覆盖、final和override关键字的使用。
摘要由CSDN通过智能技术生成

一篇文章带你看懂多态的本质

解释前文留下的问题:

假如我并没有选择多态,仅仅是通过派生类把从基类继承来的成员函数通过重定义隐藏掉,再去调用不是也能实现不同对象不同结果的效果吗? 这与多态有什么区别? 这样写存在什么问题?

先引入一下前文的一些例子:
去医院挂号,我去的话需要老老实实排队,但是军人会有专属优先窗口.再比如我们买票,成人票的话就是全款,但是我可以买学生票就会便宜很多.上面的两个例子中,挂号这个行为我和军人就是不同的对象,去调用时出现了不同的结果,同样的买票的行为,成人和学生不同的对象去调用也产生了不同的结果.

  我们可以分析一下上面提到的三个例子.挂号,我是人,军人也是人.买票,人就是人,我是学生也是人.我们可以发现,这里都存在一个继承关系,军人是特殊的人,学生是特殊的人.我们再看一遍之前的代码.

class Person{};

class Student{};

int main()
{
    Person p;
    Student s;
    //BuyTicket(p);//这样写其实是面向过程了,刚才是为了方便理解
    //BuyTicket(s);//所以这样写的,其实我们应该写成下面的形式.
    p.BuyTicket();
    s.BuyTicket();
}

  先把代码改成面向对象之后,我们观察出来这其实不就是在两个类里面分别封装了一个BuyTicket()函数嘛,但是我们前面文章讲过,派生类如果存在与基类同名的成员会构成隐藏关系.那如果是这样的话,那我完全可以把基类的函数隐藏掉,然后重定义一个同名函数不久可以实现相同的效果吗?代码示例:

class Person
{
public:
    void BuyTicket(){cout<<"Person"<<endl;}
};

class Student : public Person
{
public:
    void BuyTicket(){cout<<"Student"<<endl;}
};
int main()
{
    Person p;
    Student s;
    p.BuyTicket();
    s.BuyTicket();
}

  这样不是一样的吗?事实上这样大错特错,因为这根本就是两个不同的行为,只是名字一样罢了.
还记得我们在继承一文中讲过静态成员变量实际上是一种接口继承,那我们可以仿照这个观念,我们为成员函数加上virtual关键字为其附上虚属性,这样成员函数在继承的时候就从实现继承变为了接口继承.还记得我们上一篇文章讲使用时提到的派生类虚函数重写可以不加virtual关键字吗?原因就在这里,因为继承本身就是接口继承,继承下来的只是接口不存在实现一说,所以对虚函数的继承之后的函数天然地具备虚的属性,即便不加virtual关键字.
这里我引入一张图片来帮助理解隐藏和多态:
在这里插入图片描述

很明显可以看出来他们本质区别在于调用了不同的接口.我们可以发现,多态底层实现的重点就在于如何使用一个接口实现两种调用.

多态如何实现

虚函数表

  我们先来看看虚函数在类中是怎么存储的,这里我先定义一个类然后再通过sizeof()来看占用多少字节.

class Base
{
public:
    virtual void A(){cout<<"Base::A()"<<endl;}
    virtual void B(){cout<<"Base::B()"<<endl;}
    void C(){cout<<"Base::C()"<<endl;}
};

class Derive : public Base
{
public:
    virtual void A(){cout<<"Derive::A()"<<endl;}
};

int main()
{
    Base b;
    Derive d;
    cout<<sizeof(b)<<endl;
    cout<<sizeof(d)<<endl;
    return 0;
}

这段代码在64位平台g++编译器下的的结果是8,8.我们可以推断出这两个类中各有一个指针,事实上也确实如此,这个指针叫做虚函数表指针,顾名思义,这个指针指向一个表,表中存的是虚函数,这个表通常以nullptr结尾.
虚函数本身是和普通函数一样是存在进程地址空间的代码段的,所以严格地说虚函数表里面并不存在真正意义上的虚函数,而是虚函数的地址.

从代码编译和运行阶段分析

  我们知道了虚函数的地址存在虚函数表里,虚函数表的地址存在类里.通过会汇编之后我们会发现在多态调用中会存在多个jump指令,才会寻址到我们代码段的函数地址,所以多态调用在编译时是不确定的,只有当程序运行起来之后才能取到函数地址,而普通函数调用是确定的,编译时就能确定调用的函数地址.
  这也就是为什么我们在写多态时要满足基类指针或引用来调用,因为多态调用的本质是要去虚函数表寻址,而虚函数表是会被派生类继承下来的,当我们完成了虚函数的重写(原理层叫覆盖),程序运行起来之后就会去寻到不同的地址,进而出现不同的结果.

tips:

  1. 在多继承时,派生类中未完成重写的虚函数(这么说有点拗口,其实就是单独定义的虚函数),会存在第一个虚表中.如果是菱形继承那么会存在代码冗余且结果很可能不符合预期,所以我们尽可能避免写出菱形继承.
  2. 有时候我们会不想让每个类的虚函数被重写,那么我们可以在函数定义后面加上final关键字,这样这个函数就不允许再被重写了.
    以我们尽可能避免写出菱形继承.
  3. 有时候我们会不想让每个类的虚函数被重写,那么我们可以在函数定义后面加上final关键字,这样这个函数就不允许再被重写了.
  4. 与第二点对应的,有时候我们又想知道我们这个虚函数是否完成了重写,那么我们可以用override关键字,同样写在函数定义后面,如果没有完成重写则编译报错.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值