第三章 异常(一)

第三章 异常(一)

条款9:利用destructors避免泄露资源

一、核心概念解析

首先,我们要理解这个条款解决的核心问题:手动管理资源(如内存、文件句柄、网络连接等)时,容易因忘记释放、程序提前退出(如异常)等原因导致资源泄露

C++ 的析构函数(destructor)有一个关键特性:当一个对象的生命周期结束(如离开作用域、被 delete)时,其析构函数会自动调用。利用这个特性,我们可以将资源的释放逻辑封装到析构函数中,让资源的生命周期与对象绑定 ------ 这就是 RAII(Resource Acquisition Is Initialization,资源获取即初始化)的核心思想。

问题场景:手动管理资源的风险

先看一个反例,直观感受资源泄露的问题:

c++ 复制代码
#include <iostream>
#include <string>
// 模拟一个需要手动释放的资源(如动态内存)
void createResource(std::string*& ptr) {
    ptr = new std::string("我是需要释放的资源");
}

void releaseResource(std::string* ptr) {
    delete ptr;
    ptr = nullptr;
}

void riskyFunction(bool throwException) {
    std::string* res = nullptr;
    createResource(res); // 获取资源

    // 模拟业务逻辑:如果抛出异常,后续的releaseResource不会执行
    if (throwException) {
        throw std::runtime_error("业务逻辑异常");
    }

    // 即使没有异常,也可能忘记写这行,导致内存泄露
    releaseResource(res);
}

int main() {
    try {
        riskyFunction(true); // 传入true触发异常,资源泄露
    } catch (const std::exception& e) {
        std::cout << "捕获异常:" << e.what() << std::endl;
    }
    // 程序结束后,res指向的内存未被释放,发生泄露
    return 0;
}

问题分析

1.如果riskyFunction中抛出异常,releaseResource不会执行,资源泄露;

2.即使没有异常,手动调用releaseResource容易遗漏,导致泄露;

3.代码需要手动配对 "获取 - 释放",心智负担重。

二、解决方案:用析构函数自动释放资源

我们可以封装一个资源管理类,在构造函数中获取资源,析构函数中释放资源。只要这个类的对象离开作用域,析构函数就会自动调用,资源被释放,从根本上避免泄露。

代码示例:实现一个简单的资源管理类
c++ 复制代码
#include <iostream>
#include <string>
#include <stdexcept>

// 资源管理类:遵循RAII原则
class ResourceGuard {
private:
    std::string* m_resource; // 管理的资源(这里以动态字符串为例)
public:
    // 构造函数:获取资源(资源获取即初始化)
    explicit ResourceGuard(const std::string& content) 
        : m_resource(new std::string(content)) {
        std::cout << "资源已获取,地址:" << m_resource << std::endl;
    }

    // 析构函数:自动释放资源(无论正常退出还是异常退出)
    ~ResourceGuard() {
        if (m_resource != nullptr) {
            delete m_resource;
            m_resource = nullptr;
            std::cout << "资源已释放" << std::endl;
        }
    }

    // 禁用拷贝构造和拷贝赋值(避免浅拷贝导致重复释放)
    ResourceGuard(const ResourceGuard&) = delete;
    ResourceGuard& operator=(const ResourceGuard&) = delete;

    // 提供访问资源的接口(可选)
    std::string& getResource() const {
        if (m_resource == nullptr) {
            throw std::runtime_error("资源已释放");
        }
        return *m_resource;
    }
};

// 安全的函数:使用资源管理类
void safeFunction(bool throwException) {
    // 创建资源管理对象,构造函数获取资源
    ResourceGuard guard("我是受保护的资源");

    // 模拟业务逻辑:即使抛出异常,guard的析构函数仍会执行
    if (throwException) {
        throw std::runtime_error("业务逻辑异常,但资源不会泄露");
    }

    // 正常使用资源
    std::cout << "资源内容:" << guard.getResource() << std::endl;
    // 函数结束时,guard离开作用域,析构函数自动释放资源
}

int main() 
{
    try 
    {
        safeFunction(true); // 触发异常
    } 
    catch (const std::exception& e)
    {
        std::cout << "捕获异常:" << e.what() << std::endl;
    }

    std::cout << "程序正常结束" << std::endl;
    return 0;
}
关键细节解释:

1.RAII 核心ResourceGuard的构造函数负责 "获取资源",析构函数负责 "释放资源",资源的生命周期与guard对象绑定;

2.异常安全 :即使safeFunction抛出异常,guard对象的析构函数仍会被调用(C++ 保证栈上对象的析构函数在异常展开时执行),资源不会泄露;

3.禁用拷贝 :如果允许拷贝,多个ResourceGuard对象会管理同一份资源,析构时会重复释放导致崩溃,因此禁用拷贝构造和拷贝赋值(C++11 后也可使用移动语义);

4.通用性 :这个思路不仅适用于内存,还适用于文件句柄、锁、网络连接等所有需要手动释放的资源(比如std::fstream自动关闭文件、std::lock_guard自动释放锁,都是这个原理)。

