C++中虛擬函式工作原理
C++中的虛擬函式的作用主要是實現了多型的機制。關於多型,簡而言之就是用父類型別的指標指向其子類的例項,然後通過父類的指標呼叫實際子類的成員函式。
所謂泛型技術,比如:模板技術,RTTI技術,虛擬函式技術,要麼是試圖做到在編譯時決議,要麼試圖做到執行時決議。
虛擬函式表(Virtual Table)來實現的。主是要一個類的虛擬函式的地址表,
class Base {
public:
virtual void f() { cout << "Base::f" << endl; }
virtual void g() { cout << "Base::g" << endl; }
virtual void h() { cout << "Base::h" << endl; }
};
typedef void(*Fun)(void);
Base b;
Fun pFun = NULL;
cout << "虛擬函式表地址:" << (int*)(&b) << endl;
cout << "虛擬函式表 — 第一個函式地址:" << (int*)*(int*)(&b) << endl;
pFun = (Fun)*((int*)*(int*)(&b));
pFun();
我們就可以知道如果要呼叫Base::g()和Base::h(),其程式碼如下:
(Fun)*((int*)*(int*)(&b)+0); // Base::f()
(Fun)*((int*)*(int*)(&b)+1); // Base::g()
(Fun)*((int*)*(int*)(&b)+2); // Base::h()
注意:在虛擬函式表的最後多加了一個結點,這是虛擬函式表的結束結點。這個結束標誌的值在不同的編譯器下是不同的。在WinXP+VS2003下,這個值是NULL。而在Ubuntu 7.10 + Linux 2.6.22 + GCC 4.1.3
一般繼承(無虛擬函式覆蓋)
請注意,在這個繼承關係中,子類沒有過載任何父類的函式。那麼,在派生類的例項中,其虛擬函式表如下所示:對於例項:Derive d; 的虛擬函式表如下:
我們可以看到下面幾點:
1)虛擬函式按照其宣告順序放於表中。
2)父類的虛擬函式在子類的虛擬函式前面。
一般繼承(有虛擬函式覆蓋)
多重繼承(無虛擬函式覆蓋)
1) 每個父類都有自己的虛表。
2) 子類的成員函式被放到了第一個父類的表中。(所謂的第一個父類是按照宣告順序來判斷的)
多重繼承(有虛擬函式覆蓋)
安全性
一、任何用父類指標呼叫子類中的未覆蓋父類的成員函式的行為都會被編譯器視為非法。但在執行時,我們可以通過指標的方式訪問虛擬函式表來達到違反C++語義的行為。
Base1 *b1 = new Derive();
b1->f1(); //編譯出錯
二、訪問non-public的虛擬函式
如果父類的虛擬函式是private或是protected的,但這些非public的虛擬函式同樣會存在於虛擬函式表中,所以,我們同樣可以使用訪問虛擬函式表的方式來訪問這些non-public的虛擬函式。
Fun pFun = (Fun)*((int*)*(int*)(&d)+0);
pFun();
我們可以在VC的IDE環境中的Debug狀態下展開類的例項就可以看到虛擬函式表了(並不是很完整的)
虛擬函式的缺點
虛擬函式最主要的缺點是執行效率較低,另外就是由於要攜帶額外的資訊(VPTR),所以導致類佔的記憶體空間也會比較大,物件也是一樣的。
class A {
private:
int a;
int b;
public:
virtual void fun0() { cout<<"A::fun0"<<endl; }
};
虛繼承
對於虛繼承,若派生類有自己的虛擬函式,則它本身需要有一個虛指標,指向自己的虛表。另外,派生類虛繼承父類時,首先要通過加入一個虛指標來指向父類,因此有兩個虛指標。計算一個類物件的大小時的規律: 1、空類、單一繼承的空類、多重繼承的空類所佔空間大小為:1(位元組,下同); 2、虛擬函式本身、成員函式(包括靜態與非靜態)和靜態資料成員都是不佔用類物件的儲存空間的; 3、因此一個物件的大小≥所有非靜態成員大小的總和; 4、當類中聲明瞭虛擬函式,那麼在例項化物件時,編譯器會自動在物件裡安插一個指標vfPtr指向虛擬函式表vfTable; 5、虛承繼的情況:由於涉及到虛擬函式表和虛基表,會同時增加一個(多重虛繼承下對應多個)vfPtr指標指向虛擬函式表vfTable和一個vbPtr指標指向虛基表vbTable,這兩者所佔的空間大小為:8(或8乘以多繼承時父類的個數); 7、類物件的大小=各非靜態資料成員(包括父類的非靜態資料成員)的總和+ vfptr指標(多繼承下可能不止一個)+vbptr指標(多繼承下可能不止一個)+編譯器額外增加的位元組。class A { }; sizeof(A)<<endl; //result=1
class B { char ch; virtual void func0() { } }; sizeof(B)<<endl; //result=8
class C {
char ch1;
char ch2;
virtual void func() { }
virtual void func1() { }
}; sizeof(C)<<endl; //result=8
class D: public A, public C {
int d;
virtual void func() { }
virtual void func1() { }
}; sizeof(D)<<endl; //result=12
class E: public B, public C {
int e;
virtual void func0() { }
virtual void func1() { }
}; sizeof(E)<<endl; //result=20
求sizeof(E)的時候,首先是類B的虛擬函式地址,然後類B中的資料成員,再然後是類C的虛擬函式地址,然後類C中的資料成員,最後是類E中的資料成員e,同樣注意記憶體對齊,這樣4+4+4+4+4=20。示例二:含有虛繼承
class CommonBase { int co; };
class Base1: virtual public CommonBase {
public:
virtual void print1() { }
virtual void print2() { }
private:
int b1;
};
class Base2: virtual public CommonBase {
public:
virtual void dump1() { }
virtual void dump2() { }
private:
int b2;
};
class Derived: public Base1, public Base2 {
public:
void print2() { }
void dump2() { }
private:
int d;
};
sizeof(Derived)=32,其在記憶體中分佈的情況如下:
class Derived size(32):
+---
| +--- (base class Base1)
| | {vfptr}
| | {vbptr}
| | b1
| +---
| +--- (base class Base2)
| | {vfptr}
| | {vbptr}
| | b2
| +---
| d
+---
+--- (virtual base CommonBase)
| co
+---
示例3:
class A {
public:
virtual void aa() { }
virtual void aa2() { }
private:
char ch[3];
}; sizeof(A)<<endl; 8
class B: virtual public A {
public:
virtual void bb() { }
virtual void bb2() { }
}; 對於虛繼承,類B因為有自己的虛擬函式,所以它本身有一個虛指標,指向自己的虛表。另外,類B虛繼承類A時,首先要通過加入一個虛指標來指向父類A,然後還要包含父類A的所有內容。因此是4+4+8=16。
不用虛擬函式實現多型
用函式指標來實現多型,這樣做的好處主要是繞過了vtable。我們都知道虛擬函式表有時候會帶來一些效能損失。
typedef void (*fVoid)();
class A {
public:
static void test() { printf("hello A\n"); }
fVoid print;
A() { print = A::test; }
};
class B : public A {
public:
static void test() { printf("hello B\n"); }
B() { print = B::test; }
};
B b;
A* a = &b;
a->print();