从第一性原理理解单例模式

我们想要一个类,在整个程序运行期间,它只能被创建出一个对象实例。

为什么需要?比如:配置管理器、日志输出器、线程池------如果出现多个实例,可能导致数据冲突、资源浪费,或者违反业务逻辑(比如一个进程只能有一个打印机控制对象)。

那么问题来了:C++ 中,怎样才能强行限制一个类只能有一个实例?


第一步:禁止外部随意创建对象

正常情况下,你可以写:

cpp 复制代码
class MyClass {
public:
    MyClass() {}   // 公有构造函数
};
MyClass a;        // 可以创建
MyClass b;        // 又可以创建

要阻止这种情况,必须让外界无法调用构造函数 。把构造函数声明为 private 即可:

cpp 复制代码
class Singleton {
private:
    Singleton() {}   // 私有构造,外部不能 new
};

现在外部写 Singleton s; 会编译错误。

但光这样还不够,这个类连一个对象都造不出来了,我们要的只是唯一一个 ,不是零个。所以需要提供一个公开的接口来获取那唯一的一个实例。


第二步:提供一个全局访问点

我们需要一个静态成员函数 ,用它来返回唯一的实例。同时需要保存这个唯一实例的指针(或对象)。

保存的地方也只能是类的静态成员,因为只有静态成员属于类本身,而非某个对象。

cpp 复制代码
class Singleton {
private:
    Singleton() {}
    static Singleton* instance_;   // 静态指针,存放唯一实例的地址
public:
    static Singleton* getInstance() {
        if (instance_ == nullptr) {
            instance_ = new Singleton();
        }
        return instance_;
    }
};

// 静态成员在类外初始化
Singleton* Singleton::instance_ = nullptr;

用法:

cpp 复制代码
Singleton* p1 = Singleton::getInstance();
Singleton* p2 = Singleton::getInstance();
// p1 和 p2 指向同一个对象

此时我们实现了"懒汉式"单例:第一次调用 getInstance() 时才创建实例。


第三步:杜绝拷贝和赋值

即使构造函数私有,外部仍然可能通过拷贝构造或赋值来制造"看起来像第二个实例"的对象:

cpp 复制代码
Singleton* p1 = Singleton::getInstance();
Singleton s2 = *p1;   // 拷贝构造(如果编译器默认生成公有拷贝构造)

为了防止这种情况,需要禁止拷贝构造和赋值操作 。C++98 经典做法是把它们声明为私有且不定义;C++11 及以后可以用 = delete

cpp 复制代码
class Singleton {
private:
    Singleton() {}
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
    // ...
};

现在任何企图拷贝或赋值的代码都会编译报错,保证真的只有一个实例。


第四步:线程安全(多线程环境)

上面的懒汉式 if (instance_ == nullptr) 在多线程下不安全:两个线程同时进入判断,都可能发现指针为空,然后各自 new,破坏单例。

加锁可以解决,但 C++11 之后有一个更优雅、零开销的线程安全单例写法 ------ Meyers Singleton ,它利用函数内静态局部变量的特性。

C++11 标准保证:函数内的静态局部变量在第一次控制流经过其声明时初始化,且初始化是线程安全的

cpp 复制代码
class Singleton {
private:
    Singleton() {}
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

public:
    static Singleton& getInstance() {
        static Singleton instance;   // 第一次调用时才构造,并且线程安全
        return instance;
    }
};

用法:

cpp 复制代码
Singleton& s = Singleton::getInstance();

这里返回的是引用而非指针,更加简洁,也不需要手动 delete。在程序结束时,静态局部变量会自动销毁,生命周期管理由编译器负责。


完整代码示例

cpp 复制代码
#include <iostream>

class Singleton {
private:
    Singleton() { 
        std::cout << "Singleton created" << std::endl; 
    }
    ~Singleton() { 
        std::cout << "Singleton destroyed" << std::endl; 
    }

public:
    // 禁用拷贝和移动
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
    Singleton(Singleton&&) = delete;
    Singleton& operator=(Singleton&&) = delete;

    static Singleton& getInstance() {
        static Singleton instance;
        return instance;
    }

    void doSomething() const {
        std::cout << "Doing something..." << std::endl;
    }
};

int main() {
    Singleton& s1 = Singleton::getInstance();
    Singleton& s2 = Singleton::getInstance();
    // s1 和 s2 引用的是同一个对象
    s1.doSomething();
    return 0;
}

输出:

cpp 复制代码
Singleton created
Doing something...
Singleton destroyed
需求 对应的技术措施
只有一个实例 私有构造函数,删除拷贝/赋值操作
提供全局访问点 静态成员函数 getInstance
控制实例创建时机 懒汉式(首次使用时创建)或饿汉式(预先创建)
多线程环境下保证唯一性 C++11 静态局部变量(Meyers Singleton)
自动销毁实例 静态局部变量的 RAII 特性
相关推荐
重生之我是Java开发战士21 小时前
【Java SE】多线程(三):单例模式,阻塞队列,线程池与定时器
java·javascript·单例模式
许彰午2 天前
34_Java设计模式之单例模式
java·单例模式·设计模式
罗超驿4 天前
10.Java单例模式全解析:饿汉式与懒汉式实现及线程安全深度剖析
安全·单例模式·javaee
布朗克1684 天前
33 设计模式精讲
java·单例模式·设计模式
雨浓YN4 天前
基于设计模式的Winform软件框架-01Xml\Log\Ini日志(单例模式+生产者消费者模式)
单例模式·设计模式
仙俊红5 天前
Java 单例模式:类里面为什么可以有自己类型的字段?
java·开发语言·单例模式
swordbob5 天前
prototype 注入到 singleton 里,prototype是否还是线程安全的
安全·spring·单例模式·原型模式
谁似人间西林客7 天前
工业大数据实战:看中国智造如何用数据驱动效率革命
大数据·单例模式
张小姐的猫7 天前
【Linux】多线程 —— 线程池 | 单例模式 | 常见锁
linux·运维·服务器·c++·单例模式·设计模式·策略模式
Java面试题总结8 天前
双重检验锁的单例模式在高并发下的可见性问题
单例模式