h4ck1nH4ck1n  2022-04-05 04:06 字节时代 隐藏边栏 |   抢沙发  356 
文章评分 1 次,平均分 5.0

虚函数

  • 在类的定义中,前面有virtual关键字的成员函数称为虚函数。
  • virtual关键字只用在类定义里的函数声明中,函数体定义时不用。
class Base 
{
    virtual int Fun() ; // 函数声明的时需virtual关键字
};

int Base::Fun() // 函数体定义时无需virtual关键字
{ }

多态

多态性指相同对象收到不同消息或不同对象收到相同消息时产生不同的实现动作。
C++支持两种多态性:编译时多态性,运行时多态性。

  • 编译时多态性(静态多态):通过重载函数实现,先期联编 early binding。
  • 运行时多态性(动态多态):通过虚函数实现,滞后联编 late binding。

多态与非多态的实质区别就是函数地址是早绑定还是晚绑定。如果函数的调用,在编译器编译期间就可以确定函数的调用地址,并生产代码,是静态的,就是说地址是早绑定的。而如果函数调用的地址不能在编译器期间确定,需要在运行时才确定,这就属于晚绑定。

  • 在构造函数和析构函数中调用虚函数,不是多态。
  • 在非构造函数和非析构函数的成员函数中调用虚函数,是多态。

运行时多态性

C++运行时多态性是通过虚函数来实现的,虚函数允许子类重新定义成员函数,而子类重新定义父类的做法称为覆盖(Override),或者称为重写。

多态的目的

封装可以使得代码模块化,继承可以扩展已存在的代码,他们的目的都是为了代码重用。而多态的目的则是为了“接口重用”。也即,不论传递过来的究竟是类的哪个对象,函数都能够通过同一个接口调用到适应各自对象的实现方法。

多态实现的条件

  • 类之间必须有继承关系。
  • 子类重写了父类的虚函数。
  • 使用父类指针或引用去调用子类对象的虚函数。

多态的表现形式一

  • 派生类的指针可以赋给基类指针
  • 通过基类指针调用基类和派生类中的同名虚函数时。
  1. 若该指针指向一个基类的对象,那么被调用是基类的虚函数。
  2. 若该指针指向一个派生类的对象,那么被调用的是派生类的虚函数。

这种机制就叫做“多态”,说白点就是调用哪个虚函数,取决于指针实际指向哪种类型的对象。

多态的表现形式二

  • 派生类的对象可以赋给基类引用
  • 通过基类引用调用基类和派生类中的同名虚函数时。
  1. 若该引用的是一个基类的对象,那么被调用是基类的虚函数;
  2. 若该引用的是一个派生类的对象,那么被调用的是派生类的虚函数。

这种机制也叫做“多态”,说白点就是调用哪个虚函数,取决于引用的对象实际是哪种类型。

多态的实现原理

虚函数表和虚表指针

当类中声明虚函数时,编译器会在类中生成一个虚函数表,虚函数表是一个存储类成员函数指针的数据结构,虚函数表是由编译器自动生成与维护的,virtual成员函数会被编译器放入虚函数表中,存在虚函数时,每个对象中都有一个指向虚函数表的指针(vptr指针)。

编译器为每个类的对象提供一个虚表指针vptr,这个指针指向对象所属类的虚函数表。在程序运行时,根据对象的类型去初始化vptr,从而让vptr正确的指向所属类的虚表,从而在调用虚函数时,就能够找到正确的函数。

由于每个对象调用的虚函数都是通过虚表指针来索引的,也就决定了虚表指针的正确初始化是非常重要的。换句话说,在虚表指针没有正确初始化之前,我们不能够去调用虚函数。

  1. 如果基类有虚函数,所有继承了该基类的子类都有虚表。
  2. 如果子类没有重写虚函数,子类虚表中仍然会有所有基类虚函数的地址,这些地址指向的是基类的虚函数实现。
  3. 如果子类重写了相应的虚函数,那么虚表中的地址就会改变,指向自身的虚函数实现,如果派生类有自己的虚函数,那么虚表中就会添加该项。
  4. 派生类的虚表中虚函数地址的排列顺序和基类的虚表中虚函数地址排列顺序相同。

虚函数表和虚表指针的初始化

C++是在构造函数中进行虚表的创建和虚表指针的初始化。

在构造子类对象时,要先调用父类的构造函数,此时编译器只“看到了”父类,并不知道后面是否后还有继承者,它初始化父类对象的虚表指针vptr,该虚表指针指向父类的虚表。当执行子类的构造函数时,子类对象的虚表指针vptr被初始化, 此时vptr指向自身的虚表。因此,只有当对象的构造完全结束后vptr的指向才最终确定,到底是父类对象的vptr指向父类虚函数表还是子类对象的vptr指向子类虚函数表。

虚函数的缺点

通过虚函数表指针vptr调用重写函数是在程序运行时进行的,因此需要通过寻址操作才能确定真正应该调用的函数。而普通成员函数是在编译时就确定了调用的函数。在效率上,虚函数的效率要低很多。出于效率考虑,没有必要将所有成员函数都声明为虚函数。

虚析构函数

析构函数是在删除对象或退出程序的时候,自动调用的函数,其目的是做一些资源释放。

那么在多态的情景下,通过基类的指针删除派生类对象时,通常情况下只调用基类的析构函数,这就会存在派生类对象的析构函数没有调用到,存在资源泄露的情况。

一个类打算作为基类使用,应该将析构函数定义成虚函数。

纯虚函数和抽象类

纯虚函数

没有函数体的虚函数

class A 
{

public:
    virtual void Print( ) = 0 ; //纯虚函数
private: 
    int a;
};

抽象类

包含纯虚函数的类叫抽象类

  • 抽象类只能作为基类来派生新类使用,不能创建抽象类的对象。
  • 抽象类的指针和引用可以指向由抽象类派生出来的类的对象。

虚函数和纯虚函数的区别

  • 虚函数中的函数是实现的哪怕是空实现,它的作用是这个函数在子类里面可以被重载,运行时动态绑定实现动态。
  • 纯虚函数是个接口,是个函数声明,在基类中不实现,要等到子类中去实现。
  • 虚函数在子类里可以不重载,但是纯虚函数必须在子类里去实现。

虚函数的适用范围

  1. 只有类的成员函数才能声明为虚函数,虚函数仅适用于有继承关系的类对象。普通函数不能声明为虚函数。
  2. 静态成员函数不能是虚函数,因为静态成员函数不受限于某个对象。
  3. 内联函数(inline)不能是虚函数,因为内联函数不能在运行中动态确定位置。
  4. 构造函数不能是虚函数。
  5. 析构函数可以是虚函数,而且建议声明为虚函数。

参考

本文为原创文章,版权归所有,欢迎分享本文,转载请保留出处!

h4ck1n
H4ck1n 关注:0    粉丝:0 最后编辑于:2022-04-10
这个人很懒,什么都没写

发表评论

表情 格式 链接 私密 签到

扫一扫二维码分享