设计模式之单例模式(C++)

简介: 设计模式之单例模式(C++)

一、单例模式是什么?

      单例模式是一种创建型的软件设计模式,在工程项目中非常常见。通过单例模式的设计,使得创建的类在当前进程中只有一个实例,并提供一个全局性的访问点,这样可以规避因频繁创建对象而导致的内存飙升情况。


      实现单例模式的三个要点:


1)私有化构造函数:这样外界就无法自由地创建类对象,进而阻止了多个实例的产生。


2)类定义中含有该类的唯一静态私有对象:静态变量存放在全局存储区,且是唯一的,供所有对象使用。


3)用公有的静态函数来获取该实例:提供了访问接口。


      单例模式一般分为懒汉式和饿汉式。


1)懒汉式:在使用类对象(单例实例)时才会去创建它,不然就懒得去搞。


2)饿汉式:单例实例在类装载时构建,有可能全局都没使用过,但它占用了空间,就像等着发救济粮的饿汉提前排好队等吃的一样。

二、懒汉式实现

2.1 懒汉基础实现

      最基本的懒汉实现方法。

//Singleton.h
/****************************************************/
#include <iostream>
using namespace std;
// 单例模式演示类
class Singleton
{
public:
  // 公有接口获取唯一实例
  static Singleton* getInstance(){
    // 若为空则创建
    if (instance == nullptr) {
      cout << "实例为空,开始创建。" << endl;
      instance = new Singleton();
      cout << "创建结束。" << endl;
    }
    else {
      cout << "已有实例,返回。" << endl;
    }
    return instance;
  }
private:
  // 私有构造函数
  Singleton(){
    cout << "构造函数启动。" << endl;
  };
  // 私有析构函数
  ~Singleton(){
    cout << "析构函数启动。" << endl;
  };
private:
  // 静态私有对象
  static Singleton* instance;
};
// 初始化
Singleton* Singleton::instance = nullptr;
//main.cpp
/****************************************************/
#include <iostream>
#include "Singleton.h"
using namespace std;
int main() 
{
  cout << "main开始" << endl;
  Singleton* s1 = Singleton::getInstance();
  Singleton* s2 = Singleton::getInstance();
  Singleton* s3 = Singleton::getInstance();
  cout << "main结束" << endl;
  return 0;
}

      执行代码,让我们看看结果。

      从结果中可以看出这样设计主要有两个问题,一个是线程安全,另一个是内存泄漏。


      线程安全是因为在多线程场景下,有可能出现多个线程同时进行new操作的情况,没通过加锁来限制。


      内存泄漏是因为使用了new在堆上分配了资源,那么在程序结束时,也应该进行delete,确保堆中数据释放。


      接下来,我们先解决线程安全问题,对懒汉式实现进行改进。

2.2 基于双重检测锁的懒汉实现

      通过双重检测锁,可以确保线程安全。

//Singleton.h
/****************************************************/
#include <iostream>
#include <mutex>
using namespace std;
// 单例模式演示类
class Singleton
{
public:
  // 公有接口获取唯一实例
  static Singleton* getInstance(){
    // 若为空则创建
    if (instance == nullptr) {
      // 加锁保证线程安全
      // 如果两个线程同时进行到这一步,一个线程继续向下执行时,另一个线程被堵塞
      // 等锁解除后,被堵塞的线程就会跳过下面的if了,因为此时实例已经构建完毕
      lock_guard<mutex> l(m_mutex);
      if (instance == nullptr) {
        cout << "实例为空,开始创建。" << endl;
        instance = new Singleton();
        cout << "地址为:" << instance << endl;
        cout << "创建结束。" << endl;
      }
    }
    else {
      cout << "已有实例,返回。" << endl;
    }
    return instance;
  }
private:
  // 私有构造函数
  Singleton(){
    cout << "构造函数启动。" << endl;
  };
  // 私有析构函数
  ~Singleton(){
    cout << "析构函数启动。" << endl;
  };
private:
  // 静态私有对象
  static Singleton* instance;
  // 锁
  static mutex m_mutex;
};
// 初始化
Singleton* Singleton::instance = nullptr;
mutex Singleton::m_mutex;
//main.cpp
/****************************************************/
#include <iostream>
#include "Singleton.h"
using namespace std;
int main() 
{
  cout << "main开始" << endl;
  thread t1([] {
    Singleton* s1 = Singleton::getInstance();
  });
  thread t2([] {
    Singleton* s2 = Singleton::getInstance();
  });
  t1.join();
  t2.join();
  Singleton* s3 = Singleton::getInstance();
  cout << "地址为:" << s3 << endl;
  cout << "main结束" << endl;
  return 0;
}

      执行代码,让我们看看结果。

      这样看来没有问题,那如果取消双重检测锁,在多线程下看看会发生什么。将代码部分函数修改为下。把锁注释掉,再查看地址信息。

