1. C语言错误处理机制
我们在曾经介绍过C语言下的错误码。错误码我们过去经常见到,错误码通常是指errno变量中的值,它表示特定操作(如系统调用或库函数)发生错误的原因。errno是一个全局变量,当出现错误时会自动将错误码存储在errno中,不同的值代表着不同的错误信息。我们可以通过perror和strerror来查看错误信息。
perror:void perror(const char *s); 打印输入的参数字符串+此时errno对应的错误信息
strerror:char *strerror(int errnum); 打印指定错误码(传入参数)的错误信息
对于退出码,它是在进程结束后返回的一个退出状态信息,表示程序的执行结果,一般约定0为成功,而非0为出现错误,至于退出码和原因的对应关系没有固定的要求。

除了错误码之外,C语言下的终止程序也是一种处理方案,比如assert断言、内存错误等,只不过这种处理方式十分粗暴。
C++采取异常的方式处理错误,当一个函数发现错误后,可以选择抛出一个异常,这个异常就会顺着调用链传递,找到能够处理这个异常的模块。
2. 异常的使用方法
为了使用C++的异常,引入三个关键字:
throw:它的作用是抛出异常。
try:在try语句块中的代码会被"监视",判断其中的代码在执行中是否发生了异常。如果没有异常发生,那么相安无事。一旦发生了异常,则会停止块中的代码执行,转而搜索catch块,寻找匹配的catch来处理异常。
catch:用于捕获和指定的对象类型相同的异常。
cpp
double Div(int a, int b)
{
int ret = 0;
//throw 可以抛出一个异常,这个异常实际上是一个对象
//后续会根据这个对象的类型决定匹配的catch
if (b == 0) throw "Devision by zero"; //抛出一个const chat*类型的异常
//首先检查throw是否在try块的内部
// 在try中:查找接收的类型和异常对象类型匹配catch
// 不在try中或在try中却没有匹配的catch:直接结束当前函数,返回上一层函数中继续判断是否位于try中并进行catch匹配
else ret = double(a) / double(b);
cout << "div return" << endl;
return ret;
}
void fun()
{
int a, b;
cin >> a >> b;
cout << Div(a, b) << endl;
cout << "fun return" << endl;
}
//输入1 0:
// Div函数中:if分支为真,抛出一个const char*的异常。由于throw不在try块内,所以直接结束当前函数,返回到fun函数中捕获异常
// fun函数中:抛出异常导致退出Div栈帧后返回fun函数的调用Div函数处,同样不在try内,直接结束当前函数,返回到main函数中捕获异常
// main函数中:对于fun的调用在try中,因此进行catch匹配,并且成功匹配到了const char*类型,进入处理异常,异常处理结束后继续main函数的执行
int main()
{
try
{
fun();
}
catch (const int num) //匹配int型异常对象
{
cout << num << endl;
}
catch (const char* message) //匹配const char*型异常对象
{
cout << message << endl;
}
//catch匹配成功则进入处理异常,并且在处理完异常后继续后续代码执行
//main函数仍未成功匹配,则终止程序
catch (...)//可以捕获任意类型的异常
{
cout << "unknown exception" << endl;
}
cout << "main return" << endl;
//和函数的返回值一样,异常对象作为函数的局部对象想要被调用链其他函数捕获,就需要对其拷贝构造来传递
//当然在C++11中,这一步拷贝构造传递完全可以由移动构造来完成
return 0;
}
在这个例子中,如果输入1 0:
Div函数中:if分支为真,抛出一个const char*的异常。由于throw不在try块内,所以直接结束当前函数,返回到fun函数中捕获异常;
fun函数中:抛出异常导致退出Div栈帧后返回fun函数的调用Div函数处,同样不在try内,直接结束当前函数,返回到main函数中捕获异常;
main函数中:对于fun的调用在try中,因此进行catch匹配,并且成功匹配到了const char*类型,进入处理异常,异常处理结束后继续main函数的执行。
使用要点:
①throw用于可以抛出一个异常,这个异常实际上是一个具体的对象(如int类型对象、string类型对象等),后续的catch会根据这个对象的类型决定匹配的catch。
②抛出的异常如果在当前函数无法处理(包括没有位于try块中,或没有匹配的catch),那么异常对象会沿着函数的调用链向上传递。即直接结束当前函数,将异常带到调用这个函数的地方去找处理方案。
具体来说,throw抛出异常后如果在try中,则在后续的catch中查找接收的类型和异常对象类型匹配catch,如果找到了匹配的catch,则根据catch块的代码处理异常。
如果throw抛出时不在try中,或在try中却没有匹配的catch,那么则直接结束当前函数,返回上一层函数中继续判断是否位于try中并进行catch匹配。
③异常匹配catch会选择类型严格匹配且距离最近的,即异常对象的匹配不支持隐式类型转换,并且如果函数a调用函数b,函数b调用函数c,c抛出的异常在b和a中都可以被捕捉,但是会优先被b捕捉处理,所以不再会去a中试图catch。
④如果catch匹配成功则进入处理异常,并且在处理完异常后继续本函数后续代码执行。后续代码即为try-catch语句块之后的代码。
⑤如果直到main函数,异常仍未成功匹配,则终止程序并报错。
⑥catch (...)可以捕获任意类型的异常。
⑦和函数的返回值一样,异常对象也需要在函数之间传递。异常对象时函数的局部对象,如果想要被调用链其他函数捕获,就需要对其拷贝构造来传递,这和函数返回值一模一样。当然在C++11中,这一步拷贝构造传递完全可以由移动构造来完成。
⑧虽然异常对象的catch捕捉不支持隐式类型转换,但是对继承关系的基类和派生类之间的对象是存在例外的。允许抛出的派生类对象,而使用基类捕获。
以下给出一个基类捕获派生类异常的例子,实际上这也是异常体系的基本定义方式。
cpp
class Exception
{
public:
Exception(const string& errormessage, int id)
:_ErrorMessage(errormessage)
, _id(id)
{}
virtual string show() const //虚函数用于实现多态
{
return _ErrorMessage;
}
protected:
string _ErrorMessage; //错误信息描述
int _id; //错误id
};
class SqlException :public Exception
{
public:
SqlException(const string& errormessage, int id, const string& sql)
:Exception(errormessage,id) //构造基类部分
,_sql(sql)
{}
virtual string show() const //重写
{
string ret = "SqlException:";
ret += _ErrorMessage;
ret += "->";
ret += _sql;
return ret;
}
private:
const string _sql;
};
class HttpException :public Exception
{
public:
HttpException(const string& errormessage, int id, const string& http)
:Exception(errormessage,id) //构造基类部分
,_http(http)
{}
virtual string show() const //重写
{
string ret = "HttpException:";
ret += _ErrorMessage;
ret += "->";
ret += _http;
return ret;
}
private:
const string _http;
};
class CacheException :public Exception
{
public:
CacheException(const string& errormessage, int id)
:Exception(errormessage,id) //构造基类部分
{}
virtual string show() const //重写
{
string ret = "CacheException:";
ret += _ErrorMessage;
return ret;
}
};
void SqlServe()
{
srand(time(0));
if (rand() % 11 == 0)
{
throw SqlException("权限不足", 101, "select * from student where name=\'张三\'");
}
cout << "success" << endl;
}
void CacheServe()
{
srand(time(0));
if (rand() % 7 == 0)
{
throw CacheException("权限不足", 111);
}
if (rand() % 5 == 0)
{
throw CacheException("数据缺失", 112);
}
SqlServe();
}
void HttpServe()
{
srand(time(0));
if (rand() % 4 == 0)
{
throw HttpException("权限不足", 131, "get");
}
if (rand() % 3 == 0)
{
throw HttpException("资源缺失", 132, "post");
}
CacheServe();
}
int main()
{
while (1)
{
this_thread::sleep_for(chrono::seconds(1));
try
{
HttpServe();
}
catch (const Exception& ex) //只捕获父类
{
cout << ex.show() << endl; //多态
}
catch (...)
{
cout << "unknown exception" << endl;
}
}
return 0;
}
3. 异常的注意事项
3.1 异常重新抛出
异常支持重新抛出,即有可能单个的catch不能完全处理一个异常,在进行一些校正处理以后,希望再交给更外层的调用链函数来处理,catch则可以通过重新抛出将异常传递给更上层的函数进行处理。
异常在接收后重新抛出对于有资源申请的函数模块有很大意义。在某些情况下,并不需要在本函数处理异常,但是由于本函数申请了资源,出现异常直接结束函数会导致资源泄露。所以出现异常就需要捕获,成功捕获后释放资源,再将异常重新抛出。
cpp
double Div(int a, int b)
{
int ret = 0;
if (b == 0) throw "Devision by zero";
else ret = double(a) / double(b);
return ret;
}
void fun()
{
int* array = new int[10];
try {
int a, b;
cin >> a>> b;
cout << Div(a, b) << endl;
}
catch (const int num)
{
throw num;//重新抛出num异常对象
}
catch (...)
{
//某些情况并不需要在本函数处理异常,但是出现异常直接结束函数会导致资源泄露
//所以出现异常就需要捕获,成功捕获后释放资源,再将异常重新抛出
cout << "delete []" << array << endl;
delete[] array;
throw; //异常的重新抛出,接收什么抛出什么
}
cout << "delete []" << array << endl;
delete[] array;
}
int main()
{
try
{
fun();
}
catch (const char* message)
{
cout << message << endl;
}
catch (...)
{
cout << "unknown exception" << endl;
}
return 0;
}
3.2 new异常的处理
考虑以下修改后的fun函数代码。
cpp
void fun()
{
int* array1 = new int[10];
int* array2 = new int[10];
try {
int a, b;
cin >> a>> b;
cout << Div(a, b) << endl;
}
catch (...)
{
delete[] array1;
delete[] array2;
throw;
}
delete[] array1;
delete[] array2;
}
new也是会抛异常的,当array1申请资源失败后,需要直接抛异常;当array2申请资源失败后则需要先释放array1的资源,然后再抛异常。会发现仅仅对于两个连续的new,如果我们完善的考虑异常的问题,需要不小的try-catch语句块。
cpp
void fun()
{
int* array1 = nullptr;
int* array2 = nullptr;
try
{
array1 = new int[10];
try
{
array2 = new int[10];
}
catch (...)
{
// 如果array2分配失败,清理array1
delete[] array1;
throw;
}
}
catch (...)
{
// array1分配失败
throw;
}
try
{
int a, b;
cin >> a>> b;
cout << Div(a, b) << endl;
}
catch (...)
{
delete[] array1;
delete[] array2;
throw;
}
delete[] array1;
delete[] array2;
}
对于这种问题,最好的解决方案是C++11引入的智能指针。智能指针实际上就是为指针包装了一个类的壳子,这样指针就变为了一个局部对象,那么在函数栈帧销毁时自动调用析构函数,从而完成资源的自动释放。
cpp
template <class T>
class SmartPtr
{
public:
SmartPtr(T* ptr)
:_ptr(ptr)
{}
~SmartPtr()
{
delete(_ptr);
}
T& operator*()
{
return *_ptr;
}
T* operator*()
{
return _ptr;
}
private:
T* _ptr;
};
/
//fun()函数内:
A* a1 = new A;
A* a2 = new A;
//①C++库的智能指针
unique_ptr<A> a1 = new A;
unique_ptr<A> a2 = new A;
//②自己包装的简单的智能指针
SmartPtr<A> p1=(new A);
SmartPtr<A> p2=(new A);
3.3 异常的其他要点
①构造函数、析构函数完成的是对象的初始化和资源清理过程,所以尽量避免在构造函数和析构函数中抛异常,防止对象未完全初始化和资源泄露的问题。
②在C++98中,规定了异常的规格说明:
如果函数内会出现异常,则需要在函数后使用throw(异常类型)来列出所有可能的异常类型。
如果函数内不会出现异常,则使用throw()。
但是这套异常规格说明自C++11起被noexcept取代,C++17 移除动态异常规范(只保留 throw() 作为 noexcept 的旧式写法)。
所以在C++11下如果函数不会抛异常,则使用noexcept即可。如果不说明则表示可能会抛异常。
③异常可以方便我们更加清晰地展示错误信息,更加准确的定位错误位置,更加方便的完成错误处理。但是其也使得代码执行顺序大幅度跳转,并且非常容易引起内存泄漏和死锁等问题,需要我们多加注意。
cpp
fun1() noexcept;//表示不会抛异常
fun2();//表示可能会抛异常
4. C++标准库的异常体系

