C++从面试常考实现特殊类到单例模式的实现

目录

前言

一.如何设计一个类只能在堆上创建对象

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

三.如何设计一个类不能拷贝

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

五. 单例模式从理论到实现

六. 总结前文


前言

在面试的时候经常会遇到一些特殊类设计的题目. 这些题目其实也暗含了一些设计模式, 我们要想限制对象的构造, 最容易想到的方式当然就是先限制构造函数, 然后我们来提供特殊的构造对象的接口. 就像是单例模式一样。。。。。  

然鹅, 在构造出来对象之前我们是没有对象的, 如何调用我们自己设计的限制性的创建一个对象的函数呢???   将函数静态化, 这样这个函数是属于整个类的, 使用类名 + ::就可以实现访问了

  • 私有化构造函数    +   禁止掉拷贝构造, 不让其进行拷贝构造
  • 禁掉拷贝构造的方式方法.  1. 私有化拷贝构造( C++98 )   2. delete关键字修饰( C++11)
  • 构造函数私有化了, 但是还是需要可以构造出对象, 提供GetInstance 公有接口限制性的创建对象
  • 公有接口静态化, 使得可以通过  类名访问  (解决 如何调用问题)

一.如何设计一个类只能在堆上创建对象

  • 私有化构造函数
  • 禁掉拷贝构造
  • 提供类公有静态接口函数 限制性的只能在堆上创建
class HeapOnly {
public:
	static HeapOnly* CreateObj() {
		return new HeapOnly;
	}
private:
	HeapOnly() {};	
	//方式1: 防止拷贝构造
	HeapOnly(const HeapOnly& h);
public:
	//方式2 :防止拷贝构造
	HeapOnly(const HeapOnly& h) = delete;
};

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

  • 只能在栈上创建, 其实就是禁止掉在堆上和全局数据段创建的对象
  • 此处 除了使用上述的方式, 私有化然后提供一个  GetInstance 接口的方式以外,还存在如下的方式方法
    	void* operator new(size_t size) = delete;
    	void operator delete(void* p) = delete;

回忆杀.  我们在  new 一个对象的时候的本质:

  • 调用 全局的  operator new 分配内存
  • 调用构造函数初始化

delete一个对象的本质:

  • 调用析构函数做结束处理, 可能是写磁盘呀, 关闭文件等等操作
  • 调用 全局的  operator delete 释放资源
class StackOnly {
public:
	static StackOnly CreateObj() {
		return StackOnly();		//构造一个临时对象拷贝返回
	}
    //因为存在临时对象的返回, 存在拷贝构造, 所以没有必要也不可以禁止掉拷贝构造。。
    //思考 ? 和上述堆区创建对象不一样之处
private:
	StackOnly() {};
    //方式2: 禁止堆区构造必须调用的函数
	//void* operator new(size_t size) = delete;
	//void operator delete(void* p) = delete;
};
  • 使用禁止掉 oprator new 和 operator delete的缺陷所在, 仅仅限制了在堆区创建对象, 但是在全局数据段, 也就是静态 static 这个是没有禁止掉的..

三.如何设计一个类不能拷贝

class CopyBan {
public:
	CopyBan(int a = 0): _a(a) {
    //C++11的方式
	//CopyBan(const CopyBan& obj) = delete;
	//CopyBan& operator= (const CopyBan& obj) = delete;
private:
	//C++98方式
	CopyBan(const CopyBan& obj);
	CopyBan& operator= (const CopyBan& obj);
	int _a;
};

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

  • 方式1 : 将父类的构造函数进行一个私有化,这样继承不会报错, 但是子类无法调用父类构造函数, 也就没法复用父类的代码。。。 实例化的时候报错....  因为仅仅在实例化的时候才会真正调用父类构造函数, 才能发现调用不了    
  • 方式2: C++11方式, 来了一个 final 关键字 修饰类, 表示这个类是最终类了, 自然也就不会允许被继承, 一继承就会报错呢
//方式1, 
class NonInherit
{
public:
	static NonInherit GetInstance()
	{
		return NonInherit();
	}
private:
	NonInherit()
	{}
};

class B : public NonInherit {
	
};

//方式2:使用final 关键字修饰
class A final {	    
};

五. 单例模式从理论到实现

什么叫做单例模式:  单例, 单个对象(实例), 也就是一个类只能创建一个对象. 这个就是单例模式.

单例模式简单应用场景如下:

  • 比如在某个服务器程序中,该服务器的配置信息存放在一个文件 中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置 信息,这种方式简化了在复杂环境下的配置管理。
  • 还有就是游戏开发的时候的地图, 这种一般也是设计为单例的, 因为地图肯定也是仅仅加载一份的, 所有玩家都是使用的同一份地图

单例模式实现思路:

  • 首先因为我们对于对象创建的限制, 所以肯定还是私有化构造函数和禁止拷贝构造,  然后我们需要提供一个获取这个全局单例对象的一个对外接口 GetInstance
  • 然后因为全局仅仅一个单例对象, 一个类只能实例化出来一个对象, 这个对象来源: 堆区  + 全局数据段  (必然不可以是栈区, 因为栈区 生命周期有限)
  • 这个对象要与类同在,  要属于整个类, 而不是某一个对象, 所以需要 static 

饿汉式: 一来就要吃, 所以是在进入主函数之前就已经创建好了这个单例对象的....

使用堆区 new 一个对象 + static成员指针指向这个new出来的堆区对象

class Singleton {
public:
	static Singleton* GetInstance() {
		return _ins;
	}
	void Print() {
		cout << "饿汉子" << endl;
	}
private:
	Singleton() {}
	Singleton(const Singleton& ins) = delete;	//禁止拷贝构造
	Singleton& operator=(const Singleton& ins) = delete;
	static Singleton* _ins;
};

Singleton* Singleton::_ins = new Singleton;

另外一种及其简单的方式, 直接创建一个静态的对象  return &ins;   很骚气的方式, 利用static 的优势, 仅仅在第一次的时候创建出来这个对象, 且这个对象不会因为使用过一次之后释放掉, 后面的每一次都是复用的同一个这个对象, 也巧妙简单的实现了单例。。。

