天天看點

C++從靜态類型到單例模式

詳細論述C++語言中靜态類型和單例模式,以及為什麼會用到這個技術。

目錄

  • 1. 概述
  • 2. 詳論
    • 2.1. 靜态類型
      • 2.1.1. 靜态方法成員
      • 2.1.2. 靜态資料成員
    • 2.2. 單例模式
      • 2.2.1. 實作
      • 2.2.2. 問題
  • 3. 參考

1. 概述

很多的知識,學習的時候了解其實并不是很深,甚至覺得是是不太必要的;而到了實際使用中遇到了,才有了比較深刻的認識。

2. 詳論

2.1. 靜态類型

2.1.1. 靜态方法成員

比如說類的靜态成員函數。從學習中我們可以知道,類的靜态成員表示這個類成員直接屬于類本身;無論執行個體化這個類對象多少次,靜态成員都隻是一份相同的副本。那麼什麼時候去使用這個特性呢?一個很簡單的例子,假設我們實作了很多函數:

void FunA() {}

void FunB() {}

void FunC() {}
           

這些函數如果具有相關性,都是某個類型的工具函數,那麼我們可以将其封裝成一個工具類,并将其方法成員都定義成靜态的:

class Utils {
public:
  static void FunA() {}

  static void FunB() {}

  static void FunC() {}
};
           

這樣做的好處很多:

  1. 展現了面向對象的思想。并且,這些方法在類中本來就隻需要一份就可以了,節省了程式記憶體。
  2. 避免在全局作用域定義函數。一般的程式設計認為,定義在全局作用域的變量或者方法是不太好的。
  3. 友善使用:隻用記住Utils這個類的名字,就可以在IDE輸入提示的幫助下快熟輸入想要的函數。

2.1.2. 靜态資料成員

一個順理成章的問題就是,既然靜态方法成員這麼好用,那麼我們使用靜态資料成員也挺好的吧?一般情況下确實如此,比如我們給這個工具類定義一個靜态資料成員pai:

class Utils {
public:
  static void FunA() {}

  static void FunB() {}

  static void FunC() {}

  static double pai;
};

double Utils::pai = 3.1415926;
           

但是有一個問題在于,簡單的資料成員能夠通過指派來初始化,如果是一個比較複雜的資料成員呢?一個例子就是std::map容器資料成員,需要經過多次插入操作來初始化。這個時候隻是通過指派就很難實作了。

不僅如此,使用類的靜态資料成員還會遇到一個互相依賴的問題,如參考文獻2中所述。由于靜态變量的初始化順序是不定的,很可能會導緻靜态變量A初始化需要靜态變量B,但是靜态變量B卻沒有完成初始化,進而導緻出錯的問題。

2.2. 單例模式

2.2.1. 實作

C++并沒有靜态類和靜态構造函數的概念。在參考文獻1中,論述了一些用C++去實作靜态構造函數,進而更加合理的去初始化靜态資料成員的辦法。其中一個實作是:我們需要的類按照正常的非靜态成員類去設計,但是我們可以把這個類作為另一個包裝類的靜态成員變量,這樣就能完美實作靜态構造函數。

正是這個實作給了我靈感:我們想要的不是通路類的靜态成員變量,而是單例模式。不想像C一樣使用全局函數或者全局變量,又不想每次都去執行個體化一個對象,那麼我們需要的是單例模式。參考文獻3中給出了單例模式的最佳實踐:

class Singleton {
 public:
  ~Singleton() { std::cout << "destructor called!" << std::endl; }
  Singleton(const Singleton&) = delete;
  Singleton& operator=(const Singleton&) = delete;
  static Singleton& get_instance() {
    static Singleton instance;
    return instance;
  }

 private:
  Singleton() { std::cout << "constructor called!" << std::endl; }
};

int main() {

  Singleton& instance_1 = Singleton::get_instance();
  Singleton& instance_2 = Singleton::get_instance();

  return 0;
}
           

這段代碼的說明如下:

  1. 構造函數和析構函數都存在,無論多複雜的成員,都可以對資料成員初始化和釋放。
  2. 構造函數時私有的,是以無法直接聲明和定義。
  3. 拷貝構造函數和指派構造函數都被删除,是以無法進行拷貝和指派。
  4. 隻能通過專門的執行個體化函數get_instance()進行調用。

在執行個體化函數get_instance()内部,執行個體化了一個自身的局部的靜态類。靜态局部變量始終存放在記憶體的全局資料區,隻在第一次初始化,從第二次開始,它的值不會變化,是第一次調用後的結果值。并且最後,傳回的是這個靜态局部變量的引用。

2.2.2. 問題

無論從哪方面看,上述的單例實作,都符合單例的設計模式:全局隻提供唯一一個類的執行個體,在任何位置都可以通過接口擷取到那個唯一執行個體,無法拷貝也無法指派。但是也有幾個問題值得讨論。

第一個問題是,在多線程的環境下,初始化是否會造成沖突或者生成了兩份執行個體?關于這一點不用擔心,從C++11标準開始,局部靜态變量的初始化是線程安全的。

第二,在參考文獻4中讨論了這樣一個問題:C++單例模式跨DLL是不是就是會出問題?靜态變量是單個編譯單元的靜态變量,如果動态庫和可執行檔案都引用了get_instance()的實作,那麼動态庫和可執行檔案會分别保有一份自己的執行個體。解決方法是要麼将get_instance()放入到cpp中,要麼使用DLL的子產品導入導出接口的規則,也就是dllexport和dllimport。

第三,單例模式還有基于子產品的實作,不過我覺得模闆的實作太複雜,第二個問題就是使用模闆導緻的,這裡就不讨論了。

3. 參考

  1. C++靜态構造函數
  2. 解決靜态全局變量初始化的互相依賴問題
  3. C++ 單例模式總結與剖析
  4. C++單例模式跨DLL是不是就是會出問題?

繼續閱讀