文章目录
1 异常的基本概念
基本思想:当函数出现自身无法处理的错误时,抛出(throw)异常,由该函数的直接或间接的调用者处理异常,即问题检测与问题处理相分离。
异常处理:处理程序执行期间的错误,即程序运行过程产生的异常事件,如除0溢出、数组下标越界、读取的文件不存在、空指针、内存不足(内存溢出或内存泄露)等。
1.1 C语言中的异常处理
方法:
(1)使用整型的返回值标识错误,如0标识正常、-1标识错误。
(2)使用errno宏
(全局整型变量)记录错误:当程序出错时,errno宏
的值发生改变。
缺点:
①函数的返回值不一致,无统一标准,如某些函数返回1 / 0
表示成功/错误,某些函数返回0 / 非0
表示成功/错误。
②函数的返回值只有1个,无法判断返回值表示错误代码或正常结果,可能产生二义性。
1.2 C++中的异常处理
关键字:
try
:try块中的代码为保护代码(即可能抛出异常的代码),通常后跟一个或多个catch块。
注:若try块代码执行期间未产生异常,则try块后的所有catch子句均不会执行。
catch
:捕获异常,在希望处理问题的地方,通过异常处理程序捕获异常。
①
catch(内置基本数据类型)
可捕获抛出的内置基本数据类型的数据,如catch(int)
可捕获所有整型数据。
②catch(MyException e)
可捕获自定义异常类的对象,如捕获自定义异常类的匿名对象。
③catch(...)
可捕获任何类型的异常,通常用在最后一个catch语句,表示捕获除其它catch块异常类型之外的任何类型异常,类似于switch结构
的default子句
。
注1:多个catch子句会根据先后顺序依次被检查,匹配异常类型的catch子句会捕获并处理异常或继续抛出异常。
注2:若不存在与异常类型相匹配的catch语句,则系统会自动调用terminate函数
,该函数内部调用abort()函数
,使程序终止/中断。
throw
:throw语句可在代码块的任何地方抛出异常。
①
throw 内置基本数据类型数据;
表示抛出内置基本数据类型的异常,如throw 3.14;
表示抛出double类型异常。
②throw 自定义异常类的匿名对象;
表示抛出自定义异常类的异常,如throw MyException();
表示抛出MyException类
的异常。
注:若当前catch语句捕获的异常不想处理或无法处理,可使用throw;
继续抛出异常。
语法:
try{
//保护代码(可能抛出异常的代码)
}catch(ExceptionName e1)
{
//处理ExceptionName异常的代码
}catch(MyException e)
{
//处理自定义异常MyException的代码
}catch(...) //省略号...,表示捕获任何类型的异常
{
//处理任何异常的代码
}
/* 若程序运行期间出现异常,且未被捕获,则系统会自动调用terminate函数,使程序终止/中断 */
优点:C++异常处理机制使异常引发和异常处理不必在同一个函数中,则底层函数可关注解决具体问题,而无需过多考虑异常处理;由上层调用者在合适的位置,针对不同类型的异常设计合理的处理方法。
①函数的返回值可忽略,但异常不可忽略。若程序出现异常,且无任何地方捕获并处理,则系统会自动调用terminate函数
,使程序会终止/中断。
②整型返回值不包含任何语义信息,但异常可包含语义信息,具有见名知意的效果。
③整型返回值缺乏相关的上下文信息;异常类可拥有成员,通过类成员可传递充足的信息。
④异常处理可在调用跳级。当多个函数的调用栈均出现某个错误时,可利用异常处理的栈展开机制,只需在某一处进行异常处理,无需在每级函数均处理。
注:C语言中若程序出现异常,且无任何地方进行处理,则程序无影响;
C++中若程序出现异常,则必须在某处进行处理,若异常未被捕获,则程序会终止/中断。
示例:除数为0的异常案例
#include <iostream>
using namespace std;
//自定义异常类
class MyException {
public:
void printErrorInfo() {
cout << "自定义类型的异常" << endl;
}
};
int division(int a, int b) {
if (b == 0) {
/* 抛出异常,表示抛出的异常类型 */
//throw 1; //表示抛出int类型异常
//throw 3.14; //表示抛出double类型异常
//throw 'q'; //表示抛出char类型异常
//throw true; //表示抛出bool类型异常
//throw MyException(); //使用匿名对象,表示抛出自定义异常类的异常
//throw "const char*类型异常"; //表示抛出const char*类型异常
string ex = "字符串类型异常";
throw ex; //表示抛出字符串类型异常
}
return a / b;
}
void func() {
try {
division(5, 0);
}
catch (int) { //捕获int类型数据
cout << "int类型异常" << endl;
}
catch (double) { //捕获double类型数据
cout << "double类型异常" << endl;
}
catch (char) { //捕获char类型数据
cout << "char类型异常" << endl;
}
catch (bool) { //捕获bool类型数据
cout << "bool类型异常" << endl;
}
catch (const char*) { //捕获const char*类型数据
cout << "const char*类型异常" << endl;
}
catch (MyException e) { //捕获MyException类对象
//通过异常类对象调用成员函数
e.printErrorInfo();
}
catch (...) { //捕获任何类型的异常(本例中,可捕获string类型异常)
//不进行处理,继续抛出
throw; //本例中,继续抛出string类型异常
}
}
int main(){
try {
func(); //本例中,继续抛出string类型异常
}catch(string){
cout << "main函数中处理string类型异常..." << endl;
}
/* 若程序运行期间出现异常,且未被捕获,则系统会自动调用terminate函数,使程序会终止/中断 */
return 0;
}
1.3 异常严格类型匹配
C++异常机制与函数机制互不干涉,但异常的捕捉方式通过严格类型匹配。
例:
当throw
抛出字符串常量(const char*类型
)类型的异常时,如throw "error";
,则catch子句需严格使用catch(const char*)
捕获,而不能使用char*类型catch(char*)
或string类型catch(string)
捕获。
当throw
抛出字符串类型(string类型
)的异常时,如string err = "error";
、throw err;
,则catch子句需严格使用catch(string)
捕获,而不能使用const char*类型catch(const char*)
捕获。
字符串类型:
const char*类型
(C语言)与string类型
(C++)的关系
①const char*类型
可隐式转换为string类型
;string类型
不可隐式转换为const char*类型
。
②string类型
对象可调用成员函数c_str()
转换为const char*类型
。
函数声明:const char* string::c_str() const;
#include <iostream>
using namespace std;
int division(int a, int b) {
if (b == 0) {
/* 抛出异常,表示抛出的异常类型 */
//string ex = "error";
//throw ex; //表示抛出string字符串类型异常
//throw ex.c_str(); //表示抛出const char*类型异常
throw "error"; //表示抛出const char*类型异常
}
return a / b;
}
void func() {
try {
division(5, 0);
}
catch (char*) { //捕获char*类型数据
cout << "char*类型异常" << endl;
}
catch (const char*) { //捕获const char*类型数据
cout << "const char*类型异常" << endl;
}
catch (string) { //捕获string类型数据
cout << "string类型异常" << endl;
}
}
int main() {
func(); //"const char*类型异常"
return 0;
}
2 栈解旋(unwinding)
栈解旋:异常被抛出后,从进入try块开始,到异常被抛出(throw
)前,该段期间栈上创建的所有对象,均会被自动析构,且析构顺序与构造顺序相反【栈的特点:先进后出】。
注:智能指针:使用
类模板
,托管new操作符创建的堆区对象,避免堆区内存泄露。
C++98:auto_ptr<Object> 指针变量名(new Object);
。
C++11:unique_ptr<Object> 指针变量名(new Object);
,需包含头文件#include <memory>
。
示例:栈解旋
#include <iostream>
using namespace std;
#include <memory> //智能指针头文件
class Object {
public:
int index;
Object() {
cout << "Object默认无参构造" << endl;
}
Object(int idx) {
this->index = idx;
cout << "Object带参构造:" << this->index << endl;
}
~Object() {
cout << "Object析构函数:" << this->index << endl;
}
};
int main() {
try {
/* 栈解旋:异常被抛出后,从进入try块开始,到异常被抛出前,
栈上数据会被自动释放,且释放顺序与创建顺序相反 */
Object obj1(1);
Object obj2(2);
//使用智能指针托管堆区创建的对象,避免堆区内存泄露
//C++98智能指针
auto_ptr<Object> obj3(new Object(3));
//C++11智能指针
unique_ptr<Object> obj4(new Object(4));
throw 3.14;
}
catch (double) {
cout << "捕获double类型异常..." << endl;
}
return 0;
}
输出结果:
Object带参构造:1
Object带参构造:2
Object带参构造:3
Object带参构造:4
Object析构函数:4
Object析构函数:3
Object析构函数:2
Object析构函数:1
捕获double类型异常...
3 异常的接口声明【C++11已废弃】
使用场景:为加强程序的可读性,可在函数声明中显式列出可能抛出异常的全部类型。若某个类或函数只允许抛出指定类型的异常,可使用异常接口声明。
(1)抛出指定类型的异常:使用throw
关键字,并指定异常类型。
语法:void func() throw(T1, T2, T3);
该函数只允许抛出类型T1、T2、T3及其子类型的异常。
(2)不允许抛出任何类型的异常:使用throw
关键字,指定异常类型列表为空。
语法:void func() throw();
该函数不允许抛出任何类型的异常。
(3)允许抛出任何类型的异常:不使用throw
关键字。
语法:void func();
该函数可抛出任何类型的异常。
注1:若某个函数抛出其异常接口声明类型之外的其它类型异常,则系统会调用
unexcepted函数
,该函数内部会调用terminate函数
,使程序终止/中断。
注2:C++11已废弃dynamic exception specifications,建议使用noexcept
。
示例:异常接口声明(C++11已废弃,VS不支持,VS code暂支持)
#include <iostream>
using namespace std;
//异常的接口声明:只允许抛出特定类型的异常
void func() throw(int, double) {
//报错:terminate called after throwing an instance of 'char'
//throw 'c';
}
int main() {
try {
func();
}
catch (int) {
cout << "捕获int类型异常" << endl;
}
catch (double) {
cout << "捕获double类型异常" << endl;
}
catch (...) {
cout << "捕获其它类型异常" << endl;
}
return 0;
}
4 异常变量的生命周期
自定义异常类对象的生命周期(4种情况):
(1)以值方式抛出及捕获匿名异常对象【不建议】
抛出对象:throw MyException();
捕获对象:catch(MyException e)
特点:catch语句捕获抛出的匿名异常对象时,会调用拷贝构造函数创建匿名对象的拷贝副本,产生额外内存开销。
//1.以【值方式】抛出和捕获匿名异常对象:调用拷贝构造函数创建匿名对象的拷贝
/* 输出结果 */
//MyException类的无参构造函数
//MyException类的拷贝构造函数
//捕获自定义异常...
//MyException类的析构函数
//MyException类的析构函数
void func1() {
try {
//抛出匿名对象
throw MyException();
}
catch (MyException e) { //以值方式接收匿名对象
cout << "捕获自定义异常..." << endl;
}
}
(2)以地址值方式(指针类型)抛出及捕获栈区异常对象【不建议】
抛出对象:MyException me;
、 throw &me;
捕获对象:catch(MyException *e)
特点:异常对象抛出后立即被释放,在异常捕获前对象即不存在,对象指针指向已被释放的内存,若操作该内存则为非法操作。
注:无法抛出匿名对象的地址(
throw &MyException();
),编译器报错:&要求左值
(无法对匿名对象取地址)。
//2.以【地址值方式】抛出和捕获异常对象:对象抛出立即被释放,对象指针指向被释放的内存
/* 输出结果 */
//MyException类的无参构造函数
//MyException类的析构函数
//捕获自定义异常...
void func2() {
try {
//抛出对象的地址
MyException me;
throw &me;
//错误:抛出匿名对象的地址
//throw &MyException(); //报错:&要求左值(无法对匿名对象取地址)
}
catch (MyException *e) { //以地址值方式(指针)接收对象
cout << "捕获自定义异常..." << endl;
}
}
(3)以引用方式(起别名)抛出及捕获匿名异常对象【最建议】
抛出对象:throw MyException();
捕获对象:catch(MyException &e)
特点:匿名对象的生命周期延续至左值(引用),在程序结束后释放,即匿名对象不会立即释放。
优点:
①不会调用拷贝构造函数创建对象的拷贝,即不会产生额外内存开销;
②不会提前释放(匿名)异常对象;
③不需要手动释放堆区内存,即不会导致堆区内存泄露。
//3.以【引用方式】抛出和捕获匿名异常对象:匿名对象的生命周期延续至左值(引用)
/* 输出结果 */
//MyException类的无参构造函数
//捕获自定义异常...
//MyException类的析构函数
void func3() {
try {
//抛出匿名对象
throw MyException();
}
catch (MyException &e) { //以引用方式接收匿名对象
cout << "捕获自定义异常..." << endl;
}
}
(4)以地址值方式(指针类型)抛出及捕获堆区匿名异常对象【建议】
抛出对象:throw new MyException();
捕获对象:catch(MyException *e)
特点:堆区匿名对象不会立即释放,需使用delete
手动释放堆区内存,否则导致堆区内存泄露。效果同引用方式。
//4.以【地址值方式】(指针类型)抛出及捕获堆区匿名异常对象:堆区匿名对象需手动释放
/* 输出结果 */
//MyException类的无参构造函数
//捕获自定义异常...
void func4() {
try {
//抛出堆区匿名对象
throw new MyException();
}
catch (MyException *e) { //以地址值方式接收堆区匿名对象
cout << "捕获自定义异常..." << endl;
//delete e; //手动释放堆区内存
}
}
示例:异常对象的生命周期(4种情况)
#include <iostream>
using namespace std;
/* 自定义异常类 */
class MyException {
public:
//无参构造函数
MyException() {
cout << "MyException类的无参构造函数" << endl;
}
//拷贝构造函数
MyException(const MyException &me) {
cout << "MyException类的拷贝构造函数" << endl;
}
//析构函数
~MyException() {
cout << "MyException类的析构函数" << endl;
}
};
//1.以【值方式】抛出和捕获匿名异常对象:调用拷贝构造函数创建匿名对象的拷贝
/* 输出结果 */
//MyException类的无参构造函数
//MyException类的拷贝构造函数
//捕获自定义异常...
//MyException类的析构函数
//MyException类的析构函数
void func1() {
try {
//抛出匿名对象
throw MyException();
}
catch (MyException e) { //以值方式接收匿名对象
cout << "捕获自定义异常..." << endl;
}
}
//2.以【地址值方式】抛出和捕获异常对象:对象抛出立即被释放,对象指针指向被释放的内存
/* 输出结果 */
//MyException类的无参构造函数
//MyException类的析构函数
//捕获自定义异常...
void func2() {
try {
//抛出对象的地址
MyException me;
throw &me;
//错误:抛出匿名对象的地址
//throw &MyException(); //报错:&要求左值(无法对匿名对象取地址)
}
catch (MyException *e) { //以地址值方式接收匿名对象
cout << "捕获自定义异常..." << endl;
}
}
//3.以【引用方式】抛出和捕获匿名异常对象:匿名对象的生命周期延续至左值(引用)
/* 输出结果 */
//MyException类的无参构造函数
//捕获自定义异常...
//MyException类的析构函数
void func3() {
try {
//抛出匿名对象
throw MyException();
}
catch (MyException &e) { //以引用方式接收匿名对象
cout << "捕获自定义异常..." << endl;
}
}
//4.以【地址值方式】(指针类型)抛出及捕获堆区匿名异常对象:堆区匿名对象需手动释放
/* 输出结果 */
//MyException类的无参构造函数
//捕获自定义异常...
void func4() {
try {
//抛出堆区匿名对象
throw new MyException();
}
catch (MyException *e) { //以地址值方式接收堆区匿名对象
cout << "捕获自定义异常..." << endl;
//delete e; //手动释放堆区内存
}
}
int main() {
//func1();
//func2();
//func3();
func4();
}
5 异常的多态使用
异常的多态使用:catch语句中,使用基类的引用类型捕获子类异常对象。
示例:
#include <iostream>
using namespace std;
/* 异常的基类 */
class BaseException {
public:
//纯虚函数或虚函数
virtual void printErrorInfo() = 0;
};
//异常的子类1:空指针异常
class NullPointerException : public BaseException{
public:
//重写纯虚函数或虚函数
virtual void printErrorInfo() {
cout << "空指针异常..." << endl;
}
};
//异常的子类2:索引越界
class IndexOutOfRangeException : public BaseException {
public:
//重写纯虚函数或虚函数
virtual void printErrorInfo() {
cout << "索引越界异常..." << endl;
}
};
void main() {
try {
throw NullPointerException();
//throw IndexOutOfRangeException();
}
catch (BaseException& e) { //多态使用:基类的引用类型捕获子类异常对象
e.printErrorInfo();
}
}
6 C++标准异常库
C++标准异常库中,异常的根基类为exception类
,不同异常子类需包含不同头文件。
通过异常类的成员函数const char* exception::what()
可获取字符串标识异常。
C++异常类exception的继承层次及对应头文件:
示例:
#include<iostream>
using namespace std;
#include<stdexcept> //包含头文件
class Person {
public:
int age;
Person(int age) {
//成员属性的有效性校验
if (age < 0 || age > 120) {
//std::out_of_range(const char*);
throw out_of_range("年龄值无效(0~120)");
}
else {
this->age = age;
}
}
};
void main() {
try {
Person p(150);
}
catch (exception& e) { //多态:使用父类引用接收子类异常对象
//const char* exception::what() 获取字符串标识异常
cout << e.what() << endl; //年龄值无效(0~120)
}
}
7 练习:自定义异常类
案例练习:
(1)自定义异常类,继承自exception基类
(2)使用多态,父类引用捕获子类对象
(3)string类型与const char*类型的互相转换
字符串类型:
const char*类型
(C语言)与string类型
(C++)的关系
①const char*类型
可隐式转换为string类型
;string类型
不可隐式转换为const char*类型
。
②string类型
对象可调用成员函数c_str()
转换为const char*类型
。
函数声明:const char* string::c_str() const;
#include <iostream>
using namespace std;
#include <stdexcept>
class MyOutOfRange : public exception {
/*
//基类exception的构造函数
exception();
explicit exception(char const* const _Message);
exception(char const* const _Message, int);
exception(exception const& _Other);
exception& operator=(exception const& _Other);
//虚析构或纯虚析构:多态调用时,父类指针释放时默认不会调用子类析构函数
virtual ~exception();
//虚成员函数
const char* what() const;
//私有成员属性
const char* _Mywhat;
*/
public:
string errInfo; //记录错误提示信息
//带参构造函数
MyOutOfRange(const char* err) {
//const char*可隐式转换为string
this->errInfo = err;
}
//带参构造函数-重载
MyOutOfRange(const string& err) {
this->errInfo = err;
}
//重写父类虚成员函数
virtual const char* what() const {
//string不可隐式转换为const char*
//string类型对象可调用成员函数c_str()转换为const char*类型
//const char* string::c_str() const
return this->errInfo.c_str();
}
};
class Person {
public:
int age;
Person(int age) {
//成员属性的有效性校验
if (age < 0 || age > 120) {
//std::out_of_range(const char*);
//throw out_of_range("年龄值无效(0~120)");
//const char*类型参数
//throw MyOutOfRange("const char*型参数:年龄值无效(0~120)");
//string类型参数
string errStr = "string型参数:年龄值无效(0~120)";
throw MyOutOfRange(errStr);
}
else {
this->age = age;
}
}
};
void main() {
try {
Person p(150);
}
catch (exception& e) { //多态:使用父类引用接收子类异常对象
//const char* exception::what() 获取字符串标识异常
cout << e.what() << endl; //年龄值无效(0~120)
}
}