C++ 异常

1.异常的概念和流程

C++异常是一种在程序运行时处理错误的机制,核心是将"检测错误"和"处理错误"的代码分离,让程序更健壮、逻辑更清晰。

简单理解其流程:

  1. 抛出(throw):当代码检测到无法处理的错误(如除零、文件不存在),用 throw 关键字"抛出"一个异常对象(可是内置类型或自定义类)。

  2. 捕获(catch):在调用栈的上层,用 try-catch 块"捕获"异常, try 包裹可能出错的代码, catch 根据异常类型匹配并处理错误,try 块中的代码标识将被激活的特定异常,它后面通常跟着一个或多个 catch 块。

  3. 传播(propagate):若当前函数未捕获异常,异常会向上层调用函数传播,直到被捕获或导致程序终止(触发 terminate )。

2.C语言处理错误的方式

c语言的处理错误的方式主要右两种方法,终止程序(assert)和返回错误码。如果使用assert检查时,都会直接的终止程序,但是有时候我们遇到一些不是很严重的问题时,不想直接就终止程序,而是换一种"温柔"一点的方式提醒我们,如果使用返回错误码,那么再很多库里面都设置了会把错误码设置到errno里面,当我们使用函数后,都需要去判断,要找到错误码和错误信息也需要我们去打印出来,很不方便。

3. 异常的使用

3.1 异常的输出和匹配原则

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

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

示例1:必须要类型匹配,抛出的是什么类型,就必须用什么类型的对象来接收。

cpp 复制代码
double Division(int a, int b)
{
	if (b == 0)
	{
		throw" Division by zero conditional";
	}
	else
	{
		return ((double)a / (double)b);
	}
}
void func()
{
	int len, time;
	cin >> len >> time;
	cout << Division(len, time) << endl;
}
int main()
{
	try
	{
		func();
	}
	//catch (int errmsg)//类型必须要匹配,匹配就捕捉不到,捕捉不到就会报错,异常必须要被捕获
	//{
	//	cout << errmsg << endl;
	//}
	catch (const char* errmsg)
	{
		cout << errmsg << endl;
	}
	return 0;
}

示例2:

cpp 复制代码
double Division(int a, int b)
{
	if (b == 0)
	{

		throw" Division by zero conditional";
		cout << "a" << endl;
	}
	else
	{
		return ((double)a / (double)b);
	}
}
void func()
{
	try
	{
		int len, time;
		cin >> len >> time;
		cout << Division(len, time) << endl;
	}
	catch (const char* errmsg)//会在这边捕捉到,会先走离得近的,就不会再走主函数的捕捉了。
	{
		cout << errmsg << endl;
	}
	cout << "xxxx" << endl;
}
int main()
{
	try
	{
		func();
	}
	catch (const char* errmsg)
	{
		cout << errmsg << endl;
		cout << "zzz" << endl;
	}
	return 0;
}

运行结果:

throw抛出后向上层抛出,不会执行throw后面的代码,会直接跳到上一层,如果上层没有捕捉,会继续向上层传递,这里在func就被捕捉了,主函数main函数里面的catch里面的代码也就不会执行了。

  1. 抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个临时对象,

所以会生成一个拷贝对象,这个拷贝的临时对象会在被catch以后销毁。(这里的处理类似

于函数的传值返回)

示例:

抛出的类型可以是任意类型,为了避免在出来作用域就销毁的情况,所以返回的的是拷贝对象

cpp 复制代码
double Division(int a, int b)
{
	if (b == 0)
	{
		string s1("Division by zero conditional");//可以抛出任意类型的对象,也不会出现野指针的问题,会形成临时拷贝,抛出的是临时拷贝的s1
		throw s1;
		/*throw" Division by zero conditional";*/
	}
	else
	{
		return ((double)a / (double)b);
	}
}
void func()
{
	try
	{
		int len, time;
		cin >> len >> time;
		cout << Division(len, time) << endl;
	}
	catch (const char* errmsg)//会在这边捕捉到,会先走离得近的,就不会再走主函数的捕捉了。
	{
		cout << errmsg << endl;
	}
}
int main()
{
	try
	{
		func();
	}
	catch (const char* errmsg)
	{
		cout << errmsg << endl;
	}
	return 0;
}
  1. catch(...)可以捕获任意类型的异常,问题是不知道异常错误是什么。

示例:写代码时,为了避免抛出异常而没有被捕捉或者忘记捕捉而导致程序崩溃,catch(...)作为最后的兜底。

ps:catch作为兜底一般放在最后面,不然会影响其他的捕捉,下面是错误代码。

cpp 复制代码
double Division(int a, int b)
{
	if (b == 0)
	{
		string s1("Division by zero conditional");//可以抛出任意类型的对象,也不会出现野指针的问题,会形成临时拷贝,抛出的是临时拷贝的s1
		throw s1;

		/*throw" Division by zero conditional";*/
	}
	else
	{
		return ((double)a / (double)b);
	}
}
void func()
{
	int len, time;
	cin >> len >> time;
	cout << Division(len, time) << endl;
}
int main()
{
	try
	{
		func();
	}
	catch(...)//捕捉任意类型,防止有人抛异常不规范,导致没有捕捉到,导致程序崩溃,因为可以捕捉任意的类型,一般放在最后面
	{			//不影响其他异常的捕捉

		cout << "未知异常" << endl;
	}
	catch (const char* errmsg)
	{
		cout << errmsg << endl;
	}
	return 0;
}

正确代码:

cpp 复制代码
double Division(int a, int b)
{
	if (b == 0)
	{
		string s1("Division by zero conditional");//可以抛出任意类型的对象,也不会出现野指针的问题,会形成临时拷贝,抛出的是临时拷贝的s1
		throw s1;

		/*throw" Division by zero conditional";*/
	}
	else
	{
		return ((double)a / (double)b);
	}
}
void func()
{
	int len, time;
	cin >> len >> time;
	cout << Division(len, time) << endl;
}
int main()
{
	try
	{
		func();
	}
	//catch(...)//捕捉任意类型,防止有人抛异常不规范,导致没有捕捉到,导致程序崩溃,因为可以捕捉任意的类型,一般放在最后面
	//{			//不影响其他异常的捕捉

	//	cout << "未知异常" << endl;
	//}
	catch (const char* errmsg)
	{
		cout << errmsg << endl;
	}
	catch(...)
	{
		cout << "未知异常" << endl;
	}
	return 0;
}
  1. 实际中抛出和捕获的匹配原则有个例外,并不都是类型完全匹配,可以抛出的派生类对象,

使用基类捕获.

可以幻想这样的一个场景,很多人一起共同写一个项目,都需要抛异常,都需要捕捉出错误信息和错误编号这两个是必要的内容,把这两个信息包含在一个类中,但是不同的人可能需要抛出不同的信息,如果每个人都定义一个类,那捕捉的信息量是不是太大了,如果都放到一个类里面,那么每次都要捕捉这个类,但是利用到里面的资源又不多,所以设置一个类里面包含必要的信息,其他人要抛异常就去继承这个类,然后定义需要的资源。这里的代码放在自定义异常体系。

4. 自定义异常体系

实际使用中很多公司都会自定义自己的异常体系进行规范的异常管理,因为一个项目中如果大家

随意抛异常,那么外层的调用者基本就没办法玩了,所以实际中都会定义一套继承的规范体系。

这样大家抛出的异常调用基类来进行调用就可以了。

基类的定义:

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

模拟出现异常的情况,出现异常抛出。

主函数:

cpp 复制代码
}
int main()
{
	while (1)
	{
		Sleep(500);
		try {
			HttpServer();
		}
		catch (const Exception& e) // 这里捕获父类对象就可以
		{
			// 多态
			cout << e.what() << endl;
		}
		catch (...)
		{
			cout << "Unkown Exception" << endl;
		}
	}
	return 0;
}

使用多态捕捉异常,使用catch(...)兜底。

5.异常的重新抛出

有可能单个的catch不能完全处理一个异常,在进行一些校正处理以后,希望再交给更外层的调用

链函数来处理,catch则可以通过重新抛出将异常传递给更上层的函数进行处理。

cpp 复制代码
double Division(int a, int b)
{
	if (b == 0)
	{
		throw" Division by zero conditional";
	}
	else
	{
		return ((double)a / (double)b);
	}
}
void func()
{
	int* array = new int[10];
	int len, time;
	cin >> len >> time;
	cout << Division(len, time) << endl;
}
int main()
{
	try
	{
		func();
	}
	catch (const char* s)
	{
		cout << s << endl;
	}
}

这里如果try在捕捉的时候就会直接跳到主函数的catch,会造成array没有释放,内存泄漏。

cpp 复制代码
double Division(int a, int b)
{
	if (b == 0)
	{
		throw" Division by zero conditional";
	}
	else
	{
		return ((double)a / (double)b);
	}
}
void func()
{
	int* array = new int[10];
	try
	{
		int len, time;
		cin >> len >> time;
		cout << Division(len, time) << endl;
	}
	catch (...)
	{
		cout << "delete[]array" << endl;
		delete[]array;
		throw;//重新抛出异常,收到什么抛什么
	}
	cout << "delete[]array" << endl;
	delete[]array;
}
int main()
{
	try
	{
		func();
	}
	catch (const char* s)
	{
		cout << s << endl;
	}
}

catch (...) 捕获所有类型的异常。

delete[] array; 释放在 Func 函数中动态分配的内存。

然后,throw; 重新抛出异常,将其传递到 main 函数中的 catch 块处理。

6.异常安全

C++异常安全是指程序在抛出异常时,仍能保证资源不泄露(如内存、文件句柄)、数据状态合法可控,且不会导致程序崩溃或进入不可预测状态的设计准则。

1.构造函数完成对象的构造和初始化,最好不要在构造函数中抛出异常,否则可能导致对象不

完整或没有完全初始化

2.析构函数主要完成资源的清理,最好不要在析构函数内抛出异常,否则可能导致资源泄漏(内

存泄漏、句柄未关闭等)

3.C++中异常经常会导致资源泄漏的问题,比如在new和delete中抛出了异常,导致内存泄

漏,在lock和unlock之间抛出了异常导致死锁,C++经常使用RAII来解决以上问题。

7. 异常规范

1.异常规格说明的目的是为了让函数使用者知道该函数可能抛出的异常有哪些。 可以在函数的

后面接throw(类型),列出这个函数可能抛掷的所有异常类型。

  1. 函数的后面接throw(),表示函数不抛异常。

  2. 若无异常接口声明,则此函数可以抛掷任何类型的异常。

cpp 复制代码
// 这里表示这个函数会抛出A/B/C/D中的某种类型的异常
void fun() 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();
// C++11 中新增的noexcept,表示不会抛异常
thread() noexcept;
thread(thread&& x) noexcept;

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

C++ 提供了一系列标准的异常,定义在 中,我们可以在程序中使用这些标准的异常。它们是以父

子类层次结构组织起来的:

说明:实际中我们可以可以去继承exception类实现自己的异常类。但是实际中很多公司像上面一

样自己定义一套异常继承体系。因为C++标准库设计的不够好用。

9.异常的优缺点

9.1 C++异常的优点:

  1. 异常对象定义好了,相比错误码的方式可以清晰准确的展示出错误的各种信息,甚至可以包

含堆栈调用的信息,这样可以帮助更好的定位程序的bug。

  1. 返回错误码的传统方式有个很大的问题就是,在函数调用链中,深层的函数返回了错误,那

么我们得层层返回错误,最外层才能拿到错误,具体看下面的详细解释。

cpp 复制代码
  // 1.下面这段伪代码我们可以看到ConnnectSql中出错了,先返回给ServerStart,
ServerStart再返回给main函数,main函数再针对问题处理具体的错误。
  // 2.如果是异常体系,不管是ConnnectSql还是ServerStart及调用函数出错,都不用检查,因
为抛出的异常异常会直接跳到main函数中catch捕获的地方,main函数直接处理错误。
  int ConnnectSql()
 {
 // 用户名密码错误
 if (...)
 return 1;
  
      // 权限不足
 if (...)
 return 2;
 }
  
  int ServerStart() {
 if (int ret = ConnnectSql() < 0)
 return ret;
      int fd = socket() 
      if(fd < 0)
          return errno;
 }
  
  int main()
 {
 if(ServerStart()<0)
 ...
  
 return 0;
 }
  1. 很多的第三方库都包含异常,比如boost、gtest、gmock等等常用的库,那么我们使用它们

也需要使用异常。

  1. 部分函数使用异常更好处理,比如构造函数没有返回值,不方便使用错误码方式处理。比如T& operator这样的函数,如果pos越界了只能使用异常或者终止程序处理,没办法通过返回值表示错误。

9.2 C++异常的缺点:

  1. 异常会导致程序的执行流乱跳,并且非常的混乱,并且是运行时出错抛异常就会乱跳。这会

导致我们跟踪调试时以及分析程序时,比较困难。

  1. 异常会有一些性能的开销。当然在现代硬件速度很快的情况下,这个影响基本忽略不计。

  2. C++没有垃圾回收机制,资源需要自己管理。有了异常非常容易导致内存泄漏、死锁等异常

安全问题。这个需要使用RAII来处理资源的管理问题。学习成本较高。

  1. C++标准库的异常体系定义得不好,导致大家各自定义各自的异常体系,非常的混乱。

  2. 异常尽量规范使用,否则后果不堪设想,随意抛异常,外层捕获的用户苦不堪言。所以异常

规范有两点:一、抛出异常类型都继承自一个基类。二、函数是否抛异常、抛什么异常,都

使用 func() throw();的方式规范化。

总结:异常总体而言,利大于弊,所以工程中我们还是鼓励使用异常的。另外OO的语言基本都是

用异常处理错误,这也可以看出这是大势所趋。

相关推荐
踩坑小念7 小时前
进程 线程 协程基本概念和区别 还有内在联系
java·linux·jvm·操作系统
学习编程的Kitty8 小时前
JavaEE初阶——多线程(4)线程安全
java·开发语言·jvm
学到头秃的suhian9 小时前
垃圾收集器
java·jvm
Jul1en_12 小时前
JVM的内存区域划分、类加载机制与垃圾回收原理
java·jvm
海边夕阳20061 天前
深入解析volatile关键字:多线程环境下的内存可见性与指令重排序防护
java·开发语言·jvm·架构
无敌最俊朗@1 天前
SQLite 核心知识点讲解
jvm·数据库·oracle
fantasy5_51 天前
手写一个C++字符串类:从底层理解String的实现
java·jvm·c++
三无少女指南2 天前
深入理解JVM的安全点与安全区域
jvm·安全
Paraverse_徐志斌2 天前
异常日志不打印堆栈?谈谈 JVM 的 Fast Throw
jvm·hotspot·堆栈·npe·fast throw