Detailed explanation of c++---exception

The traditional way of handling errors in C language

The first type:
the program terminates the program while it is running, such as assert

int main()
{
    
    
	int i = 0;
	scanf("%d", &i);
	assert(i != 2);
	return 0;
}

If the expression result in the assert brackets is false, then assert will interrupt the program and report an error. Therefore, using assert can help us determine some possible errors in the program. In the above code, we enter a 2 and we can see that the program reports an error because of assert. Aborted:
Insert image description here
Another example is that a serious error is encountered during the running of the code. For example: division by 0 errors in expressions, out-of-bounds access errors in arrays and errors caused by wild pointers will all cause errors during the running of the program. The program is terminated. For example, a divide-by-zero error occurs in the following code:

int main()
{
    
    
	int i;
	cin >> i;
	int z = i / 0;
	cout << z << endl;
	return 0;
}

Insert image description here
So this is the first form of error reporting in C language during access during the running of the program.
The second type:
return an error code. For example, the code we wrote above returns a very strange error code. The error code will have a shortcoming: the programmer needs to find the corresponding error by himself. Is this very complicated? Trouble. For example, the interface functions of many libraries in the system express errors by putting error codes in errno, so this is the second error reporting method in C language to find errors through error codes.

The use of c++ exceptions

C++ requires three keywords to handle errors: try, throw, catch. Try is a block. We put the code that may cause errors into this block. Throw is the exception object that is thrown. When a problem may occur somewhere in the code, we Just use throw to throw an exception object. This object can be of any type. Outside the try block, we use the catch keyword to capture the exception objects thrown by throw and analyze these objects. Then only through the language Description: I guess you still don’t know the process of throwing an exception, so let’s take you through a piece of code to understand the above process. First, we have a function called func. In the function, we need to receive two parameters and print these The result of dividing two parameters, then at this time we create a function to divide the two parameters and return the result of the division, then the code here is as follows:

double Division(int a, int b)
{
    
    
   return ((double)a / (double)b);
}
void Func()
{
    
    
    int len, time;
    cin >> len >> time;
    cout << Division(len, time) << endl;
}

But there is a problem when writing this way. In mathematics, the divisor is not allowed to be 0, so an exception may occur here. So when we find the exception, we have to use throw to throw the exception. Then we can use the exception object thrown here. Using a string, when we find that the divisor is 0, we use throw to throw a string. The content of the string is: Division by zero condition!, then the code here is as follows:

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

After the function is written, we can use the Func function in the main function to run the function we wrote. Because an exception may be thrown in this function, we put the Func function in the try block:

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;
}
int main()
{
    
    
    try 
    {
    
    
        Func();
    }  
    return 0;
}

If an exception is thrown in the try block, you have to use catch outside the try block to receive it. Because what is thrown is a string type exception object, when you use catch to receive it, you have to write it in the brackets after the catch. String type parameters to receive, then the code here is as follows:

int main()
{
    
    
    try 
    {
    
    
        Func();
    }
    catch (const char* errmsg)
    {
    
    
        cout << errmsg << endl;
    }
    catch (...) 
    {
    
    
        cout << "unkown exception" << endl;
    }   
    return 0;
}

The corresponding statement can be printed on the screen inside and outside the statement block corresponding to catch to tell the user that there is a problem here and what is the cause of the problem. Then our example is complete here. Run it as follows and you can see if If the divisor of the input data is not 0, the catch statement here will not catch the exception, and the results of the two data calculations will be printed: if the
Insert image description here
input divisor is 0, the contents of the catch statement will not be output. Result:
Insert image description here
Then this is the use of exceptions, through try catch throw to realize the discovery and capture of exceptions.

unusual rules

The first one:
When an exception is thrown in the code, the content behind the throw scope will not be executed. For example, the following code:

