虚函数与虚表指针

多态(polymorphism)是C++语言的一大特性,该词最早出自于希腊语,意为多种状态,而C++中的多态是实现接口复用的一种手段,即在保证接口不变的情况下,根据作用对象的不同而产生不同的运行结果—。

C++中实现多态有两个必要的条件:

  • 派生类重写基类的虚函数
  • 基类的指针或引用指向派生类的对象。

对于第二点很好理解,而第一点中“重写虚函数是什么意思呢?”,下面详细探讨这一点:

虚函数

还记得我们之前的虚拟继承那篇博客吗?使用 virtual 关键字修饰被继承的类,则构成了虚拟继承,事实上,虚函数的实现机制和虚拟继承有着异曲同工之妙。声明方法如下:

virtual void f()
{...}

上面的代码定义了一个虚函数,那么虚函数和普通的函数有什么不同呢?

class Base{
public:
    void f()
    {}
private:
    int m_i;
};

对于上面的 Base 类, 我们用 sizeof 求大小的结果毫无疑问是 4,但是如果将函数f 定义为虚函数的话,它的大小会发生变化,如下:
这里写图片描述

为什么类中有了虚函数之后,类的大小会增加 4 个字节呢?
因为具有虚成员函数的类中,会增加一个虚表指针,该指针指向一个虚函数表,该表中记录了虚函数的地址。看看下面这段代码:

//...
class Base{
public:
    virtual void f()
    {
        cout << "Base::f()" << endl;
    }
    virtual void g()
    {
        cout << "Base::g()" << endl;
    }
private:
    int m_i;
};

typedef void(*Fun)(void);
//...
    Base b;

    Fun pf[2];

    pf[0] = (Fun)*(int*)(*(int*)&b);
    pf[1] = (Fun)*((int*)(*(int*)&b)+1);

    pf[0]();
    pf[1]();
//...

这里写图片描述

我们通过虚表指针取得虚函数表,再通过虚函数表中的函数地址来调用虚函数,下面详细讨论:

虚函数表

C++中的虚函数是通过虚函数表来实现的,这张表中记录了类的虚函数地址,继承体系中,出现虚函数覆盖的场景时,这张表就显得尤为重要,我们先看在无继承体系下,对象的内存布局:

这里写图片描述

在 VS 下,通过内存监视窗口可以看到,对象的最前面是一个指向虚表的指针,接着是成员变量。将虚表指针置于对象实例的最前位置,是为了保证取虚函数具有最高的性能。

这里写图片描述

多重继承体系下对象模型

看下面这段代码:

class Base1{
public:
    virtual void  f(){
        cout << "Base1::f()" << endl;
    }

    virtual void  g(){
        cout << "Base1::g()" << endl;
    }

    //...
};
class Base2{
public:
    virtual void  f(){
        cout << "Base2::f()" << endl;
    }

    virtual void  g(){
        cout << "Base2::g()" << endl;
    }
    //...
};


class Derive : public Base1, public Base2{
public:
    virtual void f(){
        cout << "Derive::f()" << endl;
    }

    virtual void g1(){
        cout << "Derive::g1()" << endl;
    }
    //...
};

派生类 Dervice 继承了两个基类 Base1Base2 。两个基类各有三个虚函数函数,派生类中重写了第一个虚函数 f() ,下面看看他的内存布局:

这里写图片描述

可以看到:在对象 d 的实例最前面是两个虚表指针,分别指向所继承的两个基类的虚表,我们可以将两个虚表指针看成一个整体,则该整体是一个指针数组,该指针有指向一个指针数组,后者的指针指向该类虚函数。所以,我们可以像下面这样调用虚函数:

Derive d;

    pf Fun = NULL;
    int **pVtab = (int**)&d;

    Fun = (pf)pVtab[0][0];
    Fun();

    Fun = (pf)pVtab[0][1];
    Fun();

    Fun = (pf)pVtab[0][1];
    Fun();

    Fun = (pf)pVtab[1][0];
    Fun();

    Fun = (pf)pVtab[1][1];
    Fun();

运行结果如下:
这里写图片描述

当然我们也可以像最开始的那种方法——先通过虚表指针取虚表,在取虚函数,最后通过类型强转调用它。

而且也可以看到,基类中被重写的虚函数位置已经被替换为派生类的虚函数指针,这样,我们就可以通过指向派生类的指针或引用取=去调用派生类的函数了。正是这一点,使得 C++ 具有多态的特性。
这里写图片描述

通过虚表访问私有函数

类的私有虚函数也会被记录在虚表中,因此我们可以在类的外部通过虚表调到私有函数,这也是虚表所带来的弊端。

class Base1{
private:
    virtual void f(){
        cout << "Base1::f()" << endl;
    }
    virtual void  g(){
        cout << "Base1::g()" << endl;
    }
};

这里写图片描述

可以看到,类的私有函数在类外被访问到了。

——完!


【作者:果冻 http://blog.csdn.net/jelly_9

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值