现在的位置: 首页 > 综合 > 正文

虚成员函数和非虚成员函数调用方式有什么不同?

2013年06月13日 ⁄ 综合 ⁄ 共 1500字 ⁄ 字号 评论关闭

非虚成员函数是静态确定的。也就是说,该成员函数(在编译时)被静态地选择,该选择基于指向对象的指针(或引用)的类型。相比而言,虚成员函数是动态确定的(在运行时)。也就是说,成员函数(在运行时)被动态地选择,该选择基于对象的类型,而不是指向该对象的指针/引用的类型。这被称作“动态绑定/动态联编”。大多数的编译器使用以下的一些的技术,也就是所谓的“VTABLE”机制:编译器发现一个类中有被声明为virtual的函数,就会为其搞一个虚函数表,也就是VTABLE。VTABLE实际上是一个函数指针的数组,每个虚函数占用这个数组的一个slot。一个类只有一个VTABLE,不管它有多少个实例。派生类有自己的VTABLE,但是派生类的VTABLE与基类的VTABLE有相同的函数排列顺序,同名的虚函数被放在两个数组的相同位置上。在创建类实例的时候,编译器还会在每个实例的内存布局中增加一个vptr字段,该字段指向本类的VTABLE。通过这些手段,编译器在看到一个虚函数调用的时候,就会将这个调用改写,在分发一个虚函数时,运行时系统跟随对象的vptr找到类的vtbl,然后跟随vtbl中适当的项找到方法的代码。
    以上技术的空间开销是存在的:每个对象一个额外的指针(仅仅对于需要动态绑定的对象),加上每个方法一个额外的指针(仅仅对于虚方法)。时间开销也是有的:和普通函数调用比较,虚函数调用需要两个额外的步骤(得到vptr的值,得到方法的地址)。由于编译器在编译时就通过指针类型解决了非虚函数的调用,所以这些开销不会发生在非虚函数上。
    下面代码演示了如何通过获取虚函数指针来调用虚函数的例子:
class Base
{
int a;
public:
virtual void fun1() {cout<<"Base::fun1()"<<endl;}
virtual void fun2() {cout<<"Base::fun2()"<<endl;}
virtual void fun3() {cout<<"Base::fun3()"<<endl;}
};

class A : public Base
{
int a;
public:
virtual void fun1() {cout<<"A::fun1()"<<endl;}
virtual void fun2() {cout<<"A::fun2()"<<endl;}
};
typedef void (*fun)();
void *getp (void* p)
{
return (void*) *(unsigned long*)p;  // 获取对象内存中的虚函数表地址,即vptr指向的内容
}

fun getfun (Base* obj, unsigned long off)
{
void *vptr = getp(obj);

unsigned char *p = (unsigned char *)vptr;
p += sizeof(void*)*off;  // 按字节数加上指针偏移,找到存储虚函数地址的内存位置

return (fun)getp(p);  // 去虚函数表中当前位置的内容,即虚函数在内存中的地址

}

int main()
{
Demo::A a;
Demo::Base *p = &a;

fun f = getfun(p, 0);
(*f)();
f = getfun(p, 1);
(*f)();
f = getfun(p, 2);
(*f)();

return 0;
}
注意:上面示例在vs2003下编译通过,其通过偏移获取虚函数表地址,进而获取虚函数地址,是基于vs下对象的内存布局是先虚函数表指针,然后成员变量的,也就是说指向虚函数表的指针被放置在对象内存的最前面。gcc下的情况有所不同,指向虚函数的指针是放在成员变量后面的。

抱歉!评论已关闭.