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() {}
};
這樣做的好處很多:
- 體現了物件導向的思想。並且,這些方法在類中本來就只需要一份就可以了,節省了程式記憶體。
- 避免在全域性作用域定義函式。一般的程式設計認為,定義在全域性作用域的變數或者方法是不太好的。
- 方便使用:只用記住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;
}
這段程式碼的說明如下:
- 建構函式和解構函式都存在,無論多複雜的成員,都可以對資料成員初始化和釋放。
- 建構函式時私有的,所以無法直接宣告和定義。
- 拷貝建構函式和賦值建構函式都被刪除,因此無法進行拷貝和賦值。
- 只能通過專門的例項化函式get_instance()進行呼叫。
在例項化函式get_instance()內部,例項化了一個自身的區域性的靜態類。靜態區域性變數始終存放在記憶體的全域性資料區,只在第一次初始化,從第二次開始,它的值不會變化,是第一次呼叫後的結果值。並且最後,返回的是這個靜態區域性變數的引用。
2.2.2. 問題
無論從哪方面看,上述的單例實現,都符合單例的設計模式:全域性只提供唯一一個類的例項,在任何位置都可以通過介面獲取到那個唯一例項,無法拷貝也無法賦值。但是也有幾個問題值得討論。
第一個問題是,在多執行緒的環境下,初始化是否會造成衝突或者生成了兩份例項?關於這一點不用擔心,從C++11標準開始,區域性靜態變數的初始化是執行緒安全的。
第二,在參考文獻4中討論了這樣一個問題:C++單例模式跨DLL是不是就是會出問題?靜態變數是單個編譯單元的靜態變數,如果動態庫和可執行檔案都引用了get_instance()的實現,那麼動態庫和可執行檔案會分別保有一份自己的例項。解決方法是要麼將get_instance()放入到cpp中,要麼使用DLL的模組匯入匯出介面的規則,也就是dllexport和dllimport。
第三,單例模式還有基於模組的實現,不過我覺得模板的實現太複雜,第二個問題就是使用模板導致的,這裡就不討論了。