EffictiveC++筆記 第3章
我根據自己的理解,對原文的精華部分進行了提煉,並在一些難以理解的地方加上了自己的“可能比較準確”的「翻譯」。
Chapter 3 資源管理
條款13: 以對象管理資源
有時即使你順利地寫了對應對象的delete語句,但是前面的區域可能會有一個過早的return語句或者拋出了異常.它們一旦執行,控制流絕不會觸及delete語句,造成內存泄漏
事實上我們可以將需要的動態資源放進對象內,因為對象的析構函數會自動釋放那些資源
c++對應的解決方案有 auto_ptr,也就是所謂的智能指針,它其實是類指針(pointer-like)對象,其析構函數自動對其所指對象調用delete,可一定程度地避免潛在的資源泄漏可能性
先假設一個名叫func的函數,它會new一個xx類型的對象並返回其指針,你可以這樣使用auto_ptr :
std::auto_ptr<xx> pt(func());
這個簡單例子示範兩個關鍵想法:
::獲得資源後立即放進管理對象; 管理對象運用析構函數確保資源被釋放::
由於auto_ptr被銷毀會自動刪除所指之物,所以註意別讓多個auto_ptr同時指向一個對象。後果是對象很可能會被刪除一次以上,那將使你的程序搭上駛向“未定義行為”的列車上。
為預防這個問題,auto_ptr有一個性質: 若通過copying函數復制它們,它們會變成null,而復制所得指針將取得資源唯一擁有權:
std::auto_ptr<xx> pt1(func());
std::auto_ptr<xx> pt2(pt1); //現在pt2指向原本pt1所指對象,而pt1變成
//nullptr
pt1 = pt2; //現在pt1指向原本pt2所指對象,而pt2變成
//nullptr
可以看出auto_ptr的底層條件:受管理的資源必須絕對沒有一個以上的auto_ptrs指向它。對於STL容器,這個特性不是很好。
替代方案是“引用計數型智能指針(reference-counting smart pointer)”,
shared_ptr
它其實也是一個智能指針,持續追蹤共有多少對象指向某筆資源,::並在無人指向它時自動刪除該資源。::
對於RCSP,你可以這麽寫:std::shared_ptr<xx> pInv(func());
對於以下操作,RCSP相對於auto_ptr正常多了:
...
std::tr1::shared_ptr<xx> pInv1(func());
std::tr1::shared_ptr<xx>. pInv2(pInv1); //現在pInv2與1指向同一個對象
pInv1 = pInv2; //同上,無任何改變
...
//pInv1和pInv2被銷毀後,它們所指的對象也就被自動銷毀
綜上,RCSP很適合STL容器的操作。
值得註意的是,auto和shared_ptr兩者都在其析構函數內做delete而不是delete[]動作。這意味著在動態分配而得的array身上使用auto_ptr和tr1::shared_ptr是餿主意,然而這樣會通過編譯!
std::auto_ptr<std::string> aps(new std::string[10]);
std::tr1::shared_ptr<int> spi(new int[1024]);
條款14: 在資源管理類中小心copying行為
有可能你偶爾會發現,你需要建立自己的資源管理類
假設使用C API函數處理類型為Mutex的互斥器對象,共有lock和unlock兩函數可用:
void lock(Mutek* pm); void unlock(Mutek* pm);
可能需要建立一個class來管理機鎖:
class Lock{
public:
explicit Lock(Mutex* pm)
:mutexPtr(pm)
{ lock(mutexPtr); } //獲得資源
~Lock() { unlock(mutexPtr); } //釋放資源
private:
Mutex* mutexPtr;
};
現在導入一個觀念:
資源取得時機便是初始化時機(Resource Acquisition Is Initialization; RAII)
並以此作為“資源管理類的脊柱”
「客戶」對Lock的用法符合RAII方式:
Mutex m; //定義需要的互斥器
...
{
Lock ml(&m); //鎖定互斥器
...
//在區塊末尾解除鎖定
}
此時假設Lock對象被復制:
Lock ml1(&m);
鎖定m
Lock ml2(ml1);
將ml1復制到ml2身上,會發生啥?
大多數情況你會選擇下面兩種操作:
- 禁止復制. 許多時候允許RAII對象被復制並不合理,如果你發現不合理,就應該禁止之。
根據條款6,我們發現可以:將copying操作聲明為private.對Lock而言看起來是這樣:
class Lock: private Uncopyable{
public:
…
};
- 對底層資源祭出“引用計數法”. 有時我們希望保有資源,直到它的最後一個使用者(某對象)被銷毀。此時復制RAII對象時應將該資源的“被引用數”遞增。也就是使用tr1::shared_ptr
通常只需內含一個tr1::shared_ptr成員變量,RAII classes便可實現出reference-counting copying行為.
存在一個問題:shared_ptr缺省行為是“當引用次數為0時刪除所指物”,然而我們想要的動作是解除鎖定而非剔除。
幸運的是tr1::shared_ptr允許指定所謂的“刪除器(deleter)”——當引用次數為0時便被調用,它是一個函數或函數對象,對於指針是可有可無的第二參數:
class Lock{
public:
explicit Lock(Mutex* pm)
:mutexPtr(pm,unlock)
{
lock(mutexPtr.get()); //以後談到“get”
}
private:
std::tr1::shared_ptr<Mutex> mutexPtr;
};
註意,不再聲明析構函數,因為沒必要,編譯器有缺省行為
條款15: 在資源管理器中提供對原始資源的訪問
假設有這種情況:
使用RCSP智能指針保存factory函數的調用結果:
std::tr1::shared_ptr<Investment> pInv(createInvestment());
假設有一個函數處理investment對象:
int dayHeld(const Investment* pi); //返回投資天數
而你想這麽調用:
int days = daysHeld(pInv);
這樣通不過編譯,因為即使pInv指向的是investment對象,但是pInv本身是類型為tr1::shared_ptr
這時你需要一個函數來將RAII class對象(本例為tr1::shared_ptr) 轉換為其內含原始資源(本例為Investment*)。
有兩種辦法:顯式轉換和隱式轉換。
tr1::shared_ptr和auto_ptr都提供一個叫get的成員函數,用來執行顯式轉換,它將返回智能指針內部的原始指針的復件 :
int days = daysHeld(pink.get())
這兩種指針還重載了指針取值操作符(operator->和operator*),這將允許隱式轉換至原始指針:
class Investment{
public:
bool isTaxFree() const;
};
...
std::tr1::shared_ptr<Investment> pInv(createInvestment());
bool ok = pInv->isTaxFree();
bool ok1 = *(pInv).isTaxFree();
...
很多傳統的做法便是xx.get()來獲取內部資源,如果這種操作很繁瑣的話,有一種更輕松的做法(隱式轉換):
class XXX{
public:
...
operator xxx() const //隱式轉換函數,xxx是返回類型
{ return f; }
...
};
比如某個函數參數原來得這麽寫
func(xx.get())
現在你可以寫成func(xx)
但這種轉換可能會引發錯誤:
假設類A裏儲存的是B類對象,當B類對象想拷貝一個A類對象時,卻發生了隱式轉換:
A obj(getA());
...
B obj2 = obj; //喔唷!原意是拷貝一個A對象,卻將obj隱式轉換為其底部的B對象了,然後再復制
很多時候顯式轉換更受歡迎
條款16: 成對使用new和delete時要采取相同形式
看一下這段代碼:
std::string* stringArray = new std::string[100];
…
delete stringArray;
很明顯此程序行為不明確。對象數組所含的100個string對象中的99
個不太可能被適當刪除,因為它們的析構函數很可能沒被調用。
delete最大的問題在於:::即將被刪除的內存之內究竟有多少對象?::這個問題決定了有多少對象的析構函數必須被調用起來。
也就是, 即將被刪除的指針,所指的是單一對象或對象數組? 數組所用的內存通常還包括“數組大小”的記錄,以便delete知道需要調用多少次析構函數。
所以記住,刪除對象數組要用 delete[] xxx;
這種規則對typedef的使用也很重要:
typedef std::string AddressLines[4];
std::string* pal = new AddressLines;
此時AddressLines是一個數組,如果這樣new:
std::string* pal = new AddressLines;
就必須匹配 delete[] pal
盡量不要對數組做typedef動作,你大可用STL中的vector等templates
條款17: 以獨立語句將newed對象置入智能指針
假設有這樣一個函數:
void process(std::tr1::shared_ptr<Widget> pw);
process將對傳來的Widget對象運用智能指針
現在考慮調用函數:
process(new Widget);
嘿嘿,這是不能通過編譯的。因為tr1::shared_ptr構造函數需要一個原始指針(raw pointer),但該構造函數是explicit的,無法隱式轉換,可以這麽寫:
process(std::tr1::shared_ptr<Widget>(new Widget));//實際上仍有風險
假設process函數有第二個參數func(),它是一個函數的返回結果:
process(std::tr1::shared_ptr<Widget>(new Widget),func());
調用process之前,編譯器必須創建代碼,做以下三件事:
- 調用func()
- 執行”new Widget”
- 調用tr1::shared_ptr構造函數
對於c++,此行編譯時,有可能編譯器先執行”new Widget”操作:
- 執行”new Widget”
- 調用func()
- 調用tr1::shared_ptr構造函數
然而如果此時func()調用導致異常,”new Widget”返回的指針將會遺失,因為它尚未被置入tr1::shared_ptr內,這將引發資源泄漏。
我們可以使用分離語句,將異常幹擾減小:
std::tr1::shared_ptr<Widget> pw(new Widget);
process(pw,func());
OVER
EffictiveC++筆記 第3章