- 條款13:以物件管理資源(Use objects to manage resources)
- 關鍵想法
- 智慧指標
- 條款14:在資源管理類中小心copying行為(Think carefully about copying behavior in resource-managing classes)
- 條款15:在資源管理類中替工對原始資源的訪問(Provide access to raw resources in resource-managing classes)
- 顯示轉換或隱式轉換
- 優缺點
- 條款16:成對使用new和delete時要採取相同形式(Use the same form in corresponding uses of new and delete)
- 條款17:以獨立語句獎newed物件置入智慧指標(Store newed objects in smart pointers in standalone statements)
前幾章的筆記多有不足,這一章會持續改進
條款13:以物件管理資源(Use objects to manage resources)
關鍵想法
考慮以下易出錯的例子:
class Investment { ... }; //投資型別繼承體系中的root類
//工廠函式,指向Investment繼承體系內的動態分配物件,引數省略
Investment* createInvestment {};
void f()
{
Investment* pInv = createInvestment(); //呼叫工廠函式
... //若這裡return則無法執行delete
delete pInv; //釋放pInv所指物件
}
解決方案:把資源放進物件,可利用解構函式自動呼叫機制確保資源釋放
以物件管理資源的兩個關鍵想法:
- 獲得資源後立刻放進管理物件(managing object)內
- 資源取得時機便是初始化時機(Resource Acquisition Is Initialization,RAII)
- 有時獲得的資源會拿來賦值而非初始化
- 管理物件運用解構函式確保資源釋放
- 不論控制流如何離開區塊,一旦物件被銷燬(如離開物件作用域)其解構函式會自動呼叫
智慧指標
auto_ptr:
- 透過copy建構函式或copy assignment運算子複製它們,它們會變成null,複製所得的指標將取得資源的唯一所有權
- 故需要元素能夠複製地STL容器不相容auto_ptr
auto_ptr在C++11中已被棄用,以下簡要介紹
void f()
{
std::auto_ptr<Investment> pInv(createInvestment());
...
} //auto_ptr的解構函式自動刪除pInv
std::auto_ptr<Investment< pInv1(createInvestment());
std::auto_ptr<Investment< pInv2(pInv1); //現在pInv2指向物件,pInv1為null
pInv1 = pInv2; //現在pInv1指向物件,pInv2為null
shared_ptr:屬於引用計數型智慧指標(reference-counting smart pointer,RCSP)
- 會持續追蹤有多少物件指向某筆資源,並在無人指向它時自動刪除該資源
- 其行為類似垃圾回收(garbage collection),但RCSP無法打破環狀引用(cycles of references,如兩個未被使用的物件彼此互指)
- 適用於STL容器
void f()
{
std::tr1::shared_ptr<Investment> pInv(createInvestment());
...
} //shared_ptr的解構函式自動刪除pInv
void f()
{
...
std::tr1::shared_ptr<Investment> pInv1(createInvestment());
std::tr1::shared_ptr<Investment> pInv2(pInv1); //指向同一個物件
pInv1 = pInv2; //同上
...
} //pInv1和pInv2被銷燬,他們所指的物件也被銷燬
auto_ptr和shared_ptr的解構函式做delete而非delete[],故不適合在動態分配而得的array身上使用,即使能透過編譯
Boost中boost::scoped_array和boost::shared_array則可用於陣列且類似auto_ptr和shared_ptr
std::auto_ptr<std::string> aps(new std::string[10]); //會呼叫錯誤形式的delete
std::tr1::shared_ptr<int> spi(new int[1024]); //同上
Tips:
- 為防止資源洩漏,請使用RAII物件,其在建構函式中獲得資源並在解構函式中釋放資源
- RAII中常用shared_ptr,其copy行為比較直觀(auto_ptr已被棄用)
條款14:在資源管理類中小心copying行為(Think carefully about copying behavior in resource-managing classes)
對mutex一點不瞭解emmm,硬著頭皮總結下
非heap-based的資源不適合使用智慧指標作為資源掌管者(resource handlers)
考慮使用C API函式處理型別為Mutex的互斥器物件(mutex objects),共有lock和unlock兩函式可用。為確保不會忘記把被鎖的Mutex解鎖,可建立類以管理機鎖,該類的結構符合RAII守則
void lock(Mutex* pm); //鎖定pm所指的互斥器
void unlock(Mutex* pm); //解除互斥器的鎖定
//管理機鎖的類,符合RAII守則
class Lock {
public:
explicit Lock(Mutex* pm)
: mutexPrt(pm)
{ lock(mutexPtr); }
~Lock() { unlock(mutexPtr); }
private:
Mutex *mutexPtr;
};
//客戶對Lock的用法符合RAII方式
Mutex m;
...
{
Lock ml(&m);
...
}
如果要複製Lock物件,則可能:
Lock m11(&m); //鎖定m
Lock m12(m11); //將m11複製到m12上
- 禁止複製
- 很多時候允許RAII物件複製不合理(則應將copying操作宣告為private)、
- 但Lock類是少有的能合理擁有同步化基礎器物(synchronization primitives)的副本,其可能可以被複制
- 對底層資源使用引用計數法
- 和RAII規則類似,但是當使用上一個Mutex時需要解除鎖定而非刪除
- tr1::shared_ptr允許指定刪除器(deleter),其為函式或函式物件,當引用次數為0時呼叫
class Lock {
public:
explicit Lock(Mutex* pm) //以Mutex初始化shared_ptr
: mutexPtr(pm, unlock) //以unlock函式作為刪除器
{
lock(mutexPtr.get());
}
private:
std::tr1::shared_ptr<Mutex> mutexPtr; //使用shared_ptr替換raw pointer
};
- 複製底部資源,即進行深複製(deep copying),如將指標及其所指物件都複製
- 轉移底部資源的擁有權
- 少數情況需要確保只有一個RAII指向一個未加工資源(raw resource),即使RAII物件被複制
- 此時資源的所有權會從被複制物轉移到目標物
Tips:
- 複製RAII物件必須一併複製它所管理的資源,故資源的copying行為決定RAII物件的copying行為
- 通常的RAII類的copying行為是:抑制copying、使用引用計數法。但其他行為也可能實現
條款15:在資源管理類中替工對原始資源的訪問(Provide access to raw resources in resource-managing classes)
由於auto_ptr已棄用,本條款不整理和其相關的內容
顯示轉換或隱式轉換
有時智慧指標不能直接使用(如下例子),需要顯示轉換或隱式轉換:
class Investment {
public:
bool isTaxFree() const;
...
}
Investment* createInvestment(); //工廠函式
std::tr1::shared_ptr<Investment> pInv(createInvestment());
int daysHeld(const Investment* pi); //返回投資天數
int days = daysHeld(pInv); //錯誤!daysHeld需要Investment*而非tr1::shared_ptr
//顯示轉換
int days = daysHeld(pInv.get());
//隱式轉換,tr1::share_ptr過載了指標取值(pointer dereferencing)運算子(->和*)
bool taxable1 = !(pInv->isTaxFree());
bool taxable2 = !((*pInv).isTaxFree());
優缺點
有時必須取得RAII物件內的原始資源,考慮用於字型的RAII類:
FontHandle getFont(); //這是C API,省略引數
void releaseFont(FontHandle fh); //來自同一組
class Font { //RAII類
public:
explicit Font(FontHandle fh) //獲得資源
: f(fh) //使用pass-by-value,因為C API這樣做
{ }
~Font() { releaseFont(f); } //釋放資源
private:
FontHandle f; //原始字型資源
};
- 顯示轉換:可讀性強,但是需要API時必須呼叫get
class Font {
public:
...
FontHandle get() const { return f; } //顯式轉換函式
...
};
void changeFontSIze(FontHandle f, int newSize);
Font f(getFont());
int newFontSize;
...
changeFontSize(f.get(), newFontSize);
- 隱式轉換:呼叫C API更自然,但是易出錯
- 可能在需要Font時意外建立FontHandle,且f被銷燬則f0成為懸空的(dangle)
class Font { //RAII類
public:
...
operator FontHandle() const { return f; } //隱式轉換函式
...
};
changeFontSize(f, newFontSize)
FontHanle f0 = f; //想要複製,但是將f1隱式轉換為其底部的FontHandle才複製
Tips:
- API往往要求訪問原始資源,故RAII類應提供取得其管理的資源的方法
- 對原始資源的訪問包含顯示轉換和隱式轉換,一般顯示轉換安全而隱式轉換方便
條款16:成對使用new和delete時要採取相同形式(Use the same form in corresponding uses of new and delete)
std::string* stringArray = new std::string[100];
...
delete stringArray
以上程式的行為不明確:
- 使用new時會發生兩件事:
- 記憶體分配
- 呼叫針對此記憶體的建構函式
- delete需要知道被刪除的記憶體內有多少物件,其決定了要呼叫多少解構函式
- 若指標指向陣列物件,則陣列所用的記憶體包含陣列大小資訊
- 若指標指向單一物件,則無上述資訊
- 需要人為告訴delete所刪除的物件型別
std::string* stringPtr1 = new std::string;
std::string* stringPtr2 = new std::string[100];
...
delete stringPtr1; //刪除物件
delete [ ] stringPtr2; //刪除物件組成的陣列
使用typedef需要考慮相同的問題(下例中陣列使用typedef並不合適,容易產生錯誤,僅做說明):
typedef std::string AddressLines[4]; //地址有4行,每行一個string
std::string* pal = new AddressLines; //本質上同new string[4]
delete pal; //錯誤!行為未有定義
delete [ ] pal; //正確
Tips:
- 如果new加上[],則delete必須加上[];如果new沒加[],則delete不能加[]
條款17:以獨立語句獎newed物件置入智慧指標(Store newed objects in smart pointers in standalone statements)
考慮涉及優先權的例子:
int priority();
void processWidget(std::tr1::shared_ptr<Widget> pw, int priority);
//錯誤!new Widget無法隱式轉換為shared_ptr,因為shared_ptr的建構函式為explicit
processWidget(new Widget, priority());
//可以透過編譯,但可能洩漏資源
processWidget(std::tr1::shared_ptr<Widget>(new Widget), priority());
編譯器產出processWidget呼叫碼之前,必須首先核酸即將被傳遞的各個實參,其要做事情有三件:
- 第一實參
- 執行new Widget
- 呼叫tr1::shared_ptr建構函式
- 第二實參
- 呼叫priority
實際執行的次序彈性很大,只能確定執行new Widget一定先於呼叫tr1::shared_ptr建構函式,但呼叫priority的次序不一定。若編譯器選擇以下次序:
- 執行new Widget
- 呼叫priority
- 呼叫tr1::shared_ptr建構函式
則如果呼叫priority出現異常,那new Widget返回的指標將遺失,其未來得及放入tr1::shared_ptr內,進而導致資源洩漏。即建立資源和資源轉換為資源管理物件直接有可能發生異常干擾
解決方法:使用分離語句,因為編譯器對於跨越語句的各項操作沒有重新排列的自由
std::tr1::shared_ptr<Widget> pw(new Widget);
processWidget(pw, priority()); //這樣呼叫不會洩漏
Tips:
- 以獨立語句將newed物件儲存於智慧指標內,否則一旦有異常則可能發生隱秘的資源洩漏