传统的C语言处理错误时主要有以下两种方式:
实际中C语言基本都是使用返回错误码的方式来处理错误,部分情况下会终止程序来处理一些非常严重的错误。
异常也是一种处理错误的方式,当一个函数发现自己无法处理的错误时就可以抛出异常,将其交由函数的直接或间接调用者来处理:
throw、try、catch 语句的语法如下:
void func() {
//当满足某个条件时抛出异常
if () {
throw e;
}
}
try {
// 保护的标识代码
func();
}
catch (ExceptionName e1) {
// catch 块
}
catch (ExceptionName e2) {
// catch 块
}
catch (ExceptionName eN) {
// catch 块
}
异常抛出和捕获的匹配原则如下:
如下:程序的调用逻辑是 main->func->division,当 division 抛出异常时,由于division本身不在 try 块中,所以异常会到 func 函数的栈帧中查找异常处理语句,而 func 函数中也没有,所以异常会继续到 main 函数中查找;同时,由于 division 函数抛出的异常的类型为 string,所以它会匹配 const char* errmsg 的 catch 块:
注意:如果 division 中抛出了异常,而 division 本身及其上层的函数都没有对异常进行捕获,即没有 try/catch 语句;或者说有 try/catch 语句但是没有与抛出类型匹配的 catch 块,程序都会直接终止:
注:实际中抛出和捕获的匹配原则有个例外,并不都是类型完全匹配,可以抛出派生类对象,然后使用基类的引用捕获,这个在实际中非常实用,具体做法我们会在下文给出例子。
在有些时候,程序直接抛出异常可能会导致发生一些意想不到的错误,比如内存泄露,因为程序抛出异常后会直接跳转到对应 catch 块处理异常,处理完毕后也会直接执行 catch 块后面的代码,而不会回来继续执行抛出异常位置后面的代码;如下 :
面对这种情况,我们可以直接在 division 函数中处理异常并释放资源,但我们通常会选择捕获异常后不处理异常,只释放资源,然后将异常重新抛出,这样可以使得程序的异常都在某一个地方集中进行捕获,方便记录日志与集中处理;如下:
double Division(int a, int b) {
// 当b == 0时抛出异常
if (b == 0)
throw "Division by zero condition!";
else {
return ((double)a / (double)b);
}
}
void Func() {
// 这里如果发生除0错误会抛出异常,但下面的资源没有得到释放
// 所以这里捕获异常后并不处理异常,而只释放资源,异常还是交给外面处理,这里捕获了再重新抛出去。
int* p = new int[10];
try {
int len, time;
cin >> len >> time;
cout << Division(len, time) << endl;
}
catch (const char* errmsg) {
cout << "delete[] ->" << p << endl;
delete[] p;
throw errmsg;
}
//如果上面没有抛异常,这里再正常释放
delete[] p;
}
int main() {
try {
Func();
}
catch (const char* errmsg) {
cout << errmsg << endl;
}
catch (int errid) {
cout << errid << endl;
}
return 0;
}
在上面异常重新抛出的场景中,由于 func() 函数中还可能会调用其他函数,而其他函数也可能会抛出异常,并且它们抛出的对象的类型可能与 division 并不相同,那么此时如果我们要实现捕获异常释放资源重新抛出就需要写多个不同参数类型的 catch 块,这显然很麻烦,所以 C++ 还支持捕获与抛出任意类型的异常:
try {
//...
}
catch(...) { //三个点表示捕获任意类型异常
//throw 表示抛出任意类型的异常--捕获到什么就抛出什么
throw;
}
同时,之前我们提到,如果到达 main 函数的栈帧后依旧没有匹配的 catch 块或依然没有对异常进行捕获,那么程序会直接终止,这显然是不好的;所以实际中通常我们都会在最后加一个 catch(…) 来捕获任意类型的异常,以此来处理未知异常,放在程序被直接终止;如下:
double Division(int a, int b) {
// 当b == 0时抛出异常
if (b == 0)
throw "Division by zero condition!";
else {
return ((double)a / (double)b);
}
}
void Func() {
// 这里如果发生除0错误会抛出异常,但下面的资源没有得到释放
// 所以这里捕获异常后并不处理异常,而只释放资源,异常还是交给外面处理,这里捕获了再重新抛出去。
int* p = new int[10];
try {
int len, time;
cin >> len >> time;
cout << Division(len, time) << endl;
}
catch (...) { //捕获任意类型异常
cout << "delete[] ->" << p << endl;
delete[] p;
throw; //捕获到什么,就抛出什么
}
//如果上面没有抛异常,这里再正常释放
delete[] p;
}
int main() {
try {
Func();
}
catch (const char* errmsg) {
cout << errmsg << endl;
}
catch (int errid) {
cout << errid << endl;
}
catch (...) {
cout << "未知异常" << endl;
}
return 0;
}
C++ 的异常存在如下的安全问题:
由于不规范使用异常会带来许多非常严重的后果,所以 C++98 引入了异常规范,异常规范中建议程序员对每个函数进行异常接口说明,其目的是让函数使用者知道该函数可能抛出的异常有哪些,如下:
// 这里表示这个函数会抛出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++98 函数异常接口只是建议性做法,而不是语法硬性要求的,同时还由于写出一个函数可能抛出的所有异常比较麻烦,所以 C++98 的异常规范在实际开发中几乎没有人遵守,形同虚设;
为了让人们能够对函数进行异常接口说明,C++11 对异常接口说明进行了简化:
函数后面不加关键字 noexcept,表示该函数可能会抛出任意类型异常;
函数后面加关键字 noexcept,表示该函数不会抛异常。
// C++11 中新增的 noexcept,表示不会抛异常
thread() noexcept;
thread(thread&& x) noexcept;
并且 C++11 还对使用 noexcept 修饰的函数进行了检查,如果该函数被 noexcept 修饰,但是可能会抛出异常,则编译器会报一个警告,但并不影响程序的正确性:
C++ 提供了一系列标准的异常,定义在 exception 中,我们可以在程序中使用这些标准的异常;它们是以父子类层次结构组织起来的,如下所示:
其中,我们比较常见类有 bad_alloc – new 空间失败时抛出此异常;runtime_error – 一些运行时错误,比如除0错误,空指针解引用等;out_of_range – 通常是越界访问;overflow_error – 通常是栈溢出。
虽然我们可以直接使用 C++ 标准提供的这些异常,也可以去继承 exception 类来实现自己的异常类,但在实际开发中很多企业都会像上面一样自己定义一套单独的异常继承体系,因为C++标准库设计的不够好用。再加上我们平时自己写代码基本不会使用异常,所以对于 C++ 标准异常我们作为了解内容即可。
使用 C++ 标准库中的异常类来捕获异常:
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;
}
在实际开发中很多企业都会自定义自己的异常体系进行规范的异常管理,以此来避免在项目中大家随意抛异常,从而导致无法对异常进行集中分类处理;所以在实际中都会定义一套继承的规范体系,这样大家抛出的异常都是派生类对象,那么在捕获将参数类型定义为基类类型即可。
下面我们以服务器开发中通常使用的异常继承体系为例:
//基类
class Exception
{
public:
Exception(const string& errmsg, int id)
:_errmsg(errmsg)
, _id(id)
{}
virtual string what() const
{
return _errmsg;
}
protected:
string _errmsg; //错误信息
int _id; //错误编号
};
//数据库查询子类
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; //SQL查询异常的独有信息
};
//缓存访问子类
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;
}
};
//网络请求子类
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; //网络异常的独有信息
};
//SQL查询
void SQLMgr()
{
srand(time(0));
if (rand() % 7 == 0)
{
throw SqlException("权限不足", 100, "select * from name = '张三'");
}
//throw "xxxxxx";
}
//缓存访问
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)
{
Sleep(1000);
try {
HttpServer();
}
catch (const Exception& e) // 这里捕获父类对象就可以
{
// 多态
cout << e.what() << endl;
}
catch (...)
{
cout << "Unkown Exception" << endl;
}
}
return 0;
}
上面程序的分析如下:
这里有两个地方需要注意:
通过像上面这样来设计异常处理程序,我们可以在程序出错时可以快速定位问题,特别是在复杂的系统中,异常往往是难以避免的。通过准确地捕获异常,我们可以及时发现错误并进行修复,提高程序的稳定性和可靠性。同时,将不同类型的异常分别封装为不同的子类,也可以更加清晰地表达异常的类型和具体信息,为后续的维护和优化带来方便。
C++ 异常的优点:
C++ 异常的缺点:
总结:异常总体而言利大于弊,所以在工程开发中我们是鼓励使用异常的;另外面向对象的语言基本都是用异常处理错误,这也是大势所趋。(注:我们进行个人开发时基本不会用到异常,所以现在对异常有一个了解即可,要想真正的学习异常还是得在公司里面进行实际开发才行)