Singleton 是最廣為人知卻也最易誤用的設計模式之一。本文從 Visual Studio CRT 偵測到的記憶體洩漏切入,層層剖析「靜態指標指向自身實例」導致解構函數永不被呼叫、手動 delete 觸發遞迴解構、全域靜態變數生命週期等核心機制,並提供兩種工業級自動釋放方案:嵌套 Deleter 類與 智慧指標,最後推廣至模板化 Singleton。
class Singleton {
public:
static Singleton* GetInstancePtr() {
if (_p == 0) _p = new Singleton();
return _p;
}
virtual ~Singleton() {}
void test() { std::cout << "running\n"; }
protected:
Singleton() {}
private:
static Singleton* _p;
};
Singleton* Singleton::_p = 0;
// 呼叫端
Singleton* p = Singleton::GetInstancePtr();
p->test();
// delete p; // 若加上這行 → 遞迴解構崩潰;不加 → VS 報告洩漏
_p ≡ 全域變數:編譯期配置 4/8 位元組於靜態儲存區,程式結束由作業系統回收指標本身,但指向的堆記憶體不會自動釋放。Singleton 實例僅存在於堆上,無任何 delete 操作,故解構函數不會執行。delete _p 會遞迴:delete this → 呼叫解構函數 → 再次 delete _p → 無窮遞迴 → 堆疊溢位。delete 同樣觸發遞迴:因刪除的是 Singleton 物件,進入其解構函數再次 delete _p。_CrtDumpMemoryLeaks() 在 main 結束前執行,此時靜態 _p 尚未被清理,故回報洩漏;實際上若有自動釋放機制,會在 main 返回後、全域物件解構階段釋放。利用全域/靜態物件解構順序:定義一個嵌套類別 PrivateDeleter,其解構函數負責 delete Singleton::_p;將 PrivateDeleter 實例宣告為 Singleton 的 靜態成員。程式結束時,全域/靜態物件按宣告逆序解構,deleter 解構時釋放 _p,避開遞迴陷阱。
class Singleton {
public:
static Singleton* GetInstancePtr() {
if (_p == 0) _p = new Singleton();
return _p;
}
virtual ~Singleton() {}
void test() { std::cout << "running\n"; }
protected:
Singleton() {}
private:
class PrivateDeleter {
public:
~PrivateDeleter() {
if (Singleton::_p) { delete Singleton::_p; Singleton::_p = 0; }
}
};
static PrivateDeleter deleter; // 關鍵:靜態實例,非指標
static Singleton* _p;
};
Singleton::PrivateDeleter Singleton::deleter;
Singleton* Singleton::_p = 0;
優點:零額外庫依賴、標準 C++、線程不安全(同原版)。
注意:VS 仍會在 main 結束前報洩漏(誤報),實際無洩漏。
以 boost::shared_ptr(或 C++11 std::shared_ptr)取代裸指標,將擁有權語意、異常安全、線程安全(控制塊引用計數)全委託給庫實作。
class Singleton {
public:
static boost::shared_ptr<Singleton> GetInstancePtr() {
if (!_p) _p = boost::shared_ptr<Singleton>(new Singleton);
return _p;
}
static Singleton& GetInstanceRef() { // 提供引用介面
if (!_p) _p = boost::shared_ptr<Singleton>(new Singleton);
return *_p;
}
virtual ~Singleton() {}
void test() { std::cout << "running\n"; }
protected:
Singleton() {}
private:
static boost::shared_ptr<Singleton> _p;
};
boost::shared_ptr<Singleton> Singleton::_p;
呼叫端簡潔安全:
Singleton::GetInstanceRef().test(); // 無需 delete,自動釋放
// 或
boost::shared_ptr<Singleton> p = Singleton::GetInstancePtr();
p->test();
進階:支援多型與複雜成員——同樣將所有靜態/非靜態指標成員改為 shared_ptr,解構函數留空即可,RAII 保證正確釋放順序。
專案中若需多個 Singleton,提煉模板:
template <typename T>
class Singleton {
public:
static boost::shared_ptr<T> GetInstancePtr() {
if (!_p) _p = boost::shared_ptr<T>(new T());
return _p;
}
static T& GetInstanceRef() {
if (!_p) _p = boost::shared_ptr<T>(new T());
return *_p;
}
protected:
Singleton() {}
private:
static boost::shared_ptr<T> _p;
};
template <typename T> boost::shared_ptr<T> Singleton<T>::_p;
// 使用:Singleton<MyService>::GetInstanceRef().doWork();
限制:T 必須有公開/受保護的預設建構函數;若需參數化建構,可結合 Factory/Builder 模式或改用 std::call_once + lambda 延遲初始化。
| 方案 | 適用場景 | 優點 | 缺點 |
|---|---|---|---|
| 手動 delete | 教學/極簡嵌入式 | 零開銷 | 易忘、異常不安全、多執行緒不安全 |
| 嵌套 Deleter | 禁用 Boost/STL、C++98 專案 | 標準 C++、零依賴、自動釋放 | 線程不安全、VS 誤報洩漏、單一實例限制 |
| shared_ptr | 現代 C++ 專案(C++11+ 或可用 Boost) | 異常安全、線程安全(引用計數)、支援多型、RAII | 引入依賴、控制塊額外記憶體、原子操作微小開銷 |
| Meyers Singleton (C++11) | 單一實例、無多型需求 | 最簡潔、線程安全(魔術靜態)、零開銷 | 無法多型、銷毀順序固定、難以單元測試 |
建議:新專案優先
std::shared_ptr方案或 Meyers Singleton(static T& instance() { static T inst; return inst; });舊專案或受限環境用嵌套 Deleter;嚴禁裸指標 + 手動 delete。
理解 Singleton 記憶體管理的本質——靜態儲存區指標擁有堆物件、缺乏自動銷毀機制——是避開此類坑洞的關鍵。掌握 RAII 與智慧指標,讓編譯器幫你守住資源安全防線。