C++异常

回忆C语言尝试用的错误处理机制:

  1. 终止程序,如assert。
    翻阅我以往C语言所编写的代码可以发现,我也常用这种方式来处理异常。当然这缺点也是明显:程序一旦接收到错误就要中止,并且只在Debug上有效。这自然是难以让人接受的
  2. 返回错误码。
    这种间接迂回的方式,还需要接受错误码,查询错误内容也是较为麻烦的。

对此C++改进了处理错误的方式,提出了异常的概念。

异常概念

异常是一种处理错误的方式,当一个函数出现了无法处理的错误就抛出异常,让函数直接或间接的调用者处理这个错误。

  • throw: 当问题出现时,程序会抛出一个异常。这是通过使用 throw 关键字来完成的。
  • catch: 在您想要处理问题的地方,通过异常处理程序捕获异常.catch 关键字用于捕获异常,可以有多个catch进行捕获。
  • try: try 块中的代码标识将被激活的特定异常,它后面通常跟着一个或多个catch 块。

也就是我们把可能出现异常的代码放进try块中,基本使用语法如下:

cpp 复制代码
try
 {
 // 保护的标识代码
}catch( ExceptionName e1 )
 {
 // catch 块
}catch( ExceptionName e2 )
 {
 // catch 块
}catch( ExceptionName eN )
 {
 // catch 块
}

异常的使用

异常的抛出和匹配遵循下面几条规则。

catch块匹配

1.首先异常是由抛出对象引发的,该对象的异常类型决定了应该激活哪个catch处理代码。

来看下下列演示代码:

cpp 复制代码
double Divide(const int& x, const int& y)
{
	if (y == 0)
		throw "Divided by 0!";
	else if (x == y)
		throw string("Nothing,just testing something.");
	else
		return (double)x / (double)y;
}

void Func()
{
	int x, y;
	cin >> x >> y;
	cout << Divide(x, y) << endl;
}

int main()
{
	while (1)
	{
		try
		{
			Func();
		}
		catch (const char* pch)
		{
			cout << "The First case:" << pch << endl;
		}
		catch (string str)
		{
			cout << "The Second case:" << str << endl;
		}
	}

	return 0;
}
复制代码
1 0
The First case:Divided by 0!
0 0
The First case:Divided by 0!
1 1
The Second case:Nothing,just testing something.
1 3
0.333333

可以看到根据throw不同的类型,执行不同的catch语块。

2.catch(...)可以捕获任意类型的异常,问题是不知道异常错误是什么。

当然了,有时候抛出异常类型太多了,如果每个类型都写一个catch语句将会很麻烦,因此我们需要一个接受全部类型的catch语句,即catch(...):

cpp 复制代码
while (1)
{
	try
	{
		Func();
	}
	catch (...)
	{
		cout << "I am here!" << endl;
	}
}
复制代码
1 0
I am here!
0 0
I am here!
1 1
I am here!
1 3
0.333333

3.被选中的处理代码是调用链中与该对象类型匹配且离抛出异常位置最近的那一个。

修改下Func和main函数,且看下面演示代码:

cpp 复制代码
void Func()
{
	int x, y;
	cin >> x >> y;
	try
	{
		cout << Divide(x, y) << endl;
	}
	catch (...)
	{
		cout << "I am Func();" << endl;
	}
}

int main()
{
	while (1)
	{
		try
		{
			Func();
		}
		catch (...)
		{
			cout << "I am main();" << endl;
		}
	}

	return 0;
}
复制代码
1 0
I am Func();
0 0
I am Func();
1 1
I am Func();
1 3
0.333333

可以看到我们的异常全部被Func函数接受了,而main函数一个都没接收到。

4.抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个临时对象,所以会生成一个拷贝对象,这个拷贝的临时对象会在被catch以后销毁。

正如一开始抛出string类型的代码,这时会拷贝一个string临时对象返回。聪明的你一定想到了,这个传值返回的拷贝调用的是移动拷贝,这样就大大减了传值返回开销。

catch中基类对象匹配派生类对象

5.实际中抛出和捕获的匹配原则有个例外,并不都是类型完全匹配,可以抛出的派生类对象,使用基类捕获。

正如前面所说,抛出类型繁复,不可能每个类型都写一个catch语块,但使用catch(...)又不知道异常具体类型。这时候就可以写一个异常基类接受派生类对象,不仅只需要写一个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;
};

然后构建不同派生类

数据库异常

cpp 复制代码
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;
};

缓存异常

cpp 复制代码
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;
	}
};

网络异常

cpp 复制代码
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 复制代码
void SQLMgr()
{
	srand(time(0));
	if (rand() % 7 == 0)
	{
		throw SqlException("权限不足", 100, "select * from name = '张三'");
	}
	else
	{
		cout << "执行Sql成功" << endl;
	}
}

void CacheMgr()
{
	srand(time(0));
	if (rand() % 5 == 0)
	{
		throw CacheException("权限不足", 100);
	}
	else if (rand() % 6 == 0)
	{
		throw CacheException("数据不存在", 101);
	}
	else
	{
		cout << "Cache获取成功" << endl;
	}

	SQLMgr();
}

void HttpServer()
{
	// ...
	srand(time(0));

	if (rand() % 3 == 0)
	{
		throw HttpServerException("请求资源不存在", 100, "get");
	}
	else if (rand() % 4 == 0)
	{
		throw HttpServerException("权限不足", 101, "post");
	}
	else
	{
		cout << "http调用成功" << endl;
	}

	CacheMgr();
}

实现异常多态

cpp 复制代码
int main()
{
	while (1)
	{
		this_thread::sleep_for(chrono::seconds(1));

		try {
			HttpServer();
		}
		catch (const Exception& e) // 这里捕获父类对象就可以
		{
			// 多态
			cout << e.what() << endl;
		}
		catch (...)
		{
			cout << "Unkown Exception" << endl;
		}
	}

	return 0;
}

尝试调用,看看运行结果为何:

复制代码
http调用成功
Cache获取成功
执行Sql成功
http调用成功
CacheException:权限不足
HttpServerException:post:权限不足
HttpServerException:post:权限不足
http调用成功
Cache获取成功
执行Sql成功

可以看到一个catch语块就能调出不同的异常处理结果。

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

我们先把原则放出来:

  1. 首先检查throw本身是否在try块内部,如果是再查找匹配的catch语句。如果有匹配的,则调到catch的地方进行处理。
  2. 没有匹配的catch则退出当前函数栈,继续在调用函数的栈中进行查找匹配的catch。
  3. 如果到达main函数的栈,依旧没有匹配的,则终止程序。上述这个沿着调用链查找匹配的catch子句的过程称为栈展开。所以实际中我们最后都要加一个catch(...)捕获任意类型的异常,否则当有异常没捕获,程序就会直接终止。
  4. 找到匹配的catch子句并处理以后,会继续沿着catch子句后面继续执行。

先看throw本身在try块内部:

cpp 复制代码
void Func1(int x)
{
	try
	{
		if (x == 1)
			throw x;
		else
			cout << "Well done!" << endl;
	}
	catch (...)
	{
		cout << "Bad thing >_<" << endl;
	}
}

int main()
{
	Func1(1);
	Func1(2);
	Func1(12);
	Func1(3);
	Func1(1);
	return 0;
}
复制代码
Bad thing >_<
Well done!
Well done!
Well done!
Bad thing >_<

throw不在try语句内:

cpp 复制代码
void Func2(int x)
{
	if (x == 1)
		throw x;
	else
		cout << "Well done!" << endl;
}

void Func1(int x)
{
	Func2(x);
	cout << "Func1()" << endl;
}
int main()
{
	int x;
	while (cin >> x)
	{
		try
		{
			Func1(x);
		}
		catch (...)
		{
			cout << "Bad thing!" << endl;
		}
	}
	return 0;
}
复制代码
1
Bad thing!
12
Well done!
Func1()
13
Well done!
Func1()
2
Well done!
Func1()
1
Bad thing!

可以看到,一旦抛出异常,代码会直接到最近catch语句跳过中间的语句:cout << "Func1()" << endl;

再看没有catch到相应异常:

cpp 复制代码
void Func()
{
	throw 1;
}
int main()
{
	try
	{
		Func();
	}
	catch (string str)
	{
		cout << "Nothing" << endl;
	}
	return 0;
}

可以看到程序直接中止了。

这个故事告诉我们,无论你对你catch到所有类型有多自信,最好还是加上catch(...)语句,毕竟你永远无法预测未来会向什么方向发展,做好你自己能做好的。

异常的重新抛出

有时候抛异常跳过之间的步骤是一件很危险的事请,请看下面代码:

cpp 复制代码
void Func2()
{
	throw 1;
}

void Func1()
{
	auto y = new int;
	cout << "new int " << endl;
	Func2();
	delete y;
	cout << "delete int" << endl;
}
int main()
{
	try
	{
		Func1();
	}
	catch (...)
	{
		cout << "catch succeed!" << endl;
	}
	return 0;
}
复制代码
new int
catch succeed!

世事变幻莫测,原本毫无问题的操作,加上更加保险的异常机制就犯了错误。

Func2异常抛出导致Func1开辟的静态空间没有回收,最终就会导致内存泄漏。

那么我们只好在Func1中先接受异常,处理好问题,再把异常抛出:

cpp 复制代码
//仅修改Func1
void Func1()
{
	auto y = new int;
	cout << "new int " << endl;
	try 
	{
		Func2();
	}
	catch (...)
	{
		delete y;
		cout << "delete int" << endl;
		throw;
	}
}
复制代码
new int
delete int
catch succeed!

throw;表示接受到什么就直接抛出什么。

这下我们就成功解决了内存泄漏的问题,但同时这个故事也告诉我们,那些看似美好的东西也潜藏着神秘的危险。

异常安全

  • 构造函数完成对象的构造和初始化 ,最好不要在构造函数中抛出异常,否则可能导致对象不完整或没有完全初始化
  • 析构函数主要完成资源的清理,最好不要在析构函数内抛出异常,否则可能导致资源泄漏(内存泄漏、句柄未关闭等)
  • C++中异常经常会导致资源泄漏的问题,比如在new和delete中抛出了异常,导致内存泄漏,在lock和unlock之间抛出了异常导致死锁,C++经常使用RAII来解决以上问题.

异常规范

C++11中新增noexcept关键字,表示这个函数不会抛出异常。当函数肯定不抛出异常时,最好加上这个关键字也是方便后来者。

cpp 复制代码
void Func()noexcept
{
	//...
	return;
}

自定义异常体系

实际使用中很多公司都会自定义自己的异常体系进行规范的异常管理,因为一个项目中如果大家随意抛异常,那么外层的调用者基本就没办法玩了,所以实际中都会定义一套继承的规范体系。这样大家抛出的都是继承的派生类对象,捕获一个基类就可以了

正如前面"catch中基类对象匹配派生类对象"部分提到的Exception类和派生类。

C++标准库的异常体系

c++中自然也会提供相应的异常体系:

异常 描述
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 当发生数学下溢时,会抛出该异常。

异常的优缺点

优点:

  1. 异常对象定义好了,相比错误码的方式可以清晰准确的展示出错误的各种信息,甚至可以包含堆栈调用的信息,这样可以帮助更好的定位程序的bug。
  2. 返回错误码的传统方式有个很大的问题就是,在函数调用链中,深层的函数返回了错误,那么我们得层层返回错误,最外层才能拿到错误。
  3. 很多的第三方库都包含异常,比如boost、gtest、gmock等等常用的库,那么我们使用它们也需要使用异常。
  4. 部分函数使用异常更好处理,比如构造函数没有返回值,不方便使用错误码方式处理。比如T& operator这样的函数,如果pos越界了只能使用异常或者终止程序处理,没办法通过返回值表示错误。

缺点:

  1. 异常会导致程序的执行流乱跳,并且非常的混乱,并且是运行时出错抛异常就会乱跳。这会导致我们跟踪调试时以及分析程序时,比较困难。
  2. 异常会有一些性能的开销。当然在现代硬件速度很快的情况下,这个影响基本忽略不计。
  3. C++没有垃圾回收机制,资源需要自己管理。有了异常非常容易导致内存泄漏、死锁等异常安全问题。这个需要使用RAII来处理资源的管理问题。学习成本较高。
  4. C++标准库的异常体系定义得不好,导致大家各自定义各自的异常体系,非常的混乱。
  5. 异常尽量规范使用,否则后果不堪设想,随意抛异常,外层捕获的用户苦不堪言。所以异常规范有两点:一、抛出异常类型都继承自一个基类。二、函数是否抛异常、抛什么异常,都使用 func() throw();的方式规范化。

总结:异常总体而言,利大于弊,所以工程中我们还是鼓励使用异常的。另外OO的语言基本都是用异常处理错误,这也可以看出这是大势所趋

相关推荐
小龙报2 小时前
《算法通关指南:算法基础篇 --- 一维前缀和 — 1. 【模板】一维前缀和,2.最大子段和》
c语言·数据结构·c++·算法·职场和发展·创业创新·visual studio
郝学胜-神的一滴2 小时前
Effective STL 第9条:C++容器元素删除技巧详解
开发语言·c++·程序人生·stl
Ma_Hong_Kai2 小时前
带复选框的combox
c++·mfc
syker3 小时前
太极指令集架构(TCIS)v1.1与主流指令集比较研究报告
c++·架构
jf加菲猫4 小时前
第1章 认识Qt
开发语言·c++·qt·ui
蒋星熠6 小时前
全栈开发实战指南:从架构设计到部署运维
运维·c++·python·系统架构·node.js·devops·c5全栈
杜子不疼.6 小时前
【C++】深入拆解二叉搜索树:从递归与非递归双视角,彻底掌握STL容器的基石
开发语言·c++
天若有情6736 小时前
从零实现轻量级C++ Web框架:SimpleHttpServer入门指南
开发语言·前端·c++·后端·mvc·web应用
mjhcsp8 小时前
C++ 三分查找:在单调与凸函数中高效定位极值的算法
开发语言·c++·算法