单例模式(Singleton Pattern)是一种设计模式,它确保一个类只有一个实例,并且提供全局访问点来访问该实例。单例模式常用于管理全局状态或资源,比如数据库连接、日志系统等。
单例模式的特点
- 唯一性:确保类在整个程序的生命周期中只有一个实例。
- 延迟实例化:通常会在第一次使用时才创建实例,避免不必要的资源消耗。
- 全局访问点:提供一个静态方法获取唯一实例,方便全局使用。
一、C++ 实现单例模式
下面是 C++ 实现单例模式的例子:
步骤 1: 定义私有的构造函数
确保类的构造函数是私有的,外部无法通过 new
来创建对象。
cpp
class Singleton {
private:
// 私有构造函数,防止外部实例化
Singleton() {
std::cout << "Singleton instance created!" << std::endl;
}
public:
// 删除复制构造函数,防止复制对象
Singleton(const Singleton&) = delete;
// 删除赋值操作符,防止对象被赋值
Singleton& operator=(const Singleton&) = delete;
// 静态方法,提供全局访问点获取唯一实例
static Singleton& getInstance() {
static Singleton instance; // 静态局部变量,保证唯一
return instance;
}
// 普通方法,做一些事情
void doSomething() {
std::cout << "Doing something!" << std::endl;
}
};
步骤 2: 使用单例
cpp
int main() {
// 获取单例实例
Singleton& s1 = Singleton::getInstance();
s1.doSomething();
// 再次获取单例实例,依然是同一个对象
Singleton& s2 = Singleton::getInstance();
s2.doSomething();
return 0;
}
输出结果:
Singleton instance created!
Doing something!
Doing something!
解释
- 私有构造函数 :
Singleton()
是私有的,因此外部无法直接创建Singleton
对象,确保了类只有一个实例。 - 静态方法
getInstance()
:这是获取唯一实例的全局访问点。在第一次调用时,静态局部变量instance
会被初始化,之后的每次调用都会返回同一个实例。 - 防止复制:通过删除复制构造函数和赋值操作符,防止创建新的副本。
- 静态局部变量 :
static Singleton instance;
保证了单例的唯一性,并且只在第一次调用getInstance()
时进行实例化,之后不会再创建新的对象。
线程安全的实现
上述代码在单线程环境下工作良好,但在多线程环境中可能会有并发问题。为了解决这个问题,C++11 之后引入了静态局部变量初始化的线程安全特性。因此,static Singleton instance;
在 C++11 之后默认是线程安全的。
总结:单例模式通过确保类的构造函数私有化,并提供静态方法获取唯一实例,控制了实例的数量。在 C++ 中,使用静态局部变量和 delete
复制构造等技巧可以很好地实现这一模式。
二、每一步代码的解释:
1. 私有构造函数
cpp
private:
Singleton() {
std::cout << "Singleton instance created!" << std::endl;
}
- 作用 :单例模式的核心之一。通过将构造函数设置为
private
,确保外部代码不能直接创建Singleton
对象,只能通过类的静态方法getInstance()
来获取实例。 - 原因 :如果构造函数是
public
的,外部代码可以随意创建多个实例,违背了单例模式只允许一个实例的原则。
2. 删除复制构造和赋值操作符
cpp
// private or public
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
- 作用 :这两行代码删除了默认的复制构造函数和赋值操作符,防止用户通过复制或赋值来创建多个
Singleton
对象。 - 原因 :即使构造函数是私有的,用户仍然可能通过
Singleton s2 = s1;
或s2 = s1;
来复制或赋值对象。这种方式也会违反单例模式的唯一性要求。
private or public?
虽然这两行代码通常放在 private
部分,但将它们声明为 public
也是合理的,并且在某些情况下可能更具可读性。无论它们是放在 private
还是 public
区域,效果都是一样的:阻止用户通过复制构造或赋值操作创建新的 Singleton
实例。
在 public
下声明的好处
-
更清晰的错误提示 :当这些操作符被放在
public
部分时,用户试图使用复制构造函数或赋值操作符时,编译器会直接报错并提示这些操作被禁用。如果放在private
中,编译器可能只会提示这些函数是不可访问的,而不会明确指出它们被删除了。 -
遵循现代 C++ 的风格 :在 C++11 及之后,删除函数通常被放在
public
区域,以表明这些操作是显式被禁止的,而不是因为访问权限限制而导致的不可访问。
3. 静态方法 getInstance()
cpp
public:
static Singleton& getInstance() {
static Singleton instance;
return instance;
}
- 作用 :
- 这是实现单例模式的关键点。
getInstance()
是一个静态方法(static
方法),是类级别的方法,不依赖于类的实例,意味着你不需要先创建对象再调用它。它可以直接通过类名访问。比如:
- 这是实现单例模式的关键点。
cpp
Singleton::getInstance();
static Singleton instance;
是静态局部变量。这个变量只在第一次调用getInstance()
时初始化,之后每次调用都会返回同一个对象。- 访问静态成员或静态资源:静态方法可以直接访问静态变量(例如在单例模式中使用的静态局部变量)。这使得静态方法可以用于管理某些全局资源,如单例模式中的唯一实例。
- 原因 :
- 静态方法能够独立于实例存在,这是它的最大优势。在单例模式中,我们希望全局访问点是一个不依赖对象的类方法,而静态方法正好符合这一要求。
static Singleton instance;
是 C++ 实现单例模式的经典方法。它利用静态局部变量的生命周期来保证对象的唯一性。静态局部变量在函数作用域中定义,但只会被初始化一次,并在整个程序运行期间保持有效。
4. 普通方法 doSomething()
cpp
void doSomething() {
std::cout << "Doing something!" << std::endl;
}
- 作用:这是一个普通的成员函数,用来展示单例对象可以调用的功能。
- 原因 :一旦获取了单例实例,你就可以调用类的其他非静态方法来执行某些操作。在这个例子中,
doSomething()
方法就是展示如何使用单例的一个例子。
为什么这么写?
- 唯一性 :通过将构造函数设为
private
和删除复制、赋值操作符,确保了外部代码无法随意创建多个实例。 - 全局访问点 :静态方法
getInstance()
允许用户在任何地方通过Singleton::getInstance()
访问唯一实例。这符合单例模式的设计思想。 - 懒加载 :使用
static Singleton instance;
实现了懒加载,意味着只有在第一次调用getInstance()
时,实例才会被创建。这提高了程序的性能,因为在不使用Singleton
的时候,不会浪费内存。 - 线程安全 :在 C++11 及以后的标准中,静态局部变量的初始化是线程安全的,因此即使在多线程环境中,多个线程同时访问
getInstance()
,也不会出现多个实例的问题。
通过这些设计,单例模式确保了类只有一个实例,并且为用户提供了一个全局的、方便的访问接口。
三、Instance 很大怎么办
在 C++ 中,static
局部变量(例如 static Singleton instance;
)的存储位置取决于系统的内存布局。具体来说,instance
被分配在静态存储区(也叫全局/静态数据区),而不是栈或堆中。
static
局部变量的存储位置
- 静态存储区 :
static
变量无论是在函数内还是类中定义,其生命周期从程序开始到程序结束。这类变量在程序启动时就被分配内存,并在整个程序运行期间都存在。 - 特点 :
- 全局生存期 :即使它是在一个函数中定义的(如
getInstance()
函数内部),static
变量的生命周期从第一次初始化开始,直到程序结束。 - 唯一初始化:静态局部变量只会在第一次调用时进行初始化,之后每次调用都会返回同一个对象,不会重复创建。
- 全局生存期 :即使它是在一个函数中定义的(如
因此,static Singleton instance;
在程序运行时,它的内存会被分配在静态数据区,直到程序结束时才释放。
如果 instance
很大该怎么做?
如果 Singleton
对象非常大,占用了大量内存,放在静态存储区可能不是一个好的选择。这时候,可以将对象的实际内容放到堆上,使用指针或智能指针来管理它。这样可以在需要时创建对象,并且控制其生命周期和内存释放。
使用动态内存分配的方式
修改 getInstance()
函数,将对象动态分配到堆上,通过指针来管理它。这里使用 std::unique_ptr
(C++11 引入的智能指针)来保证内存的自动释放。
cpp
#include <memory> // 引入 std::unique_ptr
class Singleton {
private:
Singleton() {
std::cout << "Singleton instance created!" << std::endl;
}
// 删除复制构造函数和赋值操作符
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
public:
// 静态方法返回唯一的实例指针
static Singleton& getInstance() {
// 静态智能指针,指向动态分配的 Singleton 对象
static std::unique_ptr<Singleton> instance(new Singleton());
return *instance; // 返回解引用的实例
}
// 示例方法
void doSomething() {
std::cout << "Doing something!" << std::endl;
}
};
解释
-
std::unique_ptr<Singleton> instance
:我们将Singleton
对象动态分配在堆上,并使用std::unique_ptr
来管理它。unique_ptr
会自动负责对象的内存释放,无需手动调用delete
。 -
static std::unique_ptr<Singleton> instance(new Singleton());
:静态智能指针只会在第一次调用时分配内存,并且整个程序中只会有一个Singleton
实例。智能指针会自动在程序结束时释放Singleton
的内存,避免内存泄漏。 -
返回解引用的对象 :
*instance
是对智能指针所管理对象的解引用,因此getInstance()
返回的是Singleton
实例的引用,而不是指针。
优点
- 动态分配内存:避免在静态存储区中直接分配大对象。对象被动态分配在堆上,且内存使用可以更加灵活。
- 内存安全:通过智能指针管理对象,避免了手动管理内存所带来的风险(如内存泄漏、悬空指针等)。
- 延迟加载 :
Singleton
对象仍然会在第一次使用时才创建,节省资源。
总结
- 原实现 :
static Singleton instance;
将对象存储在静态存储区中,适合小型或中等大小的对象。 - 动态内存管理 :如果对象很大,可以使用
std::unique_ptr
等智能指针,将对象动态分配到堆上,合理管理内存。这种方式使得内存更灵活、安全。
如果担心 instance
占用太多静态存储区的内存,使用动态分配的方案是一种良好的选择。
四、C++11 以前的单例模式
待补
五、补充
待补
六、多单例项目下的单例模板类
待补