特殊类设计,单例模式

Posted 两片空白

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了特殊类设计,单例模式相关的知识,希望对你有一定的参考价值。

目录

一.请设计一个类,只能在堆上创建对象

二.设计一个类,只能在栈上创建对象

三.设计一个类,不能被拷贝

四.设计一个类不能被继承

五.设计一个类,只能实例化一个对象

       5.1  单例模式由两种实现模式

        5.2 懒汉模式和饿汉模式对比


一.请设计一个类,只能在堆上创建对象

        思路:只能在堆上创建对象说明,我们不能在我们不能在外面随意创建对象,所以需要类提供一个对外接口,创建在堆上创建一个对象。这里有两个注意的点:

  1. 不能在外面随意创建对象。
  2. 类提供一个接口,在堆上创建后对象给我们。

        在类里可以调用私有成员。

不能在外面随意创建对象:

        创建对象一定需要调用类的构造函数,所以我们需要将构造函数禁掉。在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  单例模式由两种实现模式

  • 饿汉模式

        饿汉模式:是将实例的对象,在程序启动时就将对象创建出来。

        注意几点细节:

  1. 需要将构造函数,拷贝构造函数,赋值运行符重载函数屏蔽,防止在外部实例化对象。
  2. 在类中创建一个静态类对象,该静态类对象,会在程序运行时创建。需要在类外初始化。
  3. 类中写一个接口,返回静态类对象的地址,不能直接返回类对象或者是对象引用,无法调用拷贝构造。
  4. 成员函数也需要写成静态成员函数,因为类外无对象。
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 懒汉模式和饿汉模式对比

饿汉模式

优点:

  • 实现简单,不需要考虑线程安全问题。

缺点:

  • 启动慢。如果实例的对象占用资源很多,在启动时需要加载。
  • 如果多个单例类对象,在启动时实例对象的顺序不确定。如果对象之间有依赖关系,就麻烦了。

懒汉模式

优点:

  • 启动快。在需要时才会实例化对象,加载资源。
  • 多个单例类对象,实例化的顺序可以确定。取决于调用类的函数的顺序。

缺点:

  • 实现复杂。需要考虑线程安全问题。

以上是关于特殊类设计,单例模式的主要内容,如果未能解决你的问题,请参考以下文章

C++之特殊类的设计(单例模式)

C++-特殊类设计-单例模式

特殊类设计,单例模式

特殊类的设计笔试题———单例模式的类(懒汉模式,饿汉模式)

单例模式

Java设计模式--单例模式