一、为什么要有异常?
在C语言中, 一般遇到错误会返回错误码, 而不同的错误码又对应不同的错误信息, 通过对应关系可以根据错误码得出错误的大致原因, 但是这还是有点局限了, 因为返回的信息有限, 并不能很好的反馈出错误信息, 而且有的情况下是无法通过返回值来返回错误码的, 比如:
T& func()
{
//do something...
}
遇到此种情况, 是无法返回错误码的. 虽然在C语言中可以通过设置 assert 来解决遇到错误的情况, 但是 assert 有点太暴力了, 它会直接终止进程, 而且 assert 在 Debug 模式下有效, 而在 Release 模式下是无效的.
而异常可以抛出的错误类型就可以很多变了, 字符串、对象等, 可以携带更多有用的错误信息, 能够更有效的提高解决问题的效率.
二、语法规则
通过 throw 可以抛出异常, 再使用:
try
{}
catch(Exception type)
{}
来捕捉并处理异常, try 与 catch 必须配套使用, catch 可以有多个:
try
{}
catch(Exception type)
{}
catch(Exception type)
{}
...
并且 throw 抛出异常后一定要捕获处理, 如果抛出异常后没有捕获会报错, 如下:
而关于异常的执行流是这样的, 抛出异常后的语句都不会执行, 会直接跳过, 比如:
void func1(int n)
{
if (n == 0)
{
throw "exception";
}
cout << "void func1(int n)" << endl;
return;
}
void func2()
{
int n;
cin >> n;
func1(n);
cout << "void func2()" << endl;
return;
}
int main()
{
try
{
func2();
}
catch(const char* e)
{
cout << e << endl;
}
catch (...)
{
cout << "Unknown Exception" << endl;
}
cout << "int main()" << endl;
return 0;
}
在func1中 throw 抛出异常后后面的语句不会执行, 且不会返回被调用的func2处, 而是直接跳到 main 函数中匹配抛出异常类型的 catch 语句中, 且只会执行一个 catch 语句, 与 if-else-else if 相似, 执行完 catch 语句后, 后面的语句会正常执行, 比如 " cout << “int main()” << endl; " 这句会被正常执行.
而对于抛出异常的捕获规则, 遵循就近原则, 上述例子中, func2 抛出异常, 如果调用其的 func1 写了 try-catch 语句则由其捕获, 如果没有则继续由调用 func1 的函数来处理, 就这样层层往回的处理, 如果直到 main 函数都没有捕获处理的话, 编译器会直接报错.
而对于抛出的异常, 必须由和异常类型一致的 catch 语句来捕捉, 否则会报错, 比如:
void func1(int n)
{
if (n == 0)
{
throw "exception";
}
cout << "void func1(int n)" << endl;
return;
}
void func2()
{
int n;
cin >> n;
func1(n);
cout << "void func2()" << endl;
return;
}
int main()
{
try
{
func2();
}
catch(int e)
{
cout << e << endl;
}
cout << "int main()" << endl;
return 0;
}
这里抛出的异常类型是 const char*, 而只有一个 catch 且捕获类型为 int, 所以这里会直接报错:
所以要注意捕获异常类型与抛出异常类型的一致.
三、使用技巧
捕获异常类型与抛出异常类型可以一致, 也可以是父子类关系, 即抛出子类对象的异常, 可以由父类对象来接收该异常, 比如:
class Exception
{
public:
Exception(int errid, string errmsg)
:_errid(errid)
, _errmsg(errmsg)
{}
virtual void what() const
{
cout << _errid << ":" << _errmsg << endl;
}
protected:
int _errid;
string _errmsg;
};
class A_Exception : public Exception
{
public:
A_Exception(int errid, string errmsg, string amsg)
:Exception(errid, errmsg)
, _amsg(amsg)
{}
virtual void what() const
{
cout << _errid << ":" << _errmsg << ":" << _amsg << endl;
}
private:
string _amsg;
};
class B_Exception : public Exception
{
public:
B_Exception(int errid, string errmsg, string bmsg)
:Exception(errid, errmsg)
, _bmsg(bmsg)
{}
virtual void what() const
{
cout << _errid << ":" << _errmsg << ":" << _bmsg << endl;
}
private:
string _bmsg;
};
void func1(int n)
{
if (n == 0)
{
throw A_Exception(1, "Exception", "A_Exception");
}
cout << "void func1(int n)" << endl;
return;
}
void func2()
{
int n;
cin >> n;
func1(n);
cout << "void func2()" << endl;
return;
}
int main()
{
try
{
func2();
}
catch(const Exception& e)
{
e.what();
}
cout << "int main()" << endl;
return 0;
}
通过父类的指针或引用来接收子类异常的抛出, 这样可以大大规范乱抛异常不好捕获的情况, 也体现了OO语言多态的特性, 那么来看一个问题:
int main()
{
try
{
func2();
}
catch(const Exception& e)
{
e.what();
}
catch (const A_Exception& e)
{
e.what();
}
cout << "int main()" << endl;
return 0;
}
此时抛出异常的类型为 A_Exception, 那么此时会进入哪个 catch 语句呢? 按照以往的理解, 应该会匹配类型最合适的那个, 但这里进入的是 catch(const Exception& e) 这个语句, 如果存在父类和自己同一类型的 catch, 依然是遵循就近原则, 如果此时把 catch(const Exception& e) 和 catch (const A_Exception& e) 的先后位置调换以下, 就换进入 catch (const A_Exception& e) 语句.
有时候会面临着对方抛出的异常不是属于自定义异常的体系中, 我们也不清楚对方抛出了什么类型, 那么此时就可以用以下方式来捕获这种不清楚类型的异常:
catch(...)
{}
这种写法就表示捕获任意类型的异常, 一般写在所有 catch 语句的最后, 用来兜底.
四、异常安全
异常的执行流是很跳跃的, C语言错误码的返回都是一层一层的, 而异常的这种跳跃性, 就很容易的引起内存泄漏的问题, 比如:
void func1(int n)
{
if (n == 0)
{
throw "Exception";
}
cout << "void func1(int n)" << endl;
return;
}
void func2()
{
int n;
cin >> n;
int* a = new int;
func1(n);
delete a;
cout << "delete a" << endl;
cout << "void func2()" << endl;
return;
}
int main()
{
try
{
func2();
}
catch(const char* e)
{
cout << "Exception" << endl;
}
catch (...)
{
cout << "Unknown Exception" << endl;
}
cout << "int main()" << endl;
return 0;
}
向上面这种情况, 异常抛出后直接跳到了 main 函数的 catch 语句中, 那么此时的 a 就没有得到释放, 造成了内存泄漏.
解决这种问题的办法由两种:
- 异常的重新抛出: 在申请有空间的函数内, 我们可以捕获抛出的异常, 然后在 catch 语句中先把该函数申请的空间给释放了, 再将捕获到的异常重新抛出(一般来说都由最外层来处理异常).
void func2()
{
int n;
cin >> n;
int* a = new int;
try
{
func1(n);
}
catch(...)
{
delete a;
cout << "delete a" << endl;
throw;
}
cout << "void func2()" << endl;
return;
}
- 使用智能指针来管理 new 出来的对象.
另外, 最好不要在构造函数和析构函数中抛出异常, 因为如果在构造函数中抛出异常, 可能导致对象不完整或者没有被完全初始化, 而在析构函数中抛出异常, 可能导致资源没有被全部释放的情况.
五、相关规范
在C++98中提供了一个有关某函数是否会抛出异常的规范, 如果某函数不会抛出异常, 则在该函数后面加上 throw(), 比如:
void func1(int n) throw()
{
//do something...
}
如果某函数会抛出异常, 则在该函数后面加上 throw(A, B, C), 表示该函数可能抛出 A, B, C 三种异常.
这种写法不算太好, 存在混淆的情况, throw() 会让人误以为也会抛出异常, 所以在C++11中新增了 noexcept 关键字, 比如:
void func1(int n) noexcept
{
//do something...
}
在函数的后面加上该关键字就表示该函数不会抛出异常, 反之就表示会抛出异常, 这就很浅显易懂了.
在 Microsoft Visual Studio Community 2019 版本 16.11.3 IDE下, 如果某函数加上了 throw() 或 noexcept 后仍抛出异常, 编译器会直接报错.