zoukankan      html  css  js  c++  java
  • (C/C++学习)4.C++类中的虚函数表Virtual Table

    说明:C++的多态是通过一张虚函数表(Virtual Table)来实现的,简称为V-Table。在这个表中,主要为一个类的虚函数的地址表,这张表解决了继承、覆写的问题,保证其真实反应实际的虚函数调用过程。这样,在有虚函数的类的实例中这个表被分配在了这个实例的内存中,所以,当我们用父类的指针来操作一个子类的时候,这张虚函数表就显得尤为重要了,它就像一个地图一样,指明了实际所应该调用的函数。

     

    下面介绍一下与这张虚函数表有关的几个问题:

    1.普通成员函数不占存储空间,而所有虚函数入口地址存储在一张虚函数表中,由一个指针指向该虚函数表;

    2.指向该虚函数表的指针位于类实例对象内存的最前面,占四个字节;

    3.若子类覆写了父类的虚函数,则父类的虚函数被覆盖,即虚函数表中只存在子类的虚函数地址;否则,父类和子类的虚函数都存在于虚函数表中(当然,没有覆写父类的虚函数是毫无意义的),这就是多态形成的原因。

     

    通过上面的介绍,我们对虚函数表有了大致的了解,下面通过一个实例来加深一下认识:

      1 #include <iostream>
      2 using namespace std;
      3 
      4 class base
      5 {
      6 public:
      7     virtual void f(){cout<<"base::f()"<<endl;}
      8     virtual void g(){cout<<"base::g()"<<endl;}
      9     virtual void h(){cout<<"base::h()"<<endl;}
     10 private:
     11     int a;
     12 };
     13 
     14 //定义一个函数指针,并别名为pfunc,用时不需再加*,
     15 typedef void (*pfunc)(void);
     16 
     17 int main()
     18 {
     19     base b;
     20 
     21     //C++编译器使虚函数表的指针存在于对象实例中的最前面(四个字节)
     22     cout<<"sizeof(base) = "<<sizeof(base)<<'	'<<"sizeof(b) = "<<sizeof(b)<<endl<<'
    ';
     23 
     24     //分别打印对象b的起始地址和虚函数表中首个函数指针指向的地址
     25     //对象实例最前面的四个字节为指向虚函数表的指针,取内容后才为虚函数表
     26     cout<<"&b = "<<&b<<"		"<<"&VTable = "<<(int **)*(int *)(&b)<<endl<<"
    
    ";
     27 
     28     pfunc pf;
     29     //定义一个函数指针
     30     void(*p)(void);
     31     //还可以这样定义一个函数指针
     32 
     33     //虚函数表里面存放的是指向各个虚函数的指针,取内容后才是各个相应的虚函数
     34     pf = (pfunc)*((int **)*(int *)(&b)+0);
     35     pf();
     36     pf = (pfunc)*((int **)*(int *)(&b)+1);
     37     pf();
     38     pf = (void(*)())*((int **)*(int *)(&b)+2);
     39     pf();
     40 
     41     cout<<"
    
    ";
     42 
     43     p = (pfunc)*((int **)*(int *)(&b)+0);
     44     p();
     45     p = (void(*)())*((int **)*(int *)(&b)+1);
     46     p();
     47     p = (void(*)())*((int **)*(int *)(&b)+2);
     48     p();
     49 
     50     return 0;
     51 }
     52 

    程序运行结果:

    2获

    通过以上示例,我们把类实例对象b取址,然后将&b强转成int*型,然后对其取内容,取得虚函数表的地址,然后再对其取内容,就得到了第一个虚函数的地址了,然后再将其通过(int**)强转成步长为4的指针,通过加1来得到虚函数表中不同的虚函数的地址,最终强转成为函数指针,再通过该函数指针访问相应的虚函数.

    5.下面我们将通过几个例子来解释一下虚函数表的存在形式,在这部分,主要弄清楚虚函数表是怎么一回事,至于程序运行结果,读者自行实验。

    a.在父子类中,若子类没有对父类的虚函数进行覆写(当然,前面提到过,没有覆写父类的虚函数是毫无意义的。之所以要讲述没有覆写的情况,主要目的是为了给一个对比,在比较之下,我们可以更加清楚地知道其内部的具体实现),如下代码,

      1 #include<iostream>
      2 using namespace std;
      3 class base
      4 {
      5 public:
      6     virtual void func(){};
      7     virtual void foo(){};
      8 };
      9 class derive:public base
     10 {
    public: 11 virtual void func1(){}; 12 virtual void foo1(){}; 13 }; 14 int main() 15 { 16 derive d; 17 return 0; 18 } 19

    则其虚函数表如下所示:

    无标题

    注意:

    1.上面这个图中,我在虚函数表的最后多加了一个结点,这是虚函数表的结束结点,就像字符串的结束符“/0”一样,其标志了虚函数表的结束。这个结束标志的值在不同的编译器下是不同的。

    2.虚函数是按照其声明顺序放于表中的。

    3.父类的虚函数在子类的虚函数前面。

     

    b.在父子类中,若子类对父类的虚函数进行了覆写(为了对比,假设只覆写父类一个虚函数),如下代码,

      1 #include<iostream>
      2 using namespace std;
      3 class base
      4 {
      5 public:
      6     virtual void func(){};
      7     virtual void foo(){};
      8     virtual  ~base(){}
      9 };
     10 class derive:public base
     11 {
     12 public:
     13     virtual void func(){cout<<"___"<<endl;};
     14     virtual void foo1(){};
     15     virtual  ~derive(){}
     16 };
     17 int main()
     18 {
     19     base *p = new derive;
     20     p->func();
     21     delete p;
     22     return 0;
     23 }
     24 

    则其虚函数表如下所示:

    无标题

    由此,可得覆写的子类func()放在了虚函数表中原来父类func()的位置,没有覆写的虚函数依旧原样存放。这样,在上述代码中,由于p所指的func()的位置已经被derive::func()的函数地址所取代,因此在发生实际调用的时候,调用的是子类的func(),这就实现了多态。

  • 相关阅读:
    沃尔玛的问题
    为什么没有“128位”的通用处理器
    用户模式驱动模型(UMDF)简介
    Live Space的谢幕
    Cheap HDD bracket
    让 UV4 支持STC 单片机
    SQLServer 分组查询相邻两条记录的时间差
    ERP采购系统流程
    C++运算符的优先级和结合性
    Entity Framework 4.1延时加载与贪婪加载之我的理解和数据库中如何存入图片
  • 原文地址:https://www.cnblogs.com/tuihou/p/9714169.html
Copyright © 2011-2022 走看看