// 公有接口获取唯一实例
static Singleton* getInstance(){
  // 若为空则创建
  if (instance == nullptr) {
    // 加锁保证线程安全
    // 如果两个线程同时进行到这一步,一个线程继续向下执行时,另一个线程被堵塞
    // 等锁解除后,被堵塞的线程就会跳过下面的if了,因为此时实例已经构建完毕
    //lock_guard<mutex> l(m_mutex);
    if (instance == nullptr) {
      cout << "实例为空,开始创建。" << endl;
      instance = new Singleton();
      cout << "地址为:" << instance << endl;
      cout << "创建结束。" << endl;
    }
  }
  else {
    cout << "已有实例,返回。" << endl;
  }
  return instance;
}

      此时结果中可以看出,两个线程进行了两次new操作,但是最后只能捕捉到最后一次new的地址信息了,前面的那个丢失了。。。。。

      这个测试也是让大家直观地感受下双重检测锁的用处。

      接下来,我们再解决内存泄漏(资源释放)问题,对懒汉式实现进行进一步的改进。

2.3 基于双重检测锁和资源管理的懒汉实现

      在2.2的基础上,我们加入资源管理机制,以达到对资源的释放的目的,解决方法有两个:智能指针&静态嵌套类。

2.3.1 智能指针方案

      将实例指针更换为智能指针,另外智能指针在初始化时,还需要人为添加公有的毁灭函数,因为析构函数私有化了。

//Singleton.h
/****************************************************/
#include <iostream>
#include <mutex>
using namespace std;
// 单例模式演示类
class Singleton
{
public:
  // 公有接口获取唯一实例
  static shared_ptr<Singleton> getInstance(){
    // 若为空则创建
    if (instance == nullptr) {
      // 加锁保证线程安全
      // 如果两个线程同时进行到这一步,一个线程继续向下执行时,另一个线程被堵塞
      // 等锁解除后,被堵塞的线程就会跳过下面的if了,因为此时实例已经构建完毕
      lock_guard<mutex> l(m_mutex);
      if (instance == nullptr) {
        cout << "实例为空,开始创建。" << endl;
        instance.reset(new Singleton(), destoryInstance);
        cout << "地址为:" << instance << endl;
        cout << "创建结束。" << endl;
      }
    }
    else {
      cout << "已有实例,返回。" << endl;
    }
    return instance;
  }
  // 毁灭实例
  static void destoryInstance(Singleton* x) {
    cout << "自定义释放实例" << endl;
    delete x;
  }
private:
  // 私有构造函数
  Singleton(){
    cout << "构造函数启动。" << endl;
  };
  // 私有析构函数
  ~Singleton(){
    cout << "析构函数启动。" << endl;
  };
private:
  // 静态私有对象
  static shared_ptr<Singleton> instance;
  // 锁
  static mutex m_mutex;
};
// 初始化
shared_ptr<Singleton> Singleton::instance;
mutex Singleton::m_mutex;
//main.cpp
/****************************************************/
#include <iostream>
#include "Singleton.h"
using namespace std;
int main() 
{
  cout << "main开始" << endl;
  thread t1([] {
    shared_ptr<Singleton> s1 = Singleton::getInstance();
  });
  thread t2([] {
    shared_ptr<Singleton> s2 = Singleton::getInstance();
  });
  t1.join();
  t2.join();
  shared_ptr<Singleton> s3 = Singleton::getInstance();
  cout << "地址为:" << s3 << endl;
  cout << "main结束" << endl;
  return 0;
}

     应用智能指针后,在程序结束时,它自动进行资源的释放,解决了内存泄漏的问题。