进阶:使用标准库的智能指针(更推荐)

实际开发中,我们不需要自己写资源管理类,C++ 标准库提供了现成的智能指针(std::unique_ptr/std::shared_ptr),它们的底层就是利用析构函数自动释放资源:

C++ 复制代码
#include <iostream>
#include <string>
#include <memory> // 包含智能指针头文件
#include <stdexcept>

void smarterFunction(bool throwException) {
    // std::unique_ptr:独占式智能指针,析构时自动delete
    std::unique_ptr<std::string> res = std::make_unique<std::string>("智能指针管理的资源");

    if (throwException) {
        throw std::runtime_error("异常发生,但智能指针会自动释放资源");
    }

    std::cout << "资源内容:" << *res << std::endl;
}

int main() {
    try {
        smarterFunction(true);
    } catch (const std::exception& e) {
        std::cout << "捕获异常:" << e.what() << std::endl;
    }
    return 0;
}

std::unique_ptr是条款 9 的最佳实践落地 ------ 它完全遵循 RAII,无需手动管理,且性能几乎与裸指针一致。

总结

  1. 核心思想:将资源的释放逻辑封装到析构函数中,利用析构函数 "自动调用" 的特性,避免手动释放资源的遗漏或异常导致的泄露(RAII 原则);
  2. 关键做法:不要直接管理裸资源,而是用对象(如自定义资源管理类、标准库智能指针)包裹资源,让对象的生命周期与资源绑定;
  3. 实践推荐 :优先使用 C++ 标准库提供的智能指针(std::unique_ptr/std::shared_ptr),而非手写资源管理类,避免重复造轮子且更安全。

条款10:在constructors内阻止资源泄露(resource leak)

一、核心问题:构造函数的特殊性

C++ 的构造函数没有返回值,且如果在构造过程中抛出异常,当前对象的析构函数不会被调用 。这意味着:如果构造函数中分配了资源(如动态内存、文件句柄、锁、网络连接等),但在资源分配后、构造完成前抛出了异常,这些已分配的资源就无法被析构函数释放,从而导致资源泄露

二、解决方案:RAII(资源获取即初始化)

条款 10 的核心解决方案是RAII(Resource Acquisition Is Initialization):将资源的生命周期绑定到对象的生命周期 ------ 资源在对象构造时获取,在对象析构时释放。具体来说:

1.把资源封装到独立的 "资源管理类" 中;

2.在构造函数中只创建资源管理类的对象,而非直接操作裸资源;

3.即使构造函数抛出异常,资源管理类的析构函数仍会被调用,从而保证资源释放。

三、代码示例:反例(有资源泄露)+ 正例(无泄露)

c++ 复制代码
#include <iostream>
#include <stdexcept>
using namespace std;

// 模拟一个需要手动释放的资源(如动态内存、文件句柄)
class Resource {
public:
    Resource() { cout << "Resource 分配成功\n"; }
    ~Resource() { cout << "Resource 释放成功\n"; } // 析构释放资源
    void use() const { /* 资源使用逻辑 */ }
};

// 有资源泄露风险的类
class BadClass {
private:
    Resource* res1; // 裸指针管理资源1
    Resource* res2; // 裸指针管理资源2
public:
    BadClass() {
        // 第一步:分配资源1(成功)
        res1 = new Resource();
        
        // 第二步:模拟构造过程中抛出异常(比如资源2分配失败、逻辑错误)
        throw runtime_error("构造函数执行中发生异常");
        
        // 第三步:分配资源2(永远不会执行)
        res2 = new Resource();
    }

    ~BadClass() {
        // 析构函数不会被调用!因为构造函数抛异常,对象未完全构造
        delete res1;
        delete res2;
        cout << "BadClass 析构,释放所有资源\n";
    }
};

int main() {
    try {
        BadClass obj; // 构造时抛异常
    } catch (const exception& e) {
        cout << "捕获异常:" << e.what() << endl;
    }
    // 输出:Resource 分配成功 → 捕获异常 → 无"Resource 释放成功"
    // 结论:res1的资源永远无法释放,造成泄露
    return 0;
}
2. 正例:用 RAII 封装资源(解决泄露)

核心思路:用智能指针(如 std::unique_ptr) 替代裸指针 ------ 智能指针是 RAII 的典型实现,其析构函数会自动释放管理的资源,即使构造函数抛异常。

C++ 复制代码
#include <iostream>
#include <stdexcept>
#include <memory> // 包含智能指针头文件
using namespace std;

// 待管理的资源(同上)
class Resource {
public:
    Resource() { cout << "Resource 分配成功\n"; }
    ~Resource() { cout << "Resource 释放成功\n"; }
    void use() const { /* 资源使用逻辑 */ }
};

// 安全的类:用RAII(智能指针)管理资源
class GoodClass {
private:
    // 用std::unique_ptr(独占所有权)替代裸指针,自动管理资源
    unique_ptr<Resource> res1;
    unique_ptr<Resource> res2;
public:
    GoodClass() {
        // 第一步:分配资源1(封装到unique_ptr中)
        res1 = make_unique<Resource>(); // C++14及以上,等价于 unique_ptr<Resource>(new Resource())
        
        // 第二步:模拟构造过程中抛异常
        throw runtime_error("构造函数执行中发生异常");
        
        // 第三步:分配资源2(不会执行)
        res2 = make_unique<Resource>();
    }

    ~GoodClass() {
        // 即使析构函数不手动释放,unique_ptr也会自动释放资源
        cout << "GoodClass 析构\n";
    }
};

int main() {
    try {
        GoodClass obj; // 构造时抛异常
    } catch (const exception& e) {
        cout << "捕获异常:" << e.what() << endl;
    }
    // 输出:Resource 分配成功 → 捕获异常 → Resource 释放成功
    // 结论:res1的资源被unique_ptr的析构函数自动释放,无泄露
    return 0;
}
3. 扩展:自定义 RAII 资源管理类(理解底层原理)

如果需要管理非内存资源(如文件句柄、锁),可以自定义 RAII 类:

C++ 复制代码
#include <iostream>
#include <stdexcept>
#include <cstdio> // FILE相关头文件
using namespace std;

// 自定义RAII类:管理文件句柄(非内存资源)
class FileHandle {
private:
    FILE* file; // 裸句柄(仅在RAII类内部使用)
public:
    // 构造:获取资源(打开文件)
    FileHandle(const char* filename, const char* mode) {
        file = fopen(filename, mode);
        if (!file) {
            throw runtime_error("文件打开失败");
        }
        cout << "文件 " << filename << " 打开成功\n";
    }

    // 析构:释放资源(关闭文件)
    ~FileHandle() {
        if (file) {
            fclose(file);
            cout << "文件关闭成功\n";
        }
    }

    // 禁用拷贝(避免资源重复释放)
    FileHandle(const FileHandle&) = delete;
    FileHandle& operator=(const FileHandle&) = delete;

    // 提供资源访问接口
    FILE* get() const { return file; }
};

// 使用自定义RAII类的业务类
class FileProcessor {
private:
    FileHandle fh; // RAII对象,绑定文件资源
public:
    FileProcessor(const char* filename) : fh(filename, "w") {
        // 模拟构造过程中抛异常
        throw runtime_error("FileProcessor构造异常");
    }
};

int main() {
    try {
        FileProcessor fp("test.txt");
    } catch (const exception& e) {
        cout << "捕获异常:" << e.what() << endl;
    }
    // 输出:文件 test.txt 打开成功 → 捕获异常 → 文件关闭成功
    // 结论:即使构造抛异常,FileHandle的析构仍会执行,文件句柄无泄露
    return 0;
}

总结

  1. 核心风险:构造函数抛异常时,对象未完全构造,析构函数不会执行,直接管理的裸资源会泄露;
  2. 核心方案 :采用RAII思想,将资源封装到 "资源管理类" 中(如 std::unique_ptr、std::shared_ptr,或自定义 RAII 类),利用资源管理类的析构函数自动释放资源;
  3. 最佳实践 :在构造函数中避免直接操作裸资源,优先使用标准库提供的智能指针,自定义资源(如文件、锁)需封装为独立 RAII 类,杜绝构造过程中的资源泄露。

关键点回顾:

  • 构造函数抛异常 → 析构函数不执行 → 裸资源泄露;
  • RAII:资源绑定到对象生命周期,构造获取、析构释放;
  • 智能指针是 RAII 的 "现成方案",自定义 RAII 类适配非内存资源。
相关推荐
苦藤新鸡2 小时前
14.合并区间(1,3)(2,5)=(1,5)
c++·算法·leetcode·动态规划
_OP_CHEN2 小时前
【算法基础篇】(四十八)突破 IO 与数值极限:快速读写 +__int128 实战指南
c++·算法·蓝桥杯·算法竞赛·快速读写·高精度算法·acm/icpc
玖釉-2 小时前
[Vulkan 实战] 深入解析 Vulkan Compute Shader:实现高效 N-Body 粒子模拟
c++·windows·图形渲染
云泽8082 小时前
深入浅出 C++ 继承:从基础概念到模板、转换与作用域的实战指南
开发语言·c++
a***59262 小时前
C++跨平台开发:挑战与实战指南
c++·c#
十五年专注C++开发2 小时前
CMake进阶:模块模式示例FindOpenCL.cmake详解
开发语言·c++·cmake·跨平台编译
Yupureki3 小时前
《算法竞赛从入门到国奖》算法基础:入门篇-离散化
c语言·数据结构·c++·算法·visual studio
散峰而望3 小时前
OJ 题目的做题模式和相关报错情况
java·c语言·数据结构·c++·vscode·算法·visual studio code
疋瓞3 小时前
C/C++查缺补漏《5》_智能指针、C和C++中的数组、指针、函数对比、C和C++中内存分配概览
java·c语言·c++