C++基础篇--虚函数原理

   虚函数算是C++最关键和核心的内容之一,是组件的基础。下面先列出一些相关名词,再围绕它们举例说明虚函数的本质实现原理。

基础概念(英文部分来自C++编程思想)

  1)绑定:Connectinga function call to a function body is called binding.把函数调用和函数实现关联的过程)

  2)早绑定:Whenbinding is performed before the program is run (by the compiler and linker),it' s calledearly binding程序运行前,即编译和链接阶段,完成的绑定即为早绑定)。

  3)迟绑定:latebinding, which means the binding occurs at runtime, based on the type of theobject.When a language implements late binding, there must be some mechanism todetermine the type of the object at runtime and call the appropriate memberfunction.迟绑定发生在运行时,不同类型的对象绑定不同函数。实现迟绑定,必须有某种机制确定对象的具体类型然后调用合适的成员函数)。

  4)虚函数表(VTable):一个存储于常量区的函数指针表,类似函数指针数组。每个含有虚函数的类(基类及派生类)各自包含一张虚函数表(一个类一张表),表中依次存放虚函数地址派生类vtable继承它各个基类的vtable,这里继承是指:基类vtable中包含某item,派生类vtable中也将包含同样item,但值可能不同。如派生类(override)重新实现了某虚函数,则它的vtable该项item指向新写的虚函数,若未重新实现则沿用基类vtable中对应项的值。

  5) 指向虚函数表的指针(vtptr):所有包含虚函数的类所实例化的对象里,都包含该指针,运行时对象借助于它寻址到虚函数表,从而完成后绑定。因此每个包含虚函数的对象,相比普通对象会额外多占用一个指针型的存储空间。

 6) override,覆盖:派生类中使用同名同参的函数,重新定义基类某virtual函数的过程。其背后,编译器用指向派生类新函数的指针覆盖VTable中原来默认指向基类同名虚函数的指针。