2.3.2 静态嵌套类方案

      类中定义一个嵌套类,初始化该类的静态对象,当程序结束时,该对象进行析构的同时,将单例实例也删除了。

//Singleton.h
/****************************************************/
#include <iostream>
#include <mutex>
using namespace std;
// 单例模式演示类
class Singleton
{
public:
  // 公有接口获取唯一实例
  static Singleton* getInstance() {
    // 若为空则创建
    if (instance == nullptr) {
      // 加锁保证线程安全
      // 如果两个线程同时进行到这一步,一个线程继续向下执行时,另一个线程被堵塞
      // 等锁解除后,被堵塞的线程就会跳过下面的if了,因为此时实例已经构建完毕
      lock_guard<mutex> l(m_mutex);
      if (instance == nullptr) {
        cout << "实例为空,开始创建。" << endl;
        instance = new Singleton();
        cout << "地址为:" << instance << endl;
        cout << "创建结束。" << endl;
      }
    }
    else {
      cout << "已有实例,返回。" << endl;
    }
    return instance;
  }
private:
  // 私有构造函数
  Singleton() {
    cout << "构造函数启动。" << endl;
  };
  // 私有析构函数
  ~Singleton() {
    cout << "析构函数启动。" << endl;
  };
  // 定义一个删除器
  class Deleter {
  public:
    Deleter() {};
    ~Deleter() {
      if (instance != nullptr) {
        cout << "删除器启动。" << endl;
        delete instance;
        instance = nullptr;
      }
    }
  };
  // 删除器是嵌套类,当该静态对象销毁的时候,也会将单例实例销毁
  static Deleter m_deleter;
private:
  // 静态私有对象
  static Singleton* instance;
  // 锁
  static mutex m_mutex;
};
// 初始化
Singleton* Singleton::instance = nullptr;
mutex Singleton::m_mutex;
Singleton::Deleter Singleton::m_deleter;

     main.h同2.2中的一致,结果如下,可以看出,当嵌套类Deleter对象销毁时,其析构函数执行的实例删除操作也完成了。

2.4 基于局部静态对象的懒汉实现

      C++11后,规定了局部静态对象在多线程场景下的初始化行为,只有在首次访问时才会创建实例,后续不再创建而是获取。若未创建成功,其他的线程在进行到这步时会自动等待。注意C++11前的版本不是这样的。


      因为有上述的改动,所以出现了一种更简洁方便优雅的实现方法,基于局部静态对象实现。


//Sin

//Singleton.h
/****************************************************/
#include <iostream>
#include <mutex>
using namespace std;
// 单例模式演示类
class Singleton
{
public:
  // 公有接口获取唯一实例
  static Singleton& getInstance() {
    cout << "获取实例" << endl;
    static Singleton instance;
    cout << "地址为:" << &instance << endl;
    return instance;
  }
private:
  // 私有构造函数
  Singleton() {
    cout << "构造函数启动。" << endl;
  };
  // 私有析构函数
  ~Singleton() {
    cout << "析构函数启动。" << endl;
  };
};
//main.cpp
/****************************************************/
#include <iostream>
#include "Singleton.h"
using namespace std;
int main()
{
  cout << "main开始" << endl;
  thread t1([] {
    Singleton &s1 = Singleton::getInstance();
  });
  thread t2([] {
    Singleton &s2 = Singleton::getInstance();
  });
  t1.join();
  t2.join();
  cout << "main结束" << endl;
  return 0;
}

      从结果中可以看出,构造函数启动了一次,另一个线程直接获取了地址。并且当程序结束时,进行了自动释放。

三、饿汉式实现

3.1 饿汉基础实现

      饿汉和懒汉的差别就在于,饿汉提前进行了创建,所以它的基础实现也不是很复杂,如下所示。

