【C++】动态多态与虚函数

本文详细介绍了C++中的动态多态,重点讲解了虚函数的概念、特点以及其实现原理。通过实例展示了虚函数如何使得父类指针能够调用子类的重写函数,实现了多态。此外,还探讨了虚析构函数在资源释放中的关键作用,特别是解决多态中可能的内存泄漏问题。最后,阐述了纯虚函数和抽象类的应用,强调它们在定义接口和禁止实例化方面的功能。
摘要由CSDN通过智能技术生成

一、动态多态概念

多态是函数的不同实现形式。
多态是面向对象的泛型编程的一种方式,所谓的泛型编程就是试图使用不变的代码来实现可变的算法。
我们现在的多态是一种基于继承关系 + 虚函数而存在的多态。 它也被称之动态多态,也被称之为绑定多态

实现多态的必要条件:

  1. 要有继承关系
  2. 父类中要有虚函数
  3. 子类重写父类的虚函数
  4. 父类指针或引用指向子类对象

二、虚函数

2.1 虚函数的概念

在类中使用virtual关键字修饰的成员函数被成为虚函数。

2.2 虚函数的特点

虚函数具有虚属性
所谓虚属性就是这个函数可以在子类中进行重写(override)

重写:即在子类中定义和父类原型相同的函数。
原型相同:返回值 函数名 形参列表 const属性 都要相同,函数体可以不同。

重写之后,当使用父类指针指向子类对象的时,如果使用父类指针调用这个虚函数时,则将执行的是子类重写的逻辑。这种方式也被成为动态多态。

C++11新特性:override作用是检验这个子类中同名函数的返回值和形参列表是否与父类之中同名的虚函数的返回值和形参列表相同,如果不相同则直接报错。
是一种安全检查机制。

虚函数函数体:

virtual + 类中成员函数    //这个函数就是一个虚函数
{
    //虚函数的函数体。
}

代码示例:是否加virtual进行对比

#include <iostream>

using namespace std;
class person
{
public:
    void action()
    {
        cout << "正在写博客" << endl;
    }
};
class stu:public person//继承
{
public:
    void action()//继承有两个同名函数,父类的同名函数就应该被隐藏
    {
        cout << "正在学习C++" << endl;
    }
};

int main()
{
    person *p=new stu();
    p->action();
    return 0;
}
#include <iostream>

using namespace std;
class person
{
public:
    virtual void action()//父类加了virtual修饰后,成员函数成为虚函数,如果在子类之中出现同名函数将会被重写
    {
        cout << "正在写博客" << endl;
    }
};
class stu:public person//继承
{
public:
    void action()override//C++11新特性,
    //override:检验这个子类中同名函数的返回值和形参列表是否与父类之中同名的虚函数的返回值和形参列表相同,
    //如果不相同则直接报错。安全检查机制。
    {
        cout << "正在学习C++" << endl;
    }
};

int main()
{
    person *p=new stu();
    p->action();//继承有两个同名函数,父类的同名函数就应该被隐藏
    return 0;
}

结果展示:
在这里插入图片描述
在这里插入图片描述
为什么第一个代码结果是父类的action呢?
因为当继承有两个同名函数时,通过父类的指针或引用只能访问父类的函数,父类的同名函数就应该被隐藏。

总结:
对比代码结果可知当父类加了virtual修饰后,成员函数成为虚函数,如果在子类之中出现同名函数将会被重写。如果不加virtual修饰,那么通过父类的指针或引用只能访问父类的函数 。

函数重载和函数重写有什么区别?
函数重载:要求函数名相同,形参列表必须不同。
函数重写:要求函数原型必须相同,且函数重写只能发生在父子类之间。

三、动态多态实现的背后编译器帮我做了什么?

用上述代码求出在父类void action()不加virtual时和加上时的sizeof()大小为多少。

cout << sizeof(person) <<endl;
cout << sizeof(stu) <<endl;

不加virtual时都为1;
virtual时都为8;

由此可知:加virtual时编译器为我们默默安插了一根虚指针。子类继承时也拥有了这个虚指针。
这跟虚指针指向了这个类中编译器在他的rodate段生成一个虚函数表,虚函数表中就保存了这个virtual修饰的这个虚函数的地址。

重写前:
在这里插入图片描述
重写后:
在这里插入图片描述
当子类有同名函数对父类中的同名函数进行重写时,在子类的虚表中产生了一个新的函数地址,这个新的函数地址就把原来的父类中的函数地址给覆盖掉了。
所以执行的是stu的逻辑。
总结:编译器帮我们做什么好事?

  1. 好事第一步《虚函数表指针的生成时机及初始化》:当在类中使用virtual声明一个成员函数为虚函数时,在编译时,编译器会自动在基类中默默地安插一个虚函数表指针,同时.rodata段为这个类生成一张虚函数表,用来保存类中的虚函数的地址。
  2. 好事第二步:当继承发生时,父类中的虚指针和虚表就被子类给继承了下来但是虚指针还是指向的父类中的虚表,所以他的类对象空间就增大了一个指针的大小。这就验证了刚才我们的实例演示。
  3. 好事第三步《虚指针的赋值时机》:当子类构造对象时,这根继承而来的虚指针,将会在子类的构造函数中被再次赋值,所赋的值即为子类类中产生的虚函数表的地址。子类虚表中的原来的那个同名函数的地址就会被子类重写这个地址所覆盖。
  4. 好事第四步:当使用父类指针或引用,对虚函数进行调用时,通过这个虚函数表指针,在虚函数表中查找虚函数的地址,从而调用不同类的虚函数。故此形成多态。
    在这里插入图片描述

