首发于 C++随记
即学即记03-C++单例模式

即学即记03-C++单例模式

单例模式是一种较为简单的设计模式,可根据不同需求有不同的实现方法。但是单例模式是有局限性的,索引很多人会反对使用单例模式,本文一步一步循序渐进地实现简单的单例模式。

什么是单例模式

单例 Singleton 是设计模式的一种,要求内存中只能创建一个对象,具有全局变量的特点,在任何位置都可以通过接口获取到那个唯一实例;

应用场景:
- 1. 直接替换任意的全局对象(变量)
- 2. 配置文件
- 3. 词典类
- 4. 设备管理器,系统中可能有多个设备,但是只有一个设备管理器,用于管理设备驱动; - 5. 数据池,用来缓存数据的数据结构,需要在一处写,多处读取或者多处写,多处读取;

C++单例模式的实现

  • 全局只有一个实例: static 特性,同时禁止用户自己声明并定义实例(把构造函数设为 private
  • 线程安全
  • 禁止赋值和拷贝
  • 用户通过接口获取实例:使用 static 类成员函数

首先来看这段代码

#include <iostream>
#include <stdio.h>
using std::cout;
using std::endl;
//创建一个类
class Singleton {
int main() {
    Singleton s1;
    Singleton s2;  //可以创建多个对象
    return 0;

按照普通的方式创建一个类,这样就可以创建多个对象,所以让 main 中的 Singleton s1; 编译不通过。因为创建实例的时候会调用构造函数,所以我们可以从构造函数下手,将构造函数 设为私有

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

将构造函数私有化后下面的几种创建实例方式都会报错

Singleton s3;   //error
static Singleton s4;  //error
int main() {
    //设为私有后,下面的语句将会报错
    Singleton s1;
    Singleton s2;  
    Singleton * p1 = new Singleton();  //error
    return 0;

private 中的成员方法只能在类本身中调用,所以在中定义 getInstance() 方法,返回的是一个 Singleton 对象。

class Singleton {
public:
    static Singleton * getInstance() {
        return new Singleton();
private:
    Singleton() {
        cout << "构造函数: Singleton()  "<< endl;
int main() {
    Singleton *p1 = Singleton::getInstance();
    Singleton *p2 = Singleton::getInstance();
    //打印地址
    printf("&P1 = %p\n",p1);
    printf("&P1 = %p\n",p2);
    return 0;

用静态成员函数创建对象,通过打印地址发现,这两个对象地址不一样,说明不是同一对象, 所以需要用一个变量保存第一次创建之后的指针。但是这个变量 _pInstance 必须是 static 的,因为是在静态方法中使用。此外静态成员必须在类的外部进行初始化。

最基础版的单例模式(线程不安全)

使用懒汉模式实现:

懒汉式( Lazy-Initialization )的方法是直到使用时才实例化对象,也就说直到调用 getInstance() 方法的时候才 new 一个单例的对象。好处是如果未被调用就不会占用内存。
#include <iostream>
using namespace std;
class Singleton
public:
    static Singleton *getInstance() {
        if(NULL == _pInstance) {  //保证只创建一个
            _pInstance = new Singleton();
        return _pInstance;
private:
    Singleton() {
        cout << "构造函数:Singleton()" << endl;
    ~Singleton() {
        cout << "析构函数:~Singleton()"




    
 << endl;
    // 将其拷贝构造和赋值构造成为私有函数, 禁止外部拷贝和赋值
    Singleton(const Singleton &signal);
    const Singleton &operator=(const Singleton & signal);
    // 唯一单例对象指针
    static Singleton *_pInstance;
//静态成员必须初始化
Singleton * Singleton::_pInstance = nullptr;
int main() {
    Singleton *p1 = Singleton::getInstance();
    Singleton *p2 = Singleton::getInstance();
    cout << "&p1 = " << p1 << endl;
    cout << "&p2 = " << p2 << endl;
    return 0;

运行结果:

构造函数:Singleton()
&p1 = 0x7fd75b402b60
&p2 = 0x7fd75b402b60

这时打印 p1 p2 的地址,发现地址是一样的,说明构造函数只被调用了一次, p1 p2 是同一个对象。

上面最基础版本的单例模式是存在问题的:

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

对象的销毁,得到基础版的单例模式(懒汉模式)

我们可以直接通过 delete p1; 去销毁,但是我们是通过静态方法创建的对象,最好也是用静态的方法销毁,所以必须让这条语句编译不通过。由于对象在销毁的时候会调用析构函数,所以,我们可以把析构函数设置成私有的。然后在类内部定义一个 destory() 函数。

#include <iostream>
using namespace std;
class Singleton
public:
    static Singleton *getInstance() {
        if(NULL == _pInstance) {  //保证只创建一个,没有加锁,线程不安全,当线程并发时会创建多个实例
            _pInstance = new Singleton();
        return _pInstance;
    //销毁函数
    static void destory() {
        if(_pInstance) {
            delete _pInstance;  //会调用析构函数
private:
    Singleton() {
        cout << "构造函数:Singleton()" << endl;
    ~Singleton() {
        cout << "析构函数:~Singleton()" << endl;
    // 将其拷贝构造和赋值构造成为私有函数, 禁止外部拷贝和赋值
    Singleton(const Singleton &signal);
    const Singleton &operator=(const Singleton & signal);
    // 唯一单例对象指针
    static Singleton *_pInstance;
//静态成员必须初始化  懒汉模式
Singleton * Singleton::_pInstance = nullptr;
int main() {
    Singleton *p1 = Singleton::getInstance();
    Singleton *p2 = Singleton::getInstance();
    cout << "&p1 = " << p1 << endl;
    cout << "&p2 = " << p2 << endl;
    //手动释放
    Singleton::destory();
    return 0;

最终运行结果:

构造函数:Singleton()
&p1 = 0x7fe123c02b60
&p2 = 0x7fe123c02b60
析构函数:~Singleton()

单例模式的线程安全问题

什么是线程安全

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

如何保证线程安全

给共享的资源加把锁,保证每个资源变量每时每刻至多被一个线程占用。

回到上述基础版的单例模式:

  • 线程安全问题:当多线程获取单例的时候有可能引发竞争态现象,第一个条件在判断 if(NULL == _pInstance) 的时候就开始实例化单例,同时第 2 个线程也开始获取这个单例,这个时候 _pInstance 还是空的,于是也开始实例化单例,这样就会实例化出两个单例对象,这样就会出现线程安全问题了。解决方法: 加锁

此外,我们这里使用 共享指针 来解决内存泄漏问题。这里提供一个改进的,线程安全的、使用智能指针的实现;

线程安全、内存安全的懒汉式单例(智能指针,锁)

#include <iostream>
#include <memory> // shared_ptr
#include <mutex>  // mutex
using std::cout;
using std::endl;
class Singleton {
public:
    typedef std::shared_ptr<Singleton> Ptr;  //智能指针可以释放聂村
    ~Singleton() {
        cout << "析构函数" << endl;
    static Ptr getInstance() {
        if(_instance == nullptr) { //双检锁
            std::lock_guard<std::mutex> lk(_mutex);
            _instance = std::shared_ptr<Singleton>(new Singleton);
        return _instance;
private:
    //构造函数
    Singleton();
    //复制构造函数 和 赋值构造函数
    Singleton(const Singleton &sigle);
    const Singleton &operator=(const Singleton &sigle);
    static Ptr _instance;
    static std::mutex _mutex;
Singleton::Ptr Singleton::_instance = nullptr;
std::mutex Singleton::_mutex ;
Singleton::Singleton::Singleton() {
    cout << "构造函数" << endl;
int main() {
    Singleton::Ptr p1 = Singleton::getInstance();
    Singleton::Ptr p2 = Singleton::getInstance();
    cout << "&p1 = " << p1 << endl;
    cout << "&p2 = " << p2 << endl;
    return 0;

结果:

构造函数
&p1 = 0x7ff576c02b80
&p2 = 0x7ff576c02b80
析构函数

shared_ptr mutex 都是 C++11 的标准,以上这种方法的优点是:

  • 基于 shared_ptr , 用了 C++ 比较倡导的 RAII 思想,用对象管理资源,当 shared_ptr 析构的时候, new 出来的对象也会被 delete 掉。以此避免内存泄漏。
  • 加了锁,使用互斥量来达到线程安全。这里使用了两个 if 判断语句的技术称为 双检锁 ;好处是,只有判断指针为空的时候才加锁,避免每次调用 getInstance 的方法都加锁,锁的开销毕竟还是有点大的。

以上的实现方法也有不足之处:必须保证用户会使用智能指针,而且锁也有开销,此外代码量也不叫多。更重要的是,有些平台(与编译器和指令集架构有关), 双检锁会失效

双检锁失效原因有两个: - (1)编译器的写操作重排问题.

例 : B b = new B();

上面这一句并不是原子性的操作,一部分是 new 一个B对象,一部分是将 new 出来的对象赋值给 b .

直觉来说我们可能认为是先构造对象再赋值.但是很遗憾,这个顺序并不是固定的.再编译器的重排作用下,可能会出现先赋值再构造对象的情况.

  • 结合上下文,结合使用情景.

理解了 (1) 中的写操作重排以后,我卡住了一下.因为我真不知道这种重排到底会带来什么影响.实际上是因为我看代码看的不够仔细,没有意识到使用场景.双检锁的一种常见使用场景就是在单例模式下初始化一个单例并返回,然后调用初始化方法的方法体内使用初始化完成的单例对象。

举个例子说:

if(b == null){
    synchronized(b){
        if(b == null){
            b = new B();
return b;

两个线程:线程 1 和线程 2 .

最开始, b 满足为 null 的条件,线程 1 , 2 都进入了外层的 if 中,假设说线程 1 抢先进入到同步块中,然后对 b 先赋值(注意此时还没有构造).同一时刻,对于线程 2 而言,它已经不满足外层的 if 条件了,它就会跳出 双检锁 ,直接向下执行到 "return b" . 可是此时返回的 b 是一个并没有被构造的对象,如果在调用方法中使用这个 b 肯定会出现比如说 NullPointer 这样的问题。
所以说问题的核心还是编译器写操作

因此这里还有第三种的基于 Magic Staic ( 局部静态变量 )的方法达到线程安全

最推荐的懒汉式单例(magic static )

#include <iostream>
using namespace std;
class Singleton
public:
    static Singleton& get_instance(){
        static Singleton instance;
        return instance;
private:
    Singleton(){
        std::cout<<"构造函数"<<std::endl;
    ~Singleton(){
        std::cout<<"析构函数"<<std::endl;
int main(int argc, char *argv[])
    Singleton& p1 = Singleton::get_instance();
    Singleton& p2 = Singleton::get_instance();
    cout << "&p1 = " << &p1 << endl;
    cout << "&p2 = " << &p2 << endl;