C++ implements a special class from the interview routine to the implementation of the singleton pattern

content

foreword

1. How to design a class that can only create objects on the heap

2. How to design a class that can only create objects on the stack

3. How to design a class that cannot be copied

4. How to design a class that cannot be inherited

5. Singleton pattern from theory to implementation

6. Summarize the previous article


foreword

During the interview, we often encounter some special class design questions. These questions actually imply some design patterns. If we want to restrict the construction of objects, the easiest way to think of is of course to restrict the constructor first, and then we will provide special The interface for constructing objects. Just like the singleton pattern. . . . .  

However, before we construct an object, we don't have an object. How can we call the restrictive function we designed to create an object??? Make the function static, so that this function belongs to the entire class, use the class name + :: can be accessed

  • Private constructor + prohibit copy construction, do not let it copy construction
  • Ways to disable copy construction. 1. Private copy construction (C++98) 2. Delete keyword modification (C++11)
  • The constructor is privatized, but it still needs to be able to construct an object, providing the GetInstance public interface to restrict the creation of objects
  • The public interface is static so that it can be accessed through the class name (solve the problem of how to call)

1. How to design a class that can only create objects on the heap

  • private constructor
  • Disable copy construction
  • Provides class public static interface functions are restricted and can only be created on the heap
class HeapOnly {
public:
	static HeapOnly* CreateObj() {
		return new HeapOnly;
	}
private:
	HeapOnly() {};	
	//方式1: 防止拷贝构造
	HeapOnly(const HeapOnly& h);
public:
	//方式2 :防止拷贝构造
	HeapOnly(const HeapOnly& h) = delete;
};

2. How to design a class that can only create objects on the stack

  • Can only be created on the stack, in fact, it is forbidden to drop objects on the heap and created by the global data segment
  • In addition to using the above method, privatization and then providing a GetInstance interface, there are also the following methods
    	void* operator new(size_t size) = delete;
    	void operator delete(void* p) = delete;

Memories kill. The essence of when we new an object:

  • Call the global operator new to allocate memory
  • Call the constructor to initialize

The essence of delete an object:

  • Call the destructor to do the end processing, which may be to write to the disk, close the file, etc.
  • Call the global operator delete to release the resource
class StackOnly {
public:
	static StackOnly CreateObj() {
		return StackOnly();		//构造一个临时对象拷贝返回
	}
    //因为存在临时对象的返回, 存在拷贝构造, 所以没有必要也不可以禁止掉拷贝构造。。
    //思考 ? 和上述堆区创建对象不一样之处
private:
	StackOnly() {};
    //方式2: 禁止堆区构造必须调用的函数
	//void* operator new(size_t size) = delete;
	//void operator delete(void* p) = delete;
};
  • The defect of using oprator new and operator delete is prohibited, it only restricts the creation of objects in the heap area , but in the global data segment, that is, static static is not prohibited ..

3. How to design a class that cannot be copied

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;
};

4. How to design a class that cannot be inherited

  • Method 1: Make the constructor of the parent class private, so that inheritance will not report an error, but the subclass cannot call the constructor of the parent class, and thus cannot reuse the code of the parent class. . . An error is reported when instantiating.... Because only when instantiating will really call the parent class constructor, you will find that it cannot be called.    
  • Method 2: In the C++11 method, a final keyword is used to modify the class, indicating that this class is the final class, and naturally it will not be allowed to be inherited, and an error will be reported once it is inherited.
//方式1, 
class NonInherit
{
public:
	static NonInherit GetInstance()
	{
		return NonInherit();
	}
private:
	NonInherit()
	{}
};

class B : public NonInherit {
	
};

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

5. Singleton pattern from theory to implementation

What is the singleton pattern: a singleton, a single object (instance), that is, a class can only create one object. This is the singleton pattern.

The simple application scenarios of the singleton pattern are as follows:

  • For example, in a server program, the configuration information of the server is stored in a file, and the configuration data is uniformly read by a singleton object, and then other objects in the service process obtain the configuration information through the singleton object. This way simplifies configuration management in complex environments.
  • There is also the map during game development, which is generally designed as a single instance, because the map must only be loaded with one copy, and all players use the same map

