1. 程式人生 > >7 More Effective C++—條款10(建構函式內阻止記憶體洩漏)

7 More Effective C++—條款10(建構函式內阻止記憶體洩漏)

1 提出問題

上一篇文章中,我們討論瞭如下情況,當函式doSomething()被呼叫時,heap中資源無法被釋放,導致記憶體洩漏問題發生。

void function() {
	MyObject *object = new MyObject;
	object->doSomething();
	delete object;
}

本篇中,我們將討論這樣一種情況:當類中需要“包含多個heap物件“,但是在建構函式中卻出現異常的情況下,如何釋放掉已經建立的heap物件。

考慮如下情景。電話簿中需要包含如下資訊:姓名,號碼,頭像,一段特定響鈴音樂。可以定義如下類。

class BookEntry {
	public:
		BookEntry(const string &name, const string& address,
						const string& image, const string& audio);
		~BookEntry();
	private:
		string m_name, m_address;
		std::list<PhoneNumber> m_number;
		Image* m_photo;
		Audio *m_audio;
}
BookEntry::BookEntry(const string &name, const string& address,
						const string& image, const string& audio) {
	if (image != "") {
		m_image = new Image(image);
	}
	if (audio != "") {
		m_audio = new Audio(audio);
	}
}
BookEntry::~BookEntry() {
	delete m_image; // C++允許空指標被delete,不會丟擲異常
	delete m_audio; 
}

上面程式碼中,若在執行new Audio(audio)語句中出現異常,由於類並沒有建立成功,解構函式不會被呼叫,因此m_image指向的記憶體就會出現記憶體洩漏。所以,下面的程式碼不會執行。

void testBookEntry() {
	BookEntry entry = 0;
	try {
		// 建構函式丟擲異常,m_image指向heap記憶體可能會洩漏
		// entry可能為null指標
		entry = new BookEntry("Danny", "Beijing", "photo.jpg", "voic.mp3"); 
		...
	} catch (...) {
		delete entry;// 不會丟擲異常,但是m_image指向記憶體依然會洩漏
		throw;
	}
	delete entry; // 不會丟擲異常,但是m_image指向記憶體依然會洩漏

2 解決步驟

首先,對每個在heap中的記憶體資源進行記錄,會增加維護成本和開銷,導致程式效率低下,臃腫。比較好的解決辦法時,當問題出現時,集中處理,然後再進一步丟擲異常。

BookEntry::BookEntry(const string &name, const string& address,
						const string& image, const string& audio) {
	try {
		if (image != "") {
			m_image = new Image(image);
		}
		if (audio != "") {
			m_audio = new Audio(audio);
		}
	} catch (...) { // 解構函式和本動作相同,因此可包裝到一個函式中:cleanUp()
		cleanUp();
	}
}
BookEntry::~BookEntry() {
	cleanUp();
}
void BookEntry::cleanUp() {
	delete m_image;
	delete m_audio;
}

3 初始化列表中的記憶體洩漏

上面對m_image和m_audio的初始化,放在了建構函式中。

但是,如果m_image變數宣告為const型別,就不得不將其初始化動作放到初始化列表中,我們就無法使用try…catch來捕捉異常,依然會造成m_image記憶體資源洩漏。如下面所示。

Image * const m_image; // 指標的指向不能修改
BookEntry::BookEntry(const string &name, const string& address,
						const string& image, const string& audio) 
		: m_name(name), m_address(address),
		m_image(image == "" ? 0 : new Image(image)),
		m_audio(audio == "" ? 0 : new Audio(audio)) { // new Audio丟擲異常,會造成m_image記憶體洩漏
}

4 解決辦法

1 辦法一

一種辦法是,針對每個變數定義一個構建函式,在heap中建立變數的工作放到這個構建函式中,並返回建立好的指標。如下程式碼所示。

BookEntry::BookEntry(const string &name, const string& address,
						const string& image, const string& audio) 
		: m_name(name), m_address(address),
		m_image(createImage(image)),
		m_audio(createAudio(audio)) {
}
Image* createImage(const std::string& image) {
	Image *tmpImage = 0;
	try {
		 // doSomething
	} catch () {
	}
	return tmpImage;
}

2 辦法二

就像上一篇內容建議的,我們直接將指標包裹到auto_ptr中,利用“作用域”和“生命週期”來控制具體的行為。

std::auto_ptr<Image> m_image;
std::auto_ptr<Audio> m_audio;

當構造m_audio時,若出現任何異常,就像m_name, m_address一樣,m_image也會被自動銷燬,保證了記憶體不會被洩漏。