特殊类的设计(单例模式)
请设计一个类,只能在堆上创建对象
方法一
正常情况下我们既可能在栈上有可能在堆上创建对象,现在要求只能在堆上创建对象,我们可以将析构函数设置成私有,当在栈上创建对象后,最后释放对象时会报错,因为析构函数是私有的,那么有人说堆上的对象也要调用析构函数呀,我们的解决办法是加一个公有的成员函数专门来delete,成员函数是可以访问私有的,故我们在外面释放堆上的对象时可以调用这个公有的接口
实现如下:
#include<iostream>
using namespace std;
class OnlyHeap
{
public:
void DestoryObj(OnlyHeap* ptr)
{
delete ptr;
}
void DestoryObj()
{
delete this;
}
private:
~OnlyHeap()
{
cout<<"~OnlyHeap()"<<endl;
}
int _a;
};
int main()
{
OnlyHeap oh;
OnlyHeap *ptr = new OnlyHeap;
ptr->DestoryObj();
return 0;
}
方法二
也可以将构造函数私有:
#include<iostream>
using namespace std;
class OnlyHeap
{
public:
OnlyHeap* CreateObj()
{
return new OnlyHeap;
}
static OnlyHeap* CreateObj()
{
return new OnlyHeap;
}
private:
OnlyHeap()
{
cout<<"OnlyHeap()"<<endl;
}
int _a;
};
int main()
{
OnlyHeap oh;
OnlyHeap *ptr = OnlyHeap::CreateObj();
ptr->DestoryObj();
return 0;
}
但是上面的写法还是有缺陷:
OnlyHeap copy(*ptr);
我们可以通过拷贝构造创建栈对象,所以我们可以将拷贝构造设置成私有:
#include<iostream>
using namespace std;
class OnlyHeap
{
public:
OnlyHeap* CreateObj()
{
return new OnlyHeap;
}
static OnlyHeap* CreateObj()
{
return new OnlyHeap;
}
private:
OnlyHeap()
{
cout<<"OnlyHeap()"<<endl;
}
OnlyHeap(const OnlyHeap& oh);
OnlyHeap(const OnlyHeap& oh) = delete;
int _a;
};
int main()
{
OnlyHeap oh;
OnlyHeap *ptr = OnlyHeap::CreateObj();
ptr->DestoryObj();
return 0;
}
如果是上面实现的话,就不用考虑防止拷贝构造了,因为拷贝构造出来最后也需要调用析构函数,因为析构是私有,所以也会报错,我们主要需要理解第二种方式,因为设计思路非常通用。
请设计一个类,只能在栈上创建对象
方法一
通过上面的学习,相信这个问题就变得简单了,解决方法为:将构造函数私有化,给一个公有的静态成员函数在栈上创建对象
class StackOnly
{
public:
static StackOnly CreateObj()
{
return StackOnly();
}
private:
StackOnly()
:_a(0)
{
cout<<"StackOnly()"<<endl;
}
int _a;
}
int main()
{
StackOnly so;
StackOnly* ptr = new StackOnly;
StackOnly::CreateObj();
return 0;
}
方法二
还有一种方法是重载一个类专属的operator new,将它设置成私有,因为new创建对象时会调用operator new申请空间。
class StackOnly
{
public:
StackOnly()
:_a(0)
{
cout<<"StackOnly()"<<endl;
}
private:
void* operator new(size_t size);
void* operator delete(void* ptr);
void* operator new(size_t size) = delete;
void* operator delete(void* ptr) = delete;
int _a;
}
int main()
{
StackOnly so;
StackOnly* ptr = new StackOnly;
return 0;
}
我们这里将operator new限制住,这种方式存在一些漏洞,因为无法禁止在静态区创建对象:
static StackOnly sso;
请设计一个类,不能被拷贝
拷贝只会放生在两个场景中:拷贝构造函数以及赋值运算符重载,因此想要让一个类禁止拷贝,只需让该类 不能调用拷贝构造函数以及赋值运算符重载即可。
C++98
C++98的实现方式:
class CopyBan
{
private:
CopyBan(const CopyBan&);
CopyBan& operator=(const CopyBan&);
};
为什么设置成私有:如果只声明没有设置成private,用户自己如果在类外定义了,就可以不能禁止拷贝 了
为什么只声明不定义:不定义是因为该函数根本不会调用,定义了其实也没有什么意义,不写反而还简 单,而且如果定义了就不会防止成员函数内部拷贝了。
C++11
C++11扩展delete的用法,delete除了释放new申请的资源外,如果在默认成员函数后跟上=delete,表 示让编译器删除掉该默认成员函数。
class CopyBan
{
CopyBan(const CopyBan&)=delete;
CopyBan& operator=(const CopyBan&)=delete;
};
请设计一个类,不能被继承
C++98中我们将父类构造函数设置成私有,父类可以使用静态成员函数创建对象,这样可以解决这个问题,但是这种方式不够直接,没有直接禁止继承,这里其实是可以继承的,但是Derive不能创建对象,因为Derive的构造函数必须要调用父类NonInherit的构造函数,但是NonInherit构造函数是私有,在子类中不可见,那么这里继承不会报错,继承的子类创建对象就会报错
class NonInherit
{
public:
static NonInherit GetInstance()
{
return NonInherit();
}
private:
NonInherit()
{}
};
class Derive : NonInherit
{};
int main()
{
return 0;
}
C++11的实现方法:我们可以在父类后面加一个关键字final,这样直接就禁止了继承
class NonInherit final
{};
class Derive : NonInherit
{};
int main()
{
return 0;
}
C++11的不能被继承的方式,简单明了直接
请设计一个类,只能创建一个对象(单例模式)
设计模式
设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的总结。为什么会产生设计模式这样的东西呢?就像人类历史发展会产生兵法。最开始部落之间打仗时都是人拼人的对砍。后来春秋战国时期,七国之间经常打仗,就发现打仗也是有套路的,后来孙子就总结出了《孙子兵法》。孙子兵法也是类似。 使用设计模式的目的:为了代码可重用性、让代码更容易被他人理解、保证代码可靠性。 设计模式使代码编写真正工程化;设计模式是软件工程的基石脉络,如同大厦的结构一样。
单例模式
一个类只能创建一个对象,即单例模式,该模式可以保证系统中该类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息,这种方式简化了在复杂环境下的配置管理。
单例模式的本质是去管理全局对象,定义一个全局对象,大家都能用也能保证单例,但是这种方式存在很大的缺陷,你要让大家都能用,这个对象你只能定义在一个.h,如果这个.h在包含在多个.cpp,链接会报错
Singleton.h
#include<vector>
std::vector<int> v;
void f1();
Singleton.cpp
#include"Singleton.h"
void f1()
{
v.push_back(1);
v.push_back(2);
v.push_back(3);
for(auto e :v)
{
cout<< e <<" ";
}
cout<<endl;
}
test.cpp
#include<Singleton.h>
void f2()
{
v.push_back(11);
v.push_back(22);
v.push_back(33);
for(auto e :v)
{
cout<< e <<" ";
}
cout<<endl;
}
int main()
{
f1();
f2();
return 0;
}
这样编译会报错,为什么呢?全局链接时会发现有两个命名v,会产生二义性,所以会报错,为了不报错,我们这样解决:
#include<vector>
void f1();
static std::vector<int> v;
将v定义成全局静态,每个文件独自拥有v,但是不再是同一个对象,每个.cpp中各自是一个对象,但是这样会有多个vector对象了,那么怎么让不同的CPP访问到同一个vector呢?
将声明和定义分离,在.h里面声明,在.cpp里面定义就可以解决
注意不能在.h里面定义,这样的话所有包了头文件的地方都会有定义,这就重复定义了
.h文件对他声明:
#include<vector>
#nclude<iostream>
using namespace std;
std::vector<int> v;
void f1();
extern vector<int> v;
extern关键字可以置于变量或函数前,以标示变量或者函数的定义在别的文件中,提示编译器遇到此变量和函数时在其它模块寻找其定义
.cpp文件进行定义:
#include<Singleton.h>
void f1()
{
v.push_back(11);
v.push_back(22);
v.push_back(33);
for(auto e :v)
{
cout<< e <<" ";
}
cout<<endl;
}
vector<int> v;
这样此时只会有一个vector的对象了。
单例模式的实现
全局只有唯一的实例对象,那么他里面的成员也就是单例的。static修饰成员变量保证了全局只有一个唯一一个。
饿汉模式
饿汉模式:在进入main函数之前就创建对象
- 构造函数私有化,保证不能随意创建对象
- 类里面声明一个static Singleton对象(static修饰的成员全局就一个),在.cpp文件定义这个对象,声明和定义分离
- 提供一个获取单例对象的static成员函数
定义需要放在.cpp文件中:
Singleton Singleton::_sinst;
Singleton& Singleton::GetInstance()
{
return _sinst;
}
.h文件
class Singleton
{
public:
static Singleton& GetInstance();
void PushBack(int x)
{
_v.push_back(x);
}
private:
Singleton()
{}
private:
vector<int> _v;
static Singleton _sinst;
};
int main()
{
Singleton::GetInstance();
return 0;
}
但是我们上面设置的类还有缺陷,当前类可以拷贝构造,所以将拷贝构造也设置成私有:
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
饿汉模式缺陷:单例对象是main函数之前创建初始化的
-
如果单例对象的构造函数中要做很多工作,可能会导致程序启动慢 -
如果多个单例类,并且它们之间有依赖关系,那么饿汉模式无法保证
假设有三个单例类,要求三个之间有依赖关系,需要Singleton1先生成才能有Singleton2,需要有Singleton2才能有Singleton3:
那么饿汉模式无法保证,全局的静态变量在初始化时不能保证谁先初始化谁后初始化,为了更好的控制这些问题,有人就给出懒汉模式:
懒汉模式
第一次调用GetInstance时,才会创建初始化单例对象,相对于饿汉,不存在可能会导致启动慢的问题,也可以控制顺序依赖的问题了
.cpp文件里定义:
Singleton* Singleton::_spinst = nullptr;
Singleton& Singleton::GetInstance()
{
if(_spinst == nullptr)
{
_spinst = new Singleton;
}
return *_spinst;
}
class Singleton
{
public:
static Singleton& GetInstance();
void PushBack(int x)
{
_v.push_back(x);
}
private:
Singleton()
{}
private:
vector<int> _v;
static Singleton* _spinst;
};
我们现在这样写还是有问题的,有线程安全问题。假设有t1,t2两个线程,当_spinst不为nullptr,t1线程进去还没创建对象,而t2线程来了,此时t2也判断_spinst不为nullptr,也去创建对象了,此时就保证不了单例
所以我们需要再给一个成员mutex进行加锁:
class Singleton
{
public:
static Singleton& GetInstance();
void PushBack(int x)
{
_v.push_back(x);
}
private:
Singleton()
{}
private:
vector<int> _v;
static Singleton* _spinst;
static mutex _mtx;
};
需要注意的是_mtx在.cpp文件中定义,不然链接时会出错:
Singleton* Singleton::_spinst = nullptr;
mutex Singleton:: _mtx;
Singleton& Singleton::GetInstance()
{
_mtx.lock();
if(_spinst == nullptr)
{
_spinst = new Singleton;
}
_mtx.unlock();
return *_spinst;
}
但是这样加锁后还会有问题,我们访问这段代码有两种情况:
1、同时有多个线程进来
2、不同时
但是当_spinst不为nullptr了以后,其他线程来了还是要加锁解锁,效率低,那么能这样加锁吗?
Singleton* Singleton::_spinst = nullptr;
mutex Singleton:: _mtx;
Singleton& Singleton::GetInstance()
{
if(_spinst == nullptr)
{
_mtx.lock();
_spinst = new Singleton;
_mtx.unlock();
}
return *_spinst;
}
不能,这样会存在线程安全问题,当第一个线程来了后,_spinst不为空,进去加锁创建对象了,在第一个线程还没有创建对象时,第二个线程来了,也判断_spinst不为空,但是此时有锁他进不去,当第一个线程创建完线程后解锁后,第二个线程也来了,但是此时_spinst已经不为空,再创建就有问题了
所以我们进行双检查提高效率,保证了除了第一个线程需要获取锁之外,其他线程都不需要获取锁,直接返回对象,既能保证线程安全又能提高效率
Singleton* Singleton::_spinst = nullptr;
mutex Singleton:: _mtx;
Singleton& Singleton::GetInstance()
{
if(_spinst == nullptr)
{
_mtx.lock();
if(_spinst == nullptr)
{
_spinst = new Singleton;
}
_mtx.unlock();
}
reurn *_spinst;
}
其次,懒汉是可以主动去释放的:
static void DelInstance();
static void DelInstance()
{
if(_spinst != nullptr)
{
_mtx.lock();
if(_spinst!=nullptr)
{
delete _spinst;
_spinst = nullptr;
}
_mtx.unlock();
}
}
|