异常的抛出与捕获
C++中使用try-catch
块来处理异常,throw
关键字用于抛出异常。
throw
: 当问题出现时,程序会抛出一个异常,这是通过使用throw
关键字来完成的catch
: 在想要处理问题的地方,通过异常处理程序捕获异常,catch
关键字用于捕获异
常try
:try
块中的代码标识将被激活的特定异常,它后面通常跟着一个或多个catch
块。如果有一个块抛出一个异常,捕获异常的方法会使用try
和catch
关键字。try
块中放置可能抛出异常的代码,try
块中的代码被称为保护代码
double Division(int a, int b)
{
// 当b == 0时抛出异常
if (b == 0)
throw "error!!!";
else
return ((double)a / (double)b);
}
void Func()
{
int a, b;
cin >> a >> b;
cout << Division(a, b) << endl;
}
int main()
{
try
{
Func();
}
catch (const char* errmsg)
{
cout << errmsg << endl;
}
return 0;
}
此处的throw关键字用于抛出异常,"error!!!"是一个const char*类型的字符串,throw可以抛出任何类型变量。
由于main调用了Func,Func调用了Division,所以Func函数是有可能间接发生异常的,此时把调用Func的语句放到try块中,说明我们要检测这个Func函数会不会发生异常。
一旦Division抛出异常,那么就是Func发生了异常,此时try就可以检测到,由于抛出的异常是const char*,我们要检测该类型的异常,所以catch的参数就是const char*了,catch (const char* errmsg)。
一旦catch匹配到了相同类型的异常,就会执行{ }中的代码。
异常匹配原则
- 异常是通过抛出对象而引发的,该对象的类型决定了应该激活哪个
catch
的处理代码
try
{
Func();
}
catch (const char* errmsg)
{
cout << "const char*" << endl;
}
catch (int errmsg)
{
cout << "int" << endl;
}
catch (double errmsg)
{
cout << "double" << endl;
}
catch (int* errmsg)
{
cout << "int*" << endl;
}
此处有4个catch
块,当在Func
内部检测到异常时,根据被抛出的异常类型,选择执行哪一个catch
来输出信息。
2.catch(...)
可以捕获任意类型的异常,问题是不知道异常错误是什么
try
{
Func();
}
catch (const char* errmsg)
{
cout << "const char*" << endl;
}
catch (...)
{
cout << "unknow error" << endl;
}
3.被选中的处理代码是调用链中与该对象类型匹配且离抛出异常位置最近的那一个
4.抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个临时对象,所以会生成一个拷贝对象,这个拷贝的临时对象会在被
catch
以后销毁
由于被throw
的异常,本质上也是一个变量,当往回找catch
的时候,很有可能会出函数作用域,那么局部变量就会被销毁。因此catch()
内部是一个传值调用,通过不断拷贝被抛出的异常,最后把异常送到catch
中。
5.实际中抛出和捕获的匹配原则有个例外,并不都是类型完全匹配,可以抛出的派生类对象,使用基类捕获。
// 定义基类异常
class Base
{
public:
virtual const char* what()
{
return "Base Exception";
}
};
// 定义派生类异常
class Derived : public Base
{
public:
const char* what()
{
return "Derived Exception";
}
};
int main()
{
try
{
// 抛出派生类异常
throw Derived();
}
catch (Base& e)
{
// 捕获基类异常,但实际接收到的是派生类异常
cout << e.what() << endl;
}
catch (...)
{
cout << "Catch all exception" << endl;
}
return 0;
}
异常重新抛出
有可能单个的catch
不能完全处理一个异常,在进行一些校正处理以后,希望再交给更外层的调用链函数来处理,catch
则可以通过重新抛出将异常传递给更上层的函数进行处理。
double Division(int a, int b)
{
// 当b == 0时抛出异常
if (b == 0)
throw "error!!!";
else
return ((double)a / (double)b);
}
void Func()
{
int a, b;
cin >> a >> b;
try
{
cout << Division(a, b) << endl;
}
catch (const char* errmsg)
{
cout << "第1次处理异常" << endl;
throw errmsg;
}
}
int main()
{
try
{
Func();
}
catch (const char* errmsg)
{
cout << "第2次处理异常" << endl;
}
catch (...)
{
cout << "unknow error" << endl;
}
return 0;
}
异常安全问题
构造函数完成对象的构造和初始化,最好不要在构造函数中抛出异常,否则可能导致对象不完整或没有完全初始化
class A
{
public:
A(int n)
{
if (n == 0)
throw 0;
_ptr = new int[n];
}
~A()
{
delete[] _ptr;
}
private:
int* _ptr;
};
以上代码中,A
类的构造函数在n = 0
的时候,直接抛出异常throw 0
。此时会直接结束析构函数,去找catch
。但是一旦出了A
的作用域,A
就会被销毁,调用析构函数,而析构函数要delete[]
,_ptr
没有初始化,此时是野指针,这就导致了严重的问题,程序直接崩溃。
析构函数主要完成资源的清理,最好不要在析构函数内抛出异常,否则可能导致资源泄漏(内存泄漏、句柄未关闭等)
析构函数主要完成资源的清理,最好不要在析构函数内抛出异常,否则可能导致资源泄漏(内存泄漏、句柄未关闭等)
C++中异常经常会导致资源泄漏的问题,比如在new和delete中抛出了异常,导致内存泄漏,在lock和unlock之间抛出了异常导致死锁,C++经常使用RAII来解决以上问题
先看到一个普通的异常造成的内存泄漏:
void func2()
{
throw "error";
}
void func1()
{
int* array = new int[10];
func2();
delete[] array;
}
int main()
{
try
{
func1();
}
catch (const char* errmsg)
{
cout << errmsg << endl;
}
return 0;
}
以上代码中,func2
抛出了异常,由于在调用func2
之前,func1
已经new
了一块动态内存,此时如果直接去处理异常,那么delete
就会被忽略,导致内存泄漏。
对于这种内存泄漏,我们就可以用到异常的重新抛出了,将func1
写为如下形式:
void func1()
{
int* array = new int[10];
try
{
func2();
}
catch (const char* errmsg)
{
delete[] array;
throw;
}
delete[] array;
}
,C++提供了RAII(Resource Acquisition Is Initialization)机制,通过在构造函数中申请资源,在析构函数中释放资源,确保资源的安全释放。
class SmartPtr
{
public:
SmartPtr(int* ptr)
: _ptr(ptr)
{}
~SmartPtr()
{
delete[] _ptr;
}
private:
int* _ptr;
};
void func()
{
SmartPtr p1 = new int[10];
SmartPtr p2 = new int[10];
SmartPtr p3 = new int[10];
}
我们将原先的new
和delete
放到了类SmartPtr
中,此时就算哪个地方出现了异常,要去上层找catch
,由于自定义类型出了作用域,会自动调用析构函数,此时就会调用~SmartPtr
内部的delete
,就完成了自动释放资源,不用我们复杂的截获,然后一个一个delete
了。
异常规范
为了让函数的调用者可以更加清晰地知道一个函数有可能会抛出什么类型的异常,方便处理,C++98给出了异常规格说明
。
- 异常规格说明的目的是为了让函数使用者知道该函数可能抛出的异常有哪些。 可以在函数的后面接throw(类型),列出这个函数可能抛掷的所有异常类型
- 函数的后面接throw(),表示函数不抛异常
- 若无异常接口声明,则此函数可以抛掷任何类型的异常
void* fun() throw(int, double, string)
{
return nullptr;
}
throw(int, double, string)
代表函数func
只有可能抛出这三种类型的异常。
void* fun() throw()
{
return nullptr;
}
throw()
代表函数func
不会抛出任何异常。
C++11引入了noexcept
关键字,用于指定函数不会抛出任何异常。如果一个noexcept
函数内部抛出异常,程序会直接调用std::terminate()
终止。
- 如果一个函数有可能抛出异常,就不对该函数进行任何修饰
- 如果一个函数一定不会抛出异常,那么用
noexcept
来修饰该函数
异常体系
在实际开发中,由于异常抛出的类型是不确定的,公司内部一般会约定好如何抛异常,而异常继承体系是最优秀的一套体系。
先定义一个基类:
class Exception
{
public:
Exception(const string& errmsg, int id)
:_errmsg(errmsg)
, _id(id)
{}
virtual string what() const
{
return _errmsg;
}
protected:
string _errmsg;
int _id;
};
某个SQL
的程序员想要抛异常,于是它写出来以下派生类
:
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;
};
外层捕获异常的方式:
void SQLMgr()
{
if (true)
{
throw SqlException("权限不足", 100, "select * from name = '张三'");
}
}
int main()
{
try
{
SQLMgr();
}
catch (Exception e)
{
cout << e.what() << endl;
}
}
在外层只捕获基类Exception
,此时所有程序员重写的派生类都可以被捕获到。捕获到异常后,就去调用what
函数,拿到返回值并输出,此时就可以直到到底是什么问题了。
这个地方,异常最后就会输出:
SqlException:权限不足->select * from name = '张三'
看到这样一个异常,我们可以知道:该异常来自于SQL
,原因在于权限不足
,问题语句是select * from name = '张三'
。
这样的异常看的就让人很舒服,可以很快定位到错误,而且整个抛异常的过程也很简洁,不会发生接收不到异常,或者接收到未知类型的异常等问题。