  • 弊端: 单例对象在静态区, 如果单例对象过大, 不太好, 不合适
  • 且没有办法主动控制单例对象的释放
class Singleton {
public:
	static Singleton* GetInstance() {
		static Singleton ins;
		return &ins;
	}
private:
	Singleton() {}
	Singleton(const Singleton& ins) = delete;
	const Singleton& operator =(const Singleton& ins) = delete;
};

懒汉式实现单例:

相较饿汉式, 什么时候适合使用懒汉式

  •  假设单例类构造函数中,要做很多配置初始化工作,那么饿汉就不合适了。。(会导致进入主函数延迟, 程序启动会非常慢, 因为饿汉是在进行主函数之前创建的对象)

懒汉式含义:   

        懒汉式故名思意就是比较懒, 不会在进入主函数之前就创建出来单例对象, 仅仅只是在第一次需要使用的时候创建单例对象。。。  意味着 需要在 GetInstance 中实现创建对象配置等等操作。。。。。    

这个时候思考一个问题???????    对于懒汉式, 调用GetInstance 函数的时候创建对象, 其实相当于是写入操作, 会不会出现线程不安全的问题?????   

当然一个线程区调用这个GetInstance 函数是没有问题的,  但是如果是多个线程同时调用,可能就会出现冲突了....   重入,  两个线程或者多个线程同时进入判断  _ins == nullptr 都去创建对象, 就不安全了.....  所以这个时候  针对  _ins的判断和创建操作我们需要将其绑定为一个原子操作

C++从面试常考实现特殊类到单例模式的实现_第1张图片

  • 明确: 多线程读取临界资源的操作, 线程是安全的
  • 线程同时对临界资源进行写的操作, 才是线程不安全的

多个线程同时进行 读取操作,  是不会产生线程危险的问题的, 就是重入了, 只要不写入数据也是没有问题的, 所以前面的懒汉 不需要加锁保护, 就是因为他仅仅只是  return 创建好的对象, 仅仅只是读取操作, 不存在需要创建对象的写操作, 所以哪怕是多线程来读取都是没问题的.....  此处多线程写需要保护.......

 但但是上述是否存在一定的效率问题??????   要是 存在很多的线程都需要调用这个  GetInstance函数,   但是其实仅仅只是第一次写入的时候才会存在线程安全问题.,......因为一旦第一次写的时候是线程安全的, 写入进去之后其实只需要做的事情就是 读取操作了, 就不再需要写了, 这个时候, 只要经历第一次写的时候是线程安全的, 后面全部都是读取操作了, 其实就没必要再不停的加锁解锁了....   所以这个时候我们一般会用双检查来提高效率

C++从面试常考实现特殊类到单例模式的实现_第2张图片

class Singleton {
public:
	static Singleton* GetInstance() {
		if (_ins == nullptr) {			//双检查提高效率
			_lock.lock();
			if (_ins == nullptr) {
				_ins = new Singleton;
			}
			_lock.unlock();
		}
		return _ins;
	}

	static void DelInstance() {	//销毁单例对象的接口
		_lock.lock();
		if (_ins) {
			delete _ins;
			_ins = nullptr;
		}
		_lock.unlock();
	}
	void Print() {
		cout << "懒汉子" << endl;
	}
	class Garbage {	//垃圾回收类, 最后前面没有调用Del接口, 此处自动回收
	public:	
		~Garbage() {//此处不进行加锁, 因为此处的锁很可能已经释放
			if (_ins) {
				delete _ins;
				_ins = nullptr;
			}	
		}
		
	};
private:
	Singleton() {
		//配置初始化
	}
	~Singleton() {
		//程序结束之后需要做一些持久化保存工作, 比如写入磁盘操作
	}
	Singleton(const Singleton& ins) = delete;	//禁止拷贝构造
	Singleton& operator=(const Singleton& ins) = delete;
	static Singleton* _ins;
	static mutex _lock;
	static Garbage gar;
};
Singleton* Singleton::_ins = nullptr;
mutex Singleton::_lock;
Singleton::Garbage Singleton::gar;

六. 总结前文

  • 首先是从特殊类的设计做引入,: 普世方法: 私有化构造函数, 然后提供公有的限制性的创建对象的静态接口, 因为开始没有对象, static 之后可以使用类名调用创建
  • 然后引入单例模式:  一个类只能实例化一个对象的一种设计模式.....  为了实现实例化对象的限制, 还是私有化构造函数, 提供限制性创建或者是获取对象的公有静态的接口函数
  • 饿汉式单例, 一来就创建单例对象了, 进入主函数之前就创建好了, 缺陷, 如果配置文件等等过大, 可能导致程序启动起来特别慢..   它的 GetInstance函数仅仅是读取_ins单例对象的操作, 所以是线程安全的, 不需要加锁保护
  • 懒汉式单例,  第一次需要的时候再 GetInstance 中创建单例对象, 因为第一次实例化单例对象的时候可能存在多个线程写操作, 也就是多个线程都要  调用 GetInstance,这个时候为了避免函数重入这样的冲突, 所以加锁 原子操作进行保护,为了提高效率仅仅第一次写加锁保护, 于是采用双检查模式提高效率

你可能感兴趣的:(C/C++语法深度学习,c++,面试,单例模式,后端)