C++ 单例模式:饿汉模式与懒汉模式

一、什么是单例模式?

单例模式是一种设计模式,确保一个类在整个程序中只有一个实例对象,并提供一个全局访问点来获取这个实例。

通俗理解

  • 普通类:可以创建多个对象,new A() 每次都是不同的对象

  • 单例类:只能创建一个对象,无论调用多少次获取方法,返回的都是同一个对象

适用场景

  • 日志系统(整个程序只有一个日志输出对象)

  • 数据库连接池(全局共享一组连接)

  • 配置管理器(读取一次配置,全局使用)

  • 线程池(全局只有一个线程池)

cpp 复制代码
// 普通类:可以创建多个对象
A a1, a2;  // a1 和 a2 是不同的对象

// 单例类:只能创建一个对象
A& a1 = A::getInstance();
A& a2 = A::getInstance();
// a1 和 a2 指向同一个对象

单例模式确保一个类在整个程序中只有一个实例对象

核心理解 :单例对象和普通类对象不同,它必须是静态的 ,因为只有静态才能保证全局唯一。有两种方式可以创建这个唯一的对象:饿汉模式懒汉模式

二、如何禁止外部创建对象?

要实现单例模式,首先需要禁止外部直接创建对象。

1. 构造函数私有化

cpp 复制代码
class MyObject {
private:
    int value;
    
    // 构造函数私有化,外部无法创建对象
    MyObject(int x = 0) : value(x) {
        cout << "Create MyObject:" << this << endl;
    }
};

说明 :构造函数私有化后,外部代码 MyObject obj(10); 会编译报错,无法直接创建对象。这是单例模式的第一步。

2. 禁止拷贝(C++11 方式)

cpp 复制代码
class MyObject {
private:
    MyObject(const MyObject&) = delete;        // 删除拷贝构造函数
    MyObject operator=(const MyObject&) = delete;  // 删除赋值运算符
};

为什么需要禁止拷贝?

即使构造函数是私有的,仍然可以通过拷贝构造创建新对象:

cpp 复制代码
MyObject& obj1 = MyObject::getInstance();  // 获取单例
MyObject obj2 = obj1;                      // 拷贝构造会创建新对象,违反单例原则

说明 :使用 = delete 可以让编译器禁止拷贝,从根源上杜绝。

3. 析构函数必须是公有的

cpp 复制代码
public:
    ~MyObject() { cout << "Destroy MyObject:" << this << endl; }

说明:析构函数必须是公有的,否则外部无法销毁对象。单例对象通常在程序结束时自动销毁,析构函数需要能被外部调用。

三、饿汉模式

饿汉模式在程序启动时就创建好单例对象,无论是否使用。

1. 实现方式

cpp 复制代码
class MyObject {
private:
    int value;
    static MyObject objx;   // 静态对象
    
    MyObject(int x = 0) : value(x) {
        cout << "Create MyObject:" << this << endl;
    }
    
    MyObject(const MyObject&) = delete;
    MyObject operator=(const MyObject&) = delete;
    
public:
    ~MyObject() { cout << "Destroy MyObject:" << this << endl; }
    
    static MyObject& getObject() {
        return objx;   // 直接返回静态对象
    }
};

// 静态成员在类外初始化
MyObject MyObject::objx(10);

说明

  • static MyObject objx; 在类内声明静态对象

  • MyObject MyObject::objx(10); 在类外定义并初始化,调用私有构造函数,传入参数 10

  • 这个 objx 就是整个程序中唯一的 MyObject 对象,其 value 被初始化为 10

  • 无论通过 getObject() 获取多少次,返回的都是同一个对象

cpp 复制代码
MyObject& obj1 = MyObject::getObject();
MyObject& obj2 = MyObject::getObject();
// obj1 和 obj2 指向同一个对象,value 都是 10

为什么类外可以调用私有构造函数?

MyObject::objx 这个写法表示 objxMyObject 类的成员。既然是类的成员,就可以访问类的私有成员 ,包括私有构造函数。类的作用域 MyObject:: 内的代码,可以访问该类的私有成员。这不是"外部调用",而是"类内部定义的延续"。

外部代码直接 MyObject obj(10); 才是真正的外部调用,会报错。

注意:饿汉模式下,初始化参数是固定的。如果需要用不同参数初始化,就要用懒汉模式。

2. 使用示例

cpp 复制代码
int main()
{
    MyObject& obj1 = MyObject::getObject();
    MyObject& obj2 = MyObject::getObject();
    
    cout << "obj1地址:" << &obj1 << endl;
    cout << "obj2地址:" << &obj2 << endl;
    
    obj1.Print();
    obj2.Print();
    
    return 0;
}

说明obj1obj2 指向同一个地址,说明获取的是同一个对象。无论调用多少次 getObject(),返回的都是唯一的单例对象。

四、懒汉模式(基础版)

懒汉模式在第一次调用时才创建单例对象,按需创建,节省资源。

1. 实现方式

cpp 复制代码
class MyObject {
private:
    int value;
    static MyObject* pobj;   // 静态指针,初始为 nullptr
    
