C++八股 —— 單例模式

文章目錄

  • 1. 基本概念
  • 2. 設計要點
  • 3. 實現方式
  • 4. 詳解懶漢模式

1. 基本概念

線程安全(Thread Safety)

線程安全是指在多線程環境下,某個函數、類或代碼片段能夠被多個線程同時調用時,仍能保證數據的一致性和邏輯的正確性,不會因線程切換導致錯誤結果。

單例模式(Singleton Pattern)

單例設計模式是一種創建型設計模式,其核心目的是確保一個類只有一個實例存在,并提供全局訪問點來獲取該實例。它常用于管理全局資源(如配置信息、日志系統、數據庫連接池等),避免重復創建和資源競爭。

2. 設計要點

  1. 構造函數和析構函數是私有的,不允許外部生成和釋放
    • 禁止外部實例化:外部代碼無法通過 new 或直接聲明的方式創建對象,確保唯一實例的控制權在類自身。
    • 控制生命周期:析構函數私有化可防止外部意外刪除單例對象,保證其生命周期與程序一致。
    • 符合單一職責原則:類的創建和銷毀邏輯由自身管理,避免外部干擾。
  2. 靜態成員變量和靜態返回單例的成員函數
    • 全局訪問點:通過靜態方法 getInstance() 提供統一的實例獲取方式,替代直接訪問全局變量。
    • 延遲初始化(懶漢式):僅在首次調用時創建實例,節省資源。
    • 線程安全(需額外處理):可通過鎖或局部靜態變量(C++11 后)確保多線程安全。
      • 在單例模式中,如果多個線程同時調用 getInstance() 方法,可能導致多次創建實例(如懶漢模式未加鎖時),破壞單例的唯一性。
      • 解決方案
        • 加鎖(互斥量):在 getInstance() 中使用互斥鎖(如 std::mutex)確保線程同步。
        • 局部靜態變量(C++11):利用編譯器保證局部靜態變量的初始化是線程安全的。
        • 餓漢模式:提前初始化實例,避免多線程競爭。
  3. 禁用拷貝構造函數和賦值運算符
    • 防止拷貝:避免通過拷貝構造函數復制單例對象,破壞唯一性。
    • 防止賦值:禁止通過賦值運算符覆蓋單例對象,如 instance2 = instance1
    • 強制單例約束:從語法層面杜絕意外破壞單例模式的行為。
要點解決的問題實際意義
私有構造/析構外部隨意創建或銷毀實例確保實例的唯一性和可控性
靜態成員與訪問方法全局訪問與資源管理提供統一入口,支持延遲初始化與線程安全
禁用拷貝與賦值意外復制導致多實例維護單例的嚴格唯一性

3. 實現方式

懶漢模式

懶漢模式的核心是延遲初始化(Lazy Initialization),即在首次調用 getInstance() 時才創建單例實例。在此之前,實例未被分配內存。

特點

  • 優點
    • 節省資源:若單例對象未被使用,則不會創建。
    • 適合初始化耗時的對象(如文件系統、網絡連接)。
  • 缺點
    • 需處理線程安全問題(多線程下可能重復創建)。
    • 首次訪問可能因初始化導致延遲。

餓漢模式

餓漢模式的核心是提前初始化,即在程序啟動時(或類加載時)直接創建單例實例,無論是否被使用。

特點

  • 優點
    • 線程安全:實例在程序啟動時初始化,避免多線程競爭。
    • 代碼簡單:無需處理復雜的線程同步邏輯。
  • 缺點
    • 可能浪費資源:即使未使用單例對象,也會占用內存。
    • 初始化時間可能影響程序啟動速度。

實現樣例

