一篇文章带你了解C++中的异常

异常 在c语言中,对错误的处理总是两种方法: 1,使用整型的返回值表示错误(有时候用1表示正确,0表示错误;有的时候0表示正确,1表示错误) 2,使用errno

异常

在c语言中,对错误的处理总是两种方法:

1,使用整型的返回值表示错误(有时候用1表示正确,0表示错误;有的时候0表示正确,1表示错误)

2,使用errno宏(可以简单理解为一个全局整形变量)去记录错误。(如果错误,就将被改变的全局整形变量返回)

c++中仍然可以用上面的两种方法,但是有缺点。

(1)返回值不统一,到底是1表示正确,还是0表示正确。

(2)返回值只有一个,通过函数的返回值表示错误代码,那么函数就不能返回其他的值。(输出的值到底是表示异常的值-1,还是最后在那个结果就是-1呢)

抛出异常基本操作

c++处理异常的优点:

异常处理可以带调用跳级。

img

在C程序中出现了异常,返回值为-1。如果C直接将-1传给B,不进行处理(也不给B报错),那么B收到-1返回值以后就会进行自己的处理,然后返回给A,然后A再进行自己的处理,那么最终程序返回的值肯定是错误的。

所以在c++中,要求必须要处理异常。如果C处理不了允许抛给B处理,B处理不了也允许抛给A处理,如果A也处理不了,那么就直接终止代码报错。

int myDivision(int a, int b)
{
	if (b == 0)
	{
		throw -1;//抛出-1
	}
	else
		return 1;
}
int main()
{
	int a = 10;
	int b = 0;
	try 
	{
		myDivision(a, b);
	}
	catch (int)
	{
		cout << "int类型异常捕获" << endl;
	}
	return 0;
}

如果抛出来的是char类型的数据(异常),那么就需要有个char类型的接收处理代码(catch+类型)。

除了int,char,double以外的抛出类型,可以用...来接收。

catch (...)
	{
		cout << "其他类型异常捕获" << endl;
	}

如果捕获到了异常,但是不想处理,那么可以继续向上抛出异常。

int myDivision(int a, int b)
{
	if (b == 0)
	{
		throw -1;
	}
}
void test()
{
	int a = 10;
	int b = 0;
	try
	{
		myDivision(a, b);
	}
	catch (int)
	{
		throw;
	}
}
int main()
{
	try 
	{
		test();
	}
	catch (int)
	{
		cout << "int类型异常捕获" << endl;
	}
	return 0;
}  

自定义的异常类

注意:类名加()就是匿名对象

class MyException
{
public:
	void printError()
	{
		cout << "我自己的异常" << endl;
	}
};
int myDivision(int a, int b)
{
	if (b == 0)
	{
		throw  MyException();//类名加()就是匿名对象,抛出的就是匿名对象。
	}
}
int main()
{
	int a = 10;
	int b = 0;
	try
	{
		myDivision(a, b);
	}
	catch (MyException e)
	{
		e.printError();//可以直接用这个对象来调用成员函数
	}
	return 0;
}

总结:

1,c++中如果出现异常,不像c中return -1,而是直接throw -1,然后后面再用try catch进行处理。

2,可能出现异常的地方使用try

3,如果与抛出的异常匹配的处理没有找到,那么运行函数terminate将被自动调用,其缺省功能调用abort终止程序。

栈解旋

从try代码行开始 到 throw将代码抛出去之前。所有栈上的数据会被自动的释放掉。

释放的顺序和创建的顺序是相反的。(栈:先进后出)

class Person
{
public:
	Person()
	{
		cout << "Person的默认构造调用" << endl;
	}
	~Person()
	{
		cout << "Person的析构调用" << endl;
	}
};
int myDivision(int a, int b)
{
	if (b == 0)
	{
		Person p1;
		Person p2;
		throw  Person();//匿名对象
	}
}
int main()
{
	int a = 10;
	int b = 0;
	try
	{
		myDivision(a, b);
	}
	catch (Person)
	{
		cout << "拿到Person类异常,正在处理" << endl;
	}
	return 0;
}

输出结果:

Person的默认构造调用
Person的默认构造调用
Person的默认构造调用
Person的析构调用
Person的析构调用
拿到Person类异常,正在处理
Person的析构调用

在throw之前创建了两个对象,并抛出一个匿名对象。发现在抛出去之前,两个对象就被释放了。然后抛出去的对象在程序结束时候释放。这就是栈解旋

异常接口声明

只允许抛出规定类型的异常。

//异常接口的声明
void func() throw(int , double)//只允许抛出int和double类型的异常。
{
	throw 3.14;
}
int main()
{
	try
	{
		func();
	}
	catch (int)
	{
		cout << "int类型异常捕获" << endl;
	}
	catch (...)
	{
		cout << "其他类型异常捕获" << endl;
	}
	return 0;
}

throw()的意思就是不允许抛出异常。

这个代码在VS中是不能正确执行的,都不会报错。但是在QT和linux下是可以正确执行的。

异常变量的生命周期

class MyException
{
public:
	MyException()
	{
		cout << "MyException的默认构造调用" << endl;
	}
	MyException(const MyException&e)
	{
		cout << "MyException的拷贝构造调用" << endl;
	}
	~MyException()
	{
		cout << "MyException的析构调用" << endl;
	}
};
void doWork()
{
	throw MyException();//抛出匿名对象
}
int main()
{
	try
	{
		doWork();
	}
	catch (MyException e)
	{
		cout << "自定义异常的捕获" << endl;	
	}
	return 0;
}

运行的结果:

MyException的默认构造调用
MyException的拷贝构造调用
自定义异常的捕获
MyException的析构调用
MyException的析构调用

throw匿名对象的时候创建了对象,所以用默认构造。

用MyException e来接收对象的时候,是用的值来接收的,所以会调用拷贝构造函数。

然后就打印,并且将两个对象删除掉。

这样效率不高,如果接收对象的时候不用值来接收,而是用引用来接收,这样就能少调用一次的拷贝构造和一次析构函数。

catch (MyException &e)
	{
		cout << "自定义异常的捕获" << endl;	
	}

运行结果:

MyException的默认构造调用
自定义异常的捕获
MyException的析构调用

还有一种方式,就是将匿名函数的地址穿进来,这样也不需要调用析构函数。

class MyException
{
public:
	MyException()
	{
		cout << "MyException的默认构造调用" << endl;
	}
	MyException(const MyException&e)
	{
		cout << "MyException的拷贝构造调用" << endl;
	}
	~MyException()
	{
		cout << "MyException的析构调用" << endl;
	}
};
void doWork()
{
	throw & MyException();//抛出匿名对象
}
int main()
{
	try
	{
		doWork();
	}
	catch (MyException *e)
	{
		cout << "自定义异常的捕获" << endl;	
	}
	return 0;
}

运行结果:(其实没有运行成功)

MyException的默认构造调用
MyException的析构调用
自定义异常的捕获

如果传的是指针,那么匿名对象很快就会释放掉(匿名对象的特点就是执行完就释放掉),最终得到了指针也没有办法进行操作。

但如果匿名对象在=的右边,且左边还给这个对象起名了(如同上面的传对象,引用接收),那么匿名对象的寿命就会延续到左边的变量上。如果传的是指针,给指针起名和给对象起名不一样,所以就会释放。

如果不想被释放掉,还有一种方式,那就是将这个对象创建在堆区,等待着程序员自己去释放。(不会调用析构)

void doWork()
{
	throw new MyException();//抛出匿名对象
}

异常的多态

//异常的基类
class BaseException
{
public:
	virtual void printError() = 0;//纯虚函数
};
//空指针异常
class NULLPointerException:public BaseException
{
public:
	virtual void printError()
	{
		cout << "空指针异常" << endl;
	}
};
//越界异常
class outOfRangeException :public BaseException
{
public:
	virtual void printError()
	{
		cout << "越界异常" << endl;
	}
};
void doWork()
{
	//throw NULLPointerException();
	throw outOfRangeException();
}
int main()
{
	try
	{
		doWork();
	}
	catch (BaseException &e)//用父类的引用接收子类的对象
	{
		e.printError();
	}
	return 0;
}

提供一个基类的异常类,其中有个纯虚函数(有可能是虚函数),然后子类重写。

调用的时候,用父类的引用来接收子类的对象就可以,这样就实现了异常的多态。抛出的是什么类的对象,那么就会调用什么类的函数。

c++的标准异常库

img

标准库中提供了很多的异常类,它们是通过类继承组织起来的。

如果使用系统提供的标准异常的时候,需要调用规定的头文件

#include <stdexcept>std:标准 except:异常

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
#include<string>
#include<stdexcept>
class Person
{
public:
	Person(int age)
	{
		if (age < 0 || age>150)
		{
			throw out_of_range("年龄必须在0-150之间");
		}
	}
	int m_age;
};
int main()
{
	try
	{
		Person p(151);
	}
	catch (out_of_range&e)
	{
		cout << e.what() << endl;//what函数是获得字符串中的内容
	}
	return 0;
	//如果使用多态:(异常子类的名字太难记,不好写)
	//catch (exception &e)
}

自己平时不会主动调用系统的标准异常。在写的系统提供的异常后面加()的字符串然后在接收的时候用父类的引用接收,然后用这个引用e的what函数就可以找到这个字符串。

编写自己的异常类

标准异常类是优先的,可以自己编写异常类。

和上面自己写的MyException不太一样。给系统提供的派生类exception提供儿子(需要重写父类的函数等)

ps:在非静态成员函数后面加const,表示成员函数隐含传入的this指针为const指针,决定了在该成员函数中,任意修改它所在的类的成员操作是不允许的。

经过考察上面的有关out_of_range的代码可得:抛出的是out_of_range类的一个对象,接收的时候也是用引用e来接收的这个对象。然后这个引用可以调用what()的函数来返回一个字符串,这个字符串正好是创建out_of_range对象的时候待用有参函数要传入的 字符串。

所以,自己写的out_of_range类一定要有个有参构造,参数就是字符串,然后还有个what的重写函数,需要返回这个字符串,这个字符串作为属性。

ps:注意:const char*可以隐式转换为string,但是反过来就不成立。

所以如果要使得string转换成const char*,需要调用string中的成员函数函数.c_str()

const char* what() const
{
    string s;
    return s.c_str();
    //返回的就是const char*了。
}

完整代码:

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
#include<string>
#include<stdexcept>
class MyOutOfRangeException:public exception//先继承一下这个父亲
{
	//到底要重写什么呢?点开exception以后,发现有两个virtual的虚函数,一个析构,还有一个what,析构不需要重写
	//所以需要重写what函数。
public:
	MyOutOfRangeException(const char* str)
	{
		//const char*可以隐式类型转换为string 反之不可以
		this->m_myerrorString = str;
	}
	//可以再重载一下这个函数,使得接收的参数改为string类型
	MyOutOfRangeException(string str)
	{
		this->m_myerrorString = str;
	}
	virtual char const* what() const
	{
		return m_myerrorString.c_str();//加了.c_str就可以返回const char*了
	}
	string m_myerrorString;//字符串属性
};
class Person
{
public:
	Person(int age)
	{
		if (age < 0 || age>150)
		{
			throw MyOutOfRangeException("年龄必须在0-150之间");//const char*
			throw MyOutOfRangeException(string("年龄必须在0-150之间"));//string,返回的是string类的匿名对象
		}
		else
		{
			this->m_age = age;
		}
	}
	int m_age;
};
int main()
{
	try
	{
		Person p(1000); 
	}
	catch (MyOutOfRangeException e)//用exception也可以,证明创建的这个类确实是exception的子类。
	{
		cout << e.what() << endl;
	}
	return 0;
}

但是最后发现好像没有成功的将MyOutOfRangeException手写异常类变成exception的子类,不知道为啥。

总结

本篇文章就到这里了,希望能够给你带来帮助,也希望您能够多多关注好代码网的更多内容!                

标签: C++ 异常