深度探索C++對(duì)象模型筆記

一、關(guān)于對(duì)象
C 語言是程序性的,語言本身并沒有支持?jǐn)?shù)據(jù)和函數(shù)之間的關(guān)聯(lián)性
C++ 中可能采取抽象數(shù)據(jù)類型,或者是多層次的類結(jié)構(gòu)完成
C++ 的封裝并沒有增加多少成本,每一個(gè)成員函數(shù)雖然在class中聲明,但是卻不出現(xiàn)在每個(gè)對(duì)象中
每一個(gè)非內(nèi)聯(lián)的成員函數(shù)只會(huì)誕生一個(gè)函數(shù)實(shí)例 每個(gè)內(nèi)聯(lián)函數(shù)會(huì)在其每一個(gè)使用者身上產(chǎn)生一個(gè)函數(shù)實(shí)例 C++ 在布局以及存儲(chǔ)時(shí)間上主要的額外負(fù)擔(dān)是由virtual引起的
虛函數(shù)機(jī)制用以支持一個(gè)有效率的“執(zhí)行期綁定” 虛基類用來實(shí)現(xiàn)“多次出現(xiàn)在繼承關(guān)系中的基類,有一個(gè)單一而被共享的實(shí)例” 還有一些多重繼承下的額外負(fù)擔(dān),發(fā)生在一個(gè)派生類和其第二或后繼之基類的轉(zhuǎn)換之間
1.1 C++對(duì)象模式
C++對(duì)象模型有以下幾點(diǎn) 每個(gè)類中存放一個(gè)指針稱為vptr,指向虛函數(shù)表 表中每個(gè)都指向一個(gè)虛函數(shù) 非靜態(tài)數(shù)據(jù)成員放在類對(duì)象內(nèi) 靜態(tài)數(shù)據(jù)成員放在類對(duì)象外 靜態(tài)和非靜態(tài)成員函數(shù)也放在類對(duì)象外 虛函數(shù)則不同
1.2 關(guān)鍵詞所帶來的差異
int ( *pq ) ( ); //聲明當(dāng)語言無法區(qū)分那是一個(gè)聲明還是一個(gè)表達(dá)式時(shí),我們需要一個(gè)超越語言范圍的規(guī)則,而該規(guī)則會(huì)將上述式子判斷為一個(gè)“聲明“
struct和class可以相互替換,他們只是默認(rèn)的權(quán)限不一樣
如果一個(gè)程序員需要擁有C聲明的那種struct布局,可以抽出來單獨(dú)成為struct聲明,并且和C++部分組合起來
1.3 對(duì)象的差異
C++支持三種程序范式:程序模型、抽象數(shù)據(jù)類型模型、面向?qū)ο竽P?/strong>
面向?qū)ο竽P驮诶^承體系中 ,有時(shí)候編譯期間無法確定指針或引用所指類型 C++支持的多態(tài)類型:
經(jīng)由一組隱式的轉(zhuǎn)化操作:如派生類指針轉(zhuǎn)化為指向父類的指針 經(jīng)由虛函數(shù)機(jī)制 經(jīng)由dynamic_cast 和 typeid運(yùn)算符 一個(gè)class所占的大小包括:
其非靜態(tài)成員所占的大小 由于內(nèi)存對(duì)齊填補(bǔ)上的大小 加上支持虛函數(shù)而產(chǎn)生的大小 指針的類型,只能代表其讓編譯器如何解釋其所指向的地址內(nèi)容,和它本身類型無關(guān),所以轉(zhuǎn)換其實(shí)是一種編譯器指令,不改變所指向的地址,只影響怎么解釋它給出的地址
當(dāng)一個(gè)基類對(duì)象被初始化為一個(gè)子類對(duì)象時(shí),派生類就會(huì)被切割用來塞入較小的基類內(nèi)存中,派生類不會(huì)留下任何東西,多態(tài)也不會(huì)再呈現(xiàn)。
二、構(gòu)造函數(shù)語意學(xué)
2.1 默認(rèn)構(gòu)造函數(shù)的構(gòu)造操作
以下四種情況下,會(huì)合成有用的構(gòu)造函數(shù):
類聲明(或繼承)一個(gè)虛函數(shù) 類派生自一個(gè)繼承串鏈,其中有一個(gè)或更多的虛基類 帶有默認(rèn)構(gòu)造函數(shù)的成員函數(shù)對(duì)象,不過這個(gè)合成操作只有在構(gòu)造函數(shù)真正需要被調(diào)用時(shí)才發(fā)生,但只是調(diào)用其成員的默認(rèn)構(gòu)造函數(shù),其他則不會(huì)初始化 如果一個(gè)派生類的父類帶有默認(rèn)構(gòu)造函數(shù),那么子類如果沒有定義構(gòu)造函數(shù),則會(huì)合成默認(rèn)構(gòu)造函數(shù),如果有的話但是沒有調(diào)用父類的,則編譯器會(huì)插入一些代碼調(diào)用父類的默認(rèn)構(gòu)造函數(shù) 帶有一個(gè)虛函數(shù)的類 帶有一個(gè)虛基類的類 C++新手常見的兩個(gè)誤解:
任何class如果沒有定義默認(rèn)構(gòu)造函數(shù),就會(huì)被合成出來一個(gè) 編譯器合成出來的默認(rèn)構(gòu)造函數(shù)會(huì)顯式設(shè)定類中的每一個(gè)數(shù)據(jù)成員的額 默認(rèn)值
2.2 拷貝構(gòu)造函數(shù)的構(gòu)造操作
有三種情況會(huì)調(diào)用拷貝構(gòu)造函數(shù):
對(duì)一個(gè)對(duì)象做顯式的初始化操作 當(dāng)對(duì)象被當(dāng)作參數(shù)交給某個(gè)函數(shù) 當(dāng)函數(shù)傳回一個(gè)類對(duì)象時(shí) 如果類沒有聲明一個(gè)拷貝函數(shù),就會(huì)有隱式的聲明和隱式的定義出現(xiàn),同默認(rèn)構(gòu)造函數(shù)一樣在使用時(shí)才合成出來
什么情況下一個(gè)類不展現(xiàn)“淺拷貝語意”:
編譯器會(huì)合成一個(gè)拷貝構(gòu)造函數(shù),安插一些代碼用來設(shè)定虛基類指針和偏移的初值,對(duì)每個(gè)成員執(zhí)行必要的深拷貝初始化操作,以及執(zhí)行其他的內(nèi)存相關(guān)工作 編譯器會(huì)顯式的設(shè)定新類的虛函數(shù)表,而不是直接拷貝過來指向同一個(gè) 這兩個(gè)編譯器都會(huì)合成拷貝構(gòu)造函數(shù)并且安插進(jìn)那個(gè)成員和基類的拷貝構(gòu)造函數(shù) 當(dāng)類內(nèi)含有一個(gè)成員類而后者的類聲明中有一個(gè)拷貝構(gòu)造函數(shù)(例如內(nèi)含有string成員變量) 當(dāng)類繼承自一個(gè)基類而基類中存在拷貝構(gòu)造函數(shù) 當(dāng)類聲明了一個(gè)或多個(gè)虛函數(shù) 當(dāng)類派生自一個(gè)繼承串鏈,其中有一個(gè)或多個(gè)虛基類
2.3 程序轉(zhuǎn)化語意學(xué)
在將一個(gè)類作為另一個(gè)類的初值情況下,語言允許編譯器有大量的自由發(fā)揮的空間,用來提升效率,但是缺點(diǎn)是不能安全的規(guī)劃拷貝構(gòu)造函數(shù)的副作用,必須視其執(zhí)行而定
拷貝構(gòu)造的應(yīng)用,編譯器會(huì)多多少的進(jìn)行部分轉(zhuǎn)換,尤其是當(dāng)一個(gè)函數(shù)以值傳遞的方式傳回一個(gè)對(duì)象,而該對(duì)象有一個(gè)合成的構(gòu)造函數(shù),此外編譯器也會(huì)對(duì)拷貝構(gòu)造的調(diào)用進(jìn)行調(diào)優(yōu),以額外的第一參數(shù)取代NRV(Named Return Value)
2.4 成員們的初始化隊(duì)伍
四種情況下你需要使用成員初始化列表 當(dāng)初始化一個(gè)引用成員變量 當(dāng)初始化一個(gè)const 成員變量 當(dāng)調(diào)用一個(gè)基類的構(gòu)造函數(shù),而它擁有一組參數(shù) 當(dāng)調(diào)用一個(gè)類成員變量的構(gòu)造函數(shù),而它擁有一組參數(shù)
class?Word{
?String?_name;
?int?_cnt;
public:
?Word(){
??_name?=?0;
??_cnt?=?0;
?}
/*使用成員列表初始化可以解決????
?????Word()?:?_name(0),_cnt(0){
?}
*/?
}
上式不會(huì)報(bào)錯(cuò),但是會(huì)有效率問題,因?yàn)?strong>這樣會(huì)先產(chǎn)生一個(gè)臨時(shí)的string對(duì)象,然后將它初始化,之后以一個(gè)賦值運(yùn)算符將臨時(shí)對(duì)象指定給_name,再摧毀臨時(shí)的對(duì)象
成員初始化列表中的初始化順序是按照類中的成員變量聲明的順序,與成員初始化列表的排列順序無關(guān)
3、Data語意學(xué)
class?X{};
class?Y?:?public?virtual?X?{};
class?Z?:?public?virtual?X?{};
class?A?:?public?Y,public?Z?{};
sizeof(X)?//1
sizeof(Y)?//4
sizeof(Z)?//4
sizeof(A)?//8
X為1是因?yàn)榫幾g器的處理,在其中插入了1個(gè)char,為了讓其對(duì)象能在內(nèi)存中有自己獨(dú)立的地址
Y,Z是因?yàn)樘摶惐淼闹羔?/strong>
A 中含有Y和Z所以是8
每一個(gè)類對(duì)象大小的影響因素:
非靜態(tài)成員變量的大小 virtual特性 內(nèi)存對(duì)齊
3.1 數(shù)據(jù)成員的綁定
如果類的內(nèi)部有typedef,請(qǐng)把它放在類的起始處,因?yàn)榉乐瓜瓤吹降氖侨值暮瓦@個(gè)typedef相同的沖突,編譯器會(huì)選擇全局的,因?yàn)橄瓤吹饺值?/strong>
3.2 數(shù)據(jù)成員的布局
非靜態(tài)成員變量的在內(nèi)存中的順序和其聲明順序是一致的 但是不一定是連續(xù)的,因?yàn)橹虚g可能有內(nèi)存對(duì)齊的填補(bǔ)物 virtual機(jī)制的指針?biāo)诺奈恢煤途幾g器有關(guān)
3.3 成員變量的存取
靜態(tài)變量都被放在一個(gè)全局區(qū),與類的大小無關(guān),正如對(duì)其取地址得到的是與類無關(guān)的數(shù)據(jù)類型,如果兩個(gè)類有相同的靜態(tài)成員變量,編譯器會(huì)暗自為其名稱編碼,使兩個(gè)名稱都不同 非靜態(tài)成員變量則是直接放在對(duì)象內(nèi),經(jīng)由對(duì)象的地址和在類中的偏移地址取得,但是在繼承體系下,情況就會(huì)不一樣,因?yàn)榫幾g器無法確定此時(shí)的指針指的具體是父類對(duì)象還是子類對(duì)象
3.4 繼承下的數(shù)據(jù)成員
在下面給定的兩個(gè)類中依次討論不同情況:

在單一繼承沒有虛函數(shù)的情況下布局圖

這種情況下常見錯(cuò)誤: 可能會(huì)重復(fù)設(shè)計(jì)一些操作相同的函數(shù),我們可以把某些函數(shù)寫成inline,這樣就可以在子類中調(diào)用父類的某些函數(shù)來實(shí)現(xiàn)簡(jiǎn)化 把數(shù)據(jù)放在同一個(gè)類中和繼承起來的內(nèi)存布局可能不同,因?yàn)槊總€(gè)類需要內(nèi)存對(duì)齊

疊在一起的內(nèi)存布局

可見內(nèi)存大了100%
容易出現(xiàn)的不易發(fā)現(xiàn)的問題:

當(dāng)加上多態(tài)之后,對(duì)空間上增加的額外負(fù)擔(dān)包括:
析構(gòu)函數(shù)的調(diào)用順序是反向的,從子類到父類 導(dǎo)入一個(gè)虛函數(shù)表,表中的個(gè)數(shù)是聲明的虛函數(shù)的個(gè)數(shù)加上一個(gè)或兩個(gè)slots(用來支持運(yùn)行類型識(shí)別)
在每個(gè)對(duì)象中加入vptr,提供執(zhí)行期的鏈接,使每一個(gè)類能找到相應(yīng)的虛函數(shù)表
加強(qiáng)構(gòu)造函數(shù),使它能夠?yàn)関ptr設(shè)定初值,讓它指向?qū)?yīng)的虛函數(shù)表,這可能意味著在派生類和每一個(gè)基類的構(gòu)造函數(shù)中,重新設(shè)定vptr的值
加強(qiáng)析構(gòu)函數(shù),使它能夠消抹“指向類的相關(guān)虛函數(shù)表”的vptr,vptr很可能以及在子類析構(gòu)函數(shù)中被設(shè)定為子類的虛表地址。
以下是三種情況不同的繼承下會(huì)有不同的布局
Vptr放在尾端 
vptr放在前端 

