C++特殊類設計概念與示例講解

一、設計模式概念

設計模式是一套被反復使用、多數人知曉的、經過分類的、代碼設計經驗的總結。

使用設計模式的目的:為瞭代碼可重用性、讓代碼更容易被他人理解、保證代碼可靠性。

根本原因是為瞭代碼復用,增加可維護性。

設計模式的例子:迭代器模式

二、設計一個不能被拷貝的類

拷貝一共就隻有兩個場景,一個是拷貝構造,一個是賦值運算符重載。所以我們想要設計出一個不能被拷貝的類隻需要讓外部無法調用這兩個函數即可。

在C++98中,我們的方法是將拷貝構造和賦值運算符重載隻聲明不定義並且將權限設置為私有。

class anti_copy
{
public:
	anti_copy()
	{}
private:
	anti_copy(const anti_copy& ac);
	anti_copy& operator=(const anti_copy& ac);
};

設計原因:

1️⃣ 私有:如果聲明成共有,那麼就可以在類外面實現定義。

2️⃣ 隻聲明不定義:因為如果不聲明編譯器會默認生成這兩個的默認成員函數。而不定義是因為該函數不會被調用,就不用寫瞭,這樣編譯的時候就會出現鏈接錯誤。

而在C++11中引入瞭關鍵字——delete。

如果在默認成員函數後跟上=delete,表示讓編譯器刪除掉該默認成員函數。即使權限是共有也無法調用已刪除的函數。

class anti_copy
{
public:
	anti_copy()
	{}
	anti_copy(const anti_copy& ac) = delete;
	anti_copy& operator=(const anti_copy& ac) = delete;
private:
};

三、設計一個隻能在堆上創建對象的類

3.1 私有構造

首先要把構造函數給私有,不然這個類就可以在任意位置被創建。而構造函數被私有瞭以後我們怎麼創建對象呢?

我們可以在定義一個成員函數,讓這個函數在堆上申請空間,但我們知道必須現有對象才能調用成員函數。所以我們就把這個函數設置成靜態成員函數。

class OnlyHeap
{
public:
	static OnlyHeap* GetObj()
	{
		return new OnlyHeap;
	}
private:
	OnlyHeap()
	{}
};

但是這樣也不完全對,如果我們這麼寫:

class OnlyHeap
{
public:
	static OnlyHeap* GetObj()
	{
		return new OnlyHeap;
	}
private:
	OnlyHeap()
	{}
};
int main()
{
	OnlyHeap* hp1 = OnlyHeap::GetObj();
	OnlyHeap hp2(*hp1);
	return 0;
}

這裡的hp2就是棧上的對象。所以我們也要把拷貝構造給封住。

class OnlyHeap
{
public:
	static OnlyHeap* GetObj()
	{
		return new OnlyHeap;
	}
	OnlyHeap(const OnlyHeap& hp) = delete;
private:
	OnlyHeap()
	{}
};

3.2 私有析構

class OnlyHeap
{
public:
	OnlyHeap()
	{}
	OnlyHeap(const OnlyHeap& hp) = delete;
private:
	~OnlyHeap()
	{}
};
int main()
{
	OnlyHeap hp1;// error
	OnlyHeap* hp2 = new OnlyHeap;
	return 0;
}

這裡的hp1就不能創建成功,因為對象銷毀的時候會調用析構函數,但是這裡的析構是私有的,所以該對象無法調用。

但是我們要銷毀hp2該怎麼辦呢?

我們可以定義一個成員函數顯示調用析構函數。

class OnlyHeap
{
public:
	OnlyHeap()
	{}
	OnlyHeap(const OnlyHeap& hp) = delete;
	void Destroy()
	{
		this->~OnlyHeap();
	}
private:
	~OnlyHeap()
	{}
};
int main()
{
	OnlyHeap* hp2 = new OnlyHeap;
	hp2->Destroy();
	return 0;
}

四、設計一個隻能在棧上創建對象的類

為瞭不讓這個類隨便定義出對象,首先要把構造函數私有。然後跟上面隻能在堆上創建對象的方法相似,定義出一個靜態成員函數返回棧上創建的對象。

class StackOnly
{
public:
	static StackOnly GetObj()
	{
		return StackOnly();
	}
private:
	StackOnly()
	{}
};
int main()
{
	StackOnly hp = StackOnly::GetObj();
	return 0;
}

但是這裡有一個問題,無法防止創建靜態對象:

static StackOnly hp2 = StackOnly::GetObj();

五、設計不能被繼承的類

在C++98,為瞭不讓子類繼承,我們可以把構造函數私有化,因為子類需要先調用父類的構造函數初始化父類的那一部分成員。

class NoInherit
{
public:
private:
	NoInherit()
	{}
};

而在C++11中引入的新的關鍵字final,被final關鍵字修飾的類不能被繼承。

class NoInherit final
{
public:
private:
};

六、單例模式

一個類隻能創建一個對象,即單例模式,該模式可以保證系統中該類隻有一個實例,並提供一個訪問它的全局訪問點,該實例被所有程序模塊共享。

單例模式的特點就是全局隻有一個唯一對象。

6.1 餓漢模式

怎麼能做到全局隻是用一個對象呢,比方說我們現在想要實現一個英漢字典,首先我們要把構造函數私有,不然無法阻止創建對象。然後我們可以在類裡面定義一個自己類型的靜態成員變量,作用域是全局的。因為對比定義在外邊的靜態成員變量,內部的可以調用構造函數。