void func(int x)
{
    
    
	if (x == 10)
	{
    
    
		throw(10);
	}
	cout << "如果抛出了异常我这里就不会打印" << endl;
}
int main()
{
    
    
	try
	{
    
    
		int x;
		cin >> x;
		func(x);
	}
	catch (int x)
	{
    
    
		cout << "传递的值不能为10" << endl;
	}
	return 0;
}

If we enter an integer number other than 10, a paragraph will be printed on the screen:
Insert image description here
If we enter 10, that paragraph will not be printed on the screen but will tell us that the entered value cannot be 10:
Insert image description here
Second:
After the catch statement is executed, you can continue with the following content, such as the following code:

void func(int x)
{
    
    
	if (x == 10)
	{
    
    
		throw(10);
	}
	cout << "如果抛出了异常我这里就不会打印" << endl;
}
int main()
{
    
    
	try
	{
    
    
		int x;
		cin >> x;
		func(x);
	}
	catch (int x)
	{
    
    
		cout << "传递的值不能为10" << endl;
	}
	cout << "这里可以接着执行后面的内容" << endl;
	return 0;
}

When we enter a 10, we can see that there is a catch statement here, but when the code inside is executed, the content after the catch statement will be executed: Third: There may be
Insert image description here
multiple
catch statements to capture when catching an exception. Different exceptions, such as the following code:

void func(int x)
{
    
    
	if (x == 10){
    
    throw(10);}
	if (x == 9){
    
    throw("x is nine");}
	cout << "如果抛出了异常我这里就不会打印" << endl;
}
int main()
{
    
    
	try
	{
    
    
		int x;
		cin >> x;
		func(x);
	}
	catch (const char* errmsg){
    
    cout << errmsg << endl;}
	catch (int errid){
    
    cout << errid << endl;}
	return 0;
}

If the value we enter is 10, the exception object thrown by throw here is 10, so when catching, it is an integer type capture: if the value we enter is 9, the exception object thrown by throw here is a character
Insert image description here
. string, then the catch is a capture of the string char* type:
Insert image description here
although there can be multiple catch statements, the thrown exception can only be caught by one catch statement when being caught. Once a catch statement is successfully captured , then other catch statements will not be executed, so the exception is raised by throwing the object. The type of the object determines which catch processing code should be activated.

Point 4:
If the thrown exception does not match a type, the compiler will terminate the program and issue a warning. For example, the above code sometimes throws a string type exception object, and the string type is const char * Then if we make the following modifications to the above code and change the reception type of const char * to char*, can it still be received normally? For example, the following code:

void func(int x)
{
    
    
	if (x == 10){
    
    throw(10);}
	if (x == 9){
    
    throw("x is nine");}
	cout << "如果抛出了异常我这里就不会打印" << endl;
}
int main()
{
    
    
	try
	{
    
    
		int x;
		cin >> x;
		func(x);
	}
	catch (char* errmsg){
    
    cout << errmsg << endl;}
	catch (int errid){
    
    cout << errid << endl;}
	return 0;
}

The running result of the code is as follows:
Insert image description here
You can see that an error is reported here, and the exit code of the program is 3.
Insert image description here
The fifth point
is that although there cannot be the same catch statement of the same type in the same scope, it can be in different scopes. The same catch statement exists, for example the following code:

double Division(int a, int b)
{
    
    
    // 当b == 0时抛出异常
    if (b == 0)
        throw "Division by zero condition!";
    else
        return ((double)a / (double)b);
}
void Func()
{
    
    
    try
    {
    
    
        int len, time;
        cin >> len >> time;
        cout << Division(len, time) << endl;
    }
    catch (const char* errmsg){
    
    cout<<"Func->" << errmsg << endl;}
    
}
int main()
{
    
    
    try {
    
    Func();}
    catch (const char* errmsg){
    
    cout << "main->" << errmsg << endl;}
    return 0;
}