魔术与揭秘

  class Base     //基类,包含virtual函数
  {
 public:
   virtual void output(){ cout << "Base::output()" << endl;}
  };
  //派生两个类Drv0和Drv1
  class Drv0 : public Base
  {
  public:
    void output () { cout << "Drv0:: output ()" << endl;}
  };
  class Drv1 : public Base
  {
  public:
    void output () { cout << "Drv1:: output ()" << endl;}
  };
  void main()
  {
    Base b;       
    Base* pb = &b;
    pb-> output ();             //输出Base::output()
 
    Drv0 d1;   
   pb =reinterpret_cast<Base*>(&d1);
   pb-> output ();               //输出Drv0:: output()
   Drv0 d2;
   pb =reinterpret_cast<Base*>(&d2);
   pb-> output ();              //输出Drv0:: output()
 
    Drv1 d3;
    pb = reinterpret_cast<Base*>(&d3);
    pb-> output ();            //输出Drv1:: output()
}

    经过一些中间封装变换,最终同样的pb-> output ()”运行时选择了不同函数,得到不同结果。奇妙的魔术?别急,下面用C结构体实现类似功能(引入C中不存在的::,故下面为伪码):

    typedef void (*pvfun)();
    const pvfun pf_Base[2]= {Base::default,Base::output};
    const pvfun pf_Drv0[2]= {Base::default, Drv0::output};   // 
    const pvfun pf_Drv1[2]= {Base::default, Drv1::output};   // 
    typedef struct BASE
    {
      void *vtptr;
      int mBase;
    }Base;
    void Base::default()    {  printf("Base::default()");  }
    void Base::output()     {  printf("Base::output()");  }
 
    typedef struct DRV0
    {
      void *vtptr;
      int mBase;
      int mDrv0
    }Drv0;
    void Drv0::output()    {  printf("Drv0::output()");  }
 
    typedef struct DRV1
    {
      void *vtptr;
      int mBase;
      int mDrv1
    }Drv1;
    void Drv1::output()    {  printf("Drv1::output()");  }
 
    void main()
    {
      Base b;
      b.vtptr =pf_Base  ①
      Base* pb =&b; //
      *((pvfun)(pb->vtptr+0))();                //调用Base_default()
      *((pvfun)(pb->vtptr+sizeof(pvfun)))();    //调用Base_output()    
 
      Drv0 d1;   
      d1.vtptr =pf_Drv0   ②
      pb = (Base*)(&d1);
      *((pvfun)(pb->vtptr+0))();                //沿用Base_default()
      *((pvfun)(pb->vtptr+sizeof(pvfun)))();    //调用Drv0_output()    
      
      Drv0 d2;
      d2.vtptr =pf_Drv0 
      pb = (Base*)(&d2);
      *((pvfun)(pb->vtptr+0))();                //沿用Base_default()
      *((pvfun)(pb->vtptr+sizeof(pvfun))()      //调用Drv0_output ()   
 
      Drv1 d3;
      d3.vtptr =pf_Drv1   ③
      pb = (Base*)(&d3);
      *((pvfun)(pb->vtptr+0))()                //沿用Base_default ()
      *((pvfun)(pb->vtptr+sizeof(pvfun)))()    //调用Drv1_output ()
    }

    上例同样实现了用相同形式调用不同函数⑥⑦⑧,但这次能清楚看出猫腻所在:首先②③处分别为结构体成员vtptr赋了不同值;其次pf_Drv0,pf_Drv1中第2个元素Base::output分别被新函数Drv0::output和Drv1::output覆盖 魔术揭穿了,还记得么:所有软件问题都可以通过增加一个中间层解决。表面的神奇是依靠VTablevtptr组成的中间层在背后耍把戏。

    例中pf_Base/pf_Drv0/pf_Drv1就是虚函数表VTable;各结构体的成员vtptr就是指向VTable的指针;②③处把vtptr与各自struct对应的VTable关联;Drv0和Drv1新定义同名函数(output)会覆盖pf_Drv0和pf_Drv1中对应元素,如未新定义则沿用Base中元素Base::default 见④⑤。只不过这些在C++中都隐藏不可见,由编译器自动生成和处理:

    1虚函数表与类关联:编译器在编译时自动为每个包含虚函数的类及其派生类各自单独生成一张虚函数表,用于存放虚函数指针。注意:基类与派生类各有各的虚表,独立存放于不同地址,唯的一关联是:派生类如果没重新实现某基类虚函数,编译器在其VTable对应条目中默认存放基类虚函数地址作为后备;如果派生类重新实现某虚函数,则编译器在VTable中用新函数的地址代替默认的基类虚函数地址,这个过程即为上文的名词--override覆盖。

    2)对象与虚函数表关联:对包含虚函数的类,C++编译器为其每个对象插入一个指针成员vtptr,指向该类的虚函数表,即同类对象的vtptr值相同。vtptr在构造函数中初始化(编译器自动加入),即使该类没定义构造函数,默认构造函数也会初始化vtptr

    3)上面两步说明对象实例化一完毕,就已经和具体虚函数实现挂钩,调用时看似智能的选择不过是顺藤摸瓜:

      Drv0 d1;                           //这一步背后d1->vtptr= VTable(Drv0),其中VTable[0]=(*Drv0::output)()
      pb =reinterpret_cast<Base*>(&d1);  //编译器支持指针强制向上类型转换,把派生类对象的地址赋给基类指针,pb值仍是&d1
      pb-> output();                     //d1->vtptr[0](),即调用Drv0:: output ()

总结虚函数实现原理:

   编译期建立vtable表,设定表中元素;
  执行期间在对象创建时的构造函数中关联vtptr和vtable表;
  借助于指针支持的以小引大,通过强制转换将派生类对象的地址赋给基类指针;
  通过基类指针调用虚函数,先取得对象中的vtptr(obj->vtptr),再找到其所指的对应于特定父类或子类的虚函数表(VTable=*(vtptr)),然后表头加偏移量寻址到相应函数指针(vfunptr = VTable[offset]),最后执行*vfunptr()。

    这就是C++通过虚函数实现多态的背后原理,多态使我们可统一用指向基类对象的指针调用所有基类/派生类的虚函数实现,到底会调哪个,关键看对象的vtptr指针指向了哪个类的VTable,而这点在对象实例化时会通过构造函数隐含设置好。

 

  以一个问题结尾,可否在类的构造函数中调用虚函数,为什么?

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值