[C++] exception + smart pointer + special class and type conversion

God may think that I am too lonely, and sent you to fight against nothingness with me.

insert image description here



1. Abnormal

1. Traditional way of handling errors vs exceptions

1.
The traditional way of handling errors in C language is nothing more than returning an error code or directly terminating the running program. For example, assert through assert, but assert will directly terminate the program, and the user is unacceptable to such a processing method. For example, if the user makes a mistake, will the app be terminated and exited directly? For the user, the experience effect is very poor. After all, I just accidentally misoperated, and the program exited directly, which is too unreasonable! However, methods such as returning error codes are not humane enough, and programmers need to find errors by themselves. When many interfaces at the system level make errors, they always put error codes in the global variable errno. Programmers also need to print out The value of errno, and then compare the error code table to get what the error message corresponding to errno is.
In practice, the C language basically uses error codes to handle program errors, and in some cases uses the method of terminating the program to handle errors.

2.
Exception is a way of handling errors introduced by C++. When an error occurs in a function or interface, it can directly throw the exception object, and then catch will capture the exception object and perform related processing on the exception that occurred.
try is used to activate a certain code block that needs to be tested, that is, this code block will throw an exception object when some kind of error occurs. Note that try catch needs to be used together. In a certain call interface, if there is only catch without try, an error will be reported. Similarly, only try without catch will also report an error. Therefore, try and catch must be used together. One is used to activate the throw of the exception object. An exception object used to catch thrown.
Throw is the protected code block. When some kind of error occurs, throw can choose to throw an exception object. After the exception object is thrown, the execution flow will directly jump to the catch block that matches the exception object type.
catch is used to catch exception objects, which can have multiple types, and the parameters of the catch block need to match the type of exception that needs to be handled.

try
{
    
    
	func();// 保护的标识代码
}
catch( ExceptionName e1 )
{
    
    
  // catch 块
}
catch( ExceptionName e2 )
{
    
    
  // catch 块
}
catch( ExceptionName eN )
{
    
    
  // catch 块
}

2. Abnormal usage rules

2.1 Exception throwing and catching principles

1.
An exception is thrown by throwing an object whose type determines which catch's processing code is activated .
For example, in the following code, when b is 0, the Division function will throw an exception object. The type of the exception object is a constant string. After the object is thrown, the execution flow will directly jump to the catch that matches the exception object type. Block, that is, a catch block whose parameter is a constant string type, and then print the corresponding error message or other processing methods in this catch block.
Throwing an exception can throw a richer error message, which is completely determined by the programmer. In the traditional processing method of error code, the error message has been stipulated by the language, and the scalability is not strong, so the exception Object customization is much stronger than error codes.

double Division(int a, int b)
{
    
    
    // 当b == 0时抛出异常
    if (b == 0)
        throw "Division by zero condition!";
    else
        return ((double)a / (double)b);
}
void Func()
{
    
    
    int len, time;
    cin >> len >> time;
    cout << Division(len, time) << endl;// throw const char*对象 
}
int main()
{
    
    
    try 
    {
    
    
        Func();
    }
    catch (const char* errmsg)
    {
    
    
        cout << errmsg << endl;
        // 记录日志,进行统一处理。想在main里面统一进行异常的捕获
    }
    catch (int errid)
    {
    
    
        cout << errid << endl;
    }
    catch (...)
    {
    
    

    }

    return 0;
}

2.
The selected catch block that handles the exception is the catch block that matches the type of the exception object in the call chain and is closest to the position where the exception object is thrown.
For example, in the following code, func1 throws an exception object e, and the call chain is as follows, main calls func3, func3 calls func2, and func2 calls func1. When an exception object is thrown, it will first check whether it has a type that matches the catch block and try , if there is, then jump directly to the catch block to process the exception object, if not, then check whether the interface in the call chain has a matching catch block, if there is, point to it, if not, continue to look backwards for the catch piece.

insert image description here
3.
When the exception object is captured by the catch block, the catch block usually uses a reference as a parameter to receive the exception object type .
In C++, when an exception is thrown, the exception handling mechanism ensures that the exception object remains valid during the execution of the corresponding catch block. Exception objects are not destroyed by leaving the function stack frame. This is because the C++ Standard Library implements a special memory management strategy for handling exception objects.
When an exception is thrown, the exception object is created and copied to a special memory area called the exception store. This area is managed by the C++ runtime library and is separate from the program's stack memory and heap memory. Therefore, in the exception handling process, even if the function stack frame is destroyed, the exception object is still valid and can be caught in the catch block.
Using references to capture exception objects can avoid the copying of exception objects. When the exception object is large, it can directly refer to the object stored in the exception storage area, which can improve performance.

4.
catch(...) can catch any type of exception, but the disadvantage is that it does not know what the exception type is.
What about the following scenario? Memory resources are applied for in Func. If there is no try catch, only three lines of code to input len ​​time and call Divison, will there be a memory leak? Because an exception will be thrown in Division, if there is no catch in Func, it will directly go to main to match the corresponding catch block. At this time, due to the jump of execution flow, memory leak will occur in p1 in Func, and it will not be able to execute to delete. [ ] p1;
So the common practice is to retry and catch exceptions in Func. If it is stipulated to catch exceptions in main, then Func will play the role of a middleware, catch exceptions and rethrow them. In this case, The execution flow will jump to the catch inside Func() first, so p1 can be released at this time to prevent memory leaks, but what if there are too many exception objects thrown? Shall we also write a bunch of try catch blocks for middleware in Func? Is this a bit too frustrating? So there is a way to catch (...) and throw, which is to catch all exceptions, and then rethrow all the caught exceptions. Then p1 can be released in catch(...) to prevent memory leaks.
But in fact, there is another way to deal with it is to use smart pointers. The advantage of using smart pointers is that you don’t need to manually release resources yourself. If you don’t need to manually release resources yourself, then Func doesn’t need to be used as middleware to catch exceptions, because there is no exception in Func. There will be a problem of memory leaks, and we don't need to let the execution flow after the throw object stay in Func for a while in order to manually release resources.

