【C++设计模式 -- 单例(Singleton)模式】

单例模式

什么是单例模式

单例模式是一种设计模式,它确保一个类只有一个实例,并提供一个全局访问点,使得程序的各个部分可以共享这个实例。单例模式的主要目的是限制一个类的实例化过程,确保在运行时只能存在一个实例,并提供一种全局访问方式。
在单例模式中,通常会将类的构造函数声明为私有的,这样外部代码无法直接实例化该类。然后,通过静态成员函数或静态成员变量来控制实例的创建和访问。如果实例不存在,就创建一个新的实例;如果实例已经存在,就返回已有的实例。

单例模式的特点

1.私有构造函数: 单例类的构造函数通常声明为私有,以防止外部直接创建实例。
2.静态成员变量或静态成员函数: 通过静态成员变量保存唯一的实例,或者通过静态成员函数提供访问实例的方法。
3.全局访问点: 单例模式提供了一个全局的访问点,允许程序的其他部分通过特定的接口访问单例实例。

为什么要使用单例模式

全局访问点: 单例模式提供了一个全局的访问点,使得程序的各个部分可以轻松地访问到相同的实例。这有助于统一管理资源或状态。
资源共享: 当一个类的实例需要被多个部分共享时,使用单例模式可以避免重复创建实例,节省系统资源。
配置管理: 单例模式常用于管理系统的配置信息,确保在整个程序运行期间只有一个配置实例,并可以被所有模块访问。
线程池、连接池等管理对象的情况: 在需要限制系统中某些资源的数量时,可以使用单例模式来管理这些资源的实例。
日志记录: 单例模式可以用于记录系统中的日志信息,确保只有一个日志实例,并提供全局的访问点,方便记录系统的运行状态。
避免重复创建开销大的对象: 当对象的创建和销毁具有较大的开销时,使用单例模式可以减少创建次数,提高性能。
虽然单例模式有其用途,但需要慎重使用,因为它可能导致全局状态的存在,使得代码的复杂性增加。在一些情况下,依赖注入、工厂模式等也是可以考虑的替代方案。

单例模式的缺点

尽管单例模式在某些情况下是有用的,但它也有一些缺点,需要在使用时考虑:
全局状态: 单例模式引入了全局状态,这可能导致代码的复杂性增加。因为任何部分都可以访问单例实例,可能导致难以追踪和理解的依赖关系。
隐藏依赖关系: 使用单例模式可能隐藏了类之间的依赖关系,因为单例实例可以在任何地方被访问。这使得代码的耦合度增加,降低了灵活性。
测试困难: 单例模式可能会使代码难以测试。因为单例实例是全局可访问的,难以在测试中用模拟对象替代实际的单例实例。
违反单一职责原则: 单例对象通常负责自己的创建、管理和业务逻辑,可能导致违反单一职责原则。这使得代码难以维护和扩展。
可能引发并发问题: 在多线程环境中,懒汉式的单例模式可能引发竞态条件问题,需要特殊处理以确保线程安全。
阻碍扩展: 单例模式的实现方式可能会阻碍程序的扩展。例如,如果需要从单例模式切换到多例模式或其他设计模式,可能需要修改大量代码。

单例模式实现

懒汉式(Lazy Initialization)方式(不安全)

实现一个简单的懒汉式单例模式。构造函数被声明为私有,确保外部无法直接创建实例。通过静态成员变量 instance 来保存唯一的实例,在 getInstance 方法中进行懒汉式的创建。示例代码如下:

#include <iostream>

class Singleton {
    
    
public:
    // 获取单例实例的静态方法
    static Singleton& getInstance() {
    
    
        // 使用懒汉式(Lazy Initialization)方式创建单例对象
        // 如果实例不存在,则创建一个新的实例,否则返回现有实例
        if (instance == nullptr) {
    
    
            instance = new Singleton();
        }
        return *instance;
    }

    // 其他成员函数

private:
    // 将构造函数、拷贝构造函数和赋值运算符声明为私有,防止外部创建多个实例
    Singleton() {
    
    
        // 构造函数私有,防止外部创建实例
        std::cout << "Singleton instance created." << std::endl;
    }

    Singleton(const Singleton&) = delete; // 禁用拷贝构造函数
    Singleton& operator=(const Singleton&) = delete; // 禁用赋值运算符

    // 静态成员变量,用于保存唯一实例
    static Singleton* instance;
};

// 静态成员变量初始化为nullptr
Singleton* Singleton::instance = nullptr;

int main() {
    
    
    // 获取单例实例
    Singleton& singleton1 = Singleton::getInstance();
    Singleton& singleton2 = Singleton::getInstance();

    // 输出地址,确保两次获取的是同一个实例
    std::cout << "Singleton 1 address: " << &singleton1 << std::endl;
    std::cout << "Singleton 2 address: " << &singleton2 << std::endl;

    return 0;
}

