C++多态、虚函数浅析

来源:互联网 发布:mac怎样装搜狗输入法 编辑:程序博客网 时间:2024/05/18 20:50
刚才在一朋友空间里看到这篇文章,感觉非常不错,详细的解释了C++中多态和虚函数的实现机制,特转载一下:出自:http://user.qzone.qq.com/1529486906/blog/1386947592
 

3.3.1 C++多态、虚函数浅析

多态(Polymorphism),按字面的意思就是“多种形状”。多态性是允许将父对象设置成为和一个或更多的他的子对象相等的技术,赋值后,父对象就可以根据当前赋值给它的子对象的特性以不同的方式运作。在C++中,多态性是通过虚函数(Virtual Function)来实现的。

而什么是“虚函数”(或者是“虚方法”)呢?虚函数就是允许被其子类重新定义的成员函数。而子类重新定义父类虚函数的做法,称为“覆盖”(Override),或者称为“重写”。

1.覆盖

覆盖是指子类重新定义父类虚函数的做法。当子类重新定义了父类的虚函数后,父类

指针根据赋给它的不同子类指针,动态的调用属于子类的函数,这样的函数调用在编译期

间是无法确定的(调用的子类虚函数的地址无法给出)。因此,这样的函数地址是在运行期

绑定的。

2.多态的作用

封装可以隐藏实现细节,使得代码模块化;继承可以扩展已存在的代码模块(类)。它们的目的都是为了代码重用。而多态则是为了实现接口重用。而且现实往往是,要有效重用代码很难,而真正最具价值的重用是接口重用。

下节将通过具体的C++实现来进行深入分析多态、虚函数的机制。

 

3.3.2  多态实例分析

由上节介绍,在C++中,多态性是通过虚函数(Virtual Function)来实现。请看下面一段简单的代码:

#include <iostream> 
using namespace std; 
class Base  //定义基类

    int a; 
    
public:

virtual void fun1(){cout<<"Base::fun1()"<<endl;}  //虚函数1

virtual void fun2(){cout<<"Base::fun2()"<<endl;}  //虚函数2

virtual void fun3(){cout<<"Base::fun3()"<<endl;}  //虚函数
}; 
class A:public Base  //A继承Base 

    int a;

public:

void fun1(){cout<<"A::fun1()"<<endl;}  //虚函数1

void fun2(){cout<<"A::fun2()"<<endl;}  //虚函数
}; 
void foo (Base& obj) //函数foo 

    
obj.fun1();  //调用Base类函数.fun1()

obj.fun2();  //调用Base类函数.fun2()

obj.fun3();  //调用Base类函数.fun3() 

int main()  //主函数

    
Base b;

A a; 
    
foo(b);

foo(a);
    return 0;
}


    最终的运行结果如下:


Base::fun1()
Base::fun2()
Base::fun3()
A::fun1()
A::fun2()
Base::fun3()

 

仅通过基类的接口void foo(Base& obj),程序调用了正确的函数foo(a),它就好像知道输入的对象的类型一样。那么,编译器是如何得知正确代码的位置的呢?其实,编译器在编译时并不清楚要调用的函数体的正确位置,但它插入了一段能找到正确的函数体的代码。这称之为晚捆绑(Late Binding)或运行时捆绑(Runtime Binding)技术。


    1
virtual关键字

通过virtual关键字创建虚函数能引发晚捆绑,编译器在幕后完成了实现晚捆绑的必要机制。它对每个包含虚函数的类创建一个表(称为Vtable),用于放置虚函数的地址。在每个包含虚函数的类中,编译器秘密地放置了一个被称为Vpointer(缩写为VPTR)的指针,指向这个对象的Vtable

VPTR由编译器在构造函数中秘密插入的代码来完成初始化,指向相应的Vtable,这样对象就“知道”自己是什么类型了。VPTR都在对象的相同位置上,常常是对象的开头。这样,编译器可以容易地找到对象的Vtable并获取函数体的地址。如果用sizeof查看前面Base类的长度,就会发现,它的长度不仅仅是一个整型数的长度,而是增加了刚好是一个void指针的长度(一个整型数占4个字节,一个void指针占4个字节,这样正好类Base的长度为8个字节)。每当创建一个包含虚函数的类或从包含虚函数的类派生一个类时,编译器就为这个类创建唯一的Vtable

    说明:

Vtable中,放置在这个类中或是它的基类中所有虚函数的地址,这些虚函数的顺序都是一样的,所以通过偏移量可以容易地找到所需的函数体的地址。


    假如在派生类中没有对基类中的某个虚函数进行重写(Overriding),那么还使用基类的这个虚函数的地址,正如上面的程序结果所示,其过程如图3.3所示。

 

 


2.虚函数的调用过程

下面来看一下调用虚函数地过程。

如果有一个Base指针作为接口,那么它一定指向一个Base或由Base派生的对象,或者是A,或者是其他什么。这无关紧要,因为VPTR的位置都一样,一般都在对象的开头。如果是这样的话,那么包含虚函数的对象的指针,例如Base指针,指向的位置恰恰是另一个指针VPTR


    注意:

VPTR指向的Vtable其实就是一个函数指针的数组,现在,VPTR正指向它的第一个元素,那是一个函数指针。如果VPTR向后偏移一个vo id指针长度的话,那么它应该指向了Vtable中的第二个函数指针了。这看来就像是一个指针连成的链,应从当前指针获取它指向的下一个指针,这样才能“顺藤摸瓜”。

介绍一个函数:


void *getp (void* p) 
{
    
return (void*)*(unsignedlong*)p; 

}


   getp()
可以从当前指针获取它指向的下一个指针。如果能找到函数体的地址,那么用什么来存储它呢?应该用一个函数指针:

typedef void (*fun)();


    它与Base中的3个虚函数相似,这里不要任何输入和返回,读者要清楚它实际上已经被执行了。然后,创建如下函数:

fun getfun (Base* obj, unsigned long off) 

    
void *vptr = getp(obj);

unsigned char *p = (unsignedchar *)vptr;

p += sizeof(void*) * off;

return (fun)getp(p); 
}


    第一个参数是Base指针,可以输入Base或是Base派生对象的指针。第二个参数是Vtable偏移量,偏移量如果是0,那么对应fun1();如果是1,对应fun2()getfun()返回的是fun类型函数指针,即上面定义的那个。可以看到,函数首先就对Base指针调用了一次getp(),这样得到了VPTR这个指针,然后用一个unsigned char指针计算偏移量,得到的结果再次输入getp(),这次得到的就应该是正确函数体的位置了。

修改main()函数如下:


int main() 

    
Base *p = new A;

fun f = getfun(p, 0);

(*f)();

f = getfun(p, 1);

(*f)();

f = getfun(p, 2);

(*f)(); 
    /*
    当然了,您也可这样写:
    fun f;
    for(int i=0;i<3;i++)
    {
        f=getfun(p,i);
        (*f)();
    }
    */ 

delete p;
    return 0;
}


    则运行结果为:

A::fun1()
A::fun2()
Base::fun3()

可见,与前面的运行结果完全相同。

 

0 0
原创粉丝点击