關注C++細節——含有本類對象指針的類的構造函數、析構函數、拷貝構造函數、賦值運算符的例子

本例只是對含有本類對象指針的類的構造函數、析構函數、拷貝構造函數、複製運算符使用方法的一個簡單示例,以加深對構造函數和拷貝控制成員的理解。


讀C++ primer 5th 第13章後加上自己的理解,完整的寫了下課後習題的代碼。

第一版:

#include <string>
#include <iostream>

using namespace std;


class TreeNode{
private:
	string value;
	TreeNode *left;
	TreeNode *right;

public:
	TreeNode() : value(""), left(nullptr), right(nullptr){}

	~TreeNode(){
		cout << "~TreeNode()" << endl;
		if (left != nullptr){
			delete left; //遞歸析構左子樹
		}
		if (right != nullptr){
			delete right;//遞歸析構右子樹
		}
	}

	TreeNode(const TreeNode &tn) : value(tn.value), left(nullptr), right(nullptr){
		if (tn.left != nullptr){
			left = new TreeNode(*tn.left);//遞歸複製拷貝左子樹 (其實又一次調用了以(*tn.left)作爲參數的複製構造函數)
		}

		if (tn.right != nullptr){
			right = new TreeNode(*tn.right);//遞歸複製拷貝右子樹  (其實又一次調用了以(*tn.left)作爲參數的複製構造函數)
		}
	}

	TreeNode & operator=(const TreeNode& tn){
		value = tn.value;

		TreeNode * pl, *pr;

		
		//左側的對象因爲要被覆蓋,所以記得如果左側對象中的指針已經保持有對象,要記得釋放資源,否則就會內存泄露了
		if (left != nullptr){
			delete left;
			left = nullptr;
		}

		if (right != nullptr){
			delete right;
			right = nullptr;
		}

		if (tn.left != nullptr){
			left = new TreeNode(*tn.left);//遞歸賦值左子樹 (去調用複製構造函數,這樣就會去構造新new出來的這個對象的中保存的對象指針left和right,構造完後此new出來的地址賦予left)下同
		}

		if (tn.right != nullptr){
			right = new TreeNode(*tn.right);//遞歸複製右子樹
		}
		return (*this);
	}

	TreeNode *getLeft()const{
		return left;
	}

	TreeNode *getRight()const{
		return right;
	}

	void setLeft(TreeNode * const le){
		left = le;
	}

	void setRight(TreeNode * const ri){
		right = ri;
	}
};
以上這一版已經能完成正常的複製構造,賦值操作以及正常的析構,並且不會造成內存泄露,但是,這裏有一個問題就是不能支持自身給自身賦值,因爲一旦給自己賦值,就會出現,現將自己的左右子樹析構了,然後再用左右子樹做參數,這就會出現未定義的行爲,雖然多次運行可能都能得到正確結果,但是確實是非常危險的行爲。下面是改進版,此版就支持自身賦值。

ps(借用一下C++ primer中的提示:當你編寫一個賦值運算符時,一個好的模式是先將右側運算對象拷貝到一個局部臨時對象中,這樣銷除左側運算對象的現有成員就是安全的了,一旦左側運算對象的資源被銷燬,就只剩下將數據從臨時對象拷貝到左側運算對象的成員中了。)  說的確實經典。。。。


改進版:

#include <string>
#include <iostream>

using namespace std;


class TreeNode{
private:
	string value;
	TreeNode *left;
	TreeNode *right;

public:
	TreeNode() : value(""), left(nullptr), right(nullptr){}

	~TreeNode(){
		cout << "~TreeNode()" << endl;
		if (left != nullptr){
			delete left; //遞歸析構左子樹
		}
		if (right != nullptr){
			delete right;//遞歸析構右子樹
		}
	}

	TreeNode(const TreeNode &tn) : value(tn.value), left(nullptr), right(nullptr){
		if (tn.left != nullptr){
			left = new TreeNode(*tn.left);//遞歸複製拷貝左子樹 (其實又一次調用了以(*tn.left)作爲參數的複製構造函數)
		}

		if (tn.right != nullptr){
			right = new TreeNode(*tn.right);//遞歸複製拷貝右子樹  (其實又一次調用了以(*tn.left)作爲參數的複製構造函數)
		}
	}

