1、介绍
单例模式(Singleton Pattern)是一种创建型设计模式,它确保一个类只有一个实例,并提供一个全局访问点来获取这个唯一实例。这种模式在多线程环境中需要特别注意线程安全,并且应该避免在析构时产生问题(如产生死锁)。
思考:
如何绕过常规的构造函数,提供一种机制来保证一个类只有一个实例。
解决过程:
(1)要实现单例模式,先把构造函数私有化。
私有化带来的问题是:外部不可以定义对象,即不可以从外部调到类的构造函数。
解决:在类内定义一个公开的接口,返回本类对象的指针。
(2)在public 权限下定义一个函数,返回出本类对象的指针。
问题:若函数是一个普通函数,需要依赖于类对象的调用才可以,这与只产生一个单例相矛盾。
解决:把这个函数升级为静态函数,无需依赖于对象的调用,可以直接调用。
(3)把这个函数升为静态函数。
问题:静态函数是没有this指针,无法调用类中属性(无法访问普通属性)。
解决:把类中的属性升级为静态属性。静态成员函数只能调用静态成员属性。
(4)把类中的本类的指针,升级为静态属性。
(5)因为是静态成员函数,所以不能在静态成员函数中书写开辟空间的逻辑,应该把开辟空间的逻辑写在全局作用域中。
至此,一个单例模式的程序完成。
类中静态成员函数特性:
使用static关键字修饰类成员函数时,就是把这个成员函数升级成了全局函数。只不过这个全局函数隐藏在这个类之中。
(1) 静态成员函数是没有this指针的。
(2) 静态成员函数是不可以调用类中的非静态成员,只能调用或访问类中的静态成员。
(3) 静态成员函数不依赖于成员对象,可以使用 类名+类域访问符 的形式直接调用。
总结:静态成员函数与静态成员对象一样,是服务于整个类的,而不依赖于某个对象。
2、应用场景
(1)资源共享
当多个对象需要共享同一个资源时,可以使用单例模式来确保只有一个实例被创建和使用。例如,数据库连接池、线程池等,通过单例模式可以避免重复创建和销毁这些资源,从而提高系统性能。
(2)配置文件读取
单例模式可以用于读取配置文件,并在程序中共享配置信息。这样可以确保整个应用程序使用统一的配置,并且方便在需要时修改配置。
(3)日志记录
在应用程序中,通常需要记录日志以便调试和错误追踪。使用单例模式可以确保只有一个日志记录器实例存在,方便全局访问和统一的日志记录。
(4)对象缓存
在需要频繁读取和写入数据的情况下,使用缓存可以显著提高系统的性能。通过单例模式可以创建一个缓存管理器,确保只有一个缓存实例存在,并且可以被多个线程共享,方便全局访问和统一管理缓存。
(5)GUI应用程序
在图形用户界面(GUI)应用程序中,单例模式可以用于创建全局唯一的窗口、对话框等。这样可以确保在整个应用程序中只有一个这样的对象存在,方便管理和访问。
(6)配置管理器
在一个应用程序中,可能需要使用一个配置管理器来存储和管理应用程序的配置信息。使用单例模式可以确保只有一个配置管理器实例存在,方便全局访问和统一管理配置。
(7)高成本实例化对象
当实例化一个类的成本较高,而且只需要一个实例时,可以使用单例模式来避免重复实例化的开销。这可以节省系统资源并提高性能。
(8)线程安全访问
当需要对资源进行集中管理,以确保线程安全的访问时,单例模式是一个不错的选择。通过单例模式,可以避免多线程环境下的资源竞争问题。
总结来说,C++单例模式的应用场景主要集中在需要确保系统中只有一个实例存在且可以被全局访问的情况下。这些场景通常涉及资源共享、配置管理、日志记录、对象缓存等方面。使用单例模式可以简化系统的设计和管理,提高系统的性能和可维护性。在使用单例模式时,需要注意线程安全性和全局状态的管理等问题。
3、饿汉式单例模式【推荐使用】
单例模式的【饿汉式】(Eager Initialization)是在类定义时就立即初始化单例对象的方式。这种方法在程序启动时就会创建单例对象,因此它总是尽早的初始化实例。由于是在编译时确定的初始化,因此这种方式是线程安全的(在C++11及以后的版本中),并且不需要额外的同步机制。
cpp
#include <iostream>
class Singleton {
private:
// 静态成员变量,在类定义时即初始化
static Singleton* instance_;
// 构造函数是私有的,确保不能在类的外部实例化
Singleton() = default;
// 禁止拷贝构造和赋值操作
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
public:
// 获取单例对象的静态方法
static Singleton* getInstance() {
return instance_;
}
// 示例方法
void doSomething() {
std::cout << "Doing something in Singleton instance." << std::endl;
}
// 在类外定义并初始化静态成员变量
// 注意:这里不需要使用任何锁,因为初始化发生在程序启动时
static Singleton* instance_ = new Singleton();
};
// 不需要在这里再初始化instance_,因为它已经在类定义中初始化了
int main() {
// 静态成员函数不依赖于成员对象,可以使用【类名+类域访问符】的形式直接调用
Singleton* s1 = Singleton::getInstance();
Singleton* s2 = Singleton::getInstance();
// s1 和 s2 指向同一个实例
if (s1 == s2) {
std::cout << "s1 and s2 are the same instance." << std::endl;
}
s1->doSomething();
s2->doSomething(); // 这两个调用将执行相同实例的doSomething方法
return 0;
}
在这个示例中,Singleton
类的静态成员 instance_
在类定义时就初始化了,这意味着在程序启动时就会创建一个 Singleton
的实例。因此,任何对 getInstance()
的调用都会返回这个预先创建好的实例。
由于饿汉式单例模式在程序启动时就完成了初始化,所以它不存在多线程下初始化冲突的问题,是线程安全的。但是,它也有一个潜在的缺点,那就是如果单例对象非常大或者初始化很耗时,那么程序启动的时间可能会受到影响。此外,如果单例对象从未被使用,那么就会浪费内存。然而,在大多数情况下,这些缺点都是可以接受的,因为单例对象通常代表一些重要的系统资源或功能,需要尽早地初始化。
4、懒汉式单例模式
懒汉式(Lazy Initialization)单例模式是在第一次需要访问单例对象时才进行初始化的方式。这种方式也称为"延迟加载"或"延迟初始化"。由于懒汉式单例在访问时才进行初始化,因此需要考虑多线程环境下的线程安全性问题。
cpp
#include <iostream>
#include <memory>
#include <mutex>
class Singleton {
private:
// 静态成员指针,指向单例对象
static std::unique_ptr<Singleton> instance_;
// 静态互斥锁,用于保护多线程访问
static std::mutex mutex_;
// 静态标志,表示单例对象是否已经被创建
static std::once_flag onceFlag_;
// 构造函数是私有的,确保不能在类的外部实例化
Singleton() = default;
// 禁止拷贝构造和赋值操作
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
public:
// 获取单例对象的静态方法
static Singleton* getInstance() {
std::call_once(onceFlag_, []() {
instance_.reset(new Singleton());
});
return instance_.get();
}
// 示例方法
void doSomething() {
std::cout << "Doing something in Singleton instance." << std::endl;
}
};
// 初始化静态成员变量
std::unique_ptr<Singleton> Singleton::instance_ = nullptr;
std::mutex Singleton::mutex_; // 注意:这里实际上在懒汉式中并不需要mutex_,因为std::call_once已经保证了线程安全
std::once_flag Singleton::onceFlag_;
int main() {
// 静态成员函数不依赖于成员对象,可以使用【类名+类域访问符】的形式直接调用
Singleton* s1 = Singleton::getInstance();
Singleton* s2 = Singleton::getInstance();
// s1 和 s2 指向同一个实例
if (s1 == s2) {
std::cout << "s1 and s2 are the same instance." << std::endl;
}
s1->doSomething();
s2->doSomething(); // 这两个调用将执行相同实例的doSomething方法
return 0;
}
在上面的示例中,std::call_once
确保instance_
只会被初始化一次,并且这个过程是线程安全的。由于std::call_once
的存在,我们实际上并不需要std::mutex
来保护instance_
的初始化,因为std::call_once
内部已经处理了线程同步的问题。
然而,如果不使用C++11或更新的标准,你可能需要使用传统的双重检查锁定(double-checked locking)来实现线程安全的懒汉式单例模式。但请注意,双重检查锁定在C++11之前并不是完全可靠的,因为编译器和处理器可能会对内存访问进行重排序,从而破坏锁定的效果。在C++11及以后的版本中,由于引入了std::memory_order_acquire
、std::memory_order_release
等内存序,双重检查锁定可以更加可靠地实现。但是,由于std::call_once
的易用性和安全性,通常建议使用std::call_once
来实现懒汉式单例模式。