首页 > 编程语言 >C++ - 单例模式实现

C++ - 单例模式实现

时间:2023-10-11 21:11:26浏览次数:39  
标签:Singleton GetInstance single 模式 Single SingleInstance 线程 C++ 单例

1. 什么是单例模式

单例模式是指在整个系统生命周期内,保证一个类只能产生一个实例,确保该类的唯一性。

为什么需要单例模式

两个原因:

  1. 节省资源。一个类只有一个实例,不存在多份实例,节省资源。
  2. 方便控制。在一些操作公共资源的场景时,避免了多个对象引起的复杂操作。

但是在实现单例模式时,需要考虑到线程安全的问题。

 

线程安全

  • 什么是线程安全?

在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据污染等意外情况。

  • 如何保证线程安全?
  1. 给共享的资源加把锁,保证每个资源变量每时每刻至多被一个线程占用。
  2. 让线程也拥有资源,不用去共享进程中的资源。如:使用threadlocal可以为每个线程维护一个私有的本地变量。

单例模式分类

单例模式可以分为 懒汉式 和 饿汉式 ,两者之间的区别在于创建实例的时间不同。

  • 饿汉式:即类产生的时候就创建好实例对象,这是一种空间换时间的方式
  • 懒汉式:即在需要的时候,才创建对象,这是一种时间换空间的方式

单例类的特点

  • 构造函数和析构函数为私有类型,目的是禁止外部构造和析构。
  • 拷贝构造函数和赋值构造函数是私有类型,目的是禁止外部拷贝和赋值,确保实例的唯一性。
  • 类中有一个获取实例的静态方法,可以全局访问。

 

2. 单例模式实现

2.1 普通懒汉式单例(线程不安全)

方法1:类中静态变量

