C++ 单例模式(Singleton)详解

单例模式(Singleton)是一种 设计模式 ,用于保证某个类只有一个实例,并提供全局访问点。它 不是 C++ 的语言特性,只是通过 C++ 提供的语法和特性实现的一种约定。

在 C++ 中,你甚至不一定需要类:函数和变量可以独立存在,因此单例更多是 组织全局状态的一种方式,类似命名空间。使用类的好处是可以:

  • 封装状态和逻辑。

  • 提供访问控制。

  • 支持成员变量和成员函数。


为什么要用单例?

  • 全局唯一:保证整个程序只有一个实例。

    • 配置管理器(ConfigManager)

    • 日志系统(Logger)

    • 如果使用普通类,每个模块都可能创建自己的实例,这样就无法保证全局状态一致。

      单例模式就是解决这个问题的一种方式。

    • 随机数生成器(Random)

  • 支持类特性:可以包含成员变量、函数和访问控制。

  • 延迟初始化:资源只有在第一次访问时分配,避免浪费。

  • 可扩展性:可以随时增加方法或状态管理,而不破坏结构。


单例模式的核心思想

单例模式主要依赖 三点

  1. **私有构造函数:**防止外部随意创建对象。

  2. **静态实例:**类内保存一个静态对象,确保唯一性。

  3. **静态访问方法:**提供全局访问点,返回静态对象引用。

例子:

cpp 复制代码
class Singleton
{
public:
	// 静态访问该类 GetInstance() 或者简写为 Get() 单例类只有一个实例 所以返回那个实例的引用
	static Singleton& GetInstance() // 这是一个静态方法 它就是Singleton::GetInstance() 只能调用静态变量 但是s_Instance就是静态变量
	{
		return s_Instance;
	}

	void Function() {}

private:
	Singleton() {}; // Singleton不能有public的构造函数 否则就会允许被实例化 此处意味着该类不能再外部被实例化
	
	static Singleton s_Instance; // 在private 只创建一次单例类的静态实例
};

// 静态成员变量必须在类外定义
Singleton Singleton::s_Instance;

int main()
{
	// 通过GetInstance()来访问这个单例 Singleton::GetInstance()就是那个单例
	Singleton& instance = Singleton::GetInstance(); // 一定要用引用 而不是复制
	// 假如这个实例想调用什么函数
	Singleton::GetInstance().Function();
	instance.Function(); // 和上面那句的含义是一样的
}
  • GetInstance() 是静态方法,返回 唯一实例 的引用。

  • 构造函数是私有的,防止外部创建多个对象。

  • 静态成员 s_Instance 在类外初始化,只创建一次。

注意:C++ 并不会阻止用户拷贝实例,如果不加限制,多次拷贝会破坏单例约束。


防止拷贝破坏单例

在 C++ 中,类如果没有显式声明拷贝构造函数和赋值运算符,编译器会默认生成它们。

对于单例模式来说:

cpp 复制代码
Random copy = Random::GetInstance(); // 会调用拷贝构造函数

如果允许拷贝,就会产生 新的实例,破坏单例 "全局唯一"的初衷。

解决方法:在 public 里显式删除拷贝构造和赋值运算符:

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

    static Singleton& GetInstance() { return s_Instance; }

private:
    Singleton() {}
    static Singleton s_Instance;
};

这样尝试复制单例对象会直接 编译错误,保证唯一性。


局部静态实例

传统实现需要在类外初始化静态成员,有翻译单元依赖问题。现代 C++ 可以使用 局部静态变量 延迟初始化:

cpp 复制代码
// 随机数生成器
class Random
{
public:
	Random(const Random&) = delete;// 禁止拷贝

	static Random& GetInstance()
	{
		return s_Instance;
	}

	float Float() { return m_RandomGenerator; }

private:
	Random() {}; // 私有构造函数

	float m_RandomGenerator = 0.5f; // 就假装这个是我们用某种方式生成的随机数
	
	static Random s_Instance; // 静态实例
};

Random Random::s_Instance;// 传统静态成员初始化

int main()
{
	float number = Random::GetInstance().Float(); // 这样就生成了一个随机数
}
  • instance 只会创建一次,生命周期长,线程安全(C++11 起)。

  • 使用 Random::Float() 就能获取随机数,无需每次都显式调用 GetInstance()

使用单例类 就是因为它实际上是一个类 可以支持所有类特性 比如类成员变量