    MyObject(int x = 0) : value(x) {   // 私有构造函数
        cout << "Create MyObject:" << this << endl;
    }
    
    MyObject(const MyObject&) = delete;           // 禁止拷贝构造
    MyObject operator=(const MyObject&) = delete; // 禁止赋值
    
public:
    ~MyObject() { cout << "Destroy MyObject:" << this << endl; }
    
    static MyObject& getObject(int x) {
        if (pobj == nullptr) {               // 第一次调用时,指针为空
            pobj = new MyObject(x);          // 创建对象
        }
        return *pobj;                        // 返回对象(第一次创建,后续直接返回)
    }
};

MyObject* MyObject::pobj = nullptr;  // 静态成员类外初始化

说明

  • static MyObject* pobj 是指针,初始为 nullptr

  • 第一次调用 getObject() 时,pobj == nullptr,创建对象

  • 后续调用时,pobj 已不为空,直接返回已有对象

2. 线程安全问题

基础版懒汉模式在单线程 环境下没问题,但在多线程环境下存在安全隐患:

cpp 复制代码
// 线程 A 和线程 B 同时调用 getObject
if (pobj == nullptr) {    // 两个线程都可能看到 pobj == nullptr
    pobj = new MyObject(x); // 可能创建两个对象!
}

问题 :两个线程可能同时发现 pobj == nullptr,然后各自创建一个对象,违反单例原则。

结果 :内存中可能存在多个 MyObject 对象,不再是单例。

五、懒汉模式(加锁版)

为了解决线程安全问题,加互斥锁保证同一时间只有一个线程能进入临界区。

cpp 复制代码
#include <mutex>

class MyObject {
private:
    int value;
    static MyObject* pobj;           // 静态指针,初始为 nullptr
    static std::mutex m_mutex;       // 互斥锁,保证线程安全
    
    MyObject(int x = 0) : value(x) { // 私有构造函数
        cout << "Create MyObject:" << this << endl;
    }
    
    MyObject(const MyObject&) = delete;
    MyObject operator=(const MyObject&) = delete;
    
public:
    ~MyObject() { cout << "Destroy MyObject:" << this << endl; }
    
    static MyObject& getObject(int x) {
        m_mutex.lock();              // 加锁:同一时间只有一个线程能进入
        if (pobj == nullptr) {
            pobj = new MyObject(x);  // 第一次调用时创建对象
        }
        m_mutex.unlock();            // 解锁:其他线程可以进入
        return *pobj;
    }
};

MyObject* MyObject::pobj = nullptr;      // 静态成员类外初始化
std::mutex MyObject::m_mutex;            // 互斥锁类外初始化

问题 :每次调用 getObject() 都会加锁和解锁,即使对象已经创建(pobj != nullptr)也不例外。加锁操作本身有开销,在高并发场景下会影响性能。

示意图

调用次数 对象状态 是否加锁 开销
第1次 未创建 创建对象 + 锁开销
第2次 已创建 锁开销(多余)
第3次 已创建 锁开销(多余)

结论:对象创建后,锁就没有必要了,但加锁版每次都要加锁,浪费性能。

六、双重检查锁定(DCL)

为什么叫"双重检查"?

双重检查指的是两次 if (pobj == nullptr) 判断,不是两个锁。

cpp 复制代码
static MyObject& getObject(int x) {
    if (pobj == nullptr) {           // 第一次检查(无锁)
        m_mutex.lock();              
        if (pobj == nullptr) {       // 第二次检查(有锁)
            pobj = new MyObject(x);
        }
        m_mutex.unlock();
    }
    return *pobj;
}

为什么需要两次检查?

问题:两个线程同时调用,对象还未创建。

第一次检查 :两个线程都看到 pobj == nullptr,都准备进入。

如果没有第二次检查

cpp 复制代码
// 线程A 和 线程B 同时执行
if (pobj == nullptr) {   // 两个线程都看到 pobj 是空,都进入
    m_mutex.lock();
    pobj = new MyObject(x);  // 线程A 创建对象1
    m_mutex.unlock();
    // 线程B 被锁挡住,等线程A 释放锁后,线程B 拿到锁
    // 线程B 会继续执行 pobj = new MyObject(x)
    // 线程B 创建对象2
}

结果 :创建了 两个对象

原因 :两个线程都通过了第一次检查,虽然加锁保证了同一时间只有一个线程执行创建,但解锁后另一个线程会再次创建对象,最终产生两个对象

有第二次检查

cpp 复制代码
// 线程A 和 线程B 同时执行
if (pobj == nullptr) {   // 两个线程都看到 pobj 是空,都进入
    m_mutex.lock();
    if (pobj == nullptr) {      // 线程A:空,创建对象1
        pobj = new MyObject(x);
    }
    m_mutex.unlock();
    // 线程B 拿到锁后,先判断 pobj 是否为空
    // 此时 pobj 已有对象1,所以跳过创建
}

结果 :只创建了 一个对象

第一次检查的作用

对象创建后,后续调用直接跳过加锁代码,性能好。