Singleton pattern implementation ideas:

  • First of all, because of our restrictions on object creation, we must still privatize the constructor and prohibit copy construction, and then we need to provide an external interface GetInstance to obtain the global singleton object
  • Then because there is only one singleton object globally, a class can only instantiate one object. The source of this object is: heap area + global data segment (it must not be the stack area, because the life cycle of the stack area is limited)
  • This object must be with the class, it must belong to the entire class, not an object, so it needs static 

Hungry Chinese style: I have to eat as soon as I come, so this singleton object has been created before entering the main function....

Use heap area new an object + static member pointer to point to the new heap area object

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;

Another very simple way is to directly create a static object return &ins; very annoying way, using the advantages of static, only create this object for the first time, and this object will not be used once After it is released, the same object is reused every time, and the singleton is also cleverly and simply implemented. . .

  • Disadvantages:  The singleton object is in the static area. If the singleton object is too large, it is not very good and not suitable.
  • And there is no way to actively control the release of the singleton object
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;
};

Lazy implementation of singleton:

Compared to Hungry Man Style, When Is Lazy Man Style Appropriate?

  • Assuming that a lot of configuration initialization work needs to be done in the singleton class constructor, then the hungry man is not suitable. . (It will cause a delay in entering the main function, and the program startup will be very slow, because the hungry man is an object created before the main function)

Lazy man meaning:   

        Lazy-style, so the name implies, is lazy. It will not create a singleton object before entering the main function, but only create a singleton object when it is needed for the first time . . . It means that operations such as creating object configuration and so on need to be implemented in GetInstance. . . . .    

Thinking about a question at this time??????? For the lazy style, creating an object when calling the GetInstance function is actually equivalent to a write operation, will there be a problem of thread insecurity?????   

Of course, there is no problem in calling the GetInstance function in a thread area, but if it is called by multiple threads at the same time, there may be conflicts.... Reentrancy, two threads or multiple threads enter the judgment at the same time _ins == nullptr It is not safe to create objects... So at this time, we need to bind it as an atomic operation for the judgment and creation of _ins

  • Explicit: Multi-threaded operations that read critical resources are thread-safe
  • It is thread-unsafe when threads simultaneously write to critical resources.

Multiple threads reading at the same time will not cause the problem of thread danger, that is, reentrancy, as long as no data is written, there is no problem, so the slack in front does not need lock protection, because he just returns The created object is only a read operation, there is no write operation that needs to create an object, so even if it is multi-threaded to read it is no problem..... Here multi-threaded writing needs to be protected..... ..

 But is there a certain efficiency problem in the above???? If there are many threads that need to call this GetInstance function, but in fact, there will be thread safety problems only when writing for the first time.,... ..because once the first write is thread-safe, the only thing you need to do after writing is the read operation, and you don't need to write anymore. At this time, you only need to experience the first write. It is thread-safe, and all the following operations are read operations. In fact, there is no need to keep locking and unlocking.... So at this time, we generally use double-checking to improve efficiency.

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;

6. Summarize the previous article

  • The first is to introduce from the design of a special class:  universal method: private constructor, and then provide a public and restrictive static interface for creating objects, because there is no object at the beginning, static can be used to call the creation after the class name
  • Then introduce the singleton pattern: a design pattern in which a class can only instantiate one object..... In order to achieve the restriction of instantiated objects, it is still private constructors that provide restricted creation or public static access to objects interface function
  • Hungry-style singleton, the singleton object is created as soon as it comes, and it is created before entering the main function. The defect, if the configuration file is too large, may cause the program to start very slowly. Its GetInstance function only reads The operation of taking the _ins singleton object, so it is thread-safe and does not require lock protection
  • Lazy singleton, create a singleton object in GetInstance when it is needed for the first time, because there may be multiple thread write operations when the singleton object is instantiated for the first time, that is, multiple threads must call GetInstance, this time In order to avoid conflicts such as function reentrancy, the atomic operation is locked for protection . In order to improve efficiency, only the first write lock protection is applied, so double check mode is used to improve efficiency .

Guess you like

Origin blog.csdn.net/weixin_53695360/article/details/122917855