In the above code, we use the catch statement in the main function to catch the const char * type exception. In the func function, we also use catch to catch the const char * type exception. Then there will be a problem here: there are two catches here. Instead of making const char * type exceptions, which catch will catch us when we throw an exception? Then we call this form a call chain. When catching an exception, the call chain selects the one that matches the type and is closest to the location where the exception is thrown. If we know the type matching, how do we understand the location that is closest? Let's analyze the above code. First, the main function calls the func function, and in the func function, the Division function is called. Then we can draw the following picture: In the Division function, we threw an exception, and in the func
Insert image description here
and In the main function, we all have catches to receive exceptions, so the distance here can be judged based on the relationship between function calls. The main function calls the func function and then calls the Division function, and the func function directly calls the Division function, so The func function is closer, so when the catches of the func function and the main function both match the exception object, then the matching catch here is the catch in the closer func, then the result of the above code is as follows: If the func
Insert image description here
function If the catch in does not match the type of the exception object, it will still match the catch in the main function. For example, change the catch type in func to int:

void Func()
{
    
    
    try
    {
    
    
        int len, time;
        cin >> len >> time;
        cout << Division(len, time) << endl;
    }
    catch (int errmsg){
    
    cout<<"Func->" << errmsg << endl;}
}

Then the result of running the above code is as follows:
Insert image description here
Sixth point:
Throwing an exception may cause a series of problems, such as the following code:

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* p = new int[10];
    int len, time;
    cin >> len >> time;
    cout << Division(len, time) << endl;
    cout << "delete " << p << endl;
    delete[] p;   
}
int main()
{
    
    
    try {
    
    Func();}
    catch (const char* errmsg){
    
    cout << "main->" << errmsg << endl;}
    return 0;
}

There is no exception caught in the func function, but in the func function we use new to apply for a space, and use delete to release the space after calling the Division function. If no exception is thrown, there will be no problem. For example, the picture below:
Insert image description here
5 is printed normally and the space is released, but once an exception is thrown here, a memory leak will occur. For example, the picture below: the
Insert image description here
reason is that an exception occurs in the Division function, throw After an exception is thrown, there is no catch statement in the func function to receive it, so it will go directly to the main function and be received by the catch in the main function, but this means that the remaining code of the func function will not be executed. So a leak occurred. In previous studies, we believed that memory leaks were mostly caused by the person who wrote the code forgetting to use delete to release space. Now everyone should be able to find that exception throwing can also cause memory leaks. The reason is that the throwing and receiving of exceptions cause the program to skip some important code. The solution here is to receive this type of exception in the func function. We will not show this method as mentioned above. If anyone asks here If the exception must be received in the main function, how to solve it? The answer is to receive the exception and then throw the exception. Receive the thrown exception in the func function, release the memory space and then re-throw the exception. In this way, we can ensure that the exception can be received in the main function and the requested exception can be guaranteed. The space can be released normally, then the code here is as follows:

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* p = new int[10];
    int len, time;
    cin >> len >> time;
    try
    {
    
    
        cout << Division(len, time) << endl;

    }
    catch (int errmsg)
    {
    
    
        cout << "delete " << p << endl;
        delete[] p;
        throw errmsg;
    }
}
int main()
{
    
    
    try {
    
    Func();}
    catch (const char* errmsg){
    
    cout << "main->" << errmsg << endl;}
    catch (...) {
    
    cout << "unkown exception" << endl;}   
    return 0;
}

The running results of the code are as follows:
Insert image description here
Point 7:
You may encounter various exceptions when writing code, so in order to deal with some exceptions that we may not predict, C++ provides such a type of capture: catch(...)This means that it can capture any Type of exception, for example the following code:

void func(int x)
{
    
    
	if (x == 9) {
    
     throw(10); }
	else if (x == 10) {
    
     throw(1.1); }
	else {
    
     throw("hello"); }
}
int main()
{
    
    
	int x;
	cin >> x;
	try {
    
    func(x);}
	catch (int x){
    
    cout << "出现了异常" << endl;}
	catch (...){
    
    cout << "出现了未知异常" << endl;}
	return 0;
}

When we enter 9, the result of the code is as follows:
Insert image description here
When we enter 10, the following situation occurs:
Insert image description here
When other numbers are entered, the following situation occurs:
Insert image description here
Then this is the role of catch(...).
Point 8:
After an exception object is thrown, a copy of the exception object will be generated. Because the thrown exception object may be a temporary object, a copy object will be generated. This copied temporary object will be destroyed after being caught. (The processing here is similar to the value return of the function), we can understand it through the following code:

void func(int x)
{
    
    
	if (x == 10)
	{
    
    
		string s("x的值不能等于10");
		throw(s);
	}
}
int main()
{
    
    
	int x;
	cin >> x;
	try {
    
    func(x);}
	catch (const string& s){
    
    cout << s << endl;}
	return 0;
}

A temporary object is created in func, and throw throws exactly this temporary object s. The life cycle of s only exists in the if statement block. After the if statement s is destroyed, s is destroyed, so strictly speaking, the capture in the main function What is captured is not the s in the if statement but a temporary copy of s. The efficiency here may be a bit low, but don't worry, the compiler will identify the s here as the dying value, so a moving copy will be used. The efficiency will not be reduced much.

Summary of matching rules:

  1. First check whether the throw itself is inside the try block, and if so, look for a matching catch statement. If there is a match, it is
    transferred to the catch for processing.
  2. If there is no matching catch, exit the current function stack, and continue to search for a matching catch in the stack of the calling function.
  3. If the stack of the main function is reached and there is still no match, the program will be terminated. The above process of finding matching
    catch clauses along the call chain is called stack unwinding. So in practice, we have to add a catch(...) at the end to catch any type of exception
    . Otherwise, if there is an exception that is not caught, the program will terminate directly.
  4. After the matching catch clause is found and processed, it will continue to execute along the catch clause.

Exception inheritance system commonly used by servers

When writing code in the future, you will encounter the form of group cooperation. Each group is responsible for different modules. For example, there is a group responsible for the database module, a group responsible for the cache module, and a group responsible for the business module. Each group will throw an exception. However, the types of exceptions thrown by each group are very different. If they are captured together in the main function, there will be many and very complex, and the exceptions thrown by each group have their own module attributes. Therefore, it is unrealistic to use a class to solve the exception here. In order to solve this problem, the exception has a feature: in practice, there is an exception to the matching principle of throwing and catching. Not all types match completely and can be thrown. The derived class objects are captured using the base class, so that each group can throw exceptions of the derived class, and then use the base class in the main function to uniformly capture, then in the actual project we can create a parent class. There is an int type error code and a string type object in the class to record the current error description information. Then in this class, a virtual function can be created to return the internal string object to facilitate the user to print, then here The code is as follows:

class Exception
{
    
    
public:
	Exception(const string& errmsg, int id)
		:_errmsg(errmsg)
		, _id(id)
	{
    
    }
	virtual string what() const
	{
    
    
		return _errmsg;
	}
protected:
	string _errmsg;
	int _id;
};

Then the exceptions thrown by different groups will have the characteristics of this group, so here we can choose to create a class by inheriting the above class, and add a member variable to the subclass to record the special features of the current module. Special error message, and the what function corresponding to this class can be rewritten to add a symbolic content so that we can more easily know where the problem is. Then the code here is as follows:

class SqlException : public Exception
{
    
    
public:
	SqlException(const string& errmsg, int id, const string& sql)
		:Exception(errmsg, id)
		, _sql(sql)
	{
    
    }
	virtual string what() const
	{
    
    
		string str = "SqlException:";
		str += _errmsg;
		str += "->";
		str += _sql;
		return str;
	}
private:
	const string _sql;
};

There are multiple modules in a project. The above is the rewriting of the sql module. The same reason is also the rewriting of the cache module. This is also the same reason for inheriting the above parent class and completing the rewriting of the what function:

class CacheException : public Exception
{
    
    
public:
	CacheException(const string& errmsg, int id)
		:Exception(errmsg, id)
	{
    
    }
	virtual string what() const
	{
    
    
		string str = "CacheException:";
		str += _errmsg;
		return str;
	}
};

The same is true for the http module. Because http has its own error type, we add a variable to record this type of error. Then the code here is as follows:

class HttpServerException : public Exception
{
    
    
public:
	HttpServerException(const string& errmsg, int id, const string& type)
		:Exception(errmsg, id)
		, _type(type)
	{
    
    }
	virtual string what() const
	{
    
    
		string str = "HttpServerException:";
		str += _type;
		str += ":";
		str += _errmsg;
			return str;
	}
private:
	const string _type;
};

Then with these parent and child classes describing the exception, you can use the following code to test the exception:

void SQLMgr()
{
    
    
	srand(time(0));
	if (rand() % 7 == 0)
	{
    
    
		throw SqlException("权限不足", 100, "select * from name = '张三'");
	}
	//throw "xxxxxx";
	cout << "调用成功" << endl;
}
void CacheMgr()
{
    
    
	srand(time(0));
	if (rand() % 5 == 0)
	{
    
    
		throw CacheException("权限不足", 100);
	}
	else if (rand() % 6 == 0)
	{
    
    
		throw CacheException("数据不存在", 101);
	}
	SQLMgr();
}
void HttpServer()
{
    
    
	// ...
	srand(time(0));
	if (rand() % 3 == 0)
	{
    
    
		throw HttpServerException("请求资源不存在", 100, "get");
	}
	else if (rand() % 4 == 0)
	{
    
    
		throw HttpServerException("权限不足", 101, "post");
	}
	CacheMgr();
}
int main()
{
    
    
	while (1)
	{
    
    
		this_thread::sleep_for(chrono::seconds(1));
		try {
    
    
			HttpServer();
		}
			catch (const Exception& e) // 这里捕获父类对象就可以
		{
    
    
			// 多态
			cout << e.what() << endl;
		}
		catch (...)
		{
    
    
			cout << "Unkown Exception" << endl;
		}
	}
	return 0;
}

First, call the httpserver function in the main function. In this function, we generate a random number. We regard this number as a situation. If this number can be divisible by 3 and 4, then this situation will go wrong. Use it directly. throw to throw an exception. If there is no error, the function of the buffer is called. The same is true in this function. After this function, the relevant functions of the database are called and an exception is thrown when encountering some situations. Then in the main function we You can uniformly use the catch of the parent class type to uniformly catch exceptions, and use the what function inside to print out the content. Then the results of running the above code are as follows: You can see that we can clearly find out what is going on here
Insert image description here
. An exception occurred at the location, and what is the reason for the exception, and our above code adds three capture points when handling the exception, so that our program can be faced with a writing team that does not throw as specified The program will not be terminated suddenly when an exception occurs, such as the following code:

void HttpServer()
{
    
    
	// ...
	srand(time(0));
	if (rand() % 3 == 0)
	{
    
    
		throw HttpServerException("请求资源不存在", 100, "get");
	}
	else if (rand() % 4 == 0)
	{
    
    
		throw HttpServerException("权限不足", 101, "post");
	}
	else if(rand() % 5 == 0)
		throw(10);
	CacheMgr();
}

The result of running the code is as follows:
Insert image description here

Exceptionally safe

1. The constructor completes the construction and initialization of the object. It is best not to throw an exception in the constructor, otherwise the object may be incomplete or not fully initialized. 2. The destructor mainly completes the cleanup
of resources. It is best not to throw an exception in the destructor. Exceptions are thrown within, otherwise it may lead to resource leaks (memory leaks, handles not closed, etc.)
3. Exceptions in C++ often lead to resource leak problems, such as exceptions thrown in new and delete, leading to memory leaks, in lock and delete An exception was thrown between unlocks, resulting in deadlock. C++ often uses RAII to solve the above problems. We will explain RAII in this section of smart pointers.

Exceptional norms

  1. The purpose of the exception specification is to let function users know what exceptions the function may throw. You can follow the function with throw(type) to list all the exception types that this function may throw.
  2. The function is followed by throw(), indicating that the function does not throw an exception.
  3. If no exception interface is declared, this function can throw any type of exception

For example, the following code:

//c++98
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();

The above form is for c++98, so a library is provided for c++11 to specifically handle exceptions:
Insert image description here
this library contains various types of exceptions:
Insert image description here
bad_alloc is the exception thrown by new, and out_of_rang is thrown out of bounds Exceptions such as at function, etc.
Insert image description here
Insert image description here
There are various exception classes in this library, so a base class called exception is provided. This is the base class of all exception classes in the library. C++ does not force us to use each function. You have to add throw later, but C++ provides noexcepect instead of throw. Adding noexcepect means that no exception will be thrown, but adding noexcepect means that the compiler will check whether this function really does not throw an exception.
Insert image description here
Insert image description here

Exceptional pros and cons:

Advantages of C++ exceptions:

  1. Once the exception object is defined, various error information can be clearly and accurately displayed compared to the error code method, and it can even include stack call information, which can help better locate program bugs.
  2. A big problem with the traditional way of returning error codes is that in the function call chain, if a deep function returns an error, then we have to return errors layer by layer, and only the outermost layer can get the error. See the detailed explanation below for details.
  // 1.下面这段伪代码我们可以看到ConnnectSql中出错了,先返回给ServerStart,
//ServerStart再返回给main函数,main函数再针对问题处理具体的错误。
  // 2.如果是异常体系,不管是ConnnectSql还是ServerStart及调用函数出错,
  //都不用检查,因为抛出的异常异常会直接跳到main函数中catch捕获的地方,main函数直接处理错误。
  int ConnnectSql()
 {
    
    
 // 用户名密码错误
 	if (...)
 	return 1;
  // 权限不足
 	if (...)
 	return 2;
 }
 int ServerStart() {
    
    
 if (int ret = ConnnectSql() < 0)
 return ret;
 int fd = socket()
 if(fd < 0return errno;
 }  
  int main()
 {
    
    
 if(ServerStart()<0)
 ...
 return 0;
 }
  1. Many third-party libraries contain exceptions, such as boost, gtest, gmock and other commonly used libraries, so we also need to use exceptions when using them.
  2. Some functions are easier to handle using exceptions. For example, the constructor has no return value, so it is inconvenient to use error codes to handle it. For example, for functions like T& operator, if pos exceeds the limit, it can only use an exception or terminate the program, and there is no way to indicate an error through a return value.

Disadvantages of C++ exceptions:
5. Exceptions will cause the execution flow of the program to jump around, and it will be very confusing, and it will jump around when an error is thrown during runtime. This will make it more difficult for us to track and debug and analyze the program.
6. Exceptions will have some performance overhead. Of course, with the fast speed of modern hardware, this impact is basically negligible.
7. C++ does not have a garbage collection mechanism, and resources need to be managed by yourself. Exceptions can easily lead to abnormal security issues such as memory leaks and deadlocks. This requires the use of RAII to handle resource management issues. The cost of learning is higher.
8. The exception system of the C++ standard library is not well defined, causing everyone to define their own exception system, which is very confusing.
9. Use exceptions as standardized as possible, otherwise the consequences will be disastrous. If exceptions are thrown at will, the users captured by the outer layer will be miserable. So there are two points in the exception specification: 1. The exception types thrown are inherited from a base class. 2. Whether a function throws an exception and what kind of exception it throws are standardized using func() throw();.

Summary: Generally speaking, the advantages of exceptions outweigh the disadvantages, so we still encourage the use of exceptions in engineering. In addition, OO languages ​​​​basically use exceptions to handle errors, which can also be seen that this is the general trend

Guess you like

Origin blog.csdn.net/qq_68695298/article/details/131625111