C++中的虛函數的作用主要是實作了多态的機制。關于多态,簡而言之就是用父類型别的指針指向其子類的執行個體,然後通過父類的指針調用實際子類的成員函數。這種技術可以讓父類的指針有“多種形态”,這是一種泛型技術。所謂泛型技術,說白了就是試圖使用不變的代碼來實作可變的算法。比如:模闆技術,RTTI技術,虛函數技術,要麼是試圖做到在編譯時決議,要麼試圖做到運作時決議。
關于虛函數的使用方法,我在這裡不做過多的闡述。大家可以看看相關的C++的書籍。在這篇文章中,我隻想從虛函數的實作機制上面為大家 一個清晰的剖析。
可以參考《C++ Primer Plus》第五版的圖。

對C++ 了解的人都應該知道虛函數(Virtual Function)是通過一張虛函數表(Virtual Table)來實作的。簡稱為V-Table。在這個表中,主是要一個類的虛函數的位址表,這張表解決了繼承、覆寫的問題,保證其容真實反應實際的函數。這樣,在有虛函數的類的執行個體中這個表被配置設定在了這個執行個體的記憶體中,是以,當我們用父類的指針來操作一個子類的時候,這張虛函數表就顯得由為重要了,它就像一個地圖一樣,指明了實際所應該調用的函數。
這裡我們着重看一下這張虛函數表。C++的編譯器應該是保證虛函數表的指針存在于對象執行個體中最前面的位置(這是為了保證取到虛函數表的有最高的性能——如果有多層繼承或是多重繼承的情況下)。 這意味着我們通過對象執行個體的位址得到這張虛函數表,然後就可以周遊其中函數指針,并調用相應的函數。
聽我扯了那麼多,我可以感覺出來你現在可能比以前更加暈頭轉向了。 沒關系,下面就是實際的例子,相信聰明的你一看就明白了。
假設我們有這樣的一個類:
1 class Base {
2 public:
3 virtual void f() { cout << "Base::f" << endl; }
4 virtual void g() { cout << "Base::g" << endl; }
5 virtual void h() { cout << "Base::h" << endl; }
6
7 };
按照上面的說法,我們可以通過Base的執行個體來得到虛函數表。 下面是實際例程:
1 typedef void(*Fun)(void);
2
3 Base b;
4
5 Fun pFun = NULL;
6
7 cout << "虛函數表位址:" << (int*)(&b) << endl;
8 cout << "虛函數表 — 第一個函數位址:" << (int*)*(int*)(&b) << endl;
9
10 // Invoke the first virtual function
11 pFun = (Fun)*((int*)*(int*)(&b));
12 pFun();
實際運作經果如下:
虛函數表位址:0012FED4
虛函數表 — 第一個函數位址:0044F148
Base::f
通過這個示例,我們可以看到,我們可以通過強行把&b轉成int *,取得虛函數表的位址,然後,再次取址就可以得到第一個虛函數的位址了,也就是Base::f(),這在上面的程式中得到了驗證(把int*強制轉成了函數指針)。通過這個示例,我們就可以知道如果要調用Base::g()和Base::h(),其代碼如下:
1 (Fun)*((int*)*(int*)(&b)+0); // Base::f()
2 (Fun)*((int*)*(int*)(&b)+1); // Base::g()
3 (Fun)*((int*)*(int*)(&b)+2); // Base::h()
這個時候你應該懂了吧。什麼?還是有點暈。也是,這樣的代碼看着太亂了。沒問題,讓我畫個圖解釋一下。如下所示:
注意:在上面這個圖中,我在虛函數表的最後多加了一個結點,這是虛函數表的結束結點,就像字元串的結束符“/0”一樣,其标志了虛函數表的結束。這個結束标志的值在不同的編譯器下是不同的。在WinXP+VS2003下,這個值是NULL。而在Ubuntu 7.10 + Linux 2.6.22 + GCC 4.1.3下,這個值是如果1,表示還有下一個虛函數表,如果值是0,表示是最後一個虛函數表。
下面我們打開編譯器,輸入一段代碼
1 #include <iostream>
2 using namespace std;
3
4 typedef void(*Fun)(void);
5
6 class Scientist
7 {
8 private:
9 char name[40];
10 char sex;
11 public:
12 Scientist(const char *n="none", const char se='n');
13 void showname();
14 virtual void show_all();
15 virtual ~Scientist() {}
16 };
17 class Physicist : public Scientist
18 {
19 private:
20 char field[40];
21 public:
22 Physicist(const char *n="none", const char se='n', const char *f="none");
23 void show_all();
24 void show_field();
25 };
26
27 Scientist::Scientist(const char *n, const char se)
28 {
29 cout<<"Call Scientist constructor"<<endl;
30 strncpy(name, n, 40-1);
31 name[40-1]='\0';
32 sex = se;
33 }
34 void Scientist::showname()
35 {
36 cout<<"Call Scientist showname."<<endl;
37 cout<<"Scientist name: "<<name<<endl;
38 }
39 void Scientist::show_all()
40 {
41 cout<<"Call Scientist show_all."<<endl;
42 cout<<"Scientist name: "<<name<<endl;
43 cout<<"Scientist Sex: "<<sex<<endl;
44 }
45 Physicist::Physicist(const char *n, const char se, const char *f):Scientist(n,se)
46 {
47 cout<<"Call Physicist constructor"<<endl;
48 strncpy(field, f, 40-1);
49 field[40-1]='\0';
50 }
51 void Physicist::show_all()
52 {
53 cout<<"Call Physicist show_all."<<endl;
54 Scientist::show_all();
55 cout<<"field: "<<field<<endl;
56 }
57 void Physicist::show_field()
58 {
59 cout<<"Call Physicist show_field."<<endl;
60 cout<<"Physicist field: "<<field<<endl;
61 }
62 int main()
63 {
64 Physicist adam("Adam Crusher", 'M', "nuclear structure");
65 Scientist *psc = &adam;
66 psc->show_all();
67
68 cout<<endl;
69 Fun pFun = NULL;
70
71 cout<<endl;
72 cout << "虛函數表位址:" << (int*)(&adam) << endl;
73 cout << "虛函數表 — 第一個函數位址:" << (int*)*(int*)*(int*)(&adam)<< endl;
74 cout << "虛函數表 — 第二個函數位址:" << (int*)*((int*)*(int*)(&adam)+1)<< endl;
75 pFun = (Fun)*((int*)*(int*)(&adam));
76 pFun();
77 return 0;
78 }
運作結果:
分析:
程式首先定義一個子類實體學家對象adma,然後再定義一個科學家類型指針psc指向adma,然後通過psc調用show_all()函數。大家都知道,如果show_all()為非虛函數,那麼編譯器将采用靜态聯編編譯,即根據指針類型選擇方法,本程式中将調用Scientist::show_all()。如果show_all()函數使用了virtual,程式采用動态聯編,程式根據指針指向的對象的類型來選擇方法,此例中調用Physicist::show_all()。根據運作結果,程式成功調用了hysicist::show_all()。
下面開始讨論adma對象中的虛函數表。給出我畫的圖:
adam是對象,和int double聲明的變量一樣,必然有位址,其位址為0x0012FF1C。那麼由于上文說了虛函數表(指向函數指針的數組),虛函數表的指針存在于對象執行個體中最前面的位置。那麼*該位址0x0012FF1C的值就應該是虛函數表的位址0x0046F0D0,把該位址當做一個數組名,那麼*該位址,為第一個元素的值,即指向Physicist::show_all()函數的指針0x004012BC。
上面的對嗎?其實我也不知道,看看調試結果,至少讓我們眼睛直覺的相信。
adma對象位址0x0012FF1C,_vfptr位址0x0046F0D0,Physicist::show_all()位址,Physicist::析構函數位址0x004011A4。
從記憶體中看0x0012FF1C的第一個元素确實為虛函數表的位址0x0046F0D0,然後下面是name數組的内容......
虛函數表中的内容,也如願所長的為0x004012BC,0x004011A4。
繼續分析程式,&adma為取adma對象的位址,把它轉化為int*類型輸出(不是必須的)。
按理說&adma為取adma對象的位址,那麼再對&adma取*,應該是其第一個元素的值,但能這樣寫嗎?*(&adma)很明顯不行,編譯器解釋為*&adma,* &抵消。
藍色字段上文用*(int*)(&adam)。但這裡有個問題,上文代碼中标明是“虛函數表 — 第一個函數位址”,其實這個應該是虛函數表的位址,詳見上圖。
(int*)*(int*)(&adam)為第一個函數Physicist::show_all的位址,那麼*((int*)*(int*)(&adam))為Physicist::show_all函數本身,用函數指針pFun指向它。運作pFun,即相當于運作Physicist::show_all。
Call Physicist show_all.
Call Scientist show_all.
Scientist name:
Scientist Sex:
咦?這裡的值怎麼都變成空的了,難道沒有this指針了,編譯器又不知道哪個對象了嗎?
下面看修改的代碼:
1 cout<<endl;
2 cout << "虛函數表位址:" << (int*)(&adam) << endl;
3 cout << "虛函數表 — 第一個函數位址:" << (int*)*(int*)*(int*)(&adam)<< endl;
4 cout << "虛函數表 — 第二個函數位址:" << (int*)*((int*)*(int*)(&adam)+1)<< endl;
5 pFun = (Fun)*((int*)*(int*)(&adam));
6 pFun();
"虛函數表位址:" << (int*)(&adam) 不變。