在c中进程出现错误时为了防止程序继续进行错误程序,提供错误的结果;程序会设置errno错误码,更有甚者会直接将程序终止掉;这是C语言的处理异常的方式;
而在C++中,引入了throw抛异常,try catch捕捉异常的方式,可以捕捉我们所知道并且指明的错误,不让程序终止,提高用户的体验感,当然如果遇到了严重的错误依然会终止进程;
那么相较与C语言处理异常的方式C++的优势究竟在哪里呢?首先C++是兼容C语言的,所以C语言的获取异常方式C++也是拥有的;其次C++获取的错误方式更加个性化,用户可以自定义错误的返回类型,可以自定义不同类型的错误信息,也让返回的错误信息更加明了相较与C语言的返回错误码;那么接下来让我们看看C++中到底是如何处理异常的吧:
1.C++中如何处理异常
首先,什么时候会出现异常,我们应该是清楚的,所以我们会在可能出现异常的地方增加判断语句,如果出现了某个异常,就会在此处抛出异常,在调用我们此函数的地方接收我们这个函数中抛出的异常;
throw就是用来抛出异常的关键字,我们可以让throw抛出任意类型的错误信息;
try catch是上层用来接收不同类型错误信息的关键字,其中try用来调用可能会抛出一样的函数或者代码,当这其中的代码出现异常抛出异常时,会让程序直接跳转到catch内的代码中,catch内的代码对返回的错误信息进行处理,提供给程序员参考,其中一个try可以对应多个catch;
下面我们用实例来演示一下:
1.1实现C++中异常处理
double divide(int x,int y)
{
//发生了除0错误,这是内存错误本该直接终止进程的
//但我们清楚的知道可能会发生这个错误,所以在这里
//进行判断,如果出现了这个错误就抛异常,不要终止程序
if (y == 0)
{
string erro("出现除0错误");
throw erro;
}
double ret = x / (double)y;
return ret;
}
int main()
{
try {
int x, y;
while (1)
{
cin >> x >> y;
cout << x << "/" << y << "=" << divide(x, y) << endl;
}
}
catch (const string& a)//如果try中的代码出现异常就会直接跳转到此处;
{
cout << a;
}
return 0;
}
现象:
1.2异常处理的细节
1.异常出现并使用throw抛出异常时,程序会直接跳转到与返回类型匹配的catch区域,在这其中的栈帧调用空间会自动销毁;
以上面代码示例为例:
抽象图:
2.throw抛出的异常会去寻找到与他最近的匹配返回类型的catch:
1.throw值与catch值类型匹配
2.就近
3.异常抛出的错误信息和函数一样是传值返回,但C++11出现的移动构造很好的解决了这个拷贝的消耗问题;
4.异常如果throw抛出了,但如果没有被catch接收会导致程序直接终止;
异常未被捕捉报错:abort() has been called
在上层接收throw时一般会加上最后一道防线:
catch(...)
{
cout<<"未知异常,有人没有按要求抛出异常"<<endl;
}
这样表示会接收所有throw抛出的异常,防止调用的函数底层有不规范的异常抛出;
5.使用多态来获取异常
在工作中一个程序的编写一定是多个人合作进行的,那么我们一定会互相调用对方所编写的函数,而这些函数中,可能会存在异常,而调用函数的我们如果没有在上层catch此异常,一定会导致程序终止,就算是加上上一点的catch(...)我们也无法明确的知道到底是哪个地方没有规范的抛出异常;并且就算我们知道我们调用的函数底层有哪些异常,我们还得一个一个的catch所有的异常,这是一个频繁的相同的工作;所以为了解决这些问题,厉害的程序员想出了用多态来进行异常的throw与catch;下面让我们看看这究竟是如何解决的:
在函数的底层throw抛出异常时,我们定义的错误信息的类型是自定义类型要继承一个公共的父类;通过这个公共的,再重写父类中的输出错误信息函数,构成多态的形式;这样在上层的catch中只需要调用相同的接口即可获得不同异常的信息;
class parent
{
public:
virtual void what()
{
cout << "我是父类错误类" << endl;
}
};
class A:public parent
{
public:
A(int errorid, string msg)
:_errorId(errorid),_errorMsg(msg)
{
}
virtual void what()
{
cout << "class A" << endl;
cout << "errorID: " << _errorId
<< " errorMsg: " << _errorMsg << endl;
}
private:
int _errorId;
string _errorMsg;
};
class B :public parent
{
public:
B(int errorid, string msg)
:_errorId(errorid), _errorMsg(msg)
{
}
virtual void what()
{
cout << "class B" << endl;
cout << "errorID: " << _errorId
<< " errorMsg: " << _errorMsg << endl;
}
private:
int _errorId;
string _errorMsg;
};
void funA()
{
throw A(1,"除0错误");
}
void funB()
{
throw B(2, "内存空间不足");
}
int main()
{
try
{
funA();
funB();
}
catch(parent & p)
{
p.what();
}
try
{
funB();
}
catch (parent& p)
{
p.what();
}
return 0;
}
现象:
这样规范了异常的返回值类型,提高了上层调用函数的效率,也让可以让整个工作组更加严谨;
2.异常安全问题
由于异常在执行时伴随着代码的执行流跳跃,所以一定会有代码未被执行,可能会导致进程出现问题;我们下面来举一些例子:
2.1异常安全的情况
// 异常的安全问题
class A
{
public:
A()
{
cout << "A()" << endl;
}
~A()
{
cout << "~A()" << endl;
}
};
class B
{
public:
B()
{
cout << "B()" << endl;
}
~B()
{
cout << "~B()" << endl;
}
};
void fun()
{
A* pa = new A[2];
B* pb = new B[2];
//throw string("抛出异常");
delete[]pa;
delete[]pb;
}
int main()
{
try
{
fun();
}
catch (const string& str)
{
cout << str << endl;
}
return 0;
}
直接运行:
如果将注释代码解除注释:
我们会发现由于执行流的跳转导致了fun函数的代码没有执行完,从而使得出现了内存泄漏问题;
那在这样的异常如何解决呢?我们可以使用重新抛出的方式先catch住异常让我们的需要代码执行完再抛出一次异常给上层去解决;
2.2重新抛出
修改fun函数代码
void fun()
{
A* pa = new A[2];
B* pb = new B[2];
if (1)
{
try {
throw string("抛出异常");
}
catch (const string& str)
{
delete[]pa;
delete[]pb;
throw;
}
}
delete[]pa;
delete[]pb;
}
执行流跳跃会导致的线程安全问题远远不止这样的内存泄漏,还可能会引起死锁等问题,非常麻烦,所以异常的使用一定要小心谨慎;
2.3 RAII维护线程安全
其中处理异常安全还可以使用RAII的方式来处理,具体怎么处理等待下一篇智能指针的详细讲解
3.异常规范
在C++中祖师爷为了让异常更加严谨与安全为其制定了一个规则,在抛出异常的函数后方会加上throw()的声明,提示程序员这个函数调用时要注意可能会抛出什么样的异常;
3.1 throw()声明
下面的声明表示fun这个函数中可能会抛出A或者B类型的异常
fun()throw(A,B)
{}
下面的声明表示fun这个函数中不会抛出异常
fun()throw()
{}
但是祖师爷并没有严格的对这套规则进行控制,编译器进行检查的消耗也非常大也很难做到控制所有函数中只要抛出了异常就要在函数后加上throw声明的效果;又因为程序员的水平参差不齐,所以这套规范并没有得到很好的推崇;所以throw()只做提示的作用,不是严格的要求;
3.2 noexcept关键字
但是在C++11中出现了noexcept关键字,在函数后声明这个关键字则表示这个函数不会抛出任何的异常;
fun()noexcept;
当一个函数声明了此关键字但是函数中还是抛出了异常时,会导致程序崩溃,但编译还是可以编译出可执行程序的;
4.异常的优缺点
优点
1.异常的错误信息更加明确相比较errno错误码的形式;
2.异常的返回是执行流直接跳转的,想比与errno错误码更快
3.有很多库函数用到了异常
4.有些函数如构造析构无法直接返回errno值,而异常可以直接进行跳转返回;
缺点
1.异常的安全问题,执行流的跳转导致的内存泄漏,死锁等问题;
2.异常的规范使用不是严格要求的,导致异常的体系十分混乱;我们最好要规范的使用异常1.用多态的方式返回继承类2.在抛出了异常的函数后要加上throw()声明或者没有抛异常函数后加上noexcept关键字;
3.C++库中也有异常库,但是似乎定义的并不好,比较混乱(这点我是听说的,并没有进行实践,等待后续的实践验证)
5.异常的标准库
C++有它自己的一套标准库,这套标准库中有一个基类exception,其中所以的异常类都继承了这个类,并且所有类调用错误信息的接口都是what,某类.what();,获取错误信息;一般错误类中的信息至少包括一个错误的编号,还有一段错误信息;具体的异常类的使用,这里就不赘述了,使用gpt搜索即可,此外还可以查看cplus网站中的讲解