【C++】单例模式(饿汉模式、懒汉模式)
C++单例模式总结与剖析
饿汉单例模式 C++实现
C++单例模式(饿汉式)
设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的总结
,一共有23种经典设计模式
使用设计模式的目的:为了代码可重用性、让代码更容易被他人理解、保证代码可靠性
设计模式使代码编写真正工程化,设计模式是软件工程的基石脉络,如同大厦的结构一样
单例模式是设计模式中最常用的一种模式,一个类只能创建一个对象,即单例模式,该模式可以保证系统中该类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享
基础要点:
- 全局只有一个实例:static 特性,同时禁止用户自己声明并定义实例(把构造函数设为 private)
- 线程安全
- 禁止赋值和拷贝
- 用户通过接口获取实例:使用 static 类成员函数
单例的实现主要有饿汉式和懒汉式两种,分别进行介绍
饿汉式不管你将来用不用,程序启动时就创建一个唯一的实例对象
优点:简单
缺点:可能会导致进程启动慢,且如果有多个单例类对象实例启动顺序不确定
示例代码:
// Hunger_Singleton_pattern
// Created by lei on 2022/05/13
#include
#include
using namespace std;
class Example
{
public:
typedef shared_ptr<Example> Ptr;
static Ptr GetSingleton()
{
cout << "Get Singleton" << endl;
return single;
}
void test()
{
cout << "Instance location:" << this << endl;
}
~Example() { cout << "Deconstructor called" << endl; };
private:
static Ptr single;
Example() { cout << "Constructor called" << endl; };
Example &operator=(const Example &examp) = delete;
Example(const Example &examp) = delete;
};
Example::Ptr Example::single = shared_ptr<Example>(new Example);
int main()
{
Example::Ptr a = Example::GetSingleton();
Example::Ptr b = Example::GetSingleton();
a->test();
b->test();
cout << "main end" << endl;
return 0;
}
打印输出:
Constructor called
Get Singleton
Get Singleton
Instance location:0x55d43c9dce70
Instance location:0x55d43c9dce70
main end
Deconstructor called
可以看到拷贝构造函数只调用了一次,并且两个对象内存地址相同,说明该类只能实例化一个对象
饿汉单例模式的静态变量的初始化由C++完成,规避了线程安全问题,所以饿汉单例模式是线程安全的
在大多数情况下使用饿汉单例模式是没有问题的
有缺陷的懒汉模式懒汉式(Lazy-Initialization)的方法是直到使用时才实例化对象,也就说直到调用get_instance() 方法的时候才 new 一个单例的对象, 如果不被调用就不会占用内存
// Defect_Lazy_Singleton_pattern
// Created by lei on 2022/05/13
#include
#include
using namespace std;
class Singleton
{
private:
Singleton()
{
cout << "constructor called!" << endl;
}
Singleton(const Singleton &) = delete;
Singleton& operator=(const Singleton &) = delete;
static Singleton *m_instance_ptr;
public:
~Singleton()
{
cout << "destructor called!" << endl;
}
static Singleton *get_instance()
{
if (m_instance_ptr == nullptr)
{
m_instance_ptr = new Singleton;
}
return m_instance_ptr;
}
void use() const { cout << "in use" << endl; }
};
Singleton *Singleton::m_instance_ptr = nullptr; //静态成员变量类内声明类外初始化
int main()
{
Singleton *instance = Singleton::get_instance();
Singleton *instance_2 = Singleton::get_instance();
// thread t1(Singleton::get_instance);
// thread t2(Singleton::get_instance);
// thread t3(Singleton::get_instance);
// thread t4(Singleton::get_instance);
// t1.join();
// t2.join();
// t3.join();
// t4.join();
return 0;
}
打印输出:
constructor called!
取了两次类的实例,却只有一次类的构造函数被调用,表明只生成了唯一实例,这是个最基础版本的单例实现,存在以下问题
1、当多线程获取单例时有可能引发竞态条件:第一个线程在if中判断 m_instance_ptr
是空的,于是开始实例化单例;同时第2个线程也尝试获取单例,这个时候判断m_instance_ptr
还是空的,于是也开始实例化单例;这样就会实例化出两个对象
2、类中只负责new出对象,却没有负责delete对象,因此只有构造函数被调用,析构函数却没有被调用,因此会导致内存泄漏
改进的懒汉模式对应上面两个问题,有以下解决方法:
1、用mutex加锁
2、使用智能指针
// Improve_Lazy_Singleton_pattern
// Created by lei on 2022/05/13
#include
#include // shared_ptr
#include // mutex
#include
using namespace std;
class Singleton
{
public:
typedef shared_ptr<Singleton> Ptr;
~Singleton()
{
cout << "destructor called!" << endl;
}
Singleton(const Singleton &) = delete;
Singleton& operator=(const Singleton &) = delete;
static Ptr get_instance()
{
// "double checked lock"
if (m_instance_ptr == nullptr)
{
lock_guard<mutex> lk(m_mutex);
if (m_instance_ptr == nullptr)
{
m_instance_ptr = shared_ptr<Singleton>(new Singleton);
}
}
return m_instance_ptr;
}
private:
Singleton()
{
cout << "constructor called!" << endl;
}
static Ptr m_instance_ptr;
static mutex m_mutex;
};
// initialization static variables out of class
Singleton::Ptr Singleton::m_instance_ptr = nullptr;
mutex Singleton::m_mutex;
int main()
{
Singleton::Ptr instance = Singleton::get_instance();
Singleton::Ptr instance2 = Singleton::get_instance();
// thread t1(Singleton::get_instance);
// thread t2(Singleton::get_instance);
// thread t3(Singleton::get_instance);
// thread t4(Singleton::get_instance);
// t1.join();
// t2.join();
// t3.join();
// t4.join();
return 0;
}
打印输出:
constructor called!
destructor called!
只构造了一次实例,并且发生了析构
缺陷是双检锁依然会失效,具体原因可以看下面的文章
推荐的懒汉模式https://www.drdobbs.com/cpp/c-and-the-perils-of-double-checked-locki/184405726
// Recommand_Lazy_Singleton_pattern
// Created by lei on 2022/05/13
#include
#include
using namespace std;
class Singleton
{
public:
~Singleton()
{
cout << "destructor called!" << endl;
}
Singleton(const Singleton &) = delete;
Singleton &operator=(const Singleton &) = delete;
static Singleton &get_instance()
{
static Singleton instance;
return instance;
}
private:
Singleton()
{
cout << "constructor called!" << endl;
}
};
int main()
{
Singleton &instance_1 = Singleton::get_instance();
Singleton &instance_2 = Singleton::get_instance();
// thread t1(Singleton::get_instance);
// thread t2(Singleton::get_instance);
// thread t3(Singleton::get_instance);
// thread t4(Singleton::get_instance);
// t1.join();
// t2.join();
// t3.join();
// t4.join();
return 0;
}
打印输出:
constructor called!
destructor called!
这种方法又叫做 Meyers’ Singleton Meyer’s的单例, 是著名的写出《Effective C++》系列书籍的作者 Meyers 提出的。所用到的特性是在C++11标准中的Magic Static特性:
If control enters the declaration concurrently while the variable is being initialized, the concurrent execution shall wait for completion of the initialization
如果当变量在初始化的时候,并发同时进入声明语句,并发线程将会阻塞等待初始化结束
这是最推荐的一种单例实现方式:
- 通过局部静态变量的特性保证了线程安全
- 不需要使用共享指针,代码简洁
- 注意在使用的时候需要声明单例的引用
Single&
才能获取对象
C++11于once flag,call_once:分析的实现
C++11实现线程安全的单例模式(使用std::call_once)
在多线程编程中,有一个常见的情景是某个任务仅仅须要运行一次
在C++11中提供了非常方便的辅助类once_flag与call_once
once_flag和call_once的声明:
struct once_flag
{
constexpr once_flag() noexcept;
once_flag(const once_flag&) = delete;
once_flag& operator=(const once_flag&) = delete;
};
template<class Callable, class ...Args>
void call_once(once_flag& flag, Callable&& func, Args&&... args);
} // std
简单示例:
// once_flag and call_once simple example
// Created by lei on 2022/05/13
#include
using namespace std;
once_flag flag;
void do_once()
{
call_once(flag, [&]()
{ cout << "Called once" << endl; });
}
int main()
{
std::thread t1(do_once);
std::thread t2(do_once);
std::thread t3(do_once);
std::thread t4(do_once);
t1.join();
t2.join();
t3.join();
t4.join();
}
打印输出:
Called once
可以看到4个线程只执行了一次do_once( )函数
call_once实现单例模式// Call_once_Singleton_pattern
// Created by lei on 2022/05/13
#include
#include
#include
using namespace std;
once_flag cons_flag;
class A
{
public:
typedef shared_ptr<A> Ptr;
void m_print() { cout << "m_a++ = " << ++m_a << endl; }
static Ptr getInstance(int a)
{
cout << "Get instance" << endl;
if (m_instance_ptr == nullptr)
{
lock_guard<mutex> m_lock(m_mutex);
if (m_instance_ptr == nullptr)
{
call_once(cons_flag, [&]()
{ m_instance_ptr.reset(new A(a)); });
}
}
return m_instance_ptr;
}
~A()
{
cout << "Deconstructor called" << endl;
}
private:
static mutex m_mutex;
int m_a;
static Ptr m_instance_ptr;
A(int a_) : m_a(a_)
{
cout << "Constructor called" << endl
<< "m_a = " << m_a << endl;
}
A &operator=(const A &A_) = delete;
A(const A &A_) = delete;
};
A::Ptr A::m_instance_ptr = nullptr;
mutex A::m_mutex;
void test(int aa)
{
cout << "Go in test..." << endl;
A::Ptr tp = A::getInstance(aa);
cout << "tp location:" << tp << endl;
tp->m_print();
cout << endl;
}
int main()
{
thread t1(test, 1);
thread t2(test, 2);
thread t3(test, 3);
thread t4(test, 4);
t1.join();
t2.join();
t3.join();
t4.join();
cout << "main end..." << endl;
return 0;
}
打印输出:
Go in test...
Get instance
Constructor called
m_a = 4
tp location:0x7fd964000f30
m_a++ = 5
Go in test...
Get instance
tp location:0x7fd964000f30
m_a++ = 6
Go in test...
Get instance
tp location:0x7fd964000f30
m_a++ = 7
Go in test...
Get instance
tp location:0x7fd964000f30
m_a++ = 8
main end...
Deconstructor called
看到构造函数只调用了一次,并且类A实例化对象的地址始终相同
上面的两个示例程序中都用到了lambda表达式,call_once通常结合lambda一起使用
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)