✨✨小新课堂开课了,欢迎欢迎~✨✨
🎈🎈养成好习惯,先赞后看哦~🎈🎈
所属专栏:C++:由浅入深篇
小新的主页:编程版小新-CSDN博客
一.异常的概念和使用
异常的概念
在C++中,异常是一种处理程序运行时错误或异常情况的机制。它允许程序在遇到错误时,将控制权从错误的部分转移到可以处理该错误的部分。
异常处理使得程序更加健全,并且可以更优雅的处理错误情况,而不是直接崩溃。
C语言主要通过错误码的形式处理错误,错误码本质就是对错误信息进行分类编号,拿到错误码以后还要去查询错误信息。
int divide(int a, int b, int* result)
{
if (b == 0)
{
errno = EINVAL;
return EINVAL;//返回错误码
}
*result = a / b;
return 0;
}
int main()
{
int res;
int ret = divide(10, 0, &res);
if (ret != 0)
{
perror("Division failed");
return 1;
}
printf("%d\n", res);
return 0;
}
异常处理关键字
throw:当程序出现错误时,可以用throw关键字抛出一个异常。抛出的异常可以是任意类型的对象。当throw被执行时,throw后面的语句将不再被执行。
try:用于包含可能会抛出异常的代码。try块后面通常会跟一个或者多个catch块。抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个局部对象,所以会生成一个拷贝对象,这个拷贝对象会在catch子句后销毁。
catch:用来捕获并处理异常。每个catch块指定它可以处理的异常类型。当异常被抛出时,程序会寻找匹配的且离抛出异常位置最近的那一个catch块来处理它。catch可能是同一函数中的一个局部的catch,也可能是另一个函数中的catch,控制权从throw位置转移到了catch位置。
**示例**
double Divide(int a, int b)
{
try
{
// 当b == 0时抛出异常
if (b == 0)
{
string s("Divide by zero condition!");
throw s;//抛出的是s的拷贝,s在当前局部域,出了作用域就销毁了
cout << __FUNCTION__ << ":" << __LINE__ << "行执行" << endl;
}
else
{
return ((double)a / (double)b);
}
}
catch(int errid)
{
cout << errid << endl;
}
return 0;
}
void Func()
{
int len, time;
cin >> len >> time;
try
{
cout << Divide(len, time) << endl;
}
catch(const char* errmsg)
{
cout << errmsg << endl;
}
cout << __FUNCTION__ << ":" << __LINE__ << "行执行" << endl;
}
int main()
{
while (1)
{
try
{
Func();
}
catch(const string & errmsg)
{
cout << errmsg << endl;
}
}
return 0;
}
**调试演示**
异常调试演示
我们第一组输入的是10和0,在Divide函数内throw抛出一个异常,这个异常是个string对象,可以注意到,当throw被执行时,throw后面的cout << __FUNCTION__ << ":" << __LINE__ << "行执行" << endl;语句没有被执行。随后catch进行捕捉,我们发现是匹配到了主函数内catch。因为与其他的catch类型不匹配。第二组输入的3和4。不存在异常,调用完Divide函数后,执行了cout << __FUNCTION__ << ":" << __LINE__ << "行执行" << endl;语句后进入的下一轮的循环。
**运行结果**
栈展开
抛出异常后,程序暂停当前函数的执行,开始寻找与之匹配的catch子句,首先检查throw本身是否在try块内部,如果在则查找匹配的catch语句,如果有匹配的,则跳到catch的地方进行处理,如果找到匹配的catch子句处理后,catch子句代码会继续执行。如果当前函数中没有try/catch子句,或者有try/catch子句但是类型不匹配,则退出当前函数,继续在外层调用函数链中查找,上述查找的catch过程被称为栈展开。
还是以上面的代码为例。在DIvide函数中的try块内部,throw抛出一个异常。该异常是一个string对象。接下里去寻找匹配的catch语句,由于与当前函数的cach子句类型不匹配,就退出了当前函数,继续从外层的调用链中查找,来到了Func函数,依旧类型不匹配,最后来到了主函数,类型匹配,异常得到处理。
如果到达main函数,依旧没有找到匹配的catch子句,程序会调调标准库的 terminate 函数终止程序。
查找匹配的处理代码
一般情况下抛出对象和catch是类型完全匹配的,如果有多个类型匹配的,就选择离他位置更近那个,但是也有一些例外。
允许从非常量向常量的类型转换,也就是权限缩小。因为常量引用或指针只承诺不修改对象,而非常量对象完全满足这个条件。
try
{
throw 45;//抛出int类型(非常量)
}
catch (const int& e)//可以用常量引用捕获
{
//
}
允许数组转换成指向数组元素类型的指针,函数被转换成指向函数的指针。在C++中,数组和函数在大多数表达式中会自动转换为指针。(数组退化为指向数组首元素的指针,函数退化为函数值指针,保持与C的兼容)
try
{
throw"Hello";//抛出const char[6]类型,自动转化为const char*
}
catch (const char* e)
{
//...
}
允许从派生类向基类类型的转换。我们通常使用继承 来建立类之间的层次关系,基类表示一般概念,派生类表示更具体的概念。在异常处理中,我们通常希望捕获基类可以处理所有派生类异常。这样我们可以用基类捕获整个类族的异常,而不用为每个派生类编写单独的catch块。
如果到main函数,异常仍旧没有被匹配就会终止程序,不是发生严重错误的情况下,我们是不期望程序终止的,所以一般main函数中最后都会使用catch(...),它可以捕获任意类型的异常,但是是不知道异常错误是什么。
**示例**
class Exception
{
public :
Exception(const string& errmsg, int id)
: _errmsg(errmsg)
, _id(id)
{}
virtual string what() const //what是个虚函数,派生类可以重写
{
return _errmsg;
}
int getid() const
{
return _id;
}
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 //对what进行重写后,给出更详细的错误信息
{
string str = "SqlException:";
str += _errmsg;
str += "->";
str += _sql;
return str;
}
private:
const string _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 HttpException : public Exception
{
public :
HttpException(const string& errmsg, int id, const string& type)
: Exception(errmsg, id)
, _type(type)
{}
virtual string what() const
{
string str = "HttpException:";
str += _type;
str += ":";
str += _errmsg;
return str;
}
private:
const string _type;
};
void SQLMgr()
{
if (rand() % 7 == 0)
{
throw SqlException("权限不足", 100, "select * from name = '张三'");
}
else
{
cout << "SQLMgr 调用成功" << endl;
}
}
void CacheMgr()
{
if (rand() % 5 == 0)
{
throw CacheException("权限不足", 100);
}
else if (rand() % 6 == 0)
{
throw CacheException("数据不存在", 101);
}
else
{
cout << "CacheMgr 调用成功" << endl;
}
SQLMgr();
}
void HttpServer()
{
if (rand() % 3 == 0)
{
throw HttpException("请求资源不存在", 100, "get");
}
else if (rand() % 4 == 0)
{
throw HttpException("权限不足", 101, "post");
}
else
{
cout << "HttpServer调用成功" << endl;
} CacheMgr();
}
int main()
{
srand(time(0));
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;
}
**运行结果**
异常的重新抛出
在异常处理中,重新抛出是指在捕获一个异常后不完全处理它,而是将其再次抛出,让更高层的调用者继续处理。
下面程序模拟展示了聊天时发送消息,发送失败捕获异常,但是可能在电梯地下室等场景手机信号不好,则需要多次尝试,如果多次尝试都发送不出去,则就需要捕获异常再重新抛出,其次如果不是网络差导致的错误,捕获后也要重新抛出。
class Exception
{
public:
Exception(const string& errmsg, int id)
: _errmsg(errmsg)
, _id(id)
{}
virtual string what() const //what是个虚函数,派生类可以重写
{
return _errmsg;
}
int getid() const
{
return _id;
}
protected:
string _errmsg;
int _id;
};
class HttpException : public Exception
{
public:
HttpException(const string& errmsg, int id, const string& type)
: Exception(errmsg, id)
, _type(type)
{}
virtual string what() const
{
string str = "HttpException:";
str += _type;
str += ":";
str += _errmsg;
return str;
}
private:
const string _type;
};
void _SeedMsg(const string& s)
{
if (rand() % 2 == 0)
{
throw HttpException("网络不稳定,发送失败", 102, "put");
}
else if (rand() % 7 == 0)
{
throw HttpException("你已经不是对象的好友,发送失败", 103, "put");
}
else
{
cout << "发送成功" << endl;
}
}
void SendMsg(const string& s)
{
// 发送消息失败,则再重试3次
for (size_t i = 0; i < 4; i++)
{
try
{
_SeedMsg(s);
break;
}
catch(const Exception & e)
{
// 捕获异常,if中是102号错误,网络不稳定,则重新发送
// 捕获异常,else中不是102号错误,则将异常重新抛出
if (e.getid() == 102)
{
// 重试三次以后否失败了,则说明网络太差了,重新抛出异常
if (i == 3)
throw;
cout << "开始第" << i + 1 << "重试" << endl;
}
else
{
throw;
}
}
}
}
int main()
{
srand(time(0));
string str;
while (cin >> str)
{
try
{
SendMsg(str);
}
catch(const Exception & e)
{
cout << e.what() << endl << endl;
}
catch(...)
{
cout << "Unkown Exception" << endl;
}
}
return 0;
}
**运行结果**
异常安全问题
异常抛出后,后面的代码就不再执行,前面申请了资源(内存、锁等),后面进行释放,但是中间可能会抛异常就会导致资源没有释放,这里由于异常就引发了资源泄漏,产生安全性的问题。
double Divide(int a, int b)
{
// 当b == 0时抛出异常
if (b == 0)
{
throw "Division by zero condition!";
}
return(double)a / (double)b;
}
void Func()
{
// 这⾥可以看到如果发⽣除0错误抛出异常,另外下面的array没有得到释放。
// 所以这⾥捕获异常后并不处理异常,异常还是交给外层处理,这⾥捕获了再
// 重新抛出去。
int* array = new int[10];
try
{
int len, time;
cin >> len >> time;
cout << Divide(len, time) << endl;
}
catch(...)
{
// 捕获异常释放内存
cout << "delete []" << array << endl;
delete[] array;
throw; // 异常重新抛出,捕获到什么抛出什么
}
cout << "delete []" << array << endl;
delete[] array;
}
int main()
{
try
{
Func();
}
catch(const char* errmsg)
{
cout << errmsg << endl;
}
catch(const exception & e)
{
cout << e.what() << endl;
}
catch(...)
{
cout << "Unkown Exception" << endl;
}
return 0;
}
异常规范
对于用户和编译器而言,预先知道某个程序会不会抛出异常大有裨益,知道某个函数是否会抛出异常有助于简化调用函数的代码。
C++98中函数参数列表的后面接throw(),表示函数不抛异常,函数参数列表的后面接throw(类型1,类型2...)表示可能会抛出多种类型的异常,可能会抛出的类型用逗号分割。
// C++98
// 这⾥表⽰这个函数只会抛出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++11中进行了简化,函数参数列表后面加noexcept表示不会抛出异常,啥都不加表示可能会抛出异常。
// C++11
size_type size() const noexcept;
iterator begin() noexcept;
const_iterator begin() const noexcept;
编译器并不会在编译时检查noexcept,也就是说如果一个函数用noexcept修饰了,但是同时又包含了throw语句或者调用的函数可能会抛出异常,编译器还是会顺利编译通过的(有些编译器可能会报个警告)。但是一个声明了noexcept的函数抛出了异常,程序会调用terminate 终止程序。
double Divide(int a, int b) noexcept
{
// 当b == 0时抛出异常
if (b == 0)
{
throw "Division by zero condition!";
}
return(double)a / (double)b;
}
noexcept(expression)还可以作为一个运算符去检测一个表达式是否会抛出异常,可能会,注意这里说的是可能,则返回false,不会就返回true。
**示例**
int main()
{
int i = 0;
cout << noexcept(Divide(1, 2)) << endl;
cout << noexcept(Divide(1, 0)) << endl;
cout << noexcept(++i) << endl;
}
**运行结果**
二.标准库的异常
C++标准库也定义了一套自己的一套异常继承体系库,基类是exception,所以我们日常写程序,需要在主函数捕获exception即可,要获取异常信息,就调用what函数。
**示例**
#include <exception>
#include <string>
// 基础业务异常(通用场景)
class BusinessException : public std::exception //继承exception就可以
{
public:
explicit BusinessException(const std::string& msg)
: msg_(msg)
{}
const char* what() const noexcept override
{
return msg_.c_str();
}
private:
std::string msg_;
};
// 具体领域异常(推荐继承最匹配的标准异常)
class DatabaseException : public std::runtime_error //也可以继承更具体的标准异常
{
public:
explicit DatabaseException(const std::string& msg, int error_code = 0)
: std::runtime_error(msg),
error_code_(error_code) {}
int error_code() const noexcept
{
return error_code_;
}
private:
int error_code_;
};
创作不易,还请各位大佬支持~