文章目录
一、简介
1.1 简介
在 C++ 中,单例模式是一种常用的设计模式,它可以确保某个类只有一个实例,并且提供一个全局访问点。
在 C++ 中,可以使用静态成员变量来实现单例模式。下面是一种常见的实现方式:
备注:静态局部变量是指在函数内部定义的静态变量。与普通的局部变量不同,静态局部变量只会被初始化一次,并且只能在定义它的函数内部访问。其生命周期是伴随着整个程序的运行周期,但其作用域只是在该函数内部。
以下是一个示例:
class Singleton {
public:
static Singleton& getInstance() {
static Singleton instance; // 使用静态局部变量保证只会初始化一次
return instance;
}
private:
// 禁止构造函数、拷贝构造函数和赋值运算符的调用
Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
// 成员变量和成员函数
};
在这个实现中,Singleton 类只有一个公有的静态成员函数 getInstance(),该函数返回一个 Singleton 类型的引用,并保证只会初始化一次。这里使用了静态局部变量来实现,在程序第一次调用该函数时会创建一个 Singleton 类的实例,之后每次调用都会返回该实例的引用。
使用时,可以通过Singleton::getInstance()来获取Singleton类的唯一实例,例如:
Singleton& s1 = Singleton::GetInstance();
为了保证单例模式的正确性,需要禁止外部通过构造函数、拷贝构造函数和赋值运算符来创建和复制对象,因此在 Singleton 类的私有部分声明了一个私有的默认构造函数,以及一个删除的拷贝构造函数和赋值运算符。
需要注意的是,虽然使用静态局部变量可以保证单例模式的正确性和线程安全性,但是在多线程环境下,还需要考虑其他线程安全问题,例如使用互斥锁或原子操作来保证线程安全。
在 C++11 中以及后,局部静态变量的初始化是线程安全的。
一个完整的示例,可以使用静态成员变量来实现单例模式:
#include <mutex>
#include <thread>
#include <cassert>
class Singleton {
public:
//获取单例对象的引用
static Singleton& GetInstance() {
//使用了一个静态局部变量 instance 来存储单例对象,这保证了它只被初始化一次
//这个局部变量是在第一次调用 GetInstance() 函数时初始化的
//在 C++11 中,局部静态变量的初始化是线程安全的
static Singleton instance;
return instance;
}
private:
//将构造函数和析构函数声明为私有的,可以防止从外部创建新的实例或销毁已有的实例
Singleton() = default;
~Singleton() = default;
//使用 delete 关键字禁止拷贝构造函数和赋值运算符重载可以防止拷贝实例
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
};
int main() {
//GetInstance() 函数返回单例对象的引用
Singleton& s1 = Singleton::GetInstance();
Singleton& s2 = Singleton::GetInstance();
//assert() 函数用于在程序运行时检查一个表达式是否为 true,
//如果表达式为 false,则程序将终止运行,并输出一条错误信息。
assert(&s1 == &s2);
return 0;
}
将构造函数和析构函数声明为私有的,可以防止从外部创建新的实例或销毁已有的实例。
使用 delete 关键字禁止拷贝构造函数和赋值运算符重载可以防止拷贝实例。
备注:在多线程环境下,需要考虑到线程安全问题。我们可以使用锁或者其他的同步机制来保证多线程下的安全性。
1.2 特性
(1)构造函数和析构函数的可见性
在 C++ 中,如果将构造函数和析构函数声明为私有的,那么只有类中的其他成员函数和友元才能够调用它们。因此,单例模式的构造函数和析构函数应该声明为私有的,以防止外部代码对其进行实例化或销毁。例如:
class Singleton {
private:
Singleton() {} // private constructor
~Singleton() {} // private destructor
};
(2)拷贝构造函数和拷贝赋值运算符的禁用
由于单例模式只允许存在一个实例,因此复制构造函数和赋值运算符都应该被禁用。否则,复制操作将会导致多个实例的存在,破坏单例模式的定义。例如:
class Singleton {
private:
Singleton(Singleton const&) = delete; // disable copy constructor
void operator=(Singleton const&) = delete; // disable assignment operator
};
(3)线程安全的实现方法
在多线程环境下,需要确保单例模式的线程安全性。可以采用懒汉式单例模式或双重检查锁定(Double-Checked Locking)单例模式来实现线程安全的单例模式。
(4)可以使用工厂模式
在某些情况下,需要在单例模式中创建多个实例,并且每个实例都具有不同的属性。这时候可以使用工厂模式,将单例模式作为工厂类,负责创建和管理多个实例。例如:
class SingletonFactory {
public:
static Singleton& getInstance(std::string name) {
static std::map<std::string, Singleton*> instances;
auto it = instances.find(name);
if(it == instances.end()) {
instances[name] = new Singleton(name);
}
return *instances[name];
}
private:
class Singleton {
public:
std::string getName() const {
return name_;
}
private:
Singleton(std::string name) : name_(name) {}
std::string name_;
};
SingletonFactory() {} // private constructor
SingletonFactory(SingletonFactory const&) = delete; // disable copy constructor
void operator=(SingletonFactory const&) = delete; // disable assignment operator
};
在这个示例中,SingletonFactory 类作为工厂类,负责创建和管理多个 Singleton 实例。getInstance() 方法接受一个名字参数,根据不同的名字创建不同的实例。Singleton 类的构造函数接受一个名字参数,用于标识不同的实例。getName() 方法返回实例的名字。
这种方式可以让单例模式更加灵活,可以创建多个不同的实例,并且每个实例都具有不同的属性。但是需要注意,单例模式的目的是确保只有一个实例被创建,因此在使用工厂模式时需要注意不要创建多个实例。
二、多线程单例模式
2.1 简介
C++ 中的单例模式默认情况下是非线程安全的,因为如果在多个线程同时调用 getInstance() 方法时,可能会导致创建多个实例的情况。
为了确保单例模式的线程安全性,可以采用以下几种方法:
(1)饿汉式单例模式:在程序启动时就创建单例对象,这样可以避免多线程同时创建实例的情况。但是这种方式会增加内存的开销,并且可能会导致程序启动变慢。
(2)懒汉式单例模式:在 getInstance() 方法中加锁,确保只有一个线程可以创建实例。但是这种方式会影响程序的性能,因为加锁会增加代码的开销。
(3)双重检查锁定(Double-Checked Locking)单例模式:在 getInstance() 方法中使用双重检查锁定,可以在保证线程安全的同时,避免加锁带来的性能影响。但是这种方式的实现比较复杂,容易出错。
(4)使用 std::call_once() 函数:在 getInstance() 方法中使用 std::call_once() 函数,可以确保只有一个线程可以创建实例,并且避免了加锁带来的性能开销。这种方式相对简单,但是需要 C++11 或以上的编译器支持。
2.2 饿汉式单例模式
在饿汉式单例模式中,单例对象在程序启动时就被创建。这种方式的实现比较简单,代码如下:
2.2.1 c++11
#include <iostream>
#include <mutex>
#include <thread>
#include <vector>
class Singleton {
public:
//获取单例对象的引用
static Singleton& GetInstance() {
//使用了一个静态局部变量 instance 来存储单例对象,这保证了它只被初始化一次
//这个局部变量是在第一次调用 GetInstance() 函数时初始化的
static Singleton instance;
return instance;
}
private:
Singleton() = default;
~Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
};
void thread_func()
{
//调用 Singleton::getInstance() 函数来获取单例对象的引用
Singleton& singleton = Singleton::GetInstance();
std::cout << "Singleton instance address: " << &singleton << std::endl;
}
int main()
{
std::vector<std::thread> threads;
const int num_threads = 10;
for (int i = 0; i < num_threads; ++i)
{
//threads将 `thread_func` 函数作为线程函数,创建多个线程并启动它们:
threads.emplace_back(thread_func);
}
for (auto& t : threads)
{
t.join();
}
return 0;
}
这种方式的优点是简单易懂,缺点是会增加内存的开销,并且可能会导致程序启动变慢。
C++11 中局部静态变量的初始化和 C++11 以前的版本是有区别的。
在 C++11 之前,局部静态变量的初始化是不可重入的,也就是说在多线程环境下可能会引发竞态条件。如果一个线程正在初始化一个静态局部变量,而另一个线程同时调用了相同的函数,那么它们都会尝试初始化这个静态局部变量,从而可能导致不可预期的结果。
而在 C++11 中,局部静态变量的初始化是线程安全的,编译器会使用互斥锁来保证只有一个线程可以进行初始化操作,其他线程会被阻塞等待初始化完成。这种实现方式可以保证线程的安全性,避免了多线程环境下的竞态条件问题。
因此,在 C++11 中使用局部静态变量实现饿汉式单例模式是线程安全的,而在 C++11 之前,需要使用其他机制来保证线程安全,比如使用懒汉式单例模式,并使用互斥锁等同步机制来避免竞态条件问题。
2.2.2 c++11以前
#include <iostream>
#include <pthread.h>
class Singleton {
public:
/* static Singleton& GetInstance() {
return instance_;
} */
static Singleton& GetInstance() {
return *instance_;
}
private:
Singleton() {}
//static Singleton instance_;
static Singleton *instance_;
};
//单例对象在程序启动时就被创建
//Singleton Singleton::instance_;
Singleton *Singleton::instance_ = new Singleton;
void* thread_func(void*) {
Singleton& singleton = Singleton::GetInstance();
std::cout << "Singleton instance address: " << &singleton << std::endl;
return NULL;
}
int main() {
const int num_threads = 10;
pthread_t threads[num_threads];
for (int i = 0; i < num_threads; ++i) {
pthread_create(&threads[i], NULL, thread_func, NULL);
}
for (int i = 0; i < num_threads; ++i) {
pthread_join(threads[i], NULL);
}
return 0;
}
在这个例子中,单例对象在程序启动时就被创建,Singleton 类使用指针 instance_ 存储单例对象,并在 GetInstance() 函数中进行判断,如果单例对象还没有被创建,则创建一个新的对象并返回其引用。在 thread_func() 函数中,调用 Singleton::GetInstance() 函数来获取单例对象的引用,并输出其地址。
需要注意的是,由于使用了指针来存储单例对象,需要注意在程序结束时进行清理,以避免内存泄漏问题。
2.3 懒汉式单例模式
在懒汉式单例模式中,单例对象在第一次被访问时才被创建。这种方式需要在 getInstance() 方法中加锁,以确保只有一个线程可以创建实例。代码如下:
#include <iostream>
#include <mutex>
#include <thread>
#include <vector>
class Singleton {
public:
static Singleton& GetInstance() {
std::lock_guard<std::mutex> lock(mutex_);
if(!instance_) {
instance_ = new Singleton();
}
return *instance_;
}
private:
Singleton() {} // private constructor
Singleton(Singleton const&) = delete; // disable copy constructor
void operator=(Singleton const&) = delete; // disable assignment operator
static Singleton* instance_;
static std::mutex mutex_;
};
Singleton* Singleton::instance_ = nullptr;
std::mutex Singleton::mutex_;
void thread_func()
{
//调用 Singleton::getInstance() 函数来获取单例对象的引用
Singleton& singleton = Singleton::GetInstance();
std::cout << "Singleton instance address: " << &singleton << std::endl;
}
int main()
{
std::vector<std::thread> threads;
const int num_threads = 10;
for (int i = 0; i < num_threads; ++i)
{
//threads将 `thread_func` 函数作为线程函数,创建多个线程并启动它们:
threads.emplace_back(thread_func);
}
for (auto& t : threads)
{
t.join();
}
return 0;
}
在这种实现方式中,我们使用了 std::mutex 类型的互斥锁来保证线程安全性。在 getInstance() 方法中,首先使用 std::lock_guard和std::mutex 对互斥锁进行加锁,然后检查是否已经创建了实例,如果没有,则在加锁的情况下创建实例。最后返回实例的引用。
这种方式的优点是比较简单易懂,缺点是加锁会影响程序的性能,尤其是在高并发的场景下。
其中 class 静态成员变量:
在 C++ 中,类的静态成员变量是被所有类对象所共享的。静态成员变量与类的成员函数一样,不属于任何一个类对象,而是属于整个类,在内存中只有一份存储空间,因此可以通过类名来访问它们。
静态成员变量可以在类的内部声明,但不能在类的内部初始化,必须在类的外部进行定义和初始化。定义和初始化的方式与普通的全局变量类似,形式为 类型名 类名::变量名 = 初始值;。例如:
class MyClass {
public:
static int count;
};
int MyClass::count = 0;
在这个例子中,类 MyClass 声明了一个名为 count 的静态成员变量,类型为 int。静态成员变量的初始化是在类的外部进行的,初始化方式为 int MyClass::count = 0;。
静态成员变量的访问方式与普通的成员变量不同,需要使用类名和作用域解析运算符 :: 来访问。例如:
std::cout << MyClass::count << std::endl;
静态成员变量的主要特点如下:
(1)静态成员变量属于整个类,而不是某个类对象;
(2)静态成员变量在内存中只有一份存储空间,所有类对象共享这份存储空间;
(3)静态成员变量必须在类的外部进行定义和初始化;
(4)静态成员变量可以通过类名和作用域解析运算符来访问。
静态成员变量的作用:
(1)共享数据:静态成员变量可以用来存储所有对象共享的数据,这些数据可以在类的各个对象之间共享,而不必为每个对象单独分配存储空间。这样可以节省内存空间,并提高程序的效率。
(2)维护类的状态:静态成员变量可以用来维护类的状态,例如记录类的实例数、全局的计数器等。这些状态可以在所有类对象之间共享,而不必为每个对象单独维护状态。
(3)常量数据:静态成员变量可以用来存储常量数据,例如 PI 常量、常量数组等。这些数据可以在类的所有对象之间共享,而且不会被修改。
(4)全局变量:静态成员变量可以在类中实现全局变量的功能。由于静态成员变量只有一个存储空间,所以可以用来在类中实现全局变量的功能,而不必在程序中定义全局变量。
总的来说,类的静态成员变量可以用来实现数据共享、状态维护、常量数据存储和全局变量等功能,它们在面向对象编程中具有重要的作用。需要注意的是,在使用静态成员变量时,需要注意它们的生命周期和作用域,以避免出现不必要的问题。
2.4 双重检查锁定(Double-Checked Locking)单例模式
双重检查锁定(Double-Checked Locking)单例模式是一种比较常用的线程安全的单例模式实现方式。它可以在保证线程安全的同时,避免加锁带来的性能开销。代码如下:
#include <iostream>
#include <mutex>
#include <thread>
#include <vector>
class Singleton {
public:
static Singleton& GetInstance() {
if(instance_ != nullptr) {
std::lock_guard<std::mutex> lock(mutex_);
if(instance_ != nullptr) {
instance_ = new Singleton();
}
}
return *instance_;
}
private:
Singleton() {} // private constructor
Singleton(Singleton const&) = delete; // disable copy constructor
void operator=(Singleton const&) = delete; // disable assignment operator
static Singleton* instance_;
static std::mutex mutex_;
};
Singleton* Singleton::instance_ = nullptr;
std::mutex Singleton::mutex_;
void thread_func()
{
//调用 Singleton::getInstance() 函数来获取单例对象的引用
Singleton& singleton = Singleton::GetInstance();
std::cout << "Singleton instance address: " << &singleton << std::endl;
}
int main()
{
std::vector<std::thread> threads;
const int num_threads = 10;
for (int i = 0; i < num_threads; ++i)
{
//threads将 `thread_func` 函数作为线程函数,创建多个线程并启动它们:
threads.emplace_back(thread_func);
}
for (auto& t : threads)
{
t.join();
}
return 0;
}
在这种实现方式中,getInstance() 方法首先检查是否已经创建了实例,如果没有,则在加锁的情况下再次检查实例是否已经被创建。如果未被创建,则在加锁的情况下创建实例。最后返回实例的引用。
这种方式的优点是比较高效,缺点是实现比较复杂,容易出错。
2.5 使用 std::call_once() 函数
std::call_once() 函数是 C++11 中提供的一种线程安全的方式来实现单例模式。这种方式相对简单,可以保证线程安全,并且不会带来加锁的性能开销。
#include <iostream>
#include <thread>
#include <vector>
#include <mutex>
class Singleton
{
public:
//使用了 std::call_once 函数,因此在多个线程同时调用时,只有一个线程会创建单例对象 instance_,即只有一个线程执行函数init()
//其他线程会直接返回之前创建的单例对象 instance_,从而保证单例对象只被创建一次
static Singleton& getInstance()
{
std::call_once(flag_, &Singleton::init);
return *instance_;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() { std::cout << "Singleton instance created.\n"; }
static void init()
{
instance_ = new Singleton();
}
//在类的定义中,一个静态成员变量必须由该类声明为static
//并且通常还需要在类外初始化,这意味着在类的定义中仅指定其类型和名称
static std::once_flag flag_;
static Singleton* instance_;
};
//在 class 外初始化 static 成员变量
std::once_flag Singleton::flag_;
Singleton* Singleton::instance_ = nullptr;
void thread_func()
{
//调用 Singleton::getInstance() 函数来获取单例对象的引用
Singleton& singleton = Singleton::getInstance();
std::cout << "Singleton instance address: " << &singleton << "\n";
}
int main()
{
std::vector<std::thread> threads;
const int num_threads = 10;
for (int i = 0; i < num_threads; ++i)
{
//threads将 `thread_func` 函数作为线程函数,创建多个线程并启动它们:
threads.emplace_back(thread_func);
}
for (auto& t : threads)
{
t.join();
}
return 0;
}
在这种实现方式中,getInstance() 方法使用 std::call_once() 函数来确保 init() 方法只会被调用一次。在 init() 方法中,创建了单例对象的实例。最后返回实例的引用。
总结
代码参考 chatgpt