• 浅谈C++虚函数


      很长时间都没写过博客了,主要是还没有养成思考总结的习惯,今天来一发。

      我是重度拖延症患者,本来这篇总结应该是早就应该写下来的。

    一、虚函数表

      C++虚函数的机制想必大家都清楚了。不清楚的同学请参看各种C++入门书籍。这里,我要讨论一下这个虚函数机制究竟是怎么实现的。虚函数主要是靠一张VTABLE来实现的,先来看看这个VTABLE在哪里。

      首先我们看下面的代码:

     1 class ClassA
     2 {
     3 public:
     4     int m_data1;
     5     int m_data2;
     6     void vfunc1(){cout << "i am A" << endl;}
     7 };
     8 class ClassB : public ClassA
     9 {
    10 public:
    11     int m_data3;
    12     void funcB(){}
    13     void vfunc1(){cout << "i am B" << endl;}
    14 };
    15 class ClassC : public ClassB
    16 {
    17 public:
    18     int m_data1;
    19     int m_data4;
    20     void funcC(){}
    21     void vfunc1(){cout << "i am C" << endl;}
    22 };
    23 int main()
    24 {
    25     ClassA a;
    26     ClassB b;
    27     ClassC c;
    28     cout << sizeof(int) << endl;
    29     cout << sizeof(ClassA) << endl;
    30     cout << sizeof(ClassB) << endl;
    31     cout << sizeof(ClassC) << endl;
    32 
    33     cout << &(a) << endl;
    34     cout << &(a.m_data1) <<endl;
    35     cout << &(a.m_data2) <<endl;
    36     cout << endl;
    37 
    38     cout << &(b) << endl;
    39     cout << &(b.ClassA::m_data1) << endl;
    40     cout << &(b.ClassA::m_data2) << endl;
    41     cout << &(b.m_data3) <<endl;
    42     cout << endl;
    43 
    44     cout << &(c) << endl;
    45     cout << &(c.ClassA::m_data1) << endl;
    46     cout << &(c.m_data2) << endl;
    47     cout << &(c.m_data3) << endl;
    48     cout << &(c.m_data1) <<endl;
    49     cout << &(c.m_data4) <<endl;
    50     
    51     return 0;
    52 }

      我如果把上面的程序中ClassA的函数vfunc1声明成虚函数,即将第6行改为:

    1 virtual void vfunc1(){cout << "i am A" << endl;}

      程序运行的两个结果分别为:

      由上面的结果可以明显的看出,声明为虚函数的类比原来的类在大小上多了4个字节。没有虚函数的类的起始地址和第一个成员变量的地址保持一致,有虚函数的类的起始地址在第一个成员变量地址的前四个字节。这中间多出来的这四个字节就是隐藏起来的VPTR。VPTR是一个指向一个VTABLE的指针,换句话说,这多出来的四个字节里面存的是VTABLE的地址。

      而VTABLE里面就记录了这个类里面虚函数的地址。

    再看下面的代码:

    1  ClassA *pa;
    2  ClassB *pb;
    3  ClassC *pc;
    4  pa = &c;
    5  pa->vfunc1();

      我们都知道如果是虚函数,上面的代码结果肯定为

      i am C

      如果没用虚函数,结果为

      i am A

      这是怎么做到的?

      首先,我们要知道,子类继承父类,子类拥有所有父类的成员变量跟成员函数,就是说:

    1 c.vfunc1();
    2 c.ClassA::vfunc1()

      我们可以上面的方式显示地去访问被子类覆盖掉的函数和变量。可以理解为,虽然名字一样,其实子类里面有两个独立的vfunc1()函数,只不过子类调用的默认为ClassC::vfunc1()函数。

      当我们用父类的指针去指向一个子类的指针时,会有一个向上转换(我暂时这么叫)的过程。用pa指向对象c时,pa是一个ClassA类型的指针,pa只能访问ClassA类里面有的成员变量和成员函数地址,多余的,A类没有而C类有的成员变量和函数地址都被“upcasting”掉了。

      没有VTABLE时,只能找到ClassA类的vfunc1()函数的地址,找不到ClassC类的vfunc1()函数的地址。有虚函数表的存在时,对象c的虚函数表里面会记录ClassC::vfunc1()的地址,这样用pa指向对象c时,虚函数表不会被“upcasting”掉,于是,按照虚函数表里面的地址,就能够成功访问ClassC::vfunc1()。

      简言之,就是虚函数表里面存有正确的函数地址,这样就实现了动态绑定。用一张图来表示就是:

    (如果有多个虚函数,VTABLE里面就有多个地址)

    二、切片

      首先我们在三个类里面分别添加三个函数:

    1 virtual void funcSlicing(){cout << "slicing A" << endl;}
    2 virtual void funcSlicing(){cout << "slicing B" << endl;}
    3 virtual void funcSlicing(){cout << "slicing C" << endl;}

      再看如下的代码:

    1 ClassA *pa = &c;
    2 pa->funcSlicing();
    3 c.funcSlicing();
    4 ((ClassA*)(&c))->funcSlicing();
    5 ((ClassA)c).funcSlicing();

      如果你能一眼看出上面程序的运行结果,那接下来你就可以不用再看了。正确的结果是:


      

      前面三个应该很好理解,就是前面的虚函数机制。最后一句((ClassA)c).funcSlicing()的结果为什么是“slicing A”呢。这就是传说中的对象切片了。(ClassA)c这个操作意味着什么?这个操作意味着调用ClassA::默认拷贝构造函数将对象c中继承自ClassA类的成员进行copy,这个过程包含把对象c的VPTR(原来指向ClassC::VTABLE)修改为指向ClassA::VTABLE,而对象c中多余的东西则被“切割”掉了。

      这个时候(ClassA)c已经完完全全是一个ClassA了,这就是对象切片。

      也就是说,假如我有如下的一个函数:

    1 void TEST(ClassA a)
    2 {
    3     a.funcSlicing();
    4 }

      这个时候,无论我调用TEST(b)或者TEST(c),结果都应该是调用ClassA::funcSlicing(),因为发生了对象切片。

      在多态的机制里面,我们总是应该是传对象的地址或者引用,不应该以对象本身作为参数传递。

      这里,我再简单说一下纯虚函数,我们都知道有纯虚函数的抽象类是不能实例化的。为什么不能实例化?因为纯虚函数强制性的给VTABLE里面留了一个空位置,这个位置里面没有留任何函数地址,为空。而我们在创建一个包含虚函数的对象时,编译器首先要做的事情就是初始化VPTR和VTABLE。只要有一个纯虚函数存在,那么VTABLE就是不完整的,为这样的类(抽象类)创建对象,编译器会返回错误信息。

      同理,在上面的例子中,假如我们把ClassA::funcSlicing()改为纯虚函数:

    1 virtual void funcSlicing() = 0;

      TEST()函数就会编译错误,纯虚函数重要作用之一就是防止对象切片的发生。

      PS:C++果然是超级复杂,要兼顾效率和设计,完全取决于使用者的需要。我也只能是,用到哪里就好好把哪部分学一下。

      2014-1-11更新:

      偶然间看到一篇大牛的文章,C++ 虚函数表解析 ,又深刻体会到自己与别人的差距,你对一个东西理解有多深,你就可以给别人讲多清楚。由这篇文章的启发,可以用函数指针来访问虚函数表里面的函数。

      首先,声明这样一个函数指针的类型:

    1 typedef void(*Fun)(void);

      然后用下面的代码去访问虚函数表里面的函数(在陈的那篇文章第一个例子里面会有一些细节错误):

     1  Fun pFun = NULL;
     2  pFun = (Fun)*((int *)*((int *)&c + 0)+0);
     3  pFun();
     4  pFun = (Fun)*((int *)*((int *)&c + 0)+1);
     5  pFun();
     6  //虚函数表(VPTR)的地址(对象c起始四字节里面的内容):*((int *)&c + 0)
     7  //(int *)的作用是强制转换成四字节的int型指针,这样指针偏移是以四字节为单位。
     8  //虚函数表里面第一个虚函数地址 *((int *)*((int *)&c + 0)+0)
     9 
    10  int **pVtable = (int **)&c;//这样就直观多了,两次寻址。
    11  pFun = (Fun)pVtable[0][0];
    12  pFun();

      我想,聪明的你肯定清楚上面代码的执行结果。这样,就不是空口无凭了。

     

     

     

  • 相关阅读:
    【Linux】ZeroMQ 在 centos下的安装
    ZeroMQ下载、编译和使用
    在Linux系统上安装Git
    Linux下python2.7安装pip
    [Tyvj1474]打鼹鼠
    [BZOJ2908]又是nand
    [SPOJ375]Qtree
    浅谈算法——树链剖分
    [BZOJ5368/Pkusc2018]真实排名
    [FJOI2007]轮状病毒
  • 原文地址:https://www.cnblogs.com/xibaohe/p/3511358.html
Copyright © 2020-2023  润新知