继承中多态的核心---虚函数表

来源:互联网 发布:mac口红畅销的豆沙色 编辑:程序博客网 时间:2024/05/20 19:30

在继承中多态是通过父类型指针指向子类对象,并调用虚函数实现的。实现这一行为的核心就是虚函数表。

虚函数表存在于类对象中,有虚函数的类的对象中就有虚函数表。同时虚函数表在对象的最前面的位置(准确来说是指向虚函数表的虚函数表指针)。存在的这个指针(4字节)指向一张虚函数表,虚函数表中存放各个虚函数的指针,指向各个虚函数。所以获取虚函数表的地址是

class Base {

     public:

            virtual void f() { cout << "Base::f" << endl; }

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

            virtual void h() { cout << "Base::h" << endl; }

 

};

       Base b;

 cout << "指向虚函数表的指针的地址:" << &b << endl;

 cout << "指向虚函数表的指针的值" << *(int*)(&b) << endl;

 cout << "虚函数表地址:" <<  *(int*)(&b) << endl;

 cout << "虚函数表中的第一个值(指向第一个虚函数的指针)" << *(int*)*(int*)(&b) << endl;

 cout << "第一个虚函数的地址" <<   *(int*)*(int*)(&b) << endl;

typedef void(*Fun)(void);

 Fun pFun = NULL;

pFun = (Fun)*((int*)*(int*)(&b));  --------------》》用Fun强制类型转换,将指针(地址)转换为指向函数类型的指针

pFun();就会输出Base::f

    (Fun)*((int*)*(int*)(&b)+0);  // Base::f() 

            (Fun)*((int*)*(int*)(&b)+1);  // Base::g()  ,+1是因为虚函数表中存在的是指针,也就是地址,是int型的,+1就是下一个函数的地址

            (Fun)*((int*)*(int*)(&b)+2);  // Base::h()

知道了虚函数表的存放方式之后,来看下它是如何工作的。

 Class A{

void fun1();

virtual  void fun2();

}

      Class B:public A{

fun1();

virtual void fun2();

}

A* a = new B;

此时,对于a来说,把B的对象看成了A类型的对象,所以当a->fun1()时,由于fun1不是虚函数,所以它不会存入虚函数表中,按照A类型的函数来执行。

当a->fun2()时,由于fun2是虚函数,它会存入虚函数表中,调用时会到虚函数表中找fun2的正确地址,最终执行的是B类中的函数。

解释下为什么在查虚函数表时会执行到B类中的fun2:

下面,再让我们来看看继承时的虚函数表是什么样的。假设有如下所示的一个继承关系:

 

 

请注意,在这个继承关系中,子类没有重载任何父类的函数。那么,在派生类的实例中,其虚函数表如下所示:

 

对于实例:Derive d; 的虚函数表如下:

 

我们可以看到下面几点:

1)虚函数按照其声明顺序放于表中。

2)父类的虚函数在子类的虚函数前面。


覆盖父类的虚函数是很显然的事情,不然,虚函数就变得毫无意义。下面,我们来看一下,如果子类中有虚函数重载了父类的虚函数,会是一个什么样子?假设,我们有下面这样的一个继承关系。

 

 

 

为了让大家看到被继承过后的效果,在这个类的设计中,我只覆盖了父类的一个函数f()。那么,对于派生类的实例,其虚函数表会是下面的一个样子:

 

 

我们从表中可以看到下面几点,

1)覆盖的f()函数被放到了虚表中原来父类虚函数的位置。

2)没有被覆盖的函数依旧。

 

这样,我们就可以看到对于下面这样的程序,

 

            Base *b = new Derive();

 

            b->f();

 

b所指的内存中的虚函数表的f()的位置已经被Derive::f()函数地址所取代,于是在实际调用发生时,是Derive::f()被调用了。这就实现了多态。


下面我们再来看看,如果发生虚函数覆盖的情况。

 

下图中,我们在子类中覆盖了父类的f()函数。

 

 

 

下面是对于子类实例中的虚函数表的图:

 

 

我们可以看见,三个父类虚函数表中的f()的位置被替换成了子类的函数指针。这样,我们就可以任一静态类型的父类来指向子类,并调用子类的f()了。如:

 

            Derive d;

            Base1 *b1 = &d;

            Base2 *b2 = &d;

            Base3 *b3 = &d;

            b1->f(); //Derive::f()

            b2->f(); //Derive::f()

            b3->f(); //Derive::f()

 

            b1->g(); //Base1::g()

            b2->g(); //Base2::g()

            b3->g(); //Base3::g()


虚函数表带来的安全性问题:

 

让我们来看看我们可以用虚函数表来干点什么坏事吧。

 

一、通过父类型的指针访问子类自己的虚函数

我们知道,子类没有重载父类的虚函数是一件毫无意义的事情。因为多态也是要基于函数重载的。虽然在上面的图中我们可以看到Base1的虚表中有Derive的虚函数,但我们根本不可能使用下面的语句来调用子类的自有虚函数:

 

          Base1 *b1 = new Derive();

            b1->f1();  //编译出错

 

任何妄图使用父类指针想调用子类中的未覆盖父类的成员函数的行为都会被编译器视为非法,所以,这样的程序根本无法编译通过。但在运行时,我们可以通过指针的方式访问虚函数表来达到违反C++语义的行为。(关于这方面的尝试,通过阅读后面附录的代码,相信你可以做到这一点)

 

 

二、访问non-public的虚函数

另外,如果父类的虚函数是private或是protected的,但这些非public的虚函数同样会存在于虚函数表中,所以,我们同样可以使用访问虚函数表的方式来访问这些non-public的虚函数,这是很容易做到的。

 

如:

 

class Base {

    private:

            virtual void f() { cout << "Base::f" << endl; }

 

};

 

class Derive : public Base{

 

};

 

typedef void(*Fun)(void);

 

void main() {

    Derive d;

    Fun  pFun = (Fun)*((int*)*(int*)(&d)+0);

    pFun();

}

 




0 0
原创粉丝点击