cpp 复制代码
// 第2次、第100次调用时
if (pobj == nullptr) {   // pobj 已不为空,直接跳过
    // 加锁代码不会执行
}
return *pobj;

与加锁版对比

版本 每次调用都加锁? 第100次调用
加锁版 加锁(多余)
DCL 只做一次 if 判断,不加锁

注意事项

C++11 之后,推荐使用静态局部变量实现懒汉模式,更简洁:

cpp 复制代码
static MyObject& getObject(int x) {
    static MyObject objx(x);
    return objx;
}

七、推荐使用:C++11 静态局部变量懒汉模式

C++11 标准保证:静态局部变量的初始化是线程安全的

cpp 复制代码
class MyObject {
private:
    int value;
    
    MyObject(int x = 0) : value(x) {
        cout << "Create MyObject:" << this << endl;
    }
    
    MyObject(const MyObject&) = delete;
    MyObject operator=(const MyObject&) = delete;
    
public:
    ~MyObject() { cout << "Destroy MyObject:" << this << endl; }
    
    static MyObject& getObject(int x) {
        static MyObject objx(x);   // 静态局部变量,第一次调用时初始化
        return objx;
    }
};

优点

  • 代码最简洁

  • 线程安全(C++11 保证)

  • 按需创建,节省资源

注意getObject(int x) 的参数只对第一次调用有效,后续调用不会重新初始化。

cpp 复制代码
MyObject& obj1 = MyObject::getObject(10);  // 第一次调用,value = 10
MyObject& obj2 = MyObject::getObject(20);  // 返回同一个对象,value 还是 10

推荐理由:这是目前实现单例模式最简洁、最安全的方式,优先使用。

八、多线程测试

下面的代码验证 C++11 静态局部变量懒汉模式在多线程环境下的线程安全性。

cpp 复制代码
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;

class MyObject {
private:
    int value;
    
    MyObject(int x = 0) : value(x) {
        cout << "Create MyObject:" << this << endl;
    }
    
    MyObject(const MyObject&) = delete;
    MyObject operator=(const MyObject&) = delete;
    
public:
    ~MyObject() { cout << "Destroy MyObject:" << this << endl; }
    
    void Print() const {
        cout << "value:" << value << endl;
    }
    
    static MyObject& getObject(int x) {
        static MyObject objx(x);   // 静态局部变量,线程安全
        return objx;
    }
};

void threadfunc1() {
    MyObject& obj1 = MyObject::getObject(10);
    obj1.Print();
}

void threadfunc2() {
    MyObject& obj2 = MyObject::getObject(20);
    obj2.Print();
}

int main() {
    std::thread tha(threadfunc1);
    std::thread thb(threadfunc2);
    tha.join();
    thb.join();
    return 0;
}

输出(只会创建一个对象):

cpp 复制代码
Create MyObject:0x...
value:10
value:10

说明:虽然两个线程传入了不同的参数(10 和 20),但只创建了一个对象,值为第一次调用时的 10。这证明:

  1. 静态局部变量只初始化一次

  2. C++11 保证初始化过程是线程安全的

九、两种模式对比

对比项 饿汉模式 懒汉模式
创建时机 程序启动时 第一次调用时
线程安全 天然安全 需要处理
资源利用 可能浪费(不用也创建) 按需创建,节省资源
启动速度 较慢 较快
实现复杂度 简单 较复杂
推荐场景 对象一定会用到、简单程序 对象不一定用到、多线程程序、C++11推荐

十、总结

知识点 核心要点
单例模式 确保一个类只有一个实例,提供全局访问点
禁止外部创建 构造函数私有化 + 删除拷贝构造(= delete
饿汉模式 程序启动时创建静态对象,线程安全,但可能浪费资源
懒汉模式(基础) 第一次调用时创建,线程不安全
懒汉模式(加锁) 加互斥锁保证安全,但每次调用都加锁,性能差
双重检查锁定(DCL) 两次检查 + 一个锁,创建后不再加锁,兼顾安全和性能
C++11 静态局部变量 代码最简洁,线程安全,按需创建,推荐使用
单例对象必须是静态的 只有静态才能保证全局唯一
类外可以调用私有构造函数 类外定义静态成员时仍在类作用域内,可以访问私有成员
相关推荐
电商API&Tina2 小时前
淘宝 / 京东关键词搜索 API 接入与实战用途教程|从 0 到 1 搭建电商选品 / 比价 / 爬虫替代系统
java·开发语言·数据库·c++·python·spring
初圣魔门首席弟子2 小时前
Doxygen 文档注释详细介绍(含实际案例)
c++
郭涤生3 小时前
POD类型复习
c++
whitelbwwww3 小时前
标准模板库--STL库
开发语言·c++
han_3 小时前
JavaScript设计模式(十):模板方法模式实现与应用
前端·javascript·设计模式
菜_小_白3 小时前
RTP协议收发组件开发
linux·网络·c++
jf加菲猫3 小时前
第12章 数据可视化
开发语言·c++·qt·ui
wsoz3 小时前
Leetcode矩阵-day7
c++·算法·leetcode·矩阵
Emberone3 小时前
C++内存管理+模板初尝试:暴风雨前的尝试
c++