void Func()
{
    
    
    int* p1 = new int[10];
    try
    {
    
    
        int len, time;
        cin >> len >> time;
        cout << Division(len, time) << endl;// throw const char*对象 
        // func(); throw double对象
        //如果说这里抛出的异常对象非常多,那func作为中间层就要做很多的工作,例如重新抛出异常,还要写具体类型的catch
        //所以如果中间件func需要抛出的异常对象非常多,那就直接统一捕获所有异常
    }
    catch (...)
    {
    
    
        delete[] p1;
        cout << "delete[] p1->" << p1 << endl;
        throw;//捕获到什么异常,那就抛出什么异常
    }
    //catch (const char* errmsg)
    //{
    
    
    //    //第一种做法,重新再把异常抛出去
    //    //第二种做法,通过智能指针来拯救一下这里的代码,防止出现没有delete的情况而导致的内存泄露。
    //    cout << "delete[] p1" << endl;
    //    delete[] p1;

    //    throw "Division by zero condition!";//重新抛出异常
    //    //cout << errmsg << endl;

    //}
    //2.如果上面的代码不捕获异常,那进入Division之后,抛出异常,会直接跳到main里面的catch,则p1内存泄露
    //想要解决,那就需要智能指针上场了,否则太容易造成内存泄露(try throw catch就像goto语句似的,很容易内存泄露)
    delete[] p1;
    //1.异常只要被捕获,就会跳到catch执行代码,执行完之后,后面的代码就正常运行了
    cout << "delete[] p1" << endl;
}

5.
The throwing and catching of actual exceptions have special cases in type matching, for example, the base class type can be used to capture derived class type objects, which is widely used in practice.

2.2 The exception stack expansion matching principle in the function call chain

1.
Generally, when an exception is thrown, it will first check whether there is a try catch block in the function stack where the current exception object is located. If there is, continue to check whether it matches. If it matches, it will directly jump to the catch block to execute the code. If not, then exit the current function stack and continue to search up the call chain until a suitable catch block is found. If no suitable catch block is found, the program will terminate and exit.
So in general, in order to prevent the software from terminating and exiting, we will leave the last line of defense, which is to catch all exceptions. It is possible that when a programmer throws an exception, the exception thrown is illegal. At this time, catch(...) can catch this An exception that does not explicitly define a type will not cause the software to terminate and exit.
After the corresponding catch block is matched and the code in the catch block is executed, the execution flow can be executed backward normally.

double Division(int a, int b)
{
    
    
    try
    {
    
    
        if (b == 0)
        {
    
    
            string s("Division by zero condition!");
            throw s;//捕获的是s的拷贝,那就有点效率低,所以用右值引用
        }
        else
        {
    
    
            return ((double)a / (double)b);
        }
    }
    catch (const string& errstr)
    {
    
    
        cout << "除0错误" << endl;//自己既可以抛异常,又可以catch异常
    }
}
void Func()
{
    
    
    int len, time;
    cin >> len >> time;
    Division(len, time) ;// throw const char*对象 
    cout << "+++++++++++++++++++++++++" << endl;//catch执行完之后,后面的执行流正常了就
}
int main()
{
    
    
    try
    {
    
    
        Func();
    }
    catch (const string& errmsg)//捕获的时候,尽量用引用
    {
    
    
        cout << errmsg << endl;
    }
    catch (...)//最后一道防线,不至于让软件终止退出
    {
    
    
        //程序出现异常,程序是不应该被随意终止的
        //出现未知异常,一般就是异常对象没有被正确的捕获,类型没匹配正确
        cout << "未知异常" << endl;
    }

    return 0;
}

3. Exception safety and exception specification

1.
It is best not to throw an exception in the constructor, because the object may not be initialized completely due to the jump of the execution flow, which will lead to the incompleteness of the object.
It is best not to throw an exception in the destructor, because it is also possible that the object is not fully destructed due to the jump of the execution flow, which will lead to memory resource leaks, unclosed files, and other problems.
In C++, resource leaks often occur due to exceptions. For example, throwing an exception between new and delete will cause a memory leak, and throwing an exception between lock and unlcok will cause a deadlock. C++ often uses RAII to solve the above problems, that is, to bind the life cycle of the resource and the life cycle of the object, the resource is created when the object is initialized, and the resource is destroyed when the object is destroyed.

2.
In C++98, an exception specification was created, which is to add throw (type) after the function to indicate the types of exceptions thrown by this function. If the brackets are empty, it means that the function does not throw any exceptions. Of course, this is not necessary. The C++ committee does not mandate that a statement about the type of exception thrown must be added after the function, and because the design is too complicated, everyone does not like to use this method. If a function throws 4 exceptions, I have to look back to see what the types of exceptions are, which is too troublesome. Every function has to look at the type behind its declaration, which is too cumbersome, and because there is no mandatory requirement, C++98 came up with it This set of regulations is useless, and no one uses it like this.
In C++11, the keyword noexcept is added, which means that the function will not throw any exceptions. This keyword is quite good, and it can tell the user that this function will not throw any exceptions, so you don't have to worry. Of course, if there is no noexcept after the function declaration, it means that the function can throw any type of exception.

// 这里表示这个函数会抛出A/B/C/D中的某种类型的异常
void fun() throw(A,B,C,D);
// 这里表示这个函数只会抛出bad_alloc的异常
void* operator new (std::size_t size) throw (std::bad_alloc);
// 这里表示这个函数不会抛出异常
void* operator delete (std::size_t size, void* ptr) throw();
// C++11 中新增的noexcept,表示不会抛异常
thread() noexcept;
thread (thread&& x) noexcept;

4. Custom exception system

1.
Actually, in the company's large-scale projects, there are many people responsible for different modules of the project, such as different groups responsible for network services, cache, sql, etc., all throw exceptions, and the types of exceptions they throw are different. , Just relying on one class to instantiate exception objects cannot meet the needs of so many groups, so inheritance comes in handy at this time. The caller who catches the exception can use the parent class type to capture the exception objects thrown in all subclasses , In this way, each group can have its own defined derived class to meet their respective requirements for throwing exceptions.

2.
The following is the exception inheritance system in the simulation server development. You can see that the base class is Exception, and there are three derived classes: SqlException, CacheException, and HttpServerException, which correspond to SQL exceptions, cache exceptions, and http service exceptions. Each derived class The virtual function what is rewritten, so that after the parent class captures the exception object, the virtual function what inside different exception objects can be called polymorphically.
Then we wrote a call chain by ourselves, HttpServer calls CacheMgr, CacheMgr calls SQLMgr, the three functions will throw an exception when a relatively random condition is met, we use the base class to capture all derived classes in the main Exception object, and then use the base class object to call the what virtual function rewritten in the derived class. Of course, if you want to do special handling for an exception, you can also capture this type of exception separately, but in general, you can directly capture it with the base class, and then handle the exception through polymorphic calls.
Therefore, the following method can be used to solve the scenario where multiple groups throw exceptions and catch exceptions outside. Such a custom exception inheritance system is also the mainstream error handling method used by many companies.

Review of Polymorphism Knowledge

insert image description here

Printing out +++++ means that the cache function throws an exception, and the success of the printing call means that no function in the call chain throws an exception.

insert image description here

5. The exception system of the standard library and the advantages and disadvantages of exceptions

1.
In fact, the C++ standard library also implements a set of exception system, which is also designed based on the inheritance system of parent and child classes. In actual use, we can also inherit the exception class by ourselves and implement a new derived exception class by ourselves. But in fact, most companies will not use the exception system of the standard library, but choose to define a set of exception inheritance system by themselves, similar to what we defined above, because the C++ standard library design is not easy to use.

insert image description here

insert image description here

2.
The following part of the code uses the exception inheritance system of the standard library. Both reserve and at will throw exceptions. We can use the base class exception in the standard library exception system to capture the exception objects thrown by reserve and at.

int main()
{
    
    
    try 
    {
    
    
        vector<int> v(10, 5);
        // 这里如果系统内存不够也会抛异常
        v.reserve(1000000000);
        // 这里越界会抛异常
        v.at(10) = 100;
    }
    catch (const exception& e) // 这里捕获父类对象就可以
    {
    
    
        cout << e.what() << endl;
    }
    catch (...)
    {
    
    
        cout << "Unkown Exception" << endl;
    }
    return 0;
}

Because this standard exception system is not very easy to use, it has been abandoned by many companies.
insert image description here

3.
The following are the advantages and disadvantages of exceptions. Although exceptions also have many disadvantages, overall the advantages outweigh the disadvantages, and compared with the traditional way of handling errors, it has been optimized a lot, so it is still encouraged to use exceptions to handle errors.

insert image description here

2. Smart pointer

1. Why do you need smart pointers?

1.
In the following piece of code, if p1new throws an exception, the program will terminate directly and report a bad_alloc exception, and then the catch in main will catch the exception. Since p2 is not created at this time, no memory leak will occur. If p2new throws an exception, the program will also terminate directly and report a bad_alloc exception, and main will also catch the exception, but since p1 has successfully allocated and opened up memory at this time, and delete[ ] p1 cannot be executed, memory will be generated Leakage issue. If div throws an exception, the catch in Func will catch the exception and release p1 and p2 successfully, so there will be no memory leak problem.

insert image description here

2.
Some people think that it is too troublesome to release resources as above, and they always need to consider a bunch of factors, what will happen if an exception is thrown here, and what will happen if an exception is thrown there, so the big guys think this method is too cumbersome, so they do it RAII (Resource Acquisition Is Initialization) comes out, which means that when resources are acquired, they are initialized. In fact, it means to acquire resources in the constructor, release and recycle resources in the destructor, and combine the life cycle of objects with resources. In this way, we don’t need to manually write a bunch of deletes to reclaim resources, because when the object is destroyed, the resources will be automatically reclaimed, and there is no need to manually recycle memory resources, which can not only reduce the burden of thinking, And it can avoid many potential memory leak problems. Why not do it?
The following is a simple version of the smart pointer we have implemented. In fact, the smart pointer is mainly composed of two parts, one is RAII, and the other is like a pointer. Originally, we used the built-in native pointer to manage the applied resources. Now we have After the smart pointer, you can directly use the smart pointer to manage resources. In order to realize resource management, we also need to implement operator overloading such as dereference and member access.
If we use smart pointers to manage the resources we have applied for, we no longer have to worry about memory leaks caused by not reclaiming resources. We are not afraid even if an exception is thrown, because when the execution flow leaves the function stack frame, due to When the function stack frame is destroyed, the smart pointer object will also be destroyed. At this time, the destructor will be called to complete the recycling of the resources pointed to by the smart pointer.
If you feel that you have finished learning smart pointers here, it means that you are still too naive. In fact, this is just the beginning of smart pointers. The biggest problem with smart pointers is actually copying, that is, copying between pointers! (We cannot perform deep copy, because this does not conform to the original intention of copy. The original intention of copy itself is to let multiple pointers point to the same resource and manage a resource at the same time. Isn’t it wrong for you to give me a deep copy? What I want is a copy of the original intention, not a so-called deep copy)

int div()
{
    
    
	int a, b;
	cin >> a >> b;
	if (b == 0) 
	{
    
     
		throw invalid_argument("除0错误"); 
	}
	return a / b;
}
template <class T>
class SmartPtr
{
    
    
public:
	//构造函数保存资源 - RAII
	SmartPtr(T* ptr)
		:_ptr(ptr)
	{
    
    }
	//像指针一样使用
	T& operator*()
	{
    
    
		return *_ptr;
	}
	T* operator->()
	{
    
    
		return _ptr;	
	}
	T& operator[](size_t pos)
	{
    
    
		return _ptr[pos];
	}
	//析构函数释放资源
	~SmartPtr()
	{
    
    
		delete[] _ptr;
		cout << "delete[] " << _ptr << endl;
	}
private:
	T* _ptr;
};
void Func()
{
    
    
	SmartPtr<int> sp1(new int[10]);
	SmartPtr<int> sp2(new int[20]);

	*sp1 = 10;
	cout << --sp1[0] << endl;

	cout << div() << endl;
}
int main()
{
    
    
	try
	{
    
    
		Func();
	}
	catch (exception& e)
	{
    
    
		cout << e.what() << endl;
	}

	return 0;
}

2. The use and principle of smart pointers

2.1 auto_ptr

1.
One of the smart pointers first proposed by C++98 is auto_ptr. The solution to copying this pointer is so ridiculous that it has been scolded for many years since the release of C++98, so many companies The use of auto_ptr has been explicitly disallowed.
His implementation plan is to transfer the management right of the resource when the smart pointer is copied, and set the original pointer to the resource as a null pointer, which is a very absurd thing. Because there may be a null pointer access, which will cause a lot of unnecessary trouble. So when you use it normally, don't use this auto_ptr directly.

	template <class T>
	class auto_ptr
	{
    
    
	public:
		//构造函数保存资源 - RAII
		auto_ptr(T* ptr)
			:_ptr(ptr)
		{
    
    }
		auto_ptr(auto_ptr<T>& ap)//非常荒唐的一种做法,委员会不知道怎么想的
			:_ptr(ap._ptr)
		{
    
    
			ap._ptr = nullptr;
		}
		//像指针一样使用
		T& operator*()
		{
    
    
			return *_ptr;
		}
		T* operator->()
		{
    
    
			return _ptr;
		}
		T& operator[](size_t pos)
		{
    
    
			return _ptr[pos];
		}
		//析构函数释放资源
		~auto_ptr()
		{
    
    
			delete[] _ptr;
			cout << "delete[] " << _ptr << endl;
		}
	private:
		T* _ptr;
	};

2.2 unique_ptr

1.
How does unique_ptr solve the problem of copying smart pointers? His implementation strategy is also very simple, directly prohibiting copying. This is why it is called unique, because it is the only one! That is, copying is not allowed.

insert image description here
2.
The = operator of unique_ptr uses move semantics. It also transfers the management rights of resources. After the transfer, p1 will become a null pointer, so we are generally unwilling to use the assignment overload of unique_ptr.

用法1:移动语义转移资源管理权
std::unique_ptr<int> p1(new int(42));
std::unique_ptr<int> p2;
p2 = std::move(p1);  // 转移所有权

用法2:将原生指针赋值给unique_ptr
std::unique_ptr<int> p;
int* ptr = new int(42);
p = ptr;  // 转移所有权

用法3:将unique_ptr的资源管理权释放给原生指针
std::unique_ptr<int> p(new int(42));
int* ptr;
ptr = p.release();  // 释放所有权

2.3 shared_ptr

2.3.1 Copy and assignment (creating reference counts on the heap)

1.
Shared_ptr implements copying and assignment through reference counting, that is, smart pointers not only need to manage a certain resource block, but also open up an int-sized 4-byte space on the heap for storing reference counts. When a smart pointer is copied, multiple smart pointers manage a resource at the same time, and the reference count will be ++, indicating how many managers are managing the current resource. When a smart pointer object is destroyed, the reference count will be - - , when the reference count is reduced to 0, it means that there is no smart pointer to manage this resource, and you need to delete to release this space resource block.

insert image description here
2.
There are more factors to consider in the assignment of shared_ptr. We need to ensure that it cannot be assigned to ourselves, that is, two smart pointers manage a resource block. When these two smart pointers are assigned, We do not do any processing.
For other normal assignments, such as sp1=sp2, it is first necessary to determine whether the reference count of sp1 is 0 after subtraction, if it is 0, it is necessary to release the reference count and the resource block space it manages, and then the reference count of ++sp2 , and then assign the values ​​of _ptr and _pcount to the values ​​of the internal member variables of sp2, so that sp1 will become a smart pointer that manages the resources pointed to by sp2.

//构造函数保存资源 - RAII
shared_ptr(T* ptr = nullptr)
	:_ptr(ptr)
	,_pcount(new int(1))
{
    
    }
shared_ptr(const shared_ptr& sp)
	:_ptr(sp._ptr)
	,_pcount(sp._pcount)
{
    
    
	(*_pcount)++;
}
shared_ptr<T>& operator=(shared_ptr<T>& sp)
{
    
    
	if (_ptr != sp._ptr)
	{
    
    
		++(*sp._pcount);
		release();
		_ptr = sp._ptr;
		_pcount = sp._pcount;
	}
	return *this;
}		
void release()//减减引用计数,判断是否需要释放资源。
{
    
    
	if (--(*_pcount) == 0)
	{
    
    
		delete _pcount;
		delete _ptr;
	}
}
~shared_ptr()
{
    
    
	release();
}

3.
Do not use auto_ptr under any circumstances, because it has been abandoned by many companies. If you don't want to be copied, you can use unique_ptr. If you allow it to be copied, you can use shared_ptr, but when using shared_ptr, be careful about circular references and thread safety issues. We can use weak_ptr to solve circular references, and thread safety can be solved by locking. In addition, the shared_ptr we implemented above releases the resource pointed to by _ptr. The default is delete. What if the resource managed by _ptr is int[10]? Or what about FILE files? Can we still use delete to release resources? Of course not. At this time, something called a custom deleter is needed to solve it, but in fact, the custom deleter is nothing more than a functor, which is nothing new.
Finally, the interviewer may ask everyone to tear up the smart pointer during the interview. Don’t tear up auto_ptr, because this pointer has been discarded. Do you still tear it up? So if there is no clear requirement, then tear a unique_ptr, because this is relatively simple. If it is a little more difficult, you may be required to tear shared_ptr by hand, but shared_ptr may let you tear the thread-safe version by hand, and it will also let you talk about reference counting.

2.3.2 Thread safety (like reference counting, apply for a lock on the heap)

1.
After having the foundation of Linux multithreading, it is easy to understand the following thread safety issues. When multiple threads copy a smart pointer at the same time, the reference count opened by the smart pointer on the heap will be Frequently accessed and operated by multiple threads, since both sp2 and sp3 are copied from sp1, the reference count of sp1 is a shared resource, and both sp2 and sp3 will operate this reference count, so it is necessary to protect the reference count of this shared resource, otherwise it must be There will be problems.

insert image description here

2.
Locking is required to protect shared resources. Can this lock be a static lock? Of course not, then all smart pointer objects use the same lock. What we need to protect is the reference count when multiple threads manage the same resource at the same time. Not all reference counts need to be protected. If a reference Counting is only managed by one thread, so do we still use locking? Of course it is not necessary! Therefore, this lock should also be created dynamically. When multiple threads manage a resource at the same time, since there is only one lock pointed to by multiple smart pointers, you need to apply for a lock if you want to operate on the reference count. We can realize the protection of reference counting operations.

insert image description here

shared_ptr(T* ptr = nullptr)
	:_ptr(ptr)
	,_pcount(new int(1))
	, _pmtx(new mutex)
{
    
    }
shared_ptr(const shared_ptr& sp)
	:_ptr(sp._ptr)
	,_pcount(sp._pcount)
	,_pmtx(sp._pmtx)
{
    
    
	_pmtx->lock();//当多线程在进入拷贝构造函数的时候,下面代码必须是串行执行的!
	(*_pcount)++;
	_pmtx->unlock();
}
void release()//减减引用计数,判断是否需要释放资源。
{
    
    
	bool flag = false;//flag有没有线程安全的问题呢?肯定没有,因为flag在线程栈里面,他并不是线程间的共享资源

	//当多线程在进入release函数的时候,下面代码必须是串行执行的!
	_pmtx->lock();
	if (--(*_pcount) == 0)
	{
    
    
		flag = true;
		delete _pcount;
		delete _ptr;
	}
	_pmtx->unlock();
	//release这里有问题,锁mutex没销毁!引用计数减到0的时候,new出来的_pmtx也是要销毁的。
	if (flag) delete _pmtx;
}

