文章目錄
- C++特殊類設計
- 一、不能被拷貝的類
- 二、只能在堆上創建對象的類
- 三、只能在棧上創建對象的類
- 四、不能被繼承的類
- 五、只能創建一個對象的類(單例模式)
- 兩種實現方式:
- 1. 餓漢模式
- 2. 懶漢模式
- 局部靜態實現:
- 線程安全的懶漢模式實現:
C++特殊類設計
一、不能被拷貝的類
目標:禁止類的對象被拷貝。比如線程對象,io流對象。
方法:
- C++98:
方法1:將拷貝構造函數和賦值運算符重載聲明為 private,並不提供定義。
原理:
- 設置成私有:如果只聲明沒有設置成private,用户自己如果在類外定義了,就可以不 能禁止拷貝了
- 只聲明不定義:不定義是因為該函數根本不會調用,定義了其實也沒有什麼意義,不寫 反而還簡單,而且如果定義了就不會防止成員函數內部拷貝了,也不能防止友元調用。
class CopyBan {
public:
CopyBan(int val = 0):_val(val){}
//……
private:
//將複製相關的函數設為私有,則用户不可以拷貝,也無法在類外實現,不定義防止內部調用複製
CopyBan(const CopyBan&);
CopyBan& operator=(const CopyBan&);
int _val;
};
int main()
{
CopyBan a(1);
CopyBan b = a;
CopyBan c(a);
return 0;
}
- C++11:C++11擴展了delete的語法,除了釋放new的資源外,還可以用來刪除默認成員函數(禁止生成),因此可以使用 = delete 顯式刪除拷貝構造函數和賦值運算符重載。
class CopyBan {
public:
CopyBan(int val = 0) :_val(val) {}
//……
//將複製相關的函數刪除
CopyBan(const CopyBan&) = delete;
CopyBan& operator=(const CopyBan&) = delete;
private:
int _val;
};
int main()
{
CopyBan a(1);
CopyBan b = a;
CopyBan c(a);
return 0;
}
二、只能在堆上創建對象的類
目標:禁止在棧上創建對象,只能通過動態分配在堆上創建。
方法1:
- 將構造函數和拷貝構造函數設為私有。
- 提供靜態公有方法(如 CreatObj)返回堆上對象的指針。
class OnlyHeap {
public:
static OnlyHeap* CreatObj()
{
return new OnlyHeap;
}
private:
OnlyHeap(int val = 0):_val(val){}
//不可以讓其他成員複製,可能會在棧上覆制對象,因此該類也是一種不可以複製的類
//C++98方法,設為私有,只聲明不定義
OnlyHeap(const OnlyHeap&);
OnlyHeap& operator=(const OnlyHeap&);
//
//C++11方法,直接刪除
OnlyHeap(const OnlyHeap&) = delete;
OnlyHeap& operator=(const OnlyHeap&) = delete;
//
int _val;
};
int main()
{
//通過靜態成員函數獲取在堆上開的對象
OnlyHeap* ptr = SpecialClass::OnlyHeap::CreatObj();
//想利用堆上的對象複製一個棧對象也不可以,因為複製相關的函數已經被禁用
OnlyHeap a(1);
OnlyHeap b(*ptr);
return 0;
}
方法2:
私有化析構函數
class OnlyHeap {
public:
void Release()
{
delete this;
}
private:
~OnlyHeap()
{
//……
}
//
int _val;
};
int main()
{
OnlyHeap* p1 = new SpecialClass::OnlyHeap;
OnlyHeap p2; // 局部對象無法調用析構,報錯
OnlyHeap p3(*p1);//拷貝出來的局部對象也無法調用析構
p2->Release(); //調用封裝的析構函數
return 0;
}
這種方法通過私有化析構來防止局部對象的創建,只要是局部對象,在銷燬的時候都會自動調用析構,然後報錯。
三、只能在棧上創建對象的類
目標:禁止在堆上創建對象,只能在棧上創建。
方法:
- 將 operator new 和 operator delete 顯式刪除(C++11)或者將這兩個函數設為私有且不定義。
- 構造函數私有,提供靜態方法返回棧上構造的對象(非必需,因為禁用了new和delete之後,無法在堆上創建對象,也無法調用構造函數)。
class OnlyStack {
public:
//提供靜態函數返回局部對象
static OnlyStack CreatObj()
{
return OnlyStack();
}
//禁用new和delete,C++11
void* operator new(size_t size) = delete;
void operator delete(void* p) = delete;
private:
//構造私有化,防止棧對象被到堆對象
OnlyStack(int val = 0 ):_val(val){}
//C++98,設為私有且不定義
//void* operator new(size_t size);
//void operator delete(void* p);
int _val;
};
int main()
{
//通過提供的靜態成員函數可以返回局部對象
OnlyStack a = OnlyStack::CreatObj();
OnlyStack b(1);
OnlyStack* c = new OnlyStack;
return 0;
}
四、不能被繼承的類
目標:禁止其他類繼承該類。
方法:
- C++98:將構造函數設為私有,派生類無法調用基類構造函數,提供靜態方法返回對象。
class FinallyClass {
public:
//……
static GetInstance(int val)
{
return FinallyClass(val);
}
private:
//構造私有化,防止派生類調用基類構造
FinallyClass(int val = 0) :_val(val) {}
int _val;
};
class Child : public FinallyClass {
public:
//……
private:
int _data;
};
int main()
{
Child();
return 0;
}
- C++11:使用 final 關鍵字修飾類,表示該類不能被繼承
class FinallyClass final {
public:
//……
private:
//構造私有化,防止派生類調用基類構造
FinallyClass(int val = 0) :_val(val) {}
int _val;
};
class Child : public FinallyClass {
public:
//……
private:
int _data;
};
int main()
{
Child();
return 0;
}
C++98 方式通過構造權限控制實現,C++11 的 final 更直觀、安全。
五、只能創建一個對象的類(單例模式)
設計模式介紹:
設計模式是軟件工程中一套被反覆驗證、廣泛認可的代碼設計經驗總結,如同建築領域的藍圖一樣,它們針對特定場景提供了可重用的解決方案。設計模式不僅提升了代碼的可重用性、可讀性和可維護性,還促進了開發團隊之間的高效協作,使軟件設計更加標準化和工程化。通過使用這些模式,開發者能夠避免重複造輪子,降低系統複雜度,提高軟件質量,是構建健壯、靈活軟件系統的重要工具。
我們之前已經使用過設計模式來編程,適配器模式,迭代器模式等。
單例模式也是一種很常見的設計模式,其目的為確保一個類只有一個實例,並提供全局訪問點該實例被所有程序模塊共享。
兩種實現方式:
1. 餓漢模式
餓漢模式就像一個人,他在吃飯前早早已經將碗洗好,這個碗隨時可以用來吃飯,若是在程序中,早早初始化好一個對象,隨時可以使用,這就是餓漢模式。
餓漢模式有以下幾個特點:
- 程序啓動即創建單例對象,在main函數之前就創建
- 優點:實現簡單,線程安全。
- 缺點:若有很多單利類都是餓漢模式,那麼程序在啓動時會初始化多個單例對象,或者某個單例對象初始化需要加載資源等,進而導致程序啓動慢,遲遲進不了main函數。
如果兩個類有依賴關係,一個類A需要另一個類B的數據初始化,但是這兩個類的初始化順序是隨機的,那麼類A在類B之前初始化的話,會使用到類B未初始化的數據,產生未知問題。
代碼實現:
class Singleton {
public:
static Singleton* GetInstance()
{
return &_only_object;
}
private:
Singleton(int val = 0):_val(val) {};
//C++98
//Singleton(const Singleton&);
//Singleton& operator=(const Singleton&);
//C++11
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
int _val;
static Singleton _only_object; //全局唯一的對象
};
Singleton Singleton::_only_object;
//靜態成員在類外初始化,餓漢模式在程序入口main之前已經初始化對象
int main()
{
//使用提供的靜態方法來獲取對象
Singleton* ptr = Singleton::GetInstance();
return 0;
}
為什麼使用靜態成員對象來實現呢?
因為靜態成員不屬於某一個對象,而是屬於整個類,我們把構造函數私有化後,無法自己創造對象,也無法拷貝,但是聲明瞭靜態成員對象之後,就可以在類外定義,達到只創建一個對象的目的,在通過靜態成員方法返回該對象供我們使用。
如果這個單例對象在多線程高併發環境下頻繁使用,性能要求較高,那麼顯然使用餓漢模式來避 免資源競爭,提高響應速度更好。
2. 懶漢模式
懶漢模式就是一個人,吃完飯不洗碗,等到下一次吃飯的前才洗碗,即用之前才處理碗。在程序中,單例模式所需對象在第一次使用的時候在初始化,這就是懶漢實現方式,有以下特點:
- 第一次使用時創建對象。
- 優點:延遲加載,啓動快,可控制初始化順序。
- 缺點:C++11之前需處理多線程安全問題,實現複雜。
局部靜態實現:
缺陷:C++11之前,不可以這樣寫,局部static對象構造,有線程安全風險,即有多個線程同時調用構造,不能保證是原子的
靜態的成員函數只能用靜態的成員對象,因為靜態函數沒有this指針
class Singleton {
public:
static Singleton* GetInstance()
{
static Singleton only;
//創建靜態局部對象,生命週期隨着進程,第一次調用的時候才會創建對象,
//因為是靜態的,只創建一個對象,之後返回的都是同一個對象
return &only;
}
private:
Singleton(int val = 0):_val(val) {};
int _val;
//C++98
//Singleton(const Singleton&);
//Singleton& operator=(const Singleton&);
//C++11
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
};
int main()
{
SpecialClass::Singleton* ptr = SpecialClass::Singleton::GetInstance();
cout << ptr << endl;
cout << SpecialClass::Singleton::GetInstance() << endl;
cout << SpecialClass::Singleton::GetInstance() << endl;
cout << SpecialClass::Singleton::GetInstance() << endl;
return 0;
}
線程安全的懶漢模式實現:
- 使用雙重檢查確保效率與安全。
- 使用互斥鎖(mutex)保護臨界區。
static Singleton* GetInstance()
{
if (_only_obj_ptr == nullptr)
{
_only_obj_ptr = new Singleton;
}
return _only_obj_ptr;
}
單線程的時候,若是第一次訪問該函數,_only_obj_ptr == nullptr判斷為真,則會執行 _ only_obj_ptr = new Singleton;,生成一個對象,此後,在此訪問該函數則直接返回
但是該函數有個致命問題,在多線程環境下,無線程安全,若有1一個線程在執行完判斷條件後時間片到了輪轉後,有另外一個線程接着判斷,並且開闢對象,執行一定時間後,第一個線程在輪轉回來,此時已經執行過判斷,則會接着執行創建對象的代碼,此時不僅會創建第二個對象,還會將之前創建的對象覆蓋,以至於丟失數據和內存泄露,因此需要加鎖來保護線程的安全,如下所示:
static Singleton* GetInstance()
{
lock_guard< _mutex> lock( _mutex);
if (_only_obj_ptr == nullptr)
{
_only_obj_ptr = new Singleton;
}
return _only_obj_ptr;
}
該函數在多線程環境下,會互斥訪問臨界區,則不會出現線程安全的問題,只有第一個申請到互斥鎖的線程可以創建出唯一的對象,後續的線程在去執行判斷的代碼都不會為true,因為_only_obj_ptr 已經被第一個線程所設置。
但是這又帶來了新的問題,若每次訪問該函數都要進行加鎖,則效率低下,因為只有在第一次共同訪問該函數的時候才有線程安全問題,之後就算有多個線程共同訪問,判斷條件也都是為false,不會去設置_only_obj_ptr ,那麼可以在前面在加一層判斷
static Singleton* GetInstance()
{
if (_ only_obj_ptr == nullptr) //第一個判斷,保證效率
{
lock_guard< _ mutex> lock( _mutex);
if ( _only_obj_ptr == nullptr) //第二個判斷,保證線程安全
{
_only_obj_ptr = new Singleton;
}
}
return _only_obj_ptr;
}
這樣,當多個線程第一次進入函數,第一個判斷為真,他們去競爭鎖並且設置指針,之後再次進入函數則不會去競爭鎖,而是直接返回指針,減少加鎖開銷,提升效率
完整代碼如下:
using namespace std;
class Singleton {
public:
static Singleton* GetInstance()
{
if (_only_obj_ptr == nullptr) //第一個判斷,保證效率
{
lock_guard<mutex> lock(_mutex);
if (_only_obj_ptr == nullptr) //第二個判斷,保證線程安全
{
_only_obj_ptr = new Singleton;
}
}
return _only_obj_ptr;
}
private:
Singleton(int val = 0) :_val(val) {};
int _val;
//C++98
//Singleton(const Singleton&);
//Singleton& operator=(const Singleton&);
//C++11
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static Singleton* _only_obj_ptr;
static std::mutex _mutex;
};
Singleton* Singleton::_only_obj_ptr = nullptr;
mutex Singleton::_mutex;
int main()
{
SpecialClass::Singleton* ptr = SpecialClass::Singleton::GetInstance();
cout << ptr << endl;
cout << SpecialClass::Singleton::GetInstance() << endl;
cout << SpecialClass::Singleton::GetInstance() << endl;
cout << SpecialClass::Singleton::GetInstance() << endl;
return 0;
}
- 單例模式常用於配置管理、日誌系統、資源池等場景。
- 懶漢模式中,雙重檢查是保證性能與安全的常見手段。