特殊类设计,单例模式
Posted 两片空白
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了特殊类设计,单例模式相关的知识,希望对你有一定的参考价值。
目录
一.请设计一个类,只能在堆上创建对象
思路:只能在堆上创建对象说明,我们不能在我们不能在外面随意创建对象,所以需要类提供一个对外接口,创建在堆上创建一个对象。这里有两个注意的点:
- 不能在外面随意创建对象。
- 类提供一个接口,在堆上创建后对象给我们。
在类里可以调用私有成员。
不能在外面随意创建对象:
创建对象一定需要调用类的构造函数,所以我们需要将构造函数禁掉。在C++98中,是将构造函数设为成私有。构造函数不能使用C++11的delete,new还需要调用构造函数。
我们不仅需要将构造函数禁用,还需要将拷贝构造函数禁用,因为在对创建对象后,还可以拷贝构造对象。
为了不麻烦,拷贝构造只声明,不定义,构造函数不能只声明,new对象需要调用构造函数。
类提供一个接口,在堆上创建后对象给我们:
该成员函数需要设为静态成员函数,由于创建对象需要使用类中的成员函数,在类外没有类对象,如果需要调用成员函数,必须将函数设为静态成员函数,属于整个类。
#include<iostream>
using namespace std;
class HeadOnly{
public:
//提供一个接口在堆上创建对象返回
static HeadOnly* CreateObj(){
return new HeadOnly();
}
//C++11,构造函数不能delete掉,new还需要调用构造函数
//HeadOnly(const HeadOnly& hd) = delete;
private:
//C++98将构造设为私有,new还需要调用构造函数
HeadOnly()
{
}
//C++98将拷贝构造声明成私有
HeadOnly(const HeadOnly& hd);
};
int main(){
HeadOnly* hd = HeadOnly::CreateObj();
return 0;
}
二.设计一个类,只能在栈上创建对象
- 思路一:同上
不能在外面在堆上创建对象,可以在栈上创建对象
在堆上创建对象,new的时候,会调用构造函数,我们需要将构造函数在外面禁用。即,将构造函数设为私有。
构造函数私有,在类外就不能直接构造对象。但是可以在类内调用构造。
不能将拷贝构造函数禁用。成员函数返回需要拷贝构造对象。
类提供一个接口,在栈上创建好对象给我们:
该成员函数需要设为静态成员函数,理由同上,在外无对象,需要将成员函数设置属于整个类。
class StackOnly{
public:
//提供一个接口在栈上创建对象返回
static StackOnly CreateObj(){
return StackOnly();
}
private:
//C++98将构造设为私有,new还需要调用构造函数
StackOnly()
{
}
};
int main(){
//需要调用拷贝构造
StackOnly st = StackOnly::CreateObj();
return 0;
}
- 思路二:将new屏蔽
new底层调用的是全局函数operator new函数,只需要将operator new函数屏蔽即可。
如何屏蔽operator new?
在类中自定义operator new,并且设置为私有,就不会调用全局的operator new函数了。
但是这有一个缺陷,可以在静态区定义对象。
class StackOnly{
public:
private:
//自定义operator new函数
void* operator new(size_t size)
{};
};
int main(){
StackOnly st1;
//缺陷,对象定义在静态区
static StackOnly st2;
return 0;
}
三.设计一个类,不能被拷贝
拷贝主要是两个方面,拷贝构造函数和赋值运算符重载函数。因此只需要将两个函数禁用即可。
class CopyBan{
public:
//C++11,设置为delete
//CopyBan(CopyBan& cb) = delete;
//CopyBan& operator=(const CopyBan& cb) = delete;
private:
//C++98,设置为私有
CopyBan(CopyBan& cb);
CopyBan& operator=(const CopyBan& cb);
};
四.设计一个类不能被继承
- C++98中将构造函数设置为私有,派生类调不到基类的构造函数,无法继承。
class NoInherit{
public:
//提供一个接口在栈上创建对象返回
static NoInherit CreateObj(){
return NoInherit();
}
private:
NoInherit()
{
}
};
- C++11,将类用final关键字修饰,不能被继承
class NoInherit final{
//...
};
五.设计一个类,只能实例化一个对象
单例模式:一个类只能实例化一个对象。
该模式可以保证系统中该类只有一个实例,并提供一个访问它的全局访问点。
比如:某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个实例对象统一读取,然后服务进程的其它对象再通过这个单例对象获取这些配置文件,这种方式简化了在复杂环境下的配置管理。
5.1 单例模式由两种实现模式
- 饿汉模式
饿汉模式:是将实例的对象,在程序启动时就将对象创建出来。
注意几点细节:
- 需要将构造函数,拷贝构造函数,赋值运行符重载函数屏蔽,防止在外部实例化对象。
- 在类中创建一个静态类对象,该静态类对象,会在程序运行时创建。需要在类外初始化。
- 类中写一个接口,返回静态类对象的地址,不能直接返回类对象或者是对象引用,无法调用拷贝构造。
- 成员函数也需要写成静态成员函数,因为类外无对象。
class SingelTon{
public:
static SingelTon* GetInstance(){
return &st;
}
private:
//创建一个静态对象
static SingelTon st;
//将构造函数,拷贝构造,赋值重载运算符屏蔽
SingelTon(){};
SingelTon(const SingelTon& st);
SingelTon& operator=(const SingelTon& st);
};
SingelTon SingelTon::st;//初始化
int main(){
SingelTon *st = SingelTon::GetInstance();
return 0;
}
- 懒汉模式
懒汉模式:是在需要使用时再创建对象。
懒汉模式注意的细节:
- 需要将构造函数,拷贝构造函数,赋值运行符重载函数屏蔽,防止在外部实例化对象。
- 在类中成员包含一个类对象指针,而不是对象。
- 类中写一个接口,当未创建类对象时,创建类对象返回。
static SingelTon* GetInstance(){
if (st == nullptr){//对象未创建时,才会创建对象
st = new SingelTon();
}
return st;
}
- 成员函数也需要写成静态成员函数,因为类外无对象。
懒汉模式的线程安全问题:
当st为空,一个线程进入判断,在未创建对象之前,另外一个线程进入判断,由于未创建对象,判断成功,也可以进入判断代码。此时在堆上申请了两块空间,但是st只是指向一块空间,在对象销毁时,只会释放一块空间,找出其它线程申请的空间没有释放,导致内存泄漏。
在多线程的情况下,由于判断不是原子的,并且st属于临界资源。为了保存当一个线程进行判断和申请资源时,其它线程不会进入,需要在判断前加锁,在申请完资源后解锁。
锁也需要定义成静态,属于整个类。让线程看到的是一把锁。
static SingelTon* GetInstance(){
mt.lock();
if (st == nullptr){//对象未创建时,才会创建对象
st = new SingelTon();
}
mt.unlock();
return st;
}
优化:
由于只要一个线程申请了对象,其它线程就不需要在再进行加锁,解锁了。加锁会导致线程阻塞,加锁解锁也需要代价,为了提高效率,在加锁前在进行判断,是否申请过对象了。
static SingelTon* GetInstance(){
if (st != nullptr){//防止频繁加锁,影响效率
mt.lock();
if (st == nullptr){//对象未创建时,才会创建对象
st = new SingelTon();
}
mt.unlock();
}
return st;
}
整体代码:
#include<mutex>
class SingelTon{
private:
SingelTon(){};
SingelTon(const SingelTon& st);
SingelTon& operator=(const SingelTon& st);
//声明一个类指针
static SingelTon* st;
static mutex mt;//锁
public:
//需要时创建对象
static SingelTon* GetInstance(){
if (st != nullptr){//防止频繁加锁,影响效率
mt.lock();
if (st == nullptr){//对象未创建时,才会创建对象
st = new SingelTon();
}
mt.unlock();
}
return st;
}
//析构
~SingelTon(){
//将空间释放
if (st){
delete st;
}
}
};
//静态成员,在类外初始化
SingelTon* SingelTon::st = nullptr;
mutex SingelTon::mt;
int main(){
SingelTon *st = SingelTon::GetInstance();
return 0;
}
ps:这里可能申请资源时new可能会申请失败,抛异常。导致锁未释放。我们可以利用RAII思想,unique_lock()来管理锁资源。
static SingelTon* GetInstance(){
if (st != nullptr){//防止频繁加锁,影响效率
unique_lock<mutex>(mt);//使用unique_lock管理锁防止new抛异常
if (st == nullptr){//对象未创建时,才会创建对象
st = new SingelTon();
}
}
return st;
}
5.2 懒汉模式和饿汉模式对比
饿汉模式
优点:
- 实现简单,不需要考虑线程安全问题。
缺点:
- 启动慢。如果实例的对象占用资源很多,在启动时需要加载。
- 如果多个单例类对象,在启动时实例对象的顺序不确定。如果对象之间有依赖关系,就麻烦了。
懒汉模式
优点:
- 启动快。在需要时才会实例化对象,加载资源。
- 多个单例类对象,实例化的顺序可以确定。取决于调用类的函数的顺序。
缺点:
- 实现复杂。需要考虑线程安全问题。
以上是关于特殊类设计,单例模式的主要内容,如果未能解决你的问题,请参考以下文章