[C++11] special class design | type conversion

1. Special class design

1. Classes that can only create objects on the heap

Objects can only be created on the heap, that is, objects can only be created through the new operator, as follows:

  1. Make the constructor private to prevent external direct calls to the constructor to create objects on the stack.
  2. Provide a static interface for obtaining objects to the outside, which creates an object on the heap and returns it.
  3. Set the copy constructor as private, and only declare that it does not implement, to prevent external calls to the copy constructor to create objects on the stack.

code show as below:

class HeapOnly
{
    
    
public:
	//2、提供一个获取对象的接口,并且该接口必须设置为静态成员函数
	static HeapOnly* CreateObj()
	{
    
    
		return new HeapOnly;
	}
private:
	//1、将构造函数设置为私有
	HeapOnly()
	{
    
    }
	//3、将拷贝构造函数设置为私有,并且只声明不实现
	//C++98
	HeapOnly(const HeapOnly&);
	//C++11
	//HeapOnly(const HeapOnly&) = delete;
};

Explain:

  1. The CreateObj function provided to the outside must be set as a static member function, because the interface is called externally to obtain the object, and the non-static member function must be called through the object, which becomes a chicken-and-egg problem.
  2. C++98 achieves the purpose of anti-copy by declaring the copy constructor as private. C++11 can add =delete after the copy constructor to indicate that the compiler deletes the copy constructor. At this time, the anti-copy can also be achieved. purpose of copying.

2. Classes that can only create objects on the stack

method one

The way is as follows:

  1. Set the constructor as private to prevent external direct calls to the constructor to create objects on the heap.
  2. Provide a static interface for obtaining objects to the outside, which creates an object on the stack and returns it.

code show as below:

class StackOnly
{
    
    
public:
	//2、提供一个获取对象的接口,并且该接口必须设置为静态成员函数
	static StackOnly CreateObj()
	{
    
    
		return StackOnly();
	}
private:
	//1、将构造函数设置为私有
	StackOnly()
	{
    
    }
};

But this method has a defect that it cannot prevent external calls to the copy constructor to create objects.

StackOnly obj1 = StackOnly::CreateObj();
static StackOnly obj2(obj1); //在静态区拷贝构造对象
StackOnly* ptr = new StackOnly(obj1); //在堆上拷贝构造对象

But we can't set the constructor as private, and we can't =deletedelete the copy constructor, because the CreateObj function creates a local object, and the copy constructor must be called in the process of returning the local object.

Method Two

The way is as follows:

  • Shield operator new function and operator delete function.

code show as below:

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

The principle of new and delete:

  • There are actually two steps for new to apply for space on the heap. The first step is to call the operator new function to apply for space, and the second step is to execute the constructor on the requested space to complete the initialization of the object.
  • delete is also divided into two steps in releasing the heap space. The first step is to execute the destructor in this space to complete the cleaning of resources in the object. The second step is to call the operator delete function to release the space of the object.

New and delete call the global operator new function and operator delete function by default, but if a class overloads the exclusive operator new function and operator delete function, then new and delete will call the exclusive function. So as long as the operator new function and operator delete function are shielded, then new can no longer be used to create objects on the heap.

But this method also has a defect, that is, it cannot prevent external objects from being created in the static area.

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

Of course, method 1 and method 2 can also be combined. After the combination, it is just impossible to prevent copying and constructing objects in the static area.

3. Design a class that cannot be copied

To prevent a class from being copied, it is necessary to prevent the class from calling the copy constructor and assignment operator overloading function, so directly set the copy constructor and assignment operator overloading function of the class as private, or use the C++11 method Just delete these two functions.

code show as below:

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. Design a class that cannot be inherited

Method 1: C++98

Just set the constructor of this class as private, because when the constructor of the subclass is called, the constructor of the parent class must be called to initialize the part of the members of the parent class, but the private members of the parent class are not visible in the subclass Yes, so when creating a subclass object, the subclass cannot call the constructor of the parent class to initialize the members of the parent class, so the subclass cannot create objects after the class is inherited.

code show as below:

class NonInherit
{
    
    
public:
	static NonInherit CreateObj()
	{
    
    
		return NonInherit();
	}
private:
	//将构造函数设置为私有
	NonInherit()
	{
    
    }
};

Method 2: C++11

This method of C++98 is actually not thorough enough, because this class can still be inherited (the compiler will not report an error), but the object cannot be instantiated after being inherited. Therefore, the final keyword is provided in C++11. The class modified by final is called the final class. The final class cannot be inherited. At this time, even if no object is created after inheritance, a compilation error will occur.

code show as below:

class NonInherit final
{
    
    
	//...
};

2. Singleton mode

What is the singleton pattern?

  • The singleton pattern is a design pattern (Design Pattern). A design pattern is a summary of code design experience that is repeatedly used, known to most people, cataloged, and coded. The purpose of using design patterns is to reusable code, make the code easier to understand by others, and ensure the reusability of code reliability programs.
  • The singleton mode means that a class can only create one object. This mode can ensure that there is only one instance of the class in the system and provide a global access point to access it. This instance is shared by all program modules.
  • For example, in a server program, the configuration information of the server is stored in a file, and these configuration data are read by a singleton object, and then other objects in the service process obtain the configuration information through this singleton object, which is This approach simplifies configuration management in complex environments.

There are two ways to implement the singleton mode, namely the hungry man mode and the lazy man mode:

hungry man mode

The implementation of the singleton pattern is as follows:

  1. Make the constructor private, and make the copy constructor and assignment operator overloaded functions private or delete, preventing external creation or copying of objects.
  2. Provide a static pointer to the singleton object, and complete the initialization of the singleton object before the program entry.
  3. Provides a global access point for singleton objects.

code show as below:

class Singleton
{
    
    
public:
	//3、提供一个全局访问点获取单例对象
	static Singleton* GetInstance()
	{
    
    
		return _inst;
	}
private:
	//1、将构造函数设置为私有,并防拷贝
	Singleton()
	{
    
    }
	Singleton(const Singleton&) = delete;
	Singleton& operator=(const Singleton&) = delete;
	//2、提供一个指向单例对象的static指针
	static Singleton* _inst;
};

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

lazy mode

The lazy implementation of the singleton pattern is as follows:

  1. Make the constructor private, and make the copy constructor and assignment operator overloaded functions private or delete, preventing external creation or copying of objects.
  2. Provide a static pointer to the singleton object and initialize it to empty before the program entry.
  3. Provides a global access point for singleton objects.

code show as below:

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; //初始化互斥锁

Comparison of Hungry Man Mode and Lazy Man Mode

  • The advantage of the Hungry Man mode is simplicity, but its disadvantages are also obvious. The Hungry Man mode will create a singleton object before the program runs the main function. If the constructor of the singleton class does a lot of work, it will cause the program to be unable to enter the main function. It looks like a program from the outside. its stuck.
  • In addition, if there are multiple singleton classes that need to create singleton objects, and there is a certain dependency between their initializations, for example, the creation of singleton object A must be after singleton object B, then the hungry man mode will also exist problem, because we cannot guarantee which of these multiple singleton objects will be created first.
  • The lazy mode can solve the above shortcomings of the hungry mode very well, because the lazy mode does not complete the creation of singleton objects from the beginning, so it will not cause the program to be unable to enter the main function for a long time, and each singleton in the lazy mode The order of object creation is determined by the order in which the GetInstance function in each singleton class is called for the first time, so it is controllable.
  • The disadvantage of the lazy man mode is that it is more complicated than the hungry man mode in coding, and thread safety issues need to be considered when creating singleton objects.

Release of singleton object

After the singleton object is created, it may be used throughout the running of the program, so we can ignore the release of the singleton object, and the resource will be returned to the operating system automatically when the program ends normally.

If you want to consider the release of singleton objects, you can refer to the following two methods:

  1. Write a DelInstance function in the singleton class, and release the singleton object in this function. When the singleton object is no longer needed, you can actively call DelInstance to release the singleton object.

code show as below:

static void DelInstance()
{
    
    
	_mtx.lock();
	if (_inst != nullptr)
	{
    
    
		delete _inst;
		_inst = nullptr;
	}
	_mtx.unlock();
}
  1. Implement an embedded garbage collection class in the singleton class, and complete the release of the singleton object in the destructor of the garbage collection class. Define a static garbage collection class object in the singleton class. When the object is consumed, its destructor will be called, and the singleton object will be released at this time.

code show as below:

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

3. Type conversion

1. Type conversion in C language

Both C language and C++ are strongly typed languages. If the types of the variables on the left and right sides of the assignment operator are different, or the types of the formal and actual parameters do not match, or the return value type is inconsistent with the variable type receiving the return value, then it needs to be typed. convert.

There are two forms of type conversion in C language, namely implicit type conversion and explicit type conversion:

  • Implicit type conversion: The compiler automatically performs conversion during the compilation phase. If it can be converted, it will be converted, and if it cannot be converted, the compilation will fail.
  • Explicit type conversion: It needs to be handled by the user himself, and (指定类型)变量the type conversion is performed in a certain way.

It should be noted that implicit type conversion can only occur between similar types. For example, int and double represent values, but the range and precision they represent are different. The pointer type represents the address number, so there is no implicit type conversion between the integer type and the pointer type, and only explicit type conversion can be performed if conversion is required. for example:

int main()
{
    
    
	//隐式类型转换
	int i = 1;
	double d = i;
	cout << i << endl;
	cout << d << endl;

	//显式类型转换
	int* p = &i;
	int address = (int)p;
	cout << p << endl;
	cout << address << endl;
	return 0;
}

2. C++ type conversion

The C-style conversion format, while simple, has a number of disadvantages:

  • Implicit type conversion may cause problems in some cases, such as loss of data precision.
  • Explicit type conversions mix all the cases together, and the visibility of conversions is poor.

Therefore, in order to enhance the visibility of type conversion, C++ introduces four named cast operators, namely, static_cast, reinterpret_cast, const_castand dynamic_cast.

①static_cast

Static_cast is used for conversion between similar types. Any type conversion implicitly performed by the compiler can be used static_cast, but it cannot be used for conversion between two unrelated types. for example:

int main()
{
    
    
	double d = 12.34;
	int a = static_cast<int>(d);
	cout << a << endl;

	int* p = &a;
	// int address = static_cast<int>(p); //error
	return 0;
}

②reinterpret_cast

reinterpret_cast is used for conversion between two unrelated types. for example:

int main()
{
    
    
	int a = 10;
	int* p = &a;
	int address = reinterpret_cast<int>(p);
	cout << address << endl;
	return 0;
}

reinterpret_cast also has a very buggy usage. For example, in the following code, the function pointer with parameters and return value is converted into a function pointer with no parameters and no return value, and this function can also be called with the converted function pointer.

typedef void(*FUNC)();
int DoSomething(int i)
{
    
    
	cout << "DoSomething: " << i << endl;
	return 0;
}
int main()
{
    
    
	FUNC f = reinterpret_cast<FUNC>(DoSomething);
	f();
	return 0;
}

To explain : When calling the function with the converted function pointer, no parameters are passed in, so the value of the parameter i printed here is a random value

③ const_cast

const_cast is used to delete the const attribute of the variable, and the value of the const variable can be modified after conversion. for example:

int main()
{
    
    
	const int a = 2;
	int* p = const_cast<int*>(&a);
	*p = 3;
	cout << a << endl;  //2
	cout << *p << endl; //3
	return 0;
}

Let me explain :

  • In the code, const_cast is used to delete the const attribute of the address of variable a, and then the value of variable a can be modified through this pointer.
  • Since the compiler thinks that const-modified variables will not be modified, it will store the const-modified variables in the register, and when it needs to read the const variable, it will read it directly from the register, and what we modify is actually is the value of a in memory, so the final printed value of a is the value before modification.
  • If you don't want the compiler to optimize the const variable into the register, you can use the volatile keyword to modify the const variable. At this time, when you want to read the const variable, the compiler will read it from the memory, that is, keep the variable Visibility in memory.

④dynamic_cast

dynamic_cast is used to convert the pointer (or reference) of the parent class to the pointer (or reference) of the subclass.

Upward Transformation and Downward Transformation

  • Upcasting: Subclass pointer (or reference) → parent class pointer (or reference).
  • Downward transformation: pointer (or reference) of parent class → pointer (or reference) of subclass.

Among them, upward transformation is the so-called cutting/slicing, which is naturally supported by the grammar and does not require conversion, while downward transformation is not supported by the grammar and requires forced type conversion.

Downward Transformation Security Issues

Downward transformation is divided into two situations:

  1. If the pointer (or reference) of the parent class points to a parent class object, it is unsafe to convert it to a pointer (or reference) of the subclass, because the resource of the subclass may be accessed after conversion, and this resource It is not available in the parent class object.
  2. If the pointer (or reference) of the parent class points to a subclass object, it is safe to convert it to a pointer (or reference) of the subclass.

It is unsafe to use C-style cast for downcasting, because at this time, no matter whether the pointer (or reference) of the parent class points to the parent class object or the subclass object, the conversion will be performed. It is safe to use dynamic_cast for downcasting. If the pointer (or reference) of the parent class points to the object of the subclass, dynamic_cast will be converted successfully, but if the pointer (or reference) of the parent class points to the object of the parent class, then dynamic_cast will fail and return a null pointer. for example:

class A
{
    
    
public:
	virtual void f()
	{
    
    }
};
class B : public A
{
    
    };
void func(A* pa)
{
    
    
	B* pb1 = (B*)pa;               //不安全
	B* pb2 = dynamic_cast<B*>(pa); //安全

	cout << "pb1: " << pb1 << endl;
	cout << "pb2: " << pb2 << endl;
}
int main()
{
    
    
	A a;
	B b;
	func(&a);
	func(&b);
	return 0;
}

In the above code, if the address of the subclass object is passed into the func function, pb1 and pb2 will have corresponding addresses after conversion, but if the address of the parent class object is passed into the func function, then pb1 will have the corresponding address after conversion. The corresponding address, while pb2 is a null pointer.

To explain : dynamic_cast can only be used for classes containing virtual functions, because runtime type checking requires runtime type information, and this information is stored in the virtual function table. Only classes that define virtual functions have virtual function tables. .

explicit

explicit is used to modify the constructor, thereby prohibiting the implicit conversion of the single-argument constructor. for example:

class A
{
    
    
public:
	explicit A(int a)
	{
    
    
		cout << "A(int a)" << endl;
	}
	A(const A& a)
	{
    
    
		cout << "A(const A& a)" << endl;
	}
private:
	int _a;
};
int main()
{
    
    
	A a1(1);
	//A a2 = 1; //error
	return 0;
}

Syntactically, A a2 = 1 in the code is equivalent to the following two sentences of code:

A tmp(1);  //先构造
A a2(tmp); //再拷贝构造

So in the early compilers, when the compiler encounters the code A a2 = 1, it will first construct a temporary object, and then use this temporary object to copy and construct a2. But the current compiler has optimized it. When it encounters the code A a2 = 1, it will directly process it in the way of A a2(1), which is also called implicit type conversion.

But for a single-parameter custom type, the readability of the code A a2 = 1 is not very good, so you can modify the single-parameter constructor with explicit, thereby prohibiting the implicit conversion of the single-parameter constructor.

3. RTTI

RTTI (Run-Time Type Identification) is runtime type identification.

C++ supports RTTI in the following ways:

  1. typeid: Identify the type of an object at runtime.
  2. dynamic_cast: Identify at runtime whether a pointer (or reference) of a parent class points to a parent class object or a subclass object.
  3. decltype: The type of an expression or function return value is deduced at runtime.

4. Common interview questions

  1. The four types of conversions in C++ are: ____, ____, ____, ____.

    They are static_cast, reinterpret_cast, const_cast and dynamic_cast respectively.

  2. Talk about the application scenarios of the four types of conversions.

    • static_cast is used for conversion between types of similar types, and any type conversion performed implicitly by the compiler can be static_cast.
    • reinterpret_cast is used for conversion between two unrelated types.
    • const_cast is used to delete the const attribute of the variable, which is convenient for assignment.
    • dynamic_cast is used to safely convert the pointer (or reference) of the parent class to the pointer (or reference) of the subclass.

This is the end of this article, the code text is not easy, please support me a lot! ! !

Guess you like

Origin blog.csdn.net/weixin_67401157/article/details/132264711