目录
[2.1 懒汉模式实现单例模式](#2.1 懒汉模式实现单例模式)
[2.2 饿汉模式实现单例模式](#2.2 饿汉模式实现单例模式)
一、了解单例模式前的基础题
1、设计一个类,不能被拷贝
拷贝只会发生在两个场景中:拷贝构造函数以及赋值运算符重载,因此 想要让一个类禁止拷贝, 只需让该类不能调用拷贝构造函数以及赋值运算符重载即可 。
C++98
将拷贝构造函数与赋值运算符重载只声明不定义 ,并且将其访问权限设置为私有即可。
cppclass CopyBan { // ... private: CopyBan(const CopyBan&); CopyBan& operator=(const CopyBan&); //... };
原因:
- 设置成私有:如果只声明没有设置成 private ,用户自己如果在类外定义了,就可以不
能禁止拷贝了- 只声明不定义:不定义是因为该函数根本不会调用,定义了其实也没有什么意义,不写
反而还简单,而且如果定义了就不会防止成员函数内部拷贝了。
C++11
C++11 扩展 delete 的用法, delete 除了释放 new 申请的资源外,如果在默认成员函数后跟上
=delete ,表示让编译器删除掉该默认成员函数。
cppclass CopyBan { // ... CopyBan(const CopyBan&)=delete; CopyBan& operator=(const CopyBan&)=delete; //... };
2、设计一个类,只能在堆上创建对象
思路:
创建对象一定调用构造函数(或拷贝构造:两种禁用方式 ),故先禁掉构造函数(私有化构造函数,对于拷贝构造下面有两种方式),则放在类中的私有 下,但是怎么在堆上创建对象?用一个成员函数GetObj来在堆上创建对象 (因为类内能访问私有成员中的构造函数,类外不可以),那为什么要用static 修饰GetObj?这样就可以用类名::GetObj来访问了,而不用创建一个对象才能访问GetObj(因为你调用GetObj之前创建的对象一定是在栈上的),此外拷贝构造要设置为私有或直接=delete,即不能使用,因为存在你拷贝构造一个栈上的对象的场景(即用拷贝构造来创建对象)
步骤:
- 将类的构造函数私有 ,拷贝构造声明为delete,防止别人调用拷贝在栈上生成对象。
- 提供一个静态的成员函数,在该静态成员函数中完成堆对象的创建
cpp
class HeapOnly
{
public:
static HeapOnly* GetObj()
{//专门用来在堆上创建对象
return new HeapOnly;
//注意这里是返回HeapOnly指针,那只是指针的拷贝,
//而不是对象的拷贝,故不会调用构造函数
}
//C++11:防拷贝:拷贝构造函数声明成delete
HeapOnly(const HeapOnly&) = delete;
private:
//构造函数私有化
HeapOnly()
{}
//C++98防拷贝:拷贝构造函数声明为私有
//HeapOnly(const HeapOnly&);
};
int main()
{
//HeapOnly hp; //在栈上创建,失败
HeapOnly* p = HeapOnly::GetObj();//成功【创建一个在堆上的对象】
std::shared_ptr<HeapOnly> sp1(HeapOnly::GetObj());
std::shared_ptr<HeapOnly> sp2(HeapOnly::GetObj());
//HeapOnly copy(*sp1);//用栈上的对象来拷贝构造copy是不行的,故要禁掉拷贝构造
return 0;
}
3、设计一个类,只能在栈上创建对象
和只能在堆上创建对象思路的唯一区别在于: 创建的栈的对象 需传值返回,对象的拷贝要调用拷贝构造函数, 所以不能禁掉拷贝构造函数
cpp
class StackOnly
{
public:
static StackOnly CreateObj()
{
//因为返回个匿名对象,传值返回,会调用拷贝构造
//故不能禁掉拷贝构造
return StackOnly();
}
private:
StackOnly()
{}
};
int main()
{
StackOnly obj = StackOnly::CreateObj();
//StackOnly* ptr3 = new StackOnly; //失败
}
下面是有缺陷的代码:
下面的代码只是禁掉了在堆区创建数据,但是在静态区创建的数据 还是无法阻止
cpp
//该方案存在一定程度缺陷,无法阻止在数据段(静态区)创建对象
class StackOnly
{
public:
// 禁掉operator new可以把下面用new 调用拷贝构造申请对象给禁掉
void* operator new(size_t size) = delete;
};
int main()
{
StackOnly so;
//new分为operator new + 构造函数
//StackOnly* ptr3 = new StackOnly(obj); //失败
static StackOnly sso;//在静态区上开辟成功
return 0;
}
4、设计一个类,不能被继承
C++98方式
cpp// C++98中构造函数私有化,派生类中调不到基类的构造函数。则无法继承 class NonInherit { public: static NonInherit GetInstance() { return NonInherit(); } private: NonInherit() {} };
C++11方式
final 关键字, final 修饰类,表示该类不能被继承。
cppclass A final { // .... };
二、单例模式
1、单例模式的概念
设计模式:
设计模式( Design Pattern )是一套 被反复使用、多数人知晓的、经过分类的、代码设计经验的 总结。为什么会产生设计模式这样的东西呢?就像人类历史发展会产生兵法。最开始部落之间打仗时都是人拼人的对砍。后来春秋战国时期,七国之间经常打仗,就发现打仗也是有 套路的,后来孙子就总结出了《孙子兵法》。孙子兵法也是类似。
使用设计模式的目的:为了代码可重用性、让代码更容易被他人理解、保证代码可靠性。 设计模式使代码编写真正工程化;设计模式是软件工程的基石脉络,如同大厦的结构一样。
单例模式:
一个类只能创建一个对象,即单例模式,该模式可以保证系统中该类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息,这种方式简化了在复杂环境下的配置管理。
单例模式有两种实现模式:懒汉模式和饿汉模式
2、单例模式的两种实现方式
设计一个类,只能创建一个对象(单例模式)
那么题目的意思就是保证全局只有一个实例对象
①、单例模式大体结构(有缺陷)
cpp
class Singleton
{
public:
static Singleton* GetInstance()
{
if (_pinst == nullptr)
{//因为是静态成员变量,除了第一次为nullptr
//再进来不是nullptr了,直接返回_pinst即可
_pinst = new Singleton;
}
return _pinst;
}
private:
Singleton()
{}
Singleton(const Singleton& s) = delete;
static Singleton* _pinst;//静态成员的声明
};
Singleton* Singleton::_pinst = nullptr;//静态成员的定义
int main()
{
//Singleton s1; //失败
//保证获取的对象每次都是同一个
cout << Singleton::GetInstance() << endl;
cout << Singleton::GetInstance() << endl;
cout << Singleton::GetInstance() << endl;
//Singleton copy(*Singleton::GetInstance());//无法调用拷贝构造
return 0;
}
运行结果:
上面代码缺陷是线程安全问题 :如果两个线程同时要new一个对象 (即_pinst = new Singleton),这时就发生错误了 ,我们要的是只有一个对象,故引出锁解决
那我们先看看这种错误出现的场景:
为了防止线程跑太快而达不到我们想看到错误情况的效果,我们用sleep睡眠来辅助
为解决上面的问题,我们用锁
2.1 懒汉模式实现单例模式
①、错误代码1
cpp
//懒汉模式:第一次获取对象时,再创建对象
class Singleton
{
public:
static Singleton* GetInstance()
{
_mtx.lock();
if (_pinst == nullptr)
{//因为是静态成员变量,除了第一次为nullptr
//再进来不是nullptr了,直接返回_pinst即可
_pinst = new Singleton;
}
_mtx.unlock();
return _pinst;
}
Singleton(const Singleton& s) = delete;
private:
Singleton()
{}
static Singleton* _pinst;//静态成员的声明
static mutex _mtx;
};
Singleton* Singleton::_pinst = nullptr;//静态成员的定义
mutex Singleton::_mtx;
上面代码意义是创建对象保证只有一个线程在访问,解决了不会同时创建对象的问题,但是如果new失败了要抛异常怎么办 ?此时正在访问的线程都没有解锁,其他线程也无法访问了,故要用**unique_lock:**也会帮你锁,且不管你是否主动unlock解锁,都会在出了作用域后解锁
②、用unique_lock来改进
③、只需第一次加锁
只要_pinst指向已经new出来的实例对象,就无须加锁了
④、析构单例模式的对象
一般单例模式下的new出来的这个全局唯一对象是不需要释放的,因为这种单例模式下的对象,整个程序只有一个,它是一直在用的,没必要释放。
如果你就想要释放的话,两种方式:
①、静态函数
②、静态变量的生命周期
cpp
#include<vector>
#include<thread>
#include<mutex>
namespace lazy_man
{
//懒汉模式:第一次获取对象时,再创建对象
class Singleton
{
public:
static Singleton* GetInstance()
{
//_mtx.lock();
unique_lock会锁,锁完之后不管你是否解锁,出了作用域他都会自动解锁
而你现在就这一个地方需要锁,故再加个{}作用域
//{
// unique_lock<mutex> lock(_mtx);
// if (_pinst == nullptr)
// {//因为是静态成员变量,除了第一次为nullptr
// //再进来不是nullptr了,直接返回_pinst即可
// _pinst = new Singleton;
// }
//}
//双检查:
if (_pinst == nullptr)
{
//加锁只是为了保护第一次
{
unique_lock<mutex> lock(_mtx);
if (_pinst == nullptr)
{//因为是静态成员变量,除了第一次为nullptr
//再进来不是nullptr了,直接返回_pinst即可
_pinst = new Singleton;
//只要_pinst指向已经new出来的实例对象,就无须加锁了
}
}
}
//_mtx.unlock();
return _pinst;
}
//如果你就想释放这个对象的话,自己写个静态函数即可,手动调
static void DelInstance()
{
unique_lock<mutex> lock(_mtx);
delete _pinst;
_pinst = nullptr;
}
Singleton(const Singleton& s) = delete;
private:
Singleton()
{}
static Singleton* _pinst;//静态成员的声明
static mutex _mtx;
};
Singleton* Singleton::_pinst = nullptr;//静态成员的定义
mutex Singleton::_mtx;
//1、如果要手动释放单例对象,可以调用DelInstance
//2、如果需要程序结束时,正常释放单例对象,可以加入下面的设计
class GC
{
public:
~GC()
{
Singleton::DelInstance();
}
};
static GC gc;//main函数结束就会调用它的析构函数,进而释放_pinst
void x()
{
Singleton s1; //失败
保证获取的对象每次都是同一个
//cout << Singleton::GetInstance() << endl;
//cout << Singleton::GetInstance() << endl;
//cout << Singleton::GetInstance() << endl;
Singleton copy(*Singleton::GetInstance());//无法调用拷贝构造
//代码中存在线程问题:若多个线程同时获取一个对象呢?
vector<std::thread> vthreads;
int n = 4;
for (size_t i = 0; i < n; ++i)
{
vthreads.push_back(std::thread([]()
{
//cout << std::this_thread::get_id() << ":";
cout << Singleton::GetInstance() << endl;
}));//线程对象里面用了一个lambda表达式
}
for (auto& t : vthreads)
{
t.join();
}
}
}
int main()
{
lazy_man::x();
return 0;
}
运行结果:
2.2 饿汉模式实现单例模式
饿汉模式 有个静态成员变量,静态变量在程序运行前创建,在程序的整个运行期间始终存在,他始终保持原先的值, 除非给他赋予一个不同的值或程序结束。正因为程序前创建,那此时只有主线程,不存在线程安全问题。
cpp
namespace hungry_man
{
//饿汉模式 --main函数之前就创建对象
class Singleton
{
public:
static Singleton* GetInstance()
{
return &_inst;
}
Singleton(const Singleton& s) = delete;
private:
Singleton()
{}
static Singleton _inst;
};
//static对象是在main函数之前创建的,这时只有主线程,故不存在线程安全问题
Singleton Singleton::_inst;
void x()
{
Singleton s1; //失败
保证获取的对象每次都是同一个
//cout << Singleton::GetInstance() << endl;
//cout << Singleton::GetInstance() << endl;
//cout << Singleton::GetInstance() << endl;
Singleton copy(*Singleton::GetInstance());//无法调用拷贝构造
//代码中存在线程问题:若多个线程同时获取一个对象呢?
vector<std::thread> vthreads;
int n = 4;
for (size_t i = 0; i < n; ++i)
{
vthreads.push_back(std::thread([]()
{
//cout << std::this_thread::get_id() << ":";
cout << Singleton::GetInstance() << endl;
}));//线程对象里面用了一个lambda表达式
}
for (auto& t : vthreads)
{
t.join();
}
}
}
int main()
{
hungry_man::x();
return 0;
}
2.3懒汉和饿汉模式的区别