面试题答案
一键面试异常处理面临的挑战
- 异常安全保证:
- 基本异常安全:在构造函数抛出异常后,对象应该处于一个有效但未完全初始化的状态,不会泄露资源。例如,在构造函数中分配了内存,如果抛出异常,必须确保内存被正确释放。
- 强异常安全:如果构造函数抛出异常,程序状态应保持不变,就像构造函数从未被调用过一样。这对于复杂的继承体系来说更具挑战性,因为派生类可能依赖于基类部分的成功初始化。
- 基类和派生类构造函数之间的协调:
- 如果基类构造函数抛出异常,派生类构造函数的初始化列表可能无法执行完整的初始化。例如,派生类可能依赖于基类初始化的成员变量来进行自身的初始化,基类构造失败会导致派生类构造也无法完成。
- 当派生类构造函数抛出异常时,需要确保基类部分已正确清理(如果基类部分已成功初始化)。
异常处理策略
- 资源管理:使用RAII(Resource Acquisition Is Initialization)原则,通过智能指针(如
std::unique_ptr
和std::shared_ptr
)管理资源。这样在对象析构时,资源会自动释放,避免资源泄露。 - 构造函数设计:
- 基类构造函数应尽可能早地完成可能抛出异常的操作,并且在构造成功后,将对象置于一个有效状态。
- 派生类构造函数在初始化列表中调用基类构造函数,并在自身构造函数体中完成自身特有的初始化。如果派生类构造函数在基类构造成功后抛出异常,应确保清理自身已分配的资源。
- 异常传播:构造函数可以直接抛出异常,让调用者处理。但要注意异常类型的选择,尽量使用标准库中的异常类型或自定义的异常层次结构,以便调用者可以通过
catch
块进行适当的处理。
示例代码
#include <iostream>
#include <memory>
class Base {
public:
Base(int value) {
if (value < 0) {
throw std::invalid_argument("Base constructor: negative value not allowed");
}
data = std::make_unique<int>(value);
std::cout << "Base constructor called" << std::endl;
}
~Base() {
std::cout << "Base destructor called" << std::endl;
}
private:
std::unique_ptr<int> data;
};
class Derived : public Base {
public:
Derived(int baseValue, int derivedValue) : Base(baseValue) {
if (derivedValue < 0) {
throw std::invalid_argument("Derived constructor: negative value not allowed");
}
additionalData = std::make_unique<int>(derivedValue);
std::cout << "Derived constructor called" << std::endl;
}
~Derived() {
std::cout << "Derived destructor called" << std::endl;
}
private:
std::unique_ptr<int> additionalData;
};
int main() {
try {
Derived d(10, -5);
} catch (const std::invalid_argument& e) {
std::cerr << "Caught exception: " << e.what() << std::endl;
}
return 0;
}
在这个示例中:
Base
类的构造函数如果传入负数会抛出std::invalid_argument
异常。如果构造成功,它会使用std::unique_ptr
分配一个整数。Derived
类继承自Base
,它的构造函数首先调用Base
类的构造函数。如果Base
类构造成功,Derived
类继续自身的初始化,如果传入负数也会抛出std::invalid_argument
异常。- 在
main
函数中,尝试创建一个Derived
对象,如果构造函数抛出异常,catch
块会捕获并处理异常,同时由于RAII原则,已分配的资源会被正确释放。