天天看點

Effective C++ (三) 資源管理

程式需要管理的資源有哪些?

  • 動态配置設定的記憶體
  • 檔案描述符
  • 互斥鎖
  • UI中的字型和筆刷
  • 資料庫、socket連接配接

條款13 以對象管理資源

資源擷取即初始化(RAII,Resource Acquisition Is Initialization)是C++管理資源避免記憶體洩漏的方法。書中提到了共享指針shared_ptr和auto_ptr(從C++17開始被移除)。

RAII簡單來說就是資源在構造期間獲得,在析構期間釋放,自動管理資源的一種方式。

智能指針分類:std::shared_ptr、std::unique_ptr、std::weak_ptr和std::auto_ptr。注意事項:

  • 循環引用問題
  • 混合适用原始指針和智能指針

這個條款是RAII 于heap-based資源上的應用,其實作的基礎就是三個智能指針:std::shared_ptr std::unique_ptr和std::weak_ptr。

條款14 在資源管理類中小心copying行為

事實上,利用智能指針還可以實作在一些非heap-based上利用RAII思想完成自動化釋放資源。以C++的互斥鎖為例說明這個RAII思想是如何發揮作用的。

假設C API使用Mutex的互斥鎖,

void lock(Mutex *pm);
void unlock(Mutex * pm);
           

lock方法進行加鎖,unlock進行解鎖。下面我們定義一個RAII對象來管理這個互斥鎖:

class Mylock
{
public:
	explict Mylock(Mutex * lo):m_lo(lo)
	{
		lock(m_lo);
	}
	~Mylock()
	{
		unlock(m_lo);
	}
		
private:
	Mutex m_lo;
}

int main()
{
	Mutex  lolo;
	{
		Mylock(&lolo);
	}
}
           

這樣以來就不需要再擔心忘記解鎖的問題了。但是這會帶來一個新的問題,當RAII對象被複制時,會發生什麼?作者給出的兩種解決方法是:

  • 禁止複制
  • 引用計數法(借助std::shared_ptr)

有些RAII對象被拷貝是不合理的,條款6告訴我們最簡單的就是使用delete關鍵字。

引用計數的方法不需要和之前提到的那樣寫析構函數:

class Mylock
{
public:
	explicit Mylock(Mutex lo):m_loPtr(lo,unlock)
	{
		lock(m_loPtr.get());
	}	
private:
	std::shared_ptr<Mutex> m_loPtr;
}

int main()
{
	Mutex lolo;
	{
		Mylock(&lolo);
	}
}
           

這裡沒有必要使用析構函數,因為shared_ptr已經傳遞了一個删除器,離開作用域時将會自動調用對應的unlock。如果你想要拷貝是控制權轉移,那麼應該用unique_ptr代替shared_ptr。

資源管理類(heap-based)拷貝行為需要區分深拷貝和淺拷貝,應該根據你的需求來選擇拷貝類型。

條款15 在資源管理中提供對原始資源的通路

智能指針有很多優點,在某些情況下,智能指針調用非常麻煩。

Investment * createInvestment();//建立一個資源句柄
int dayHeld(const Investment * pi);//原始指針作為資源的參數
           

上面是一種C-like API,一切資源操作都是通過一個内置指針操作的。我們可以用一個智能指針來管理這個資源:

std::shared_ptr<Investment> pInv(createInvestment());//使用函數傳回原始指針,無論是new運算符還是函數傳回都可以作為智能指針的參數
int days=dayHeld(pInv.get());
           

智能指針get()方法可以傳回這個内置指針,實作對C-like API的相容。智能指針是通過隐式轉換成底層指針來實作與内置指針一樣的功能的

-> * bool

。下面是一個RAII對象:

FontHandle getFont();
void releaseFont(FontHandle fh);
class Font
{
public:
	explicit Font(FontHandle fh)
		:f(fh)
	{}
	~Font() { releaseFont(f); }
private:
	FontHandle f;
};
           

因為大多數接口都是C-API,将會非常頻繁的請求Font到FontHanle之間的轉換,有兩種方法解決這個問題:

  • get方法顯式要求轉換
  • 隐式類型轉換運算符

對于前者:

class FontHandle;
FontHandle getFont();
void releaseFont(FontHandle fh);
class Font
{
public:
	explicit Font(FontHandle fh)
		:f(fh)
	{}
	~Font() { releaseFont(f); }
	FontHandle get() { return f; }
private:
	FontHandle f;
};
           

當需要使用内置指針(句柄)時,就可以通過get方法實作了:

某些程式員可能會認為每一個地方都要求這樣的轉換,足以讓人倒胃口,甯可不使用這個類。是以才有了第二個方法:

class Font
{
public:
	...
	operator FontHandle()const 
	{return f;}
	...
}
           

使用的時候再也不用帶上這個惱人的get了:

這樣做有缺點,如:

使用顯示轉換更加安全,使用隐式對使用者更加友好。

條款16 成對使用new和delete時要采取相同形式

這個條款相對簡單,堆上配置設定空間和删除空間,數組和普通對象的底層結構不同,前者在開始位置會有關于數組大小的說明。是以,如果你在new表達式中使用[],必須在相應的delete中也使用[],如果你在new表達式中沒有使用[],那也一定不要在delete表達式使用[]。

條款17 以獨立語句将newed對象置于智能指針

這個條款的主要目的是防止因為執行順序導緻的記憶體洩露。

int priority();
void processWidget(std::shared_ptr pw,int priority);
           

一個錯誤調用processWidget的語句:

這個是不能通過編譯的,因為智能指針将内置指針到智能指針的轉換設定為删除。

可以改成這樣:

實際調用過程,程式參數調用的順序是不确定的,但是唯一能保證的是,new Widget一定早于std::shared_ptr。假如調用順序如下:

  1. 執行new Widget
  2. 調用priority
  3. 調用std::shared_ptr

假設步驟2出現異常,程式提前退出,new Widget的資源尚未被shared_ptr接管就退出了程式,就會出現記憶體洩漏。

如何解決?

将std::shared_ptr用單獨的語句進行構造後傳入這樣的使用智能指針的語句:

std::shared_ptr<Widget> pw(new Widget);
processWidget(pw,priority());
           

繼續閱讀