突破编程_C++_高级教程(异常处理的基础知识)

1 异常处理基础

C++ 异常处理是一种处理程序中运行时错误的机制。它是 C++ 语言的一个重要特性,允许程序在遇到异常情况时,通过抛出和捕获异常来转移程序的执行流程,从而实现对错误的集中处理和恢复。

异常处理的概念主要包括三个方面:

(1)异常的抛出(throw): 当程序遇到无法处理的错误时,会抛出一个异常。这个异常是一个对象,包含了关于错误的信息。在 C++ 中,使用 throw 关键字来抛出一个异常。
(2)异常的捕获(catch): 程序的其他部分可以捕获并处理这些异常。在 C++ 中,使用 catch 关键字来捕获异常。同时可以指定要捕获的异常类型,并编写处理该异常的代码。
(3)异常的传递(propagation): 如果在函数内部抛出了异常,且该函数没有捕获该异常,那么异常会被传递给调用该函数的代码。这个过程会一直持续到异常被捕获或程序终止。

异常处理的重要性主要体现在以下几个方面:

(1)错误处理: 异常处理机制允许程序在遇到错误时,以一种结构化的方式处理错误。这有助于减少程序的崩溃和不稳定,提高程序的健壮性。
(2)错误传播: 通过异常的传递,错误可以被传播到程序的更高层次进行处理。这使得程序可以在更合适的地方处理错误,而不是在错误发生的低层次。
(3)简化错误处理代码: 使用异常处理,可以将错误处理代码与正常业务逻辑代码分离,使代码更加清晰和易于维护。
(4)提供有用的错误信息: 异常对象通常包含有关错误的详细信息,如错误类型、错误位置等。这使得在调试和排查错误时更加容易。

总体而言,C++ 异常处理是一种强大的错误处理机制,它有助于提高程序的健壮性、可维护性和可调试性。在编写 C++ 程序时,合理使用异常处理机制是非常重要的。

1.1 异常处理的流程:try-catch-throw

C++ 的异常处理流程主要通过 try、catch 和 throw 三个关键字来实现:

(1)throw(抛出)
当程序遇到无法处理的错误时,它会使用 throw 关键字抛出一个异常。
throw 语句后面通常跟着一个对象实例,该对象包含了关于错误的信息。这个对象可以是任何 C++ 数据类型,包括基本数据类型和自定义类型。
抛出异常后,程序的正常执行流程会被打断,程序的控制权会立即转移到与该异常类型匹配的 catch 块。
(2)try(尝试)
try 块用来包含可能会抛出异常的代码。
通常将可能抛出异常的代码段放在 try 块中,这样当异常被抛出时,程序会知道从哪里开始寻找匹配的 catch 块。
try 块可以包含多个语句,也可以嵌套其他的 try 块。
(3)catch(捕获)
catch 块用来捕获并处理异常。
catch 块后面跟着一对括号,括号内指定了要捕获的异常类型。
如果 try 块中抛出的异常类型与 catch 块中指定的类型匹配,那么该 catch 块的代码将被执行。
可以定义多个 catch 块来捕获不同类型的异常,或者捕获所有类型的异常(通过 catch(…))。

如下为样例代码:

#include <iostream>  
using namespace std;

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

int main() 
{
	int a = 1;
	int b = 0;
	double c = 0;

	try {
		c = division(a, b);
		cout << c << endl;
	}
	catch (const char* e) {
		cerr << e << endl;
	}

	return 0;
}

上面代码的输出为:

Division by zero condition!

在上面代码的 division 函数中,当发现除数为零时,使用 throw 抛出一个异常,异常信息是"Division by zero condition!"。随后程序控制流转到 try 块中的 catch 部分,因为 catch 块捕获了与 throw 抛出的异常类型匹配的异常。

注意事项:

  • 如果没有任何 catch 块能够捕获异常,程序将调用 std::terminate 并终止执行。
  • throw 语句之后的代码不会被执行,直到找到一个匹配的 catch 块或者程序终止。
  • 可以在一个 try 块后面跟随多个 catch 块,以处理不同类型的异常。
  • catch 块按照它们出现的顺序来检查异常类型,因此应该将更具体的异常类型放在前面,将更一般的异常类型(如catch(…))放在后面。

1.2 异常类型与异常对象

在 C++ 中,异常可以是任何数据类型,包括基本数据类型、类类型、结构体、联合体,甚至是指针和数组。当使用 throw 关键字抛出异常时,可以抛出一个值或者一个对象。这个值或对象就是异常对象,它包含了关于异常的信息。

异常类型
异常类型指的是异常对象的数据类型。在 C++ 中,异常类型可以是任何有效的 C++ 数据类型。常见的异常类型包括:

(1)标准异常类型:C++ 标准库提供了一组标准异常类型,这些类型定义在 <exception>头文件中。标准异常类型包括 std::exception 及其派生类,如 std::runtime_error 和 std::logic_error。这些异常类型通常用于报告程序运行时遇到的错误。

(2)自定义异常类型:除了标准异常类型外,你还可以定义自己的异常类型。自定义异常类型通常是类的派生类型,这些类通常从std::exception或其派生类继承,以便能够使用异常处理机制中的通用功能。

异常对象
异常对象是在 throw 语句中抛出的值或对象。当异常被抛出时,它会被传递给最近的匹配的 catch 块进行处理。异常对象可以是任何有效的 C++ 表达式,包括变量、字面量、函数调用结果等。

如下是一个自定义异常类型的样例:

#include <iostream>  
#include <exception>  

// 自定义异常类型  
class MyException : public std::exception 
{
public:
	const char* what() const throw() 
	{
		return "MyException occurred";
	}
};

void func() 
{
	throw MyException(); // 抛出异常对象  
}

int main() 
{
	try {
		func(); // 调用可能抛出异常的函数  
	}
	catch (const MyException& e) { // 捕获自定义异常类型  
		std::cerr << "Caught MyException: " << e.what() << std::endl;
	}
	catch (...) { // 捕获其他类型的异常  
		std::cerr << "Caught unknown exception" << std::endl;
	}
	return 0;
}

在上面代码中,定义了一个名为 MyException 的自定义异常类型,它继承自 std::exception 。在 func 函数中,抛出了一个 MyException 对象。在 main 函数中,使用 try 块来调用 func 函数,并使用 catch 块来捕获并处理 MyException 类型的异常。
然后使用三个点(…)来捕获所有类型的异常。这种捕获方式通常放在最后,作为一个兜底捕获(catch-all),用于捕获未被其他 catch 块捕获的异常。

2 抛出异常

2.1 使用 throw 关键字抛出异常

在 C++ 中,throw 关键字用于抛出一个异常。当程序遇到无法处理的错误或异常情况时(例如无效的参数、资源不足或无法执行请求的操作等),可以使用 throw 来抛出一个异常对象,从而中断当前的执行流程并跳转到与该异常类型匹配的 catch 块。

如下是使用 throw 关键字抛出异常的基本语法:

throw expression;

这里的 expression 是一个表达式,它产生了一个值或对象,这个值或对象就是异常对象。这个对象会被传递给最近的匹配的 catch 块进行处理。
在上面展示的代码样例中,有涉及到使用 throw 关键字的部分:

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

这里 divide 函数接受两个整数作为参数,并尝试执行除法。如果除数为零,则抛出一个类型为 const char* 的异常对象,包含一个描述错误的字符串。

2.2 抛出不同类型的异常

throw 关键字可以抛出任何 C++ 类型的对象作为异常,包括基本数据类型(如 int、char、bool 等)、类类型、结构体、联合体,甚至是指针和数组。通常,为了提供有关异常的更多信息,程序员会抛出自定义的异常类对象,这些对象可能包含错误代码、错误消息或其他有助于诊断问题的信息。

2.2.1 使用标准库异常类型

C++ 标准库提供了两个基本的异常类型:std::exception,它是所有标准异常类型的基类,以及 std::runtime_error 和 std::logic_error,它们都是从 std::exception 派生出来的。

std::runtime_error 通常用于报告程序运行时出现的错误,这些错误通常是由外部因素(如输入错误或资源不足)引起的。
std::logic_error 则用于报告编程逻辑错误,这些错误通常是由程序员的错误(如无效的参数值或错误的函数调用)引起的。

如下为样例代码:

#include <iostream>  
#include <exception>  
#include <stdexcept>  

void func1() 
{
	// 模拟一个运行时错误  
	throw std::runtime_error("An unexpected runtime error occurred.");
}

void func2(int value)
{
	if (value < 0) {
		// 模拟一个逻辑错误  
		throw std::logic_error("Negative value is not allowed.");
	}
}

int main() 
{
	try {
		func1();
		func2(-1);
	}
	catch (const std::runtime_error& e) {
		std::cerr << "Caught runtime_error: " << e.what() << std::endl;
	}
	catch (const std::logic_error& e) {
		std::cerr << "Caught logic_error: " << e.what() << std::endl;
	}
	catch (...) {
		std::cerr << "Caught unknown exception" << std::endl;
	}

	return 0;
}

2.2.2 定义自定义异常类型

在标准库提供的异常类型无法满足需求的情况下,需要定义自己的异常类型来提供更具体的错误信息。自定义异常类型通常通过继承 std::exception 或其派生类来实现。

如下为样例代码:

#include <iostream>  
#include <exception>  
#include <string>  

// 自定义异常类型,继承自 std::runtime_error  
class MyCustomException : public std::runtime_error 
{
public:
	MyCustomException(const std::string& message) : std::runtime_error(message) {}
};

void func() 
{
	// 抛出自定义异常  
	throw MyCustomException("This is a custom exception.");
}

int main() 
{
	try {
		func();
	}
	catch (const MyCustomException& e) {
		std::cerr << "Caught custom exception: " << e.what() << std::endl;
	}
	catch (const std::exception& e) {
		std::cerr << "Caught generic exception: " << e.what() << std::endl;
	}
	catch (...) {
		std::cerr << "Caught unknown exception" << std::endl;
	}

	return 0;
}

在上面代码中,定义了一个名为 MyCustomException 的自定义异常类型,它继承自 std::runtime_error。这个自定义异常类型接受一个字符串参数来初始化错误信息,并通过 std::runtime_error 的构造函数传递给基类。在 main 函数中,捕获了这个自定义异常,并打印了错误消息。

2.2.3 抛出基本数据类型

通常,不建议直接抛出基本数据类型作为异常,因为这样做不会提供有关错误的上下文信息。最好使用异常类,这些类可以提供更多的信息,如错误消息、错误代码或堆栈跟踪。如果确实需要抛出基本数据类型,可以将其封装在一个对象或结构体中,然后抛出该对象或结构体。

如下为样例代码:

#include <iostream>  
#include <exception>  

// 封装基本数据类型的结构体  
struct BasicError 
{
	enum class ErrorCode {
		InvalidValue,
		// 其他错误代码  
	};

	ErrorCode code;
	int value;

	BasicError(ErrorCode code, int value) : code(code), value(value) {}
};

void func() 
{
	// 抛出封装了基本数据类型的异常  
	throw BasicError{ BasicError::ErrorCode::InvalidValue, 1 };
}

int main() 
{
	try {
		func();
	}
	catch (const BasicError& e) {
		std::cerr << "Caught an exception with error code: " << static_cast<int>(e.code)
			<< " and value: " << e.value << std::endl;
	}
	catch (...) {
		std::cerr << "Caught an unknown exception" << std::endl;
	}

	return 0;
}

在这个例子中,定义了一个 BasicError 结构体,它封装了一个 enum class 类型的错误代码和一个 int 类型的值。然后,我们在 func 函数中抛出了一个 BasicError 对象。在 main 函数中,捕获了这个异常,并打印了错误代码和值。

2.3 抛出异常时的栈展开

在 C++ 中,当抛出异常时,会发生栈展开(Stack Unwinding)的过程。这个过程涉及到从抛出异常的位置开始,逐步销毁(析构)栈上创建的所有对象,直到到达可以处理该异常的catch块为止。

栈展开的过程大致如下:

(1)异常抛出: 当throw语句被执行时,程序会创建一个异常对象(如果throw后面跟着一个表达式),并将这个对象与当前的异常处理上下文一起推送到运行时系统。

(2)查找匹配的catch块: 运行时系统开始从抛出异常的位置向上遍历调用栈,查找匹配的catch块。这个查找过程是根据异常对象的类型进行的,只有当异常对象的类型与catch块参数类型匹配时,才会停止查找。

(3)栈展开:在查找过程中, 每经过一个栈帧(即一个函数调用),运行时系统都会调用该栈帧中局部对象的析构函数,以释放资源。这个过程就是栈展开。如果某个对象在其析构函数中抛出了另一个异常,那么当前的异常处理过程会被暂停,转而处理新抛出的异常。这种情况称为异常嵌套(Exception Nesting)。

(4)执行匹配的catch块: 一旦找到了匹配的catch块,运行时系统就会跳转到该catch块的代码处执行。在catch块中,你可以对异常进行处理,例如打印错误信息、恢复程序状态等。

(5)异常处理结束: 当catch块执行完毕后,程序会继续执行后续的代码,或者返回到抛出异常的地方(如果异常被重新抛出)。

需要注意的是,栈展开过程可能会导致程序的性能开销增加,因为需要销毁大量的对象。因此,在设计程序时,应该尽量避免抛出大量的异常,以及尽量减少局部对象的数量,以提高程序的性能。

此外,由于栈展开涉及到对象的析构函数,因此在设计类时,应该确保析构函数的正确性,避免在析构函数中抛出异常或执行可能抛出异常的代码,以防止异常嵌套的发生。

2.4 抛出异常的最佳实践

虽然抛出和处理异常是一种强大的错误处理机制。但是如果不当使用,它可能会导致性能下降、代码复杂性增加或难以调试的问题。以下是关于 C++ 中抛出异常的一些最佳实践:

(1)异常类型选择:
使用标准异常类型(如 std::runtime_error、std::logic_error 等)来处理常见的错误情况。
对于特定于应用程序的错误,定义自己的异常类型,这些类型应该继承自 std::exception 以允许使用基类指针或引用捕获所有异常。

(2)异常消息:
在抛出异常时,建议提供一个有意义的异常消息。这个消息应该描述发生了什么错误以及可能的原因。
可以使用 std::string 或 const char* 作为异常消息的类型,以提供足够的灵活性来包含详细的错误信息。

(3)避免过度使用异常:
异常处理是有成本的(比如上面讲到的"抛出异常时的栈展开"),因此在不应该使用异常的情况下避免使用它们。例如,对于可以通过返回值或错误码来表示的错误情况,就不要使用异常。
另外也要避免在性能关键的代码路径中抛出异常,因为这可能会导致性能下降。

(4)异常安全性:
在抛出异常之前,确保所有资源都已经正确释放或转移。这也被称为异常安全性(有助于防止资源泄漏)。
可以使用智能指针(如 std::unique_ptr 和 std::shared_ptr)来自动管理资源,以减少资源泄漏的风险。

(5)异常传播:
在捕获异常后,如果不对其进行处理,应该重新抛出它(使用 throw; 语句)。这允许异常继续向上传播,直到找到适当的处理程序。
避免在 catch 块中忽略异常,这可能会导致难以调试的问题。

(6)异常捕获:
尽可能具体地捕获异常。使用引用捕获异常(如 catch(const std::runtime_error& e)),以便可以访问异常对象的任何额外信息。
在多个 catch 块中捕获不同类型的异常,以处理不同类型的错误情况。

(7)日志记录:
在捕获异常时,考虑记录异常信息到日志文件中。这有助于在调试和监控应用程序时识别问题。

(8)异常处理代码清晰性:
将异常处理代码与正常逻辑代码分开,以提高代码的可读性和可维护性。
可以使用 try 块来封装可能抛出异常的代码,以明确指示异常可能发生的位置。

(9)测试:
确保对异常处理代码进行充分的测试,包括测试异常是否被正确抛出、捕获和处理。
使用断言和模拟来测试异常路径,以确保它们在各种情况下都能正确工作。

遵循这些最佳实践可以帮助编写健壮、可维护和高效的 C++ 代码,同时充分利用异常处理机制的优势。

3 捕获异常

3.1 使用 catch 块捕获异常

catch 块用于捕获 try 块中抛出的异常。catch 块通过指定异常类型来捕获并处理特定类型的异常。当 try 块中的代码抛出一个异常时,程序会查找匹配的 catch 块来处理该异常。

如下为样例代码:

#include <iostream>  
#include <stdexcept> // 包含标准异常类的头文件  

int main() 
{
	try {
		// 尝试执行可能抛出异常的代码  
		throw std::runtime_error("An error occurred!");
	}
	catch (const std::runtime_error& e) {
		// 捕获类型为 std::runtime_error 的异常,并处理它  
		std::cerr << "Caught exception: " << e.what() << std::endl;
	}
	catch (...) {
		// 捕获所有其他类型的异常  
		std::cerr << "Caught unknown exception." << std::endl;
	}

	return 0;
}

上面代码的输出为:

Caught exception: An error occurred!

在上面代码中,try 块中的代码抛出了一个 std::runtime_error 异常。然后,程序查找匹配的 catch 块来处理这个异常。第一个 catch 块尝试捕获类型为 std::runtime_error 的异常,并打印出异常的消息。如果 try 块抛出了其他类型的异常,那么程序会尝试匹配第二个 catch 块,这个块捕获所有其他类型的异常。

注意:catch块的顺序很重要。在上面的例子中,如果将捕获所有异常的 catch 块(catch (…))放在前面,那么它总是会匹配,因为 … 可以匹配任何类型的异常。因此,通常建议将更具体的 catch 块放在前面,而将捕获所有异常的 catch 块放在最后,作为一个兜底选项。

3.2 异常的重新抛出

在 C++ 中,可以在 catch 块中重新抛出捕获到的异常。这通常在需要将异常传播到更高的调用层次时发生。重新抛出异常使用 throw; 语句(在没有指定任何参数情况下,异常对象会保持其原始类型和值),调用栈会继续展开,直到找到另一个 catch 块来处理该异常。

如下为样例代码:

#include <iostream>  
#include <stdexcept>  

void errorThrow() 
{
	// 这里可能会抛出异常  
	throw std::runtime_error("An error occurred in errorThrow()");
}

void handleException() 
{
	try {
		errorThrow();
	}
	catch (const std::exception& e) {
		// 在这里处理异常,例如记录日志  
		std::cerr << "Exception caught: " << e.what() << std::endl;

		// 重新抛出异常,以便在更高的层次处理  
		throw; // 没有指定异常对象,所以重新抛出当前捕获的异常  
	}
}

int main() 
{
	try {
		handleException();
	}
	catch (const std::exception& e) {
		// 在main函数中处理异常  
		std::cerr << "Exception handled in main: " << e.what() << std::endl;
		return 1; // 指示程序异常退出  
	}

	return 0; // 正常退出  
}

上面代码的输出为:

Exception caught: An error occurred in errorThrow()
Exception handled in main: An error occurred in errorThrow()

在上面代码中,errorThrow 函数抛出一个 std::runtime_error 异常。handleException 函数调用 errorThrow,并在 catch 块中捕获任何 std::exception 类型的异常。在 catch 块中,异常被处理(例如记录日志),然后使用 throw; 语句重新抛出。

main 函数中的另一个 try-catch 块捕获这个重新抛出的异常,并做进一步的处理。如果 handleException 函数没有重新抛出异常,那么 main 函数中的 catch 块将不会捕获到异常,程序将终止。

重新抛出异常的一个常见应用场景,在某个层次上执行一些通用的异常处理(如日志记录)的同时,仍然希望上层调用者有机会处理这个异常。通过重新抛出异常,可以保持异常的传播机制,同时允许上层代码执行额外的处理或决定程序的最终行为。

  • 33
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值