上一章講過了關于類對象內(nèi)存分布,對于nostatic數(shù)據(jù)將會放在對象內(nèi)存空間中惊橱,static數(shù)據(jù)成員和nostatic局荚、static函數(shù)成員將不會放在對象內(nèi)存中,對于虛擬繼承和含有虛函數(shù)的類來說钞澳,將會在對象內(nèi)存中增加一個虛表指針怠惶,指向該類的虛表,其中虛表中將會存放虛函數(shù)的地址和虛擬基類的地址轧粟。一個類中只含有一個共享虛表(繼承基類的虛表也是繼承類的虛表策治,一般繼承類的虛函數(shù)會存放在第一個基類的虛表中方便提取)兰吟,對象中可以含有多個虛指針通惫,繼承至基類,除了直接虛擬繼承的繼承類才會產(chǎn)生新的vptr和虛表用于指示虛擬基類的位置混蔼。
下面是如何構建類對象履腋,即構造函數(shù)的深入探索。
首先強調(diào)兩個C++新手容易陷入的誤區(qū):
(1)編譯器會為沒有顯示聲明構造函數(shù)的類合成默認構造函數(shù)惭嚣;-------------并不是所有的都合成默認構造函數(shù)遵湖,只有只有四種情況下才會合成,其他情況下不會合成料按。
(2)編譯器合成出來的默認構造函數(shù)會為每個數(shù)據(jù)成員賦予默認值奄侠。------------編譯器不負責對數(shù)據(jù)成員的賦值工作,并不是編譯器所需的载矿,而應該是程序員完成的工作垄潮。(除非其有默認構造函數(shù)的subobject和memeber在構造時會初始化)
那么烹卒,哪四種情況會導致編譯器為類合成一個默認的構造函數(shù)呢?
(1)類成員存在默認構造函數(shù)的弯洗,在合成默認構造函數(shù)時必須調(diào)用該成員的默認構造函數(shù)或者是擴充到已有的默認構造函數(shù)中旅急;
(2)所繼承的基類存在默認構造函數(shù)的,編譯器會合成默認構造函數(shù)牡整,在構造函數(shù)中調(diào)用基類默認構造函數(shù)藐吮;
(3)含有虛函數(shù)的類,編譯器會合成默認構造函數(shù)逃贝,主要是因為要初始化虛表指針谣辞;
(4)虛繼承的類,編譯器會合成默認構造函數(shù)沐扳,也是因為要初始化虛表指針泥从,指向虛擬基類對象。
當已經(jīng)顯示定義了構造函數(shù)沪摄,即使不是默認的構造函數(shù)躯嫉,編譯器也不會為類合成默認構造函數(shù)。如果顯示定義的構造函數(shù)沒有完成一些默認構造的功能杨拐,編譯器將會擴充顯示定義的構造函數(shù)祈餐,調(diào)用能夠調(diào)用的默認構造函數(shù)比如成員默認構造函數(shù)或基類默認構造函數(shù)。
復制構造函數(shù)深度探索:
復制構造函數(shù)只要用于三個方面:用一個對象的內(nèi)容構造出一個新對象哄陶;參數(shù)以值傳遞的形式傳遞時帆阳;以一個對象作為返回值時。在這三種情況下將會調(diào)用復制構造函數(shù)奕筐,有的情況下需要產(chǎn)生臨時對象舱痘。
例如:string s=a;將不會產(chǎn)生臨時對象离赫,string s=string(a);將會產(chǎn)生臨時對象t,然后在調(diào)用復制構造函數(shù)構造s塌碌;string s(a);也不會產(chǎn)生臨時對象渊胸。
當參數(shù)和返回值以值傳遞的形式,一般情況下都會產(chǎn)生臨時對象存儲數(shù)據(jù)台妆,當在編譯器NRV優(yōu)化(下面細講)的情況下翎猛,返回值可能不要產(chǎn)生臨時對象婶熬,而是直接操作畏梆,例如:
<pre>
T operator+(T &a,T &b); Tc=a+b;
</pre>
在編譯器優(yōu)化的情況下焕济,operator +函數(shù)極有可能是這個樣子的:
<pre>
//編譯器內(nèi)部偽碼
void operator +(T &a,T &b,T &c)
{
//直接對c進行操作芥被,這種情況下不需要產(chǎn)生構造對象
}
</pre>
復制構造的兩種方式:bitwise和memeberwise族展,前者是單純的位拷貝届腐,后者則是以成員為單位進行遞歸拷貝(所謂遞歸拷貝就是當成員為一個類對象時將會按照該對象的成員進行拷貝知道結束)鸵隧。另外粉臊,復制構造函數(shù)也會對數(shù)組的所有成員進行拷貝。
當類中沒有顯示定義的復制構造函數(shù)的時候遗座,編譯器會不會合成一個復制構造函數(shù)呢舀凛?要根據(jù)該類的復制構造形式而定,bitwise形式不會合成復制構造函數(shù)途蒋,memberwise形式會合成猛遍。
以下有四種情況會導致類不是bitwise形式,會在無顯示定義復制構造函數(shù)的時候合成默認復制構造函數(shù):
(1)類成員對象含所有顯示復制構造函數(shù)(無論是編譯器合成的還是自定義的号坡,有時候為了實現(xiàn)NRV優(yōu)化會自定義復制構造函數(shù))懊烤,編譯器會為這樣的類合成默認復制構造函數(shù);
(2)類的基類對象含有復制構造函數(shù)宽堆,編譯器會構造默認復制構造函數(shù)奸晴,構造的時候?qū){(diào)用擴充基類復制構造函數(shù);
(3)類中含有虛函數(shù)的日麸,編譯器將會構造默認復制構造函數(shù)寄啼,這樣可以保證當基類對象被繼承類對象初始化的時候,基類對象的vptr指向正確的虛函數(shù)表代箭,而不能單純的復制vptr那么容易墩划,否則將會造成基類對象的vptr指向繼承類的虛表了;
(4)虛擬繼承的類嗡综,編譯器將會構造默認復制構造函數(shù)乙帮,同樣是當基類對象被子類對象初始化時,必須保證指向虛擬基類對象的指針被正確設置极景,因為虛擬基類對象在不同子類內(nèi)存中的位置是不同的察净,在虛表中對應offset值是不同的,所以必須保證vptr所指向的虛表的正確性盼樟,因此編譯器將會為虛擬繼承的類合成默認復制構造函數(shù)氢卡。
NRV編譯器優(yōu)化:前提是編譯器提供該服務,且類中存在復制構造函數(shù)晨缴,無論是編譯器合成的還是自定義的译秦,否則優(yōu)化無從談起,因為trival復制構造函數(shù)就是最有效率的構造方法击碗,有些類為了使用NRV優(yōu)化甚至強行提供顯示復制構造函數(shù)筑悴。
NRV優(yōu)化主要用在返回值為對象的函數(shù)身上,主要方法是減少臨時對象的數(shù)目來提高效率稍途。
優(yōu)化前:
<pre>
X bar()//將會產(chǎn)生臨時對象存儲返回的值
{
X xx;
//處理
return xx;
}
</pre>
優(yōu)化后:
<pre>
void bar(X &_result)//這樣在指定函數(shù)返回對象是將不會產(chǎn)生臨時對象
{
X xx;
//處理xx
//調(diào)用復制構造函數(shù)
_result.X::X(xx);
return阁吝;
}
</pre>
X yy=bar();相當于bar(yy);
但是,當bar()單獨使用的時候械拍,仍然會產(chǎn)生一個臨時對象存儲結果突勇,因為函數(shù)沒有返回的對象装盯。
而NRV優(yōu)化又是在上述的基礎上直接對_result上處理,不需要在函數(shù)內(nèi)部產(chǎn)生一個xx對象与境,然后再調(diào)用復制構造函數(shù)验夯,這樣就減省了復制構造的消耗,但是會使用默認構造函數(shù)摔刁。
<pre>
void bar(X &_result)//NRV優(yōu)化
{
_result.X::X();
//對_result直接進行處理
return;
}
</pre>
成員初始化表的使用:
有以下三種情況必須使用成員初始化表:
(1)調(diào)用基類構造函數(shù)或者基類復制構造函數(shù)
(2)類對象中的引用初始化
(3)類中const對象的初始化
其成員初始化既可以使用成員初始化列表挥转,也可以使用普通的方式,但還是普通方式效率太低共屈。
普通初始化方式:首先將成員默認構造绑谣,然后產(chǎn)生一個臨時對象使用復制構造參數(shù)對象,最后使用賦值方式初始化拗引;
而成員初始化列表則會在用戶代碼之前按照成員聲明的順序并按相應的方式進行初始化借宵。
注意:
成員初始化列表中的順序不決定初始化順序,成員初始化順序由成員聲明的順序而定矾削;
成員初始化列表中的內(nèi)容先于用戶代碼執(zhí)行
原文地址:http://www.cnblogs.com/tracylee/archive/2012/12/18/2824125.html