	TreeNode & operator=(const TreeNode& tn){
		value = tn.value;

		TreeNode * pl, *pr;

		pl = pr = nullptr;

		if (tn.left != nullptr){
			pl = new TreeNode(*tn.left);//遞歸賦值左子樹 (去調用複製構造函數,這樣就會去構造新new出來的這個對象的左指針和右指針,構造完後此new出來的地址賦予left)下同
		}

		if (tn.right != nullptr){
			pr = new TreeNode(*tn.right);//遞歸複製右子樹
		}
		
		//左側的對象因爲要被覆蓋,所以記得如果左側對象的指針已經保持有對象要記得釋放資源,否則就會內存泄露了
		if (left != nullptr){
			delete left;
			left = nullptr;
		}

		if (right != nullptr){
			delete right;
			right = nullptr;
		}

		//一下將臨時對象賦值過來就ok了,這樣的操作也很好的支持了自身賦值
		left = pl;
		right = pr;
		return (*this);
	}

	TreeNode *getLeft()const{
		return left;
	}

	TreeNode *getRight()const{
		return right;
	}

	void setLeft(TreeNode * const le){
		left = le;
	}

	void setRight(TreeNode * const ri){
		right = ri;
	}
};

以上的代碼因爲對指針類型的成員進行的是深度拷貝,所以效率比較低,也很容易看出來,每次賦值和賦值構造都要遞歸的不斷分配新的內存保證賦值對象間能夠互不影響。

一般情況下對於這種含有指針類型對象的類我們的很多操作可以直接更改指針來進行實現,但是這樣的話就必須保證最終所有的內存都能正確的釋放而且不能重複釋放資源,很好的一種方式是使用智能指針,在這裏先模擬一下智能指針即增加一個引用計數來實現正確拷貝及賦值等操作。


代碼如下:

#include <string>
#include <iostream>

class TreeNode{
private:
	string value;
	TreeNode *left;
	TreeNode *right;

	int *use;


public:
	TreeNode() : value(""), left(nullptr), right(nullptr), use(new int(1)){}
	TreeNode(string str) : value(str), left(nullptr), right(nullptr), use(new int(1)){}

	~TreeNode(){

		//析構的時候,先把該對象的引用計數指針減一,如果引用計數不爲0,則說明還有其他對象指向該對象的資源,這時候就不能析構掉
		//本對象的資源,防止其他引用對象產生未定義行爲。
		if (--(*use) != 0){
			return;
		}
		if (left != nullptr){
			delete left; //遞歸析構左子樹
		}
		if (right != nullptr){
			delete right;//遞歸析構右子樹
		}

		delete use;
	}

	//拷貝構造函數就比較簡單了,只是單純的公用同一內存即可,但是別忘了將引用計數器加1操作
	TreeNode(const TreeNode &tn) : value(tn.value), left(tn.left), right(tn.right), use(tn.use){
		++(*use);
	}

	TreeNode & operator=(TreeNode& tn){
		//因爲要複製右側對象,所以先把右側對象的計數增一,這樣能解決自賦值問題
		//這樣在自賦值時,不會因爲要清理左側對象而導致未定義行爲
		++(*tn.use);

		//左側對象的要被覆蓋掉,所以左側對象的引用計數要先減一,如果減一後計數值位0,則說明左側對象現在可以安全的清理掉
		if (--*use == 0){
			if (left != nullptr){
				delete left;
			}

			if (right != nullptr){
				delete right;
			}

			delete use;//記得把左側對象的引用計數指針也清理掉
		}

		//因爲左側對象的資源已經正確處理(如果有的話,沒有保持資源的話及上面的left和right爲nullptr無需釋放資源)
		//則可以直接設置指針覆蓋實現賦值運算的目的
		left = tn.left;
		right = tn.right;
		use = tn.use;

		return (*this);
	}

	bool operator< (const TreeNode &tn){
		return (this->value < tn.value);
	}

	TreeNode *getLeft()const{
		return left;
	}

	TreeNode *getRight()const{
		return right;
	}

	void setLeft(TreeNode * const le){
		left = le;
	}

	void setRight(TreeNode * const ri){
		right = ri;
	}
};


不斷回顧,以加深對構造函數和拷貝控制成員的理解。

發佈了222 篇原創文章 · 獲贊 45 · 訪問量 63萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章