(GeekBand)Fourth class

一、虛表、虛指針、動態綁定

這一部分,我們介紹下 繼承體系下,類和對象的存儲形式。

1.1 vptr 虛指針 和 vtable 虛表

對于虛指針和虛表的定義,這里引用一段quora上的一個回復(這里我已經翻譯成中文): 如果一個類存在一個或多個虛函數,編譯器會為這個類的實例 (對象) 創建一個隱藏的成員變量,即虛指針(virtual-pointer),簡稱 vptr。 vptr 指向一個包含一組函數指針的表,我們稱之為 虛表 (virtual table),簡稱 vtable。虛表由編譯器創建,虛表中的每一項均是 一個指向對應虛函數的指針。

為了實現動態綁定 (dynamic binding),編譯器為每一個擁有虛函數的類 (和它的子類) 創建一張虛表。編譯器將虛函數的地址存放到對應 類的虛表中。 當通過基類指針 (或父類指針,Base * pb) 調用虛函數時,編譯器插入一段在虛表中查找虛函數地址和獲取 vptr 的代碼。 所以才能夠調用到”正確”的函數,實現動態綁定。

關于 vptr 和 vtable 的調用,這里用侯捷老師 PPT 上的一張圖表示:

關于 類 A、B、C 的結構聲明參考下面的代碼 (注意這里不包含具體實現):

// 上圖中 類 A、B、C 的聲明classA{public:virtualvoidvfunc1();virtualvoidvfunc2();voidfunc1();voidfunc2();private:intm_data1,m_data2;}classB:publicA{public:virtualvoidvfunc1();voidfunc2();private:intm_data3;}classC:publicB{public:virtualvoidvfunc1();voidfunc2();private:intm_data1,m_data4;}

1.2 this pointer (template method)

在繼承體系中,子類對象調用一個方法時,如果該類本身這個函數,那么會調用這個函數;如果本身沒有,那么編譯器會沿著繼承樹向上查找父類中是否有該方法。

侯捷老師PPT中的一張圖很好地體現了這種調用機制:

1.3 dynamic binding 動態綁定

1.3.1 什么是動態綁定?

動態綁定是編程語言的一種特性(或機制),它允許程序在運行時決定執行操作的細節,而不是在編譯時就確定。在設計一個軟件時,通常會出現下面兩類情況:

類的接口已經確定,但是還不知道具體怎么實現

開發者知道需要什么算法,但是不知道具體的操作

這兩種情況下,開發者都需要延遲決定,延遲到什么時候呢?延遲到已經有足夠的信息去做一個正確的決策。此時如果能不修改原先的實現,我們的目標就達到了。

動態綁定正是為了滿足這些需求而存在,結果就是更靈活和可伸縮的軟件架構。比如在軟件開發初期,不需要做出所有設計決策。這里我們討論下靈活性和可伸縮性:

flexibility (靈活性): 很容易將現存組件和新的配置合并到一起

extensibility (擴展性): 很容易添加新組件

C++ 通過 虛表和虛指針機制 實現對動態綁定的支持,具體的機制我們在上面已經談到,這里不再贅述。

1.3.2 動態綁定在 C++ 中的體現

在 C++ 中,動態綁定的標志是在聲明類方法時,在方法名前面顯式地添加 virtual 關鍵字。比如下面這樣:

classBase{public:virtualvoidvfunc1(){std::cout<<"Base::vfunc1()"<

只有類的成員函數才能被聲明為虛函數,下面三種是不可以的:

普通的函數 (不屬于任何一個類)

類的成員變量

靜態方法 (static 修飾的成員函數)

virtual 修飾的成員函數的接口是固定的,但是子類中的同名成員函數可以修改默認實現,比如像下面這樣:

classDerived_1{public:virtualvoidvfunc1(){std::cout<<"Derived_1::vfunc1() "<

注意:上面的代碼中, virtual 是可選的,即便不寫,它仍然是虛函數!

在程序運行時,虛函數調度機制會根據對象的”動態類型”選擇對應的成員函數。 被選擇的成員函數依賴于被指針指向的對象,而不是指針的類型。看下面代碼:

voidfoo(Base*bp){bp->vf1();/* virtual */}Baseb;Base*bp=&b;bp->vf1();// 打印 "Base::vfunc1()"Derived_1d;bp=&d;bp->vf1();// 打印 "Derived_1::vfunc1()"foo(&b);// 打印 "Base::vfunc1()"foo(&d);// 打印 "Derived_1::vfunc1()",這里存在一個隱式的向上轉型

關于動態綁定,更多細節參考C++ dynamic binding

Part 2: const 補充

這個小結中,關于 const 的所有例子均來自于msdn。為了便于理解, 對代碼進行了稍微的調整。

2.1 const 修飾指針

下面這個例子中, const 修飾的是指針,因此不能修改指針 aptr 的值,即 aptr 不能指向另一個位置。

// constant_values3.cppintmain(){char*mybuf=0,*yourbuf;char*constaptr=mybuf;*aptr='a';// OKaptr=yourbuf;// C3892}

2.2 const 修飾指針指向的數據

下面這個例子中, const 修飾的是指針指向的數據,因此可以修改指針的值,但是不能修改指針指向的數據。

// constant_values4.cpp#include intmain(){constchar*mybuf="test";char*yourbuf="test2";printf_s("%s\n",mybuf);constchar*bptr=mybuf;// Pointer to constant dataprintf_s("%s\n",bptr);// *bptr = 'a';? // Error}

2.3 const 修飾成員函數

在聲明成員函數時,如果在函數末尾使用 const 關鍵字,那么可以稱這個函數是”只讀”函數。 const成員函數不能修改任何 非static的成員變量, 也不能調用任何 非const 成員函數。

const成員函數在聲明和定義時,都必須帶有 const 關鍵字。看下面這個例子:

// constant_member_function.cppclassDate{public:Date(intmn,intdy,intyr);intgetMonth()const;// A read-only functionvoidsetMonth(intmn);// A write function; can't be constprivate:intmonth;};intDate::getMonth()const{returnmonth;// Doesn't modify anything}voidDate::setMonth(intmn){month=mn;// Modifies data member}intmain(){DateMyDate(7,4,1998);constDateBirthDate(1,18,1953);MyDate.setMonth(4);// OkayBirthDate.getMonth();// OkayBirthDate.setMonth(4);// C2662 Error}

Part 3:new 和 delete

3.1 分解 new 和 delete

new 和 delete 都是表達式,因此不能被重載。它們均有不同步驟組成:

new 的執行步驟:

調用operator new 分配內存 (malloc)

對指針進行類型轉換

調用構造函數

delete 的執行步驟:

調用析構函數

調用operator delete釋放內存 (free)

雖然,new 和 delete 不能被重載,但是 operator new 和 operator delete 可以被重載。 更多細節查看msdn 上的相關頁面。 關于重寫 operator new/delete的一些原因,參考Customized Allocators with Operator New and Operator Delete

3.2 重載 operator new 和 operator delete

3.2.1 重載全局 operator new 和 operator delete

用戶可以通過重新定義 全局 new 和 delete 操作符,以便通過日志或其它方式記錄內存的分配和釋放。 其中一個應用場景是用于檢查內存泄漏。代碼如下:

// 這段代碼來自于 msdn:https://msdn.microsoft.com/en-us/library/kftdy56f.aspx// spec1_the_operator_delete_function1.cpp

// compile with: /EHsc

// arguments: 3#include

#include

#include

#include usingnamespacestd;intfLogMemory=0;// Perform logging (0=no; nonzero=yes)?intcBlocksAllocated=0;// Count of blocks allocated.// User-defined operator new.void*operatornew(size_tstAllocateBlock){staticintfInOpNew=0;// Guard flag.if(fLogMemory&&!fInOpNew){fInOpNew=1;clog<<"Memory block "<<++cBlocksAllocated<<" allocated for "<1)for(inti=0;i

編譯并運行這段代碼,可以看到如下輸出:

oscar@ubuntu:~/$ g++ -o main spec1_the_operator_delete_function1.cpp -lm

oscar@ubuntu:~/$ ./main 3

Memory block 1 allocated for 10 bytes

Memory block 1 deallocated

Memory block 1 allocated for 10 bytes

Memory block 1 deallocated

Memory block 1 allocated for 10 bytes

Memory block 1 deallocated

故事到這里還沒有結束,細心的童鞋可能會發現:創建和釋放 char* pMem 時,使用的分別是 operator new[] (size_t) 和 operator delete[] (void*), 并沒有調用 operator new 和 operator delete。打印的結果卻告訴我:operator new 和 operator delete 確實被調用了(作驚恐狀)!!!

這里,我找到了 cpluscplus.com 上關于 operator new[] 的表述。不解釋,直接上圖:

關于重新定義 operator new[] 和 operator delete[],參考 msdn上new and delete Operators頁面最下方類成員函數 operator new[] 和 operator delete[] 的實現,它們是類似的。

3.2.2 重載類的成員函數 operator new 和 operator delete

上面我們介紹了重寫全局 operator new、operator new[]、operator delete、operator delete[] 的覆蓋 (override)。 下面我們看看 類作用域下這四個函數如何實現,應用場景以及注意事項。

在類中重寫 operator new/delete([]) 成員函數時,必須聲明它們為 static,因此不能聲明為虛函數。

下面給出一個重寫類 operator new/delete 方法的例子:

// https://msdn.microsoft.com/en-us/library/kftdy56f.aspx

// spec1_the_operator_new_function1.cpp#include

#include

#include

#include usingnamespacestd;classBlanks{public:Blanks(){}Blanks(intdummy){throw1;}staticvoid*operatornew(size_tstAllocateBlock);staticvoid*operatornew(size_tstAllocateBlock,charchInit);staticvoid*operatornew(size_tstAllocateBlock,doubledInit);staticvoidoperatordelete(void*pvMem);staticvoidoperatordelete(void*pvMem,charchInit);staticvoidoperatordelete(void*pvMem,doubledInit);};void*Blanks::operatornew(size_tstAllocateBlock){clog<<"Blanks::operator new( size_t )\n";void*pvTemp=malloc(stAllocateBlock);returnpvTemp;}void*Blanks::operatornew(size_tstAllocateBlock,charchInit){clog<<"Blanks::operator new( size_t, char )\n";// throw 20;void*pvTemp=malloc(stAllocateBlock);if(pvTemp!=0)memset(pvTemp,chInit,stAllocateBlock);returnpvTemp;}void*Blanks::operatornew(size_tstAllocateBlock,doubledInit){clog<<"Blanks::operator new( size_t, double)\n";returnmalloc(stAllocateBlock);}voidBlanks::operatordelete(void*pvMem){clog<<"Blanks::opeator delete (void*)\n";free(pvMem);}voidBlanks::operatordelete(void*pvMem,charchInit){clog<<"Blanks::opeator delete (void*, char)\n";free(pvMem);}voidBlanks::operatordelete(void*pvMem,doubledInit){clog<<"Blanks::opeator delete (void*, double)\n";free(pvMem);}// For discrete objects of type Blanks, the global operator new function

// is hidden. Therefore, the following code allocates an object of type

// Blanks and initializes it to 0xa5intmain(){Blanks*a5=new('c')Blanks;deletea5;cout<

linux運行上的代碼,結果如下:

Blanks::operator new( size_t, char )

Blanks::opeator delete (void*)

Blanks::operator new( size_t )

Blanks::opeator delete (void*)

Blanks::operator new( size_t, double)

terminate called after throwing an instance of 'int'

Aborted (core dumped)

很容易發現,不管我們使用哪個版本的 operator new,最后調用的都是 不含額外的參數的 operator delete。 構造函數拋出異常時,也沒有調用對應的 operator delete 成員函數。 那么包含額外參數的 delete什么時候會被調用到,應用場景由有哪些呢?

我們繼續找相關的文檔,msdn上有這樣一段文字:

voidoperatordelete(void*);voidoperatordelete(void*,size_t);

Only one of the preceding two forms can be present for a given class. The first form takes a single argument of type void *, which contains a pointer to the object to deallocate. The second form—sized deallocation—takes two arguments, the first of which is a pointer to the memory block to deallocate and the second of which is the number of bytes to deallocate. The return type of both forms is void (operator delete cannot return a value).

The intent of the second form is to speed up searching for the correct size category of the object to be deleted, which is often not stored near the allocation itself and likely uncached; the second form is particularly useful when an operator delete function from a base class is used to delete an object of a derived class.

這里的解釋也有些問題,通過上面的例子,可以推斷 operator new/delete 均可以被重載。 創建對象時,可以使用不同版本的operator new,但是銷毀時,只調用不包含額外參數的operator delete。 delete 的應用場景之一是:在繼承體系中,Base* 指向一個子類對象,調用 delete 銷毀該對象時,必須保證銷毀父類對象,而不是根據子類對象的大小進行截斷銷毀。

事實上,上面所說的應用場景也沒有得到驗證。我對上面的代碼進行了修改,銷毀時調用的仍然是不含額外參數的 delete:

// https://msdn.microsoft.com/en-us/library/kftdy56f.aspx

// spec1_the_operator_new_function1.cpp#include

#include

#include

#include usingnamespacestd;classBase{public:virtual~Base(){}};classBlanks:publicBase{// ...? 沒有改變 ...};intmain(){Base*a5=new('c')Blanks;// 打印 Blanks::operator new( size_t, char )deletea5;// 打印 Blanks::opeator delete (void*)}

根據侯捷老師關于 basic_string 的分析,operator delete 并沒有傳入額外的參數,而是通過 Allocator::deallocate 去刪除。 因此 重載 operator delete 沒有任何意義,需要時 重新定義 operator delete(void* p)即可。 需要查看 stl 文章和源碼的話,可以去Code Projectsgi網站上查看。

注意:為類定義 operator new/delete 成員函數會覆蓋 全局默認的 operator new/delete。 如果要使用默認的 operator new/delete,那么在創建對象和銷毀對象時,需要使用 ::new 和 ::delete。

課程目錄:

一、對象模型:vptr和vtbl


二、對象模型:關于this

三、對象模型:關于Dynamic Binding

四、談談const

五、關于new,delete

六、重載operator new,operator delete,operator new[],operator delete[]

七、示例

八、重載new(),delete()$示例

九、Basic_String使用new(extra)擴充申請量

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,443評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,530評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,407評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,981評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,759評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,204評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,263評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,415評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,955評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,650評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,892評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,675評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,967評論 2 374

推薦閱讀更多精彩內容