在C語言中如何存儲(chǔ)并初始化成員變量
成員變量必須在構(gòu)造函數(shù)的初始化列表中完成初始化。Smart pointer members minimize dependencies while allowing exception safety。
通過以指針存儲(chǔ)成員變量的方法最小化依賴
當(dāng)成員變量的頭文件非常大或者非常復(fù)雜;或者當(dāng)你有大量的數(shù)據(jù)成員,并且不想減慢編譯速度和強(qiáng)化相互依賴時(shí)。你會(huì)怎么做?簡(jiǎn)單來說就是將成員變量保存為指針形式,并用在類的構(gòu)造函數(shù)中使用new為其分配空間。(在某種特殊的情況下,可用引用形式的成員變量代替)。同樣要確保在析構(gòu)函數(shù)中刪除它們。下面是一段雛形代碼。
// User.h
class PointerMember;
class RefParam;
class User
{
public:
User( const RefParam &inParam );
virtual ~User();
private:
PointerMember *mPointerMember;
};
// User.cpp
#include "User.h"
User::User( const RefParam &inParam )
: mPointerMember( new PointerMember( inParam ) )
{
return;
}
User::~User()
{
delete mPointerMember;
return;
}
這樣當(dāng)你要使用成員變量時(shí),原來使用mValMember.Something的地方就要用mPointerMember->Something了。文本編輯器或者集成開發(fā)環(huán)境的查詢替換方法可以很容易地在切換存儲(chǔ)方法。
初始化列表
注意,在構(gòu)造函數(shù)初始化列表中初始化對(duì)象的指針成員(可以是任何類型成員)是非常重要的。對(duì)于C++的初學(xué)者來說,像上面的例子中所看到的,下面語句位于大括號(hào)之前看起來感覺非常別扭。
: mPointerMember( new PointerMember( inParam ) )
在類對(duì)象的生命周期中,如果實(shí)際應(yīng)用時(shí)不需要經(jīng)常使用指針成員變量時(shí),可以選擇將該指針成員初始化為nil(注意:刪除一個(gè)nil指針永遠(yuǎn)是安全的。因?yàn)閐elete方法的實(shí)現(xiàn)在將指針變量傳遞給堆管理器前,首先檢驗(yàn)指針的值)。如果指針變量需要在構(gòu)造之前分配存儲(chǔ)空間的話,一定要在初始化列表中完成,而不像下面代碼一樣在構(gòu)造函數(shù)體中完成。
User::User( const RefParam &inParam )
{
mPointerMember = new PointerMember( inParam ); // DON'T DO THIS
return;
}
我所工作的大型C++項(xiàng)目中,那些很少使用初始化列表初始化成員變量的,都到處充斥著錯(cuò)誤。其中有一個(gè)項(xiàng)目,源碼共70多兆,我在那家公司工作的時(shí)候除了調(diào)試錯(cuò)誤沒做其他任何事情。搞定了一摞錯(cuò)誤,又會(huì)出現(xiàn)一筐錯(cuò)誤。適當(dāng)?shù)某跏蓟蓡T變量失敗不只是代碼的問題,還與更高層次問題相關(guān)。
一般來說,構(gòu)造函數(shù)體應(yīng)該只用來開展對(duì)成員變量的操作,或者是全部完成初始化后對(duì)整個(gè)對(duì)象的操作?;驹瓌t是保留函數(shù)體給不適合由初始化列表完成的代碼。
開始學(xué)習(xí)適當(dāng)?shù)氖褂贸跏蓟斜硪詠?,在寫信?gòu)造函數(shù)或者重寫老的構(gòu)造函數(shù)后,函數(shù)體往往是空的,或者僅包含不多的幾行代碼,因?yàn)槿康膶?shí)際工作都在初始化列表中完成了。要完成這些工作有時(shí)候需要一些額外的工作,但是最后還是能把這些工作量找回來的。
注意了,初始化列表是引用型和常量型成員變量初始化的唯一地方,如果在初始化列表中初始常量成員變量失敗了,它可能已在默認(rèn)構(gòu)造函數(shù)中初始化了,你在其他任何地方都不能改變它,構(gòu)造函數(shù)體也不例外。如果以這種方式初始化引用型成員變量失敗,代碼就不能通過編譯。下面的代碼在g++中將發(fā)生致命錯(cuò)誤。
class HasRefMember
{
public:
HasRefMember( int &inIntToAlias );
private:
int &mSomebodyElsesInt;
};
HasRefMember::HasRefMember( int &inIntToAlias ) // No
initialization list!
{ // refinit.cpp: In method `HasRefMember::HasRefMember(int &)':
// refinit.cpp:11: uninitialized reference member `HasRefMember::mSomebodyElsesInt'
mSomebodyElsesInt = inIntToAlias;// The compiler doesn't even get this far
}
關(guān)于初始化列表的其他一些注意事項(xiàng):在初始化列表中,成員變量的初始化順序要與類型生命中的順序一致。實(shí)際情況下,C++編譯器總是按照變量聲明的順序初始化成員變量。初始化列表順序與聲明相匹配將避免混淆。進(jìn)一步說,如果你理解了成員變量按照一定的順序初始化的話,你可以安排順序使得構(gòu)造函數(shù)的后面的變量使用前面的變量作為參數(shù)。如下例所示。
class Example
{
public:
Example( double inVal );
private:
double mSqrt;
double m2Sqrt;
};
Example::Example( double inVal )
: mSqrt( sqrt( inVal ) ),
m2Sqrt( mSqrt * 2 )
{
return;
}
如果我們改變了了成員變量的聲明順序,但是保留初始化列表原樣不變的話,m2Sqrt將初始化為內(nèi)存殘留的垃圾值(一個(gè)未定義的值),mSqrt將初始化為inVal的平方根。
因此,如果要改變類聲明中成員變量的順序的話,確保同時(shí)檢驗(yàn)并更新初始化列表。周期性地檢驗(yàn)程序中的構(gòu)造函數(shù),以確保成員變量的正確順序。一些編譯器會(huì)非常友好的提示發(fā)生的順序錯(cuò)誤。
對(duì)于初始化列表中的一些語法方面的局限性,需要另外的一些工作才能行得通。
列表中的每一項(xiàng)都是一次對(duì)成員變量構(gòu)造函數(shù)的調(diào)用。某些類型看起來沒有調(diào)用構(gòu)造函數(shù),但你可以用一個(gè)值或者相同類型的對(duì)象的引用(你調(diào)用了拷貝構(gòu)造函數(shù))來初始化它。如果你沒有重寫自己的拷貝構(gòu)造函數(shù)的話,編譯器將會(huì)提供一個(gè)默認(rèn)的拷貝構(gòu)造函數(shù)(盡管默認(rèn)構(gòu)造函數(shù)不能提供正確的功能)。僅僅分配了內(nèi)置數(shù)據(jù)類型的構(gòu)造。
構(gòu)造函數(shù)的參數(shù)可以僅僅是一串由逗號(hào)分割的0值或者表達(dá)式,不可以是聲明語句、基本塊或者對(duì)無返回值類型的函數(shù)的調(diào)用??梢允欠祷貙?duì)應(yīng)數(shù)據(jù)類型的一個(gè)函數(shù)(例如傳遞給拷貝構(gòu)造函數(shù)一個(gè)值)。有一點(diǎn)很重要,你不能使用loop循環(huán)或者if語句。如果需要的話,就只能放在調(diào)用初始化的子過程中了。
不要在初始化列表中調(diào)用非靜態(tài)的成員函數(shù)。對(duì)象還沒有完全創(chuàng)建之前,如果你或者將來某個(gè)程序維護(hù)者引用一個(gè)還沒有初始化的成員時(shí),將會(huì)導(dǎo)致未定義的行為之類的結(jié)果。如果需要寫一個(gè)子過程來計(jì)算得到一個(gè)參數(shù)給一個(gè)構(gòu)造成員的話,聲明該字過程為靜態(tài)并顯示地給他傳遞它可能用到的參數(shù),但只能傳遞那些在調(diào)用它時(shí)已經(jīng)構(gòu)造好的參數(shù),而且不要傳遞this指針。
注意:可能需要調(diào)用基類的成員變量,因?yàn)榛愐呀?jīng)充分的構(gòu)造好了,還可以調(diào)用在其他類中定義的函數(shù),只要不傳遞this指針作為參數(shù),同樣是因?yàn)閷?duì)象還沒有完全構(gòu)造好。
我謹(jǐn)慎的建議,在希望接受的函數(shù)是基類的成員函數(shù)指針時(shí),是可以傳遞this指針給初始化列表的。這是因?yàn)樵谧宇惖臉?gòu)造函數(shù)調(diào)用時(shí),基類對(duì)象已經(jīng)完全構(gòu)造好了。這種情況只適用于所用的指針必須為基類指針的情況,相反的情況是不允許的,這正體現(xiàn)了封裝——作為一個(gè)集成類對(duì)象,在它起作用的位置,不能改變基類部分的特征。
有時(shí)你可能想要在初始化中調(diào)用一個(gè)子過程??赡苣悴幌雽懸粋€(gè)只在一個(gè)地方用到的完整的子過程。也可能你不想在類的頭文件中聲明子過程原型,從而引起所有依賴該頭文件的源碼的漫長(zhǎng)的重編譯過程。也可能構(gòu)造函數(shù)需要頻繁調(diào)用,而你避開子過程調(diào)用的開銷(這種情況考慮使用內(nèi)聯(lián)函數(shù))。還有可能是子過程的返回值是一個(gè)很大的對(duì)象,構(gòu)造、拷貝并銷毀原對(duì)象將導(dǎo)致很大一筆運(yùn)行時(shí)開銷。
這里順帶介紹一下條件表達(dá)式操作符:“?:”。它時(shí)對(duì)值進(jìn)行判斷的if語句的縮寫,因此,他可以作為一個(gè)表達(dá)式用于初始化列表。很多人因?yàn)楦杏X他晦澀而不喜歡使用它,我感覺表達(dá)式復(fù)雜時(shí)它能夠勝任。但它只是用于初始化列表參數(shù)的情況,要不然你就要些一個(gè)包含if語句的子過程了。
#include
class InitExample
{
public:
InitExample( std::string const &inFileName, bool inWritable );
private:
int mFileDescriptor;
};
#include
#include
#include
#include
InitExample::InitExample( std::string const &inFileName, bool inWritable )
: mFileDescriptor( open( inFileName.c_str(),
inWritable ? O_RDWR : O_RDONLY ) )
{
if ( mFileDescriptor < 0 )
throw std::exception();
return;
}
到最后,有時(shí)候初始化列表變得很長(zhǎng)很難閱讀。從這一點(diǎn)上來看,就要考慮是否你的類包含了太多的成員。也許尋找一組位于不同類中的自然協(xié)作的成員更合適,最后通過組合來實(shí)現(xiàn)最初的類。
來源:2008前進(jìn)0次