C++:异常

目录

一. C语言处理错误的方法

二. C++中异常的概念

三. 异常的抛出和捕捉

3.1 异常抛出和捕捉的原则

3.2 继承异常体系

四. 异常规范问题

五. 异常安全问题

六. 异常的优缺点

七. 总结


一. C语言处理错误的方法

C语言是面向过程的语言,有以下两种处理错误的方法:

  • 终止程序,如通过assert断言强行终止程序运行。缺点:在大型软件项目中,某个部分发生错误就终止整个程序很难别接收,因为可能这个软件的大部分功能可以正常使用,只是局部功能收到了影响。另外assert只在Debug版本下才有用,在Release版本下被忽略。
  • 设置全局错误码errno,errno是C语言内置的全局错误码,被包在头文件<errno.h>中。缺点:需要用户自己去根据错误码匹配错误信息,不够直观。

C语言处理错误涉及一个内置全局变量errno及两个库函数strerror和perror。

  • strerror函数:以字符串的形式,获取错误码对应错误信息,在头文件<string.h>中。
  • perror函数:打印错误信息,可以添加自定义的前置声明。

代码1.1企图用只读的方式打开不存在的文件test.txt,检查文件指针,发现为NULL,然后依次打印错误码、调用strerror和perror打印错误信息,程序运行结果见图1.1。

代码1.1:C语言错误码处理异常

#include<stdio.h>
#include<errno.h>
#include<string.h>

int main()
{
	FILE* pf = fopen("test.txt", "r");

	if (NULL == pf)
	{
		printf("errno = %d\n", errno);     //打印错误码
		printf("%s\n", strerror(errno));   //获取错误码对应错误信息并打印
		perror("fopen");   //输出 fopen:错误信息
		return 1;
	}

	fclose(pf);
	return 0;
}
图1.1 代码1.1运行结果

  

二. C++中异常的概念

C++及其它面向对象的语言(如JAVA),对错误的处理方法一般都为抛异常。异常,就是当函数遇到一个其自身无法处理的错误时,将这个错误抛出,由外部的函数进行处理或者直接终止程序。C++处理异常有3个重要关键字:

  • try:后跟大括号,大括号内部是可能抛出异常的语句。
  • catch:捕获异常,并在其后面的大括号里对异常进行处理。
  • throw:抛出特定的异常。

代码2.2定义了一个整形除法运算函数Div,如果除数为0,那么就抛除0异常错误(throw "Divide by 0 error"),通过catch不会Div中的错误,在外部catch抛出的错误,并打印错误信息。

代码2.2:C++抛异常

#include<iostream>

int Div(int a, int b)
{
	if (0 == b)
		throw "Divide by 0 error";
	else
		return a / b;
}

int main()
{
	try
	{
		int ret = Div(3, 0);  //抛异常:除0错误
	}
	catch (const char* errmsg)
	{
		std::cout << errmsg << std::endl;
	}

	return 0;
}

三. 异常的抛出和捕捉

3.1 异常抛出和捕捉的原则

  1. 异常都是由对象抛出的,抛出对象的类型,决定了被哪一个catch捕捉。
  2. 异常对象被调用链中与抛出位置最近的且类型匹配的catch捕捉。调用链是指可理解为函数的嵌套调用顺序,如main函数调用Func1,Func1调用Func2,Func2调用Func3,那么main()->Func1()->Func2()->Func3()就是一条调用链(见图3.1)。
  3. 当有异常抛出时,先检查throw是否在catch内部,然后沿着调用链,去查找匹配的catch,如果到了main()函数还没有catch被激活,那么程序终止运行。
  4. 一般而言,不允许因为异常没有被捕获而造成程序终止运行。catch(...)能匹配任意类型的异常,因此一般要求所有的try...catch...体系都要有catch(...)的存在。
  5. 由于异常对象为临时对象,因此在异常对象抛出后,会生成一份它的拷贝,传递给catch进行捕捉,这类似于函数的值返回/传值调用。
  6. 在实际的大型工程项目中,经常使用继承体系的对象来捕获异常。抛出派生类作为异常对象,用基类的引用来捕获。
图3.1 调用链

如代码3.1所示,在Func函数中调用Div函数,Div函数会发生除0错误而抛异常,异常对象的类型为char*,在Func和main函数中,都有对char*类型异常的捕获,因为Func离异常抛出的位置更近,所以优先激活Func函数中的catch,运行程序输出:Func() -> Divide by 0 error

代码3.1:异常捕捉1

int Div(int a, int b)
{
	if (0 == b)
		throw "Divide by 0 error";
	else
		return a / b;
}

void Func()
{
	try
	{
		int ret = Div(3, 0);
	}
	catch(const char* errmsg)
	{
		std::cout << "Func() -> " << errmsg << std::endl;
	}
}

int main()
{
	try
	{
		Func();
	}
	catch(const char* errmsg)
	{
		std::cout << "main() -> " << errmsg << std::endl;
	}

	return 0;
}

如代码3.2所示,在Func函数中被调用的Div函数抛除0异常,在Func函数中的catch只能匹配到int类型的异常对象,无法激活,到了main()函数中,才会有捕捉char*类型的异常对象的catch,运行程序输出:main() -> Divide by 0 error

代码3.2:异常捕捉2

int Div(int a, int b)
{
	if (0 == b)
		throw "Divide by 0 error";
	else
		return a / b;
}

void Func()
{
	try
	{
		int ret = Div(3, 0);
	}
	catch (int errId)
	{
		std::cout << "Func() -> " << errId << std::endl;
	}
}

int main()
{
	try
	{
		Func();
	}
	catch (const char* errmsg)
	{
		std::cout << "main() -> " << errmsg << std::endl;
	}

	return 0;
}

在代码3.3中,调用链中没有catch能够捕捉Div函数抛出的char*类型的异常对象,因此,程序在抛出异常后会终止执行。

代码3.3:异常捕捉3

int Div(int a, int b)
{
	if (0 == b)
		throw "Divide by 0 error";
	else
		return a / b;
}

void Func()
{
	try
	{
		int ret = Div(3, 0);
	}
	catch (int errId)
	{
		std::cout << "Func() -> " << errId << std::endl;
	}
}

int main()
{
	try
	{
		Func();
	}
	catch (int errId)
	{
		std::cout << "Func() -> " << errId << std::endl;
	}

	return 0;
}
图3.2 因异常没有被捕捉而造成的程序终止

代码3.4中虽然Func函数和main函数都没有直接接收char*类型的catch,但是,在main函数中有catch(...)可以被任意类型的异常对象激活,运行程序输出:未知异常。 

代码3.4:异常捕捉4

int Div(int a, int b)
{
	if (0 == b)
		throw "Divide by 0 error";
	else
		return a / b;
}

void Func()
{
	try
	{
		int ret = Div(3, 0);
	}
	catch (int errId)
	{
		std::cout << "Func() -> " << errId << std::endl;
	}
}

int main()
{
	try
	{
		Func();
	}
	catch (int errId)
	{
		std::cout << "Func() -> " << errId << std::endl;
	}
	catch (...)
	{
		std::cout << "未知异常" << std::endl;
	}

	return 0;
}

3.2 继承异常体系

C++虽然有内置的标准异常体系(见图3.3),但是,在实际的大型项目中,一般都会自定义一套区别与C++标准的异常体系,通常采用继承的体系,来定义异常对象。一般会以派生类对象作为异常对象抛出,用基类的引用来catch派生类的异常对象。

图3.3 C++标准库异常体系

图3.4 自定义异常继承体系

代码3.5定义了一套服务器异常继承体系,包含一个基类Exception,这个基类中有一个what成员函数,用于获取异常信息。认为服务器可能会抛出网络异常、缓存异常和数据库异常,因此,以Exception类为基类,定义了三个派生类对象:HttpServeException、CacheException和SqlException,分别用于网络异常、缓存异常和数据库出错时抛出异常对象,每个派生类对象重写what()函数,用于获取各自的错误信息,在主函数中,只需要使用基类对象引用Exception接收异常对象即可。

代码3.5:服务器异常继承体系

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:
	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;
};

四. 异常规范问题

  • 在函数后面接throw(类型1, 类型2, ... ),用于指定函数可以抛出的异常类型。函数后面接throw(),表示函数不会抛出任何异常。
  • 如果函数后面没有throw(...)进行声明,则表示函数可以抛出任何类型的异常。
  • C++11新增noexcept关键字,用于表示某个函数不会抛异常。
  • C++异常规范并不是强制规定,编译器不会对异常规范进行检查,这是由于C++要兼容C语言语法及一些历史坑造成的,声明func throw(A,B,C),我们就会认为func只会抛出ABC三种类型异常对象,但即使抛出D类型异常对象也不会报错,异常规范,非常考验程序员的素养。
void func1() throw(A, B, C);  //func1会抛出A、B、C三种类型异常对象
void func2() throw();     //func2不会抛出异常
void func3();     //func3可以抛出各种类型的异常对象

//C++11 noexcept关键字
void func4() noexcept;   //func4不会抛异常

五. 异常安全问题

安全隐患1:构造函数和析构函数不要抛出异常

如果构造函数抛出异常,那么有可能出现对象构造不完全的问题,后面使用该对象可能出现不确定的错误。析构函数中不能抛异常,如果析构函数抛出异常,可能会造成资源释放不完全从而导致内存泄漏问题。

如果构造函数和析构函数确实有可能抛出异常,那么就应当在函数内部使用try...catch...处理掉异常,不能让异常离开构造和析构函数。

代码5.1:在析构函数中捕捉异常

class AA
{
public:
	close()
	{
		...
		if (...)
		{
			throw "wrong";
		}
	}

	~AA()
	{
		try
		{
			close()   //释放资源(可能抛异常)
		}
		catch (const char* errmsg)
		{
			...  //异常处理
		}
		catch (...)
		{
			//未知异常
		}
	}
};

安全隐患2:C++没有垃圾回收机制,new/delete若抛异常容易引发内存泄漏问题。(应使用智能指针RAII解决)

代码3.2中,如果Func函数中Div抛出了除0异常,并且adelete[] arr1出现了异常,那么程序会直接抛出delete[] arr1产生的异常,而调用func的函数却无法释放arr2的空间,造成arr2的空间没有被释放,引发内存泄漏。

代码3.2:new / delete 异常安全问题

int Div(int a, int b)
{
	if (0 == b)
		throw "Divide by 0 error";
	else
		return a / b;
}

void func()
{
	int* arr1 = new int[10];
	int* arr2 = new int[10];

	int a = 0, b = 0;
	std::cin >> a >> b;

	try
	{
		int ret = Div(a, b);
	}
	catch (...)
	{
		//如果此处抛异常,那么delete[] arr2不会被执行,异常直接被throw抛到func之外
		delete[] arr1;   

		//此处有不被执行导致的内存泄漏风险
		delete[] arr2;

		throw;   //抛出捕获到的异常
	}

	delete[] arr1;
	delete[] arr2;
}

六. 异常的优缺点

异常的优点:

  1. 方便定位错误和,查看错误信息,还可以包含调用堆栈的信息,方便Bug的修改。
  2. C语言采用错误码需要层层返回进行处理,无法一次跳过多级调用,我们返回最外层来处理错误码获取错误信息。
  3. 对于构造函数和析构函数等没有返回值的函数,采用抛异常的方法更加方便。

异常的缺点:

  1. 异常容易造成执行流乱跳,不便于调试程序。
  2. 相比于JAVA等主流语言,C++没有垃圾回收机制,抛异常容易引起内存泄漏、锁死等问题。
  3. C++标准库中异常体系并不优秀,实际项目中多会自定义异常体系,造成混乱。同时,C++异常标准不具有强制性,因此对程序员的素质要求极高,且容易造成混乱。

七. 总结

  • C语言一般采用错误码+返回值的方法来标识错误,C++等面向对象的语言则采用抛异常的方法标处理错误。C++通过对象来抛出异常,通过try...catch...来捕获异常并进行相应处理。
  • 异常的捕获位置由异常对象的类型确定,会被调用流中离throw最近的且类型匹配的catch捕获,一般需要catch(...)捕获未知类型异常,如果异常到了main()函数还没有被捕获则程序终止。
  • 在实际项目中一般采用继承结构来定义异常体系,抛出的异常为派生类对象,使用基类对象的引用进行捕捉。
  • C++异常存在安全问题,new/delete体系中抛异常易出现内存泄漏问题,不允许构造函数和析构函数抛出异常,构造函数和析构函数中的异常应当在函数内部进行处理。
  • C++有异常规范,但不严格要求,编译器更不会检查。
  • 异常的最大优点在于容易定位错误和获取错误信息,最大的缺点在于容易造成执行流乱跳,程序追踪调试困难。
  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值