1. 對(duì)象模型(Object Model)
1.1 關(guān)于vptr(虛指針)和vtbl(虛表)
只要有虛函數(shù)(無論多少個(gè))宽气,都會(huì)多出一個(gè)虛指針(指向虛表)意狠。
某個(gè)虛函數(shù)在C中的實(shí)現(xiàn)形式如圖最下端紅字所示局义。其中n代表所調(diào)用的虛函數(shù)排在虛表中的位置(0,1,...)
右下角:為了能存放不同類型惊来,必須為指向父類的指針蝗罗。
最下端:編譯器虛機(jī)制(動(dòng)態(tài)綁定)實(shí)現(xiàn)的3個(gè)條件:
- 通過指針
- 指針向上轉(zhuǎn)型(指向子類對(duì)象)
- 調(diào)用的是虛函數(shù)
1.2 關(guān)于this
在C++中夹纫,所以的成員函數(shù)都有一個(gè)隱藏的第一參數(shù)—— this pointer
所以myDoc在調(diào)用從父類繼承而來的OnFileOpen函數(shù)時(shí)栖忠,隱藏的第一參數(shù)為&myDoc崔挖,即指向自己的 this 指針贸街,因此OnFileOpen函數(shù)中的Serialize函數(shù)在調(diào)用時(shí)形式為:this->Serialize。因其滿足動(dòng)態(tài)綁定(Dynamic Binding)的3個(gè)條件狸相,編譯為如下形式:(*(this->vptr)[n])(this)薛匪,調(diào)用的是子類CMyDoc重新定義的虛函數(shù)Serialize()。然后再繼續(xù)執(zhí)行父類OnFileOpen()函數(shù)余下的語句脓鹃。整個(gè)過程的執(zhí)行順序如圖中箭頭所示逸尖。
1.3 關(guān)于Dynamic Binding(動(dòng)態(tài)綁定)
右圖展示的是匯編語言里左圖中語句的具體實(shí)現(xiàn)過程,調(diào)用a.vfunc1()時(shí)瘸右, call 的是固定的地址(004011a9)娇跟,為靜態(tài)綁定。
此圖展示的是匯編語言里動(dòng)態(tài)綁定的實(shí)現(xiàn)過程太颤,等價(jià)于右圖中的語句苞俘。
2. 談?wù)?const
上圖為const成員函數(shù)(const的作用:說明其不會(huì)修改數(shù)據(jù)成員),任何不會(huì)修改數(shù)據(jù)成員的函數(shù)都應(yīng)該聲明為const 類型龄章。如果在編寫const成員函數(shù)時(shí)吃谣,不慎修改了數(shù)據(jù)成員,或者調(diào)用了其它非const成員函數(shù)做裙,編譯器將指出錯(cuò)誤岗憋,這無疑會(huì)提高程序的健壯性。
表中為基本規(guī)則锚贱,表上為特殊規(guī)則仔戈。
圖右框中,const 是函數(shù)簽名的一部分拧廊,所以這兩個(gè)操作符重載可以并存杂穷。
字符串 string 的設(shè)計(jì)運(yùn)用了 reference counting(引用計(jì)數(shù)設(shè)計(jì)模式):同樣的內(nèi)容,多個(gè)對(duì)象共享卦绣;如果某個(gè)對(duì)象要改內(nèi)容耐量,就單獨(dú)拷貝一份讓它改。
3. 控制內(nèi)存分配[1]
某些應(yīng)用程序?qū)?nèi)存分配有特殊的需求滤港,因此我們無法將標(biāo)準(zhǔn)內(nèi)存管理機(jī)制直接應(yīng)用于這些程序廊蜒。它們常常需要自定義內(nèi)存分配的細(xì)節(jié),比如使用關(guān)鍵字 new 將對(duì)象放置在特定的內(nèi)存空間中溅漾。為了實(shí)現(xiàn)這一目的山叮,應(yīng)用程序需要重載 new 運(yùn)算符和 delete 運(yùn)算符以控制內(nèi)存分配的過程。
3.1 重載 new 和 delete[1]
盡管我們說能夠“重載 new 和 delete”添履,但是實(shí)際上重載這兩個(gè)運(yùn)算符與重載其他運(yùn)算符的過程大不相同屁倔。要想真正掌握重載 new 和 delete 的方法,首先要對(duì) new 表達(dá)式和 delete 表達(dá)式的工作機(jī)理有更多了解暮胧。
當(dāng)我們使用一條 new 表達(dá)式時(shí):
string *sp = new string ("a value"); //分配并初始化一個(gè) string 對(duì)象
string *arr = new string[10]; //分配10個(gè)默認(rèn)初始化的 string 對(duì)象
實(shí)際執(zhí)行了三步操作:
- new 表達(dá)式調(diào)用了一個(gè)名為 operator new(或者 operator new[])的標(biāo)準(zhǔn)庫函數(shù)锐借。該函數(shù)分配一塊足夠大的问麸、原始的、未命名的內(nèi)存空間以便存儲(chǔ)特定類型的對(duì)象(或者對(duì)象的數(shù)組)钞翔。
- 編譯器運(yùn)行相應(yīng)的構(gòu)造函數(shù)以構(gòu)造這些對(duì)象严卖,并為其傳入初始值。
- 對(duì)象被分配了空間并構(gòu)造完成布轿,返回一個(gè)指向該對(duì)象的指針哮笆。
當(dāng)我們使用一條 delete 表達(dá)式刪除一個(gè)動(dòng)態(tài)分配的對(duì)象時(shí):
delete sp; //銷毀*sp,然后釋放 sp 指向的內(nèi)存空間
delete [] arr; //銷毀數(shù)組中的元素汰扭,然后釋放對(duì)應(yīng)的內(nèi)存空間
實(shí)際執(zhí)行了兩步操作:
- 對(duì) sp 所指的對(duì)象或者 arr 所指的數(shù)組中的元素執(zhí)行對(duì)應(yīng)的析構(gòu)函數(shù)稠肘。
- 編譯器調(diào)用名為 operator delete(或者 operator delete[])的標(biāo)準(zhǔn)庫函數(shù)釋放內(nèi)存空間。
拓展:關(guān)于 delete 和 delete[] 的區(qū)別萝毛,可以參見這篇博文:delete 和 delete []的真正區(qū)別
如果應(yīng)用程序希望控制內(nèi)存分配的過程启具,則它們需要定義自己的 operator new 函數(shù)和 operator delete 函數(shù)。即使在標(biāo)準(zhǔn)庫中已經(jīng)存在這兩個(gè)函數(shù)的定義珊泳,我們?nèi)耘f可以定義自己的版本鲁冯。編譯器不會(huì)對(duì)這種重復(fù)的定義提出異議,相反色查,編譯器將使用我們自定義的版本替換標(biāo)準(zhǔn)庫定義的版本薯演。
注意:當(dāng)自定義了全局的 operator new 函數(shù)和 operator delete 函數(shù)后,我們就擔(dān)負(fù)起了控制內(nèi)存分配的職責(zé)秧了。這兩個(gè)函數(shù)必須是正確的:因?yàn)樗鼈兪浅绦蛘麄€(gè)處理過程中至關(guān)重要的一部分跨扮。
應(yīng)用程序可以在全局作用域中定義 operator new 函數(shù)和 operator delete 函數(shù),也可以將它們定義為成員函數(shù)验毡。當(dāng)編譯器發(fā)現(xiàn)一條 new 表達(dá)式或 delete 表達(dá)式后衡创,將在程序中查找可供調(diào)用的 operator 函數(shù)。如果被分配(釋放)的對(duì)象是類類型晶通,則編譯器首先在類及其基類的作用域中查找璃氢。此時(shí)如果該類含有 operator new 成員或者 operator delete 成員,則相應(yīng)的表達(dá)式將調(diào)用這些成員狮辽。否則一也,編譯器在全局作用域查找匹配的函數(shù)。此時(shí)如果編譯器找到了用戶自定義的版本喉脖,則使用該版本執(zhí)行 new 或者 delete 椰苟;如果沒找到,則使用標(biāo)準(zhǔn)庫定義的版本树叽。
我們可以使用作用域運(yùn)算符令 new 表達(dá)式或 delete 表達(dá)式忽略定義在類中的函數(shù)舆蝴,直接執(zhí)行全局作用域中的版本。例如, ::new 只在全局作用域中查找匹配的 operator new 函數(shù)洁仗, ::delete 與之類似层皱。
operator new 接口和 operator delete 接口
標(biāo)準(zhǔn)庫定義了 operator new 函數(shù)和 operator delete 函數(shù)的8個(gè)重載版本。其中前4個(gè)版本可能拋出 bad_alloc 異常京痢,后4個(gè)版本則不會(huì)拋出異常:
// 這些版本可能拋出異常
void* operator new(size_t); //分配一個(gè)對(duì)象
void* operator new[](size_t); //分配一個(gè)數(shù)組
void* operator delete(void*) noexcept; //釋放一個(gè)對(duì)象
void* operator delete[](void*) noexcept; //釋放一個(gè)數(shù)組
//這些版本承諾不會(huì)拋出異常
void* operator new(size_t, nothrow_t&) noexcept;
void* operator new[](size_t, nothrow_t&) noexcept;
void* operator delete(void*, nothrow_t&) noexcept;
void* operator delete[](void*, nothrow_t&) noexcept;
類型 nothrow_t 是定義在 new 頭文件中的一個(gè) struct,在這個(gè)類型中不包含任何成員篷店。new 頭文件還定義了一個(gè)名為 nothrow 的 const 對(duì)象祭椰,用戶可以通過這個(gè)對(duì)象請(qǐng)求 new 的非拋出版本。與析構(gòu)函數(shù)類似疲陕, operator delete 也不允許拋出異常方淤。當(dāng)我們重載這些運(yùn)算符時(shí),必須使用 noexcept 異常說明符指定其不拋出異常蹄殃。
應(yīng)用程序可以自定義上面函數(shù)版本中的任意一個(gè)携茂,前提是自定義的版本必須位于全局作用域或者類作用域中。當(dāng)我們將上述運(yùn)算符函數(shù)定義成類的成員時(shí)诅岩,它們是隱式靜態(tài)的讳苦。我們無須顯式地聲明 static,因?yàn)?operator new 用在對(duì)象構(gòu)造之前而 operator delete 用在對(duì)象銷毀之后吩谦,所以這兩個(gè)成員必須是靜態(tài)的鸳谜,而且它們不能操縱類的任何數(shù)據(jù)成員。
對(duì)于 operator new 函數(shù)或者 operator new[] 函數(shù)來說式廷,它的返回類型必須是 void*咐扭,第一個(gè)形參的類型必須是 size_t 且該形參不能含有默認(rèn)實(shí)參。當(dāng)我們?yōu)橐粋€(gè)對(duì)象分配空間時(shí)使用 operator new滑废;為一個(gè)數(shù)組分配空間時(shí)使用 operator new[] 蝗肪。當(dāng)編譯器調(diào)用 operator new 時(shí),把存儲(chǔ)指定類型對(duì)象所需的字節(jié)數(shù)傳給 size_t 形參蠕趁;當(dāng)調(diào)用 operator new[] 時(shí)薛闪,傳入函數(shù)的則是存儲(chǔ)數(shù)組中所以元素所需的空間。
下面這個(gè)函數(shù)無論如何都不能被用戶重載:
void* operator new(size_t, void*); //不允許重新定義這個(gè)版本
這種形式只供標(biāo)準(zhǔn)庫使用俺陋,不能被用戶重新定義逛绵。
對(duì)于 operator delete 函數(shù)或者 operator delet[] 函數(shù)來說,它們的返回類型必須是 void, 第一個(gè)形參的類型必須是 void* 倔韭。執(zhí)行一條 delete 表達(dá)式將調(diào)用相應(yīng)的 operator 函數(shù)术浪,并用指向待釋放內(nèi)存的指針來初始化 void* 形參。
當(dāng)我們將 operator delete 或 operator delete[] 定義成類的成員時(shí)寿酌,該函數(shù)可以包含另外一個(gè)類型為 size_t 的形參胰苏。此時(shí),該形參的初始值是第一個(gè)形參所指對(duì)象的字節(jié)數(shù)醇疼。 size_t 形參可用于刪除繼承體系中的對(duì)象硕并。如果基類中有一個(gè)虛析構(gòu)函數(shù)法焰,則傳遞給 operator delete 的字節(jié)數(shù)將因待刪除指針?biāo)笇?duì)象的動(dòng)態(tài)類型不同而有所區(qū)別。而且倔毙,實(shí)際運(yùn)行的 operator delete 函數(shù)版本也因?qū)ο蟮膭?dòng)態(tài)類型決定埃仪。
術(shù)語: new 表達(dá)式與 operator new 函數(shù)
標(biāo)準(zhǔn)庫函數(shù) operator new 和 operator delete 的名字容易讓人誤解。和其他 operator 函數(shù)不同(比如 operator=)陕赃,這兩個(gè)函數(shù)并沒有重載 new 表達(dá)式或 delete 表達(dá)式卵蛉。實(shí)際上,我們根本無法自定義 new 表達(dá)式或 delete 表達(dá)式的行為么库。
一條 new 表達(dá)式的執(zhí)行過程總是先調(diào)用 operator new 函數(shù)以獲取內(nèi)存空間傻丝,然后在得到的內(nèi)存空間中構(gòu)造對(duì)象。與之相反诉儒,一條 delete 表達(dá)式的執(zhí)行過程總是先銷毀對(duì)象葡缰,然后調(diào)用 operator delete 函數(shù)釋放對(duì)象所占的空間。
提供的新的 operator new 函數(shù)和 operator delete 函數(shù)的目的在于改變內(nèi)存分配的方式忱反,但是不管怎樣泛释,我們都不能改變 new 運(yùn)算符和 delete 運(yùn)算符的基本含義。
malloc 函數(shù)與 free 函數(shù)
當(dāng)我們定義了自己的全局 operator new 和 operator delete 后温算,這兩個(gè)函數(shù)必須以某種方式執(zhí)行分配內(nèi)存與釋放內(nèi)存的操作胁澳。也許我們的初衷僅僅是使用一個(gè)特殊定制的內(nèi)存分配器,但是這兩個(gè)函數(shù)還應(yīng)該同時(shí)滿足某些測試的目的米者,即檢驗(yàn)其分配內(nèi)存的方式是否與常規(guī)方式類似韭畸。
為此,我們可以使用名為 malloc 和 free 的函數(shù)蔓搞,C++ 從 C 語言中繼承了這些函數(shù)胰丁,并將其定義在 cstdlib 頭文件中。
malloc 函數(shù)接受一個(gè)表示待分配字節(jié)數(shù)的 size_t喂分,返回指向分配空間的指針或者返回 0 以表示分配失敗锦庸。 free 函數(shù)接受一個(gè) void*,它是 malloc 返回的指針的副本蒲祈, free 將相關(guān)內(nèi)存返回給系統(tǒng)甘萧。調(diào)用 free(0) 沒有任何意義。
如下所示是編寫 operator new 和 operator delete 的一種簡單方式梆掸,其他版本與之類似:
void* operator new(size_t size){
if (void* mem = malloc(size))
return mem;
else
throw bad_alloc();
}
void operator delete(void* mem) noexcept { free(mem); }
3.2 定位 new 表達(dá)式[1]
盡管 operator new 函數(shù)和 operator delete 函數(shù)一般用于 new 表達(dá)式扬卷,然而它們畢竟是標(biāo)準(zhǔn)庫的兩個(gè)普通函數(shù),因此普通的代碼也可以直接調(diào)用它們酸钦。
在C++的早期版本中怪得, allocator 類還不是標(biāo)準(zhǔn)庫的一部分。應(yīng)用程序如果想把內(nèi)存分配與初始化分離開來的話,需要調(diào)用 operator new 和 operator delete徒恋。這兩個(gè)函數(shù)的行為與 allocator 的 allocate 成員和 deallocate 成員非常類似蚕断,它們負(fù)責(zé)分配或釋放內(nèi)存空間,但是不會(huì)構(gòu)造或銷毀對(duì)象入挣。
與 allocator 不同的是亿乳,對(duì)于 operator new 分配的內(nèi)存空間來說我們無法使用 construct 函數(shù)構(gòu)造對(duì)象。相反径筏,我們應(yīng)該使用 new 的 定位 new ( placement new )形式構(gòu)造對(duì)象葛假。如我們所知, new 的這種形式為分配函數(shù)提供了額外的信息匠璧。我們可以使用定位 new 傳遞一個(gè)地址桐款,此時(shí)定位 new 的形式如下所示:
new (place_address) type
new (place_address) type (initializers)
new (place_address) type [size]
new (place_address) type [size] { braced initializer list }
其中 place_address 必須是一個(gè)指針咸这,同時(shí)在 initializers 中提供一個(gè)(可能為空的)以逗號(hào)分隔的初始值列表夷恍,該初始值列表將用于構(gòu)造新分配的對(duì)象。
當(dāng)僅通過一個(gè)地址值調(diào)用時(shí)媳维,定位 new 使用 operator new(size_t, void*) “分配”它的內(nèi)存酿雪。這是一個(gè)我們無法自定義的 operator new 版本。該函數(shù)不分配任何內(nèi)存侄刽,它只是簡單地返回指針實(shí)參指黎;然后由 new 表達(dá)式負(fù)責(zé)在指定的地址初始化對(duì)象以完成整個(gè)工作。事實(shí)上州丹,定位 new 允許我們?cè)谝粋€(gè)特定的醋安、預(yù)先分配的內(nèi)存地址上構(gòu)造對(duì)象。
注意:當(dāng)只傳入一個(gè)指針類型的實(shí)參時(shí)墓毒,定位 new 表達(dá)式構(gòu)造對(duì)象但是不分配內(nèi)存吓揪。
盡管在很多時(shí)候使用定位 new 與 allocator 的 construct 成員非常相似,但在它們之間也有一個(gè)重要的區(qū)別所计。我們傳給 cpnstruct 的指針必須指向同一個(gè) allocator 對(duì)象分配的空間柠辞,但是傳給定位 new 的指針無須指向 operator new 分配的內(nèi)存。實(shí)際上主胧,傳給定位 new 表達(dá)式的指針甚至不需要指向動(dòng)態(tài)內(nèi)存叭首。
顯式的析構(gòu)函數(shù)調(diào)用
就像定位 new 與使用 allocate 類似一樣,對(duì)析構(gòu)函數(shù)的顯式調(diào)用也與使用 destroy 很類似踪栋。我們既可以通過對(duì)象調(diào)用析構(gòu)函數(shù)焙格,也可以通過對(duì)象的指針或引用調(diào)用析構(gòu)函數(shù),這與調(diào)用其他成員函數(shù)沒什么區(qū)別:
string *sp = new string("a value"); //分配并初始化一個(gè) string 對(duì)象
sp->~string();
在這里我們自己調(diào)用了一個(gè)析構(gòu)函數(shù)夷都。箭頭運(yùn)算符解引用指針 sp 以獲得 sp 所指的對(duì)象间螟,然后我們調(diào)用析構(gòu)函數(shù),析構(gòu)函數(shù)的形式是波浪線(~)加上類型的名字。
和調(diào)用 destrory 類似厢破,調(diào)用析構(gòu)函數(shù)可以清除給定的對(duì)象但是不會(huì)釋放該對(duì)象所在的空間荣瑟。如果需要的話,我們可以重新使用該空間摩泪。
調(diào)用析構(gòu)函數(shù)會(huì)銷毀對(duì)象笆焰,但是不會(huì)釋放內(nèi)存。