3.
Is shared_ptr thread-safe? We say he is thread safe, but his thread safety refers to his own reference count ++ or - - operation is safe, when shared_ptr copy or assignment or destruction, shared_ptr itself is thread safe, However, the resources managed by shared_ptr are not thread-safe. For example, in the following example, the date class is managed. When multiple threads operate on the shared date class resources at the same time, it can be seen from the results that the date class resources are not thread-safe. of.
In fact, this is also easy to understand. When we say whether shared_ptr is thread-safe, we are talking about the shared_ptr pointer itself. As for whether the managed resources are thread-safe, shared_ptr has no reason to protect it.

insert image description here

insert image description here

insert image description here

2.3.3 Circular reference (weak_ptr, can point to, but does not participate in resource management)

1.
Shared_ptr is actually perfect. It supports copying and assignment, and is thread-safe, but unfortunately, shared_ptr also has its own shortcomings, which is the problem of circular references.
When shared_ptr is used to manage linked list nodes, when the linked list nodes are linked, there will be a type mismatch problem, and the object cannot be assigned to the original pointer, so we simply change _next and _prev to shared_ptr, as long as it is changed to shared_ptr will have a big problem at this time.

insert image description here
2.
When pointing to each other, when the smart pointers n1 and n2 that manage different nodes are destroyed, the reference counts will be reduced to 1, because _next and _prev also manage each other, and _next and _prev also Both are smart pointers.
At this time, there will be a problem of infinite loop management. Neither listnode1 nor 2 can be destroyed, because the node will be destroyed only when the reference count is reduced to 0. (What is the destruction of class member variables? When the class object is destroyed, it will be destroyed.)

insert image description here

3.
In order to solve such a problem, C++11 introduces weak_ptr, which is also very simple to solve such a problem. Just do not allow weak_ptr to participate in resource management. What is not to participate in resource management? To put it bluntly, it is to enable weak_ptr to support the assignment of shared_ptr, but when assigning values, the reference count will not be ++, that is to say, weak_ptr supports the pointing between nodes, but does not support the operation of reference counting.
It is also very simple to implement weak_ptr. We only need to support the assignment operator overload between weak_ptr and shared_ptr, so that the link between nodes can be completed, but when the node pointer _next or _prev is assigned to shared_ptr, shared_ptr The reference count pointed to will not change, thus solving the circular reference problem. At the same time, weak_ptr also supports operations like pointers, and both dereference and member selection operators must be supported.

template <class T>
class weak_ptr
{
    
    
public:
	weak_ptr()
		:_ptr(nullptr)
	{
    
    }
	weak_ptr(const shared_ptr<T>& sp)
		:_ptr(sp.getptr())
	{
    
    }
	weak_ptr<T>& operator=(const shared_ptr<T>& sp)
	{
    
    
		_ptr = sp.getptr();//getptr()仅仅是为了解决类外无法访问shared_ptr的private成员_ptr
		return *this;
	}
	//像指针一样使用
	T& operator*()
	{
    
    
		return *_ptr;
	}
	T* operator->()
	{
    
    
		return _ptr;
	}
private:
	T* _ptr;
};
struct ListNode
{
    
    
	int val;

	wyn::weak_ptr<ListNode> _prev;
	wyn::weak_ptr<ListNode> _next;

	~ListNode()
	{
    
    
		cout << "~ListNode" << endl;
	}
};

insert image description here

2.3.4 Custom deleter (actually a callable object)

1.
The custom deleter sounds awesome, but it is actually very simple. The library provides a default_delete class by default. If you want to implement a custom deleter yourself, you can implement a callable object and pass it to the shared_ptr pointer.

insert image description here

2.
In the following custom deleter, the functor DeleteArray is used for delete [ ], and the functor CloseFile is used to close the pointer, so if the current default method of releasing resources does not meet your needs, you can implement the custom deleter yourself , and pass this deleter to shared_ptr, and shared_ptr will call this callable object in the destructor to release resources.
But our own shared_ptr is a bit different from the library. We cannot pass a custom deleter in the constructor. We can only use the deleter by adding template parameters. The main thing we implement is a simple version. The intention is to analyze the principle clearly. The constructor of shared_ptr in the library can directly support the transfer of deleters. In fact, there are many classes at the bottom layer to solve it, because he also needs to pass this deleter to the destructor. The deleter must It is used in the destructor, so there is an intermediate transfer process.
In order to simplify this process, we directly added the second template parameter of shared_ptr. Through this parameter, we directly create a deleter object in the class, and then use this callable object in the destructor to release resources.

insert image description here

insert image description here

We directly use D to create a class member variable: the custom deleter object _del, and release the pointed resource in the destructor
insert image description here

3. The relationship between smart pointers in C++11 and boost

It's not important to understand the following topics.
The first smart pointer auto_ptr was produced in C++ 98.
C++ boost gave more practical scoped_ptr, shared_ptr and weak_ptr.
C++ TR1 introduced shared_ptr and so on. But note that TR1 is not a standard version.
C++11, introduced unique_ptr and shared_ptr and weak_ptr. It should be noted that unique_ptr corresponds to boost's scoped_ptr. And the implementation principles of these smart pointers refer to the implementation in boost.

3. Special class design and C++ type conversion

1. Four common special classes

Please design a class that cannot be copied

If a class is copied, it will only happen in two cases, one is copy construction and the other is copy assignment.
In C++98, the method adopted is to only declare and not define the copy construction and copy assignment functions, and seal them as private, so that classes that cannot be copied can be designed. If you don't seal private, you can define the function outside the class, and call the copy construction outside the class. If only private is sealed, we also need to prevent copying in the class. The way to prevent copying in the class is to just declare and not define, so that even if copy and assignment can be called in the class, the object cannot be copied because the function is not defined and only Declaring the function name will not enter the symbol table, the function does not have a valid address, and the copy or assignment cannot be completed.

class CopyBan
{
    
    
    // ...
    
private:
    CopyBan(const CopyBan&);
    CopyBan& operator=(const CopyBan&);
    //...
};

In C++11, the use of the keyword delete is expanded. delete can not only release space resources, but also add delete at the end of the member function declaration to indicate that the member function is banned, and the compiler will delete such a class member function , at this time, no matter inside or outside the class, the member function that has been deleted cannot be called.

class CopyBan
{
    
    
    // ...
    CopyBan(const CopyBan&)=delete;
    CopyBan& operator=(const CopyBan&)=delete;
    //...
};

Please design a class that can only create objects on the heap

If you only want to create objects on the heap, the first thing you need to do is to seal the constructor. You are not allowed to create objects outside the class, but you must be able to create objects in the class. If you can’t even create objects in the class, Then there is no object of this class, which is not the result we want to see. Then it is to implement a static method in which the creation of the heap object is completed.
Because it is possible to copy construct objects on the stack by dereferencing the heap object pointer outside the class, so in order to prevent this from happening, we must seal off the copy construction to prevent objects on the stack from being created outside the class.
It doesn't matter whether the copy assignment is sealed or not, because if the above requirements are realized, all objects of this class must be created on the heap, and the assignment between existing objects must be between objects on the heap. Assignment, so there is no need to prohibit copy assignment.

class HeapOnly
{
    
    
public:
	static HeapOnly* CreateObj()
	{
    
    
		return new HeapOnly;
	}
	HeapOnly(const HeapOnly& hp) = delete;//禁拷贝构造
	//拷贝赋值不用禁,因为赋值的前提是已经存在的对象,那已经存在的对象之间的赋值不都还是在堆上吗?

private:
	//封为私有,不让你随便在任意内存位置创建对象,但没有说不让你创建对象
	HeapOnly(){
    
    }
};

The first thing to know is that when the scope of the objects on the stack and data segments ends or the program terminates, the destructor will be called automatically to complete the resource cleanup of the objects, and we do not need to release them manually. Objects on the heap require programmers to manually delete to release space resources, because the life cycle of objects on the heap is not limited by scope, and is only controlled by the process of dynamically allocating and releasing memory.
So in addition to the above method, we can also seal the destructor. Because if the destructor is blocked, objects cannot be created on the stack and data segments, because these objects cannot call the destructor to complete resource cleanup, so objects can only be created on the heap, so the destructor is blocked It is also a practice of creating objects only on the heap.

class HeapOnly
{
    
    
public:
	HeapOnly()
	{
    
    

	}
	void Destroy()
	{
    
    
		this->~HeapOnly();
	}
private:
	~HeapOnly()
	{
    
    

	}
	HeapOnly(const HeapOnly& hp) = delete;
};

Please design a class that can only create objects on the stack

Make the constructor private so that new cannot be used outside the class to create objects in the heap area. If it is implemented in this way, then we must provide a static member method to return the object created on the stack. At this time The copy constructor cannot be disabled, because the return value of the static method StackOnly must be a copy of the value, and at the same time it will bring a problem. In the main, the copy construction can actually be used to define the static area object, so it is not satisfied at this time. Objects are created on the above, so if your requirements are not so strict, then don't prohibit copy construction, allowing objects to be created in both the static area and the stack area. But if your requirements are stricter and you must only create objects on the stack, then disable copy construction. When using objects, use the anonymous object returned by the StackOnly method, or indirectly use an rvalue reference to use this object .
In addition to privatizing the constructor, we can also explicitly disable the operator new function, because the operator new function will be called when a new object is created. After sealing it, objects outside the class cannot be created on the heap. . If this is the case, then there is no need to privatize the constructor and provide static methods. We can directly call the constructor outside the class to create the object on the stack, and then we seal the copy constructor, which can also prevent data segment Objects are created on.
(Review a knowledge point here. When new and delete create and destroy objects, they will call the two global functions opeartor new and operator delete, while operator new actually encapsulates malloc, and operator delete actually encapsulates _free_dbg, while _free_dbg is The macro definition of free, so free is actually _free_dbg, then operator actually encapsulates free. So you can understand operator new/delete as malloc and free, that is, the application and release of space resources.
Therefore, the realization principle of new is to first call the operator new function to complete the application of space resources, and then call the constructor on the applied space to complete the construction of the object. The implementation principle of delete is to call the destructor to complete the cleaning of resources in the object on the requested space (the cleaned up resources are generally dynamically opened space resources), and then call the operator delete function to release the requested space)



方法1: 禁用void* operator new(size_t size)
class StackOnly
{
    
    
public:
	StackOnly() {
    
    }
	void* operator new(size_t size) = delete;
	StackOnly(const StackOnly& st) = delete;
};

int main()
{
    
    
	StackOnly st1;//栈
	static StackOnly st2(st1);//数据段
	StackOnly* st3 = new StackOnly;//堆

	return 0;
}


方法2: 私有化构造函数,提供类静态方法
class StackOnly
{
    
    
public:
	static StackOnly CreateObj()
	{
    
    
		return StackOnly();
	}
	StackOnly(const StackOnly& st) = delete;

	void test()//测试外部使用对象的调用方法
	{
    
    
		cout << "test()" << endl;
	}
private:
	StackOnly() {
    
    }
};

int main()
{
    
    
	//下面是两种使用栈上对象的方法
	StackOnly::CreateObj().test();//栈
	StackOnly&& st1 = StackOnly::CreateObj();//栈
	st1.test();

	static StackOnly st2(st1);//数据段
	StackOnly* st3 = new StackOnly;//堆

	return 0;
}

Please design a class that cannot be inherited

In C++98, the constructor of the base class can be privatized. At this time, the derived class cannot be transferred to the constructor of the base class to complete the initialization of member variables, and the base class cannot be inherited.
In C++11, the final keyword is used to modify the class, indicating that the class is the final class and cannot be inherited.

2. Singleton mode (only one instantiated object)

1.
The singleton mode is a kind of design mode. In fact, when we were learning STL before, we have already been exposed to two design modes, the iterator mode and the adapter mode. One is to encapsulate the underlying details so that the upper layer can achieve A mode that is used uniformly, and one is a mode created by using existing containers through conditional settings and other methods. Design pattern is a summary of engineering existing code design experience. Java likes to talk about 23 design patterns. C++ doesn't like design patterns very much. You only need to understand and use several common design patterns.
The singleton pattern is also a very widely used design pattern. This pattern can ensure that there is only one instance of the instantiated object of this class in the program, and can provide a method to access the unique object. There are two ways to implement the singleton mode: hungry and lazy.

2.
The implementation method of Hungry Man is relatively simple, that is, when the class is loaded, the only object is created, that is to say, when the binary file .exe is loaded into the memory, this object will be created, we You only need to provide a static method GetInstance() to obtain a singleton, this method only returns the address of the only static object, and calls the class member method through this pointer. In addition, to disable copy construction to prevent the creation of a second object, this completes the singleton mode of the hungry man, how about it, is it very simple?

class Singleton
 {
    
    
  public:
     static Singleton* GetInstance()
     {
    
    
          return &m_instance;
     }
     private:
     // 构造函数私有
    Singleton(){
    
    };
    
    // C++98 防拷贝
    Singleton(Singleton const&); 
    Singleton& operator=(Singleton const&); 
      
    // or
      
    // C++11
    Singleton(Singleton const&) = delete; 
    Singleton& operator=(Singleton const&) = delete; 
  
    static Singleton m_instance;
 };
 
  类型 类名::类静态成员(下面代码看起来可能有点绕)
  Singleton Singleton::m_instance;  // 在程序入口之前就完成单例对象的初始化

3.
The implementation of the lazy man is a bit more complicated. It is actually a kind of delayed loading idea, that is, when the function is called, the singleton object will be created. For example, the following GetInstance_lazy, when this function is called, Only then will the function stack frame be created, and new Singleton will be executed, and then the singleton object will be created.
However, GetInstance_lazy has thread safety issues. When multiple threads are competing to execute the new Singleton in GetInstance_lazy, multiple objects may be instantiated. For example, when a thread judges that the null pointer is successful, it is switched out. Then another thread also judges that the null pointer is successful. At this time, it will apply for the memory space of the singleton object, and when the switched out thread is rescheduled, it restores its own context and continues to run backwards. So this thread will also apply for a singleton object once, so that more than 2 singleton objects will appear, which does not meet the requirements of the singleton mode, so in order to ensure thread safety, it needs to be locked!
But locking also involves a problem. For example, we use the most primitive lock to lock and unlock. Of course, this method is also feasible. For example, when we did not want to use RAII for locking and unlocking in POSIX, we have always used it. Both are lock and unlock of native locks, but there may still be some problems here, such as what to do if new throws an exception? That will lead to deadlock, and the thread will not release the lock, so in order to avoid such problems, we choose to use a safer locking method, that is, RAII-style locking, and lock in the constructor. Unlocking is done in the destructor, but there is another detail in the constructor, such as locks are not allowed to be copied and assigned, so we must use references to receive mutexes, class LockGuard is only responsible for locking and unlocking, transfer locks The work should be a matter of the upper layer, so the class member variable of class LockGuard is a referenced mutex. Of course, the referenced member variable must be initialized in the initialization list. (Review a knowledge point, when the member variable in the class appears const modification, the referenced member variable, or the custom object does not have a suitable default constructor, the initialization must be displayed at the position of the initialization list, and the member variable cannot be inside the constructor assign initial value)
In addition, another issue that needs to be explained is the topic of releasing singleton object resources. Singleton objects are created on the heap, so the space resources must be released manually by the programmer. Can we do this inside Singleton? What about implementing a destructor and then deleting the singleton object? Of course not! Because the singleton object cannot call the destructor, because the life cycle of the singleton object is only linked to the operation of application and release, which is different from the objects on the stack area and data segment, and they will die when their life ends. The destructor is called automatically, but the space on the heap does not, and the space resources must be manually released by the programmer. So there are two ways in the following code. The first is to implement a DelInstance interface by ourselves. When we don’t want to use the singleton object, we can manually call this interface to release the space resources of the singleton object. The other is to implement an internal garbage collection class GC, and use this class to define the static object _gc, which is a member variable of the singleton class, so when the program ends, the life of the static object _gc ends, it will Automatically call its own destructor, and GC itself is an internal class, which can directly access the static pointer _SinglePtr, so when _gc calls its own destructor, the space resources of the singleton object will also be destroyed with _gc But if it is released, this is an automatic method, and we don't need to care about anything.
The last thing I need to say is about double check. Using RAII-style locking can indeed ensure the thread safety of GetInstance_lazy, but its efficiency is not very high, because when each thread calls, it needs to be locked first. Enter Only after the critical section can we know whether _SinglePtr is a null pointer, and then we can return the pointer _SinglePtr that has allocated memory. Can it make the judgment more efficient? Of course it is possible, we can judge whether it is a null pointer before locking, then the judgment logic at this time will not be a serial judgment, but a parallel + concurrent judgment, and the efficiency will naturally increase.

insert image description here
Another way to realize the lazy is to directly return the static object instead of returning the pointer. Of course, this method can also achieve lazy loading, because only when the function GetInstance is called, the static object will be opened. Objects, the objects created in this way are stored on the data segment, and the above methods are stored in the heap area.
insert image description here

4.
After introducing the implementation of the hungry man mode and the two lazy man modes, let's talk about their shortcomings and advantages.
Hungry mode → data segment
a. When the singleton object initializes too much data, it will slow down the program startup speed, and the speed of execution flow entering the main function will be very slow.
b. There is no problem of thread safety, because the singleton object has been opened and initialized when the class is loaded.
c. When multiple singleton objects are initialized with dependencies, the Hungry Mode cannot be controlled, which depends entirely on the work of the operating system loading files into memory.
Lazy mode → heap
a. Singleton objects are created only when needed, so it will not affect the startup speed of the program. After the execution flow enters the main function, the singleton object will only be created when GetInstance is called.
b. There is a problem of thread safety, so locks and double checks are needed to ensure safety and efficiency.
c. You can control the initialization sequence of multiple singleton objects by yourself by manually calling GetInstance.
Lazy mode → data segment
a. Before C++11, the initialization of local static objects cannot be guaranteed to be thread-safe. After C++11, the initialization of static local objects can be guaranteed to be thread-safe. So this method may not be thread-safe for compilers that do not support C++11 very well. When using this method, pay attention to the usage environment. If C++11 supports it well, there is no problem with this method.

5.
Although deleting the copy construction is theoretically enough, but in order to better ensure the correctness of the singleton mode, the two functions of copy construction and copy assignment are usually deleted at the same time.

insert image description here

3. Four types of forced type conversion in C++

1.
C++ hates the explicit type conversion and implicit type conversion of C language, because if the implicit type conversion is not careful, it will bring many unexpected errors in advance, such as the type promotion between size_t and int in the past, and in addition The display type conversion of the C language is too general for scenarios, and they are all written in the same form, making it difficult to track wrong conversions.
Therefore, C++ directly adds four types of forced type conversions, hoping that programmers can use standard explicit type conversions instead of implicit type conversions and general explicit type conversions before C language. But this is just an expectation. C++ must be compatible with C language, so the previous type conversion method cannot be abandoned, and it is still left as a historical burden.

static_cast
Static_cast is used for conversion of non-polymorphic types. Any implicit type conversion of the compiler can be converted with static_cast, but static_cast cannot be used for conversion of two unrelated types.

int main()
{
    
    
    // 隐式类型转换
    int i = 1;
    //C++规范转换 -- static_cast适用于相似类型的转换(这些类型的表示意义差不多)
    double d = static_cast<double>(i);
    printf("%d, %.2f\n", i, d);
}

reinterpret_cast

reinterpret means to reinterpret, it can be used to convert between pointer types, and can also convert pointer types to integer types, such as converting a void* type pointer to a pointer of an actual type, or converting a derived class pointer to a base class pointer.

int main()
{
    
    
	int i = 0;
	// 显示的强制类型转换
    int* p1 = &i;
    //C++规范转换 -- reinterpret_cast适用于不相关的类型之间的转换。
    //这里不能用static_cast,因为int和int*不算是相似类型。
    int address = reinterpret_cast<int>(p1);
    int* p2 = reinterpret_cast<int*>(i);
    printf("%x, %d\n", p1, address);
}

const_cast
The most common use of const_cast is to delete the const attribute to facilitate the assignment of variables.
Therefore, the variable modified by const is not stored in the .rodata section. It can be modified. The const attribute of the variable can be deleted through const_cast, thereby modifying the variable. Such a variable is called a constant variable. (Generally speaking, some people like to directly call the .rodata section the code section. Of course, this is also possible, because the location of the .rodata section and the code section are very close, and both are read-only attributes, so there is no big problem in calling it this way.)

The following is a classic problem. The compiler will optimize the value of the variable modified by const. When fetching this value, it will not go to the memory to fetch the value, but go directly to the register to fetch it. Under vs, the values ​​are all It is not stored in the register, but directly pressed into the function stack frame as a corresponding symbol. Therefore, when printing, in order to optimize, it will not go to the memory to get the modified value of a, but directly get the value in the function stack frame or the value of the register, so the printed results are 2 and 3, The values ​​seen in the monitoring window are in the memory, so they are all 3. If we don't want the compiler to make such an optimization, we can consider using the volatile keyword to maintain memory visibility, so that the printed value of a will not be optimized.

int main()
{
    
    
    //C++规范转换 -- const_cast 去掉const属性,单独分出来这个类型转换,警示你C++的const很危险,用的时候谨慎一些
    //const_cast最常用的用途就是删除变量的const属性,方便赋值
    volatile const int a = 2;//const变量并不是存在于常量区,而是存在于栈上的,属于常变量
    int* p2 = const_cast<int*>(&a);
    *p2 = 3;
    cout << a << endl;
    cout << *p2 << endl;
}

insert image description here
The following code is another way to modify the value, that is, to modify the value of the constant variable by reference. The experimental results are the same as above. After adding the volatile keyword, the memory visibility can be maintained.

int main()
{
    
    
	volatile const int a = 10;
	int& b = const_cast<int&>(a);
	b = 100;

	cout << a << endl;
	cout << b << endl;
}

insert image description here

dynamic_cast

dynamic_cast is used to convert a pointer or reference of a parent class object to a pointer or reference type of a subclass object, which we call dynamic conversion.
As for the pointer or reference of the subclass object to the pointer or reference of the parent class object, this process is natural and does not require forced conversion. Only when it is reversed does it need to be cast.
For example, in the following code, you can convert the ptr of the base class type to the dptr of the derived class type. If ptr points to the parent class, there will be a risk of out-of-bounds access. If ptr points to the subclass, there is no problem, but the pointer It is enough to move a few bytes in the access range.
When the dynamic_cast conversion type fails, a null pointer is returned, and if the conversion succeeds, a valid pointer to the derived class object is returned.

class Base
{
    
    
public:
    virtual void f() {
    
    }

    int _base = 0;
};
class Derive : public Base
{
    
    
public:
    int _derive = 0;
};
void Func(Base* ptr)
{
    
    
    // C++规范的dynamic_cast是安全的,如果ptr指向的是父类对象,那么下面就会转换失败,返回0。指向子类则转换成功
    Derive* dptr = dynamic_cast<Derive*>(ptr);//父类转成子类,存在越界访问的风险

    cout << dptr << endl;

    if (dptr)
    {
    
    
        dptr->_base++;
        dptr->_derive++;//这里就会发生越界访问,因为子类私有成员是不属于父类访问的,父类只能访问到子类中父类的那一部分

        cout << dptr->_base << endl;
        cout << dptr->_derive << endl;
    }

}
int main()
{
    
    
    Base b1;
    Derive d1;

    Func(&b1);
    Func(&d1);

    return 0;
}

Below is the result of running the program.
insert image description here

The following figure explains why when ptr points to the parent class, there will be an out-of-bounds access problem.
insert image description here
dynamic_cast can only be used for polymorphic types. If it is not a polymorphic type, dynamic_cast cannot be used
insert image description here
Let's review the knowledge points of static binding and dynamic binding. In fact, if static binding and dynamic binding are simply understood, you can understand that one enters the symbol table and one does not enter the symbol table. The compiler can enter the symbol table The specific function to be called is determined during compilation. If the symbol table is not entered, it is necessary to dynamically find out what the specific function to be called is during the running of the program.
insert image description here

Supplementary topic 1:
RTTI: short for Run-time Type identification, that is: runtime type identification.
C++ supports RTTI through the typeid operator, dynamic_cast operator, etc.
And decltype is not really run-time type information (RTTI). It's just compile-time type inference. Although decltype and RTTI have different purposes, they can complement each other. decltype can be used for compile-time type checking and inference, avoiding the runtime overhead of using RTTI. RTTI can do more dynamic type checking and conversion at runtime, which is not possible with decltype of.

Supplementary topic 2:
Common interview questions: What are the four types of conversions in C++? What are the application scenarios for the four types of conversions?

Guess you like

Origin blog.csdn.net/erridjsis/article/details/130345670