天天看點

(C/C++學習)4.C++類中的虛函數表Virtual Table

說明:C++的多态是通過一張虛函數表(Virtual Table)來實作的,簡稱為V-Table。在這個表中,主要為一個類的虛函數的位址表,這張表解決了繼承、覆寫的問題,保證其真實反應實際的虛函數調用過程。這樣,在有虛函數的類的執行個體中這個表被配置設定在了這個執行個體的記憶體中,是以,當我們用父類的指針來操作一個子類的時候,這張虛函數表就顯得尤為重要了,它就像一個地圖一樣,指明了實際所應該調用的函數。

下面介紹一下與這張虛函數表有關的幾個問題:

1.普通成員函數不占存儲空間,而所有虛函數入口位址存儲在一張虛函數表中,由一個指針指向該虛函數表;

2.指向該虛函數表的指針位于類執行個體對象記憶體的最前面,占四個位元組;

3.若子類覆寫了父類的虛函數,則父類的虛函數被覆寫,即虛函數表中隻存在子類的虛函數位址;否則,父類和子類的虛函數都存在于虛函數表中(當然,沒有覆寫父類的虛函數是毫無意義的),這就是多态形成的原因。

通過上面的介紹,我們對虛函數表有了大緻的了解,下面通過一個執行個體來加深一下認識:

1 #include <iostream>
  2 using namespace std;
  3 
  4 class base
  5 {
  6 public:
  7     virtual void f(){cout<<"base::f()"<<endl;}
  8     virtual void g(){cout<<"base::g()"<<endl;}
  9     virtual void h(){cout<<"base::h()"<<endl;}
 10 private:
 11     int a;
 12 };
 13 
 14 //定義一個函數指針,并别名為pfunc,用時不需再加*,
 15 typedef void (*pfunc)(void);
 16 
 17 int main()
 18 {
 19     base b;
 20 
 21     //C++編譯器使虛函數表的指針存在于對象執行個體中的最前面(四個位元組)
 22     cout<<"sizeof(base) = "<<sizeof(base)<<\'\t\'<<"sizeof(b) = "<<sizeof(b)<<endl<<\'\n\';
 23 
 24     //分别列印對象b的起始位址和虛函數表中首個函數指針指向的位址
 25     //對象執行個體最前面的四個位元組為指向虛函數表的指針,取内容後才為虛函數表
 26     cout<<"&b = "<<&b<<"\t\t"<<"&VTable = "<<(int **)*(int *)(&b)<<endl<<"\n\n";
 27 
 28     pfunc pf;
 29     //定義一個函數指針
 30     void(*p)(void);
 31     //還可以這樣定義一個函數指針
 32 
 33     //虛函數表裡面存放的是指向各個虛函數的指針,取内容後才是各個相應的虛函數
 34     pf = (pfunc)*((int **)*(int *)(&b)+0);
 35     pf();
 36     pf = (pfunc)*((int **)*(int *)(&b)+1);
 37     pf();
 38     pf = (void(*)())*((int **)*(int *)(&b)+2);
 39     pf();
 40 
 41     cout<<"\n\n";
 42 
 43     p = (pfunc)*((int **)*(int *)(&b)+0);
 44     p();
 45     p = (void(*)())*((int **)*(int *)(&b)+1);
 46     p();
 47     p = (void(*)())*((int **)*(int *)(&b)+2);
 48     p();
 49 
 50     return 0;
 51 }
 52       

程式運作結果:

(C/C++學習)4.C++類中的虛函數表Virtual Table

通過以上示例,我們把類執行個體對象b取址,然後将&b強轉成int*型,然後對其取内容,取得虛函數表的位址,然後再對其取内容,就得到了第一個虛函數的位址了,然後再将其通過(int**)強轉成步長為4的指針,通過加1來得到虛函數表中不同的虛函數的位址,最終強轉成為函數指針,再通過該函數指針通路相應的虛函數.

5.下面我們将通過幾個例子來解釋一下虛函數表的存在形式,在這部分,主要弄清楚虛函數表是怎麼一回事,至于程式運作結果,讀者自行實驗。

a.在父子類中,若子類沒有對父類的虛函數進行覆寫(當然,前面提到過,沒有覆寫父類的虛函數是毫無意義的。之是以要講述沒有覆寫的情況,主要目的是為了給一個對比,在比較之下,我們可以更加清楚地知道其内部的具體實作),如下代碼,

1 #include<iostream>
  2 using namespace std;
  3 class base
  4 {
  5 public:
  6     virtual void func(){};
  7     virtual void foo(){};
  8 };
  9 class derive:public base
 10 {
public:
 11     virtual void func1(){};
 12     virtual void foo1(){};
 13 };
 14 int main()
 15 {
 16     derive d;
 17     return 0;
 18 }
 19       

則其虛函數表如下所示:

(C/C++學習)4.C++類中的虛函數表Virtual Table

注意:

1.上面這個圖中,我在虛函數表的最後多加了一個結點,這是虛函數表的結束結點,就像字元串的結束符“/0”一樣,其标志了虛函數表的結束。這個結束标志的值在不同的編譯器下是不同的。

2.虛函數是按照其聲明順序放于表中的。

3.父類的虛函數在子類的虛函數前面。

b.在父子類中,若子類對父類的虛函數進行了覆寫(為了對比,假設隻覆寫父類一個虛函數),如下代碼,

1 #include<iostream>
  2 using namespace std;
  3 class base
  4 {
  5 public:
  6     virtual void func(){};
  7     virtual void foo(){};
  8     virtual  ~base(){}
  9 };
 10 class derive:public base
 11 {
 12 public:
 13     virtual void func(){cout<<"___"<<endl;};
 14     virtual void foo1(){};
 15     virtual  ~derive(){}
 16 };
 17 int main()
 18 {
 19     base *p = new derive;
 20     p->func();
 21     delete p;
 22     return 0;
 23 }
 24       

則其虛函數表如下所示:

(C/C++學習)4.C++類中的虛函數表Virtual Table

由此,可得覆寫的子類func()放在了虛函數表中原來父類func()的位置,沒有覆寫的虛函數依舊原樣存放。這樣,在上述代碼中,由于p所指的func()的位置已經被derive::func()的函數位址所取代,是以在發生實際調用的時候,調用的是子類的func(),這就實作了多态。