多重繼承
**單一繼承特點(diǎn):**派生類和父類對(duì)象都是從相同的地址開始,區(qū)別只是派生類比較大能容納自己的非靜態(tài)成員變量
多重繼承下會(huì)比較復(fù)雜

一個(gè)派生對(duì)象,把它的地址指定給最左邊的基類,和單一繼承一樣,因?yàn)槠鹗嫉刂肥且粯拥?,但是后面的需要更改,因?yàn)樾枰由锨懊婊惖拇笮?,才能得到后面基類的地?/strong>

虛繼承
STL標(biāo)準(zhǔn)庫中使用的虛繼承:
虛繼承關(guān)系圖 
虛繼承關(guān)系:

虛繼承例子
虛繼數(shù)據(jù)在內(nèi)存中的分布
虛繼承數(shù)據(jù)模型 
虛繼承數(shù)據(jù)模型2 
3.5 對(duì)象成員的效率
程序員如果關(guān)心程序效率,應(yīng)該實(shí)際測(cè)試,不要光憑推論、常識(shí)判斷或假設(shè)。 優(yōu)化操作并不一定總是能夠有效運(yùn)行,我不止一次以優(yōu)化方式來 編譯一個(gè)已通過編譯的正常程序,卻以失敗收?qǐng)?/section>
3.6 指向數(shù)據(jù)成員的指針
vptr通常放在起始處或尾端,與編譯器有關(guān),C++標(biāo)準(zhǔn)允許放在類中的任何位置
取某個(gè)類成員變量的地址,通常取到得的是在類的首地址的偏移位置
例如 & Point3d::z;將得到在類的偏移位置,最低限度是類的成員大小總和,而這個(gè)偏移量通常都被加上了1如果用一個(gè)真正綁定類對(duì)象(也就是使用 . 操作符訪問成員變量)去取地址,得到的將會(huì)是內(nèi)存中真正的地址
在多重繼承下,若要蔣第二個(gè)積累的指針和一個(gè)與派生類綁定的成員結(jié)合起來,那么將會(huì)因?yàn)樾枰尤肫屏慷兊孟喈?dāng)復(fù)雜
四、Function 語意學(xué)
C++支持三種類型的成員函數(shù):static 、non-static 、virtual 不能直接存取non-static數(shù)據(jù) 不能被聲明為const static函數(shù)限制:
4.1 成員函數(shù)的各種調(diào)用方式
非靜態(tài)成員函數(shù):C++會(huì)保證至少和一般的普通的函數(shù)有相同的效率,經(jīng)過三個(gè)步驟的轉(zhuǎn)換
改寫函數(shù),安插一個(gè)額外的參數(shù)到該函數(shù)中,用來提供一個(gè)存取管道------即this指針 對(duì)每一個(gè)非靜態(tài)成員的存取操作改成使用this指針來調(diào)用 將成員函數(shù)改寫成一個(gè)外部函數(shù),并且名稱改為獨(dú)一無二的 虛函數(shù)成員函數(shù):也會(huì)經(jīng)過類似的轉(zhuǎn)化
vptr是編譯器產(chǎn)生的指針,指向虛函數(shù)表,其名稱也會(huì)被改為獨(dú)一無二 1 是該函數(shù)在虛函數(shù)表中的索引 ptr 則是this指針 例如: ptr->normalize()會(huì)被轉(zhuǎn)化為( * ptr->vptr[1] )( ptr )靜態(tài)成員函數(shù):它沒有this指針,因此會(huì)有以下限制:
它不能直接存取類中的非成員變量 它不能夠被聲明為const、volatile 和 virtual 它不需要經(jīng)過類的對(duì)象才能被調(diào)用----雖然很多事情況是這樣調(diào)用的
4.2 詳解虛成員函數(shù)
虛函數(shù)一般實(shí)現(xiàn)模型:每一個(gè)類都有一個(gè)虛函數(shù)表,內(nèi)含該類中有作用的虛函數(shù)地址,然后每個(gè)對(duì)象有一個(gè)虛函數(shù)指針,指向虛表位置
多態(tài)含義:以一個(gè)基類的指針(或引用),尋址出一個(gè)子類對(duì)象
什么是積極多態(tài)?
當(dāng)被指出的對(duì)象真正使用時(shí),多態(tài)就變成積極的了
單一繼承虛函數(shù)布局圖

