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