在C++程序开发中,动态内存管理如果处理不当,很容易在异常抛出时出现资源泄露问题,智能指针通过RAII机制封装资源生命周期,是实现异常安全的重要手段,其自身的操作也需要保证异常安全才能发挥最大作用。

智能指针异常安全的核心基础
智能指针的异常安全本质是遵循RAII原则,即资源获取即初始化,把资源的管理和对象的生命周期绑定。当异常抛出时,栈上的智能指针对象会自动析构,从而释放其管理的资源,避免资源泄露。要实现这一点,智能指针的构造、析构、拷贝、赋值等操作都需要满足异常安全的要求。
构造函数的异常安全
智能指针构造时如果传入动态分配的资源,需要保证资源在构造失败时不会泄露。如果构造函数中除了获取资源之外还有其他可能抛异常的操作,需要先获取资源再执行其他步骤,或者在构造失败时主动释放已获取的资源。
#include <memory>
#include <iostream>
// 自定义简单智能指针示例
template <typename T>
class SimpleSmartPtr {
private:
T* ptr;
public:
// 构造函数,直接接管传入的资源,不执行其他可能抛异常的操作
explicit SimpleSmartPtr(T* p = nullptr) noexcept : ptr(p) {}
// 析构函数释放资源
~SimpleSmartPtr() {
delete ptr;
}
// 禁止拷贝构造和赋值,避免资源重复释放
SimpleSmartPtr(const SimpleSmartPtr&) = delete;
SimpleSmartPtr& operator=(const SimpleSmartPtr&) = delete;
// 移动构造,异常安全
SimpleSmartPtr(SimpleSmartPtr&& other) noexcept : ptr(other.ptr) {
other.ptr = nullptr;
}
T& operator*() const { return *ptr; }
T* operator->() const { return ptr; }
T* get() const { return ptr; }
};
int main() {
try {
// 构造时直接传入new的资源,构造函数无额外抛异常操作,保证安全
SimpleSmartPtr<int> ptr(new int(10));
std::cout << *ptr << std::endl;
// 即使这里抛异常,ptr析构时会释放int资源
throw std::runtime_error("test exception");
} catch (const std::exception& e) {
std::cout << "catch exception: " << e.what() << std::endl;
}
return 0;
}
拷贝与赋值操作的异常安全
如果智能指针支持拷贝语义,比如std::shared_ptr,拷贝时需要修改引用计数,赋值操作需要先释放旧资源再接管新资源,这些步骤都需要保证异常安全。通常的做法是先准备好新资源,再替换旧资源,保证操作要么完全成功,要么完全失败,不出现中间状态。
以std::shared_ptr的赋值运算符为例,其实现逻辑大致如下:先增加新资源的引用计数,再减少旧资源的引用计数,如果减少后引用计数为0就释放旧资源。增加新引用计数的操作放在前面,即使后续步骤抛异常,新资源的引用计数已经正确,不会出现资源提前释放的问题。
#include <iostream>
#include <atomic>
// 简化的shared_ptr引用计数实现示例
template <typename T>
class SimpleSharedPtr {
private:
T* ptr;
std::atomic<int>* ref_count;
public:
// 构造
explicit SimpleSharedPtr(T* p = nullptr) : ptr(p), ref_count(new std::atomic<int>(p ? 1 : 0)) {}
// 拷贝构造,异常安全:先增加引用计数,再赋值
SimpleSharedPtr(const SimpleSharedPtr& other) noexcept : ptr(other.ptr), ref_count(other.ref_count) {
if (ptr) {
ref_count->fetch_add(1, std::memory_order_relaxed);
}
}
// 赋值运算符,异常安全实现
SimpleSharedPtr& operator=(const SimpleSharedPtr& other) {
if (this != &other) {
// 先处理新资源,增加引用计数,这一步如果抛异常也不会影响当前对象
T* new_ptr = other.ptr;
std::atomic<int>* new_ref = other.ref_count;
if (new_ptr) {
new_ref->fetch_add(1, std::memory_order_relaxed);
}
// 再处理旧资源,减少引用计数,为0则释放
if (ptr) {
if (ref_count->fetch_sub(1, std::memory_order_acq_rel) == 1) {
delete ptr;
delete ref_count;
}
}
// 最后替换成员
ptr = new_ptr;
ref_count = new_ref;
}
return *this;
}
// 析构
~SimpleSharedPtr() {
if (ptr && ref_count->fetch_sub(1, std::memory_order_acq_rel) == 1) {
delete ptr;
delete ref_count;
}
}
T& operator*() const { return *ptr; }
T* operator->() const { return ptr; }
};
int main() {
SimpleSharedPtr<int> p1(new int(20));
SimpleSharedPtr<int> p2;
try {
// 赋值操作异常安全,即使后续抛异常,p1的资源也不会泄露
p2 = p1;
std::cout << *p2 << std::endl;
throw std::runtime_error("assign exception");
} catch (const std::exception& e) {
std::cout << "catch: " << e.what() << std::endl;
}
return 0;
}
常见异常安全场景的处理
函数中智能指针作为参数传递
当智能指针作为函数参数按值传递时,会触发拷贝构造,只要智能指针的拷贝构造是异常安全的,就不会有问题。如果按引用传递,需要注意函数内部是否会修改智能指针的状态,避免异常导致资源被意外释放。
智能指针管理数组资源
如果使用智能指针管理数组,需要指定删除器,比如std::unique_ptr<int[], std::default_delete<int[]>>,或者自定义删除器,保证析构时使用delete[]而不是delete,否则会出现未定义行为,即使没有异常也会有问题。
#include <memory>
#include <iostream>
int main() {
try {
// 管理数组的智能指针,指定删除器为delete[]
std::unique_ptr<int[], std::default_delete<int[]>> arr_ptr(new int[5]{1,2,3,4,5});
for (int i = 0; i < 5; ++i) {
std::cout << arr_ptr[i] << " ";
}
std::cout << std::endl;
throw std::runtime_error("array exception");
} catch (const std::exception& e) {
std::cout << "catch: " << e.what() << std::endl;
}
return 0;
}
异常安全等级与智能指针
C++的异常安全分为三个等级:基本异常安全(异常抛出后程序处于有效状态,没有资源泄露)、强异常安全(操作要么完全成功,要么回到操作前的状态)、不抛异常保证。智能指针的析构函数通常保证不抛异常,这是其能实现异常安全的基础。我们在实现自定义智能指针时,也要保证析构函数不抛异常,否则在栈展开时抛异常会导致程序终止。
注意:智能指针的get()方法返回原始指针,如果通过原始指针手动释放资源,会破坏智能指针的异常安全机制,导致资源重复释放或者泄露,因此不要随意使用get()获取原始指针后手动管理资源。
总的来说,实现C++智能指针的异常安全操作,核心是遵循RAII原则,保证资源获取和释放的配对,操作过程中先准备新状态再替换旧状态,析构函数不抛异常,同时避免手动干预智能指针管理的资源,就能有效避免异常场景下的资源问题。