当前位置: 首页 > news >正文

C++单例模式

先简单介绍一下单例模式:

        单例模式(Singletion Pattern)是一种软件开发中的设计模式,属于创建型模式(也称工厂模式,封装对象的创建过程,使客户端可以透明地创建对象,而不需要关心对象的内部实现细节)。单例模式确保一个类只有一个实例,并提供一个全局访问点来访问这个实例。单例模式通常用于管理共享资源,如数据库连接、文件系统、硬件设备等,或者在多个线程之间共享数据。

 由上面的定义,我们也可以知道单例模式的几个特点:

  1. 全局唯一性:确保一个类只有一个实例,并提供一个全局访问点。
  2. 自我实例化:单例类负责创建自己的唯一实例。
  3. 延迟初始化:单例实例通常在第一次使用时创建,而不是在程序启东市。
  4. 可访问性:单例类通常提供一个静态的访问方法,允许客户端访问其唯一实例。

实现单例模式需要以下几个步骤:

  • 私有构造函数:防止外部通过new直接创建类的实例。
  • 静态私有实例变量:保存类的唯一实例。
  • 静态公有方法:提供一个全局访问点用于获取唯一实例。

简单的单例模式(懒汉Lazy Singleton)

#include <iostream>class Singleton{
private://私有构造函数Singleton(){}//静态私有实例变量static Singleton* instance;
public://禁止拷贝构造函数和赋值操作Singleton(const Singleton&) = delete;Singleton& operator=(const Singleton&) = delete;//公有静态方法,提取全局访问点static Singleton* getInstance(){//判断是否第一次调用if(instance = nullptr){instance = new Singleton();}return instance;}//实例方法void dosomething(){std::cout<<"Hello world"<<std:;endl;}
}// 初始化静态私有实例变量
Singleton * Singleton::instance = nullptr;int main(){//获取单例实例Singleton* singleton = Singleton::getInstance();//使用单例实例singleton->dosomething();
}

        这个简单实例也称为懒汉单例模式(Lazy Singleton),单例在第一次使用才会被初始化,或称为延迟初始化,也正因为仅在第一次使用时创建单例实例,能够节省资源,但在某些情况下,可能会导致内存泄漏。举例如下:

  • 单例实例通常为静态成员变量,静态成员变量的生命周期与程序的生命周期相同,若单例实例持有资源(动态分配的内存、文件描述符等),而这些资源在程序运行期间未正确释放,会导致内存泄漏。
  • 单例实例是静态的,它的销毁通常需要在程序结束时通过显式的代码来完成。如果忘记编写这样的销毁代码,持有的资源不会释放。

这里提供一个会造成内存泄漏的例子

class Singleton {
private:static Singleton* instance;// 假设单例持有动态分配的内存int* data;Singleton() {data = new int[100]; // 动态分配内存}public:static Singleton* getInstance() {if (instance == nullptr) {instance = new Singleton();}return instance;}// 公有析构函数,虽然是公有的,但由于是私有的构造函数,外部无法创建实例~Singleton() {delete[] data; // 释放动态分配的内存}
};Singleton* Singleton::instance = nullptr;int main() {// 使用单例Singleton* singleton = Singleton::getInstance();// 单例实例持有的动态分配的内存没有释放return 0;
}

Singleton持有一个指向动态分配数组的指针data,由于没有提供销毁单例实例的函数,当程序结束时,data指向的内存未被释放,从而导致内存泄漏。

针对这一问题,有以下两种解决方案:

1.使用智能指针

2.使用静态的嵌套类对象

智能指针

上述代码可以改为:

#include <memory> // 包含智能指针的头文件class Singleton {
private:static std::unique_ptr<Singleton> instance; // 使用智能指针管理单例实例int* data;// 私有构造函数Singleton() {data = new int[100]; // 动态分配内存}// 禁止拷贝构造函数和赋值操作符Singleton(const Singleton&) = delete;Singleton& operator=(const Singleton&) = delete;public:// 静态公有方法,提供全局访问点static Singleton* getInstance() {if (instance.get() == nullptr) {instance.reset(new Singleton()); // 使用智能指针管理单例实例}return instance.get();}// 公有析构函数,虽然是公有的,但由于是私有的构造函数,外部无法创建实例~Singleton() {delete[] data; // 释放动态分配的内存}
};// 初始化静态成员变量
std::unique_ptr<Singleton> Singleton::instance = nullptr;int main() {// 使用单例Singleton* singleton = Singleton::getInstance();// 程序结束时,智能指针会自动释放单例实例,包括其持有的动态分配的内存return 0;
}

嵌套类

