C++學習之深入理解虛擬函式--虛擬函式表解析
前言
C++中的虛擬函式的作用主要是實現了多型的機制。關於多型,簡而言之就是用父類型別的指標指向其子類的例項,然後通過父類的指標呼叫實際子類的成員函式。這種技術可以讓父類的指標有“多種形態”,這是一種泛型技術。所謂泛型技術,說白了就是試圖使用不變的程式碼來實現可變的演算法。比如:模板技術,RTTI技術,虛擬函式技術,要麼是試圖做到在編譯時決議,要麼試圖做到執行時決議。
關於虛擬函式的使用方法,我在這裡不做過多的闡述。大家可以看看相關的C++的書籍。在這篇文章中,我只想從虛擬函式的實現機制上面為大家一個清晰的剖析。
當然,相同的文章在網上也出現過一些了,但我總感覺這些文章不是很容易閱讀,大段大段的程式碼,沒有圖片,沒有詳細的說明,沒有比較,沒有舉一反三。不利於學習和閱讀,所以這是我想寫下這篇文章的原因。也希望大家多給我提意見。
言歸正傳,讓我們一起進入虛擬函式的世界。
虛擬函式表
對C++ 瞭解的人都應該知道虛擬函式(Virtual Function)是通過一張虛擬函式表(Virtual Table)來實現的。簡稱為V-Table。在這個表中,主是要一個類的虛擬函式的地址表,這張表解決了繼承、覆蓋的問題,保證其容真實反應實際的函式。這樣,在有虛擬函式的類的例項中這個表被分配在了這個例項的記憶體中,所以,當我們用父類的指標來操作一個子類的時候,這張虛擬函式表就顯得由為重要了,它就像一個地圖一樣,指明瞭實際所應該呼叫的函式。
這裡我們著重看一下這張虛擬函式表。C++的編譯器應該是保證虛擬函式表的指標存在於物件例項中最前面的位置(這是為了保證取到虛擬函式表的有最高的效能——如果有多層繼承或是多重繼承的情況下)。
聽我扯了那麼多,我可以感覺出來你現在可能比以前更加暈頭轉向了。沒關係,下面就是實際的例子,相信聰明的你一看就明白了。
假設我們有這樣的一個類:
class Base {
public:
virtual void f() { cout << "Base::f" << endl; }
virtual void g() { cout << "Base::g" << endl; }
virtual
};
按照上面的說法,我們可以通過Base的例項來得到虛擬函式表。下面是實際例程:
typedef void(*Fun)(void);
Base b;
Fun pFun = NULL;
cout << "虛擬函式表地址:" << (int*)(&b) << endl;
/*我認為 (int*)(&b) 為虛擬函式表地址 存放的地址,因為物件記憶體放的是虛擬函式表的地址
*/
cout << "虛擬函式表 — 第一個函式地址:" << (int*)*(int*)(&b) << endl;
// Invoke the first virtual function
pFun = (Fun)*((int*)*(int*)(&b));
pFun();
實際執行經果如下:(Windows XP+VS2003, Linux 2.6.22 + GCC 4.1.3)
虛擬函式表地址:0012FED4
虛擬函式表 — 第一個函式地址:0044F148
Base::f
通過這個示例,我們可以看到,我們可以通過強行把&b轉成int *,取得虛擬函式表的地址,然後,再次取址就可以得到第一個虛擬函式的地址了,也就是Base::f(),這在上面的程式中得到了驗證(把int*強制轉成了函式指標)。通過這個示例,我們就可以知道如果要呼叫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()
這個時候你應該懂了吧。什麼?還是有點暈。也是,這樣的程式碼看著太亂了。沒問題,讓我畫個圖解釋一下。如下所示:
注意:在上面這個圖中,我在虛擬函式表的最後多加了一個結點,這是虛擬函式表的結束結點,就像字串的結束符“/0”一樣,其標誌了虛擬函式表的結束。這個結束標誌的值在不同的編譯器下是不同的。在WinXP+VS2003下,這個值是NULL。而在Ubuntu 7.10 + Linux 2.6.22 + GCC 4.1.3下,這個值是如果1,表示還有下一個虛擬函式表,如果值是0,表示是最後一個虛擬函式表。
下面,我將分別說明“無覆蓋”和“有覆蓋”時的虛擬函式表的樣子。沒有覆蓋父類的虛擬函式是毫無意義的。我之所以要講述沒有覆蓋的情況,主要目的是為了給一個對比。在比較之下,我們可以更加清楚地知道其內部的具體實現。
一般繼承(無虛擬函式覆蓋)
下面,再讓我們來看看繼承時的虛擬函式表是什麼樣的。假設有如下所示的一個繼承關係:
請注意,在這個繼承關係中,子類沒有過載任何父類的函式。那麼,在派生類的例項中,其虛擬函式表如下所示:
對於例項:Derive d; 的虛擬函式表如下:
我們可以看到下面幾點:
1)虛擬函式按照其宣告順序放於表中。
2)父類的虛擬函式在子類的虛擬函式前面。
(當然沒父類時有虛擬函式 和父類中沒虛擬函式時也會有個虛表)
我相信聰明的你一定可以參考前面的那個程式,來編寫一段程式來驗證。
一般繼承(有虛擬函式覆蓋)
覆蓋父類的虛擬函式是很顯然的事情,不然,虛擬函式就變得毫無意義。下面,我們來看一下,如果子類中有虛擬函式過載了父類的虛擬函式,會是一個什麼樣子?假設,我們有下面這樣的一個繼承關係。
為了讓大家看到被繼承過後的效果,在這個類的設計中,我只覆蓋了父類的一個函式:f()。那麼,對於派生類的例項,其虛擬函式表會是下面的一個樣子:
我們從表中可以看到下面幾點,
1)覆蓋的f()函式被放到了虛表中原來父類虛擬函式的位置。
2)沒有被覆蓋的函式依舊。
這樣,我們就可以看到對於下面這樣的程式,
Base *b = new Derive();
b->f();
由b所指的記憶體中的虛擬函式表的f()的位置已經被Derive::f()函式地址所取代,於是在實際呼叫發生時,是Derive::f()被呼叫了。這就實現了多型。
多重繼承(無虛擬函式覆蓋)
下面,再讓我們來看看多重繼承中的情況,假設有下面這樣一個類的繼承關係。注意:子類並沒有覆蓋父類的函式。
對於子類例項中的虛擬函式表,是下面這個樣子:
我們可以看到:
1) 每個父類都有自己的虛表(所以對應每個基類,子類物件中就多一個指標所佔的空間)。
2) 子類的成員函式被放到了第一個父類的表中。(所謂的第一個父類是按照宣告順序來判斷的)
這樣做就是為了解決不同的父類型別的指標指向同一個子類例項,而能夠呼叫到實際的函式。
多重繼承(有虛擬函式覆蓋)
下面我們再來看看,如果發生虛擬函式覆蓋的情況。
下圖中,我們在子類中覆蓋了父類的f()函式。
下面是對於子類例項中的虛擬函式表的圖:
我們可以看見,三個父類虛擬函式表中的f()的位置被替換成了子類的函式指標。這樣,我們就可以任一靜態型別的父類來指向子類,並呼叫子類的f()了。如:
Derive d;
Base1 *b1 = &d;
Base2 *b2 = &d;
Base3 *b3 = &d;
b1->f(); //Derive::f()
b2->f(); //Derive::f()
b3->f(); //Derive::f()
b1->g(); //Base1::g()
b2->g(); //Base2::g()
b3->g(); //Base3::g()
安全性
每次寫C++的文章,總免不了要批判一下C++。這篇文章也不例外。通過上面的講述,相信我們對虛擬函式表有一個比較細緻的瞭解了。水可載舟,亦可覆舟。下面,讓我們來看看我們可以用虛擬函式表來乾點什麼壞事吧。
一、通過父型別的指標訪問子類自己的虛擬函式
我們知道,子類沒有過載父類的虛擬函式是一件毫無意義的事情。因為多型也是要基於函式過載的。雖然在上面的圖中我們可以看到Base1的虛表中有Derive的虛擬函式,但我們根本不可能使用下面的語句來呼叫子類的自有虛擬函式:
Base1 *b1 = new Derive();
b1->f1(); //編譯出錯
任何妄圖使用父類指標想呼叫子類中的未覆蓋父類的成員函式的行為都會被編譯器視為非法,所以,這樣的程式根本無法編譯通過。但在執行時,我們可以通過指標的方式訪問虛擬函式表來達到違反C++語義的行為。(關於這方面的嘗試,通過閱讀後面附錄的程式碼,相信你可以做到這一點)
二、訪問non-public的虛擬函式
另外,如果父類的虛擬函式是private或是protected的,但這些非public的虛擬函式同樣會存在於虛擬函式表中,所以,我們同樣可以使用訪問虛擬函式表的方式來訪問這些non-public的虛擬函式,這是很容易做到的。
如:
class Base {
private:
virtual void f() { cout << "Base::f" << endl; }
};
class Derive : public Base{
};
typedef void(*Fun)(void);
void main() {
Derive d;
Fun pFun = (Fun)*((int*)*(int*)(&d)+0);
pFun();
}
結束語
C++這門語言是一門Magic的語言,對於程式設計師來說,我們似乎永遠摸不清楚這門語言揹著我們在幹了什麼。需要熟悉這門語言,我們就必需要了解C++裡面的那些東西,需要去了解C++中那些危險的東西。不然,這是一種搬起石頭砸自己腳的程式語言。
在文章束之前還是介紹一下自己吧。我從事軟體研發有十個年頭了,目前是軟體開發技術主管,技術方面,主攻Unix/C/C++,比較喜歡網路上的技術,比如分散式計算,網格計算,P2P,Ajax等一切和網際網路相關的東西。管理方面比較擅長於團隊建設,技術趨勢分析,專案管理。歡迎大家和我交流,我的MSN和Email是:[email protected]
附錄一:VC中檢視虛擬函式表
我們可以在VC的IDE環境中的Debug狀態下展開類的例項就可以看到虛擬函式表了(並不是很完整的)
附錄 二:例程
下面是一個關於多重繼承的虛擬函式表訪問的例程:
#include <iostream>
using namespace std;
class Base1 {
public:
virtual void f() { cout << "Base1::f" << endl; }
virtual void g() { cout << "Base1::g" << endl; }
virtual void h() { cout << "Base1::h" << endl; }
};
class Base2 {
public:
virtual void f() { cout << "Base2::f" << endl; }
virtual void g() { cout << "Base2::g" << endl; }
virtual void h() { cout << "Base2::h" << endl; }
};
class Base3 {
public:
virtual void f() { cout << "Base3::f" << endl; }
virtual void g() { cout << "Base3::g" << endl; }
virtual void h() { cout << "Base3::h" << endl; }
};
class Derive : public Base1, public Base2, public Base3 {
public:
virtual void f() { cout << "Derive::f" << endl; }
virtual void g1() { cout << "Derive::g1" << endl; }
};
typedef void(*Fun)(void);
int main()
{
Fun pFun = NULL;
Derive d;
int** pVtab = (int**)&d;
//Base1's vtable
//pFun = (Fun)*((int*)*(int*)((int*)&d+0)+0);
pFun = (Fun)pVtab[0][0];
pFun();
//pFun = (Fun)*((int*)*(int*)((int*)&d+0)+1);
pFun = (Fun)pVtab[0][1];
pFun();
//pFun = (Fun)*((int*)*(int*)((int*)&d+0)+2);
pFun = (Fun)pVtab[0][2];
pFun();
//Derive's vtable
//pFun = (Fun)*((int*)*(int*)((int*)&d+0)+3);
pFun = (Fun)pVtab[0][3];
pFun();
//The tail of the vtable
pFun = (Fun)pVtab[0][4];
cout<<pFun<<endl;
//Base2's vtable
//pFun = (Fun)*((int*)*(int*)((int*)&d+1)+0);
pFun = (Fun)pVtab[1][0];
pFun();
//pFun = (Fun)*((int*)*(int*)((int*)&d+1)+1);
pFun = (Fun)pVtab[1][1];
pFun();
pFun = (Fun)pVtab[1][2];
pFun();
//The tail of the vtable
pFun = (Fun)pVtab[1][3];
cout<<pFun<<endl;
//Base3's vtable
//pFun = (Fun)*((int*)*(int*)((int*)&d+1)+0);
pFun = (Fun)pVtab[2][0];
pFun();
//pFun = (Fun)*((int*)*(int*)((int*)&d+1)+1);
pFun = (Fun)pVtab[2][1];
pFun();
pFun = (Fun)pVtab[2][2];
pFun();
//The tail of the vtable
pFun = (Fun)pVtab[2][3];
cout<<pFun<<endl;
return 0;
}
參考:
相關推薦
C++學習之深入理解虛擬函式--虛擬函式表解析
前言 C++中的虛擬函式的作用主要是實現了多型的機制。關於多型,簡而言之就是用父類型別的指標指向其子類的例項,然後通過父類的指標呼叫實際子類的成員函式。這種技術可以讓父類的指標有“多種形態”,這是一種泛型技術。所謂泛型技術,說白了就是試圖使用不變的程式碼來實現可變的演算
C++學習之多型篇(虛擬函式和虛解構函式的實現原理--虛擬函式表)
通過下面的程式碼來說明: #include <iostream> #include <stdlib.h> #include <string> using namespace std; /** * 定義動物類:Animal * 成員
c++學習之路:2.預設引數&函式過載&堆記憶體分配
預設引數 規則:程式從右向左延伸讀取 例子:如下sortarr函式,在創造函式的時候可以直接賦值,這樣執行的時候就執行預設值。 又如debug函式,不傳參就會列印------------------; 函式過載 理解:幾個同名函式,所設有的引數不一樣,就代表為
機器學習之深入理解SVM
在瀏覽本篇部落格之前,最好先檢視一下我寫的另一篇文章機器學習之初識SVM(點選可查閱哦),這樣可以更好地為了結以下內容做鋪墊! 支援向量機學習方法包括構建由簡至繁的模型:線性可分支援向量機、線性支援向量機及非線性支援向量機。當訓練資料線性可分時,通過
C++學習之如何理解*&,即指標的引用
指標大家都知道啦, 然後你需要知道指標的指標 如int **p; 將指標的指標分層理解: int *(*p);括號內的整體表示一個指向int型別的指標物件,這時*p中的p自然就是指向指標的指標了。 接下來看int *&p; 也來分層理解,表示為 int *(&
機器學習之深入理解神經網路理論基礎、BP演算法及其Python實現
人工神經網路(Artificial Neural Networks,ANN)系統是 20 世紀 40 年代後出現的。它是由眾多的神經元可調的連線權值連線而成,具有大規模並行處理、分散式信 息儲存、良
整理OD學習之深入理解訊息迴圈
今天我們一起深入探討下帶有對話方塊的訊息迴圈。先看下圖:今天我們就來研究這個圖!! 為了方便研究,我就選用看雪例項裡的一個程式。從圖上你可以看出一個主視窗,還有一個訊息框。為了程式介面的簡單,還請暫時把你看到的這個訊息框當成對話方塊。這樣一來你所看到的2個元素分別就是:主
虛擬函式 virtual function 筆記(與繼承)----C++學習之路
1.虛擬函式的概念 在語法上,只要在成員函式前加上virtual,例如:virtual void draw() const =0; 所謂繼承其實就是繼承函式的呼叫權。子類可以呼叫父函式的函式。 但是當我們子類要去修改這個函式,則要用到虛擬函式。 2.虛擬函式的分類
C++學習筆記 (六) ---- 多型與虛擬函式
①、多型的概念 先上一個示例 #include <iostream> using namespace std; //基類People class People{ public: People(char *name, int age); void display(
讀書筆記之深入理解Java虛擬機器
深入理解Java虛擬機器 2.2 執行時資料區域 2.2.1.程式計數器 可以看做是當前執行緒所執行的位元組碼的 行號指示器。 每個執行緒都需要一個 獨立的程式計數器。(這類記憶體區域為"執行緒私有"的記憶體
C++學習之建構函式的理解--模板形式的建構函式不會遮掩編譯器生成的預設建構函式
我們都知道編譯器會為我們定義預設的空引數建構函式和複製建構函式,其實複製建構函式和一般的帶引數的建構函式的區別也就僅僅在與此(我認為),也就是在不顯示定義複製建構函式的時候,系統會為我們合成一個預設的複製建構函式。 1.定義一般的建構函式時,系統還是會為我們生成一個預設的複
OpenStack實踐系列⑦深入理解neutron和虛擬機
ice 賬號 由器 1.0 鏡像 多租戶 ridge 不同的 img OpenStack實踐系列⑦深入理解neutron和虛擬機 五、深入理解Neutron 5.1 虛擬機網卡和網橋 [[email protected]/* */ ~]# ifconfig
c++學習之路:2.預設引數&函式過載&堆記憶體分配
預設引數 規則:程式從右向左延伸讀取 例子:如下sortarr函式,在創造函式的時候可以直接賦值,這樣執行的時候就執行預設值。 又如debug函式,不傳參就會列印------------------; 函式過載 理解:幾個同名函式,所設有的引數不一樣,就代表為不同函式。 所以傳參的時候
《深入理解NGINX 模組開發與架構解析》之摘抄學習
1.基於Nginx框架開發程式有5個優勢: (1).Nginx將網路、磁碟及定時器等非同步事件的驅動都做了非常好的封裝,基於它開發將可以忽略這些事件處理的細節; (2).Nginx封裝了許多平臺無關的介面、容器,適用於跨平臺開發。
c++ 入門之深入探討拷貝函式和賦值運算子
在c++入門之深入探討類的一些行為時,說明了拷貝函式即複製建構函式運用於如下場景: 物件作為函式的引數,以值傳遞的方式傳給函式。 物件作為函式的返回值,以值的方式從函式返回 使用一個物件給另一個物件初始化 針對上述的三種情況,實際上很多時候,我們都會用到;如果我們採用系統預設的拷貝函式
C++學習之路(47)---C++類模板與模板類深入詳解
1、在c++的Template中很多地方都用到了typename與class這兩個關鍵字,而且有時候二者可以替換,那麼是不是這兩個關鍵字完全一樣呢? 事實上class用於定義類,在模板引入c++後,最初定義模板的方法為:template<class T>,這裡cl
C++學習之分支語句和邏輯運算子(if語句、邏輯表示式、字元函式庫(cctype)和?:運算子)
1.當C++程式必須決定是否執行某個操作時,通常使用if語句來實現操作。if有兩種格式:if和if else. if(test-condition) statement 如果測試條件為true,則if語句將載入程式執行語句或語句塊;如果條件是false,程式將跳
C++學習之對多型的理解
最近學習C++多型及子類記憶體結構,有一些理解與看法,記錄下來 1.多型產生,虛擬函式,虛擬函式指標,虛擬函式表 這一部分不詳細描述,個人參考的書籍是Siddhartha Rao的<21天學通C++>的第11章:多型 瞭解了編譯器利用虛擬函式表與物件的虛擬函式指標來實現多型的
菜鳥的C#學習之旅——C#方法過載與函式過載
目錄 過載 方法過載 函式過載 總結 過載 過載,簡單說,就是函式或者方法有相同的名稱,但是引數列表不相同的情形,這樣的同名不同引數的函式或者方法之間,互相稱之為過載函式或者方法。 過載的
C語言之最好理解的通過函式指標作為引數實現回撥函式
1、函式指標回撥解釋 回撥函式就是一個通過函式指標呼叫的函式。如果你把函式的指標(地址)作為引數傳遞給另一個函式,當這個指標被用來呼叫其所指向的函式時,我們就說這是回撥函式 2、程式碼實現 #i