首页 > 代码库 > 玩坏C++的多态机制
玩坏C++的多态机制
面向对象的三个特点,封装继承多态,好了,当面向对象遇上指针,就有了C++。三个特点中的封装继承很容易理解,按笔者的个人理解,封装继承是为多态而生。当一个父类有多个继承类时,通过对父指针赋予不同继承类的对象,就可以灵活地调用继承类中的继承函数了,这就是多态。
虚函数是实现多态的重要元素,请看:
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的私有函数。
玩坏C++的多态机制