漫谈虚函数

          首先写这篇文章之前,想要说明的是这篇文章告诉了我很多细节性的知识点,所以在此感谢该文的作者v_JULY_v,有读者想访问其主页可以点击这里

          C++的虚函数大家比较熟悉,说到虚函数就必须提及多态性,C++的多态性能有效提高基类的可操作性,利用指向基类的指针或者基类的引用,根据运行时编译确定所需要执行的子类的方法,大大提高了代码的可重用性以及类的可扩展性与灵活性。这里多提一句:虚函数与多态性,涉及到的是父类与子类之间方法的重载,这里的重载可以理解为方法的覆盖,这与同一个类内部方法的重载是不一样的。那么有无虚函数到底有什么样的区别呢?我们先看一个例子:

      一、一段代码告诉你虚函数的奇特

           我们先来看下面一段代码:
          
#include<iostream>
using namespace std;
class A
{
     public:
      virtual void fun{
        cout<<"A"<<endl;
      }
};
class B:public A
{
     public:
      virtual void fun{
        cout<<"B"<<endl;
     }
};
int main(int argc,char* argv)
{
     A* a = new A();
     A* b = new B();
     a->fun();
     b->fun();
}
请问输出的结果是什么?恩,答案是A和B。现在对上面的代码中A类和B类的fun函数去掉virtual关键字,执行同样的main函数,答案又是如何呢?这次的答案是A和A。
为什么会产生如上的不同结果呢?
由于基类在派生出派生类的时候,会把自己的成员变量与成员函数一并的派生过去(这里认为是public继承,protected和private继承先不讨论)。所以派生类在构造自身对象的时候会先调用基类的构造函数,再调用自己的构造函数,所以说派生类的一部分是属于基类的。当派生类对象调用非虚函数的时候,函数与对象之间的绑定已经在编译期就已经静态确定了,而调用虚函数方法的时候却要到运行时才能动态确定绑定的对象。归结来说, 无论基类的指针是指向基类对象还是派生类对象,只要调用的方法不是虚方法,就无视子类的方法,直接调用基类自己的方法(同名),不知道这样说大家明白了不,一个是静态绑定,另一个则是动态绑定。

二、虚函数的本质

关于虚函数的本质, 先用一句话做开头:每个类都有一个虚函数表,虚函数表里记录了该类中对应的虚函数的函数地址。类在产生对象的时候,对象会带有一个vptr指针,指针用来指向虚函数表。
OK,来看一个例子:

class Demo { 
public: 
   virtual ~Demo();  
   virtual Demo& fun( float ) = 0; 
   float a() const { return _a; }     //非虚函数,不作存储
   virtual float b() const { return 0; }  
   virtual float c() const { return 0; }  
   // ...
protected: 
   Demo( float x = 0.0 ); 
   float _a; 
};

在这段代码里有5个函数,其中虚函数有4个,分别是虚析构函数(类在被定义为派生类的时候析构函数请定义为虚),fun(),a(),b(),c()。所以在该类产生的虚表中存放了这四个虚函数的地址,而由该类产生的对象则有两部分内容,第一个是成员变量_a,另一个则是指向虚函数表的指针vptr。当Demo类被继承时,子类一般会派生出属于自己的新成员函数与成员变量,这时候会发生什么呢?

我们假设在Demo类的基础上派生出新的成员变量float _b;当然了派生的时候可以重写虚方法,所以在派生类的对象中,新添加了新的部分_b,同时vptr指向了新的虚表。所以在一个类的生成过程中,会产生一定的内存分配来给虚表占用,这就会联想出一个问题:如果一个类反复的一层层继承下去,会不会导致大量的虚表产生而导致内存浪费呢?

请我们一起思考下吧...这也是MFC采用消息映射的原因之一。

三、虚函数表的原理

谈及虚函数表的内容已经在上文提过,现在来讨论下当继承发生的时候,虚函数表会发生什么变化?
首先明确一点,只有继承发生时,父类与子类存在virtual函数,虚函数指针才会产生。
看下图:
派生类从基类产生,基类有虚函数f(),g(),h()而派生类有虚函数f1(),g1(),h1(),那么这整个过程在虚函数表里如何体现?
当Derive生成对象时,有一个指向虚函数表的指针vptr,虚表里的函数地址是按照类中虚函数的声明顺序排放的,假设虚函数表的以栈的结构存储,那么最上面放的先是Base的虚函数而之后才存放Derive的虚函数,注意,此时两个类的虚函数是没有继承关系的,你是你的,我是我的,我比你大,所以在虚函数里我摆前面~
那么当Base在派生出派生类Derive时虚函数被重写了会发生什么?
答案是同名函数,Derive的放在了Base类的前面,如下图:
所以只要虚函数不被重写,谁归属的类级别大谁就放在虚函数的前面,只有被重写之后,子类的虚函数会被置于虚表中基类同名虚函数的前面。
此时我们再联想开头的例子,两个方法都是虚方法,被重写的时候子类的fun其实是摆放在基类的fun之前的(虚表中),所以尽管是指向基类对象的指针,指针在调用函数的时候最先找到的是子类的虚函数地址,所以调用了。(什么是运行时?运行的时候才发现调用那个函数,什么是运行的时候,产生对象的时候,产生对象的时候意味产生虚表指针,有了虚表指针就可以找到虚表,找到虚表就知道了虚函数的顺序,就可以调用了,这就是运行时调用,请允许我那么狗血的理解)。虚函数表的地址存放在对象内存的前四个字节,虚函数表只存放虚函数的指针数组,只放虚函数的地址哟。


最后多说一些,想多了解更多细节的还是请回头看看JULY的文章吧(开头有说明),谢谢大家。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值