异常c/c++

目录

1.c语言传统处理错误方式

1、终止程序

2、返回错误码

2.c++异常概念

3.异常的使用

3.1异常的抛出与捕获

3.2异常安全(还有一些异常重新抛出)

3.3异常规范

4.自定义异常体系

5.c++标准库的异常体系

6.异常优缺点

1、优点

2、缺点

7、补充


1.c语言传统处理错误方式

1、终止程序

如assert断言、内存错误(原因有很多,越界访问啊等等)、除0错误

2、返回错误码

错误码,常见的就是程序运行后,如果没有错误的话,就是返回0,不正常的时候返回负的多少或者其他奇怪的数字。很多库的接口函数都把错误码放到errno中,表示错误。遇到这种,错误的原因需要查表或者自行寻找到错误原因。

2.c++异常概念

异常在很多面向对象的语言都有,python也有。

异常是一种处理错误的方式,可以理解为程度比assert轻一点,处理方式不是粗暴的终止程序。当出现某个编译器无法处理的错误时,可以将其作为异常抛出,由程序员预先设置的程序来处理这个错误。

**throw:**抛出异常,"throw 字符串或数字"

**catch:**捕获异常,比如捕获到了字符串或数字,就会进入对应的块域中,执行设定的语句。可以多个catch捕获多个异常(比如针对字符串的、针对数字的)

**try:**在try里面的语句,都会在运行时进行检查,如果遇到异常,把异常抛出后,会马上跳到catch中。

cpp 复制代码
try
{
  // 保护的标识代码
}catch( ExceptionName e1 )
{
  // catch 块
}catch( ExceptionName e2 )
{
  // catch 块
}catch( ExceptionName eN )
{
  // catch 块
}
cpp 复制代码
void fa() {
	int a; cin >> a;
	if (a == 2)
	{
		throw "wdawd";
	}
	else if (a == 3)
	{
		throw 4;
	}
	else if (a == 4)
	{
		string s("dwwdawda");
		throw s;
	}
	else if (a == 5)
	{
		stack<int>s;
		s.push(2);
		throw s;
	}
}

int main()
{
	try {
		fa();
	}
	catch (const char* err)
	{
		cout << err << endl;
	}
	catch (int x)
	{
		cout << x << endl;
	}
	catch (const string& es)
	{
		cout << es << endl;
	}
	catch (...) {
		cout << 231231 << endl;
	}
	return 0;
}

如果没有遇到异常,在执行完try里面的语句后,会直接跳过catch里面的内容。

注意,如果是库里的对象返回的话,建议用const 类型&来接受,因为这样如果这个对象有移动拷贝的话,这个写法可以接受右值也可以接受左值,提高效率。

3.异常的使用

3.1异常的抛出与捕获

相比c语言传统的错误码(遇到错误,要层层返回,每层都要处理,直到返回main函数),异常可以直接跳回到匹配的catch处。这个过程中,跳过的函数都是会正常销毁的

异常抛出和匹配原则

1、异常是通过抛出对象引出的,该对象的类型决定了应该激活哪个catch

2、被选中的处理代码是调用链 (就是从main到第一个函数到其内的函数再到其内的函数,一路下去)中**与对象类型匹配且距离抛出异常的位置最近的,**比如在第一个函数处也有try,然后执行try里面的内容时抛了异常,并且catch的内容与抛出的对象的类型是匹配的,那就会执行第一个函数处的对应catch而不是main函数处的catch。

3、catch(...)可以捕获任意类型的异常,问题是不知道异常是什么 用来兜底的,建议一定加

4、抛出异常后,对于相应的对象,会生成该对象的拷贝 ,因为这个对象可能是个临时对象 (比如局部变量),这个拷贝出来的对象,在catch之后会销毁。(效率还是很高的,因为临时对象的话,走移动拷贝,效率挺高的)。比如我们抛出了一个string对象,就上面的代码中一样。

5、类型不一定严格匹配,可以抛出派生类对象,让基类捕获,利用多态。具体看下面的异常体系

在函数调用链中异常栈展开匹配原则

1、throw抛出后,要确认自己是否在try里面 ,在的话才去找匹配的catch,然后跳到匹配的catch处

2、没有匹配的catch,就跳出当前的函数栈 ,在调用该函数的函数栈中继续找匹配的catch

3、如果在main函数中都没有找到 匹配的catch就会终止程序 。而整个沿着调用链向上找匹配的catch的过程,就是栈展开。所以实际中,我们都会加个catch(...),否则有异常没捕获就会直接终止程序。

4、在匹配的catch处执行完对应的语句后,会沿着当前的try ....catch之后的语句继续执行

3.2异常安全(还有一些异常重新抛出)

异常安全的核心问题就是在调用链非常长的情况下,异常会直接跳到catch,而这个过程很可能横跨了很多个函数。

1、不要在构造函数里抛异常,因为抛异常会直接跳到catch。如果构造函数里面要进行初始化,最典型的就是开辟多段空间给多个指针变量,如果这个过程中抛异常了,导致有些指针变量没有被初始化,仍然是野指针或空指针,那么析构函数在delete或者free的时候就会抛异常或者报错。简单点就是说可能会造成对象不完整或者没有完全初始化

2、析构函数不要抛异常。因为析构函数负责资源的清理,如果在析构函数中抛异常,很可能导致内存泄漏等问题,比如某个空间没有被delete掉。

3、异常经常会导致内存泄漏的问题。比如**new和delete中间有一块抛出了异常,**导致直接跳到catch了,delete就不会执行,这样就内存泄漏了,还会导致lock和unlock之间的死锁问题(这个是线程的知识,看我linux和网络的部分即可)

接下来是一种处理方法,利用异常重新抛出

cpp 复制代码
void fa() {
	int a; cin >> a;
	if (a == 2)
	{
		throw "wdawd";
	}
	else if (a == 3)
	{
		throw 4;
	}
	else if (a == 4)
	{
		string s("dwwdawda");
		throw s;
	}
	else if (a == 5)
	{
		stack<int>s;
		s.push(2);
		throw s;
	}
}
void f() {
	int* x = new int;
	try {
		fa();
	}
	catch (...) {//不一定是...,只是最保险的就是这个,不管抛了什么异常都可以接受
		delete x;
		throw;//不管接受到什么异常,都通通重新抛出去。
	}
}
int main()
{
	try {
		f();
	}
	catch (const char* err)
	{
		cout << err << endl;
	}
	catch (int x)
	{
		cout << x << endl;
	}
	catch (const string& es)
	{
		cout << es << endl;
	}
	catch (...) {
		cout << 231231 << endl;
	}
	return 0;
}

核心思路就是在关键位置提前捕获,然后再重新抛出

比较恶心的情况:

cpp 复制代码
void f() {
	int* x = new int;
    int *x1,x2;
	try {
		 x1 = new int;
		try {
			 x2 = new int;
		}
		catch (...) {
			delete x1;
			delete x;
			throw;
		}
	}
	catch (...) {
		delete x;
		throw;
	}
    delete x;
    delete x1;
    delete x2;
}

这个方法写起来太麻烦了,可见这个方法也不是很好,依靠智能指针可以更加的高效处理这个问题

cpp 复制代码
class SmartPtr {
public:
	SmartPtr(int *ptr)
		:_ptr(ptr)
	{}
	~SmartPtr() {
		delete _ptr;
	}
private:
	int* _ptr;
};
void f() {
	SmartPtr s1(new int);
	SmartPtr s2(new int);
	SmartPtr s3(new int);
	//这样就可以依靠析构函数自动清理资源了。
	//就算抛了异常,s1、s2、s3是局部变量,在出了作用域会自动销毁,调用它的析构函数
	
}

3.3异常规范

出于上面的情况,98的时候,希望大家写异常的时候规范一些。

cpp 复制代码
// 这里表示这个函数会抛出A/B/C/D中的某种类型的异常
void fun() throw(A,B,C,D);
// 这里表示这个函数只会抛出A的异常
void* operator new (std::size_t size) throw (A);
// 这里表示这个函数不会抛出异常
void* operator delete (std::size_t size, void* ptr) throw();

单单靠上面3个,还是会出问题,比如写了抛abc,但实际上可能抛了d。
又比如函数深层调用,一层调一层那岂不是每一层函数都要写好多的throw()。
而且因为考虑到兼容c语言的问题,这只能是个建议而不是规定。


// C++11 中新增的noexcept,表示不会抛异常
thread() noexcept;
thread (thread&& x) noexcept;

//通过这个简化,只标记不会抛异常的函数。
但要小心的是,间接层加了noexcept,但这层函数调用的函数抛了异常,是不会检测到的。

而且一旦检测到抛出了规定外的异常,编译器也只是报错。

4.自定义异常体系

在实际过程中,大家都是设计一个异常体系,否则的下不同组各自管自己抛异常,最后害惨了外面管异常的,因为catch(...)是不确定究竟是什么异常,所以通过设计异常体系,让异常的信息更加的准确,方便处理。

接下来是一个比较常见的异常体系

cpp 复制代码
class Exception
{
public:
	Exception(const string& errmsg, int id)
		:_errmsg(errmsg)
		, _id(id)
	{}
	virtual string what() const
	{
		return _errmsg;   //返回错误描述  
	}
protected:
	string _errmsg;//错误描述,比如基础的错误信息,比如内存错误等等
	int _id; //错误id
};


//这里会有一个类,比如说是数据库开发组的。
class SqlException : public Exception
{
public:
	SqlException(const string& errmsg, int id, const string& sql)
		:Exception(errmsg, id)
		, _sql(sql)
	{}
	virtual string what() const
	{
		string str = "SqlException:";//表明自己是什么错误类的
		str += _errmsg;
		str += "->";
		str += _sql;
		return str;
        //再基础的错误信息基础上,加上自己的额外备注信息,让错误描述更加的准确
	}
private:
	const string _sql;
};

//缓存组,具体做什么因人而异,主要是不同的组都会加上一串字符串,表示这个异常是
//哪个组负责的
class CacheException : public Exception
{
public:
	CacheException(const string& errmsg, int id)
		:Exception(errmsg, id)
	{}
	virtual string what() const
	{
		string str = "CacheException:";
		str += _errmsg;
		return str;
	}
};

//网络组的,记录请求类型等等的
class HttpServerException : public Exception
{
public:
	HttpServerException(const string& errmsg, int id, const string& type)
		:Exception(errmsg, id)
		, _type(type)
	{}
	virtual string what() const
	{
		string str = "HttpServerException:";
		str += _type;
		str += ":";
		str += _errmsg;
		return str;
	}
private:
	const string _type;
};

那么如何捕获呢?

cpp 复制代码
catch (const Exception& e) // 这里捕获父类对象就可以
 {
 // 多态
 cout << e.what() << endl;
 }

利用继承的特性,用父类对象的引用来接受派生类或者父类本身。

然后利用虚函数的特性,在满足多态的情况下,如果接受的派生类,那么what调用的就是派生类的what,如果接受的是父类,就会调用父类的what。

5.c++标准库的异常体系

c++标准库也是弄了一个异常体系。

图片出自菜鸟编程。

6.异常优缺点

1、优点

1、相比传统的错误码,异常可以更加清晰的展现出错误的信息,甚至包含堆栈的调用信息,这样可以帮助更好的定位bug

2、错误码必须通过层层的retrun返回错误码,然后才能拿到错误码,相比之下异常可以直接拿到信息,跳过中间的过程。

3、很多第三方的库都包含了异常,使用的时候也得关注异常。

4、部分情况用异常更好,比如构造函数没有返回值,只能抛异常。比如at这个函数,返回什么很重要,但问题是返回什么都可能不是错误信息,反而可能是一个正常的值,但确实下标越界了。

2、缺点

1、异常会导致程序的执行流乱跳,会非常混乱,尤其是运行时的时候抛异常就会乱跳。导致调试跟踪以及分析程序的时候会非常困难。

2、异常有一定的性能开销,不过在现在的硬件下,基本可以忽略不计。

3、c++没有垃圾回收机制,资源需要自己管理,容易造成内存泄漏、死锁等异常安全问题,想要解决,建议是rall(智能指针的内容)。

4、标准库的异常体系定义的一般,导致所有人都是各自定义各自的,比较混乱。

5、异常需要尽量的规范使用,否则负责捕获异常处理异常的人会很痛苦。主要是,所有的异常继承自一个基类;函数是否抛异常、抛什么异常,都使用func() throw()的形式规范

7、补充

异常一旦发生,说明程序出现了非法的情况

程序中只要有异常,就必须处理。一旦有抛出异常,那么必须捕获,否则代码最后会崩溃。

基类的const类型引用,可以捕获所有的子类的异常对象

相关推荐
七七七七071 小时前
浅谈C++/C命名冲突
c语言·c++
c-c-developer1 小时前
C++ Primer 特定容器算法
c++
宇寒风暖2 小时前
侯捷 C++ 课程学习笔记:Spaces in Template Expression、 nullptr and stdnull
开发语言·c++·笔记·学习
明月看潮生3 小时前
青少年编程与数学 02-010 C++程序设计基础 13课题、数据类型
开发语言·c++·青少年编程·编程与数学
Awkwardx3 小时前
C++初阶—list类
开发语言·c++
2501_902556233 小时前
C++ 中 cin 和 cout 教程
数据结构·c++
萌の鱼4 小时前
leetcode 73. 矩阵置零
数据结构·c++·算法·leetcode·矩阵
Duramentee4 小时前
C++ 设计模式 十九:观察者模式 (读书 现代c++设计模式)
c++·观察者模式·设计模式
了不起的杰5 小时前
【c++语法基础】c/c++内存管理
java·c语言·c++
Chasing追~5 小时前
SQLite数据库从0到1
数据库·c++·qt·sqlite