《Effective C++》第三版-3. 資源管理(Resource Management)

Roanapur發表於2024-04-30

目錄
  • 條款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的次序不一定。若編譯器選擇以下次序:

  1. 執行new Widget
  2. 呼叫priority
  3. 呼叫tr1::shared_ptr建構函式

則如果呼叫priority出現異常,那new Widget返回的指標將遺失,其未來得及放入tr1::shared_ptr內,進而導致資源洩漏。即建立資源和資源轉換為資源管理物件直接有可能發生異常干擾

解決方法:使用分離語句,因為編譯器對於跨越語句的各項操作沒有重新排列的自由

std::tr1::shared_ptr<Widget> pw(new Widget);
processWidget(pw, priority());  //這樣呼叫不會洩漏

Tips:

  • 以獨立語句將newed物件儲存於智慧指標內,否則一旦有異常則可能發生隱秘的資源洩漏

相關文章