文章目錄

  • C++特殊類設計
  • 一、不能被拷貝的類
  • 二、只能在堆上創建對象的類
  • 三、只能在棧上創建對象的類
  • 四、不能被繼承的類
  • 五、只能創建一個對象的類(單例模式)
  • 兩種實現方式:
  • 1. 餓漢模式
  • 2. 懶漢模式
  • 局部靜態實現:
  • 線程安全的懶漢模式實現:

C++特殊類設計

一、不能被拷貝的類

目標:禁止類的對象被拷貝。比如線程對象,io流對象。

方法

  • C++98
    方法1:將拷貝構造函數和賦值運算符重載聲明為 private,並不提供定義。
    原理:
  1. 設置成私有:如果只聲明沒有設置成private,用户自己如果在類外定義了,就可以不 能禁止拷貝了
  2. 只聲明不定義:不定義是因為該函數根本不會調用,定義了其實也沒有什麼意義,不寫 反而還簡單,而且如果定義了就不會防止成員函數內部拷貝了,也不能防止友元調用。
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++特殊類的設計 - 指南_初始化

  • 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;
}

C++特殊類的設計 - 指南_創建對象_02


二、只能在堆上創建對象的類

目標:禁止在棧上創建對象,只能通過動態分配在堆上創建。

方法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;
}

C++特殊類的設計 - 指南_初始化_03

這種方法通過私有化析構來防止局部對象的創建,只要是局部對象,在銷燬的時候都會自動調用析構,然後報錯。


三、只能在棧上創建對象的類

目標:禁止在堆上創建對象,只能在棧上創建。

方法

  • 將 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++特殊類的設計 - 指南_創建對象_04

  • 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++特殊類的設計 - 指南_線程安全_05

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;
}

C++特殊類的設計 - 指南_初始化_06

線程安全的懶漢模式實現:
  • 使用雙重檢查確保效率與安全。
  • 使用互斥鎖(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;
  }
  • 單例模式常用於配置管理、日誌系統、資源池等場景。
  • 懶漢模式中,雙重檢查是保證性能與安全的常見手段。