1 什麼是單例模式
單例模式簡單的來說就是:一個類只能有一個實例。
2 C++如何實現一個單例模式呢?
下面的代碼就能夠實現一個單例模式了~, 要點:
- 定義構造函數爲私有的
- 定義一個私有的static的類對象指針
- 定義給一個公有的static函數getInstance獲取唯一一個實例化對象
class Singleton {
private:
Singleton() {};
static Singleton* m_pInstance;
public:
static Singleton* getInstance() {
if (m_pInstance == NULL) {
m_pInstance = new Singleton();
}
return m_pInstance;
}
};
// 靜態成員變量在類中僅僅是聲明,沒有定義,所以要在類的外面定義,實際上是給靜態成員變量分配內存
// 如果在類的外面有初始化值就用初始值初始化,否則系統就用默認值初始化它
Singleton* Singleton::m_pInstance = nullptr;
int main() {
Singleton* instance = Singleton::getInstance();
return 0;
}
上面的代碼雖然能夠實現需求,但是指針指向的內存卻沒辦法釋放,會導致內存泄露。我們使用VS調試工具測試一下:
使用VS進行內存檢測調試:VS環境中進行內存泄漏的檢測
引入頭文件:#include <crtdbg.h>
在main函數最後加上_CrtDumpMemoryLeaks();
程序運行結束後查看調試結果會發現有內存泄漏警告!因此上面的單例模式代碼是存在問題的,會造成內存泄露。
2.X 釋放內存
經過認真學習我總結了如下幾種解決方法:
方法X:錯誤警告!錯誤警告!錯誤警告!
這是錯誤的方法,能犯這種低級錯誤說明對於析構函數的認識不夠到位!
一開始我想:直接在析構函數裏面釋放m_pInstance指針指向的空間不就好了嗎?如果寫出下面的代碼那最終就會產生下圖酷炫的結果:
#include "stdafx.h"
#include <iostream>
#include <crtdbg.h>
using namespace std;
class Singleton {
private:
Singleton() {};
static Singleton* m_pInstance;
public:
~Singleton() {
printf("析構函數被調用");
delete m_pInstance;
}
static Singleton* getInstance() {
if (m_pInstance == NULL) {
m_pInstance = new Singleton();
}
return m_pInstance;
}
};
Singleton* Singleton::m_pInstance = nullptr;
int main() {
Singleton* instance = Singleton::getInstance();
delete instance;
_CrtDumpMemoryLeaks();
system("pause");
return 0;
}
爲什麼會這樣呢?因爲析構函數只會在如下幾種情況被調用 析構函數何時被調用:
- 對象生命週期結束,被銷燬時;
- 主動調用delete (和new配套使用);
- 對象i是對象o的成員,o的析構函數被調用時,對象i的析構函數也被調用。
而在上面的代碼中我們手動調用了delete,delete就會調用析構函數,然後析構函數裏面又有一個delete,於是就會套娃,所以上面這種方法是錯誤的,還不如直接使用下面的方法1。
方法1:在代碼中手動delete:
int main() {
Singleton* instance = Singleton::getInstance();
delete instance;
return 0;
}
方法2:不使用new創建對象,使用局部靜態變量
優點: 不需要考慮資源釋放,程序結束時,靜態區資源自動釋放
class Singleton {
private:
Singleton() {};
public:
static Singleton& getInstance() {
static Singleton instance;
return instance;
}
};
int main() {
Singleton instance = Singleton::getInstance();
system("pause");
return 0;
}
方法3:進程結束時,靜態對象的生命週期隨之結束,其析構函數會被調用來釋放對象。因此,我們可以利用這一特性,在單例類中聲明一個內嵌類,該類的析構函數專門用來釋放new出來的單例對象,並聲明一個該類類型的static對象。
class Singleton {
private:
Singleton() {};
static Singleton* m_pInstance;
class Garbo {
public:
~Garbo() {
if (Singleton::m_pInstance) {
delete Singleton::m_pInstance;
}
}
};
static Garbo garbo; // 定義一個靜態成員,在程序結束時,系統會調用它的析構函數
public:
static Singleton* getInstance() {
if (m_pInstance == NULL) {
m_pInstance = new Singleton();
}
return m_pInstance;
}
};
// 給靜態變量分配內存
Singleton* Singleton::m_pInstance = nullptr;
Singleton::Garbo Singleton::garbo;
int main() {
Singleton* instance = Singleton::getInstance();
system("pause");
return 0;
}
3 懶漢模式和餓漢模式
懶漢模式:故名思義,不到萬不得已就不會去實例化類,也就是說在第一次用到類實例的時候纔會去實例化。與之對應的是餓漢式單例(注意,懶漢本身是線程不安全的,爲保證多線程下安全,必須加鎖)
餓漢模式:餓了肯定要飢不擇食。所以在單例類定義的時候就進行實例化(本身就是線程安全的)
懶漢模式[1]
第一次調用時才初始化,避免內存浪費,但是爲保證多線程下安全,必須加鎖(下面的代碼沒有解決資源釋放問題)
#include "stdafx.h"
#include <iostream>
#include <mutex>
using namespace std;
class Singleton {
public:
static Singleton* getInstance() {
if (m_pInstance == nullptr) {
lock_guard<mutex> lock(m_mutex);
if (m_pInstance == nullptr) {
m_pInstance = new Singleton();
}
}
return m_pInstance;
}
private:
Singleton() {};
static Singleton *m_pInstance;
static mutex m_mutex;
};
Singleton* Singleton::m_pInstance = nullptr;
mutex Singleton::m_mutex;
int main() {
Singleton::getInstance();
system("pause");
return 0;
}
餓漢模式
類加載時就初始化,會浪費內存,但線程安全
#include <iostream>
using namespace std;
class Singleton {
public:
static Singleton* getInstance() {
return m_pInstance;
}
private:
Singleton() {};
static Singleton *m_pInstance;
};
// 直接初始化實例
Singleton* Singleton::m_pInstance = new Singleton();
int main() {
Singleton::getInstance();
delete Singleton::getInstance();
return 0;
}
兩種模式的特點與應用場景
- 懶漢:在訪問量較小時,採用懶漢實現。這是以時間換空間。
- 餓漢:由於要進行線程同步,所以在訪問量比較大,或者可能訪問的線程比較多時,採用餓漢實現,可以實現更好的性能。這是以空間換時間。
4 擴展(TODO)
針對單例模式可以擴展出如下問題:
問題2的答案:https://www.nowcoder.com/questionTerminal/0a584aa13f804f3ea72b442a065a7618
1. 將構造函數聲明爲私有的有哪些用途?
答:如設計一個單例模式
2. 將析構函數聲明爲私有的有哪些用途?
答:定義一個只能在堆上生成的對象,使用靜態建立對象的方法(A a)會將對象放在棧中,由編譯器自動釋放空間,因此在建立對象的時候編譯器會檢查該類的構造函數和析構函數是否能正常訪問。如果將析構函數定義爲私有的,那麼編譯器就無法訪問析構函數,因此會報錯。所以如果將一個類的析構函數聲明爲私有的,該類只能在堆上生成對象。
原因:C++ 是靜態綁定語言,編譯器管理棧上對象的生命週期,編譯器在爲類對象分配棧空間時,會先檢查類的析構函數的訪問性。若析構函數不可訪問,則不能在棧上創建對象。
3. 類中的static成員函數和非static成員函數存在哪裏?有什麼區別?
答:static成員函數和非static成員函數都存在代碼區,但是非static函數有指向調用該函數對象的this指針,static函數沒有this指針
4. 所有的類對象共享一份代碼還是每個類都有一份代碼?
答:所有類的對象共享一份代碼,代碼存在代碼區。
References: