一个函数指针引发的血案 - 玩坏C++的多态 (polymorphic)方式

    面向对象的三个特点,封装继承多态,好了,当面向对象遇上指针,就有了C++。三个特点中的封装继承很容易理解,按笔者的个人理解,封装继承是为多态而生。当一个父类有多个继承类时,通过对父指针赋予不同继承类的对象,就可以灵活地调用继承类中的继承函数了,这就是多态。

    在C++中提供了类型转换操作符dynamic_cast<type *>(ptr)来实现继承体系中安全的向下类型转换,使用它的前提是该继承体系中使用了多态,对,使用了就行,哪怕只是父类中有个虚函数。dynamic_cast相对于万能的C转换方式更安全些,原因在于如果转换不恰当,dynamic_cast将返回NULL或抛出异常(转换对象是引用时)。

      虚函数是实现多态的重要元素,请看:

class A
{
public:
    void a0(){cout <<"a0"<<endl;}
    virtual void a1(){cout <<"a1"<<endl;}
};

class B: public A
{
public:
    void a0(){cout <<"b.a0"<<endl;};
    void a1(){cout <<"b.a1"<<endl;};
};

main()
{
    A * a0 = new B ();
    a0->a0();
    a0->a1();
    A a1 = B();
    a1.a0();
    a1.a1();
delete a;
}

 

    输出是a0, b.a1, a0, a1。喻示了两点:其一,C++多态是通过指针来实现的,这和Java中通过类型转换(C#中称为装箱和拆箱)不同,因为执行A a1=B()时,首先调用了B de 构造函数构造出B对象,然后调用A的复制构造函数构造A,因此,最终调用的是A的复制构造函数,在调用函数时当然也调用A的函数了;其二,virtual的功能是使用多态时,子类的同名同参数的函数得以覆盖父类函数,而对于非虚函数,C++中在通过对象调用成员函数时,函数的入口在编译时就静态地确定了,而编译器是不在乎指针在赋值后会指向什么对象的。

    这一切来自于C++的虚函数表机制。虚函数表是一个连续的内存空间,保存着一系列虚函数指针。在构造一个子对象时,内存空间最开始的4B保存一个虚函数表的入口地址。如上例中,A的虚函数表为<A::a1>,B继承A并重写了虚函数a1,因此B的虚函数表为<B::a1>,即在继承的时候,用B::a1的函数地址覆盖了A::a1的地址。于是有了下面的代码:

class A
{
public:
    void a0(){cout <<"a0"<<endl;}
    virtual void a1(){cout <<"a1"<<endl;}
    virtual void a2(){cout <<"a2"<<endl;}
};

class B: public A
{
public:
    void a0(){cout <<"b.a0"<<endl;};
    void a1(){cout <<"b.a1"<<endl;};
    void a2(){cout <<"b.a2"<<endl;}
};

type void ( * Function)();

main()
{
    A * a = new B ();
    Function p =  (void (*)(void))*( (int *) *(int*)(a) + 0 );
    p();
    delete a;
}

其中:

  a是对象的地址,

  (int *) a是对象最开始4字节的地址

  * (int *)a是对象的最开始的4字节

  (int *) * (int *)a是对象最开始的四个字节,实际上是个地址,是什么地址呢,是虚函数表存放的地址

  * (int *) * (int *)a是虚函数表的第一项,也就是第一个虚函数的地址,因此+0表示第一个函数,+1表示第二个函数,以此类推

  于是通过函数指针p,成功地访问了B的第一个虚继承函数a1。虚函数表就是这么一回事。下面两个小魔术来了。

 

1,通过函数指针访问子类的私有函数

2,通过函数指针访问父类的私有函数

class A
{
    virtual void a0()
    {
        printf("A::a0 (private)\n");
    }
public:
    explicit A(){}
    virtual void a1()
    {
        printf("A::a1 (public)\n");
    }
};

class B : public A
{
public:
    explicit B(){}
private:
    int y;
    virtual void a1()
    {
        printf("B::a1 (private)\n");
    }
};

typedef void (* Function)();

main()
{
    A * a = new B();
    Function p;
    p = ( void (*)(void) ) *( (int *)  *(int*)(a) + 0 );
    p();
    p = (Function) *( (int*) *(int*)(a) + 1 );
    p();
a->a1(); delete a; }

    其中A的虚函数表是<private A::a0, public A::a1>, B的虚函数表是<private A::a0, private B::a1>。

    在第一次调用时p指向private A::a0,而第二次调用时p指向private B::a1。权限的检查是在编译阶段,因此动态的指针调用绕过了权限检查。

    在第三次调用时(a->a1())时,由于对权限的检查是在编译阶段,而编译器不检查a到底指向什么对象(因为这是动态的),只看a的类型。编译器发现a是A的指针,而a1()在类A中是public函数,因此权限检查顺利地pass。随后开始执行,此时a->a1()的指针指向B::a1(),于是乎,我们成功地用父类指针A * a调用了子类B的私有函数。

    

posted @ 2014-10-09 12:11  chng  阅读(790)  评论(0编辑  收藏  举报
BackToTop