C++单例模式万字详解:6种实现演进+线程安全彻底解决+CRTP终极模板

目录

引言:

一、单例模式概念

[1. 控制实例数量](#1. 控制实例数量)

[2. 提供全局访问点](#2. 提供全局访问点)

[3. 延迟实例化](#3. 延迟实例化)

[4. 避免命名冲突](#4. 避免命名冲突)

[5. 管理共享资源](#5. 管理共享资源)

设计单例模式的考虑因素

适用场景

不适用场景

二、单例模式六种实现演进

[1. 局部静态变量方式](#1. 局部静态变量方式)

[2. 静态成员变量指针方式(饿汉式)](#2. 静态成员变量指针方式(饿汉式))

[3. 静态成员变量指针方式(懒汉式)](#3. 静态成员变量指针方式(懒汉式))

[4. 智能指针方式(懒汉式)](#4. 智能指针方式(懒汉式))

[5. 辅助类智能指针单例模式](#5. 辅助类智能指针单例模式)

[6. 通用的单例模板类(CRTP)](#6. 通用的单例模板类(CRTP))

三、六种单例模式全方位对比总结

结语:


引言:

单例模式是 C++ 开发、后端面试最高频的设计模式 ,几乎所有工程项目中都能看到它的身影:日志管理器、配置中心、任务调度器、数据库连接池、网络管理模块,全部依赖单例保证全局唯一实例。很多小白只会最简单的懒汉/饿汉写法,但并不知道:C++98 存在线程安全漏洞、传统双重检查锁失效、裸指针内存泄漏、无法自动析构 等致命问题。从 C++98 粗糙实现,到 C++11 线程安全、智能指针自动内存管理,再到 CRTP 模板泛型终极单例,单例模式经历了完整的迭代优化。

本文从零梳理 6 种单例实现方案,由浅入深、层层递进,彻底讲懂:线程安全问题、内存泄漏隐患、各类写法优劣对比。


一、单例模式概念

今天给大家讲讲单例模式演变流程,从C++98到C++11经历了哪些变化?哪一种单例模式更为安全。

单例模式(Singleton Pattern) 是一种创建型设计模式,旨在确保一个类在整个应用程序生命周期中仅且只有一个实例,并提供一个全局访问点以获取该实例。设计单例模式的主要原因和作用包括以下几个方面:

1. 控制实例数量

单例模式确保一个类只有一个实例,防止在程序中创建多个实例可能导致的资源浪费或状态不一致问题。例如,数据库连接池、配置管理类等通常使用单例模式,以确保全局只有一个连接池或配置实例。

2. 提供全局访问点

单例模式通过提供一个全局访问点,使得在程序的任何地方都可以方便地访问该实例。这对于那些需要在多个模块或组件之间共享的资源或服务尤为重要,如日志记录器、缓存管理器等。

3. 延迟实例化

单例模式通常采用懒加载的方式,即在第一次需要使用实例时才创建。这有助于节省系统资源,特别是在实例创建成本较高或初期并不需要该实例的情况下。

4. 避免命名冲突

通过将单例实例作为一个类的静态成员,可以避免在全局命名空间中引入多个实例,减少命名冲突的风险。

5. 管理共享资源

在多线程环境下,单例模式可以有效管理共享资源,确保线程安全。例如,操作系统中的线程池、任务管理器等常使用单例模式,以协调多个线程对资源的访问。

设计单例模式的考虑因素

虽然单例模式有诸多优点,但在设计和使用时也需要注意以下几点:

  • 线程安全:在多线程环境下,需要确保单例实例的创建和访问是线程安全的,常用的方法有双重检查锁定(Double-Checked Locking)和使用静态内部类等。
  • 延迟初始化:根据需求选择是否采用延迟初始化,以平衡性能和资源利用。
  • 可测试性:单例模式可能会影响代码的可测试性,特别是在单元测试中,可能需要通过依赖注入等手段来替代单例实例。
  • 限制扩展:单例模式通过限制实例数量可能会限制类的扩展性,需谨慎使用。

适用场景

  • 需要确保全局只有一个实例的场景,如配置管理、日志系统、设备驱动等。
  • 需要全局访问点来协调系统中的多个部分,如缓存、线程池等。

不适用场景

  • 需要多个实例以满足不同需求的场景。
  • 对象的生命周期需要更灵活控制的场合。

总的来说,单例模式通过控制类的实例数量和提供全局访问点,为系统资源管理和状态一致性提供了有效的解决方案。然而,在实际应用中,应根据具体需求和上下文环境,谨慎决定是否使用单例模式,以避免潜在的设计问题。


二、单例模式六种实现演进

1. 局部静态变量方式

这是最简单、最推荐的方式,尤其适合C++11及以后版本

cpp 复制代码
//通过静态成员变量实现单例
//懒汉式
class Single2
{
private:
    Single2()
    {
    }
    Single2(const Single2 &) = delete;
    Single2 &operator=(const Single2 &) = delete;
public:
    static Single2 &GetInst()
    {
        static Single2 single;
        return single;
    }
};

上述代码通过局部静态成员single实现单例类,原理就是函数的局部静态变量生命周期随着进程结束而结束 。上述代码通过懒汉式的方式实现。

调用如下:

cpp 复制代码
void test_single2()
{
    //多线程情况下可能存在问题
    cout << "s1 addr is " << &Single2::GetInst() << endl;
    cout << "s2 addr is " << &Single2::GetInst() << endl;
}

程序输出:

确实生成了唯一实例,在C++98年代,上述单例模式存在隐患,对于多线程方式生成的实例可能时多个。

随着C++ 11的来临,这种方式不再存在线程安全问题,是最为简单也是最适合新手的方式。


2. 静态成员变量指针方式(饿汉式)

可以定义一个类的静态成员变量,用来控制实现单例,这种方式依靠静态成员提前初始化保证生成的单例是唯一的。

cpp 复制代码
//饿汉式
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()
}

程序输出:

可见无论单线程还是多线程模式下,通过静态成员变量的指针实现的单例类都是唯一的。

饿汉式是在程序启动时就进行单例的初始化 ,这种方式也可以通过懒汉式调用,无论饿汉式还是懒汉式都存在一个问题,就是什么时候释放内存?

多线程情况下,释放内存就很难了,还有二次释放内存的风险。


3. 静态成员变量指针方式(懒汉式)

我们定义一个单例类并用懒汉式方式调用

cpp 复制代码
//懒汉式指针
//即使创建指针类型也存在问题
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文件里初始化静态成员,并定义一个测试函数

cpp 复制代码
//懒汉式
//在类的cpp文件定义static变量
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();
    }
    //何时释放new的对象?造成内存泄漏
}
int main(){
    test_singlelazy();
}

函数输出:

此时生成的单例对象的内存空间还没回收,这是个问题,另外如果多线程情况下多次delete也会造成崩溃。

C++11改进

我们可以利用C++11 提供的once_flag实现安全的创建

cpp 复制代码
#ifndef DAY34_SINGLETON_SINGLETON_H
#define DAY34_SINGLETON_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;

};


#endif //DAY34_SINGLETON_SINGLETON_H

static成员要在cpp中初始化

cpp 复制代码
SingletonOnceFlag *SingletonOnceFlag::_instance = nullptr;

测试:

cpp 复制代码
#include "Singleton.h"
#include <thread>
#include <mutex>
int main() {
    system("chcp 65001 > nul");
    std::mutex mtx;
    std::thread t1([&](){
        SingletonOnceFlag::getInstance();
        std::lock_guard<std::mutex> lock(mtx);
        SingletonOnceFlag::getInstance()->PrintAddress();
    });

    std::thread t2([&](){
        SingletonOnceFlag::getInstance();
        std::lock_guard<std::mutex> lock(mtx);
        SingletonOnceFlag::getInstance()->PrintAddress();
    });

    t1.join();
    t2.join();

    return 0;
}

结果:

cpp 复制代码
0x19a74de7420
0x19a74de7420

4. 智能指针方式(懒汉式)

可以利用智能指针自动回收内存的机制设计单例类

cpp 复制代码
#ifndef DAY34_SINGLETON_SINGLETON_H
#define DAY34_SINGLETON_SINGLETON_H

#include <mutex>
#include <iostream>
#include <memory>

class SingletonOnceFlag{
public:
    static std::shared_ptr<SingletonOnceFlag> getInstance(){
        static std::once_flag flag;
        std::call_once(flag, []{
            _instance = std::shared_ptr<SingletonOnceFlag>(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 std::shared_ptr<SingletonOnceFlag> _instance;

};


#endif //DAY34_SINGLETON_SINGLETON_H

同样在SingletonOnceFlag.cpp中进行单例成员的初始化

cpp 复制代码
#include "Singleton.h"

std::shared_ptr<SingletonOnceFlag> SingletonOnceFlag::_instance = nullptr;

测试:

cpp 复制代码
#include <iostream>
#include "Singleton.h"
#include <thread>
#include <mutex>
int main() {
    system("chcp 65001 > nul");
    std::mutex mtx;
    std::thread t1([&](){
        SingletonOnceFlag::getInstance();
        std::lock_guard<std::mutex> lock(mtx);
        SingletonOnceFlag::getInstance()->PrintAddress();
    });

    std::thread t2([&](){
        SingletonOnceFlag::getInstance();
        std::lock_guard<std::mutex> lock(mtx);
        SingletonOnceFlag::getInstance()->PrintAddress();
    });

    t1.join();
    t2.join();

    return 0;
}

析构信息:

cpp 复制代码
0x1d620a47420
0x1d620a47420
this is singleton destruct

5. 辅助类智能指针单例模式

智能指针在构造的时候可以指定删除器,所以可以传递一个辅助类或者辅助函数帮助智能指针回收内存时调用我们指定的析构函数。

cpp 复制代码
#ifndef DAY34_SINGLETON_SINGLETON_H
#define DAY34_SINGLETON_SINGLETON_H

#include <mutex>
#include <iostream>
#include <memory>

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;
    }

    void PrintAddress() {
        std::cout << _instance << std::endl;
    }
    //定义友元类,通过友元类调用该类析构函数
    friend class SafeDeletor;
private:
    SingleAutoSafe() = default;
    SingleAutoSafe(const SingleAutoSafe&) = delete;
    SingleAutoSafe& operator=(const SingleAutoSafe& st) = delete;
    ~SingleAutoSafe() {
        std::cout << "this is singleton destruct" << std::endl;
    }
    static std::shared_ptr<SingleAutoSafe> _instance;

};


#endif //DAY34_SINGLETON_SINGLETON_H

在cpp文件中实现静态成员的定义

cpp 复制代码
#include "Singleton.h"

std::shared_ptr<SingleAutoSafe> SingleAutoSafe::_instance = nullptr;

SafeDeletor要写在SingleAutoSafe上边,并且SafeDeletor要声明为SingleAutoSafe类的友元类,这样就可以访问SingleAutoSafe的析构函数了。

我们在构造single时制定了SafeDeletor(),single在回收时,会调用SingleAutoSafe的仿函数,从而完成内存的销毁。

并且SingleAutoSafe的析构函数为私有的无法被外界手动调用了

测试:

cpp 复制代码
#include <iostream>
#include "Singleton.h"
#include <thread>
#include <mutex>
int main() {
    system("chcp 65001 > nul");
    std::mutex mtx;
    std::thread t1([&](){
        SingleAutoSafe::getInstance();
        std::lock_guard<std::mutex> lock(mtx);
        SingleAutoSafe::getInstance()->PrintAddress();
    });

    std::thread t2([&](){
        SingleAutoSafe::getInstance();
        std::lock_guard<std::mutex> lock(mtx);
        SingleAutoSafe::getInstance()->PrintAddress();
    });

    t1.join();
    t2.join();

    return 0;
}

程序输出:


6. 通用的单例模板类(CRTP)

我们可以通过声明单例的模板类,然后继承这个单例模板类的所有类就是单例类了。达到泛型编程提高效率的目的。

CRTP的概念

CRTP是一种将派生类作为模板参数传递给基类的技术,即一个类继承自一个以自身为模板参数的基类。这种模式常用于实现静态多态、接口的默认实现、编译时策略选择等。

比如:

cpp 复制代码
template <typename T>
class TempClass {
  //...  
};
//CRTP
class RealClass: public TempClass<RealClass>{
  //...  
};

单例基类实现

cpp 复制代码
#include <memory>
#include <mutex>
#include <iostream>
using namespace std;
template <typename T>
class Singleton {
protected:
    Singleton() = default;
    Singleton(const Singleton<T>&) = delete;
    Singleton& operator=(const Singleton<T>& st) = delete;

    static std::shared_ptr<T> _instance;
public:
    static std::shared_ptr<T> GetInstance() {
        static std::once_flag s_flag;
        std::call_once(s_flag, [&]() {
            _instance = shared_ptr<T>(new T);
        });

        return _instance;
    }
    void PrintAddress() {
        std::cout << _instance.get() << endl;
    }
    ~Singleton() {
        std::cout << "this is singleton destruct" << std::endl;
    }
};

template <typename T>
std::shared_ptr<T> Singleton<T>::_instance = nullptr;

我们定义一个网络的单例类,继承上述模板类即可,并将构造和析构设置为私有,同时设置友元保证自己的析构和构造可以被友元类调用.

cpp 复制代码
//通过继承方式实现网络模块单例
class SingleNet : public Singleton<SingleNet>
{
    friend class Singleton<SingleNet>;
private:
    SingleNet() = default;
public:
    ~SingleNet() {
        std::cout << "SingleNet destruct " << std::endl;
    }
};

测试:

cpp 复制代码
#include <iostream>
#include "Singleton.h"
#include <thread>
#include <mutex>
int main() {
    system("chcp 65001 > nul");
    std::mutex mtx;
    std::thread t1([&](){
        SingleNet::GetInstance();
        std::lock_guard<std::mutex> lock(mtx);
        SingleNet::GetInstance()->PrintAddress();
    });

    std::thread t2([&](){
        SingleNet::GetInstance();
        std::lock_guard<std::mutex> lock(mtx);
        SingleNet::GetInstance()->PrintAddress();
    });

    t1.join();
    t2.join();

    return 0;
}

程序输出:


三、六种单例模式全方位对比总结

实现方案 线程安全 内存无泄漏 懒加载 适用场景
局部静态变量 C++11 安全 日常开发、新手首选
饿汉式静态指针 全局必启动核心模块
双重锁懒汉式 老旧项目兼容
call_once 单例 追求线程安全场景
智能指针+删除器 服务端长期运行项目
CRTP 模板单例 企业级通用开发(终极方案)

结语:

从 C++98 简陋的饿汉、懒汉裸指针实现,到 C++11 基于静态变量、call_once、智能指针的安全迭代,再到 CRTP 模板泛型终极方案,单例模式的演进本质上就是解决线程安全、内存安全、代码复用的过程。

对于初学者,C++11 局部静态变量写法 足够应对绝大多数场景;对于服务端工程、长期运行的后台项目,推荐使用自定义删除器智能指针单例 ;如果是大型框架开发,CRTP 通用模板单例是最高效、最优雅的工业级方案。

相关推荐
j7~2 小时前
【C++11】C++11 新特性全套详解(列表初始化、右值引用、lambda 表达式、function 与 bind 包装器等)
c++·右值引用·lambda表达式·可变参数模板·移动语义·包装器·c++11发展史
无小道2 小时前
C/C++——可变参数
c语言·开发语言·c++·可变参数
hele_two2 小时前
C++模拟蚁群(二)
c++·算法·数学建模·图形渲染
WK100%2 小时前
LeetCode双题:滑动窗口破解最小子数组
c++·经验分享·笔记·算法
@福者3 小时前
C++ QT 入门
开发语言·c++·qt
沧海一笑-dj3 小时前
【C++】Visual Studio 2026下载和安装
c++·visual studio·vs·vs2026
HugoStudio_SWAN3 小时前
洛谷 P1321 单词覆盖还原——从蜡板上的密信到数字水印
c++·学习·程序人生·算法
励志不掉头发的内向程序员3 小时前
【LibreCAD 2D架构】从鼠标点击到屏幕像素:LibreCAD绘图架构全链路解析之Action与命令系统
linux·开发语言·c++·qt·学习·系统架构
码匠许师傅3 小时前
【设计模式精讲】15.享元模式(Flyweight)
c++·设计模式·享元模式·uml