静态方法封装内部实现

为了更方便访问,我们可以把内部成员函数封装起来,通过 静态方法调用:

cpp 复制代码
// 随机数生成器
class Random
{
public:
	Random(const Random&) = delete;

	static Random& GetInstance()
	{
		return s_Instance;
	}

	static float Float() { return GetInstance().IFloat(); } // 静态方法
private:
	float IFloat() { return m_RandomGenerator; } // 也可以用FloatImpl Impl是implementation 但是IFloat看起来更像一个接口 意思就是Internal内部的Float函数
	Random() {};

	float m_RandomGenerator = 0.5f;
	
	static Random s_Instance;
};

Random Random::s_Instance;

int main()
{
	float number = Random::Float(); // 就不需要再使用Random::GetInstance().Float()
}

好处:

  • 调用方式更简洁:Random::Float()

  • 不需要每次都写 Random::GetInstance().Float()

  • 对外只暴露接口,隐藏内部实现

局部 static 替代类静态成员

传统静态成员:

  • 必须在类外初始化:Random Random::s_Instance;

  • 对整个类可见

  • 存在翻译单元依赖问题(如果有多个 cpp 文件,初始化顺序需要注意)

现代 C++ 推荐 局部静态变量

cpp 复制代码
class Random {
public:
    Random(const Random&) = delete;

    static Random& GetInstance() {
        static Random instance; // 局部 static,只在第一次调用时创建
        return instance;
    }

    static float Float() { return GetInstance().IFloat(); }

private:
    float IFloat() { return m_RandomGenerator; }
    Random() {};
    float m_RandomGenerator = 0.5f;
};

解释局部 static 的意义

  1. 只在方法第一次调用时初始化

    • 延迟初始化(Lazy Initialization),节省启动开销。

    • 对象的生命周期从第一次访问开始,到程序结束。

  2. 作用域仅限方法内部

    • 外部无法直接访问 instance,保证封装性。
  3. 线程安全(C++11 以后)

    • 局部 static 的初始化在多线程环境下是安全的,不需要额外锁。
  4. 无需在类外初始化

    • 解决了传统静态成员需要在 cpp 文件外部定义的问题。

单例 vs 命名空间

方式 优点 缺点
命名空间 简单、无需实例化 无法管理状态生命周期
单例类 封装、状态管理、访问控制 需要注意拷贝和初始化

总结:单例类就是一种 组织全局对象的方式,既能保证唯一性,又能利用类特性。


单例模式的注意事项

  1. **避免拷贝:**显式删除拷贝构造函数和赋值操作,确保全局唯一。

  2. **生命周期管理:**局部静态变量生命周期到程序结束,传统静态成员也类似,但需要类外定义。

  3. **线程安全:**局部静态变量初始化自 C++11 起是线程安全的。

  4. **使用场景:**仅在需要全局唯一对象时使用单例,避免滥用全局状态。

总结

单例模式的本质是 类 + 静态实例 + 静态访问方法,并通过:

  • 私有构造函数避免外部实例化。

  • 删除拷贝构造与赋值运算符,防止复制。

  • 使用局部静态实例,实现延迟初始化和线程安全。

虽然完全可以用命名空间实现类似功能,但使用单例类能够更好地组织全局状态,并支持类的特性和面向对象扩展。

相关推荐
默默coding的程序猿4 小时前
1.单例模式有哪几种常见的实现方式?
java·开发语言·spring boot·spring·单例模式·设计模式·idea
Nuyoah11klay5 小时前
华清远见25072班C++学习day7
c++
bkspiderx5 小时前
C++设计模式之行为型模式:迭代器模式(Iterator)
c++·设计模式·迭代器模式
努力努力再努力wz6 小时前
【C++进阶系列】:万字详解智能指针(附模拟实现的源码)
java·linux·c语言·开发语言·数据结构·c++·python
凤年徐6 小时前
【C++】string的模拟实现
c语言·开发语言·c++
牟同學6 小时前
从赌场到AI:期望值如何用C++改变世界?
c++·人工智能·概率论
夜晚中的人海7 小时前
【C++】智能指针介绍
android·java·c++
chennn128 小时前
c++相关学习
开发语言·c++·学习
m0_552200828 小时前
《UE5_C++多人TPS完整教程》学习笔记61 ——《P62 武器开火特效(Fire Weapon Effects)》
c++·游戏·ue5