Effective C++ 条款13:以对象管理资源(RAII)

Effective C++ 条款13:以对象管理资源(RAII) Effective C 条款13以对象管理资源获得资源后立刻放进管理对象内。管理对象运用析构函数确保资源被释放。RAIIResource Acquisition Is Initialization是 C 资源管理的核心原则。一、资源管理的问题在 C 中手动管理资源容易出错voidprocessFile(constchar*filename){FILE*filefopen(filename,r);// ... 各种操作 ...if(some_error){return;// 糟糕忘记 fclose 了}// ... 更多操作 ...fclose(file);// 如果前面有 return、异常这里不会执行}常见问题问题原因内存泄漏new后没有delete文件句柄泄漏打开后忘记关闭锁未释放加锁后忘记解锁网络连接未关闭异常导致跳过关闭代码数据库事务未提交/回滚提前返回导致事务悬挂二、RAII 原则RAIIResource Acquisition Is Initialization的核心思想资源在构造函数中获得在析构函数中释放。classFileGuard{public:explicitFileGuard(constchar*filename):file_(fopen(filename,r)){if(!file_){throwstd::runtime_error(Failed to open file);}}~FileGuard(){if(file_){fclose(file_);}}FILE*get()const{returnfile_;}private:FILE*file_;};// 使用voidprocessFileSafe(constchar*filename){FileGuardfile(filename);// 资源获取// ... 各种操作 ...if(some_error){return;// 安全FileGuard 的析构函数会自动 fclose}// ... 更多操作 ...}// 函数结束时file 的析构函数自动释放资源三、智能指针RAII 的最佳实践C11 起提供了三种智能指针它们是 RAII 原则的标准实现1. std::unique_ptr独占所有权#includememoryvoiduseUniquePtr(){// 独占所有权不能复制只能移动std::unique_ptrintptr(newint(42));// 自动释放内存autoptr2std::make_uniqueint(100);// C14 推荐// 转移所有权std::unique_ptrintptr3std::move(ptr);// 现在 ptr 为空ptr3 拥有资源// 自定义删除器autofileDeleter[](FILE*f){fclose(f);};std::unique_ptrFILE,decltype(fileDeleter)file(fopen(test.txt,r),fileDeleter);}// 自动调用删除器释放资源2. std::shared_ptr共享所有权#includememoryvoiduseSharedPtr(){// 多个指针共享同一资源std::shared_ptrintptr1std::make_sharedint(42);{std::shared_ptrintptr2ptr1;// 引用计数 1std::cout*ptr2std::endl;// 输出 42}// ptr2 销毁引用计数 -1// ptr1 仍然有效}// ptr1 销毁引用计数为 0释放资源3. std::weak_ptr弱引用#includememoryvoiduseWeakPtr(){std::shared_ptrintsharedstd::make_sharedint(42);std::weak_ptrintweakshared;// 不增加引用计数if(autolockedweak.lock()){// 尝试获取强引用std::cout*lockedstd::endl;}}// 解决循环引用问题四、智能指针对比特性unique_ptrshared_ptrweak_ptr所有权独占共享无弱引用复制禁止允许允许移动允许允许允许引用计数无有不影响计数内存开销最小额外分配控制块最小适用场景独占资源共享资源打破循环引用五、自定义 RAII 类当标准智能指针不满足需求时可以自定义 RAII 类示例1互斥锁管理器#includemutexclassLockGuard{public:explicitLockGuard(std::mutexmutex):mutex_(mutex){mutex_.lock();// 构造时加锁}~LockGuard(){mutex_.unlock();// 析构时解锁}// 禁止复制LockGuard(constLockGuard)delete;LockGuardoperator(constLockGuard)delete;private:std::mutexmutex_;};// 使用std::mutex mtx;voidsafeFunction(){LockGuardlock(mtx);// 加锁// ... 临界区代码 ...}// 自动解锁C11 标准库已提供std::lock_guard和std::unique_lock生产环境优先使用标准库。示例2数据库事务管理器classTransaction{public:explicitTransaction(DatabaseConnectionconn):conn_(conn),committed_(false){conn_.beginTransaction();}voidcommit(){conn_.commit();committed_true;}~Transaction(){if(!committed_){conn_.rollback();// 未提交则回滚}}private:DatabaseConnectionconn_;boolcommitted_;};// 使用voidtransferMoney(Accountfrom,Accountto,doubleamount){Transactiontx(dbConnection);from.withdraw(amount);to.deposit(amount);tx.commit();// 成功提交}// 如果中途异常自动回滚示例3动态数组管理templatetypenameTclassArrayGuard{public:explicitArrayGuard(size_t size):data_(newT[size]),size_(size){}~ArrayGuard(){delete[]data_;}Toperator[](size_t index){returndata_[index];}constToperator[](size_t index)const{returndata_[index];}size_tsize()const{returnsize_;}// 支持移动语义ArrayGuard(ArrayGuardother)noexcept:data_(other.data_),size_(other.size_){other.data_nullptr;other.size_0;}// 禁止复制或实现深拷贝ArrayGuard(constArrayGuard)delete;ArrayGuardoperator(constArrayGuard)delete;private:T*data_;size_t size_;};六、实际应用场景场景1工厂函数返回智能指针#includememoryclassImage{public:voidrender(){}};// 工厂函数 - 返回 unique_ptr 明确转移所有权std::unique_ptrImagecreateImage(conststd::stringpath){autoimgstd::make_uniqueImage();// ... 加载图片 ...returnimg;// 移动语义无需复制}// 使用voiddisplayImage(){autoimgcreateImage(photo.jpg);img-render();}// 自动释放 Image场景2PIMPL 惯用法// Widget.h - 头文件中无需包含完整定义classWidget{public:Widget();~Widget();// 需要显式声明因为 unique_ptr 需要完整类型Widget(Widget)noexcept;// 移动构造Widgetoperator(Widget)noexcept;// 移动赋值voiddoSomething();private:classImpl;// 前向声明std::unique_ptrImplpImpl;// RAII 管理实现};// Widget.cppclassWidget::Impl{public:voiddoSomething(){/* ... */}std::vectorintdata;};Widget::Widget():pImpl(std::make_uniqueImpl()){}Widget::~Widget()default;Widget::Widget(Widget)noexceptdefault;WidgetWidget::operator(Widget)noexceptdefault;voidWidget::doSomething(){pImpl-doSomething();}场景3资源池管理#includememory#includestackclassConnection{public:voidquery(conststd::stringsql){}};classConnectionPool{public:usingConnectionPtrstd::shared_ptrConnection;ConnectionPtracquire(){std::lock_guardstd::mutexlock(mutex_);if(!available_.empty()){autoconnavailable_.top();available_.pop();// 自定义删除器归还连接而非真正释放returnConnectionPtr(conn.get(),[this](Connection*c){this-release(c);});}returnstd::make_sharedConnection();}private:voidrelease(Connection*conn){std::lock_guardstd::mutexlock(mutex_);available_.push(std::shared_ptrConnection(conn,[](Connection*){}));}std::stackstd::shared_ptrConnectionavailable_;std::mutex mutex_;};七、常见陷阱与注意事项陷阱1不要混合使用原始指针和智能指针voidbadPractice(){int*rawnewint(42);std::shared_ptrintptr1(raw);std::shared_ptrintptr2(raw);// 危险两个独立的引用计数}// double free// 正确做法voidgoodPractice(){autoptr1std::make_sharedint(42);autoptr2ptr1;// 共享引用计数}陷阱2避免循环引用classB;// 前向声明classA{public:std::shared_ptrBb_ptr;// 强引用};classB{public:std::shared_ptrAa_ptr;// 强引用 - 循环引用};// 正确做法其中一个使用 weak_ptrclassBFixed{public:std::weak_ptrAa_ptr;// 弱引用打破循环};陷阱3不要在构造函数中暴露 this 指针给智能指针classBadExample:publicstd::enable_shared_from_thisBadExample{public:BadExample(){// 危险对象尚未构造完成std::shared_ptrBadExampleptr(this);// UB}};// 正确做法classGoodExample:publicstd::enable_shared_from_thisGoodExample{public:std::shared_ptrGoodExamplegetShared(){returnshared_from_this();// 安全使用}};八、总结请记住获得资源后立刻放进管理对象如智能指针管理对象运用析构函数确保资源被释放推荐使用std::unique_ptr管理独占资源推荐使用std::shared_ptr管理共享资源优先使用std::make_unique和std::make_shared创建智能指针自定义 RAII 类时注意复制行为的设计参见条款14RAII 是 C 最核心的设计原则之一。通过将资源生命周期绑定到对象生命周期我们可以写出异常安全、资源安全的代码彻底告别内存泄漏和资源泄漏的烦恼。现代 C 的智能指针和容器已经很好地实践了 RAII优先使用标准库设施必要时再自定义资源管理类。参考阅读《Effective C》第3版Scott Meyers条款13《Effective Modern C》Scott MeyersC Core Guidelines: R.1 - R.30 (Resource Management)cppreference.com: std::unique_ptr, std::shared_ptr, std::weak_ptr