【設計模式】創建型 -- 單例模式 (c++實現)

文章目錄

  • 單例模式
  • 使用場景
  • c++實現
    • 靜態局部變量
    • 餓漢式(線程安全)
    • 懶漢式(線程安全)
    • 懶漢式(線程安全)+ 智能指針
    • 懶漢式(線程安全)+智能指針+call_once
    • 懶漢式(線程安全)+智能指針+call_once+CRTP


單例模式

單例模式是指在內存只會創建且僅創建一次對象的設計模式,確保在程序運行期間只有唯一的實例。

使用場景

當對象需要被共享的時候又或者某類需要頻繁實例化.

  • 設備管理器,系統中可能有多個設備,但是只有一個設備管理器,用于管理設備驅動;
  • 數據池,用來緩存數據的數據結構,需要在一處寫,多處讀取或者多處寫,多處讀取;
  • 回收站,在整個系統運行過程中,回收站一直維護著僅有的一個實例;
  • 應用程序的日志應用,一般都何用單例模式實現,這一般是由于共享的日志文件一直處于打開狀態,因為只能有一個實例去操作,否則內容不好追加;
  • 網站的計數器,一般也是采用單例模式實現,否則難以同步。

實際開發中,如果不是完美符合使用場景,不推薦使用。
如果實際開發經驗不夠,很容易看什么都是單例。

c++實現

單例模式的關鍵點:創建且僅創建一次對象
2個關鍵點:

  1. 如何只創建一次?
  2. 如何禁止拷貝和賦值?(保證只有一個)

靜態局部變量

對于1:這很容易想到靜態局部變量
當一個函數中定義一個局部靜態變量,那么這個局部靜態變量只會初始化一次,就是在這個函數第一次調用的時候,以后無論調用幾次這個函數,函數內的局部靜態變量都不再初始化。

對于2:可以將拷貝構造和賦值重載設置位私有成員。

綜上,我們可以得到第一個版本

class Singleton1 
{
public:static Singleton1& getInstance(){static Singleton1 s_single;return s_single;}
private:Singleton1() = default;Singleton1(const Singleton1&) = delete;Singleton1& operator=(const Singleton1&) = delete;
};

上述版本的單例模式在C++11 以前存在多線程不安全的情況,多個線程同時執行這段代碼,編譯器可能會初始化多個靜態變量

magic static, 它是C++11標準中提供的新特性

  • 如果在初始化變量時控制同時進入聲明,則并發執行應等待初始化完成。
  • 如果當變量在初始化的時候,并發同時進入聲明語句,并發線程將會阻塞等待初始化結束。

即c++規定各廠商優化編譯器,能保證線程安全。所以為了保證運行安全請確保使用C++11以上的標準。

但是有些編譯器它就是不遵循c++的規定,比如vistual studio

/Zc:threadSafeInit 是 Microsoft Visual Studio 編譯器中的一個編譯選項,作用是啟用或禁用線程安全的靜態局部變量初始化。這個選項對于 C++11 引入的“magic statics”(線程安全的靜態局部變量)機制尤為重要。
當啟用 /Zc:threadSafeInit(默認在 C++11 及更高標準中啟用)時,編譯器會確保靜態局部變量的初始化是線程安全的。這意味著如果多個線程首次訪問同一個靜態局部變量,編譯器會保證該變量只被初始化一次,并確保其他線程可以看到初始化后的正確值。

在項目- 屬性 - C/C++ -命令行里可以查看,我這里沒有,即默認開啟。
在這里插入圖片描述

實際開發中一定要注意是否遵循規定。
如果遵循,推薦使用靜態局部變量的方式,又簡單又安全。

餓漢式(線程安全)

餓漢式:程序啟動即初始化

在C++11 推出以前,局部靜態變量的方式實現單例存在線程安全問題,所以部分人提出了一種方案,就是在主線程啟動后,其他線程沒有啟動前,由主線程先初始化單例資源,這樣其他線程獲取的資源就不涉及重復初始化的情況了。

//餓漢式初始化
class Singleton2
{
public:static Singleton2* getInstance(){if (s_single == nullptr){s_single = new Singleton2();}return s_single;}
private:Singleton2() = default;Singleton2(const Singleton2&) = delete;Singleton2& operator=(const Singleton2&) = delete;static Singleton2* s_single;
};
Singleton2* Singleton2::s_single = Singleton2::getInstance();

雖然從使用的角度規避多線程的安全問題,但是又引出了很多問題,如1. 啟動即初始化,可能導致程序啟動時間延長。2. 從規則上束縛了開發者

懶漢式(線程安全)

懶漢式:需要時即初始化

事例何時初始化應該由開發者決定。因此我們使用懶漢式初始化。但懶漢式初始化存在線程安全問題,即資源的重復初始化,因此,我們需要加鎖。

#include <mutex>
class Singleton3
{
public:static Singleton3* getInstance(){//這里不加鎖判斷,提高性能if (s_single != nullptr){return s_single;}s_mutex.lock();//1處if (s_single != nullptr) //2處{s_mutex.unlock();return s_single;}s_single = new Singleton3();//3處s_mutex.unlock();return s_single;}
private:Singleton3() = default;Singleton3(const Singleton3&) = delete;Singleton3& operator=(const Singleton3&) = delete;static Singleton3* s_single;static std::mutex s_mutex;
};
Singleton3* Singleton3::s_single = nullptr;
std::mutex Singleton3::s_mutex;

為什么2處要加一個判斷呢?
假如現在有線程A, B同時調用getInstance()

  1. 此時s_single == nullptr, A和B同時進入1處,假設A加上鎖,B等待
  2. A執行完3處的命令后,通過s_mutex.unlock()解鎖,此時B加上鎖。
  3. 如果沒有2處,B會再執行一遍3處,這會導致內存泄漏,而加上2處后,B會判斷s_single != nullptr, 解鎖返回

懶漢式(線程安全)+ 智能指針

但這還沒完,懶漢式相比餓漢式有一個最大的不同:不確定是哪個線程初始化的。那之后由誰析構呢?
其實不必操心,我們可以利用c++的RAIII,使用智能指針。

#include <mutex>
class Singleton3
{
public:static std::shared_ptr<Singleton3> getInstance(){if (s_single != nullptr){return s_single;}s_mutex.lock();if (s_single != nullptr){s_mutex.unlock();return s_single;}s_single = std::shared_ptr<Singleton3>(new Singleton3);s_mutex.unlock();return s_single;}
private:Singleton3() = default;Singleton3(const Singleton3&) = delete;Singleton3& operator=(const Singleton3&) = delete;static std::shared_ptr<Singleton3> s_single;static std::mutex s_mutex;
};
std::shared_ptr<Singleton3> Singleton3::s_single = nullptr;
std::mutex Singleton3::s_mutex;

有些人認為雖然智能指針能自動回收內存,如果有開發人員手動delete指針怎么辦?將析構函數設為私有,為智能指針添加刪除器

#include <mutex>
class Singleton3
{
public:static std::shared_ptr<Singleton3> getInstance(){if (s_single != nullptr){return s_single;}s_mutex.lock();if (s_single != nullptr){s_mutex.unlock();return s_single;}s_single = std::shared_ptr<Singleton3>(new Singleton3, [](Singleton3* single) {delete single;});s_mutex.unlock();return s_single;}
private:Singleton3() = default;~Singleton3() = default; //析構私有Singleton3(const Singleton3&) = delete;Singleton3& operator=(const Singleton3&) = delete;static std::shared_ptr<Singleton3> s_single;static std::mutex s_mutex;
};
std::shared_ptr<Singleton3> Singleton3::s_single = nullptr;
std::mutex Singleton3::s_mutex;

上面的代碼仍然存在危險,主要原因在于new操作是由三部分組成的

  1. 分配內存
    在第一個階段,new 操作會調用內存分配函數(默認是 operator new),在堆上為新對象分配足夠的空間。如果內存分配失敗,通常會拋出 std::bad_alloc 異常。

  2. 調用構造函數
    分配到內存后,new 操作會在剛剛分配的內存上調用對象的構造函數,初始化該對象的各個成員。構造函數的參數可以在 new 語句中直接傳遞。

  3. 返回指針
    構造函數執行完畢后,new 操作會返回一個指向新創建對象的指針。如果是 new[] 操作符(即分配數組),則返回指向數組起始元素的指針

這里的問題就再2和3的順序上,有些編譯器會優化,將2和3的順序顛倒。

    static Singleton3* getInstance(){if (s_single != nullptr) //1處{return s_single;}s_mutex.lock();if (s_single != nullptr) {s_mutex.unlock();return s_single;}s_single = new Singleton3();//2處s_mutex.unlock();return s_single;}

如果2和3的順序顛倒,那么順序變為
1.分配內存
3.返回指針
2.調用構造
可能出現下面的情況:
線程A執行到2處的new的第3步,此時s_single已經不為空,但是指向的對象還未調用構造。
線程B剛好執行1處,此時s_single != nullptr, 直接返回s_single。外部將接受到一個還沒來的及調用構造函數的對象的指針。

為解決這個問題,C++11 推出了std::call_once函數保證多個線程只執行一次

懶漢式(線程安全)+智能指針+call_once

std::call_once 是 C++11 引入的一個函數,用于保證某段代碼在多線程環境中只被執行一次。這對單例模式、懶加載或只需執行一次的初始化操作非常有用。
std::call_once 與一個 std::once_flag 對象配合使用。std::once_flag 是一個標志,確保 std::call_once 所調用的函數只會執行一次,不論有多少個線程試圖同時調用它。

#include <mutex>
class Singleton
{
public:static std::shared_ptr<Singleton> getInstance(){static std::once_flag s_flag;std::call_once(s_flag, [&]() {s_single = std::shared_ptr<Singleton>(new Singleton, [](Singleton* single) {delete single;});});return s_single;}
private:Singleton() = default;~Singleton() = default; //析構私有Singleton(const Singleton&) = delete;Singleton& operator=(const Singleton&) = delete;static std::shared_ptr<Singleton> s_single;
};
std::shared_ptr<Singleton> Singleton::s_single = nullptr;

懶漢式(線程安全)+智能指針+call_once+CRTP

為了讓單例類更通用,可以通過繼承實現多個單例類。
注:這里需要使用c++的CRTP(奇異遞歸模板模式),不知道是什么,自己查一下。

#include <mutex>
template<typename T>
class Singleton
{
public:static std::shared_ptr<T> getInstance(){static std::once_flag s_flag;std::call_once(s_flag, [&]() {s_instance = std::shared_ptr<T>(new T);});return s_instance;}
protected: Singleton() = default;Singleton(const Singleton<T>&) = delete;Singleton& operator=(const Singleton<T>&) = delete;static std::shared_ptr<T> s_instance;
};
template<typename T>
std::shared_ptr<T> Singleton<T>::s_instance = nullptr;class A :public Singleton<A> //CRTP
{friend class Singleton<A>;
public://...};

friend class Singleton<A>;的目的是允許 Singleton<A> 類訪問 A 的受保護構造函數。沒有這個 friend 聲明,Singleton<A> 將無法調用 A 的構造函數,從而無法在 getInstance 方法中正確地創建 A 的實例。

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

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

相關文章

C語言之九九乘法表

一、代碼展示 二、運行結果 三、代碼分析 首先->是外層循環是小于等于9的 然后->是內層循環是小于等于外層循環的 最后->就是\n讓九九乘法表的格式更加美觀(當然 電腦不同 有可能%2d 也有可能%3d) 四、與以下素數題目邏輯相似 五、運行結果

自動化備份全網服務器數據平臺

自動化備份全網服務器數據平臺 項目背景知識 總體需求 某企業里有一臺Web服務器&#xff0c;里面的數據很重要&#xff0c;但是如果硬盤壞了數據就會丟失&#xff0c;現在領導要求把數據做備份&#xff0c;這樣Web服務器數據丟失在可以進行恢復。要求如下&#xff1a;1.每天0…

stm32+esp8266+機智云手機app

現在很多大學嵌入式畢設都要求云端控制&#xff0c;本文章就教一下大家如何使用esp8266去連接機智云的app去進行顯示stm32的外設傳感器數據啊&#xff0c;控制一些外設啊等。 因為本文章主要教大家如何移植機智云的代碼到自己的工程&#xff0c;所以前面的一些準備工作&#x…

時序數據庫 TDengine Cloud 私有連接實戰指南:4步實現數據安全傳輸與成本優化

小T導讀&#xff1a;在物聯網和工業互聯網場景下&#xff0c;企業對高并發、低延遲的數據處理需求愈發迫切。本文將帶你深入了解 TDengineCloud 如何通過全托管服務與私有連接&#xff0c;幫助企業實現更安全、更高效、更低成本的數據采集與傳輸&#xff0c;從架構解析到實際配…

【Java面試系列】Spring Boot中自動配置原理與自定義Starter開發實踐詳解 - 3-5年Java開發必備知識

【Java面試系列】Spring Boot中自動配置原理與自定義Starter開發實踐詳解 - 3-5年Java開發必備知識 引言 Spring Boot作為Java生態中最流行的框架之一&#xff0c;其自動配置機制和Starter開發是面試中的高頻考點。對于3-5年經驗的Java開發者來說&#xff0c;深入理解這些原理…

解決Spring Boot Test中的ByteBuddy類缺失問題

目錄 解決Spring Boot Test中的ByteBuddy類缺失問題前奏問題描述問題解決第一步&#xff1a;移除ByteBuddy的特定版本號第二步&#xff1a;更新maven-surefire-plugin配置第三步&#xff1a;清理并重新構建項目 結語 解決Spring Boot Test中的ByteBuddy類缺失問題 前奏 今天&…

IntelliJ IDEA使用技巧(json字符串格式化)

文章目錄 一、IDEA自動格式化json字符串二、配置/查找格式化快捷鍵 本文主要講述idea中怎么將json字符串轉換為JSON格式的內容并且有層級結構。 效果&#xff1a; 轉換前&#xff1a; 轉換后&#xff1a; 一、IDEA自動格式化json字符串 步驟一&#xff1a;首先創建一個臨…

眨眼睛查看密碼工具類

“眨眼睛查看密碼”工具類實現思路&#xff1a; 一、核心功能 實現點擊眼睛圖標切換密碼明文/星號顯示&#xff0c;提升表單輸入體驗。包含以下關鍵功能&#xff1a; ? 初始狀態&#xff1a;密碼框顯示為星號&#xff0c;閉眼圖標可見。 ? 點擊閉眼圖標&#xff1a;切換為明…

【GPT入門】第33課 從應用場景出發,區分 TavilyAnswer 和 TavilySearchResults,代碼實戰

【GPT入門】第33課 從應用場景出發&#xff0c;區分 TavilyAnswer 和 TavilySearchResults&#xff0c;代碼實戰 1. 區別應用場景 2. 代碼使用3.代碼執行效果 在langchain_community.tools.tavily_search中&#xff0c;TavilyAnswer和TavilySearchResults有以下區別和應用場景&…

【Java設計模式】第10章 外觀模式講解

10. 外觀模式 10.1 外觀模式講解 定義:為子系統提供統一接口,簡化調用。類型:結構型模式適用場景: 子系統復雜需簡化調用分層系統需統一入口優點: 降低耦合符合迪米特法則(最少知道原則)缺點: 擴展子系統需修改外觀類,違反開閉原則10.2 外觀模式 Coding // 子系統:…

Dubbo的簡單介紹

Dubbo的簡單介紹 Dubbo 是一個高性能的 Java RPC 框架&#xff0c;最初由阿里巴巴開發&#xff0c;用于構建分布式服務。它主要用于提供服務間的通信&#xff0c;支持高效的遠程調用和服務治理&#xff0c;常用于大規模分布式系統中。Dubbo 提供了以下幾個核心功能&#xff1a…

每日一題(小白)數組娛樂篇17

對一個數組進行接收進行操作后輸出。輸入三個操作數abc&#xff0c;將數組下標a到b的數字加上c&#xff1b;輸入四個操作數abcd&#xff0c;將下標c到d的數字復制到a到b&#xff0c;可以借用一個中間量數組實現&#xff1b;兩個操作數ab&#xff0c;將數組下標a到b的數字加和輸…

總結一下常見的EasyExcel面試題

說一下你了解的POI和EasyExcel POI&#xff08;Poor Obfuscation Implementation&#xff09;&#xff1a;它是 Apache 軟件基金會的一個開源項目&#xff0c;為 Java 程序提供了讀寫 Microsoft Office 格式文件的功能&#xff0c;支持如 Excel、Word、PowerPoint 等多種文件格…

01-Redis-基礎

1 redis誕生歷程 redis的作者筆名叫做antirez&#xff0c;2008年的時候他做了一個記錄網站訪問情況的系統&#xff0c;比如每天有多少個用戶&#xff0c;多少個頁面被瀏覽&#xff0c;訪客的IP、操作系統、瀏覽器、使用的搜索關鍵詞等等(跟百度統計、CNZZ功能一樣)。最開始存儲…

在 Ubuntu 上離線安裝 Prometheus 和 Grafana

在 Ubuntu 上離線安裝 Prometheus 和 Grafana 的步驟如下: 一.安裝驗證 二.安裝步驟 1.準備離線安裝包 在一臺可以訪問互聯網的機器上下載 Prometheus 和 Grafana 的二進制文件。 Prometheus 下載地址:Prometheus 官方下載頁面Grafana 下載地址:Grafana 官方下載頁面下載所…

mapbox基礎,加載ESRI OpenStreetMap開放街景標準風格矢量圖

????? 主頁: gis分享者 ????? 感謝各位大佬 點贊?? 收藏? 留言?? 加關注?! ????? 收錄于專欄:mapbox 從入門到精通 文章目錄 一、??前言1.1 ??mapboxgl.Map 地圖對象1.1 ??mapboxgl.Map style屬性二、??加載ESRI OpenStreetMap開放街景標準風…

Java 集合有序性與重復性總結及記憶技巧

Java 集合有序性與重復性總結及記憶技巧 一、集合分類速查表 集合類型是否有序是否允許重復記憶口訣ArrayList? 有序&#xff08;插入順序&#xff09;? 可重復"數組列表&#xff0c;順序記牢"LinkedList? 有序&#xff08;插入順序&#xff09;? 可重復"…

記錄學習的第二十三天

老樣子&#xff0c;每日一題開胃。 我一開始還想著暴力解一下試試呢&#xff0c;結果不太行&#x1f602; 接著兩道動態規劃。 這道題我本來是想用最長遞增子序列來做的&#xff0c;不過實在是太麻煩了&#xff0c;實在做不下去了。 然后看了題解&#xff0c;發現可以倒著數。 …

MTK-Android12-13 屏幕永不休眠功能實現

MTK-Android12-13 屏幕永不休眠功能實現 文章目錄 需求場景參考資料修改文件簡要分析實現方案默認休眠時間設置 def_screen_off_timeout息屏時間添加永不休眠 screen_timeout_entries更新休眠時間 updateUserActivitySummaryLocked 總結 需求 屏幕永不休眠功能 備注&#xff…

Lua 中,`math.random` 的詳細用法

在 Lua 中&#xff0c;math.random 是用于生成偽隨機數的核心函數。以下是其詳細用法、注意事項及常見問題的解決方案&#xff1a; Lua 中&#xff0c;math.random 的詳細用法—目錄 一、基礎用法1. 生成隨機浮點數&#xff08;0 ≤ x < 1&#xff09;2. 生成指定范圍的隨機…