目录
1. 异常基本概念
1.1 C语言异常处理错误的方式
在C语言中, 处理错误的方式主要有两种:
- 返回错误码 缺陷: 需自行查找对应的错误码;
- 终止程序 缺陷: 程序直接被终止;
但都有其缺陷, 都只能提供简略的错误信息, 所以在 C++11 中引入了新的处理方案—异常.
1.2 异常处理
异常是一种面向对象的 处理错误的方式, 可以抛出异常的详细信息.
C++11 中新增了三个关键字:
- try: 监测当前代码区是否存在异常.
- catch: 当异常被抛出时, 捕获异常.
- throw: 当出现异常时, 抛出异常.
在 try 块中放置可能出现异常的代码, 由 throw 抛出, catch 捕获, 就是异常处理的大概流程.
所以在 try 块内的代码又被称为保护代码.
int main()
{
try
{
// 抛出
throw /*异常对象*/;
}
catch (/*异常对象*/)
{
// 异常处理...
}
return 0;
}
2. 异常的使用
2.1 异常的抛出和捕获
异常由 throw 关键字抛出, 抛出的的是一个对象, 可以包含异常信息等内容;
并且 catch 只会捕获和参数部分类型相匹配的异常对象.
void func(const void* ptr)
{
if (!ptr)
{
throw "异常: 空指针";
}
}
int main()
{
try
{
func(nullptr);
}
catch (const char* s)
{
cout << s << endl;
}
return 0;
}
若没有异常, 那么会直接跳过 catch 块, 执行之后的代码.
void func(const void* ptr)
{
if (!ptr)
{
throw (string("异常: 空指针"));
}
}
int main()
{
try
{
func("nullptr");
}
catch (const char* s)
{
cout << s << endl;
}
return 0;
}
若不匹配, 则不会捕获; 若异常没有被捕获, 程序将会异常终止.
void func(const void* ptr)
{
if (!ptr)
{
throw (string("异常: 空指针"));
}
}
int main()
{
try
{
func(nullptr);
}
catch (const char* s)
{
cout << s << endl;
}
return 0;
}
所以一个 try 块 可以对应多个 catch, 具体看 throw 的对象类型.
void func(const void* ptr)
{
if (!ptr)
{
throw (string("异常: 空指针"));
}
}
int main()
{
try
{
func(nullptr);
}
catch (const char* s)
{
cout << s << endl;
}
catch (const string& s)
{
cout << s << endl;
}
return 0;
}
并且一个 try 块 不可以有多个相同类型的 catch.
当存在多个 try | catch 块时, throw 抛出会根据栈帧顺序, 被最近的, 类型匹配的 catch 块捕获;
并且跳过的栈帧会被自动清理.
catch 块只能进入一次, 异常被捕获后, 无法再进入其他 catch 块.
void func2(const void* ptr)
{
if (!ptr)
{
throw (string { "异常: 空指针" });
}
}
void func1()
{
try
{
func2(nullptr);
}
catch (const string& s)
{
cout << "func1: " << s << endl;
}
}
int main()
{
try
{
func1();
}
catch (const string& s)
{
cout << "main: " << s << endl;
}
return 0;
}
throw 抛出的异常对象处理类似函数的传值返回, 都是局部对象.
catch(…) 可以捕获任意类型的异常, 类似一个保底, 避免程序因异常无法捕获而终止.
void func(const void* ptr)
{
if (!ptr)
{
string s("异常: 空指针");
throw s;
}
}
int main()
{
try
{
func(nullptr);
}
catch (...)
{
cout << "未知异常"<< endl;
}
return 0;
}
异常捕获支持继承和多态, 可以使用基类对象捕获 抛出的派生类对象
通常被应用于自定义的异常体系, 规范异常的行为, 可以统一进行异常处理.
例:
// 基类
class Exception
{
public:
Exception(const string& errmsg, int errcode)
: _errmsg(errmsg), _errno(errcode)
{}
virtual string what() const
{
return to_string(_errno) + " : " + _errmsg;
}
protected:
string _errmsg;
int _errno = 0;
};
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;
};
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;
};
void SQLMgr()
{
if (rand() % 6 == 0)
{
throw SqlException("权限不足", 100, "select * from name = '张三'");
}
cout << "!!!!!!!!!!!!!!!!!!!!!!!!运行成功" << endl;
}
void CacheMgr()
{
if (rand() % 4 == 0)
{
throw CacheException("权限不足", 100);
}
else if (rand() % 5 == 0)
{
throw CacheException("数据不存在", 101);
}
SQLMgr();
}
void HttpServe()
{
if (rand() % 2 == 0)
{
throw HttpServerException("请求资源不存在", 100, "get");
}
else if (rand() % 3 == 0)
{
throw HttpServerException("权限不足", 101, "post");
}
CacheMgr();
}
int main()
{
srand(time(0));
int i = 10;
while (i--)
{
try
{
HttpServe();
}
catch (const Exception& e)
{
// 异常处理
cout << e.what() << endl << "===================" << endl;
}
catch (...)
{
cout << "未知异常" << endl;
}
}
return 0;
}
2.2 异常的重新抛出
由于异常抛出会直接跳转至 catch 中, 导致部分代码为执行, 可能会导致内存泄漏.
int main()
{
try
{
int* i = new int;
//func(nullptr);
cout << "delete" << endl;
delete i;
}
catch (const string& s)
{
cout << s << endl;
}
return 0;
}
那么就需要重新抛出, 在可能出现异常, 且影响空间释放的的位置, 主动捕获, 释放空间后, 再次抛出.
并且异常捕获一般由最外层统一处理.
int main()
{
try
{
int* i = new int;
try
{
func(nullptr);
}
catch (const string& s)
{
cout << "delete" << endl;
delete i;
throw s;
}
cout << "delete" << endl;
delete i;
// 其他操作...
}
catch (const string& s)
{
cout << s << endl;
}
return 0;
}
2.3 异常安全
- 尽量不要在构造函数中抛出异常, 可能导致对象不完整或没有完全初始化;
- 尽量不要在析构函数内抛出异常, 可能导致资源泄漏;
- 在使用 malloc/free, new/delete, fopen/fclose, lock/unlock 等资源管理配套函数时, 需注意资源泄漏或死锁等问题.
2.4 异常规范
C++98 标准规定:
- 可以在函数的后面接 throw(type1, type2, type3 …), 表示此函数可能抛出的所有异常类型;
void func() throw(/*type*/, /*type*/, /*type*/) // 可能抛出三种中的一种
- 若函数的后面接 throw(), 表示函数不抛出异常, 但也可以抛出;
void func() throw() // 函数不抛出异常
- 若无异常接口声明, 则此函数可以抛出任何类型的异常.
void func() // 可以抛出任意类型的异常
但 非强制性规定.
C++11 中也更新了一个异常规范:
- 若函数的后面接 noexcept 关键字, 也表示函数不抛出异常.
void func() noexcept // 函数不抛出异常
但若函数或内部的调用仍会抛出异常, 那么程序将直接终止.
void func1(const void* ptr) // noexcept 这里和下面结果一样, 并且这里有 throw, 编译器会报 warning
{
if (!ptr)
{
string s("异常: 空指针");
throw s;
}
}
void func(const void* ptr) noexcept
{
func1(nullptr);
}
int main()
{
try
{
func(nullptr);
}
catch (const string& s)
{
cout << s << endl;
}
return 0;
}
3. 异常体系
3.1 C++ 标准库的异常体系
C++ 标准库中提供了一套 异常体系, 可以直接使用或继承 std::exception 基类, 实现其他异常处理.
3.2 自定义异常体系
由于 C++ 标准库的异常体系并不能满足需求, 所以可以根据需求自定义异常体系.
4. 异常的优缺点
优点:
- 可以清晰准确的展示出错误的各种信息, 更好的定位程序 Bug;
- 比起错误码, 异常可以直接被捕获;
- 为了兼容第三方库, 很多第三方库包含了异常, 比如 boost, gtest, gmock;
- 部分函数使用异常更好处理, 比如 越界访问等.
缺点:
- 会导致执行流跨度过大,并且非常混乱, 导致部分调试比较困难.
- 异常有一些性能上的开销, 但现在基本可以忽略不计.
- C++ 没有垃圾回收机制, 自行管理资源, 易导致内存泄漏, 死锁等问题, 需要使用 RAII 来处理资源管理问题.
- C++ 标准库的异常体系定义不够好, 导致出现了各种异常体系, 比较混乱.
异常尽量规范使用:
- 抛出异常类型都继承自一个基类;
- 函数是否抛异常可以使用 throw() 注明.