class Singleton {
public:static Singleton* getInstance() {return &instance; // 直接返回已初始化的實例}
private:static Singleton instance;Singleton() {}~Singleton() {}
};
// 程序啟動時初始化(餓漢模式)
Singleton Singleton::instance;

對比懶漢模式與餓漢模式

特性懶漢模式餓漢模式
初始化時機首次調用 getInstance()程序啟動時(或類加載時)
線程安全需額外處理(如加鎖或 C++11 特性)天然線程安全
資源占用按需分配,節省資源提前占用內存,可能浪費資源
適用場景初始化耗時、使用頻率不確定的對象初始化簡單、使用頻繁的對象

實際開發中,推薦使用 C++11 的局部靜態變量懶漢模式(Meyers’ Singleton,線程安全且代碼簡潔),或根據場景選擇餓漢模式。

4. 詳解懶漢模式

參考:【C++面試題】手撕單例模式_嗶哩嗶哩_bilibili

樣例1

class Singleton1 {
public:// 要點2static Singleton1 * GetInstance() {if(_instance == nullptr) {_instance = new Singleton1();}return _instance;}
private:// 要點1Singleton1() {}~Singleton1() {std::cout << "~Singleton1()\n";}// 要點3Singleton1(const Singleton1 &) = delete;Singleton1& operator = (const Singleton1&) = delete;Singleton1(Singleton1 &&) = delete;Singleton1& operator = (Singleton1 &&) = delete;// 要點2static Singleton1 *_instance; 
};
Singleton1* Singleton1::_instance = nullptr;

存在錯誤:

  • 該類創建的單例對象在堆中,雖然資源會被釋放,但其在釋放的時候是無法調用析構函數的。
  • 非線程安全

樣例2

class Singleton2 {
public:static Singleton2 * GetInstance() {if(_instance == nullpte) {_instance = new Singleton2();atexit(Destructor);}return _instance;}
private:static void Destructor() {if(nullptr != _instance) {delete _instance;_instance = nullptr;}}Singleton2() {}~Singleton2() {std::cout << "~Singleton2()\n";}Singleton2(const Singleton2 &) = delete;Singleton2& operator = (const Singleton2&) = delete;Singleton2(Singleton2 &&) = delete;Singleton2& operator = (Singleton2 &&) = delete;static Singleton2 *_instance; 
};
Singleton2* Singleton2::_instance = nullptr;

針對樣例1的問題,添加atexit(),在程序結束時手動釋放對象,從而調用析構函數

存在問題:

  • 非線程安全

樣例3

class Singleton3 {
public:static Singleton3 * GetInstance() {std::lock_guard<std::mutex> lock(_mutex);if(_instance == nullptr) {std::lock_guard<std::mutex> lock(_mutex);if(_instance == nullptr) {_instance = new Singleton3();// 1. 分配內存// 2. 調用構造函數// 3. 返回對象指針 atexit(Destructor);}}return _instance;}
private:static void Destructor() {if(nullptr != _instance) {delete _instance;_instance = nullptr;}}Singleton3() {}~Singleton3() {std::cout << "~Singleton3()\n";}Singleton3(const Singleton3 &) = delete;Singleton3& operator = (const Singleton3&) = delete;Singleton3(Singleton3 &&) = delete;Singleton3& operator = (Singleton3 &&) = delete;static Singleton3 *_instance; static std::mutex _mutex;
};
Singleton3* Singleton3::_instance = nullptr;
std::mutex Singleton3::_mutex;

在創建實例對象是使用互斥鎖來實現線程安全

  • 單檢測

    先加鎖,再判斷是否需要創建對象;

    該方法只需要檢測一次,但是在已經創建對象的情況下,只需要檢測然后返回就行,不需要再第一次檢測前加鎖(力度過大,效率低)

  • 雙檢測(Double-Checked Locking,DCL)

    先做第一次檢測,然后在需要創建對象時才加鎖,此時多線程程序會出現多個線程同時通過一次檢測到創建對象的代碼塊,所以需要第二次檢測對象是否創建來避免重復創建

存在問題

在多線程程序中,CPU會進行指令重排,如new操作的正常順序應該是(1-2-3),在指令重排之后執行順會變為(1-3-2)。此時如果某個線程執行到new的“返回對象指針操作”,而另外一個線程執行到第一次檢測,則會出現另外一個線程返回為初始化對象的情況。


樣例4:(面試八股的重點

class Singleton4 {
public:static Singleton4 * GetInstance() {Singleton4* tmp = _instance.load(std::memory_order_relaxed);std::atomic_thread_fence(std::memory_order_acquire);if(tmp == nullptr) {std::lock_guard<std::mutex> lock(_mutex);tmp = _instance.load(std::memory_order_relaxed);if(tmp == nullptr) {tmp = new Singleton4();std::atomic_thread_fence(std::memory_order_release);_instance.store(tmp, std::memory_order_relaxed);atexit(Destructor);}}return tmp;}
private:static void Destructor() {Singleton4* tmp = _instance.load(std::memory_order_relaxed);if(nullptr != tmp) {delete tmp;}}Singleton4() {}~Singleton4() {std::cout << "~Singleton4()\n";}Singleton4(const Singleton4 &) = delete;Singleton4& operator = (const Singleton4&) = delete;Singleton4(Singleton4 &&) = delete;Singleton4& operator = (Singleton4 &&) = delete;static std::atomic<Singleton4*> _instance;static std::mutex _mutex;
};
std::atomic<Singleton4*> Singleton4::_instance;
std::mutex Singleton4::_mutex;

使用內存屏障和原子操作來解決指令重排的問題

內存屏障

  • 作用
    強制限制指令重排,并確保內存操作的可見性(即一個線程的寫入對其他線程立即可見)。
  • 類型
    • 獲取屏障(acquire fence)
      后續讀/寫操作不會重排到屏障前,且能讀取其他線程的釋放操作結果。
    • 釋放屏障(release fence)
      前面的讀/寫操作不會重排到屏障后,且保證當前線程的寫入對其他線程可見。
  • 代碼中的應用
    • 獲取屏障:確保 if(tmp == nullptr) 之后的代碼能看到其他線程的完整初始化結果。
    • 釋放屏障:確保 new 的構造操作完成后,再存儲指針到 _instance

原子操作

  • 定義
    不可分割的操作,保證對變量的讀寫要么完全執行,要么不執行,不會出現中間狀態。
  • 內存順序(Memory Order)
    • memory_order_relaxed:僅保證原子性,無同步或順序約束(允許指令重排)。
    • memory_order_acquire/release:與屏障配合,實現同步語義。
  • 代碼中的應用
    _instance 被聲明為 std::atomic<Singleton4*>,確保其讀寫是原子的,避免數據競爭。

原子操作詳情參考:C++八股 —— 原子操作-CSDN博客


樣例5

class Singleton5 {
public:static Singleton5* GetInstance() {static Singleton5 instance;return &instance;}
private:Singleton5() {}~Singleton5() {std::cout << "~Singleton5()\n";}Singleton5(const Singleton5 &) = delete;Singleton5& operator = (const Singleton5&) = delete;Singleton5(Singleton5 &&) = delete;Singleton5& operator = (Singleton5 &&) = delete;
};

靜態局部變量具備單例的全部三個特性

最簡單也是最推薦的版本


樣例6

template<typename T>
class Singleton {
public:static T* GetInstance() {static T instance;return &instance;}
protected:Singleton() {}virtual ~Singleton() {std::cout << "~Singleton()\n";}
private:Singleton(const Singleton &) = delete;Singleton& operator = (const Singleton&) = delete;Singleton(Singleton &&) = delete;Singleton& operator = (Singleton &&) = delete;
};class DesignPattern : public Singleton<DesignPattern> {friend class Singleton<DesignPattern>;
private:DesignPattern() {}~DesignPattern() {std::cout << "~DesignPattern()\n";}
};

類模板封裝單例的三個特性,使用時直接繼承即可。

  • 基類構造和析構函數設置為protected是因為需要其對子類時可見的
  • 友元是為了讓基類能訪問子類的構造析構函數

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

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

相關文章

軟件工程:如何做好軟件產品

1、什么是產品 從項目到產品 產品&#xff1a;滿足行業共性需求的標準產品。即要能夠做到配置化的開發&#xff0c;用同一款產品最大限度地滿足不同客戶的需求&#xff0c;同時讓產品具有可以快速響應客戶需求變化的能力。 好的產品一定吸收了多個項目的共性&#xff0c;一定是…

Cinnamon修改面板小工具圖標

Cinnamon開始菜單-CSDN博客 設置模塊都是做好的&#xff0c;比GNOME簡單得多&#xff01; 在 applet.js 里增加 const Settings imports.ui.settings;this.settings new Settings.AppletSettings(this, HTYMenusonichy, instance_id); this.settings.bind(menu-icon, menu…

sqlsugar WhereIF條件的大于等于和等于查出來的坑

一、如下圖所示&#xff0c;當我用 .WhereIF(input.Plancontroltype > 0, u > u.Plancontroltype (DnjqPlancontroltype)input.Plancontroltype) 這里面用等于的時候&#xff0c;返回結果一條數據都沒有。 上圖中生成的SQL如下&#xff1a; SELECT id AS Id ,code AS …

centos 7 部署awstats 網站訪問檢測

一、基礎環境準備&#xff08;兩種安裝方式都要做&#xff09; bash # 安裝必要依賴 yum install -y httpd perl mod_perl perl-Time-HiRes perl-DateTime systemctl enable httpd # 設置 Apache 開機自啟 systemctl start httpd # 啟動 Apache二、安裝 AWStats&#xff0…

React從基礎入門到高級實戰:React 實戰項目 - 項目四:企業級儀表盤

React 實戰項目&#xff1a;企業級儀表盤 歡迎來到 React 開發教程專欄 的第 29 篇&#xff01;在前 28 篇文章中&#xff0c;我們從 React 的基礎概念逐步深入到高級技巧&#xff0c;涵蓋了組件設計、狀態管理、路由配置、性能優化和實時通信等核心內容。這一次&#xff0c;我…

STM32----IAP遠程升級

一、概述&#xff1a; IAP&#xff0c;全稱是“In-Application Programming”&#xff0c;中文解釋為“在程序中編程”。IAP是一種對通過微控制器的對外接口&#xff08;如USART&#xff0c;IIC&#xff0c;CAN&#xff0c;USB&#xff0c;以太網接口甚至是無線射頻通道&#…

模擬搭建私網訪問外網、外網訪問服務器服務的實踐操作

目錄 實驗環境 實踐要求 一、準備工作 1、準備四臺虛擬機&#xff0c;分別標號 2、 防火墻額外添加兩塊網卡&#xff0c;自定義網絡連接模式 3、 關閉虛擬機的圖形管理工具 4、關閉防火墻 5、分別配置四臺虛擬機的IP地址&#xff0c;此處舉一個例子&#xff08;使用的臨…

刪除遠程已經不存在但本地仍然存在的Git分支

1. 獲取遠程分支列表 首先&#xff0c;確保你獲取了遠程倉庫的最新分支信息&#xff1a; git fetch -p -p 參數會自動清理本地倉庫中那些在遠程已經被刪除的分支的引用。 2. 查看本地分支與遠程分支的對比 運行以下命令來查看哪些本地分支沒有對應的遠程分支&#xff1a; …

GIT(AI回答)

在Git中&#xff0c;git push 命令主要用于將本地分支的提交推送到?遠程倉庫?&#xff08;如GitHub、GitLab等&#xff09;。如果你希望將本地分支的改動同步到另一個?本地分支?&#xff0c;這不是 git push 的設計目的。以下是正確的替代方法&#xff1a; 方法1&#xff1…

深入剖析AI大模型:大模型時代的 Prompt 工程全解析

今天聊的內容&#xff0c;我認為是AI開發里面非常重要的內容。它在AI開發里無處不在&#xff0c;當你對 AI 助手說 "用李白的風格寫一首關于人工智能的詩"&#xff0c;或者讓翻譯模型 "將這段合同翻譯成商務日語" 時&#xff0c;輸入的這句話就是 Prompt。…

React - 組件通信

組件通信 概念&#xff1a;組件通信就是組件之間數據傳遞&#xff0c;根據組件嵌套關系不同&#xff0c;有不同的通信方法 父傳子 —— 基礎實現 實現步驟 父組件傳遞數據 - 在子組件標簽上綁定屬性子組件接收數據 - 子組件通過props參數接收數據 聲明子組件并使用 //聲明子…

RKNN開發環境搭建2-RKNN Model Zoo 環境搭建

目錄 1.簡介2.環境搭建2.1 啟動 docker 環境2.2 安裝依賴工具2.3 下載 RKNN Model Zoo2.4 RKNN模型轉化2.5編譯C++1.簡介 RKNN Model Zoo基于 RKNPU SDK 工具鏈開發, 提供了目前主流算法的部署例程. 例程包含導出RKNN模型, 使用 Python API, CAPI 推理 RKNN 模型的流程. ??本…

計算機視覺頂刊《International Journal of Computer Vision》2025年5月前沿熱點可視化分析

追蹤計算機視覺領域的前沿熱點是把握技術發展方向、推動創新落地的關鍵&#xff0c;分析這些熱點&#xff0c;不僅能洞察技術趨勢&#xff0c;更能為科研選題和工程實踐提供重要參考。本文對計算機視覺頂刊《International Journal of Computer Vision》2025年5月前沿熱點進行了…

互聯網大廠Java求職面試:云原生與微服務架構的深度探討

互聯網大廠Java求職面試&#xff1a;云原生與微服務架構的深度探討 第一輪提問 面試官&#xff1a; “鄭薪苦&#xff0c;假設我們要設計一個大規模電商平臺的微服務架構&#xff0c;你會如何設計其訂單服務&#xff1f;” 鄭薪苦&#xff1a; “首先&#xff0c;我會采用…

STM32實戰:數字音頻播放器開發指南

基于STM32的數字音頻播放器/效果器是個很棒的項目&#xff01;這涉及到多個嵌入式開發的關鍵技術點。下面我為你拆解實現方案和關鍵學習內容&#xff1a; 系統架構概覽 [SD Card] -> [File System (FATFS)] -> [Audio Decoder (WAV/MP3)] -> [DSP Processing (EQ, R…

基于TurtleBot3在Gazebo地圖實現機器人遠程控制

1. TurtleBot3環境配置 # 下載TurtleBot3核心包 mkdir -p ~/catkin_ws/src cd ~/catkin_ws/src git clone -b noetic-devel https://github.com/ROBOTIS-GIT/turtlebot3.git git clone -b noetic https://github.com/ROBOTIS-GIT/turtlebot3_msgs.git git clone -b noetic-dev…

【Vue】scoped+組件通信+props校驗

【scoped作用及原理】 【作用】 默認寫在組件中style的樣式會全局生效, 因此很容易造成多個組件之間的樣式沖突問題 故而可以給組件加上scoped 屬性&#xff0c; 令樣式只作用于當前組件的標簽 作用&#xff1a;防止不同vue組件樣式污染 【原理】 給組件加上scoped 屬性后…

IDEA 中 Maven Dependencies 出現紅色波浪線的原因及解決方法

在使用 IntelliJ IDEA 開發 Java 項目時&#xff0c;尤其是基于 Maven 的項目&#xff0c;開發者可能會遇到 Maven Dependencies 中出現紅色波浪線的問題。這種現象通常表示項目依賴未能正確解析或下載&#xff0c;導致代碼提示錯誤、編譯失敗等問題。本文將詳細分析該問題的常…

把二級域名綁定的wordpress網站的指定頁面

要將二級域名(如 beijing.wodepress.com)綁定到 WordPress 網站的指定頁面(如 wodepress.com/beijing)&#xff0c;你需要完成以下步驟&#xff1a; 步驟 1&#xff1a;創建二級域名 登錄你的域名控制面板(如 cPanel、阿里云、騰訊云等)。 找到 DNS 管理 或 域名解析 部分。…

FreeRTOS學習01_移植FreeRTOS到STM32(圖文詳解)

移植FreeRTOS到STM32 1、前言2、獲取 STM32 的裸機工程模板3、下載 FreeRTOS V9.0.0 源碼4、FreeRTOS文件夾內容簡介5、移植FreeRTOS5.1 更改STM32工程模板文件夾名字5.2 提取FreeRTOS最簡源碼5.3 拷貝 FreeRTOSConfig.h 文件到 user 文件夾5.4 添加 FreeRTOS 源碼到工程組文件…