C++ 单例模式(Singleton)详解
单例模式(Singleton)是一种 设计模式,用于保证某个类只有一个实例,并提供全局访问点。它 不是 C++ 的语言特性,只是通过 C++ 提供的语法和特性实现的一种约定。
在 C++ 中,你甚至不一定需要类:函数和变量可以独立存在,因此单例更多是 组织全局状态的一种方式,类似命名空间。使用类的好处是可以:
封装状态和逻辑。
提供访问控制。
支持成员变量和成员函数。
为什么要用单例?
全局唯一:保证整个程序只有一个实例。
配置管理器(ConfigManager)
日志系统(Logger)
如果使用普通类,每个模块都可能创建自己的实例,这样就无法保证全局状态一致。
单例模式就是解决这个问题的一种方式。随机数生成器(Random)
支持类特性:可以包含成员变量、函数和访问控制。
延迟初始化:资源只有在第一次访问时分配,避免浪费。
可扩展性:可以随时增加方法或状态管理,而不破坏结构。
单例模式的核心思想
单例模式主要依赖 三点:
私有构造函数:防止外部随意创建对象。
静态实例:类内保存一个静态对象,确保唯一性。
静态访问方法:提供全局访问点,返回静态对象引用。
例子:
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++ 中,类如果没有显式声明拷贝构造函数和赋值运算符,编译器会默认生成它们。
对于单例模式来说:
Random copy = Random::GetInstance(); // 会调用拷贝构造函数
如果允许拷贝,就会产生 新的实例,破坏单例 “全局唯一”的初衷。
解决方法:在 public
里显式删除拷贝构造和赋值运算符:
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++ 可以使用 局部静态变量 延迟初始化:
// 随机数生成器
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()
。
使用单例类 就是因为它实际上是一个类 可以支持所有类特性 比如类成员变量
静态方法封装内部实现
为了更方便访问,我们可以把内部成员函数封装起来,通过 静态方法调用:
// 随机数生成器
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++ 推荐 局部静态变量:
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 的意义:
只在方法第一次调用时初始化
延迟初始化(Lazy Initialization),节省启动开销。
对象的生命周期从第一次访问开始,到程序结束。
作用域仅限方法内部
外部无法直接访问
instance
,保证封装性。
线程安全(C++11 以后)
局部 static 的初始化在多线程环境下是安全的,不需要额外锁。
无需在类外初始化
解决了传统静态成员需要在 cpp 文件外部定义的问题。
单例 vs 命名空间
方式 | 优点 | 缺点 |
---|---|---|
命名空间 | 简单、无需实例化 | 无法管理状态生命周期 |
单例类 | 封装、状态管理、访问控制 | 需要注意拷贝和初始化 |
总结:单例类就是一种 组织全局对象的方式,既能保证唯一性,又能利用类特性。
单例模式的注意事项
避免拷贝:显式删除拷贝构造函数和赋值操作,确保全局唯一。
生命周期管理:局部静态变量生命周期到程序结束,传统静态成员也类似,但需要类外定义。
线程安全:局部静态变量初始化自 C++11 起是线程安全的。
使用场景:仅在需要全局唯一对象时使用单例,避免滥用全局状态。
总结
单例模式的本质是 类 + 静态实例 + 静态访问方法,并通过:
私有构造函数避免外部实例化。
删除拷贝构造与赋值运算符,防止复制。
使用局部静态实例,实现延迟初始化和线程安全。
虽然完全可以用命名空间实现类似功能,但使用单例类能够更好地组织全局状态,并支持类的特性和面向对象扩展。