特殊类设计 ------ 设计模式

目录

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

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

(1)方法1

(2)方法2 

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

4.设计一个类,不能被继承

(1)C++98

(2)C++11

5.设计一个类,只能创建一个对象(单例模式)

(1)单例模式

(2)如何保证全局(一个进程中) 只有一个唯一实例对象

(3)饿汉模式

(4)懒汉模式

(5)饿汉模式和懒汉模式对比

(6)其他版本的懒汉 

(7)单例对象的释放


 

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

①只能在堆上创建对象,即只能通过new操作符创建对象:

  • 将构造函数设置为私有,防止外部直接调用构造函数在栈上创建对象。
  • 向外部提供一个获取对象的static接口,该接口在堆上创建一个对象并返回。
  • 将拷贝构造函数设置为私有,并且只声明不实现,防止外部调用拷贝构造函数在栈上创建对象。

                 

 ②代码

class HeapOnly
{
public:
	//2、提供一个获取对象的接口,并且该接口必须设置为静态成员函数,外部能够访问
	static HeapOnly* CreateObj()
	{
		return new HeapOnly;
	}

private:
	//1、将构造函数设置为私有
	HeapOnly()
	{}

	//3、将拷贝构造函数设置为私有,并且只声明不实现
    --拷贝构造,只声明不实现(实现也可以,但是没必要)
    --如果拷贝构造放到 public ,这里如果有人搞坏事,它在外面给你实现了,就不行了

	//C++98
	HeapOnly(const HeapOnly&);

	//C++11 --这里就是把这个函数给禁了,不管你是public / private
	//HeapOnly(const HeapOnly&) = delete;
};

                 

③补充

  • 向外部提供的CreateObj函数必须设置为静态成员函数,因为外部调用该接口就是为了获取对象的,而非静态成员函数必须通过对象才能调用。
  • C++98通过将拷贝构造函数声明为私有以达到防拷贝的目的,C++11可以在拷贝构造函数后面加上=delete,表示让编译器将拷贝构造函数删除,此时也能达到防拷贝的目的。

                

                

                

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

(1)方法1

①基本思路

  • 将构造函数设置为私有,防止外部直接调用构造函数在堆上创建对象。
  • 向外部提供一个获取对象的static接口,该接口在栈上创建一个对象并返回
class StackOnly
{
public:
	//2、提供一个获取对象的接口,并且该接口必须设置为静态成员函数
	static StackOnly CreateObj()
	{
		return StackOnly();
	}
private:

	//1、将构造函数设置为私有
	StackOnly()
	{}
};

                                 

②该方法有一个缺陷就是,无法防止外部调用拷贝构造函数创建对象

  • 但是我们不能将构造函数设置为私有,也不能用=delete 的方式将拷贝构造函数删除,因为CreateObj函数当中创建的是局部对象,返回局部对象的过程中势必需要调用拷贝构造函数。
StackOnly obj1 = StackOnly::CreateObj();
static StackOnly obj2(obj1); //在静态区拷贝构造对象
StackOnly* ptr = new StackOnly(obj1); //在堆上拷贝构造对象

                        

(2)方法2 

  •  屏蔽operator new函数和operator delete函数。
class StackOnly
{
public:
	StackOnly()
	{}
private:
	//C++98
	void* operator new(size_t size);
	void operator delete(void* p);

	//C++11
	//void* operator new(size_t size) = delete;
	//void operator delete(void* p) = delete;
};

                 

 ①new和delete的原理:

  • new在堆上申请空间实际分为两步,第一步是调用operator new函数申请空间,第二步是在申请的空间上执行构造函数,完成对象的初始化工作。
  • delete在释放堆空间也分为两步,第一步是在该空间上执行析构函数,完成对象中资源的清理工作,第二步是调用operator delete函数释放对象的空间。

                         

②new和delete默认调用的是全局的operator new函数和operator delete函数,但如果一个类重载了专属的operator new函数和operator delete函数,那么new和delete就会调用这个专属的函数。所以只要把operator new函数和operator delete函数屏蔽掉,那么就无法再使用new在堆上创建对象了。

                         

③该方法也有一个缺陷,就是无法防止外部在静态区创建对象。

static StackOnly obj; //在静态区创建对象

                

        

                

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

  • 要让一个类不能被拷贝,就要让该类不能调用拷贝构造函数和赋值运算符重载函数,因此直接将该类的拷贝构造函数和赋值运算符重载函数设置为私有,或者用C++11的方式将这两个函数删除即可。
class CopyBan
{
public:
	CopyBan()
	{}
private:
	//C++98
	CopyBan(const CopyBan&);
	CopyBan& operator=(const CopyBan&);

	//C++11
	//CopyBan(const CopyBan&) = delete;
	//CopyBan& operator=(const CopyBan&) = delete;
};

                

                

        

4.设计一个类,不能被继承

(1)C++98

  • 将该类的构造函数设置为私有即可,因为子类的构造函数被调用时,必须调用父类的构造函数初始化父类的那一部分成员,但父类的私有成员在子类当中是不可见的,所以在创建子类对象时子类无法调用父类的构造函数对父类的成员进行初始化,因此该类被继承后子类无法创建出对象。
  •  这种方法有缺陷,不够彻底 , 这里的不能继承指的是子类继承后,子类不能创建对象
class NonInherit
{
public:
	static NonInherit CreateObj()
	{
		return NonInherit();
	}
private:
	//将构造函数设置为私有
	NonInherit()
	{}
};

class B :public NonInherit //这里是没问题的
{};

                         

(2)C++11

  • C++98的这种方式其实不够彻底,因为这个类仍然可以被继承(编译器不会报错),只不过被继承后无法实例化出对象而已。
  • C++11中提供了final关键字,被final修饰的类叫做最终类,最终类无法被继承,此时就算继承后没有创建对象也会编译出错。
class NonInherit final
{
	//...
};

                        

                

                

5.设计一个类,只能创建一个对象(单例模式)

(1)单例模式

  • 单例模式是一种设计模式(Design Pattern),设计模式就是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式的目的就是为了可重用代码、让代码更容易被他人理解、保证代码可靠性程序的重用性。
  • 单例模式指的就是一个类只能创建一个对象,该模式可以保证系统中该类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。
  • 比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象同一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息,这种方式简化了在复杂环境下的配置管理。

                 

(2)如何保证全局(一个进程中) 只有一个唯一实例对象

  • 构造函数私有 , 拷贝和赋值要防拷贝禁掉
  • 提供一个GetInstance获取单例对象 

                         

(3)饿汉模式

①实现方式

  • 将构造函数设置为私有,并将拷贝构造函数和赋值运算符重载函数设置为私有或删除,防止外部创建或拷贝对象。
  • 提供一个静态指向单例对象的成员指针,初始化时new一个对象给它,程序开始main执行之前就创建单例对象。
  • 提供一个全局访问点获取单例对象。

                 

 ②代码

class Singleton
{
public:
	//3、提供一个全局访问点获取单例对象
	static Singleton* GetInstance()
	{
		return _inst;
	}

private:
	//1、将构造函数设置为私有,并防拷贝
	Singleton()
	{}
	Singleton(const Singleton&) = delete;
	Singleton& operator=(const Singleton&) = delete;

	//2、提供一个指向单例对象的static指针
    -为什么定义成静态的? 静态成员函数没有this指针,只能访问静态成员变量
	static Singleton* _inst;
};

//在程序入口之前完成单例对象的初始化
Singleton* Singleton::_inst = new Singleton;

                         

③线程安全相关问题

  • 饿汉模式在程序运行主函数之前就完成了单例对象的创建,由于main函数之前是不存在多线程的,因此饿汉模式下单例对象的创建过程是线程安全的。
  • 后续所有多线程要访问这个单例对象,都需要通过调用GetInstance函数来获取,这个获取过程是不需要加锁的,因为这是一个读操作。
  • 当然,如果线程通过GetInstance获取到单例对象后,要用这个单例对象进行一些线程不安全的操作,那么这时就需要加锁了。
     

                         

(4)懒汉模式

①实现方式

  • 将构造函数设置为私有,并将拷贝构造函数和赋值运算符重载函数设置为私有或删除,防止外部创建或拷贝对象。
  • 提供一个指向单例对象的static指针,并在程序入口之前先将其初始化为空。
  • 提供一个全局访问点获取单例对象。

                 

②代码

class Singleton
{
public:
	//3、提供一个全局访问点获取单例对象
	static Singleton* GetInstance()
	{
		//双检查
        -保护第一次需要加锁,后面都不需要加锁的场景,可以使用双检查加锁
        -特定:第一次加锁,后面不加锁,保护线程安全,同时提高效率
		if (_inst == nullptr)
		{
			_mtx.lock(); //加锁只有第一次有意义,后面再有线程来没必要加锁,加锁会引发效率低下
			if (_inst == nullptr)
			{
				_inst = new Singleton;
			}
			_mtx.unlock();
		}
		return _inst;
	}

private:
	//1、将构造函数设置为私有,并防拷贝
	Singleton()
	{}
	Singleton(const Singleton&) = delete;
	Singleton& operator=(const Singleton&) = delete;

	//2、提供一个指向单例对象的static指针
	static Singleton* _inst;
	static mutex _mtx; //互斥锁
};

//在程序入口之前先将static指针初始化为空
Singleton* Singleton::_inst = nullptr;
mutex Singleton::_mtx; //初始化互斥锁

                         

③ 线程安全相关问题

  • 懒汉模式在程序运行之前没有进行单例对象的创建,而是等到某个线程需要使用这个单例对象时再进行创建,也就是GetInstance函数第一次被调用时创建单例对象。
  • 因此在调用GetInstance函数获取单例对象时,需要先判断这个static指针是否为空,如果为空则说明这个单例对象还没有创建,此时需要先创建这个单例对象然后再将单例对象返回。
  • GetInstance函数第一次调用时需要对static指针进行写入操作,这个过程不是线程安全的,因为多个线程可能同时调用GetInstance函数,如果不对这个过程进行保护,此时这多个线程就会各自创建出一个对象。

                         

④双检查加锁:

  • 对GetInstance函数中创建单例对象的过程进行保护,本质就是需要引入互斥锁,最简单的加锁方式就是在进行if判断之前加锁,在整个if语句之后进行解锁。
  • 实际只有GetInstance函数第一次被调用,创建单例对象时需要使用互斥锁进行保护,而后续调用GetInstance函数获取单例对象只是一个读操作,是不需要使用互斥锁进行保护的。
  • 如果简单的将加锁解锁操作放到if语句前后,那么在后续调用GetInstance函数获取已经创建好的单例对象时,就会进行大量无意义的加锁解锁操作,导致线程不断切入切出,进而影响程序运行效率。
  • 对于这种只有第一次需要加锁保护的场景可以使用双检查加锁,双检查就是在当前加锁和解锁的外面再进行一次if判断,判断static指针是否为空。
  • 这样一来,后续调用GetInstance函数获取已经创建好的单例对象时,外层新加的if判断就会起作用,这样就避免了后续无意义的加锁解锁操作。
  • 即第一次调用GetInstance函数是为了对单例对象指针进行修改,需要加锁;后面的调用只需要读取单例对象即可,不会修改,不用加锁,通过简单的逻辑判断即可。
     

                 

 (5)饿汉模式和懒汉模式对比

  • 饿汉模式的优点就是简单,但是它的缺点也比较明显。饿汉模式在程序运行主函数之前就会创建单例对象,如果单例类的构造函数中所做的工作比较多,就会导致程序迟迟无法进入主函数,在外部看来就好像是程序卡住了。
  • 此外,如果有多个单例类需要创建单例对象,并且它们之间的初始化存在某种依赖关系,比如单例对象A的创建必须在单例对象B之后,此时饿汉模式也会存在问题,因为我们无法保证这多个单例对象中的哪个对象先创建。
  • 而懒汉模式就能很好的解决上述饿汉模式的缺点,因为懒汉模式并不是一开始就完成单例对象的创建,因此不会导致程序迟迟无法进入主函数,并且懒汉模式中各个单例对象创建的顺序是由各个单例类中的GetInstance函数第一次被调用的顺序决定,因此是可控制的。
  • 懒汉模式的缺点就是,在编码上比饿汉模式复杂,在创建单例对象时需要考虑线程安全的问题。
     

                 

(6)其他版本的懒汉 

①实现方式

  • 将构造函数设置为私有,并将拷贝构造函数和赋值运算符重载函数设置为私有或删除,防止外部创建或拷贝对象。
  • 提供一个全局访问点获取单例对象。

②代码 

class Singleton
{
public:
	//2、提供一个全局访问点获取单例对象
	static Singleton* GetInstance()
	{
		static Singleton inst;
		return &inst;
	}

private:
	//1、将构造函数设置为私有,并防拷贝
	Singleton()
	{}
	Singleton(const Singleton&) = delete;
	Singleton& operator=(const Singleton&) = delete;
};

                         

③在单例类的GetInstance函数中定义一个静态的单例对象并返回。

  • 由于实际只有第一次调用GetInstance函数时才会定义这个静态的单例对象,这也就保证了全局只有这一个唯一实例。
  • 并且这里单例对象的定义过程是线程安全的,因为现在的C++标准保证多线程初始化static变量不会发生数据竞争,可以视为原子操作。
  • 该方法属于懒汉模式,因为局部静态变量不是在程序运行主函数之前初始化的,而是在第一次调用GetInstance函数时初始化的

                        

④这种版本的懒汉主要有如下缺点:

  • 单例对象定义在静态区,因此太大的单例对象不适合使用这种方式。
  • 单例对象创建在静态区后没办法主动释放。
     

                 

(7)单例对象的释放

  • 单例对象创建后一般在整个程序运行期间都可能会使用,所以我们可以不考虑单例对象的释放,程序正常结束时OS会自动回收。
class Singleton
{ 
private:  //一般情况下单例 的类是不考虑释放的,最后OS会回收的
	Singleton()
	{}

	~Singleton()
	{
		// 程序结束时,需要处理一下,持久化保存一些数据 : 例如文件
	}
}

                 

在单例类中编写一个DelInstance函数,在该函数中进行单例对象的释放动作,当不再需要该单例对象时就可以主动调用DelInstance释放单例对象。

static void DelInstance()
{
	_mtx.lock();
	if (_inst != nullptr)
	{
		delete _inst;
		_inst = nullptr;
	}
	_mtx.unlock();
}

 

在单例类中实现一个内嵌的垃圾回收类 , 在垃圾回收类的析构函数中完成单例对象的释放。在单例类中定义一个静态的垃圾回收类对象,当该对象被消耗时就会调用其析构函数,这时便对单例对象进行了释放。

//垃圾回收类
class CGarbo
{
public:
	~CGarbo()
	{
		if (_inst != nullptr)
		{
			delete _inst;
			_inst = nullptr;
		}
	}
};

                        

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值