异常处理
1.简介
与java一样,程序运行时常会碰到一些错误,如果不处理,很可能导致程序崩溃。C++ 异常处理机制就可以让我们捕获并处理这些错误
C++ 异常处理机制会涉及 try、catch、throw 三个关键字。
2.常见运行时错误
运行时错误是指程序在运行期间发生的错误,例如除数为 0、内存分配失败、数组越界、文件不存在等。C++ 异常(Exception)机制就是为解决运行时错误而引入的。
案例:
#include <iostream>
#include <string>
using namespace std;
int main(){
string str = "http://c.biancheng.net";
char ch1 = str[100]; //下标越界,ch1为垃圾值
cout<<ch1<<endl;
char ch2 = str.at(100); //下标越界,抛出异常
cout<<ch2<<endl;
return 0;
}
str[]😕/不会检查下标越界,不会抛出异常,所以即使有错误,try 也检测不到
//换句话说,发生异常时必须将异常明确地抛出,try 才能检测到;如果不抛出来,即使有异常 try 也检测不到。所谓抛出异常,就是明确地告诉程序发生了什么错误。
try-catch抓捕异常
#include <iostream>
#include <exception>
using namespace std;
int main(int argc, char const *argv[])
{
string str = "http://c.biancheng.net";
try
{
char ch1 = str[100];//因为[]不会检查下标越界,不会抛出异常,所以即使有错误,try 也检测不到
//换句话说,发生异常时必须将异常明确地抛出,try 才能检测到;如果不抛出来,即使有异常 try 也检测不到。所谓抛出异常,就是明确地告诉程序发生了什么错误。
cout << ch1 << endl;
}
catch (exception e)
{
cout << "[1]out of bound!" << endl;
}
try
{
char ch2 = str.at(100);//推荐使用at,会检查下标越界
cout << ch2 << endl;
}
catch (exception &e)
{ //exception类位于<exception>头文件中
cout << "[2]out of bound!" << endl;
}
system("pause");
return 0;
}
3.抛出异常throw
throw 可以抛出异常。自定义的异常
#include <iostream>
#include <string>
#include <exception>
using namespace std;
void func()
{
throw "Unknown Exception"; //抛出异常
cout << "[1]This statement will not be executed." << endl;
}
int main()
{
try
{
func();
cout << "[2]This statement will not be executed." << endl;
}
catch (const char *&e)
{
cout << e << endl;
}
system("pause");
return 0;
}
4.catch异常类型
异常既然是一份数据,那么就应该有数据类型。C++ 规定,异常类型可以是 int、char、float、bool 等基本类型,也可以是指针、数组、字符串、结构体、类等聚合类型。C++ 语言本身以及标准库中的函数抛出的异常,都是 exception 类或其子类的异常。也就是说,抛出异常时,会创建一个 exception 类或其子类的对象。
我们可以将 catch 看做一个没有返回值的函数,当异常发生后 catch 会被调用,并且会接收实参(异常数据)。
总的来说,catch 和真正的函数调用相比,多了一个「在运行阶段将实参和形参匹配」的过程。
————————与java基本一致,注意,在C++眼中,除以0不可接受的,不应当作为异常处理。
#include <iostream>
#include <string>
using namespace std;
class Base
{
};
class Derived : public Base
{
};
int main()
{
try
{
throw Derived(); //抛出自己的异常类型,实际上是创建一个Derived类型的匿名对象
cout << "This statement will not be executed." << endl;
}
catch (int)
{
cout << "Exception type: int" << endl;
}
catch (char *)
{
cout << "Exception type: cahr *" << endl;
}
catch (Base)
{ //匹配成功(向上转型)
cout << "Exception type: Base" << endl;
}
catch (Derived)
{
cout << "Exception type: Derived" << endl;
}
system("pause");
return 0;
}
5.catch在匹配过程中类型转换
C/C++ 中存在多种多样的类型转换,以普通函数(非模板函数)为例,发生函数调用时,如果实参和形参的类型不是严格匹配,那么会将实参的类型进行适当的转换,以适应形参的类型,这些转换包括:
- 算数转换:例如 int 转换为 float,char 转换为 int,double 转换为 int 等。
- 向上转型:也就是派生类向基类的转换,请猛击《C++向上转型(将派生类赋值给基类)》了解详情。
- const 转换:也即将非 const 类型转换为 const 类型,例如将 char * 转换为 const char *。
- 数组或函数指针转换:如果函数形参不是引用类型,那么数组名会转换为数组指针,函数名也会转换为函数指针。
- 用户自定的类型转换。
catch 在匹配异常类型的过程中,也会进行类型转换,但是这种转换受到了更多的限制,仅能进行「向上转型」、「const 转换」和「数组或函数指针转换」,其他的都不能应用于 catch。
案例:
#include <iostream>
using namespace std;
int main()
{
int nums[] = {1, 2, 3};
try
{
throw nums;
cout << "This statement will not be executed." << endl;
}
catch (const int *)
{
cout << "Exception type: const int *" << endl;
}
system("pause");
return 0;
}
Exception type: const int *
请按任意键继续. . .
5.throw 抛出异常规范
throw 关键字除了可以用在函数体中抛出异常,还可以用在函数头和函数体之间,指明当前函数能够抛出的异常类型,这称为异常规范。
double func (char param) throw (int);
这条语句声明了一个名为 func 的函数,它的返回值类型为 double,有一个 char 类型的参数,并且只能抛出 int 类型的异常。如果抛出其他类型的异常,try 将无法捕获,只能终止程序。
如果函数会抛出多种类型的异常,那么可以用逗号隔开:
double func (char param) throw (int, char, exception);
如果函数不会抛出任何异常,那么( )
中什么也不写:
double func (char param) throw ();
如此,func() 函数就不能抛出任何类型的异常了,即使抛出了,try 也检测不到。
1.虚函数中的异常规范
C++ 规定,派生类虚函数的异常规范必须与基类虚函数的异常规范一样严格,或者更严格。只有这样,当通过基类指针(或者引用)调用派生类虚函数时,才能保证不违背基类成员函数的异常规范。请看下面的例子:
class Base{
public:
virtual int fun1(int) throw();
virtual int fun2(int) throw(int);
virtual string fun3() throw(int, string);
};
class Derived:public Base{
public:
int fun1(int) throw(int); //错!异常规范不如 throw() 严格
int fun2(int) throw(int); //对!有相同的异常规范
string fun3() throw(string); //对!异常规范比 throw(int,string) 更严格
}
2) 异常规范与函数定义和函数声明
C++ 规定,异常规范在函数声明和函数定义中必须同时指明,并且要严格保持一致,不能更加严格或者更加宽松。
简单说:与声明保持一致。
//错!定义中有异常规范,声明中没有
void func1();
void func1() throw(int) { }
//错!定义和声明中的异常规范不一致
void func2() throw(int);
void func2() throw(int, bool) { }
//对!定义和声明中的异常规范严格一致
void func3() throw(float, char*);
void func3() throw(float, char*) { }
注意:请抛弃异常规范,不要再使用它
异常规范的初衷是好的,它希望让程序员看到函数的定义或声明后,立马就知道该函数会抛出什么类型的异常,这样程序员就可以使用 try-catch 来捕获了。如果没有异常规范,程序员必须阅读函数源码才能知道函数会抛出什么异常。
不过这有时候也不容易做到。例如,func_outer() 函数可能不会引发异常,但它调用了另外一个函数 func_inner(),这个函数可能会引发异常。再如,您编写的函数调用了老式的库函数,此时不会引发异常,但是库更新以后这个函数却引发了异常。总之,异常规范的初衷实现起来有点困难,所以大家达成的一致意见是,最好不要使用异常规范。
异常规范是 C++98 新增的一项功能,但是后来的 C++11 已经将它抛弃了,不再建议使用。
6.exception类
C++异常基类。C++语言本身或者标准库抛出的异常都是 exception 的子类,称为标准异常(Standard Exception)。
class exception
{
public:
exception() _GLIBCXX_USE_NOEXCEPT { } 构造函数
virtual ~exception() _GLIBCXX_TXN_SAFE_DYN _GLIBCXX_USE_NOEXCEPT;//虚析构函数
virtual const char*
what() const _GLIBCXX_TXN_SAFE_DYN _GLIBCXX_USE_NOEXCEPT; //虚函数
};
std::exception 该异常是所有标准 C++ 异常的父类。
std::bad_alloc 该异常可以通过 new 抛出。
std::bad_cast 该异常可以通过 dynamic_cast 抛出。
std::bad_exception 这在处理 C++ 程序中无法预期的异常时非常有用。
std::bad_typeid 该异常可以通过 typeid 抛出。
std::logic_error 理论上可以通过读取代码来检测到的异常。
std::domain_error 当使用了一个无效的数学域时,会抛出该异常。
std::invalid_argument 当使用了无效的参数时,会抛出该异常。
std::length_error 当创建了太长的 std::string 时,会抛出该异常。
std::out_of_range 该异常可以通过方法抛出,例如 std::vector 和 std::bitset<>::operator。
std::runtime_error 理论上不可以通过读取代码来检测到的异常。
std::overflow_error 当发生数学上溢时,会抛出该异常。
std::range_error 当尝试存储超出范围的值时,会抛出该异常。
std::underflow_error 当发生数学下溢时,会抛出该异常。
7.泛型捕获
void function(){
try {
/* your code */
}catch(...) {
/* handler */
}
}
8.自定义异常类型
#include <iostream>
using namespace std;
class MyException : public exception
{
public:
MyException();
MyException(string emsg);
~MyException(){};
virtual const char *what() const throw();
protected:
string emsg;
};
MyException::MyException() : exception()
{
}
MyException::MyException(string emsg)
{
this->emsg = emsg;
}
const char *MyException::what() const throw()
{
return this->emsg.c_str();
}
void test()
{
MyException my("数据错误");
throw my;
cout << "不会执行" << endl;
}
int main(int argc, char const *argv[])
{
try
{
test();
}
catch (const MyException &e)
{
cout << e.what() << endl;
}
system("pause");
return 0;
}
数据错误
请按任意键继续. . .
9.总结
catch:可以直接抓捕要抛出的类型包括可能的转换类型,相比java更加直接,java需要包裹exception,c++不需要
exception: 不同于java,它的类型大部分不会自己打印,而是需要自定义,java中,有各种系统帮我们定义好的异常。
argv[])
{
try
{
test();
}
catch (const MyException &e)
{
cout << e.what() << endl;
}
system(“pause”);
return 0;
}
数据错误
请按任意键继续. . .
### 9.总结
catch:可以直接抓捕要抛出的类型包括可能的转换类型,相比java更加直接,java需要包裹exception,c++不需要
exception: 不同于java,它的类型大部分不会自己打印,而是需要自定义,java中,有各种系统帮我们定义好的异常。