电子说
虚函数作为C++的重要特性,让人又爱又怕,爱它功能强大,但又怕驾驭不好,让它反咬一口,今天我们用CPU的角度,撕掉语法的伪装,重新认识一下虚函数。
虚函数是C++实现面向对象设计及多态特性的重要手段。没有虚函数,C++和C的区别就不大,都需要借助大量的“函数指针”,进行面向对象的程序设计(特别是功能扩展方面)。
有了虚函数的存在,函数指针的使用率大大降低,代码可读性,代码数量都能得到大幅度的改善。
最厉害的是,C++的虚函数实现机制,几乎同时在空间、效率上获得了最优解。
学习C++,虚函数是一条必经之路!
先来看两段简单代码
让我们先比较一下普通函数体与虚函数体有什么区别,显然,两个函数是完全一致的,虚函数跟普通函数一样,都会夹带一个隐藏参数this指针。所以,如你所见,虚函数在实现方面,跟普通函数没有任何区别。
让我们再看看调用它们的时候,会有什么不同
通过对比,大部分地方也是相同的,箭头指的那两条指令都是在输入:隐藏参数 this指针。唯一的区别是,调用普通函数时,call指令的目标地址在编译阶段就确定了,也就是所谓的“静态绑定”;但调用虚函数时,call指令只能根据rdx寄存器的值来确定函数的位置,也就是所谓的“动态绑定”。
再深入理解下这几条指令
原来当类A有虚函数的时候,类A就会偷偷生成一个隐藏成员变量,方便起见,我们给这个隐藏变量起一个名字:V(指针类型),V存放着虚函数表的地址,根据偏移,就可以得到要执行的vtest_1 的地址,将其存在寄存器rdx里面,随后一条:call rdx 指令,一个虚函数的调用就完成了。如果说,类的成员函数会夹带隐藏参数 this指针,还能接受的话,那么,我说类还会夹带隐藏变量V,你能接受吗?如果真的存在隐藏变量V,在哪里给V初始化呢?答案是在A的构造函数中,把V初始化成类A的虚函数表地址,如下:
尽管我没有写构造函数,编译器还是会给我 生成一个默认的构造函数 ,它一定、必须要帮我完成隐藏变量V的初始化。
当然,A有派生类B的话
那么隐藏变量V会在B的构造函数中被初始化为B的虚函数表地址,从而保证A、B的虚函数相互独立,井水不犯河水,但考虑到派生类B的构造函数,还会调用基类A的构造函数。因此,变量V一会儿会被初始化成类A的虚函数表,一会又会被初始化成类B的虚函数表,为了避免晕头,往往会禁止在构造函数里面调用虚函数。
小结一下:
1、虚函数在函数体的实现方面跟普通函数没有任何区别。
2、虚函数的调用需要借助类对象的隐藏变量 V(vptr)来完成,隐藏变量V(vptr)会在构造函数中被初始化成虚函数表的内存地址。
3、调用任何虚函数的套路都是一样的,唯一的区别是要根据它们在虚函数表的位置设置正确的偏移量。
大家可以看看调用vtest_1()和调用vtest_2()的唯一区别是什么?
不得不佩服虚函数的实现方法,几乎同时在效率的空间上得到了最优解,因为虚函数的出现,函数指针的使用率大大降低,如果你还是被函数指针困扰的时候,或许可以考虑一下虚函数。
全部0条评论
快来发表一下你的评论吧 !