//Singleton.h
/****************************************************/
#include <iostream>
#include <mutex>
using namespace std;
// 单例模式演示类
class Singleton
{
public:
  // 公有接口获取唯一实例
  static Singleton* getInstance() {
    cout << "获取实例" << endl;
    cout << "地址为:" << instance << endl;
    return instance;
  }
private:
  // 私有构造函数
  Singleton() {
    cout << "构造函数启动。" << endl;
  };
  // 私有析构函数
  ~Singleton() {
    cout << "析构函数启动。" << endl;
  };
private:
  // 静态私有对象
  static Singleton* instance;
};
// 初始化
Singleton* Singleton::instance = new Singleton();
//main.cpp
/****************************************************/
#include <iostream>
#include "Singleton.h"
using namespace std;
int main()
{
  cout << "main开始" << endl;
  thread t1([] {
    Singleton* s1 = Singleton::getInstance();
  });
  thread t2([] {
    Singleton* s2 = Singleton::getInstance();
  });
  t1.join();
  t2.join();
  cout << "main结束" << endl;
  return 0;
}

      输出结果中可知,main还没开始,实例就已经构建完毕,获取实例的函数也不需要进行判空操作,因此也就不用双重检测锁来保证线程安全了,它本身已经是线程安全状态了。

      但是内存泄漏的问题还是要解决的,这点同懒汉是一样的。

3.2 基于资源管理的饿汉实现

      内存泄漏解决方法有两个:智能指针&静态嵌套类。

3.2.1 智能指针方案

      将实例指针更换为智能指针,另外智能指针在初始化时,还需要人为添加公有的毁灭函数,因为析构函数私有化了。

//Singleton.h
/****************************************************/
#include <iostream>
#include <mutex>
using namespace std;
// 单例模式演示类
class Singleton
{
public:
  // 公有接口获取唯一实例
  static shared_ptr<Singleton> getInstance() {
    cout << "获取实例" << endl;
    cout << "地址为:" << instance << endl;
    return instance;
  }
  // 毁灭实例
  static void destoryInstance(Singleton* x) {
    cout << "自定义释放实例" << endl;
    delete x;
  }
private:
  // 私有构造函数
  Singleton() {
    cout << "构造函数启动。" << endl;
  };
  // 私有析构函数
  ~Singleton() {
    cout << "析构函数启动。" << endl;
  };
private:
  // 静态私有对象
  static shared_ptr<Singleton> instance;
};
// 初始化
shared_ptr<Singleton> Singleton::instance(new Singleton(), destoryInstance);
//main.cpp
/****************************************************/
#include <iostream>
#include "Singleton.h"
using namespace std;
int main()
{
  cout << "main开始" << endl;
  thread t1([] {
    shared_ptr<Singleton> s1 = Singleton::getInstance();
  });
  thread t2([] {
    shared_ptr<Singleton> s2 = Singleton::getInstance();
  });
  t1.join();
  t2.join();
  cout << "main结束" << endl;
  return 0;
}

   加入了智能指针后,不出意外地进行了自动的资源释放。

3.2.2 静态嵌套类方案

      类中定义一个嵌套类,初始化该类的静态对象,当程序结束时,该对象进行析构的同时,将单例实例也删除了。

//Singleton.h
/****************************************************/
#include <iostream>
#include <mutex>
using namespace std;
// 单例模式演示类
class Singleton
{
public:
  // 公有接口获取唯一实例
  static Singleton* getInstance() {
    cout << "获取实例" << endl;
    cout << "地址为:" << instance << endl;
    return instance;
  }
private:
  // 私有构造函数
  Singleton() {
    cout << "构造函数启动。" << endl;
  };
  // 私有析构函数
  ~Singleton() {
    cout << "析构函数启动。" << endl;
  };
  // 定义一个删除器
  class Deleter {
  public:
    Deleter() {};
    ~Deleter() {
      if (instance != nullptr) {
        cout << "删除器启动。" << endl;
        delete instance;
        instance = nullptr;
      }
    }
  };
  // 删除器是嵌套类,当该静态对象销毁的时候,也会将单例实例销毁
  static Deleter m_deleter;
private:
  // 静态私有对象
  static Singleton* instance;
};
// 初始化
Singleton* Singleton::instance = new Singleton();
Singleton::Deleter Singleton::m_deleter;
//main.cpp
/****************************************************/
#include <iostream>
#include "Singleton.h"
using namespace std;
int main()
{
  cout << "main开始" << endl;
  thread t1([] {
    Singleton* s1 = Singleton::getInstance();
  });
  thread t2([] {
    Singleton* s2 = Singleton::getInstance();
  });
  t1.join();
  t2.join();
  cout << "main结束" << endl;
  return 0;
}

 同懒汉的一样,不多做阐述。

