C++ 关键字virtual的原理简略概要

        相信有不少同学们像我一样,上编程课听老师讲完语法后虽然会用,但心里还是堵着慌,为什么呢?经过我的思索发现,还是少了基础原理的了解!这样不仅容易忘,还不敢下手敲代码,属于死记硬背阶段,想越过这个坎,我想万事还得靠自己!对吃饭的家伙一知半解,这样怎么能行!

        这篇文章的思路是从结果推回原理,所以耐下心,从头每个概念都看完,最后才能对原理进行理解。(注:这篇文章需要对静态/动态绑定,指针数组,函数指针,重写等概念有基本了解)

1.什么是虚函数指针?

        类内声明了virtual的函数在编译时产生的一个指向该函数的函数指针,叫作虚函数指针。

        CS知识点:当你声明一个非虚函数时,编译器会在编译时确定要调用的函数,并将其直接编译为对应的函数调用指令,所以非虚函数不需要用表来调用函数,也就不会有函数指针。

 2.什么是虚函数表?

        虚函数表是一个函数指针数组,用于存放该类的虚函数指针。虚函数表是对类来说的,每个类都有自己的一个虚函数表。

3.实例对象怎么通过虚函数表调用虚函数?

        ①若一个类声明了虚函数,那么编译时会据此产生一个虚函数表,同时该类的对象在被创建时会各自产生一个指针(virtural pointer后文简称vptr),这个指针指向这个类的虚函数表。

        ②在调用某对象的虚函数时,对象先通过这个vptr,去该类的虚函数表上找对应函数指针,再进行函数的调用。

        由此窥见虚函数表的作用:是动态调用函数而非静态调用(其奥妙之处后文有说)

4.因为重写涉及派生类,我们先讲派生类和基类的虚函数表有什么区别:

  1. 派生类和基类各自都有一张表,但派生类的表中含有从基类继承过来的所有虚函数指针。例如:在基类表中如果虚函数表内有两个元素{[0],[1]}(虚函数表这个数组内有[0],[1]两个元素,就代表基类声明了两个虚函数,在表上元素的顺序取决于声明的先后),那么派生类的虚函数表就有{([0],[1]),[2],[3]...},以此递归派生类的派生类的虚函数表...
  2. 派生类的对象内也存在各自的vptr,指向的是派生类的虚函数表。在调用虚函数时也是索引类的虚函数表来调用。

好了,有了派生类和基类的虚函数表的概念,我们知道:

  1. 派生类和基类各自有一个表,表内元素的顺序是:基类和派生类的虚函数指针按声明的先后顺序进行排列。(当然啦,没有基类怎么会有子类呢,所以肯定是基类的元素在子类的之前啦)
  2. 对象调用虚函数就是通过vptr在表中索引正确函数指针并调用函数。

了解以上概念后,我们就可以开始讲函数重写的原理了:

        其实原理很简单,当我们在派生类中对基类某个方法进行重写:

class Base {

public:

    virtual void talk() {

        std::cout << "Base talk" << std::endl;

    }

};

class Derive : public Base {

public:

    void talk() override {

        std::cout << "Derive talk" << std::endl;

    }

};

        编译时,派生类的虚函数表{[0]}中的元素[0],就不再是继承自基类的元素[0],而是被重写后的函数的虚函数指针覆盖,从而指向了重写后的函数,所以虽然派生类的虚函数表内还是只有一个成员[0],但其指向的函数已经是重写后的函数了。

此时我们创建一个派生类对象,并调用这个对象的重写后的虚函数:

        编译器先通过vptr在表中查找talk这个方法,发现talk是成员[0],然后从成员[0]提取指针,再进入指针指向的函数,然后我们就发现这个函数就是我们重写后的函数,至此,virtual的原理很清楚了:先通过vptr在虚函数表中索引出对应的函数指针,再调用该函数指针所指的函数,以此实现动态调用的效果(指针的移动是动态的),也体现了多态的核心思想:一名多用,降低成本!

        现在我们回到提到过的问题,为什么强调虚函数表的奥妙是动态调用呢?奥妙就在:当我们通过vptr在虚函数表中做动态操作以查找对应函数时,就已经避免了静态调用的同名识别问题,从而准确调用派生类的函数,也由此避免了析构不全,构造重叠等等的问题,更创造了多态中的一种新形态。你看这天才般的想法就知道,大佬们的头秃也是有回报的啊!

        但是也有一点,因为虚函数需要程序动态调用,虚函数表也需要内存来存,所以显而易见虚函数不可避免地对性能有影响,所以也不能滥用virtual啊同学们(虽然影响小,但蚊子腿也是肉)

关于virtual的应用原理拓展:

        其实所有有关virtual关键字的都是这么运作的:虚继承,重写,虚构造,虚析构...都是建立在虚函数表的基础上,比如虚继承,虚继承是怎么避免多次构造基类的呢?没错,就是把派生类的虚函数表中的基类构造函数的虚函数指针一层一层地覆盖,最后只留下一份,比如A虚继承自B和C,但B和C虚继承自D这样一个菱形继承,此时A的虚函数表里有什么?没错,只有B,C,D这三个集合的并集而不会是B+C+D,原因就在我上面说的指针覆盖上。以此类推,virtual的核心不难发现,就是一个函数指针数组(虚函数表)+一个函数指针数组的指针(vptr)的配合使用,数组+指针谁还不会啊!

  • 21
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值