查看虛函數表


如果你看到這篇文章時,急着去吃飯或泡MM,請跳轉到藍色字段開始閱讀。

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)  不變。

*(int*)(&adam)為對象第一個元素的值,即虛函數表的地址。用(int*)*(int*)(&adam)輸出0x0046F0D0。

*(int*)*(int*)(&adam)為虛函數表數組中第一個元素的值,用(int*)*(int*)*(int*)(&adam)輸出。依次類推cout << "虛函數表 — 第二個函數地址:" << (int*)*((int*)*(int*)(&adam)+1)<< endl;

文章藍色字段以上參考:http://blog.csdn.net/haoel/article/details/1948051/


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM