C++异常(异常的基本语法、栈解旋unwinding、异常接口声明、异常变量的生命周期、异常的多态使用、C++系统标准异常库)


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++98auto_ptr<Object> 指针变量名(new Object);
C++11unique_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的继承层次及对应头文件
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)
    }
}
  • 5
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
C++ 中抛出异常和处理异常的执行过程大致如下: 1. 当程序执行到 throw 语句时,会创建一个异常对象,该对象包含了异常的信息,然后将这个异常对象抛出(throw)到调用栈上,直到找到一个能够处理这个异常的 catch 语句。 2. 如果找不到任何能够处理这个异常的 catch 语句,则程序会调用 std::terminate() 函数,这个函数会终止程序的执行,并将程序的状态报告给操作系统。 3. 如果找到了一个能够处理异常的 catch 语句,程序会跳转到这个 catch 语句,并执行其中的代码。在执行 catch 语句之前,会先执行一些清理工作,这个过程叫做栈展开(stack unwinding)。 4. catch 语句可以处理多种类型的异常,如果没有指定异常类型,则可以处理任何类型的异常。如果指定了异常类型,则只能处理这个类型或者这个类型的子类型的异常。 5. 如果在 catch 语句中没有重新抛出异常,则程序会继续执行 catch 语句后面的代码,并继续执行程序的剩余部分。 6. 如果在 catch 语句中重新抛出了异常,则程序会继续在调用栈上查找其他的 catch 语句,直到找到一个能够处理这个异常的 catch 语句或者没有任何能够处理这个异常的 catch 语句为止。 需要注意的是,异常处理的过程可能会影响程序的性能,因为在栈展开的过程中,需要执行一些清理工作,例如调用对象的析构函数等。另外,如果异常处理不当,可能会引起一些不可预料的问题,例如内存泄漏等。因此,在编写程序时,应该合理使用异常,并遵循一些良好的编程习惯,例如不要在析构函数中抛出异常等。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值