一個(gè)類派生自Point會(huì)發(fā)生什么事?三種可能的情況
它可以繼承基類所聲明的虛函數(shù)的函數(shù)實(shí)例,該函數(shù)實(shí)例的地址會(huì)被拷貝進(jìn)子類的虛表的相對(duì)應(yīng)的slot之中 它可以使用自己的虛函數(shù)實(shí)例----它自己的函數(shù)實(shí)例地址必須放在對(duì)應(yīng)的slot之中 它可以加入一個(gè)新的虛函數(shù),這時(shí)候虛函數(shù)表的尺寸會(huì)增加一個(gè)slot,而新加入的函數(shù)實(shí)例地址也會(huì)被放進(jìn)該slot之中 多重繼承下的虛函數(shù)
多繼承虛函數(shù)圖 
虛擬繼承下的虛函數(shù)
虛繼承下的虛函數(shù)圖 
4.4指向成員函數(shù)的指針
對(duì)于普通的成員函數(shù),編譯器會(huì)將其轉(zhuǎn)化為一個(gè)函數(shù)指針,然后使用成員函數(shù)的地址去初始化
例如 ? double (Point :: *pmf ) ( );轉(zhuǎn)化為 ? double ( Point :: coord )( ) = &Point :: x;這樣調(diào)用 ? (coord) (& origin)或 (coord)(ptr)對(duì)一個(gè)虛函數(shù)取地址,在vc編譯器下,要么得到vacll thunk地址(虛函數(shù)時(shí)候),要么得到的是函數(shù)地址(普通函數(shù))
4.5 內(nèi)聯(lián)函數(shù)
inline只是向編譯器提出一個(gè)請(qǐng)求,是否真的優(yōu)化取決于編譯器自己的判定
對(duì)于形式參數(shù),會(huì)采用:
常量表達(dá)式替換 常量替換 引入臨時(shí)變量來避免多次求值操作 對(duì)于局部變量,會(huì)采用:
使用臨時(shí)變量
五、構(gòu)造、析構(gòu)、拷貝語意學(xué)
應(yīng)注意的一些問題: 構(gòu)造函數(shù)不要寫為純虛函數(shù),因?yàn)楫?dāng)抽象類中有數(shù)據(jù)的時(shí)候,將無法初始化 把所有函數(shù)設(shè)計(jì)成虛函數(shù),再由編譯器去除虛函數(shù)是錯(cuò)誤的,不應(yīng)該成為虛函數(shù)的函數(shù)不要設(shè)計(jì)成虛函數(shù) 當(dāng)你無法抉擇一個(gè)函數(shù)是否需要為const時(shí),尤其是抽象類,最好不設(shè)置成const
5.1 "無繼承" 情況下的對(duì)象構(gòu)造
對(duì)于沒有初始化的全局變量,C語言中會(huì)將其放在一個(gè)未初始化全局區(qū),而C++會(huì)將所有全局對(duì)象初始化 對(duì)于不需要構(gòu)造函數(shù)、析構(gòu)函數(shù)、賦值操作的類或結(jié)構(gòu)體,C++會(huì)將其打上POD標(biāo)簽,賦值時(shí)按照c那樣的位搬運(yùn) 對(duì)于含有虛函數(shù)的類,編譯器會(huì)在構(gòu)造函數(shù)的開始放入一些初始化虛表和虛指針的操作 面對(duì)函數(shù)以值方式返回,編譯器會(huì)將其優(yōu)化為加入一個(gè)參數(shù)的引用方式,避免多次構(gòu)造函數(shù)
5.2 繼承體系下的對(duì)象構(gòu)造
構(gòu)造函數(shù)會(huì)含有大量的隱藏嘛,因?yàn)榫幾g器會(huì)進(jìn)行擴(kuò)充:
如果類被列于成員初始化列表中,那么如果有任何顯式指定的參數(shù),都應(yīng)該傳遞過去。若沒有列于list中,而類中有一個(gè)默認(rèn)構(gòu)造,亦應(yīng)該調(diào)用 此外,類中的每一個(gè)虛基類的偏移位置必須在執(zhí)行期可被存取 如果類對(duì)象是最底層的類,其構(gòu)造函數(shù)可能被調(diào)用,某些用以支持這一行為的機(jī)制必須被放進(jìn)來 如果基類被列于成員初始化列表中,那么任何顯式指定的參數(shù)應(yīng)該傳遞過去 如果基類沒有被列于基類初始化列表中,而它有默認(rèn)的構(gòu)造函數(shù),那么就調(diào)用 如果基類是多重繼承下的第二個(gè)或后繼的基類,那么this指針必須有所調(diào)整 記錄在成員初始化列表中的成員數(shù)據(jù)初始化會(huì)被放進(jìn)構(gòu)造函數(shù)的本體,并以成員在類中聲明的順序?yàn)轫樞?/section> 如果有一個(gè)成員并沒有出現(xiàn)在成員初始化列表中,但是它由一個(gè)默認(rèn)構(gòu)造函數(shù),那么也會(huì)被調(diào)用 在那之前,如果類對(duì)象由虛表指針,它必須被設(shè)定初值,指向適當(dāng)?shù)奶摫?/strong> 在那之前,所有上一層的基類構(gòu)造函數(shù)必須被調(diào)用,以基類聲明的順序(不是成員初始化列表出現(xiàn)的順序) 在那之前,所有虛基類構(gòu)造函數(shù)必須被調(diào)用,從左到右,從最深到最淺 不要忘記在賦值函數(shù)中,檢查自我賦值的情況
5.3 對(duì)象復(fù)制語意學(xué)
當(dāng)一個(gè)類復(fù)制給另一個(gè)類時(shí),能采用的有三種方式:
如果有需要,會(huì)自動(dòng)生成一個(gè)淺拷貝,至于什么時(shí)候需要深拷貝(見第二章講) 什么都不做,會(huì)實(shí)施默認(rèn)行為 提供一個(gè)拷貝復(fù)制運(yùn)算符 顯式地拒絕把一個(gè)類拷貝給另一個(gè) 虛基類會(huì)使其復(fù)制操作調(diào)用一次以上,因此我們應(yīng)該避免在徐杰類中聲明數(shù)據(jù)成員
5.5 析構(gòu)語意學(xué)
什么時(shí)候會(huì)合成析構(gòu)函數(shù)?
在類內(nèi)含的成員函數(shù)有析構(gòu)函數(shù) 基類含有析構(gòu)函數(shù) 析構(gòu)的正確順序:
析構(gòu)函數(shù)的本體首先被執(zhí)行,vptr會(huì)在程序員的代碼執(zhí)行前被重設(shè)。 如果類擁有成員類對(duì)象,而后者擁有析構(gòu)函數(shù),那么他們會(huì)以其聲明順序的相反順序被調(diào)用 如果類內(nèi)涵一個(gè)vptr,現(xiàn)在被重新設(shè)定,指向適當(dāng)?shù)姆e累的虛表 如果有任何直接的非虛基類擁有析構(gòu)函數(shù),它們會(huì)以其聲明順序的相反順序被調(diào)用 如果有任何虛基類擁有析構(gòu)函數(shù),而且目前討論的這個(gè)類是最尾端的類,那么它們會(huì)以其原來的構(gòu)造順序的相反順序被調(diào)用
六、執(zhí)行期語意學(xué)
C++難以從程序源碼看出表達(dá)式的復(fù)雜過程,因?yàn)槟悴⒉恢谰幾g器會(huì)在其中添加多少代碼
編譯器對(duì)不同的對(duì)象會(huì)做不同的操作:
如果對(duì)象沒有定義構(gòu)造函數(shù)和析構(gòu)函數(shù),那么編譯器只需要分配需要存儲(chǔ)10個(gè)連續(xù)空間即可 如果有構(gòu)造函數(shù),且如果有名字,則會(huì)分為是否含有虛基類調(diào)用不同的函數(shù)來構(gòu)造,如果沒有名字,例如沒有knots,則會(huì)使用new來分配在堆中 當(dāng)聲明結(jié)束時(shí)會(huì)有類似構(gòu)造的析構(gòu)過程 我們無法在程序中取出一個(gè)構(gòu)造函數(shù)的地址 會(huì)增加臨時(shí)的對(duì)象用來判斷其是否被構(gòu)造,用來保證在第一次進(jìn)入含有該靜態(tài)對(duì)象的起始處調(diào)用一次構(gòu)造函數(shù),并且在離開文件的時(shí)候利用臨時(shí)對(duì)象判斷是否已經(jīng)被構(gòu)造來決定是否析構(gòu) 如果異常處理被支持,那么那些對(duì)象將不能被放置到try區(qū)段之內(nèi) 增加了程序的復(fù)雜度 對(duì)于全局對(duì)象:編譯器會(huì)在添加__main函數(shù)和_exit函數(shù)(和C庫中的不同),并且在這兩個(gè)函數(shù)中對(duì)所有全局對(duì)象進(jìn)行靜態(tài)初始化和析構(gòu)
全局對(duì)象編譯器操作 
使用被靜態(tài)初始化的對(duì)象,有一些缺點(diǎn):
因此,不建議用那些需要靜態(tài)初始化的全局對(duì)象
對(duì)于局部靜態(tài)對(duì)象:
對(duì)于對(duì)象數(shù)組:(例如
Point knots[10])
6.2 new 和 delete 運(yùn)算符
對(duì)于普通類型變量:
例如
int *pi = new int(5)調(diào)用函數(shù)庫中的new運(yùn)算符 if(int *pi = _new (sizeof(int) ))再配置初值 *pi = 5對(duì)于delete來說
delete pi;則先進(jìn)行保護(hù) if( pi != 0)再調(diào)用delete ? _delete(pi)對(duì)于成員對(duì)象:
對(duì)于
Point3d* origin = new Point3d實(shí)際調(diào)用operator new,其代碼如下
extern?void*?operator?new?(size_t?size){
????if(size?==?0)
????????size?=?1;
????void?*?last_alloc;
????while(!(last_alloc?=?malloc(size))){
????????if(_new_handler)
????????????(?*_new_handler)();
????????else
????????????return?0;
????}
????return?last_alloc;
}語言要求每一次對(duì)new的調(diào)用都必須傳回一個(gè)獨(dú)一無二的指針,為了解決這個(gè)問題,傳回一個(gè)指向默認(rèn)為1Byte的內(nèi)存區(qū)塊,允許程序員自己定義_new_handler函數(shù),并且循環(huán)調(diào)用
至于delete也相同
extern?void?operator?delete?(void?*ptr){
?if(ptr)
??free(?(char*)ptr)
}對(duì)于對(duì)象數(shù)組,會(huì)在分配的內(nèi)存上方放上cookies,來存儲(chǔ)數(shù)組個(gè)數(shù),方便delete調(diào)用來析構(gòu)
程序員最好避免以一個(gè)基類指向一個(gè)子類所組成的數(shù)組---如果子類對(duì)象比其基類大的話
解決方式:
for(int?ix?=?0;?ix??Point3d?*p?=?&((Point3d*)ptr)[ix];
?delete?p;
}程序員必須迭代走過整個(gè)數(shù)組,把delete運(yùn)算符實(shí)施與每一個(gè)元素身上。以此方式,調(diào)用操作將是virtual。因此,Point3d和Point的析構(gòu)函數(shù)都會(huì)實(shí)施與每一個(gè)對(duì)象上
Placement Operator new的語意
有一個(gè)預(yù)先定義好的重載的new運(yùn)算符,稱為placement operator new。它需要第二個(gè)參數(shù),類型為void*
形如
Point2w *ptw = new (arena) Point2w,其中arena指向內(nèi)存中的一個(gè)區(qū)塊,用以放置新產(chǎn)生出來的Point2w 對(duì)象void*?operator?new(size_t?,?void*?p){
?return?p;
}如果我們?cè)谝延袑?duì)象的基礎(chǔ)上調(diào)用placement new的話,原來的析構(gòu)函數(shù)并不會(huì)被調(diào)用,而是直接刪除原來的指針,但是不能使用delete 原來的指針
正確的方法應(yīng)該是 :
//錯(cuò)誤:
delete?p2w;
p2w?=?new(arwna)?Point2w;
//正確:
p2w->Point2w;
p2w?=?new(arena)?Point2w;6.3 臨時(shí)性對(duì)象
臨時(shí)對(duì)象在類的表達(dá)式并賦值,函數(shù)以值方式傳參等都會(huì)產(chǎn)生臨時(shí)對(duì)象-----而臨時(shí)對(duì)象會(huì)構(gòu)造和析構(gòu),所以會(huì)拖慢程序的效率,我們應(yīng)該盡量避免
七、站在對(duì)象模型的頂端
三個(gè)著名的C++語言擴(kuò)充性質(zhì):模板、異常(EH)、RTTI(runtime type identification)
7.1 Template
模板實(shí)例化時(shí)間線:
當(dāng)編譯器看到模板類的聲明時(shí),什么都不會(huì)做,不會(huì)進(jìn)行實(shí)例化 模板類中明確類型的參數(shù),通過模板類的某個(gè)實(shí)例化版本才能存取操作 即使是靜態(tài)類型的變量,也需要與具體的實(shí)例版本關(guān)聯(lián),不同版本有不同的一份 如果聲明一個(gè)模板類的指針,那么不會(huì)進(jìn)行實(shí)例化,但是如果是引用,那么會(huì)進(jìn)行實(shí)例化 對(duì)于模板類中的成員函數(shù),只有在函數(shù)使用的時(shí)候才會(huì)進(jìn)行實(shí)例化 模板名稱決議的方法-----即如果非成員函數(shù)在類中調(diào)用,那么會(huì)調(diào)用名稱相同的哪個(gè)版本:
會(huì)根據(jù)該函數(shù)是否與模板有關(guān)來判斷,如果是已知類型,那么會(huì)在定義的范圍內(nèi)直接查找,如果以來模板的具體類型,那么會(huì)在實(shí)例化的范圍查找 7.2 異常處理
C++異常處理由三個(gè)主要的語匯組件:
一個(gè)throw子語。它在程序某處發(fā)出一個(gè)exception,exception可以說內(nèi)建類型也可以是自定義類型 一個(gè)或多個(gè)catch子句。每一個(gè)catch子句都是一個(gè)exceotion hander,它用來表示說,這個(gè)子句準(zhǔn)備處理某種類型exception,并且在封閉的大括號(hào)區(qū)段中提供實(shí)際的處理程序 一個(gè)try區(qū)段。它被圍繞一系列的敘述句,這些敘述句可能會(huì)引發(fā)catch子句起作用 當(dāng)一個(gè)異常被拋出去,控制權(quán)會(huì)從函數(shù)調(diào)用中被釋放出來,并尋找一個(gè)吻合的catch子句。如果都沒有吻合者,那么默認(rèn)的處理例程 terminate()會(huì)被調(diào)用,當(dāng)控制權(quán)被放棄后,堆棧中的每一個(gè)函數(shù)調(diào)用也就被推離。這個(gè)程序被稱為 unwingding the stack 。在每一個(gè)函數(shù)被推離堆棧之前,函數(shù)的局部類的析構(gòu)會(huì)被調(diào)用
因此一個(gè)解決辦法就是將類封裝在一個(gè)類中,這樣變成局部類,如果拋出異常也會(huì)被自動(dòng)析構(gòu)
當(dāng)一個(gè)異常拋出,編譯系統(tǒng)必須:
摧毀所有活躍局部類 從堆棧中將目前的函數(shù)(unwind)釋放掉 進(jìn)行到程序堆棧的下一個(gè)函數(shù)中去,然后重復(fù)上述步驟2—5 檢查發(fā)生throw操作的函數(shù) 決定throw操作是否發(fā)生在try區(qū)段 若是,編譯系統(tǒng)必須把異常類型拿來和每一個(gè)catch子句進(jìn)行比較 如果比較后吻合,流程控制應(yīng)該交到catch子句中 如果throw的發(fā)生并不在try區(qū)段中,或沒有一個(gè)catch子句吻合,那么系統(tǒng)必須: 7.3 執(zhí)行期類型識(shí)別
當(dāng)兩個(gè)類有繼承關(guān)系的時(shí)候,我們有轉(zhuǎn)換需求時(shí),可以進(jìn)行向下轉(zhuǎn)型,但是很多時(shí)候是不安全的
C++的RTTI (執(zhí)行期類型識(shí)別)提供了一個(gè)安全的向下轉(zhuǎn)型設(shè)備,但是只對(duì)多態(tài)(繼承和動(dòng)態(tài)綁定)的類型有效,其用來支持RTTI的策略就是,在C++的虛函數(shù)表的第一個(gè)slot處,放一個(gè)指針,指向保存該類的一些信息----即type_info類(在編譯器文件中可以找到其定義)
dynamic_cast運(yùn)算符可以在執(zhí)行期決定真正的類型
如果引用真正轉(zhuǎn)換到適當(dāng)?shù)淖宇?,則可以繼續(xù) 如果不能轉(zhuǎn)換的化,會(huì)拋出 bad_cast 異常 通常使用 try 和 catch 來進(jìn)行判斷是否成功 如果轉(zhuǎn)型成功,則會(huì)返回一個(gè)轉(zhuǎn)換后的指針 如果是不安全的,會(huì)傳回0 放在程序中通過 if 來判斷是否成功,采取不同措施 對(duì)于指針來說: 對(duì)于引用來說: Typeid運(yùn)算符:
可以傳入一個(gè)引用,typeid運(yùn)算符會(huì)傳回一個(gè)const reference,類型為type_info。其內(nèi)部已經(jīng)重載了 == 運(yùn)算符,可以直接判斷兩個(gè)是否相等,回傳一個(gè)bool值 例如 ? if( typeid( rt ) == typeid( fct ) )RTTI雖然只適用于多態(tài)類,但是事實(shí)上type_info object也適用于內(nèi)建類,以及非多態(tài)的使用者自定類型,只不過內(nèi)建類型 得到的type_info類是靜態(tài)取得,而不是執(zhí)行期取得
