天天看點

虛成員函數和非虛成員函數調用方式有什麼不同?

非虛成員函數是靜态确定的。也就是說,該成員函數(在編譯時)被靜态地選擇,該選擇基于指向對象的指針(或引用)的類型。相比而言,虛成員函數是動态确定的(在運作時)。也就是說,成員函數(在運作時)被動态地選擇,該選擇基于對象的類型,而不是指向該對象的指針/引用的類型。這被稱作“動态綁定/動态聯編”。大多數的編譯器使用以下的一些的技術,也就是所謂的“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下的情況有所不同,指向虛函數的指針是放在成員變量後面的。

繼續閱讀