class Singleton {
private:static Singleton* instance; // 静态指针,用于存储单例的唯一实例int* data; // 指向动态分配内存的指针// 私有构造函数,只能内部调用Singleton() {data = new int[100]; // 动态分配内存}// 私有析构函数,只能内部调用~Singleton() {delete[] data; // 释放动态分配的内存,防止内存泄漏}// 内部类,用于在程序结束时自动销毁单例实例class Deletor {public:// 析构函数,在程序结束时调用~Deletor() {if (Singleton::instance != nullptr)delete Singleton::instance; // 如果单例实例存在,则删除它}};// 静态成员变量,用于确保Deletor的析构函数在程序结束时被调用static Deletor deletor;public:// 静态公有方法,提供全局访问点,用于获取单例实例static Singleton* getInstance() {if (instance == nullptr) { // 如果单例实例尚未创建instance = new Singleton(); }return instance;}
};// 初始化静态成员变量
Singleton* Singleton::instance = nullptr; // 初始化为nullptr,表示单例实例尚未创建
Singleton::Deletor Singleton::deletor; // 创建Deletor实例,以便在程序结束时调用其析构函数// 主函数
int main() {Singleton* singleton = Singleton::getInstance(); // 获取单例实例// 使用单例实例...return 0; // 程序结束,Deletor的析构函数将被调用,自动销毁单例实例
}

        在程序运行结束时,系统会调用静态成员deletor的析构函数,该析构函数会删除单例的唯一实例。用这种方法释放单例对象。

  • 在单例类内部定义专有的嵌套类。
  • 在单例类内定义私有的专门用于释放的静态成员
  • 利用程序在结束时析构全局变量的特性,选择最终的释放时机。

对于内存泄漏的检测,考虑使用valgrind

多线程下的单例模式

这些代码在单线程下环境是正确的,但是当处于多线程环境时,会发生竞争。最简单的解决竞争的方式是加入锁机制来保护临界区。

于是就有了最简单的多线程环境下的单例模式代码:

#include <iostream>class Singleton{
private://私有构造函数Singleton(){}//静态私有实例变量static Singleton* instance;mutex mutex;
public://禁止拷贝构造函数和赋值操作Singleton(const Singleton&) = delete;Singleton& operator=(const Singleton&) = delete;//公有静态方法,提取全局访问点static Singleton* getInstance(){//判断是否第一次调用//上锁    mutex.lock();if(instance = nullptr){instance = new Singleton();}//解锁mutex.unlock();return instance;}//实例方法void dosomething(){std::cout<<"Hello world"<<std:;endl;}
}// 初始化静态私有实例变量
Singleton * Singleton::instance = nullptr;int main(){//获取单例实例Singleton* singleton = Singleton::getInstance();//使用单例实例singleton->dosomething();
}

Double Checked Locking(DCL)

考虑到线程安全仅存在于第一次初始化(new)过程中,而在后续获取该实例时并不会遇到,也就没有必要再使用lock。使用双重检查锁定(Double-Checked Locking)来确保单例实例的线程安全,主要目的时减少加锁的次数。在细节上:

  1. 第一次检查:在getinstance方法中,首先检查instance是否为nullptr,若是,则继续执行、
  2. 加锁:使用std::lock_guard来确保mutex在作用域内被正确地加锁和解锁。这种方法被称为作用域锁因为它在lock_guard对象的作用域结束时自动解锁
  3. 第二次检查:在加锁前,再次检查instance是否为nullptr,若仍为nullptr,则加锁并创建单例实例。
  4. 创建实例:在加锁后,如果instance仍为nullptr,则创建实例。
  5. 返回实例:无论是否创建了新实例,最终都返回instance指针。
