1.C++ 中实现单例模式:饿汉式和懒汉式。
C++ 中的单例设计模式是一种创建型设计模式,它确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。这种设计模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
C++ 中实现单例模式主要有两种方式:饿汉式和懒汉式。
- 饿汉式:
饿汉式是在类加载时就完成了初始化,所以类加载较慢,但获取对象的速度快。它是线程安全的,在程序启动时或在单例类的静态初始化时创建单例实例。
示例代码:
class Singleton {
private:
static Singleton* instance;
Singleton() {} // 私有构造函数
public:
static Singleton* getInstance() {
return instance;
}
// 其他成员函数...
};
// 初始化静态成员变量
Singleton* Singleton::instance = new Singleton();
在这个例子中,instance
在程序加载时就被初始化,因此被称为“饿汉式”。
(这种方式典型但不纯正,纯正的看下文3.0使用&引用的方式)
- 懒汉式:
懒汉式是类加载速度快,但运行时获取对象的速度慢。它是线程不安全的(如果需要线程安全,需要加锁处理),在第一次调用时初始化,因此被称为“懒汉式”。
示例代码:
class Singleton {
private:
static Singleton* instance;
Singleton() {} // 私有构造函数
public:
static Singleton* getInstance() {
if (instance == nullptr) {
instance = new Singleton();
}
return instance;
}
// 其他成员函数...
};
// 初始化静态成员变量
Singleton* Singleton::instance = nullptr;
在这个例子中,instance
在第一次调用 getInstance()
时才被初始化。需要注意的是,在多线程环境下,这种实现方式可能会导致创建多个实例,因此需要通过加锁等机制来保证其线程安全性。如果需要线程安全的懒汉式单例,通常会使用双重检查锁定(Double Checked Locking)等机制。
总的来说,饿汉式和懒汉式各有优缺点,选择哪种方式取决于具体的应用场景和需求。饿汉式在程序启动时创建实例,因此不存在线程安全问题,但可能会浪费资源。而懒汉式则在需要时才创建实例,更加节省资源,但需要注意线程安全问题。
2.1普通的懒汉式单例模式实现
普通的懒汉式单例模式不包含线程安全的处理,这意味着在多线程环境中可能会出现问题,因为多个线程可能同时尝试创建单例实例。不过,在单线程环境中,或者你确定在多线程环境中不会有问题的情况下(例如,你控制了单例的访问,并确保它在任何时间点只被一个线程访问),这种简单的懒汉式单例是可以接受的。
下面是一个普通的懒汉式单例模式的实现:
class SimpleLazySingleton {
private:
static SimpleLazySingleton* instance;
SimpleLazySingleton() {}
SimpleLazySingleton(const SimpleLazySingleton&) = delete;
SimpleLazySingleton& operator=(const SimpleLazySingleton&) = delete;
public:
static SimpleLazySingleton* getInstance() {
if (instance == nullptr) {
instance = new SimpleLazySingleton();
}
return instance;
}
};
// 初始化静态成员变量
SimpleLazySingleton* SimpleLazySingleton::instance = nullptr;
int main() {
// 获取单例对象的指针
SimpleLazySingleton* singleton = SimpleLazySingleton::getInstance();
// 使用单例对象...
// 注意:由于这是普通的懒汉式单例,所以在多线程环境下可能不安全。
// 在实际应用中,如果涉及多线程,请考虑线程安全的单例实现。
return 0;
}
在这个实现中,getInstance()
方法会检查 instance
是否为 nullptr
。如果是,它会创建一个新的 SimpleLazySingleton
实例。由于这种方法没有使用任何同步机制,因此在多线程环境中可能会导致创建多个实例,从而违反单例模式的初衷。
如果你打算在多线程环境中使用懒汉式单例,你应该考虑使用前面提到的线程安全实现,比如通过添加互斥锁来保护对 instance
的访问。在单线程环境或者你确定单例的访问是序列化的情况下,这个简单的实现就足够了。
2.2线程安全的懒汉式单例模式的实现(单一锁和双重锁)
懒汉式单例模式的核心思想是“延迟初始化”,即单例对象在第一次被需要时才被创建。为了在多线程环境下保证单例的唯一性和线程安全,通常需要加入同步机制来防止多个线程同时创建单例对象。
单一的锁
下面是一个线程安全的懒汉式单例模式的实现,使用了C++11中的std::mutex
来保证线程安全:
#include <mutex>
class LazySingleton {
private:
static LazySingleton* instance;
static std::mutex mutex_;
LazySingleton() {}
LazySingleton(const LazySingleton&) = delete;
LazySingleton& operator=(const LazySingleton&) = delete;
public:
static LazySingleton* getInstance() {
std::lock_guard<std::mutex> lock(mutex_); // 使用锁保护共享资源
if (instance == nullptr) {
instance = new LazySingleton();
}
return instance;
}
};
// 初始化静态成员变量
LazySingleton* LazySingleton::instance = nullptr;
std::mutex LazySingleton::mutex_;
int main() {
// 获取单例对象的指针
LazySingleton* singleton = LazySingleton::getInstance();
// 使用单例对象...
// 注意:在实际应用中,通常不会在外部删除单例对象。
// 如果确实需要在程序结束时清理资源,应该提供一个合适的销毁机制。
return 0;
}
在这个实现中,LazySingleton::getInstance()
方法在每次调用时都会检查单例对象是否已经创建。如果没有创建,则使用 new
关键字创建一个新的 LazySingleton
对象,并将其地址赋值给静态成员变量 instance
。为了保证在多线程环境下的线程安全,使用了 std::mutex
和 std::lock_guard
来保护对 instance
的访问和修改。
请注意,这个实现中并没有提供删除单例对象的机制。在实际应用中,单例对象的生命周期通常与程序的生命周期相同,因此不需要显式地删除它。如果确实需要在某个时刻销毁单例对象,应该谨慎地设计一个安全的销毁机制,并确保在销毁后不会再有其他代码尝试访问该对象。
另外,这个实现中使用了C++11的特性。如果你的编译器不支持C++11或更高版本,你可能需要使用其他同步机制(如 pthread_mutex_t
)来实现线程安全。
双重检查锁定(Double-Checked Locking, DCL)(更高效)
双重检查锁定通常用于在多线程环境中优化懒汉式单例模式的初始化,以避免不必要的锁开销。
双重检查锁定的大致代码模式如下:
class LazySingleton {
private:
static LazySingleton* instance;
static std::atomic<bool> initialized; // 使用原子标志位来表示是否已经初始化
static std::mutex mutex_;
// ... 其他成员和方法 ...
public:
static LazySingleton* getInstance() {
if (!initialized.load(std::memory_order_acquire)) { // 第一次检查
std::lock_guard<std::mutex> lock(mutex_);
if (!instance) { // 第二次检查
instance = new LazySingleton();
initialized.store(true, std::memory_order_release);
}
}
return instance;
}
};
// ... 初始化静态成员变量 ...
在双重检查锁定中,首先会检查instance
是否已经被初始化(第一次检查),如果没有被初始化,则尝试获取锁,并在持有锁的情况下再次检查instance
(第二次检查)。这样做是为了避免在instance
已经被其他线程初始化之后仍然获取锁。
但是,要注意的是,双重检查锁定在C++11之前可能由于内存顺序问题而不起作用,因为编译器和处理器可能会重新排序读写操作。C++11引入了std::memory_order
来允许程序员明确指定内存顺序,从而确保双重检查锁定可以正确地工作。然而,在C++11及之后的版本中,更推荐使用std::call_once
或std::once_flag
结合局部静态变量的方法来实现线程安全的单例初始化,因为这种方法更简单且不需要显式管理锁。
3.(纯正的)饿汉单例模式实现
下面是一个纯正的饿汉式单例设计模式的示例。在饿汉式单例模式中,单例对象在程序开始时或在单例类首次被加载到内存中时就被创建,因此不存在线程安全问题,因为静态初始化在多线程环境中是安全的。
class HungrySingleton {
private:
// 私有静态成员变量,在类加载时就被初始化
static HungrySingleton instance;
// 私有构造函数,防止外部使用 new 运算符创建对象
HungrySingleton() {}
// 禁止拷贝构造函数和赋值操作符
HungrySingleton(const HungrySingleton&) = delete;
HungrySingleton& operator=(const HungrySingleton&) = delete;
public:
// 提供一个全局的静态方法,返回单例对象的引用
static HungrySingleton& getInstance() {
return instance;
}
};
// 初始化静态成员变量(注意没有使用new)
HungrySingleton HungrySingleton::instance;// 饿汉式,静态初始化
int main() {
// 获取单例对象的引用
HungrySingleton& singleton = HungrySingleton::getInstance();
// 使用单例对象...
return 0;
}
在这个饿汉式单例模式中,HungrySingleton::instance
是一个静态成员变量,它会在类首次被加载到内存中时自动创建,从而保证了单例对象的唯一性和线程安全性。注意,由于该对象在程序启动时就被创建,因此如果构造函数的执行耗时较长或者依赖于某些运行时信息,那么这种方法可能不是最佳选择。此时,可以考虑使用懒汉式单例或者其他更复杂的单例实现方式。
另外,请注意,在C++11及以后的版本中,可以使用更现代的方式来实现单例模式,例如使用局部静态变量、std::call_once
和 std::once_flag
等工具来确保线程安全的单例初始化。
4.饿汉单例为啥用引用,懒汉单例为啥用用指针?
在饿汉式单例设计模式中,我使用了引用(&
)返回单例对象,而在懒汉式单例设计模式中,我使用了指针(*
)返回单例对象的地址。这主要是因为两种模式在对象创建和访问方式上的差异。
饿汉式单例
在饿汉式单例中,单例对象在程序开始时就已经被创建,因此我们可以安全地返回一个引用。由于对象已经存在,我们不需要担心对象为空或者尚未被创建的情况。使用引用可以更加直观地表达我们返回的是一个已经存在的对象,而不是一个指向对象的指针。此外,引用比指针更安全,因为它不能被重新赋值指向其他对象。
懒汉式单例
在懒汉式单例中,单例对象是在第一次调用 getInstance()
方法时才被创建的。因此,在创建对象之前,我们需要有一个方式来表示对象尚未存在,这就是为什么使用指针的原因。指针可以被初始化为 nullptr
,表示当前还没有单例对象。当第一次调用 getInstance()
时,我们检查指针是否为 nullptr
,如果是,则创建对象并让指针指向它。之后,每次调用 getInstance()
都会返回这个已经创建的对象的地址。
总的来说,使用引用或指针主要取决于对象的创建时机和访问方式。在饿汉式中,由于对象在程序开始时就已经存在,所以可以直接返回引用。而在懒汉式中,由于对象的创建是延迟的,所以需要使用指针来表示对象可能尚未存在的情况。
5.饿汉单例如何销毁呢?
在饿汉式单例模式中,单例对象的生命周期通常与程序的生命周期相同,因此,在大多数情况下,你并不需要显式地销毁它。单例对象在程序启动时自动创建,并在程序结束时由操作系统自动回收。
然而,如果你确实需要在某个特定时刻销毁单例对象(尽管这并不常见),你可以考虑以下几种方法:
-
提供销毁接口:
你可以在单例类中提供一个静态的销毁方法,用于释放单例对象并重置静态指针。但这种方法需要谨慎使用,因为它可能会引入线程安全问题,特别是在多线程环境中。 -
使用智能指针:
如果你使用C++11或更高版本,可以考虑使用std::unique_ptr
或std::shared_ptr
等智能指针来管理单例对象的生命周期。这样,当智能指针超出作用域或被重置时,它会自动删除所指向的对象。但这种方法可能会使单例模式的实现变得复杂,并可能引入额外的性能开销。 -
利用程序结束时的清理:
通常,当程序结束时,操作系统会自动回收分配给程序的所有资源,包括内存。因此,在大多数情况下,你不需要显式地销毁单例对象。如果你确实需要在程序结束时执行一些清理操作(如关闭文件、释放其他资源等),你可以考虑在main
函数的末尾或程序的退出处理代码中执行这些操作。 -
使用局部静态对象:
另一种实现饿汉式单例的方法是使用局部静态对象。这种方法利用了C++的局部静态对象初始化特性,即局部静态对象在第一次被使用时进行初始化,且其生命周期贯穿整个程序运行期间。这种方法不需要显式销毁对象,因为局部静态对象的销毁是由编译器自动处理的。
请注意,无论你选择哪种方法,都需要确保在销毁单例对象后不会再有其他代码尝试访问它,否则会导致未定义行为。
最后,需要强调的是,在大多数情况下,你并不需要显式地销毁饿汉式单例对象。如果你确实需要这样做,请确保你完全理解了相关的风险和后果,并采取了适当的同步措施来防止多线程环境中的竞态条件。
下面是一个简单的示例,展示了如何在单例类中添加一个销毁接口(但请注意,这种方法通常不推荐):
class HungrySingleton {
private:
static HungrySingleton* instance;
// ... 其他成员和方法 ...
public:
static HungrySingleton* getInstance() {
if (!instance) {
instance = new HungrySingleton();
}
return instance;
}
static void destroyInstance() {
delete instance;
instance = nullptr;
}
};
// 初始化静态成员变量
HungrySingleton* HungrySingleton::instance = nullptr;
在这个例子中,destroyInstance
方法提供了显式销毁单例对象的能力。但请记住,在调用 destroyInstance
之后,任何对 getInstance
的后续调用都将返回一个空指针,除非再次创建实例。这可能会导致程序中的其他问题,因此需要谨慎处理。