需要注意的是,懒汉式的单例模式在多线程环境中可能会存在竞态条件问题。因为多个线程可能同时检测到实例为nullptr,然后都尝试创建一个新的实例。为了确保线程安全,可以使用不同的线程安全机制来修复这个问题。

双重检查锁(Double-Checked Locking)(线程安全)

为了保证线程安全,可以使用std::mutex来确保在创建实例时只有一个线程能够进入临界区,从而避免了竞态条件问题。这种方式称为双重检查锁(Double-Checked Locking)机制,它在实现上相对高效,但也需要小心使用,确保在C++11及以上标准下才能正确工作。示例代码如下:

#include <iostream>
#include <mutex>

class Singleton {
    
    
public:
    // 获取单例实例的静态方法
    static Singleton& getInstance() {
    
    
        // 使用双重检查锁(Double-Checked Locking)确保线程安全
        if (instance == nullptr) {
    
    
            std::lock_guard<std::mutex> lock(mutex);  // 使用互斥锁保护实例的创建过程
            if (instance == nullptr) {
    
    
                instance = new Singleton();
            }
        }
        return *instance;
    }

    // 其他成员函数

private:
    Singleton() {
    
    
        std::cout << "Singleton instance created." << std::endl;
    }

    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

    // 静态成员变量,用于保存唯一实例
    static Singleton* instance;
    static std::mutex mutex;  // 互斥锁,确保线程安全
};

Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mutex;

int main() {
    
    
    // 获取单例实例
    Singleton& singleton1 = Singleton::getInstance();
    Singleton& singleton2 = Singleton::getInstance();

    // 输出地址,确保两次获取的是同一个实例
    std::cout << "Singleton 1 address: " << &singleton1 << std::endl;
    std::cout << "Singleton 2 address: " << &singleton2 << std::endl;

    return 0;
}

局部静态变量(线程安全)

除了使用双重检查锁机制,还有其他一些线程安全的单例模式实现方式。以下是其中的一种,使用局部静态变量,示例代码如下:

#include <iostream>

class Singleton {
    
    
public:
    // 获取单例实例的静态方法
    static Singleton& getInstance() {
    
    
        static Singleton instance;  // 局部静态变量,确保线程安全
        return instance;
    }

    // 其他成员函数

private:
    Singleton() {
    
    
        std::cout << "Singleton instance created." << std::endl;
    }

    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
};

int main() {
    
    
    // 获取单例实例
    Singleton& singleton1 = Singleton::getInstance();
    Singleton& singleton2 = Singleton::getInstance();

    // 输出地址,确保两次获取的是同一个实例
    std::cout << "Singleton 1 address: " << &singleton1 << std::endl;
    std::cout << "Singleton 2 address: " << &singleton2 << std::endl;

    return 0;
}

在这个实现中,将实例声明为static的局部变量,这样它只会在第一次调用getInstance时被初始化。C++11及以上标准保证了局部静态变量的线程安全性,因为初始化过程在多线程环境中是互斥的。这种方式的优点是简单且线程安全,不需要显式使用互斥锁。
需要注意的是,这种方式的局限性在于不能在运行时控制单例的创建时机,因为它是在第一次调用getInstance时自动创建的。如果需要在程序启动时就创建单例,或者需要延迟到程序的某个具体时刻再创建,就需要考虑其他实现方式。

通过std::call_once创建(C++11 线程安全)

#include <iostream>

class Singleton {
    
    
public:
    // 获取单例实例的静态方法
    static Singleton& getInstance() {
    
    
        std::call_once(initFlag, &Singleton::initInstance);
        return *instance;
    }

    // 其他成员函数

private:
    Singleton() {
    
    
        std::cout << "Singleton instance created." << std::endl;
    }

    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

    // 初始化单例实例的函数
    static void initInstance() {
    
    
        instance = new Singleton();
    }

    // 静态成员变量,用于保存唯一实例
    static Singleton* instance;
    static std::once_flag initFlag;  // 标记是否已经初始化过
};

Singleton* Singleton::instance = nullptr;
std::once_flag Singleton::initFlag;

int main() {
    
    
    // 获取单例实例
    Singleton& singleton1 = Singleton::getInstance();
    Singleton& singleton2 = Singleton::getInstance();

    // 输出地址,确保两次获取的是同一个实例
    std::cout << "Singleton 1 address: " << &singleton1 << std::endl;
    std::cout << "Singleton 2 address: " << &singleton2 << std::endl;

    return 0;
}

在上面的代码中,std::once_flag 用于标记初始化是否已经完成,std::call_once 保证 initInstance 函数只会在第一次调用 getInstance 时执行一次。这样可以确保线程安全地创建单例实例,同时避免了手动管理锁所带来的复杂性。

猜你喜欢

转载自blog.csdn.net/a1379292747/article/details/135264703