存在内存泄漏问题,没有自动释放内存(不推荐使用

single.h

class Single
{
public:
    // 获取单实例对象
    static Single* GetInstance();

    // 打印实例地址
    void Print();

private:
    // 禁止外部构造
    Single();

    // 禁止外部析构
    ~Single();

    // 禁止外部拷贝构造
    Single(const Single& single) = delete;

    // 禁止外部赋值操作
    const Single& operator=(const Single& single) = delete;


private:
    static Single* m_single;
};

single.cpp

#include "single.h"
#include <iostream>
using namespace std;

Single* Single::m_single = NULL;//在类外初始化静态变量

Single* Single::GetInstance()
{
    if (m_single == NULL)
    {
        m_single = new Single();
    }
    return m_single;
}

void Single::Print()
{
    cout << "我的实例内存地址是:" << this << std::endl;
}

Single::Single()
{
    cout << "构造函数" << std::endl;
}

Single::~Single()
{
    cout << "析构函数" << std::endl;
}

main.cpp

#include "single.h"

//最基础的懒汉式(线程不安全、内存不安全)
//有以下问题:
//1.线程不安全
//2.内存泄漏

int main()
{
    Single* s1 = Single::GetInstance();
    Single* s2 = Single::GetInstance();

    return 0;
}

执行结果:

可以看到,获取了两次类的实例,构造函数被调用一次,表明只生成了唯一实例,这是个最基础版本的单例实现,他有哪些问题呢?

1. 线程安全的问题:当多线程获取单例时有可能引发竞态条件:第一个线程在if中判断 m_instance_ptr是空的,于是开始实例化单例;同时第2个线程也尝试获取单例,这个时候判断m_instance_ptr还是空的,于是也开始实例化单例;这样就会实例化出两个对象,这就是线程安全问题的由来; 解决办法:加锁

2. 内存泄漏:注意到类中只负责new出对象,却没有负责delete对象因此只有构造函数被调用,析构函数却没有被调用;因此会导致内存泄漏。

解决办法:当然我们自己手动调用delete来进行释放是可以的,但是维护在何处释放又成了问题。正确解决办法: 使用共享指针,后面会讲到;

 

方法2:类中静态变量

改进版:不存在内存泄漏问题,手动开辟手动释放(推荐使用

  • 缺点:函数调用之后需要手动释放对象,代码管理难度和内存泄漏风险提高。

single.h

class Single
{
public:
    // 获取单实例对象
    static Single* GetInstance();
    //手动释放内存
    static void FreeInstance();

    // 打印实例地址
    void Print();

private:
    // 禁止外部构造
    Single();

    // 禁止外部析构
    ~Single();

    // 禁止外部拷贝构造
    Single(const Single& single) = delete;

    // 禁止外部赋值操作
    const Single& operator=(const Single& single) = delete;


private:
    static Single* m_single;
};

single.cpp

#include "single.h"
#include <iostream>
using namespace std;

Single* Single::m_single = NULL;//在类外初始化静态变量

Single* Single::GetInstance()
{
    if (m_single == NULL)
    {
        m_single = new Single();
    }
    return m_single;
}

void Single::FreeInstance()
{
    if (m_single)
    {
        delete m_single;
        m_single = nullptr;
    }
}

void Single::Print()
{
    cout << "我的实例内存地址是:" << this << std::endl;
}

Single::Single()
{
    cout << "构造函数" << std::endl;
}

Single::~Single()
{
    cout << "析构函数" << std::endl;
}

main.cpp

#include "single.h"

int main()
{
    Single* s1 = Single::GetInstance();
    Single* s2 = Single::GetInstance();
    Single::FreeInstance();
    
    return 0;
}

运行结果:

 

2.2 加锁的懒汉式单例(线程安全)

使用互斥锁保证线程安全。(可以使用

方法1:返回普通指针

single.h:

///  加锁的懒汉式实现  //
#include <mutex>

class SingleInstance
{

public:
    // 获取单实例对象
    static SingleInstance *GetInstance();

    //释放单实例,进程退出时调用
    static void deleteInstance();
	
    // 打印实例地址
    void Print();

private:
    // 将其构造和析构成为私有的, 禁止外部构造和析构
    SingleInstance();
    ~SingleInstance();

    // 将其拷贝构造和赋值构造成为私有函数, 禁止外部拷贝和赋值
    SingleInstance(const SingleInstance &signal);
    const SingleInstance &operator=(const SingleInstance &signal);

private:
    // 唯一单实例对象指针
    static SingleInstance *m_SingleInstance;
    static std::mutex m_Mutex;
};

single.cpp:

#include "single.h"
#include <iostream>
using namespace std;

//初始化静态成员变量
SingleInstance* SingleInstance::m_SingleInstance = nullptr;
mutex SingleInstance::m_Mutex;

// 注意:不能返回指针的引用,否则存在外部被修改的风险!
SingleInstance* SingleInstance::GetInstance()
{
    //  这里使用了两个 if 判断语句的技术称为双检锁;好处是,只有判断指针为空的时候才加锁,
    //  避免每次调用 GetInstance的方法都加锁,锁的开销毕竟还是有点大的。
    if (m_SingleInstance == nullptr)
    {
        unique_lock<mutex> lock(m_Mutex); // 加锁
        if (m_SingleInstance == nullptr)
        {
            volatile auto temp = new (nothrow) SingleInstance();
            m_SingleInstance = temp;
        }
    }
    return m_SingleInstance;
}

void SingleInstance::deleteInstance()
{
    unique_lock<mutex> lock(m_Mutex); // 加锁
    if (m_SingleInstance)
    {
        delete m_SingleInstance;
        m_SingleInstance = nullptr;
    }
}

void SingleInstance::Print()
{
    cout << "我的实例内存地址是:" << this << endl;
}

SingleInstance::SingleInstance()
{
    cout << "构造函数" << endl;
}

SingleInstance::~SingleInstance()
{
    cout << "析构函数" << endl;
}

main.cpp

#include "single.h"

int main()
{
    SingleInstance* single1 = SingleInstance::GetInstance();
    SingleInstance* single2 = SingleInstance::GetInstance();
    single1->Print();
    single2->Print();
    SingleInstance::deleteInstance();

    return 0;
}

运行结果:

 

方法2:返回智能指针

推荐使用

single.h:

#include <iostream>
#include <memory>
#include <mutex>
using namespace std;

class Singleton
{
public:

    static shared_ptr<Singleton> GetInstance();

    void print();

    ~Singleton();

private:
    Singleton();
};

single.cpp:

#include "single.h"

static shared_ptr<Singleton> singleton = nullptr;
static mutex singletonMutex;

shared_ptr<Singleton> Singleton::GetInstance()
{
    if (singleton == nullptr)
    {
        unique_lock<mutex> lock(singletonMutex);
        if (singleton == nullptr)
        {
            auto temp = shared_ptr<Singleton>(new Singleton());
            singleton = temp;
        }
    }
    return singleton;
}

void Singleton::print()
{
    cout << "我的实例内存地址是:" << this << endl;
}

Singleton::Singleton()
{
    cout << "构造函数!" << endl;
}

Singleton::~Singleton()
{
    cout << "析构函数!" << endl;
}

main.cpp

#include "single.h"

int main()
{
    shared_ptr<Singleton> single1 = Singleton::GetInstance();
    single1->print();
    shared_ptr<Singleton> single2 = Singleton::GetInstance();
    single2->print();

    return 0;
}

运行结果:

 

2.3 局部静态变量的懒汉单例(C++11线程安全)

方法1:局部静态变量-栈区写法

单例的经典实现方式是「局部静态变量的懒汉单例」,(最推荐使用这种方式)。

single.h:

class Single
{
public:
    // 获取单实例对象
    static Single& GetInstance();
    // 打印实例地址
    void Print();

private:
    // 禁止外部构造
    Single();
    // 禁止外部析构
    ~Single();
    // 禁止外部拷贝构造
    Single(const Single& single) = delete;
    // 禁止外部赋值操作
    const Single& operator=(const Single& single) = delete;
};

single.cpp:

#include "single.h"
#include <iostream>
using namespace std;

Single& Single::GetInstance()
{
    /**
     * 局部静态特性的方式实现单实例。
     * 静态局部变量只在当前函数内有效,其他函数无法访问。
     * 静态局部变量只在第一次被调用的时候初始化,也存储在静态存储区,生命周期从第一次被初始化起至程序结束止。
     */
    static Single single;
    return single;
}

void Single::Print()
{
    cout << "我的实例内存地址是:" << this << std::endl;
}

Single::Single()
{
    cout << "构造函数" << std::endl;
}

Single::~Single()
{
    cout << "析构函数" << std::endl;
}

main.cpp

#include "single.h"

int main()
{
    Single& s1 = Single::GetInstance();
    Single& s2 = Single::GetInstance();

    return 0;
}

运行结果:

通过在GetInstance函数中引入静态变量,达到了在不使用智能指针和锁的情况下,也能够保证线程安全和内存安全的效果,

  • C++11标准中的Magic Static特性保证线程安全,如果当变量在初始化的时候,并发同时进入声明语句,并发线程将会阻塞等待初始化结束。
  • 没有使用指针来new对象,没有内存泄漏的问题。

由于静态局部变量在程序执行到该对象的声明处时才被首次初始化,所以这也是一种懒汉式。(饿汉式用的是类的静态成员变量,不属于这个类,)

 

方法2:局部静态变量-堆区写法

用开内存的方式,动态申请内存一块内存空间(不太推荐使用

  • 缺点:函数调用之后需要手动释放对象,代码管理难度和内存泄漏风险提高。

single.h

class Single
{
public:
    // 获取单实例对象
    static Single* GetInstance();

    // 打印实例地址
    void Print();

private:
    // 禁止外部构造
    Single();

    // 禁止外部析构
    ~Single();

    // 禁止外部拷贝构造
    Single(const Single& single) = delete;

    // 禁止外部赋值操作
    const Single& operator=(const Single& single) = delete;
};

single.cpp

#include "single.h"


Single* Single::GetInstance()
{
    static Single* single = NULL;
    if (single == NULL)
    {
        single = new Single();
        cout << "single = " << single << endl;
    }
    return single;
}

void Single::Print()
{
    cout << "我的实例内存地址是:" << this << std::endl;
}

Single::Single()
{
    cout << "构造函数" << std::endl;
}

Single::~Single()
{
    cout << "析构函数" << std::endl;
}

main.cpp

#include "single.h"

int main()
{
    Single* s1 = Single::GetInstance();
    cout << "s1 = " << s1 << endl;
    s1->Print();

    return 0;
}

运行结果:

注意:这里就有问题了,没有进入析构函数,没有自动释放内存,会导致内存泄漏!

既然没有自动释放,那就手动释放一下,改成如下代码:

可以看到报错了,提示说Single的析构函数不可访问,这时候需要把Single的析构函数改成公有的。

 

改完后的执行结果,如下图:

 

2.4 饿汉式单例(线程安全)

single.h:

// 饿汉实现 /

class Singleton
{
public:
    // 获取单实例
    static Singleton* GetInstance();

    // 释放单实例,进程退出时调用
    static void deleteInstance();
    
    // 打印实例地址
    void Print();

private:
    // 将其构造和析构成为私有的, 禁止外部构造和析构
    Singleton();
    ~Singleton();

    // 将其拷贝构造和赋值构造成为私有函数, 禁止外部拷贝和赋值
    Singleton(const Singleton &signal);
    const Singleton &operator=(const Singleton &signal);

private:
    // 唯一单实例对象指针
    static Singleton *g_pSingleton;
};

single.cpp:

#include "single.h"
#include <iostream>
using namespace std;

// 代码一运行就初始化创建实例 ,本身就线程安全
Singleton* Singleton::g_pSingleton = new (std::nothrow) Singleton();

Singleton* Singleton::GetInstance()
{
    return g_pSingleton;
}

void Singleton::deleteInstance()
{
    if (g_pSingleton)
    {
        delete g_pSingleton;
        g_pSingleton = nullptr;
    }
}

void Singleton::Print()
{
    cout << "我的实例内存地址是:" << this << std::endl;
}

Singleton::Singleton()
{
    cout << "构造函数" << std::endl;
}

Singleton::~Singleton()
{
    cout << "析构函数" << std::endl;
}

main.cpp:

#include "single.h"

int main()
{
    Singleton* s1 = Singleton::GetInstance();
    Singleton* s2 = Singleton::GetInstance();
    Singleton::deleteInstance();

    return 0;
}

 运行结果:

 

2.5 使用 C++11 std::call_once 实现单例(C++11线程安全)

#include <iostream>
#include <memory>
#include <mutex>
using namespace std;

class Singleton
{
public:
    static shared_ptr<Singleton> getSingleton();

    void print() 
    {
        cout << "Hello World." << endl;
    }

    ~Singleton() 
    {
        cout << "析构函数!" << endl;
    }

private:
    Singleton() 
    {
        cout << "构造函数!" << endl;
    }
};

static shared_ptr<Singleton> singleton = nullptr;
static once_flag singletonFlag;

shared_ptr<Singleton> Singleton::getSingleton() 
{
    call_once(singletonFlag, [&] 
        {
        singleton = shared_ptr<Singleton>(new Singleton());
        });
    return singleton;
}


int main()
{
    shared_ptr<Singleton> s1 = Singleton::getSingleton();
    shared_ptr<Singleton> s2 = Singleton::getSingleton();

    return 0;
}

运行结果:

 

————————————————
版权声明:本文为CSDN博主「unonoi」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。

原文链接:https://blog.csdn.net/unonoi/article/details/121138176

标签:Singleton,GetInstance,single,模式,Single,SingleInstance,线程,C++,单例
From: https://www.cnblogs.com/zhuchunlin/p/17758198.html

相关文章

  • 组合模式
           ......
  • Visual Studio C++ 项目调试启动无法正常调试的问题
    启动项目后,设置的断点不起作用原因是需要在项目设置里面启用下面的选项......
  • 适配器模式
        ......
  • 单例
        ......
  • 原型模式
      ......
  • JAVA设计模式——策略模式
    策略模式是一种行为型设计模式,它允许在运行时选择算法的行为。它定义了一系列算法,将每个算法封装起来并使它们可以相互替换。策略模式使算法的变化独立于使用算法的客户端。在策略模式中,我们有一个上下文对象,该对象包含一个指向策略对象的引用。策略对象实现了一个公共接口,该接口......
  • C++黑马程序员——P223-226. set容器 构造和赋值,大小和交换,插入和删除,查找和统计
    P223.set容器——构造和赋值P224.set容器——大小和交换P225.set容器——插入和删除P226.set容器——查找和统计P223.set容器构造和赋值特点:所有元素都会在插入时自动被排序本质:set/multiset属于关联式容器,底层结构是用二叉树实现。set和multiset的区别set不......
  • 1——of C++ and Java togather
    因为那个C++最全的笔记是从第18课开始做(笔者说18课之前都很基础),所以这里就对前18课的知识做个笔记总结C++的工作过程这里提到的C++工作过程主要涉及两个:编译与链接之前考研时候学到,(在组成原理的某个章节),计算机的工作过程其实就涉及“将源程序转换成可执行文件”,与其中便......
  • C++ - 基于范围的 for 循环
    在C++98/03中,不同的容器和数组遍历的方式不尽相同,写法不统一,也不够简洁,而C++11基于范围的for循环可以以简洁、统一的方式来遍历容器和数组,用起来也更方便了。1.for循环新语法在介绍新语法之前,先来看一个使用迭代器遍历容器的例子:#include<iostream>#include<vector>......
  • C++ - move()函数
    C++11标准中借助右值引用可以为指定类添加移动构造函数,这样当使用该类的右值对象(可以理解为临时对象)初始化同类对象时,编译器会优先选择移动构造函数。注意,移动构造函数的调用时机是:用同类的右值对象初始化新对象。那么,用当前类的左值对象(有名称,能获取其存储地址的实例对象)初始化......