一、不能被拷貝的類
設計思路:
拷貝只會發生在兩個場景中:拷貝構造和賦值重載,因此想要讓一個類禁止拷貝,只需讓該類不能調用拷貝構造以及賦值重載即可。
C++98方案:
將拷貝構造與賦值重載只聲明不定義,并且將其訪問權限設置為私有即可。
class CopyBan
{// ...
private:CopyBan(const CopyBan&);CopyBan& operator=(const CopyBan&);//...
};
原因:
-
設置成私有:如果只聲明沒有設置成private,用戶自己如果在類外定義了,就不能禁止拷貝了。
-
只聲明不定義:不定義是因為該函數根本不會調用,定義了其實也沒有什么意義,不寫反而還簡單,而且如果定義了就不會防止成員函數內部拷貝了。
C++11方案:
C++11擴展delete的用法,delete除了釋放new申請的資源外,如果在默認成員函數后跟上=delete
,表示讓編譯器刪除掉該默認成員函數。
class CopyBan
{// ...CopyBan(const CopyBan&)=delete;CopyBan& operator=(const CopyBan&)=delete;//...
};
二、只能在堆上創建的類
思路一:將構造、拷貝構造函數私有
- 將類的構造、拷貝構造聲明成私有。
- 提供一個靜態的成員函數,在該靜態成員函數中使用new申請堆空間并調用構造函數完成堆對象的初始化,最后返回該對象的指針。
class HeapOnly
{int _val;// 把構造和拷貝構造設置成私有HeapOnly(int val = 0): _val(val){}// 一定要把拷貝構造也設為私有HeapOnly(const HeapOnly &obj);public:// 提供一個靜態的成員函數,使用new申請堆空間并調用構造函數完成堆對象的創建。static HeapOnly *CreateObj(int val = 0){return new HeapOnly(val);}
};int main()
{// HeapOnly obj;HeapOnly *pobj1 = HeapOnly::CreateObj(10);// HeapOnly obj(*pobj1);return 0;
}
思路二:將析構函數私有
編譯器在為類對象分配棧空間時,會先檢查類的構造和析構函數的訪問性。由于棧的創建和釋放都需要由系統完成的,所以若是無法調用構造或者析構函數,自然會報錯。如果類的析構函數是私有的,則編譯器將報錯。
當然為了我們能夠釋放動態創建的對象,我們必須提供一個公有函數,該函數的唯一功能就是刪除堆對象。
- 將類的析構函數聲明成私有。
- 提供一個公有的成員函數,執行
delete this
調用析構函數清理對象資源并釋放堆空間。
class HeapOnly
{int _val;// 把析構設置成私有~HeapOnly(){cout << "~HeapOnly()" << endl;}public:HeapOnly(int val = 0): _val(val){}// 提供一個公有的成員函數,執行delete this調用析構函數清理對象資源并釋放堆空間void DestroyObj(){delete this;}
};int main()
{// HeapOnly obj;HeapOnly *pobj = new HeapOnly(10);// HeapOnly obj(*pobj);// delete pobj;pobj->DestroyObj();return 0;
}
三、只能在棧上創建的類
思路:重載operator new
我們還可以將new操作符重載并設置為私有訪問。
class StackOnly
{int _val;void* operator new(size_t t);
public:StackOnly(int val = 0): _val(val){}StackOnly(const StackOnly &obj): _val(obj._val){}
};int main()
{StackOnly obj(10);StackOnly obj1(obj);// StackOnly *pobj = new StackOnly(10);// StackOnly *pobj1 = new StackOnly(obj);return 0;
}
四、不能被繼承的類
C++98方案:將構造函數私有
派生類中調不到基類的構造函數,則無法繼承。
class NonInherit
{
public:static NonInherit CreatObj(){return NonInherit();}
private:NonInherit(){}
};
C++11方案:final關鍵字
final修飾類,表示該類不能被繼承。
class A final
{// ....
};
五、單例模式
5.1 設計模式
設計模式(Design Pattern)是一套被反復使用、多數人知曉的、經過分類的代碼設計經驗總結。
使用設計模式的目的:
為了代碼可重用性、讓代碼更容易被他人理解、保證代碼可靠性。 設計模式使代碼編寫真正工程化;設計模式是軟件工程的基石脈絡,如同大廈的結構一樣。
常用的設計模式:
- 適配器模式:對已有的類進行適配包裝形成具有全新功能和性質的類,如:棧、隊列、優先級隊列、function包裝器。
- 迭代器模式:幾乎所有容器通用的遍歷訪問方式,可以封裝隱藏容器的底層結構,以類似指針的使用方式訪問容器中的數據。如:數組(vector)、鏈表(list)、哈希表(unordered_map)、樹(map)的迭代器。
- 單例模式:接下來的內容
- 工廠模式:工廠模式是一種創建對象的設計模式,它通過定義一個工廠類來封裝對象的創建過程,并通過調用工廠類的方法來創建對象,從而將對象的創建與使用分離。
- 觀察者模式:觀察者模式是一種對象間的一對多依賴關系,當一個對象的狀態發生變化時,它的所有依賴者都會得到通知并自動更新。
單例模式:
- 一個類只能創建一個對象,即單例模式。該模式可以保證系統中(進程中)該類只有一個實例,并提供一個訪問它的全局訪問點,該實例被所有程序模塊(線程及函數)共享。
- 比如在某個服務器程序中,該服務器的配置信息存放在一個文件中,這些配置數據由一個單例對象統一讀取,然后服務進程中的其他對象再通過這個單例對象獲取這些配置信息,這種方式簡化了在復雜環境下的配置管理。
- 比如空間配置器一般也是單例模式。
- 單例模式有兩種實現模式:餓漢模式和懶漢模式。
5.2 餓漢模式
所謂餓漢模式,就是說不管你將來用不用,程序啟動時(main函數之前)就創建一個唯一的實例對象。
方法一:在堆區創建單例
設計思路:
- 私有構造、拷貝構造和析構,保證系統中該類只有一個實例;
- 包含一個該類的靜態指針并在類外使用new創建單例,提供一個訪問單例的全局訪問點;
- 包含一個互斥鎖成員,保證多線程互斥訪問該單例;
- 提供一個用于獲取全局訪問點(靜態指針)的靜態成員函數;
- 包含一個靜態的內部類對象,該對象析構時會順便析構單例,自動釋放。
class Singleton
{// 成員變量vector<string> _dir;// 該類的靜態指針,提供一個訪問單例的全局訪問點static Singleton *s_ins;// 互斥鎖成員,保證多線程互斥訪問該單例mutex s_mtx;// 靜態的內部類對象,該對象析構時會順便析構單例,自動釋放struct GC{~GC(){if (s_ins != nullptr){delete s_ins;s_ins = nullptr;}}};static GC s_gc;// 私有構造、拷貝構造和析構,保證系統中該類只有一個實例Singleton(){cout << "Singleton()" << endl;};Singleton(const Singleton &st);~Singleton(){// 單例對象的析構一般會做一些持久化操作(數據落盤)// ......cout << "~Singleton()" << endl;}public:// 提供一個靜態成員函數,用于獲取全局訪問點(靜態指針)static Singleton *GetInstance(){return s_ins;}void Add(const string &name){s_mtx.lock();_dir.push_back(name);s_mtx.unlock();}void Print(){s_mtx.lock();for (auto &name : _dir){cout << name << endl;}s_mtx.unlock();}
};// 程序啟動時(main函數之前)創建
Singleton *Singleton::s_ins = new Singleton;
Singleton::GC Singleton::s_gc;int main()
{// 系統中該類只有一個實例,不允許通過任何方式實例化// Singleton st;// static Singleton st1;// Singleton* pst = new Singleton;// Singleton st(*(Singleton::GetInstance()));// 單線程場景// Singleton::GetInstance()->Add("張三");// Singleton::GetInstance()->Add("李四");// Singleton::GetInstance()->Add("王五");// Singleton::GetInstance()->Print();// 多線程場景int n = 6;srand((unsigned int)time(nullptr));thread t1([n]() mutable{while(n--){Singleton::GetInstance()->Add("線程1:" + to_string(rand()));this_thread::sleep_for(chrono::milliseconds(10));} });thread t2([n]() mutable{while(n--){Singleton::GetInstance()->Add("線程2:" + to_string(rand()));this_thread::sleep_for(chrono::milliseconds(10));} });t1.join();t2.join();Singleton::GetInstance()->Print();
}
運行結果(多線程場景):
方法二:在靜態區創建單例
設計思路:
- 私有構造、拷貝構造和析構,保證系統中該類只有一個實例;
- 包含一個該類的靜態對象并在類外定義,提供一個訪問單例的全局訪問點;
- 包含一個互斥鎖成員,保證多線程互斥訪問該單例;
- 提供一個用于獲取全局訪問點(靜態對象的引用)的靜態成員函數;
- 由于單例是在靜態區創建的,進程結束時,系統會自動調用單例析構釋放其資源。
// 餓漢模式2
class Singleton
{// 成員變量vector<string> _dir;// 該類的靜態對象,提供一個訪問單例的全局訪問點static Singleton s_ins;// 互斥鎖成員,保證多線程互斥訪問該單例mutex s_mtx;// 私有構造、拷貝構造和析構,保證系統中該類只有一個實例Singleton(){cout << "Singleton()" << endl;};Singleton(const Singleton &st);// 由于單例是在靜態區創建的,進程結束時,系統會自動調用單例析構釋放其資源。~Singleton(){// 單例對象的析構一般會做一些持久化操作(數據落盤)// ......cout << "~Singleton()" << endl;}public:// 提供一個靜態成員函數,用于獲取全局訪問點(靜態對象的引用)static Singleton &GetInstance(){return s_ins;}void Add(const string &name){s_mtx.lock();_dir.push_back(name);s_mtx.unlock();}void Print(){s_mtx.lock();for (auto &name : _dir){cout << name << endl;}s_mtx.unlock();}
};// 程序啟動時(main函數之前)創建
Singleton Singleton::s_ins;
運行結果:同上
餓漢模式的缺點:
- 由于單例對象是在main函數之前創建的,如果單例對象很大,很復雜,其創建和初始化所占用的時間較多。會拖慢程序的啟動速度。
- 如果當前進程暫時不需要使用該單例對象,而餓漢模式在啟動時創建單例占用了空間和時間資源。
- 如果具有依賴關系的兩個單例都是餓漢模式,需要先創建單例1再創建單例2。餓漢模式無法控制其創建和初始化順序。
提示:餓漢模式的全局訪問點除了定義靜態指針還可以直接定義成靜態對象。如果是靜態對象,進程在退出時會自動調用其析構函數。
5.3 懶漢模式
如果單例對象的構造十分耗時或者占用很多資源,比如加載插件、 初始化網絡連接、讀取文件等等。而且有可能程序運行時不會用到該對象,如果也在程序一開始就進行初始化,就會導致程序啟動時非常的緩慢。 所以這種情況使用懶漢模式(延遲加載)更好。
所謂懶漢模式,就是在任意程序模塊第一次訪問單例時實例化對象。
方法一:在堆區創建單例
設計思路:
- 私有構造、拷貝構造和析構,保證系統中該類只有一個實例;
- 包含一個該類的靜態指針并在類外初始化為nullptr,提供一個訪問單例的全局訪問點;
- 包含一個靜態互斥鎖并在類外定義,保證多線程互斥地創建和訪問該單例;
- 提供一個靜態成員函數,用于首次調用創建單例(注意雙檢查加鎖)和獲取全局訪問點(靜態指針);
- 包含一個靜態的內部類對象,該對象析構時會順便析構單例,自動釋放。
// 懶漢模式
class Singleton
{// 成員變量vector<string> _dir;// 該類的靜態指針,提供一個訪問單例的全局訪問點static Singleton *s_ins;// 靜態互斥鎖,保證多線程互斥地創建和訪問該單例static mutex s_mtx;// 靜態的內部類對象,該對象析構時會順便析構單例,自動釋放struct GC{~GC(){if (s_ins != nullptr){delete s_ins;s_ins = nullptr;}}};static GC gc;// 私有構造、拷貝構造和析構,保證系統中該類只有一個實例Singleton(){cout << "Singleton()" << endl;};Singleton(const Singleton &st);~Singleton(){// 單例對象的析構一般會做一些持久化操作(數據落盤)// ......cout << "~Singleton()" << endl;}
public:static Singleton *GetInstance(){// 懶漢模式:在第一次訪問實例時創建// 雙檢查加鎖if (s_ins == nullptr) // 第一道檢查:提高效率,不需要每次獲取單例都加鎖解鎖{s_mtx.lock();if (s_ins == nullptr) // 第二道檢查:保證線程安全和只new一次{s_ins = new Singleton;}s_mtx.unlock();}return s_ins;}void Add(const string &name){s_mtx.lock();_dir.push_back(name);s_mtx.unlock();}void Print(){s_mtx.lock();for (auto &name : _dir){cout << name << endl;}s_mtx.unlock();}// 一般單例對象的生命周期隨進程,系統會在進程退出時釋放其內存,不需要中途析構單例對象// 不過在一些特殊場景下,可能需要進行顯示手動釋放static void DelInstance(){s_mtx.lock();if (s_ins != nullptr){delete s_ins;s_ins = nullptr;}s_mtx.unlock();}
};// 靜態成員要在類外定義
Singleton *Singleton::s_ins = nullptr;
mutex Singleton::s_mtx;
Singleton::GC Singleton::gc;
運行結果(多線程場景):
方法二:在靜態區創建單例(C++11)
設計思路:
- 私有構造、拷貝構造和析構,保證系統中該類只有一個實例;
- 提供一個靜態成員函數,用于首次調用創建單例(創建靜態局部對象)和獲取全局訪問點(靜態對象的指針);
- 包含一個互斥鎖成員,保證多線程互斥訪問該單例;
- 由于單例是在靜態區創建的,進程結束時,系統會自動調用單例析構釋放其資源。
// 懶漢模式2
class Singleton
{// 成員變量vector<string> _dir;// 互斥鎖成員,保證多線程互斥訪問該單例mutex s_mtx;// 私有構造、拷貝構造和析構,保證系統中該類只有一個實例Singleton(){cout << "Singleton()" << endl;};Singleton(const Singleton &st);~Singleton(){// 單例對象的析構一般會做一些持久化操作(數據落盤)// ......cout << "~Singleton()" << endl;}public:static Singleton *GetInstance(){// C++11之前,這里不能保證初始化靜態對象的線程安全問題// C++11之后,這里可以保證初始化靜態對象的線程安全問題static Singleton s_ins; //首次調用時創建局部靜態對象return &s_ins;}void Add(const string &name){s_mtx.lock();_dir.push_back(name);s_mtx.unlock();}void Print(){s_mtx.lock();for (auto &name : _dir){cout << name << endl;}s_mtx.unlock();}
};
運行結果:同上
懶漢模式模式完美解決了餓漢模式的問題,就是相對復雜一些。