C++八股 —— 单例模式

文章目录

  • [1. 基本概念](#1. 基本概念)
  • [2. 设计要点](#2. 设计要点)
  • [3. 实现方式](#3. 实现方式)
  • [4. 详解懒汉模式](#4. 详解懒汉模式)

1. 基本概念

线程安全(Thread Safety)

线程安全是指在多线程环境下 ,某个函数、类或代码片段能够被多个线程同时调用时,仍能保证数据的一致性和逻辑的正确性,不会因线程切换导致错误结果。

单例模式(Singleton Pattern)

单例设计模式是一种创建型设计模式,其核心目的是确保一个类只有一个实例 存在,并提供全局访问点来获取该实例。它常用于管理全局资源(如配置信息、日志系统、数据库连接池等),避免重复创建和资源竞争。

2. 设计要点

  1. 构造函数和析构函数是私有的,不允许外部生成和释放
    • 禁止外部实例化 :外部代码无法通过 new 或直接声明的方式创建对象,确保唯一实例的控制权在类自身。
    • 控制生命周期:析构函数私有化可防止外部意外删除单例对象,保证其生命周期与程序一致。
    • 符合单一职责原则:类的创建和销毁逻辑由自身管理,避免外部干扰。
  2. 静态成员变量和静态返回单例的成员函数
    • 全局访问点 :通过静态方法 getInstance() 提供统一的实例获取方式,替代直接访问全局变量。
    • 延迟初始化(懒汉式):仅在首次调用时创建实例,节省资源。
    • 线程安全(需额外处理) :可通过锁或局部静态变量(C++11 后)确保多线程安全。
      • 在单例模式中,如果多个线程同时调用 getInstance() 方法,可能导致多次创建实例(如懒汉模式未加锁时),破坏单例的唯一性。
      • 解决方案
        • 加锁(互斥量) :在 getInstance() 中使用互斥锁(如 std::mutex)确保线程同步。
        • 局部静态变量(C++11):利用编译器保证局部静态变量的初始化是线程安全的。
        • 饿汉模式:提前初始化实例,避免多线程竞争。
  3. 禁用拷贝构造函数和赋值运算符
    • 防止拷贝:避免通过拷贝构造函数复制单例对象,破坏唯一性。
    • 防止赋值 :禁止通过赋值运算符覆盖单例对象,如 instance2 = instance1
    • 强制单例约束:从语法层面杜绝意外破坏单例模式的行为。
要点 解决的问题 实际意义
私有构造/析构 外部随意创建或销毁实例 确保实例的唯一性和可控性
静态成员与访问方法 全局访问与资源管理 提供统一入口,支持延迟初始化与线程安全
禁用拷贝与赋值 意外复制导致多实例 维护单例的严格唯一性

3. 实现方式

懒汉模式

懒汉模式的核心是延迟初始化 (Lazy Initialization),即在首次调用 getInstance() 时才创建单例实例。在此之前,实例未被分配内存。

特点

  • 优点
    • 节省资源:若单例对象未被使用,则不会创建。
    • 适合初始化耗时的对象(如文件系统、网络连接)。
  • 缺点
    • 需处理线程安全问题(多线程下可能重复创建)。
    • 首次访问可能因初始化导致延迟。

饿汉模式

饿汉模式的核心是提前初始化 ,即在程序启动时(或类加载时)直接创建单例实例,无论是否被使用。

特点

  • 优点
    • 线程安全:实例在程序启动时初始化,避免多线程竞争。
    • 代码简单:无需处理复杂的线程同步逻辑。
  • 缺点
    • 可能浪费资源:即使未使用单例对象,也会占用内存。
    • 初始化时间可能影响程序启动速度。

实现样例

cpp 复制代码
class Singleton {
public:
    static Singleton* getInstance() {
        return &instance; // 直接返回已初始化的实例
    }
private:
    static Singleton instance;
    Singleton() {}
    ~Singleton() {}
};
// 程序启动时初始化(饿汉模式)
Singleton Singleton::instance;

对比懒汉模式与饿汉模式

特性 懒汉模式 饿汉模式
初始化时机 首次调用 getInstance() 程序启动时(或类加载时)
线程安全 需额外处理(如加锁或 C++11 特性) 天然线程安全
资源占用 按需分配,节省资源 提前占用内存,可能浪费资源
适用场景 初始化耗时、使用频率不确定的对象 初始化简单、使用频繁的对象

实际开发中,推荐使用 C++11 的局部静态变量懒汉模式(Meyers' Singleton,线程安全且代码简洁),或根据场景选择饿汉模式。

4. 详解懒汉模式

参考:【C++面试题】手撕单例模式_哔哩哔哩_bilibili

样例1

cpp 复制代码
class Singleton1 {
public:
    // 要点2
	static Singleton1 * GetInstance() {
		if(_instance == nullptr) {
			_instance = new Singleton1();
		}
		return _instance;
	}
private:
    // 要点1
	Singleton1() {}
	~Singleton1() {
		std::cout << "~Singleton1()\n";
	}
    // 要点3
	Singleton1(const Singleton1 &) = delete;
	Singleton1& operator = (const Singleton1&) = delete;
	Singleton1(Singleton1 &&) = delete;
	Singleton1& operator = (Singleton1 &&) = delete;
	// 要点2
	static Singleton1 *_instance; 
};
Singleton1* Singleton1::_instance = nullptr;

存在错误:

  • 该类创建的单例对象在堆中,虽然资源会被释放,但其在释放的时候是无法调用析构函数的。
  • 非线程安全

样例2

cpp 复制代码
class Singleton2 {
public:
	static Singleton2 * GetInstance() {
		if(_instance == nullpte) {
			_instance = new Singleton2();
			atexit(Destructor);
		}
		return _instance;
	}
private:
	static void Destructor() {
		if(nullptr != _instance) {
			delete _instance;
			_instance = nullptr;
		}
	}
	Singleton2() {}
	~Singleton2() {
		std::cout << "~Singleton2()\n";
	}
	Singleton2(const Singleton2 &) = delete;
	Singleton2& operator = (const Singleton2&) = delete;
	Singleton2(Singleton2 &&) = delete;
	Singleton2& operator = (Singleton2 &&) = delete;
	
    static Singleton2 *_instance; 
};
Singleton2* Singleton2::_instance = nullptr;

针对样例1的问题,添加atexit(),在程序结束时手动释放对象,从而调用析构函数

存在问题:

  • 非线程安全

样例3

cpp 复制代码
class Singleton3 {
public:
	static Singleton3 * GetInstance() {
		std::lock_guard<std::mutex> lock(_mutex);
		if(_instance == nullptr) {
			std::lock_guard<std::mutex> lock(_mutex);
			if(_instance == nullptr) {
				_instance = new Singleton3();
				// 1. 分配内存
				// 2. 调用构造函数
				// 3. 返回对象指针 
				atexit(Destructor);
			}
		}
		return _instance;
	}
private:
	static void Destructor() {
		if(nullptr != _instance) {
			delete _instance;
			_instance = nullptr;
		}
	}
	Singleton3() {}
	~Singleton3() {
		std::cout << "~Singleton3()\n";
	}
	Singleton3(const Singleton3 &) = delete;
	Singleton3& operator = (const Singleton3&) = delete;
	Singleton3(Singleton3 &&) = delete;
	Singleton3& operator = (Singleton3 &&) = delete;
	
	static Singleton3 *_instance; 
	static std::mutex _mutex;
};
Singleton3* Singleton3::_instance = nullptr;
std::mutex Singleton3::_mutex;

在创建实例对象是使用互斥锁来实现线程安全

  • 单检测

    先加锁,再判断是否需要创建对象;

    该方法只需要检测一次,但是在已经创建对象的情况下,只需要检测然后返回就行,不需要再第一次检测前加锁(力度过大,效率低)

  • 双检测(Double-Checked Locking,DCL)

    先做第一次检测,然后在需要创建对象时才加锁,此时多线程程序会出现多个线程同时通过一次检测到创建对象的代码块,所以需要第二次检测对象是否创建来避免重复创建

存在问题:

在多线程程序中,CPU会进行指令重排,如new操作的正常顺序应该是(1-2-3),在指令重排之后执行顺会变为(1-3-2)。此时如果某个线程执行到new的"返回对象指针操作",而另外一个线程执行到第一次检测,则会出现另外一个线程返回为初始化对象的情况。


样例4 :(面试八股的重点)

cpp 复制代码
class Singleton4 {
public:
	static Singleton4 * GetInstance() {
		Singleton4* tmp = _instance.load(std::memory_order_relaxed);
		std::atomic_thread_fence(std::memory_order_acquire);
		if(tmp == nullptr) {
			std::lock_guard<std::mutex> lock(_mutex);
			tmp = _instance.load(std::memory_order_relaxed);
			if(tmp == nullptr) {
				tmp = new Singleton4();
				std::atomic_thread_fence(std::memory_order_release);
				_instance.store(tmp, std::memory_order_relaxed);
				atexit(Destructor);
			}
		}
		return tmp;
	}
private:
	static void Destructor() {
		Singleton4* tmp = _instance.load(std::memory_order_relaxed);
		if(nullptr != tmp) {
			delete tmp;
		}
	}
	Singleton4() {}
	~Singleton4() {
		std::cout << "~Singleton4()\n";
	}
	Singleton4(const Singleton4 &) = delete;
	Singleton4& operator = (const Singleton4&) = delete;
	Singleton4(Singleton4 &&) = delete;
	Singleton4& operator = (Singleton4 &&) = delete;
	
	static std::atomic<Singleton4*> _instance;
	static std::mutex _mutex;
};
std::atomic<Singleton4*> Singleton4::_instance;
std::mutex Singleton4::_mutex;

使用内存屏障和原子操作来解决指令重排的问题

内存屏障

  • 作用
    强制限制指令重排,并确保内存操作的可见性(即一个线程的写入对其他线程立即可见)。
  • 类型
    • 获取屏障(acquire fence)
      后续读/写操作不会重排到屏障前,且能读取其他线程的释放操作结果。
    • 释放屏障(release fence)
      前面的读/写操作不会重排到屏障后,且保证当前线程的写入对其他线程可见。
  • 代码中的应用
    • 获取屏障 :确保 if(tmp == nullptr) 之后的代码能看到其他线程的完整初始化结果。
    • 释放屏障 :确保 new 的构造操作完成后,再存储指针到 _instance

原子操作

  • 定义
    不可分割的操作,保证对变量的读写要么完全执行,要么不执行,不会出现中间状态。
  • 内存顺序(Memory Order)
    • memory_order_relaxed:仅保证原子性,无同步或顺序约束(允许指令重排)。
    • memory_order_acquire/release:与屏障配合,实现同步语义。
  • 代码中的应用
    _instance 被声明为 std::atomic<Singleton4*>,确保其读写是原子的,避免数据竞争。

原子操作详情参考:C++八股 ------ 原子操作-CSDN博客


样例5

cpp 复制代码
class Singleton5 {
public:
	static Singleton5* GetInstance() {
		static Singleton5 instance;
		return &instance;
	}
private:
	Singleton5() {}
	~Singleton5() {
		std::cout << "~Singleton5()\n";
	}
	
	Singleton5(const Singleton5 &) = delete;
	Singleton5& operator = (const Singleton5&) = delete;
	Singleton5(Singleton5 &&) = delete;
	Singleton5& operator = (Singleton5 &&) = delete;
};

静态局部变量具备单例的全部三个特性

最简单也是最推荐的版本


样例6

cpp 复制代码
template<typename T>
class Singleton {
public:
	static T* GetInstance() {
		static T instance;
		return &instance;
	}
protected:
	Singleton() {}
	virtual ~Singleton() {
		std::cout << "~Singleton()\n";
	}
private:
	Singleton(const Singleton &) = delete;
	Singleton& operator = (const Singleton&) = delete;
	Singleton(Singleton &&) = delete;
	Singleton& operator = (Singleton &&) = delete;
};

class DesignPattern : public Singleton<DesignPattern> {
	friend class Singleton<DesignPattern>;
private:
	DesignPattern() {}
	~DesignPattern() {
		std::cout << "~DesignPattern()\n";
	}
};

类模板封装单例的三个特性,使用时直接继承即可。

  • 基类构造和析构函数设置为protected是因为需要其对子类时可见的
  • 友元是为了让基类能访问子类的构造析构函数
相关推荐
jndingxin18 分钟前
c++ 面试题(1)-----深度优先搜索(DFS)实现
c++·算法·深度优先
Watink Cpper38 分钟前
[灵感源于算法] 算法问题的优雅解法
linux·开发语言·数据结构·c++·算法·leetcode
老一岁42 分钟前
C++ 类与对象的基本概念和使用
java·开发语言·c++
随意02344 分钟前
STL 3算法
开发语言·c++·算法
偷懒下载原神44 分钟前
《C++ 继承》
开发语言·c++
随意0231 小时前
STL 4函数对象
开发语言·c++
温宇飞1 小时前
C++ 成员指针详解
c++
老猿讲编程2 小时前
汽车车载软件平台化项目规模颗粒度选择的一些探讨
c++·汽车
clock的时钟2 小时前
c++第七天--继承与派生
开发语言·c++
John_ToDebug2 小时前
Chrome 浏览器前端与客户端双向通信实战
前端·c++·chrome