四、总结

      上述讲了这么多关于单例模式的内容,我尽可能地将测试的结果也同步展示了,目的就是帮助大家更好地理解。文中所有的代码都是完整的,可以直接复制到自己的项目中测试验证下。


      最后,如果说让我选择用什么样的实现,那我选择用局部静态对象的方法,代码简洁,线程安全,内存无泄漏,有什么理由说不呢?除非你是C++11之前的版本。。。。


      如果文章帮助到你了,可以点个赞让我知道,我会很快乐~加油!

相关文章
|
2月前
|
设计模式 安全 Java
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
30 2
|
7天前
|
设计模式 存储 前端开发
前端必须掌握的设计模式——单例模式
单例模式是一种简单的创建型设计模式,确保一个类只有一个实例,并提供一个全局访问点。适用于窗口对象、登录弹窗等场景,优点包括易于维护、访问和低消耗,但也有安全隐患、可能形成巨石对象及扩展性差等缺点。文中展示了JavaScript和TypeScript的实现方法。
|
13天前
|
设计模式 安全 Java
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
18 2
|
27天前
|
设计模式 Java 数据库连接
Java编程中的设计模式:单例模式的深度剖析
【10月更文挑战第41天】本文深入探讨了Java中广泛使用的单例设计模式,旨在通过简明扼要的语言和实际示例,帮助读者理解其核心原理和应用。文章将介绍单例模式的重要性、实现方式以及在实际应用中如何优雅地处理多线程问题。
32 4
|
1月前
|
设计模式 安全 Java
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
|
18天前
|
设计模式 安全 Java
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
|
1月前
|
设计模式 存储 数据库连接
PHP中的设计模式:单例模式的深入理解与应用
【10月更文挑战第22天】 在软件开发中,设计模式是解决特定问题的通用解决方案。本文将通过通俗易懂的语言和实例,深入探讨PHP中单例模式的概念、实现方法及其在实际开发中的应用,帮助读者更好地理解和运用这一重要的设计模式。
19 1
|
26天前
|
设计模式 安全 Java
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
25 0
|
2月前
|
设计模式 存储 数据库连接
PHP中的设计模式:单例模式的深入解析与实践
在PHP开发中,设计模式是提高代码可维护性、扩展性和复用性的关键技术之一。本文将通过探讨单例模式,一种最常用的设计模式,来揭示其在PHP中的应用及优势。单例模式确保一个类仅有一个实例,并提供一个全局访问点。通过实际案例,我们将展示如何在PHP项目中有效实现单例模式,以及如何利用这一模式优化资源配置和管理。无论是PHP初学者还是经验丰富的开发者,都能从本文中获得有价值的见解和技巧,进而提升自己的编程实践。
|
2月前
|
设计模式 安全 Java
C# 一分钟浅谈:设计模式之单例模式
【10月更文挑战第9天】单例模式是软件开发中最常用的设计模式之一,旨在确保一个类只有一个实例,并提供一个全局访问点。本文介绍了单例模式的基本概念、实现方式(包括饿汉式、懒汉式和使用 `Lazy&lt;T&gt;` 的方法)、常见问题(如多线程和序列化问题)及其解决方案,并通过代码示例详细说明了这些内容。希望本文能帮助你在实际开发中更好地应用单例模式,提高代码质量和可维护性。
56 1