[設計模式]C++單例模式的幾種寫法以及通用模板

之前在這篇文章中簡單的介紹了一下單例模式的作用和應用C++中單例模式詳解_c++單例模式的作用-CSDN博客,今天我將在在本文梳理單例模式從C++98到C++11及以后的演變過程,探討其不同實現方式的優劣,并介紹在現代C++中的最佳實踐。

什么是單例模式?

簡單來說,單例模式(Singleton Pattern)是一種設計模式,它能保證一個類在整個程序運行期間,只有一個實例存在

這種唯一性的保證在特定場景下至關重要。例如,對于一個數據庫連接管理器 Manager,如果系統中存在多個實例,不同模塊可能會通過不同實例進行操作,從而引發數據狀態不一致或資源競爭的問題 。通過將 Manager 設計為單例,所有模塊都通過唯一的訪問點來與數據庫交互,這不僅能保證數據和狀態的統一,還能有效規避資源浪費 。

總結而言,單例模式主要具備兩大價值:

  • ? ? ? ? · 控制實例數量:節約系統資源,避免因多重實例化導致的狀態沖突 。
    • ? ? ? ? · 提供全局訪問點:為不同模塊提供一個統一的、可協調的訪問接口 。

因此,該模式廣泛應用于配置管理、日志系統、設備驅動、數據庫連接池等需要全局唯一實例的場景中 。


單例模式的幾種寫法

方式一:局部靜態變量(最簡潔的現代寫法)

//通過靜態成員變量實現單例
//懶漢式
class Single2
{
private:Single2(){}Single2(const Single2 &) = delete;Single2 &operator=(const Single2 &) = delete;public:static Single2 &GetInst(){static Single2 single;return single;}
};

它的核心原理就是利用了函數局部靜態變量的特性:它只會被初始化一次 。無論你調用 GetInst() 多少次,single 這個靜態實例只會在第一次調用時被創建。

調用代碼:

void test_single2(){//多線程情況下可能存在問題cout << "s1 addr is " << &Single2::GetInst() << endl;cout << "s2 addr is " << &Single2::GetInst() << endl;}

程序輸出:

s1 addr is 0x7f8a1b402a10
s2 addr is 0x7f8a1b402a10

可以看到,兩次獲取到的實例地址是完全一樣的。

需要注意的是,在 C++98 的年代,這種寫法在多線程環境下是不安全的,可能會因為并發導致創建出多個實例 。但是隨著 C++11 標準的到來,編譯器對這里做了優化,保證了局部靜態變量的初始化是線程安全的 。所以,在 C++11 及之后的版本,這已成為實現單例最受推崇的方式之一,兼具簡潔與安全。

方式二:靜態成員變量指針(餓漢式)

這種方式定義一個靜態的類指針,并在程序啟動時就立刻進行初始化,因此被稱為“餓漢式”。

由于實例在主線程啟動、其他業務線程開始前就已完成初始化,它自然地避免了多線程環境下的競爭問題。

//餓漢式
class Single2Hungry{
private:Single2Hungry(){}Single2Hungry(const Single2Hungry &) = delete;Single2Hungry &operator=(const Single2Hungry &) = delete;public:static Single2Hungry *GetInst(){if (single == nullptr){single = new Single2Hungry();}return single;}private:static Single2Hungry *single;
};

初始化和調用:

//餓漢式初始化,在.cpp文件中
Single2Hungry *Single2Hungry::single = Single2Hungry::GetInst();void thread_func_s2(int i){cout << "this is thread " << i << endl;cout << "inst is " << Single2Hungry::GetInst() << endl;
}void test_single2hungry(){cout << "s1 addr is " << Single2Hungry::GetInst() << endl;cout << "s2 addr is " << Single2Hungry::GetInst() << endl;for (int i = 0; i < 3; i++){thread tid(thread_func_s2, i);tid.join();}
}int main(){test_single2hungry();
}

程序輸出:

s1 addr is 0x7fb3d6c00f00
s2 addr is 0x7fb3d6c00f00
this is thread 0
inst is 0x7fb3d6c00f00
this is thread 1
inst is 0x7fb3d6c00f00
this is thread 2
inst is 0x7fb3d6c00f00

餓漢式的優點是實現簡單且線程安全。但其缺點也很明顯:無論后續是否使用,實例在程序啟動時都會被創建,可能造成不必要的資源開銷。此外,通過裸指針 new 創建的實例,其內存釋放時機難以管理,在復雜的多線程程序中極易引發內存泄漏或重復釋放的嚴重問題。

方式三:靜態成員變量指針(懶漢式與雙重檢查鎖定)

與“餓漢”相對的就是“懶漢”,即只在第一次需要用的時候才去創建實例 。這能節省資源,但直接寫在多線程下是有問題的。為解決其在多線程下的安全問題,一種名為雙重檢查鎖定(Double-Checked Locking)的優化技巧應運而生。

//懶漢式指針,帶雙重檢查鎖定
class SinglePointer{
private:SinglePointer(){}SinglePointer(const SinglePointer &) = delete;SinglePointer &operator=(const SinglePointer &) = delete;public:static SinglePointer *GetInst(){// 第一次檢查if (single != nullptr){return single;}s_mutex.lock();// 第二次檢查if (single != nullptr){s_mutex.unlock();return single;}single = new SinglePointer();s_mutex.unlock();return single;}private:static SinglePointer *single;static mutex s_mutex;
};//在.cpp文件中定義
SinglePointer *SinglePointer::single = nullptr;
std::mutex SinglePointer::s_mutex;

調用代碼:

void thread_func_lazy(int i){cout << "this is lazy thread " << i << endl;cout << "inst is " << SinglePointer::GetInst() << endl;
}void test_singlelazy(){for (int i = 0; i < 3; i++){thread tid(thread_func_lazy, i);tid.join();}
}

程序輸出:

this is lazy thread 0
inst is 0x7f9e8a00bc00
this is lazy thread 1
inst is 0x7f9e8a00bc00
this is lazy thread 2
inst is 0x7f9e8a00bc00

該模式試圖通過減少鎖的持有時間來提升性能。然而,這種實現在C++中是存在嚴重缺陷的。new 操作并非原子性,它大致包含三個步驟:

  • ????????1. 分配內存;
    • ????????2. 調用構造函數;
      • ????????3. 賦值給指針 。

編譯器和處理器出于優化目的,可能對指令進行重排,導致第3步先于第2步完成 。若此時另一線程訪問,它會獲取一個非空但指向未完全構造對象的指針,進而引發未定義行為 。

?C++11的現代解決方案:once_flag 與智能指針

為了安全地實現懶漢式加載,C++11 提供了 std::once_flag 和 std::call_once。call_once 能確保一個函數(或 lambda 表達式)在多線程環境下只被成功調用一次 。

// Singleton.h
#include <mutex>
#include <iostream>class SingletonOnceFlag{
public:static SingletonOnceFlag* getInstance(){static std::once_flag flag;std::call_once(flag, []{_instance = new SingletonOnceFlag();});return _instance;}void PrintAddress() {std::cout << _instance << std::endl;}~SingletonOnceFlag() {std::cout << "this is singleton destruct" << std::endl;}private:SingletonOnceFlag() = default;SingletonOnceFlag(const SingletonOnceFlag&) = delete;SingletonOnceFlag& operator=(const SingletonOnceFlag& st) = delete;static SingletonOnceFlag* _instance;
};// Singleton.cpp#include "Singleton.h"SingletonOnceFlag *SingletonOnceFlag::_instance = nullptr;

這樣就完美解決了線程安全問題,但內存管理的問題依然存在。此時,std::shared_ptr 智能指針成為了理想的解決方案,它能實現所有權的共享和內存的自動回收。

智能指針版本:

// Singleton.h (智能指針版)#include <memory>class SingletonOnceFlag{
public:static std::shared_ptr<SingletonOnceFlag> getInstance(){static std::once_flag flag;std::call_once(flag, []{// 注意這里不能用 make_shared,因為構造函數是私有的_instance = std::shared_ptr<SingletonOnceFlag>(new SingletonOnceFlag());});return _instance;}//... 其他部分相同private://...static std::shared_ptr<SingletonOnceFlag> _instance;
};// Singleton.cpp (智能指針版)#include "Singleton.h"std::shared_ptr<SingletonOnceFlag> SingletonOnceFlag::_instance = nullptr;

測試代碼:

#include "Singleton.h"
#include <thread>
#include <mutex>int main() {std::mutex mtx;std::thread t1([&](){auto inst = SingletonOnceFlag::getInstance();std::lock_guard<std::mutex> lock(mtx);inst->PrintAddress();});std::thread t2([&](){auto inst = SingletonOnceFlag::getInstance();std::lock_guard<std::mutex> lock(mtx);inst->PrintAddress();});t1.join();t2.join();return 0;
}

程序輸出 (析構函數被正確調用):

0x7fde7b408c20
0x7fde7b408c20
this is singleton destruct

進階玩法:私有析構與自定義刪除器

有些大佬追求極致的封裝,他們會把析構函數也設為private,防止外部不小心 delete 掉單例實例 。但這樣 shared_ptr 默認的刪除器就無法調用析構了。解決辦法:我們可以給 shared_ptr 指定一個自定義的刪除器(Deleter),通常是一個函數對象(仿函數)。這個刪除器類被聲明為單例類的友元(friend),這樣它就有了調用私有析構函數的權限。

// Singleton.h
class SingleAutoSafe; // 前置聲明// 輔助刪除器
class SafeDeletor{
public:void operator()(SingleAutoSafe *sf){std::cout << "this is safe deleter operator()" << std::endl;delete sf;}
};class SingleAutoSafe{
public:static std::shared_ptr<SingleAutoSafe> getInstance(){static std::once_flag flag;std::call_once(flag, []{_instance = std::shared_ptr<SingleAutoSafe>(new SingleAutoSafe(), SafeDeletor());});return _instance;}// 聲明友元類,讓 SafeDeletor 可以訪問私有成員friend class SafeDeletor;
private:SingleAutoSafe() = default;// 析構函數現在是私有的了~SingleAutoSafe() {std::cout << "this is singleton destruct" << std::endl;}// ...static std::shared_ptr<SingleAutoSafe> _instance;};

程序輸出:

0x7f8c0a509d30
0x7f8c0a509d30
this is safe deleter operator()

可以看到,程序結束時,shared_ptr 調用了我們的 SafeDeletor,從而安全地銷毀了實例。這種方式提供了最強的封裝性。


終極方案:基于CRTP的通用單例模板

在大型項目中,為每個需要單例的類重復編寫樣板代碼是低效的。更優雅的方案是定義一個通用的單例模板基類。任何類只需繼承該基類,便能自動獲得單例特性。這通常通過奇異遞歸模板模式實現,即派生類將自身作為模板參數傳遞給基類。

單例基類實現:

// Singleton.h
#include <memory>
#include <mutex>template <typename T>
class Singleton {
protected:Singleton() = default;Singleton(const Singleton<T>&) = delete;Singleton& operator=(const Singleton<T>& st) = delete;virtual ~Singleton() {std::cout << "this is singleton destruct" << std::endl;}static std::shared_ptr<T> _instance;public:static std::shared_ptr<T> GetInstance() {static std::once_flag s_flag;std::call_once(s_flag, []() {// new T 這里能成功,因為子類將基類設為了友元_instance = std::shared_ptr<T>(new T);});return _instance;}void PrintAddress() {std::cout << _instance.get() << std::endl;}
};template <typename T>
std::shared_ptr<T> Singleton<T>::_instance = nullptr;

使用這個模板基類:

現在,如果我們想讓一個網絡管理類 SingleNet 成為單例,只需要這樣做:

// SingleNet.h
#include "Singleton.h"// CRTP: SingleNet 繼承了以自己為模板參數的 Singleton
class SingleNet : public Singleton<SingleNet>{// 將基類模板實例化后設為友元,這樣基類的 GetInstance 才能 new 出 SingleNetfriend class Singleton<SingleNet>;private:SingleNet() = default;~SingleNet() {std::cout << "SingleNet destruct " << std::endl;}
};

測試代碼:

// main.cpp
int main() {std::thread t1([&](){SingleNet::GetInstance()->PrintAddress();});std::thread t2([&](){SingleNet::GetInstance()->PrintAddress();});t1.join();t2.join();return 0;}

程序輸出:

0x7f9a2d409f40
0x7f9a2d409f40
SingleNet destruct
this is singleton destruct

我們幾乎沒寫任何單例相關的邏輯,只通過一次繼承和一句友元聲明,就讓 SingleNet 變成了一個線程安全的、自動回收內存的單例類。這就是泛型編程的強大之處。


總結

本文介紹了單例模式從傳統到現代的多種實現方式。可總結為:

  • 日常開發:對于C++11及以上版本,局部靜態變量法是實現單例的首選,它兼具代碼簡潔性與線程安全性。
  • 深入理解:了解餓漢式、懶漢式及雙重檢查鎖定的歷史與缺陷,對于理解并發編程中的陷阱至關重要。
  • 企業級實踐:在大型項目中,基于智能指針CRTP 的通用單例模板是最佳實踐,它能提供類型安全、自動內存管理和最高的代碼復用性。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/915245.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/915245.shtml
英文地址,請注明出處:http://en.pswp.cn/news/915245.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

小架構step系列19:請求和響應

1 概述作為Web程序&#xff0c;通用形式是發起HTTP請求并獲取返回的結果&#xff0c;在這個過程中&#xff0c;需要把請求映射到代碼的接口上&#xff0c;提供這種接口的類一般稱為Controller&#xff0c;也就是需要把請求映射到Controller的接口方法上&#xff0c;把請求的參數…

論文分享 | LABRADOR:響應引導的針對物聯網設備的黑盒模糊測試

由于固件仿真以及重托管的技術挑戰&#xff0c;部分企業級 IoT 設備只能在黑盒環境下進行模糊測試。分享一篇發表于 2024 年 S&P 會議的論文 Labrador&#xff0c;它利用響應來引導請求變異&#xff0c;實現了針對 IoT 設備的高效黑盒模糊測試。 猴先生說&#xff1a;這篇論…

WPF為啟動界面(Splash Screen)添加背景音樂

1. 添加音頻文件到項目 將音頻文件&#xff08;如.mp3/.wav&#xff09;放入項目文件夾&#xff08;如Resources&#xff09;在解決方案資源管理器中右鍵文件 → 屬性&#xff1a; 生成操作&#xff1a;選擇Resource&#xff08;嵌入資源&#xff09;或Content&#xff08;內容…

【Jmeter】報錯:An error occured:Unknown arg

問題 調試Jmeter時&#xff0c;報錯&#xff1a;‘An error occurred: Unknown arg: l’&#xff0c;腳本如下&#xff1a; $JMETER_PATH -n -t "$target_jmx" -l "$SCENARIO_REPORT_DIR/result_${threads}.jtl" -e -o "$SCENARIO_REPORT_DIR/htm…

vue3使用KeepAlive組件及一些注意事項

目錄 一、KeepAlive的作用 二、緩存組件配置 2.1、過濾緩存組件 2.2、最大緩存實例數 三、KeepAlive組件的生命周期 四、錯誤用法 4.1、緩存v-if包裹的動態組件 4.2、拼寫錯誤 一、KeepAlive組件的作用 首先&#xff0c;keep-alive是一個vue的內置組件&#xff0c;官網…

辛普森悖論

辛普森悖論第一步&#xff1a;概念拆解想象你在比較兩個班級的考試成績&#xff1a;?第一天?&#xff1a;實驗組&#xff08;1個學生考了90分&#xff09;&#xff0c;對照組&#xff08;99個學生平均考了80分&#xff09;?第二天?&#xff1a;實驗組&#xff08;50個學生平…

有效的括號數據結構oj題(力口20)

目錄 目錄 題目描述 題目分析解析 解決代碼 寫題感悟&#xff1a; 題目描述 還有實例 題目分析解析 對于這個題目&#xff0c;我們首先有效字符串需要滿足什么&#xff0c;第一個左右括號使用相同類型的括號&#xff0c;這好理解&#xff0c;無非就是小括號和小括號大括號…

Mock 單元測試

作者&#xff1a;小凱 沉淀、分享、成長&#xff0c;讓自己和他人都能有所收獲&#xff01; 本文的宗旨在于通過簡單干凈實踐的方式教會讀者&#xff0c;如何使用 Mock (opens new window)進行工程的單元測試&#xff0c;以便于驗證系統中的獨立模塊功能的健壯性。 從整個工程所…

MySQL 深度性能優化配置實戰指南

?? 一、硬件與系統層優化:夯實性能基石 ??硬件選型策略?? ??CPU??:讀密集型場景選擇多核CPU(如32核);寫密集型場景選擇高主頻CPU(如3.5GHz+)。 ??內存??:建議≥64GB,??緩沖池命中率≥99%?? 是性能關鍵指標。 ??存儲??:??必用NVMe SSD??,I…

Visual Studio Code(VSCode)中設置中文界面

在VS Code中設置中文界面主要有兩種方法&#xff1a;通過擴展市場安裝中文語言包或通過命令面板直接切換語言。?方法一&#xff1a;通過擴展市場安裝中文語言包?打開VS Code&#xff0c;點擊左側活動欄的"擴展"圖標&#xff08;或按CtrlShiftX&#xff09;。在搜索…

叉車機器人如何實現托盤精準定位?這項核心技術的原理和應用是什么?

隨著智慧物流和智能制造的加速發展&#xff0c;智能化轉型成為提升效率、降低成本的關鍵路徑&#xff0c;叉車機器人&#xff08;AGV/AMR叉車&#xff09;在倉儲、制造、零售等行業中的應用日益廣泛。 其中&#xff0c;托盤定位技術是實現其高效、穩定作業的核心環節之一&…

NO.6數據結構樹|二叉樹|滿二叉樹|完全二叉樹|順序存儲|鏈式存儲|先序|中序|后序|層序遍歷

樹與二叉樹的基本知識 樹的術語結點&#xff1a; 樹中的每個元素都稱為結點&#xff0c; 例如上圖中的 A,B,C…根結點&#xff1a; 位于樹頂部的結點&#xff0c; 它沒有父結點,比如 A 結點。父結點&#xff1a; 若一個結點有子結點&#xff0c; 那么這個結點就稱為其子結點的父…

數據集下載網站

名稱簡介鏈接Kaggle世界上最大的數據科學競賽平臺之一&#xff0c;有大量結構化、圖像、文本等數據集可直接下載?支持一鍵下載、APIPapers with Code可按任務&#xff08;如圖像分類、文本生成等&#xff09;查找模型與數據集&#xff0c;標注 SOTA?與論文強關聯Hugging Face…

Tomcat 生產 40 條軍規:容量規劃、調優、故障演練與安全加固

&#xff08;一&#xff09;容量規劃 6 條 軍規 1&#xff1a;線程池公式 maxThreads ((并發峰值 平均 RT) / 1000) 冗余 20 %&#xff1b; 踩坑&#xff1a;壓測 2000 QPS、RT 200 ms&#xff0c;理論 maxThreads500&#xff0c;線上卻設 150 導致排隊。軍規 2&#xff1a;…

深入解析 Amazon Q:AWS 推出的企業級生成式 AI 助手

在人工智能助手競爭激烈的當下&#xff0c;AWS 重磅推出的 Amazon Q 憑借其強大的企業級整合能力&#xff0c;正成為開發者提升生產力的新利器。隨著生成式 AI 技術席卷全球&#xff0c;各大云廠商紛紛布局智能助手領域。在 2023 年 re:Invent 大會上&#xff0c;AWS 正式推出了…

物流自動化WMS和WCS技術文檔

導語大家好&#xff0c;我是社長&#xff0c;老K。專注分享智能制造和智能倉儲物流等內容。歡迎大家使用我們的倉儲物流技術AI智能體。新書《智能物流系統構成與技術實踐》新書《智能倉儲項目出海-英語手冊&#xff0c;必備&#xff01;》完整版文件和更多學習資料&#xff0c;…

Web3.0 實戰項目、簡歷打造、精準投遞+面試準備

目錄 一、獲取真實企業級 Web3.0 項目的 5 種方式 1. 參與開源項目&#xff08;推薦指數&#xff1a;?????&#xff09; 2. 參與黑客松&#xff08;Hackathon&#xff09; 3. 遠程實習 & DAO 協作項目&#xff08;兼職也可&#xff09; 4. Web3 Startup 實戰項目合…

pymongo庫:簡易方式存取數據

文檔 基礎使用 前提&#xff1a;開發機器已安裝mongo配置環境&#xff0c;已啟動服務。 macOS啟動服務&#xff1a;brew services start mongodb-community8.0 macOS停止服務&#xff1a;brew services stop mongodb-community8.0安裝&#xff1a;python3 -m pip install pym…

Java 線程池與多線程并發編程實戰全解析:從異步任務調度到設計模式落地,200 + 核心技巧、避坑指南與業務場景結合

多線程編程在現代軟件開發中扮演著至關重要的角色&#xff0c;它能夠顯著提升應用程序的性能和響應能力。通過合理利用異步線程、多線程以及線程池等技術&#xff0c;我們可以更高效地處理復雜任務&#xff0c;優化系統資源的使用。同時&#xff0c;在實際應用中&#xff0c;我…

gitee 分支切換

ssh-keygen -t rsa -C "pengchengzhangcplaser.com.cn" ssh -T gitgitee.comgit remote add origin 倉庫地址git config --global user.email "youexample.com"git config --global user.name "Your Name"# 1. 更新遠程信息 git fetch origin# …