C + + exception handling mechanism from simple to deep, and the bottom analysis of function call assembly process Bottom layer simulation of C++11 intelligent pointer

I abnormal

1.1. Programming model and basic usage of exceptions

  • Let's use it to explain the above model
double Div(int a, int b) {
	if (b == 0) throw "Zero Div";//Throw a string constant
	cout << "If an exception is thrown, Cut yourself and all at once, Will not execute" << endl;
	return (double)a / (double)b;
}

int main() {
	try {
		cout << Div(4, 0) << endl;
	}
	catch (int errid) {
		//Capture error code shaping for processing
		cout << "error number: " << errid << endl;
	}
	catch (const char* msg) {
		cout << "error message" << msg << endl;
	}
	cout << "Exception handling is over, Keep going backwards, Unless exception handling is interrupted" << endl;
	return 0;
}
  • Analysis: since the exception is thrown, the subsequent code will no longer be executed
  • After exception handling, as long as the process is not terminated, continue exception handling to finish the execution of the next statement

1.2. Custom exception class

class MyException {
public:
	MyException(int errid, string errmsg) 
		: _errid(errid)
		, _errmsg(errmsg) 
	{}

	const string& what() const noexcept {
		return _errmsg;
	}

	int GetErrid() const noexcept {
		return _errid;
	}
private:
	int _errid;
	string _errmsg;
};

1.3. Finding process of exception handling function (looking back along the function stack)

class MyException {
public:
	MyException(int errid, string errmsg) 
		: _errid(errid)
		, _errmsg(errmsg) 
	{}

	const string& what() const noexcept {
		return _errmsg;
	}

	int GetErrid() const noexcept {
		return _errid;
	}
private:
	int _errid;
	string _errmsg;
};


void h() {
	throw MyException(0, "Go back to the exception handler along the function call direction");
}

void g() {
	try {
		h();
	}
	catch (int errid) {
		cout << "g()The error code is: " << errid << endl;
	}
}

void f() {
	try{
		g();
	}
	catch (const runtime_error& re) {
		cout << "f Processing function in function: " << re.what() << endl;
	}

}

int main() {
	try {
		f();
	}
	catch (const MyException& e) {
		cout << "Processing function in main function: " << e.what() << endl;
	}
	catch (...) {//	Generally, in order to handle exceptions, he will be added at the end
		cout << "Processing function in main function: " << "Unknown exception caught" << endl;
	}
	return 0;
}
  • The result, of course, is to go back along the function stack to find the corresponding processing function in the main function for processing

1.4. Re throw of exceptions, multi catch processing (more outer layer processing)

1.4.1 prevent memory leakage during exception handling

class Test {
public:
	Test() {
		//default ctor 
	}	
	~Test() {
		cout << "dtor" << endl;
	}
};

int main() {
	try {
		//Test t;    There must be no problem with the stack object
		Test* pt = new Test;//What about the pile area?
		throw 1;
        delete pt;   //What happens??? No execution, memory leak
	}
	catch (int errid) {
		cout << "Will I call destructors???" << endl;
		cout << errid << endl;
	}
	return 0;
}

  • The result is that the destructor is not called automatically. What's the matter???? Indicates a memory leak
  • Therefore, memory leakage is also a problem that needs special attention in write exception handling

1.4.2 multi catch processing of exceptions. The inner layer does not process and continues to throw out,

double Div(int a, int b) {
	if (b == 0) {
		throw MyException(0, "Zero Div");
	}
	return (double)a / (double)b;
}

void func() {
 // Here you can see that if an exception except 0 error occurs, the following array is not released.
 // Therefore, the exception is not handled after it is captured here. The exception is still handed over to the outside for processing. After it is captured here
 // Throw it back.
	int* arr = new int[4]{ 0 };
	try{
		int a, b;
		cout << "Please enter the divisor and divisor: " << endl;
		cin >> a >> b;
		cout << Div(a, b) << endl;
	}
	catch (...) {	//I only deal with memory. As for information and so on, I continue to throw it to other functions for processing		
		cout << "delete[]" << endl;
		delete[] arr;
		throw;//Keep throwing it out
	}
}
int main() {
	try {
		func();
	}
	catch (const MyException& e) {
		cout << "error message: " << e.what() << endl;
	}
	return 0;
}

1.5. Inheritance of exception class (polymorphic processing, base class object references subclass object)

  • Ask the first question?? Why should we inherit to deal with it? Can't we write by ourselves???

First of all, it's not impossible to write by ourselves, but the scene is inappropriate. It's great for everyone to write an exception class. But when we need to call the corresponding processing function, do we need to modify the class name everywhere to call the corresponding processing function? Where would you like to go??

Can we use the base class to refer to subclass objects? In this way, when calling the interface, we only need to use the base class to call the corresponding processing function. We only need to inherit the base class for rewriting, and the required processing function is OK

Review the definition of polymorphism: passing in different objects and calling the same function will produce different effects. In fact, subclasses override the virtual function of the base class

So now, OK. In fact, the company generally has its own exception handling mechanism and its own exception handling class. Well, when we use it, we inherit it according to the situation and rewrite the virtual function

  • eg: simply write by hand
class MyException {
public:
	MyException(int errid, string errmsg)
		: _errid(errid)
		, _errmsg(errmsg)
	{}

	virtual string what() const noexcept {
		return _errmsg;
	}

	virtual int GetErrid() const noexcept {
		return _errid;
	}
protected:
	int _errid;
	string _errmsg;
};
//Inherited subclasses
class SqlException : public MyException {
public:
	SqlException(int errid = 0, const char* msg = "") 
		: MyException(errid, msg)
	{}
	virtual string what() const noexcept {
		string tmp("SqlException: ");
		tmp += _errmsg;
		return tmp;
	}
};
class CacheException : public MyException {
public:
	CacheException(int errid = 0, const char* msg = "")
		: MyException(errid, msg)
	{}
	virtual string what() const noexcept {
		string tmp("CacheException: ");
		tmp += _errmsg;
		return tmp;
	}

};
class HttpServerException : public MyException {
public:
	HttpServerException(int errid = 0, const char* msg = "")
		: MyException(errid, msg)
	{}
	virtual string what() const noexcept {
		string tmp("HttpServerException: ");
		tmp += _errmsg;
		return tmp;
	}
};

II This paper introduces some assembly instructions and registers, and analyzes the assembly process of function call

2.1. Register assembly instruction basis

  • epi: instruction register, which stores the address of the next instruction
  • esp # and ebp are both pointer registers
  • esp: stack top register, pointing to the top of function stack, stack pointer
  • ebp: stack bottom register, pointing to the bottom of the stack, frame pointer
  • push: input data into function stack and modify {esp
  • pop: data out of function stack, modify {esp
  • sub: subtraction operation
  • Add: add operation
  • Call: function call
  • jump: enter calling function
  • ret: the return address after the function call, which returns the outer calling function
  • move: data transfer, stack top and bottom changes

 

2.2. Analysis of assembly instruction part of function call stack (VS2019)

  • Parameter reverse push stack
  • call calls the function and automatically pushes the RET address when entering (entering the called function)
  • ret address of the push before ret at the end of the function call (return the calling function)
  • Cleaning parameters depends on the processing mechanism. Some are cleaned by the called function itself, and some are cleaned by the calling function

III Realization of intelligent pointer from shallow to deep

3.1 introduction to intelligent pointer RAII Technology

  • First of all, find out why we need smart pointers. Smart pointers are a class for resource recycling and use management pointed to by pointers.
  • Memory leak: what is called memory leak? Memory leak refers to that we lose control of a piece of memory, but we don't release it before losing control The operating system allocates the memory from the heap to our process. If we don't actively delete it, the operating system can't allocate it to other processes during the process. Then the owner who originally allocated the memory doesn't delete it after using it, and this memory can't be allocated, so it's equivalent to a memory leak
  • Harm of memory leakage: long-term running programs have memory leakage, which has a great impact, such as operating system, background services, etc. memory leakage will lead to slower and slower response and finally get stuck.  
  • RAII idea: RAII (Resource Acquisition Is Initialization) is a simple technology that uses the object life cycle to control program resources (such as memory, file handle, network connection, mutex, etc.).
  • Therefore, the intelligent pointer appears. The life cycle of using the intelligent pointer to define the object is limited. The intelligent pointer is defined as a stack object. When the function ends, the destructor will be called automatically, and the resources will be released naturally (to the unique_lock to avoid deadlock, the smart pointers here are all based on this idea)

3.2 basic framework model of intelligent pointer

It's OK to have construction, deconstruction and basic pointer operation. This is a general framework model that doesn't need to be possessed

template<class T >
class SmartPtr {
public:
	SmartPtr(T* ptr)
		: _ptr(ptr) 
	{}

	~SmartPtr() {
		if (_ptr)
			delete _ptr;
	}

	T& operator() {
		return *_ptr;
	}

	T* operator() {
		return _ptr;
	}
private:
	T* _ptr;
};

3.3 characteristic analysis of four kinds of smart pointers

  • auto_ptr: one of the four kinds of smart pointers. The problem is to copy and construct. After assignment, the pointer will be suspended. If you operate on the suspended pointer, an error will be reported
  • unique_ In order to solve the problem of copying and suspending the pointer of overloaded PTR, PTR is directly copied and assigned
  • shared_ptr: it also solves the problem of pointer suspension after auto copy and assignment, but it is not realized by prohibiting copy and assignment overload, but by a way called reference technology to avoid pointer suspension. Whether it is assignment or copy, it only operates the reference count + 1. After copying, The original pointer will not be suspended because it is transferred to the copy, but shares the same address and memory resources with the copy.        
  • Note: for the operation of shared resources, attention must be paid to protection to avoid the problem of function reentry. Mutex lock is used to ensure that only one thread writes to the shared critical resources at a time Therefore, all + and - Operations counting to references need lock protection
  • weak_ptr: in order to solve the problem of circular reference, circular reference, that is, there is a reference counting relationship between each other, and the release of each other is limited. The real delete...

  •  shared_ptr solves the principle of circular reference: when counting references, the_ pre and_ Change the next pointer to weak_ptr smart pointer
  • The principle is, node1 - >_ next = node2; And node2 - >_ prev = node1; Time weak_ptr_ Next and_ Prev does not increase the reference count of node1 and node2.
struct ListNode
{
	int _data;
	weak_ptr<ListNode> _prev;
	weak_ptr<ListNode> _next;
	~ListNode() { cout << "~ListNode()" << endl; }
};

3.4 implementation code of three kinds of intelligent pointer simulation

auto_ptr 

namespace tyj {
	template<class T>
	class auto_ptr {
	public:
		auto_ptr(T* ptr) 
			: _ptr(ptr) 
		{}

		auto_ptr(auto_ptr<T>& ap) {
			_ptr = ap._ptr;//Transfer resources
			ap._ptr = nullptr;//The original pointer is suspended
		}

		auto_ptr<T>& operator=(auto_ptr<T>& ap) {
			if (this != &ap) {
				if (_ptr)
					delete _ptr;//Clean up existing resources
				_ptr = ap._ptr;
				ap._ptr = nullptr;
			}
		}
		~auto_ptr() {
			if (_ptr)
				delete _ptr;
		}

		T& operator*() {
			return *_ptr;
		}

		T* operator->() {
			return _ptr;
		}
	private:
		T* _ptr;
	};

}

int main() {
	int* pint = new int[4]{ 0 };
	tyj::auto_ptr<int> smartp(pint);
	*smartp = 1;
	cout << *smartp << endl;
	tyj::auto_ptr<int> smartp2(smartp);
	//*smartp = 1;
	//cout << *smartp << endl;
	//smartp can no longer be written. It has been suspended
	return 0;
}

unique_ptr: directly prohibit copy construction and assignment overload

namespace tyj {
	template<class T>
	class unique_ptr {
	public:
		unique_ptr(T* ptr = nullptr)
			: _ptr(ptr)
		{}

		~unique_ptr() {
			if (_ptr)
				delete _ptr;
		}

		T& operator*() {
			return *_ptr;
		}

		T* operator->() {
			return _ptr;
		}
	private:
		T* _ptr;
		unique_ptr(unique_ptr<T>& up) = delete;	//Disable missing constructors
		unique_ptr<T>& operator=(unique_ptr<T>& up) = delete;//Disable copy overload
	};
}

shared_ptr: using reference counting: because reference counting is a critical resource, its operation must be protected by mutual exclusion and atomic operation... (protect critical resources)

namespace tyj {
	template <class T>
	class shared_ptr {
	public:
		shared_ptr(T* ptr = nullptr) 
			: _ptr(ptr)
			, _pmtx(new mutex)
			, _pRefCount(new int(1))
		{}
		~shared_ptr() {
			Release();		//Release resources
		}
		//Increase reference count, assign copy
		shared_ptr(const shared_ptr<T>& sp)
			: _ptr(sp._ptr)
			, _pRefCount(sp._pRefCount)
			, _pmtx(sp._pmtx) {
			AddRefCount();//Increase reference count
		}
		shared_ptr<T>& operator=(shared_ptr<T>& sp) {
			if (this != &sp) {
				Release();//Release possible resources first
				_ptr = sp._ptr;
				_pmtx = sp._pmtx;
				_pRefCount = sp._pRefCount;
				AddRefCount();//Increase reference count
			}
			return *this;
		}

		int UseCount() { 
			return *_pRefCount; 
		}

		T* Get() {
			return _ptr;
		}

		T& operator*() {
			return *_ptr;
		}

		T* operator->() {
			return _ptr;
		}

	private:
		void AddRefCount() {//Increase reference count
			_pmtx->lock();
			++(*_pRefCount);
			_pmtx->unlock();
		}

		void Release() {//Free the resource, minus the reference count
			bool deleteFlag = 0;//Judge whether resources need to be released, and the real delete
			_pmtx->lock();
			if (--(*_pRefCount) == 0) {
				delete _ptr;
				delete _pRefCount;
				deleteFlag = 1;
			}
			_pmtx->unlock();

			if (deleteFlag) {
				delete _pmtx;//Finally release the lock
			}
		}

	private:
		T* _ptr;//Pointer to the managed resource
		int* _pRefCount;//Reference count pointer
		mutex* _pmtx;//mutex 
	};
}

IV summary

  • This paper first introduces the basic model of exception, and then puts forward the focus of exception learning
  • 1. try {protection code} catch (capture type) {exception handling logic block}
  • 2. You can customize the exception class, inherit the standard class, and then rewrite the handling function and exception information function
  • 3. The exception handling function is found back along the function stack and in the opposite direction of function call. If the handling function cannot be found in the called function, go back to the calling function to find the handling function
  • 4. the inheriting reason of exception class: in order to use the base class to receive all derived class objects, then calling the same function interface to achieve different call results and different processing mechanisms. Unify the user's calling interface and class (the user only needs to use the base class object reference to receive, and it's OK to drop the function. As for the specific object passed in, it doesn't need to be controlled)
  • 5. Learning of registers and assembly instructions, esp: stack top register, ebp: stack bottom register, epi: instruction register, etc
  • 6. There are four kinds of smart pointers. The iterative evolution of smart pointers has a reason:
  • auto_ptr problem is that the ontology pointer will be suspended after copying or assignment   unique_ In order to solve the problem of pointer dangling, PTR directly prohibits assignment overload and copy construction_ PTR solves the problem of suspension by adding reference count, weak_ptr , for shared_ The circular reference of PTR refers to each other and restricts each other, resulting in the problem that resources cannot be completely released_ The underlying principle of PTR is realized by + + without reference counting when circular cross referencing

Keywords: C++ Back-end Interview

Added by ChrisBoden on Fri, 25 Feb 2022 15:50:17 +0200