一、虛表、虛指針、動態綁定
這一部分,我們介紹下 繼承體系下,類和對象的存儲形式。
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 Project和sgi網站上查看。
注意:為類定義 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)擴充申請量