让我们在看看底层逻辑
底层逻辑:
没有加virtual时:
在这里插入图片描述
加了virtual时:
在这里插入图片描述
这就是动态多态实现的原因,即加了virtual后,定义重名函数被重写覆盖之后放到了寄存器上,call指令是call的寄存器,即重写函数。

虚函数表的结构:
在这里插入图片描述
对上图的解释:
在这里插入图片描述

四、虚析构

构造函数,拷贝构造函数,拷贝赋值函数等都不能是虚函数。
但是析构函数可以是虚函数,称之为虚析构函数。

多态在使用时会遇到巨大的问题,我们采用的解决方案就是虚析构。
虚析构的作用:指引delete关键字,正确释放空间。

4.1 解决多态中资源释放的问题

C++的语法规定:当父类的析构函数为虚析构时,那么子类的析构将是对父类析构的重写。
如果使用多态,请务必把最远端父类中的析构函数设定为虚析构,这样可以避免子类对象的内存泄漏问题。

代码示例:

#include <iostream>
using namespace std;
class A
{
public:
    A()
    {
        cout << "A的构造" << endl;
    }
    virtual ~A()//在析构函数前加virtual进行修饰,此时这个析构就是一个虚析构。
    {
        cout << "A的析构" << endl;
    }
    virtual void show()
    {
        cout << "学习C++" << endl;
    }
};
class B: public A
{
private:
    int *p;
public:
    B()
    {
        cout << "B的构造" << endl;
        p=new int[20];
    }
    ~B()//在析构函数前加virtual进行修饰,此时这个析构就是一个虚析构。
    {
        cout << "B的析构" << endl;
        delete []p;
    }
    void show()override
    {
        cout << "我是野猫徐" << endl;
    }
};
int main()
{
    A *p=new B;
    p->show();
    delete p;
    return 0;
}

结果展示:

不加虚析构
在这里插入图片描述

加了虚析构
在这里插入图片描述

如果析构函数不是虚函数,此处的delete p只会调用父类的析构函数会造成子类的指针成员泄漏。
如果析构函数是虚函数,此处的 delete p 就会先调用子类的析构,然后调用父类的析构。

4.2 解决多重继承中的资源释放的问题

代码示例:

#include <iostream>
using namespace std;
class A
{
public:
    A()
    {
        cout << "A的构造" << endl;
    }
    virtual ~A()
    {
        cout << "A的析构" << endl;
    }
};
class B
{
public:
    B()
    {
        cout << "B的构造" << endl;
    }
    virtual ~B()
    {
        cout << "B的析构" << endl;
    }
};
class C : public A, public B
{
public:
    C()
    {
        cout << "C的构造" << endl;
    }
    ~C()
    {
        cout << "C的析构" << endl;
    }
};
int main()
{
    //A* a = new C;
    B* b = new C;
    delete b;
    return 0;
}

结果展示:

不使用虚析构
在这里插入图片描述
使用虚析构
在这里插入图片描述

五、纯虚函数和抽象类

5.1 纯虚函数

5.1.1 纯虚函数定义

类中的成员函数由virtual进行修饰且没有函数体的函数(只有声明,没有定义),就称之为纯虚函数,也被称之为接口函数。

5.1.2 纯虚函数的语法形式

在虚函数的基础上,将{}和函数体用 =0 替换,那么该虚函数就是一个纯虚函数了。

class 类名
{
    virtual 返回值  函数名 (形参列表)  =  0;  //这种语法形式就是纯虚函数。
}

5.1.3 纯虚函数的意义

有些场景下,我们想使用的是子类的对象,而基类本身实例化出来的对象是没有意义的。
只是做为顶级父类中的一种功能描述,而没有具体的实现,这个函数实现是要在子类之中完成的。
例如:动物类派生了 狮子、老虎、长颈鹿、企鹅、大象…等,我们想用的是老虎、狮子等,而动物类本身实例化出来的对象是没有用的。

5.1.4 使用纯虚函数的要求

由于父类中没有纯虚函数的定义,只有声明,所以要求子类中必须重写父类的纯虚函数

5.2 抽象类

类中有纯虚函数的类,就叫抽象类,这种类一般做为顶级父类使类使用。这种类也称为接口类。
抽象类必须继承,且抽象类中纯虚函数必须重写。如果子类没有重写父类的纯虚函数,那么该子类也即成为抽象类。
抽象类不能定义实例,因为因为其成员函数不完整(没有函数体)。只是做为一个最远端父类的指针或引用,来指向或引用子类实例 。

代码示例

#include <iostream>
using namespace std;
class A
{
public:
    A()
    {
        cout << "A的构造" << endl;
    }
    virtual~A()
    {
        cout << "A的析构" << endl;
    }
    virtual void show()=0;
};
class B:public A
{
public:
    B()
    {
        cout << "B的构造" << endl;
    }
    ~B()
    {
        cout << "B的析构" << endl;
    }
    void show() override
    {
        cout << "正在学习C++" << endl;
    }
};
int main()
{
    A* a = new B;
    a->show();
    delete a;
    return 0;
}

结果展示:
在这里插入图片描述
总结:

  1. 抽象类不允许实例化对象;
  2. 可以定义抽象类的指针或者引用,来指向子类对象;
评论 39
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

夜猫徐

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值