异常(C++)

异常

  • 前言
  • 一、程序的错误分类
  • 二、异常
    • 1. 概念
    • 2. 捕获异常的关键字和格式
    • 3. 异常的使用
      • 异常的原则
      • 异常再抛出
      • 异常说明
      • 注意事项
    • 4. 自定义异常体系
    • 5. C++标准库的异常体系
  • 三、总结

前言

  1. 在程序运行时经常碰到一些错误,例如年龄、身高不能为负,除数为0等,这些错误放到程序中如果不加以管制,程序就会崩溃。C++提供了异常机制,让我们能跟捕获运行时错误,给程序一次机会,给用户一个反馈。
  1. C++异常处理机制可以让我们捕获并进行处理错误,然后我们可以通过捕获后的判断,重新给程序指一条明路,或者在程序结束之前,做一些必要的工作,例如将错误写到日志等。

说到C++,C语言的处理错误方式也简单提一下:
assert宏

  1. 生产环境可能被禁用(会带来性能开销,并且不能对用户提供任何实际好处)编译器会优化掉。
  2. 开发环境可以用来检查条件,但是不提供详细的错误信息,并且终止程序。
  3. 只能用来检查布尔表达式,无法用于更复杂的逻辑和错误处理。

返回错误码

  1. 程序员要自己查找对应的错误,系统很多库函数也都是通过错误码(errno)表示错误

一、程序的错误分类

程序的错误大致可以分为三种,分别时语法错误、逻辑错误和运行时错误。

  1. 语法错误:在编译和链接阶段就能发现,必须全符合语法规则才能生成可执行代码。这种错误最简单。
  2. 逻辑错误:编码思路问题,执行的结果不是预定的,也可以通过调试解决。
  3. 运行时错误:程序运行期间发生的错误,如同除0,越界等。C++异常(Exception)机制就是为了解决这种错误。

二、异常

1. 概念

异常:异常是一种处理错误的方式,当一个函数发现自己无法处理的错误时就可以抛异常,让函数的直接或间接调用者处理这个错误。

2. 捕获异常的关键字和格式

C++异常处理涉及三个关键字

  • throw:出现问题,程序使用throw抛出异常
  • catch:有抛出也得有捕获,catch关键字就是用来捕获异常。捕获的地方就是处理问题的地方。
  • try:try块中的代码为保护代码,如果出错会激活特定的异常,后面跟着一个或多个catch块。

eg:语法

try{
	//保护区,就是可能会出错的代码
}
catch(ExceptionName e1){
	//出错后,通过异常处理程序捕获异常,在这里处理问题或者 再抛出
}
catch(ExceptionName e2){
	//因为异常对象类型的多样,所以可以多个catch块捕获匹配的异常
}
catch(...){
	//如果try块中出现异常,而前面的catch块也没有捕获,这是最后一道防线,所有类型都会在这里捕获,但不知道异常的错误原因。
}

注意: 可能有些不好理解,后面内容我会逐步介绍

3. 异常的使用

异常的原则

简单的例子:

double Division(int a, int b)
{
	if (b == 0)
		throw "Division by zero condition!";
	else
		return (double)a / (double)b;
}

void Func()
{
	int len, time;
	cin >> len >> time;
	cout << Division(len, time) << endl;
}

int main()
{
	try 
	{
		Func();
	}
	catch (const char* errmsg)
	{
		cout << errmsg << endl;
	}
	catch (...)
	{
		cout << "unknow exception" << endl;
	}

	return 0;
}

异常(C++)_第1张图片
异常(C++)_第2张图片

上例的函数调用链中栈的细节:

  1. 检查被throw是否在try块中,如果在并触发异常。
  2. 被throw抛出对象的类型和最近的catch块的参数进行匹配,匹配成功,则调到catch块中处理。没有匹配成功,则退出当前函数栈,在调用该函数的栈中继续匹配。
  3. 如果到main函数的栈依旧没有匹配,则终止程序(非正常结束)。但一般有catch(…)兜底。
  4. 如果匹配到catch子句处理完块中内容后,会沿着try,catch语句后继续执行

匹配原则

  1. throw抛出的对象类型与调用链中位置最近并且参数类型一样的catch匹配。
  2. throw抛出异常会生成一个异常对象的拷贝(因为抛出的对象可能是一个临时对象),这个拷贝的对象会被catch以后销毁。(类似函数传值返回)
  3. catch(…)可以捕获任意类型异常,但是不知道异常错误是什么。
  4. 匹配原则的例外:可以抛出派生类对象,使用基类捕获。(后面讲)

异常再抛出

一个单独的catch语句不能完整的处理某个异常,再执行一些校正操作之后,可能会抛给调用链更上一层的函数接着处理异常。
这个简单的校正工作可能就是改变参数内容,但是这里又要注意是不是引用
eg:

//简单的校正操作
catch (my_error& eObj){                     //引用类型 
	eObj.status = errCodes::severeErr;      //修改了异常对象
	throw;
}
catch (other_error eObj) {                     //非引用类型 
	eObj.status = errCodes::severeErr;         //只修改了异常对象的局部副本
	throw;
}

异常再抛出:

try
{
	//保护代码
	//...
}
catch (const char* errmsg)
{
	cout << errmsg << endl;
}
catch (...)
{
	throw;//异常再抛出,不含任何表达式
	//注意异常进行抛出后,是直接跳过这个函数栈的后面部分,例如下面的打印语句就  不会在进行执行
	cout << "unknow Exception" << endl;
}

异常说明

目的是为了让使用者知道该函数抛出的异常类型。

异常说明:

//这里表示这个函数会抛出A/B/C/D中的某种类型的异常
void func()  throw(A, B, C, D);

//这里表示这个函数只会抛出bad_alloc的异常
void* operator new (std::size_t size) throw (std::bad_alloc);

//这里表示这个函数不会抛出异常
void* operator delete (std::size_t size, void* ptr) throw ();

//若无异常接口声明:表示这个函数可以抛出任何类型的异常
void func1()

C++11新增的noexcept,表示不会抛出异常

void recoup(int) noexcept; //不会抛出异常
void alloc(int)            //可能抛出异常

注意: noexcept的位置也很特殊,因为const,final,override或者虚函数=0,都可以跟在函数后面,所以noexcept要在const及引用限定符之后,在final,override或者虚函数=0之前。

注意事项

建议

  1. 不要在构造函数中抛出异常(可能导致对象未完成初始化)
  2. 不要在析构函数中抛出异常(可能导致资源泄漏)
  3. 不要在lock和unlock之间抛异常(死锁)(后面的博客讲)

4. 自定义异常体系

实际很多公司会自己定义异常体系,规范管理。大家抛出的都是继承的派生类对象,捕获一个基类即可。

异常(C++)_第3张图片

eg:服务器开发中使用的异常继承体系(例子)

class Exception
{
public:
	Exception(const string& errmsg, int id)
		:_errmsg(errmsg)
		,_id(id)
	{}

	virtual string what() const
	{
		return _errmsg;
	}

protected:
	string _errmsg;
	int _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:
	SqlException(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;
};

调用捕获:



void SQLMgr()
{
	srand(time(0));
	if (rand() % 7 == 0)
	{
		throw SqlException("权限不足", 100, "select * from name = '张三'");
	}
	//throw "xxxxxx";
}

void CacheMgr()
{
	srand(time(0));
	if (rand() % 5 == 0)
	{
		throw CacheException("权限不足", 100);
	}
	else if (rand() % 6 == 0)
	{
		throw CacheException("数据不存在", 101);
	}
	SQLMgr();
}

void HttpServer()
{
	// ...
	srand(time(0));
	if (rand() % 3 == 0)
	{
		throw HttpServerException("请求资源不存在", 100, "get");
	}
	else if (rand() % 4 == 0)
	{
		throw HttpServerException("权限不足", 101, "post");
	}
	CacheMgr();
}

int main()
{
	while (true)
	{
		Sleep(500);      //引用系统头文件
		try {
			HttpServer();
		}
		catch (const Exception& e) // 这里捕获父类对象就可以
		{
			// 多态
			cout << e.what() << endl;
		}
		catch (...)
		{
			cout << "Unkown Exception" << endl;
		}
	}
	return 0;
}

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

异常(C++)_第4张图片
注意: C++标准库的异常体系设计的不好用,所以一般都是定义自己的异常体系

三、总结

异常总体而言利大于弊

异常的优缺点

优点:

  1. 异常对象定义好了,可以更加清晰准确的展示出错误的各种信息,更好的定位bug。
  2. 不需要层层返回检查并错误码,异常体系直接跳到catch捕获的位置
  3. 很多第三方库都包含异常,想要使用这些库也需要使用异常
  4. 部分函数使用异常更好处理,例如没有返回值的或者不便于返回的。

缺点:

  1. 执行流乱跳,导致跟踪调试时以及分析程序困难,很混乱
  2. 性能的开销
  3. C++标准库的异常体系定义的不好,大家各自定义异常体系,不统一

你可能感兴趣的:(C++,c++,异常)