static Singleton* getInstance() {if (instance == nullptr) {std::lock_guard<std::mutex> lock(mutex); // 作用域锁if (instance == nullptr) {instance = new Singleton(); // 创建单例实例}}return instance;
}

“加入DCL后,其实还是有问题的,关于memory model。在某些内存模型中(虽然不常见)或者是由于编译器的优化以及运行时优化等等原因,使得instance虽然已经不是nullptr但是其所指对象还没有完成构造,这种情况下,另一个线程如果调用getInstance()就有可能使用到一个不完全初始化的对象。换句话说,就是代码中第2行:if(instance == NULL)和第六行instance = new Singleton();没有正确的同步,在某种情况下会出现new返回了地址赋值给instance变量而Singleton此时还没有构造完全,当另一个线程随后运行到第2行时将不会进入if从而返回了不完全的实例对象给用户使用,造成了严重的错误。在C++11没有出来的时候,只能靠插入两个memory barrier(内存屏障)来解决这个错误,但是C++11引进了memory model,提供了Atomic实现内存的同步访问,即不同线程总是获取对象修改前或修改后的值,无法在对象修改期间获得该对象。”

C++ 单例模式 - 知乎 (zhihu.com)

C++11规定了local static在多线程条件下的初始化行为,要求编译器保证了内部静态变量的线程安全性。在C++11标准下,《Effective C++》提出了一种更优雅的单例模式实现,使用函数内的 local static 对象。这样,只有当第一次访问getInstance()方法时才创建实例。这种方法也被称为Meyers' Singleton。C++0x之后该实现是线程安全的,C++0x之前仍需加锁。

Meyers’ Singleton(也称为Singleton模式或懒汉式Singleton模式)是一种单例模式实现,它确保一个类只有一个实例,并提供一个全局访问点来获取这个实例。与传统的懒汉式单例模式不同,Meyers’ Singleton在C++中通过使用内部静态类来实现,这是一种在C++11之前常用的方法,因为它能够保证线程安全,同时避免在全局范围内声明静态对象。

Meyers’ Singleton的关键点在于它使用了内部静态类来延迟实例的创建,直到首次使用时才创建实例。内部静态类在类定义时创建,因此它不会被全局实例化,从而避免了全局初始化的开销。

class Singleton {
public:static Singleton& getInstance() {// 内部静态类,确保在类定义时创建,而不是在全局范围内实例化static Singleton instance;return instance;}private:// 私有构造函数,防止外部通过new创建对象实例Singleton() {};// 私有拷贝构造函数,防止外部复制对象实例Singleton(const Singleton&) = delete;// 私有拷贝赋值运算符,防止外部复制对象实例Singleton& operator=(const Singleton&) = delete;
};

getInstance方法返回一个内部静态类Singleton的实例。由于Singleton是一个内部静态类,它不会在全局范围内实例化,而是在类定义时实例化。这意味着只有在首次调用getInstance方法时,才会创建单例实例。

Meyers’ Singleton模式提供了一种线程安全的懒汉式单例实现,它避免了全局初始化的开销,并且通过内部静态类保证了线程安全。在C++11之后,可以使用std::call_oncestd::once_flag来更简洁地实现单例模式。

饿汉式单例模式

单例实例在程序运行时被立即执行初始化,它确保一个类只有一个实例,并且在类加载时立即创建这个实例。饿汉式单例模式的特点是实例在类加载时就创建,因此不需要在第一次使用时创建,这使得饿汉式单例模式在类加载时就会占用一定的内存。

饿汉式单例模式的优点是简单且线程安全,因为实例在类加载时就已经创建,所以不会出现线程竞争的问题。然而,它的缺点是资源利用率不高,因为实例在类加载时就创建了,即使你从未使用过这个单例。

class Singleton {
private:// 私有构造函数,防止外部通过new创建对象实例Singleton() {}// 私有拷贝构造函数,防止外部复制对象实例Singleton(const Singleton&) = delete;// 私有拷贝赋值运算符,防止外部复制对象实例Singleton& operator=(const Singleton&) = delete;public:// 静态成员变量,在程序启动时创建单例实例static const Singleton& getInstance() {return instance;}private:// 静态成员变量,类加载时初始化static const Singleton instance;
};// 在类外初始化静态成员变量
const Singleton Singleton::instance;
  1. 静态常量成员变量instance 是一个静态常量成员变量,它在程序启动时创建,并且在整个程序运行期间都存在。

  2. 构造函数和析构函数:由于 instance 是一个常量,因此必须在类外部进行初始化,并且只能通过常量表达式进行初始化。

  3. 线程安全:饿汉式单例模式在程序启动时就创建了单例实例,因此它本身就是线程安全的,不需要担心多线程下的初始化问题。

  4. 常量引用返回getInstance 方法返回的是对单例实例的常量引用,这防止了对单例实例的修改。

  5. 不需要显式析构函数:由于 instance 是静态常量成员,它的生命周期由系统管理,不需要显式析构。

参考文章:

C++程序员们,快来写最简洁的单例模式吧 - 老司机 - 博客园 (cnblogs.com)

C++中的单例模式 | 神奕的博客 (songlee24.github.io)


http://www.mrgr.cn/news/47149.html

相关文章:

  • HOW - Form 表单确认校验两种模式(以 Modal 场景为例)
  • 杭州铭师堂的云原生升级实践
  • 非PHP开源内容管理系统(CMS)一览
  • 解读若依微服务架构图:架构总览、核心模块解析、消息与任务处理、数据存储与缓存、监控与日志
  • AAAI2020 | Ghost | 通过幽灵网络学习可迁移的对抗样本
  • 【JavaEE进阶】获取Cookie/Session
  • 2015年国赛高教杯数学建模D题众筹筑屋规划方案设计解题全过程文档及程序
  • DAY5 数组
  • ABC340
  • 人像抠图怎么好看?1分钟教会你
  • 【高等代数笔记】线性空间(十九-二十四上半部分)
  • LangChain: AI大语言模型的新篇章
  • 2.1 App测试与发布指南
  • 剧本杀小程序:提升玩家游戏体验,带动经济效益
  • Meta推出的AI视频音频生成模型:Movie Gen
  • 数据排列组合实现
  • CentOS系统解压缩.7z后缀的文件
  • jenkins中的allure和email问题梳理
  • java通知提醒实现使用`java.util.Timer`或`ScheduledExecutorService`进行定时提醒
  • Unicode
  • 12.JVM类加载机制
  • 2024年诺贝尔物理学奖2
  • 怎么高效对接SaaS平台数据?
  • ITSS-IT服务工程师和ITSS-IT服务经理的区别
  • GEE 错误:Can‘t transform (11121.0,18905.0),Can‘t transform (-1.0,-1.0)
  • C#中ref关键字和out关键字