C++单例模式

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

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

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

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

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

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

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

cpp 复制代码
#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),单例在第一次使用才会被初始化,或称为延迟初始化,也正因为仅在第一次使用时创建单例实例,能够节省资源,但在某些情况下,可能会导致内存泄漏。举例如下:

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

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

cpp 复制代码
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.使用静态的嵌套类对象

智能指针

上述代码可以改为:

cpp 复制代码
#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;
}

嵌套类

cpp 复制代码
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

多线程下的单例模式

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

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

cpp 复制代码
#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指针。
cpp 复制代码
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的关键点在于它使用了内部静态类来延迟实例的创建,直到首次使用时才创建实例。内部静态类在类定义时创建,因此它不会被全局实例化,从而避免了全局初始化的开销。

cpp 复制代码
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来更简洁地实现单例模式。

饿汉式单例模式

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

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

cpp 复制代码
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)

相关推荐
兵哥工控1 分钟前
MFC工控项目实例三十二模拟量校正值添加修改删除
c++·mfc
闲人一枚(学习中)3 分钟前
设计模式-创建型-原型模式
设计模式
长弓聊编程11 分钟前
Linux系统使用valgrind分析C++程序内存资源使用情况
linux·c++
cherub.19 分钟前
深入解析信号量:定义与环形队列生产消费模型剖析
linux·c++
暮色_年华33 分钟前
Modern Effective C++item 9:优先考虑别名声明而非typedef
c++
Iced_Sheep34 分钟前
干掉 if else 之策略模式
后端·设计模式
重生之我是数学王子41 分钟前
QT基础 编码问题 定时器 事件 绘图事件 keyPressEvent QT5.12.3环境 C++实现
开发语言·c++·qt
我们的五年1 小时前
【Linux课程学习】:进程程序替换,execl,execv,execlp,execvp,execve,execle,execvpe函数
linux·c++·学习
做人不要太理性2 小时前
【C++】深入哈希表核心:从改造到封装,解锁 unordered_set 与 unordered_map 的终极奥义!
c++·哈希算法·散列表·unordered_map·unordered_set
程序员-King.2 小时前
2、桥接模式
c++·桥接模式