這裡要註意把拷貝也要封住。

class Singleton
{
public:
	static Singleton& GetObj()
	{
		return _s;
	}
	void insert(const std::string& s1, const std::string& s2)
	{
		_dict[s1] = s2;
	}
	void Print()
	{
		for (auto& e : _dict)
		{
			cout << e.first << "->" << e.second << endl;
		}
	}
	// 防拷貝
	Singleton(const Singleton&) = delete;
	Singleton& operator=(const Singleton&) = delete;
private:
	Singleton()
	{}
	std::map<std::string, std::string> _dict;
private:
	static Singleton _s;// 聲明
};
Singleton Singleton::_s;// 定義
int main()
{
	Singleton::GetObj().insert("corn", "玉米");
	Singleton& dic1 = Singleton::GetObj();
	dic1.insert("apple", "蘋果");
	dic1.insert("banana", "香蕉");
	Singleton& dic2 = Singleton::GetObj();
	dic2.insert("pear", "梨");
	dic2.Print();
	return 0;
}

餓漢模式有什麼特點呢?

它會在一開始(main之前)就創建對象。

餓漢模式有什麼缺點呢?

1️⃣ 如果單例對象構造十分耗時或者占用很多資源,比如加載插件啊, 初始化網絡連接啊,讀取文件啊等等,而有可能該對象程序運行時不會用到,那麼也要在程序一開始就進行初始化,就會導致程序啟動時非常的緩慢。

2️⃣ 多個單例類之間如果有依賴關系餓漢模式就無法控制,比方說要求A類初始化時必須調用B,但是餓漢無法控制先後順序。

所以針對這些問題,就有瞭懶漢模式。

6.2 懶漢模式

第一次使用實例對象時,創建對象(用的時候創建)。進程啟動無負載。多個單例實例啟動順序自由控制。

我們可以直接對上面餓漢模式的代碼進行修改,把靜態成員變量變成指針。然後把獲取的函數改變一下:

static Singleton& GetObj()
	{
		// 第一次調用才會創建對象
		if (_s == nullptr)
		{
			_s = new Singleton;
		}
		return *_s;
	}

整體代碼:

class Singleton
{
public:
	static Singleton& GetObj()
	{
		// 第一次調用才會創建對象
		if (_s == nullptr)
		{
			_s = new Singleton;
		}
		return *_s;
	}
	void insert(const std::string& s1, const std::string& s2)
	{
		_dict[s1] = s2;
	}
	void Print()
	{
		for (auto& e : _dict)
		{
			cout << e.first << "->" << e.second << endl;
		}
	}
	// 防拷貝
	Singleton(const Singleton&) = delete;
	Singleton& operator=(const Singleton&) = delete;
private:
	Singleton()
	{}
	std::map<std::string, std::string> _dict;
private:
	static Singleton* _s;// 聲明
};
Singleton* Singleton::_s = nullptr;// 定義

6.2.1 線程安全問題

上面的代碼存在問題,當多個線程同時調用GetObj(),就會創建多個對象。所以為瞭線程安全我們要加鎖。為瞭保證鎖自動銷毀,我們可以自定義一個鎖。

template <class Lock>
class LockAuto
{
public:
	LockAuto(Lock& lk)
		: _lk(lk)
	{
		_lk.lock();
	}
	~LockAuto()
	{
		_lk.unlock();
	}
private:
	Lock& _lk;
};
class Singleton
{
public:
	static Singleton& GetObj()
	{
		// 第一次調用才會創建對象
		if (_s == nullptr)// 隻有第一次才用加鎖
		{
			LockAuto<mutex> lock(_mutex);
			if (_s == nullptr)
			{
				_s = new Singleton;
			}
		}
		return *_s;
	}
	void insert(const std::string& s1, const std::string& s2)
	{
		_dict[s1] = s2;
	}
	void Print()
	{
		for (auto& e : _dict)
		{
			cout << e.first << "->" << e.second << endl;
		}
	}
	// 防拷貝
	Singleton(const Singleton&) = delete;
	Singleton& operator=(const Singleton&) = delete;
private:
	Singleton()
	{}
	std::map<std::string, std::string> _dict;
private:
	static Singleton* _s;// 聲明
	static mutex _mutex;// 鎖
};
Singleton* Singleton::_s = nullptr;// 定義
mutex Singleton::_mutex;// 定義

6.2.2 新寫法

class Singleton
{
public:
	static Singleton& GetObj()
	{
		static Singleton dic;
		return dic;
	}
	void insert(const std::string& s1, const std::string& s2)
	{
		_dict[s1] = s2;
	}
	void Print()
	{
		for (auto& e : _dict)
		{
			cout << e.first << "->" << e.second << endl;
		}
	}
	// 防拷貝
	Singleton(const Singleton&) = delete;
	Singleton& operator=(const Singleton&) = delete;
private:
	Singleton()
	{}
	std::map<std::string, std::string> _dict;
};

這裡就用瞭靜態局部變量隻會在第一次定義的時候初始化。在C++11之前是不能保證線程安全的,但是C++11之後就可以瞭。

到此這篇關於C++特殊類設計概念與示例講解的文章就介紹到這瞭,更多相關C++特殊類設計內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: