面试题 1 :什么是异常处理?为什么需要它?
在C++中,异常处理是一种处理程序运行时错误的机制。它允许程序员在程序的某个部分中定义和处理可能会出现的异常情况,即“异常”。这些异常情况通常是由错误条件、非法操作或其他意外情况引起的。
异常处理主要包括以下三个部分:
(1)抛出异常(Throw): 当程序遇到错误或异常情况时,它会使用throw关键字抛出一个异常。throw关键字后面通常跟着一个值,这个值可以是任何数据类型,它包含了关于异常的信息。
(2)捕获异常(Catch): catch 关键字用于捕获异常。它定义了一个或多个代码块,每个代码块处理特定类型的异常。当异常被抛出时,程序会查找匹配的 catch 块来处理该异常。
(3)异常规格(Exception Specification): 这是 C++98 中引入的一个特性,用于声明函数可能抛出的异常类型。然而,在 C++11 及以后的版本中,异常规格已被弃用,推荐使用 noexcept 关键字来声明函数是否抛出异常。
为什么需要异常处理:
(1)错误处理: 异常处理提供了一种统一的机制来处理错误。相比于传统的错误代码返回值,异常处理更加直观和易于维护。
(2)资源管理: 当对象在创建或使用过程中分配了资源(如内存、文件句柄等),并且这些资源需要在对象生命周期结束时释放时,异常处理可以确保在发生异常时这些资源得到正确释放,防止资源泄漏。
(3)代码清晰: 通过将错误处理代码从正常业务逻辑代码中分离出来,异常处理可以保持代码更加清晰和易读。
(4)错误传播: 异常可以被抛出并向上传播到调用栈的更高级别,这样可以在更高层次上处理错误,而不是在每个可能出错的地方都进行错误检查。
(5)程序健壮性: 通过妥善处理异常,可以提高程序的健壮性和稳定性,减少程序崩溃或未定义行为的可能性。
总体而言,异常处理是 C++ 中一个强大的错误处理机制,它有助于编写更加健壮、清晰和可维护的代码。
面试题 2 :可以抛出什么类型的异常?
在 C++ 中,可以抛出任何类型的异常。这包括基本数据类型(如int、char、bool等)、复合数据类型(如结构体、类等)、指针类型,甚至是异常类型本身(即派生自 std::exception 的异常类)。
通常会选择抛出派生自 std::exception 的异常类型,因为这样做可以提供更多的上下文信息,并允许使用 catch 块捕获所有类型的异常(通过捕获 std::exception 类型的引用或指针)。
下面是一些示例:
(1)抛出基本数据类型:
throw 1; // 抛出一个整数
(2)抛出复合数据类型:
struct MyError
{ int errorCode; const char* errorMessage;
}; void funcThrowError()
{ MyError error = {100, "Internal Server Error"}; throw error; // 抛出一个自定义的结构体
}
(3)抛出指针类型:
void* ptr = new int(2);
throw ptr; // 抛出一个指针
(4)抛出派生自std::exception的异常类型:
#include <exception>
#include <string> class MyException : public std::exception
{
public: const char* what() const throw() { return "MyException occurred"; }
}; void funcThrowError()
{ throw MyException(); // 抛出一个自定义异常类
}
在实践中,建议使用派生自 std::exception 的异常类型,因为它们提供了 what() 成员函数,该函数返回一个描述异常的字符串,这对于调试和错误报告非常有用。
面试题 3 :什么是异常规格(Exception Specification)?
异常规格(Exception Specification)是 C++98 中引入的一种特性,用于声明函数可能抛出的异常类型。它允许程序员指定函数可能抛出的异常类型,并在函数实现中确保这些约束被遵守。然而,需要注意的是,在 C++11 及以后的版本中,异常规格已被弃用,并推荐使用 noexcept 关键字来声明函数是否抛出异常。
异常规格(Exception Specification)用于声明函数可能抛出的异常类型。在函数声明中,可以通过在函数参数列表后面添加 throw(exception_type_list) 来指定函数可能抛出的异常类型。exception_type_list 是一个逗号分隔的异常类型列表。
如下为样例代码:
void func() throw(int, std::exception)
{ // 函数实现 if (/* some condition */) { throw std::runtime_error("An error occurred"); }
}
这表示 func 函数可能抛出 int 类型或 std::exception 类型及其派生类型的异常。如果函数抛出了其他类型的异常,程序将调用 std::unexpected() 函数,除非该函数被 std::set_unexpected() 函数覆盖。
需要注意的是,基于类型的异常规格并不要求函数实际抛出列表中的异常类型,它只是一个声明,用于告知编译器和调用者该函数可能抛出的异常类型。
面试题 4 :说一下你对 noexcept 关键字的理解。
noexcept 是在 C++11 引入的关键字,它提供了一种更简洁且更强大的方式来指定函数是否抛出异常。noexcept关键字可以接受一个可选的常量表达式,用于指示函数是否可能抛出异常。
如下为样例代码:
void func1() noexcept { // 函数实现,保证不抛出异常
} void func2() noexcept(true) { // 函数实现,保证不抛出异常
} void func3() noexcept(false) { // 函数实现,可能抛出异常
}
使用 noexcept 关键字的好处是,如果函数声明为 noexcept 且实际上抛出了异常,编译器会保证调用 std::terminate() 来结束程序,而不是调用 std::unexpected()。此外,noexcept 还允许编译器进行更多的优化,因为它知道函数不会抛出异常。
因此,在现代 C++ 编程中,推荐使用 noexcept 关键字来替代 C++98 中的异常规格。
面试题 5 :如何重新抛出当前捕获的异常?
在C++中,如果在一个 catch 块中捕获了一个异常,并且想要在当前函数或更高级别的函数中重新抛出它,可以使用 throw; 语句(没有带任何参数)来重新抛出该异常。这允许异常继续向上传播,直到它被另一个 catch 块捕获或程序终止。
如下为样例代码:
#include <iostream>
#include <stdexcept> void handleException()
{try {// ... 这里可能会抛出异常的代码 ... throw std::runtime_error("An error occurred");}catch (const std::exception& e) {// 处理异常,例如记录日志 std::cerr << "Caught exception: " << e.what() << std::endl;// 重新抛出异常 throw;}
}int main()
{try {handleException();}catch (const std::exception& e) {// 在主函数中捕获并处理异常 std::cerr << "Main function caught exception: " << e.what() << std::endl;return 1; // 返回一个错误代码 }return 0;
}
上面代码的输出为:
Caught exception: An error occurred
Main function caught exception: An error occurred
在上面代码中,handleException 函数捕获了一个 std::runtime_error 异常,并打印了一条错误消息。然后,它使用 throw; 语句重新抛出了这个异常,使得异常可以继续向上传播到 main 函数中的 catch 块。
注意:当重新抛出异常时,异常的类型和对象(如果是非基本类型)都会保持不变。这意味着在 main 函数中的 catch 块可以捕获到与 handleException 函数中相同的异常类型,并访问其 what() 方法来获取错误消息。
此外,如果想要捕获异常并对其进行处理,然后再抛出一个新的异常,可以使用 throw 语句后跟一个新的异常对象。这通常用于包装原始异常,并添加更多的上下文信息或转换异常类型。例如:
catch (const std::exception& e) { // 处理原始异常 // ... // 抛出一个新的异常,包含原始异常的信息 throw std::runtime_error("An error occurred while processing: " + e.what());
}
面试题 6 :什么是栈展开(Stack Unwinding)?
在C++异常处理中,栈展开(Stack Unwinding)是一个过程,当异常被抛出时,程序会开始从当前执行点回溯(unwind)调用栈,以查找能够处理该异常的catch块。这个过程涉及销毁在抛出异常点之后创建的所有局部对象,释放它们所占用的资源。
栈展开的具体步骤如下:
(1)查找匹配的catch块: 当异常被抛出时,程序会开始从抛出异常的点回溯调用栈,检查每个函数调用的栈帧,看是否有 catch 块可以捕获该异常。这通常是通过检查每个函数的异常规格(如果使用了 C++98 的异常规格)或 catch 块参数类型来完成的。
(2)销毁局部对象: 在回溯过程中,对于每个栈帧,程序会销毁在抛出异常点之后创建的所有局部对象。这包括使用构造函数创建的对象,以及可能通过 new 表达式在栈上分配的对象。局部对象的析构函数会被调用,以释放它们所占用的资源。
(3)继续回溯: 如果当前栈帧中没有匹配的 catch 块,程序会继续回溯调用栈,重复步骤1和2,直到找到匹配的 catch 块或回溯到程序的入口点。
(4)处理异常: 一旦找到了匹配的 catch 块,程序将跳转到该 catch 块的代码,异常得到处理。此时,栈展开过程结束。
栈展开是 C++ 异常处理机制的一个重要组成部分,它确保了当异常发生时,程序能够正确地清理资源,并将控制权转移到能够处理该异常的代码。这也使得异常处理比传统的错误码处理方式更加安全和方便。