【構(gòu)造和析構(gòu)函數(shù)】
通常我們的看法是:當(dāng)定義一個(gè)類的時(shí)候,如果沒有為它寫一個(gè)構(gòu)造函數(shù),系統(tǒng)將幫我們生成一個(gè),并完成成員的初始化。但是,從編譯器來(lái)看,上述看法中的兩點(diǎn)認(rèn)識(shí)都不夠正確。編譯器只會(huì)在編譯需要的情況下(nontrivial的條件)自動(dòng)生成默認(rèn)構(gòu)造函數(shù)構(gòu)造函數(shù)。一般包括下面四種情況:1.類中包含的數(shù)據(jù)成員有默認(rèn)構(gòu)造函數(shù);2.其基類包含默認(rèn)構(gòu)造函數(shù);3.具有虛成員函數(shù);4.虛繼承至某個(gè)類。其他情況被稱為是trivial,編譯器將不會(huì)添加一個(gè)默認(rèn)構(gòu)造函數(shù)。把握好編譯器需要才添加這個(gè)前提,很容易理解為什么要添加默認(rèn)構(gòu)造函數(shù)。默認(rèn)函數(shù)完成編譯器需要做的一些事情,包括初始化基類,虛指針等。搞清楚這個(gè)概念有助于了解構(gòu)造函數(shù)的存在意義。構(gòu)造函數(shù)是為了完成對(duì)象的初始化(即初始化分配來(lái)的空間),與空間的分配沒有關(guān)系,而我們會(huì)經(jīng)常自覺不自覺的混淆這個(gè)概念,在處理一些問(wèn)題(比如new操作符)的時(shí)候,理解不夠清晰。現(xiàn)在了解了編譯器的原理,可以加深對(duì)構(gòu)造函數(shù)的認(rèn)識(shí)。
另外如果一個(gè)類中被定義了一個(gè)構(gòu)造函數(shù),編譯器也會(huì)根據(jù)它的需求擴(kuò)充該構(gòu)造函數(shù)。考慮下面一段代碼:
class A
{
virtual void aMethod() {}
};
class B :
public A
{
public:
A(int i) : aValue(i) {}
virtual void aMethod() {}
private:
int aValue;
};
它的構(gòu)造函數(shù)會(huì)被擴(kuò)充成大概下面的樣子:
A* A::A(A* this, int i)
{
// 1.如果有虛基類會(huì)被先行調(diào)用
// 2.基類的構(gòu)造函數(shù)被調(diào)用(可視為遞歸過(guò)程)
B::B();
// 3.初始化虛指針
// 4.展開初始化列表,如果有數(shù)據(jù)元素有默認(rèn)構(gòu)造函數(shù)將被調(diào)用
aValue(i);
// 5.構(gòu)造函數(shù)實(shí)體
}
請(qǐng)注意虛表和虛指針被初始化的位置。它處于基類構(gòu)造函數(shù)被調(diào)用后,本身內(nèi)容被執(zhí)行之前。也就是如果在該類基類的構(gòu)造函數(shù)中放置一個(gè)虛函數(shù),它執(zhí)行的內(nèi)容是定義在基類本身的版本(也許你想調(diào)用子類的實(shí)現(xiàn)版本),這個(gè)概念與其他的虛函數(shù)的調(diào)用不一樣(其他調(diào)用的都是屬于子類的版本)。因?yàn)樵诨悩?gòu)造的時(shí)候,虛指針依然指向基類的虛表,只有初始化虛指針后才會(huì)生成子類的虛表。這中虛表構(gòu)建的方式和其他一些語(yǔ)言會(huì)有所不同(比如C#),所以當(dāng)你在你的構(gòu)造函數(shù)中放置虛函數(shù)時(shí)(通常不建議這樣做),請(qǐng)保證你是真的要使用本身版本的虛函數(shù)。
默認(rèn)析構(gòu)函數(shù)與默認(rèn)構(gòu)造函數(shù)的問(wèn)題類似。即,只有在編譯期需要的時(shí)候生成,目地是完成清理的工作,與空間回收無(wú)關(guān)。而且,析構(gòu)函數(shù)并不一定要于構(gòu)造函數(shù)配對(duì)。比如類中只是有一個(gè)虛函數(shù)的時(shí)候,就不必生成默認(rèn)析構(gòu)函數(shù)。所有的析構(gòu)和構(gòu)造對(duì)稱出現(xiàn)的想法,都只是我們出于美學(xué)的主觀臆斷罷了。而對(duì)析構(gòu)函數(shù)的擴(kuò)展,其順序與構(gòu)造函數(shù)相反,這一點(diǎn)倒是符合了對(duì)稱性。
【拷貝構(gòu)造函數(shù)】
C++的每個(gè)類型中都有一個(gè)拷貝構(gòu)造函數(shù)(伴隨著一個(gè)復(fù)制操作符),如果類中沒有顯性的聲明,系統(tǒng)會(huì)安插一個(gè)(想一想,為什么是必須添加的),以完成由一個(gè)已有對(duì)象復(fù)制生成新的對(duì)象。拷貝構(gòu)造函數(shù)可以分成兩種。一種是按位拷貝(bitwise copy),另一種是按成員拷貝(member copy)。所謂按位拷貝就是指把一個(gè)對(duì)象在內(nèi)存中的每一位,一一拷貝到另一個(gè)新的位置,它的特點(diǎn)是拷貝速度快。而按函數(shù)拷貝就是分別調(diào)用各成員的拷貝構(gòu)造函數(shù),依次拷貝所需的成員,其特點(diǎn)是控制力強(qiáng),可以選擇需要拷貝的成員,或者對(duì)成員做特殊的處理。系統(tǒng)自動(dòng)生成的拷貝構(gòu)造函數(shù)通常是按位拷貝,但在某些情況(這些情況和上面生成構(gòu)造函數(shù)的情況類似)下會(huì)采用按成員拷貝。
為什么需要按函數(shù)拷貝,舉個(gè)例子就理解這個(gè)問(wèn)題。假設(shè)下面這樣一個(gè)繼承體系:設(shè)定B類是A類的子類,且A, B的對(duì)象大小不等,并B重載A的一個(gè)虛方法。當(dāng)一個(gè)B對(duì)象拷貝到另一個(gè)B對(duì)象中,按位拷貝不會(huì)出現(xiàn)問(wèn)題。但如果把一個(gè)B對(duì)象拷貝到一個(gè)A對(duì)象上,再簡(jiǎn)單的應(yīng)用按位拷貝就會(huì)出現(xiàn)問(wèn)題。可能vptr被切割掉了(如果vptr放在對(duì)象的最后),要不即使不被切割掉也不會(huì)被正確設(shè)置(A對(duì)象和B對(duì)象的虛指針指向了同一個(gè)虛表)。因此在這種場(chǎng)合下,按位拷貝就不夠用了,系統(tǒng)會(huì)安插一個(gè)具有成員拷貝的拷貝構(gòu)造函數(shù),對(duì)虛指針的拷貝問(wèn)題進(jìn)行特別的處理。
理解這個(gè)問(wèn)題的益處在于可以幫助寫出更有效的代碼。通常我們寫類的拷貝構(gòu)造函數(shù)都會(huì)采用成員拷貝的方式。但其實(shí)很多時(shí)候更好的方式是先做一個(gè)按位拷貝,再修改不符合按位拷貝的內(nèi)容。這樣做不僅編碼簡(jiǎn)單清晰,運(yùn)行的也更加高效。
理解該問(wèn)題另一個(gè)益處是幫助我們理解自定義拷貝構(gòu)造函數(shù)的時(shí)機(jī)。編譯器安插的拷貝構(gòu)造函數(shù),無(wú)論是哪種方式,都是一個(gè)淺拷貝。因?yàn)檫@對(duì)于編譯器來(lái)說(shuō),以足夠保證它不會(huì)出錯(cuò)了。但如果我們類型中涉及到指針類型的成員時(shí)候,我們可能需要的是一個(gè)深拷貝。這對(duì)于編譯器來(lái)說(shuō)不重要,但對(duì)于保證我們所需的程序邏輯是很重要的。只有在這種編譯器無(wú)法滿足需求的情況下才使用自定義拷貝構(gòu)造函數(shù),除此之外都不用畫蛇添足。因?yàn)椴坏黾恿斯ぷ髁浚€很有可能降低了效率(當(dāng)用成員拷貝頂替了位拷貝時(shí))。
【動(dòng)態(tài)分配】
眾所周知,C++在運(yùn)行期間在堆上管理內(nèi)存通常采用new和delete操作符。但new操作有時(shí)候不僅僅是等價(jià)于調(diào)用new操作符以完成空間的分配。比如int *pi = new int(5)。它相當(dāng)于先調(diào)用new操作符:int *pi = __new(sizeof(int)),用以完成空間分配;再調(diào)用*pi = 5,用以完成初值設(shè)定。而深入一些來(lái)說(shuō),new操作符所作的內(nèi)存分配也不是簡(jiǎn)單的按需分配。當(dāng)分配失敗的時(shí)候,會(huì)調(diào)用new_handler來(lái)進(jìn)行一些處理(可能包括回收空間,輸出信息等等),然后再重新分配直至分配成功或new_handler不再處理位置。因此,我們有時(shí)候只需要重寫new_handler的行為就可以達(dá)到我們的目的(比如自定義重分配方式),而不再需要重寫new操作符,這樣降低了工作的難度。只有真正需要改變內(nèi)存分配策略的時(shí)候,才需要重載new操作符。
相比較delete操作符所做的事情比較純粹,就是釋放對(duì)象占用的堆中的空間。但我們?nèi)绾沃缹?duì)象占用了多大的空間,我們?cè)撫尫哦嗌倏臻g呢?常規(guī)的做法是在new的時(shí)候安插一個(gè)cookie,里面存放對(duì)象大小等信息,在delete的時(shí)候,函數(shù)訪問(wèn)該cookie,重而保證正確的釋放。很明顯,cookie的插入增加了對(duì)象的大小,降低了操作的效率,還會(huì)出現(xiàn)安全性的問(wèn)題。所以當(dāng)程序中有大量的小對(duì)象存在,或者是需要頻繁獲取和釋放空間的時(shí)候,往往會(huì)常用一些更具技巧性的分配方案,以滿足需求(比如,池化分配)。
數(shù)組的分配與單個(gè)對(duì)象的分配問(wèn)題類似,可以簡(jiǎn)單的視為連續(xù)分配n個(gè)對(duì)象空間。只是為了保證delete的正確執(zhí)行,在數(shù)組對(duì)象分配的時(shí)候,還需要多安插一些信息。
而除了常見的new操作符外,C++還支持另一種被稱為Placement operator new的操作符,它多接受一個(gè)void*指針類型,默認(rèn)情況下,是用于在指定地址開始分配空間(更確切的說(shuō)法應(yīng)該在該子句前加上優(yōu)先)。當(dāng)然也有的時(shí)候,通過(guò)重載,void*也會(huì)指向其他對(duì)象(比如ostream對(duì)象),用已完成其他的特殊功能(比如書寫日志)。不論哪種,都需要保證placement new和placement delete成對(duì)出現(xiàn),這樣才能完成正常的空間回收。
【變量管理】
當(dāng)在一個(gè)函數(shù)中構(gòu)造了一個(gè)對(duì)象(棧中),會(huì)在離開函數(shù)域的時(shí)候析構(gòu)這個(gè)對(duì)象。如果在函數(shù)頭構(gòu)造了該對(duì)象,而該函數(shù)有多個(gè)出口(return語(yǔ)句),編譯器就會(huì)在每個(gè)出口前調(diào)用該對(duì)象析構(gòu)函數(shù)。因此,我們最好在第一次使用該對(duì)象前構(gòu)造該對(duì)象,這不僅僅是一種良好的編程習(xí)慣,有利于閱讀和理解,并且也是一種減少無(wú)謂的代碼損失的好方法。
另外一個(gè)特殊變量是全局變量。程序中任何代碼調(diào)用全局變量的時(shí)候,都要保證全局變量已經(jīng)按要求完成初始化。因此,系統(tǒng)會(huì)需要在進(jìn)入main函數(shù)后首先初始化全局變量,在離開main函數(shù)前,清理全局變量。一種實(shí)現(xiàn)方法,是在編譯每個(gè)全局變量的時(shí)候,為該變量添加一個(gè)用于構(gòu)造的函數(shù)(eg. __sti_xxx())和用于釋放的函數(shù)(eg. __std_xxx())。完成編譯后,還需要收集所有用于全局變量構(gòu)造和釋放的函數(shù)把它們放入兩個(gè)特殊的函數(shù)中(eg. _main()和exit())。把它們分別放在main()函數(shù)的入口和出口處。用于在進(jìn)入main后初始化和離開main前解構(gòu)。收集的具體實(shí)現(xiàn)取決于編譯器,如果編譯器支持跨平臺(tái)性,對(duì)文件的操作要特別小心,避免調(diào)用平臺(tái)文件格式相關(guān)的操作代碼。