讲师中心 微信公众号
AI工具推荐 视频效率加速

C++如何使用RAII管理资源和内存

浅敏酱_4926

浅敏酱_4926

发布时间:2025-09-20 15:53:01

|

641人浏览过

|

来源于php中文网

原创

RAII通过将资源生命周期与对象绑定,在构造时获取资源、析构时释放,确保异常安全和自动清理。C++中智能指针(如std::unique_ptr、std::shared_ptr)、std::lock_guard、std::fstream等标准库工具是RAII的典型应用,同时可自定义RAII类或使用unique_ptr配合自定义删除器管理非标准资源,提升代码安全性与简洁性。

c++如何使用raii管理资源和内存

C++中,RAII(Resource Acquisition Is Initialization,资源获取即初始化)是一种核心的编程范式,它通过将资源的生命周期与对象的生命周期绑定,确保资源在不再需要时能被自动、安全地释放,极大地简化了错误处理和资源管理,有效避免了内存泄漏和资源泄漏。

解决方案

RAII的本质在于,当一个对象被创建时,它会获取所需的资源(例如内存、文件句柄、互斥锁等),并在对象生命周期结束时(无论是正常退出作用域还是通过异常退出),其析构函数会自动释放这些资源。这种机制让资源管理变得几乎“无感”,开发者可以更专注于业务逻辑,而不用时刻担心资源的清理问题。

实践中,我们主要通过以下方式应用RAII:

  1. 使用智能指针管理动态内存: 这是最常见的RAII应用。
    std::unique_ptr
    和
    std::shared_ptr
    是C++标准库提供的强大工具,它们分别实现独占所有权和共享所有权,确保动态分配的内存在不再被引用时自动释放。
  2. 使用标准库提供的RAII包装器: 例如,
    std::lock_guard
    和
    std::scoped_lock
    用于管理互斥锁,确保锁在离开作用域时自动解锁。
    std::fstream
    等文件流对象也隐含了RAII,它们在构造时打开文件,在析构时关闭文件。
  3. 自定义RAII类: 对于标准库没有直接支持的资源(如C风格的句柄、特定的硬件资源、网络连接等),我们可以自己设计RAII类。核心思想是在构造函数中获取资源,在析构函数中释放资源。

为什么RAII是C++现代编程不可或缺的基石?

说RAII是C++现代编程的基石,这绝不是夸大其词。我个人觉得,它解决的痛点太核心了。你想想看,在没有RAII的时代,或者在C语言那种需要手动管理资源的语境下,一个函数里如果涉及多次资源分配(比如

malloc
、
fopen
、
pthread_mutex_lock
),然后中间又可能出现各种错误或者异常,那你得写多少
goto
或者层层
if
来确保每一步的资源都能正确释放?代码会变得异常复杂和脆弱。

立即学习“C++免费学习笔记(深入)”;

RAII彻底改变了这种局面。它把资源管理这个“脏活累活”封装进了对象的生命周期里。当对象被创建,资源就“顺带”被获取了;当对象超出作用域,无论是因为正常执行完毕,还是因为某个地方抛了异常,析构函数都会被调用,资源也就自然而然地被释放了。这带来的好处是多方面的:

  • 异常安全: 这是RAII最显著的优势之一。当程序发生异常时,堆栈会展开(stack unwinding),所有局部对象的析构函数都会被调用。这意味着即使在异常情况下,资源也能得到妥善清理,避免了泄漏。
  • 代码简洁性与可读性: 开发者不再需要手动编写大量的资源释放代码。业务逻辑和资源管理逻辑被清晰地分离,代码变得更干净、更容易理解。
  • 减少错误: 手动管理资源很容易出错,比如忘记释放、重复释放、在错误的时机释放。RAII机制将这些操作自动化,大大降低了人为错误的风险。
  • 提高可靠性: 资源泄漏是导致程序不稳定、性能下降甚至崩溃的常见原因。RAII从根本上解决了这类问题,提升了程序的整体健壮性。

所以,对我来说,RAII不仅仅是一个编程技巧,它更是一种思维模式,是C++“零开销抽象”哲学的一个完美体现。它让我们可以用更高级、更安全的方式来处理底层资源,同时几乎不引入额外的运行时开销。

C++标准库中哪些工具完美诠释了RAII的精髓?

标准库里,有几个明星成员,它们简直就是RAII的教科书式范例,用得好能让你的C++代码安全性和健壮性提升好几个档次。

首先,也是最重要的,是智能指针家族:

  • std::unique_ptr
    : 独占所有权。它确保一个资源(通常是动态分配的内存)在任何时候只有一个
    unique_ptr
    实例拥有。当
    unique_ptr
    超出作用域或被重置时,它所指向的资源会自动被
    delete
    。它不能被复制,只能被移动,这正是其“独占”的体现。

    #include <memory>
    #include <iostream>
    #include <vector>
    
    void processData(std::vector<int>* rawPtr) {
        if (!rawPtr) return;
        std::cout << "Processing data from raw pointer. Size: " << rawPtr->size() << std::endl;
        // 假设这里可能抛出异常
    }
    
    void exampleUniquePtr() {
        std::cout << "--- std::unique_ptr Example ---" << std::endl;
        // 动态分配一个vector
        std::unique_ptr<std::vector<int>> vecPtr = std::make_unique<std::vector<int>>();
        vecPtr->push_back(10);
        vecPtr->push_back(20);
    
        std::cout << "Vector size (before move): " << vecPtr->size() << std::endl;
    
        // unique_ptr 不能复制,只能移动
        std::unique_ptr<std::vector<int>> anotherVecPtr = std::move(vecPtr);
        // 此时 vecPtr 已经为空,所有权转移给了 anotherVecPtr
    
        if (vecPtr == nullptr) {
            std::cout << "vecPtr is now null after move." << std::endl;
        }
    
        // 使用另一个指针进行操作
        std::cout << "Vector size (after move, via anotherVecPtr): " << anotherVecPtr->size() << std::endl;
    
        // 也可以获取裸指针进行某些兼容C API的操作,但要小心
        // processData(anotherVecPtr.get());
    
        // 当 anotherVecPtr 超出作用域,它指向的vector会自动被delete
        std::cout << "anotherVecPtr will be destroyed, memory released." << std::endl;
    }
  • std::shared_ptr
    : 共享所有权。多个
    shared_ptr
    可以共同拥有同一个资源。它通过引用计数来管理资源的生命周期:每当一个新的
    shared_ptr
    指向该资源,引用计数就增加;每当一个
    shared_ptr
    被销毁或不再指向该资源,引用计数就减少。当引用计数降为零时,资源就会被释放。

    C++14
    C++14

    C++14 对 C++11 的修正与增强版本,适合旧系统维护和较老工具链兼容。

    下载
    #include <memory>
    #include <iostream>
    
    class MyResource {
    public:
        MyResource(int id) : id_(id) {
            std::cout << "MyResource " << id_ << " acquired." << std::endl;
        }
        ~MyResource() {
            std::cout << "MyResource " << id_ << " released." << std::endl;
        }
        void doSomething() {
            std::cout << "MyResource " << id_ << " doing something." << std::endl;
        }
    private:
        int id_;
    };
    
    void passSharedPtr(std::shared_ptr<MyResource> res) {
        std::cout << "Inside passSharedPtr. Use count: " << res.use_count() << std::endl;
        res->doSomething();
    } // res 离开作用域,引用计数减1
    
    void exampleSharedPtr() {
        std::cout << "\n--- std::shared_ptr Example ---" << std::endl;
        std::shared_ptr<MyResource> ptr1 = std::make_shared<MyResource>(1);
        std::cout << "ptr1 created. Use count: " << ptr1.use_count() << std::endl;
    
        {
            std::shared_ptr<MyResource> ptr2 = ptr1; // 复制,引用计数增加
            std::cout << "ptr2 created. Use count: " << ptr1.use_count() << std::endl;
            passSharedPtr(ptr2); // 传递副本,引用计数再次增加,函数结束后减回
            std::cout << "After passSharedPtr. Use count: " << ptr1.use_count() << std::endl;
        } // ptr2 离开作用域,引用计数减1
    
        std::cout << "After ptr2 destroyed. Use count: " << ptr1.use_count() << std::endl;
        // ptr1 离开作用域,引用计数减1,降为0,MyResource被释放
        std::cout << "ptr1 will be destroyed." << std::endl;
    }

除了智能指针,还有互斥锁的RAII包装器:

  • std::lock_guard
    和
    std::scoped_lock
    :
    这两个类用于管理
    std::mutex
    等互斥量。它们在构造函数中锁定互斥量,在析构函数中自动解锁。这保证了即使在多线程环境中发生异常,互斥量也能被正确释放,避免死锁。
    std::scoped_lock
    是C++17引入的,可以一次性锁定多个互斥量,避免死锁(通过采用特定的锁定策略)。

    #include <mutex>
    #include <thread>
    #include <iostream>
    #include <vector>
    
    std::mutex mtx;
    int shared_data = 0;
    
    void increment_data_safe() {
        std::cout << std::this_thread::get_id() << ": Trying to acquire lock..." << std::endl;
        // lock_guard 在构造时锁定 mtx,在离开作用域时解锁
        std::lock_guard<std::mutex> lock(mtx);
        std::cout << std::this_thread::get_id() << ": Lock acquired. Incrementing data." << std::endl;
        shared_data++;
        // 模拟一些可能抛异常的操作
        if (shared_data % 3 == 0) {
            // throw std::runtime_error("Simulated error!"); // 即使抛异常,锁也会被释放
        }
        std::cout << std::this_thread::get_id() << ": Data incremented to " << shared_data << ". Releasing lock." << std::endl;
    } // lock_guard 离开作用域,mtx 自动解锁
    
    void exampleLockGuard() {
        std::cout << "\n--- std::lock_guard Example ---" << std::endl;
        std::vector<std::thread> threads;
        for (int i = 0; i < 5; ++i) {
            threads.emplace_back(increment_data_safe);
        }
    
        for (auto& t : threads) {
            t.join();
        }
        std::cout << "Final shared_data: " << shared_data << std::endl;
    }

这些工具都是RAII的典范,它们将复杂的资源管理逻辑隐藏在简单、安全的接口之下,让C++开发者能够编写出更健壮、更易于维护的代码。

自定义RAII类:如何为非标准资源构建健壮的自动管理机制?

有时候,标准库的智能指针或包装器可能无法直接满足我们对某些特定资源的管理需求。比如,你可能在和C语言库交互,获取的是原始的文件描述符、某个硬件设备的句柄,或者是一个数据库连接,这些都不是简单的

new/delete
可以管理的。这时候,我们就需要自己动手,构建自定义的RAII类。

核心原则很简单:构造函数获取资源,析构函数释放资源。 但实际操作起来,还有一些细节需要注意,特别是C++11之后的“五法则”(或“零法则”)。

让我们以一个简单的C风格文件句柄为例:

#include <cstdio> // For FILE*, fopen, fclose
#include <iostream>
#include <stdexcept> // For std::runtime_error
#include <utility>   // For std::move

// 自定义一个FileHandle RAII类
class FileHandle {
public:
    // 构造函数:获取资源
    explicit FileHandle(const char* filename, const char* mode)
        : file_ptr_(std::fopen(filename, mode)) {
        if (!file_ptr_) {
            throw std::runtime_error("Failed to open file: " + std::string(filename));
        }
        std::cout << "File '" << filename << "' opened." << std::endl;
    }

    // 析构函数:释放资源
    ~FileHandle() {
        if (file_ptr_) {
            std::fclose(file_ptr_);
            std::cout << "File closed." << std::endl;
        }
    }

    // 禁止拷贝构造和拷贝赋值,因为文件句柄通常是独占的
    FileHandle(const FileHandle&) = delete;
    FileHandle& operator=(const FileHandle&) = delete;

    // 移动构造函数:转移所有权
    FileHandle(FileHandle&& other) noexcept
        : file_ptr_(other.file_ptr_) {
        other.file_ptr_ = nullptr; // 源对象不再拥有资源
        std::cout << "FileHandle moved." << std::endl;
    }

    // 移动赋值运算符:转移所有权
    FileHandle& operator=(FileHandle&& other) noexcept {
        if (this != &other) { // 避免自我赋值
            if (file_ptr_) { // 如果当前对象持有资源,先释放
                std::fclose(file_ptr_);
                std::cout << "Current file closed before move assignment." << std::endl;
            }
            file_ptr_ = other.file_ptr_;
            other.file_ptr_ = nullptr;
            std::cout << "FileHandle move assigned." << std::endl;
        }
        return *this;
    }

    // 提供一个方法来访问底层资源,但要小心使用
    FILE* get() const {
        return file_ptr_;
    }

    // 提供一个操作资源的示例
    void write(const std::string& data) {
        if (file_ptr_) {
            std::fprintf(file_ptr_, "%s", data.c_str());
            std::fflush(file_ptr_); // 确保写入
        }
    }

private:
    FILE* file_ptr_;
};

void exampleCustomRAII() {
    std::cout << "\n--- Custom RAII FileHandle Example ---" << std::endl;
    try {
        // 创建一个FileHandle对象
        FileHandle myFile("test.txt", "w+");
        myFile.write("Hello, RAII!\n");

        // 演示移动语义
        FileHandle anotherFile = std::move(myFile); // myFile现在为空
        anotherFile.write("This was moved.\n");

        // 即使这里抛出异常,anotherFile也会在栈展开时被正确关闭
        // throw std::runtime_error("Simulated error after writing.");

    } catch (const std::runtime_error& e) {
        std::cerr << "Error: " << e.what() << std::endl;
    }
    std::cout << "End of custom RAII example. File 'test.txt' should be closed." << std::endl;
}

关键点和思考:

  1. 构造函数获取,析构函数释放: 这是RAII的灵魂。构造函数里
    fopen
    ,析构函数里
    fclose
    ,简单直接。
  2. 异常安全: 如果
    fopen
    失败,构造函数会抛出异常。因为对象还没完全构造成功,析构函数不会被调用。这没问题,因为资源根本就没获取成功。如果构造成功,那么无论后续代码如何,析构函数总会被调用。
  3. 禁用拷贝: 对于像文件句柄这种独占性资源,通常不应该允许拷贝。如果允许拷贝,两个对象就会拥有同一个
    FILE*
    ,当它们各自析构时,就会导致双重关闭(double-close),这是非常危险的。所以我们使用
    = delete
    来禁用拷贝构造和拷贝赋值。
  4. 支持移动语义: 虽然不能拷贝,但支持移动是现代C++的习惯。移动语义允许资源所有权从一个对象转移到另一个对象,效率高,且符合“独占”的逻辑。在移动构造和移动赋值中,务必将源对象的资源指针设为
    nullptr
    ,防止源对象析构时误操作。
  5. get()
    方法:
    提供一个
    get()
    方法来获取底层裸指针(或句柄),这在需要与C API交互时很有用。但要提醒使用者,一旦通过
    get()
    获取了裸指针,就意味着你暂时脱离了RAII的保护,需要自行确保裸指针不会被误用或提前释放。
  6. 析构函数
    noexcept
    :
    析构函数最好是
    noexcept
    的。如果在析构函数中抛出异常,并且这个异常没有被捕获,那么程序会立即终止(
    std::terminate
    )。这在资源清理时尤其危险。所以,像
    fclose
    这类操作,即使失败,也通常不应该抛异常,而是记录日志。

更灵活的方式:使用

std::unique_ptr
的自定义删除器

很多时候,你甚至不需要完全自定义一个RAII类。对于那些只需要在对象销毁时执行特定清理函数的资源,

std::unique_ptr
配合自定义删除器(custom deleter)是一个更简洁、更灵活的选择。

#include <memory>
#include <cstdio>
#include <iostream>
#include <functional> // For std::function

void exampleUniquePtrCustomDeleter() {
    std::cout << "\n--- std::unique_ptr with Custom Deleter Example ---" << std::endl;

    // 定义一个文件关闭器 lambda
    auto file_closer = [](FILE* fp) {
        if (fp) {
            std::fclose(fp);
            std::cout << "File closed by custom deleter." << std::endl;
        }
    };

    // 使用 std::unique_ptr 和自定义删除器管理 FILE*
    // unique_ptr<FILE*, decltype(file_closer)> file_ptr(std::fopen("log.txt", "w"), file_closer);
    // 或者用 std::function 包装
    std::unique_ptr<FILE, std::function<void(FILE*)>> log_file(
        std::fopen("log.txt", "w"),
        [](FILE* fp) {
            if (fp) {
                std::fclose(fp);
                std::cout << "Log file closed by std::function deleter." << std::endl;
            }
        }
    );


    if (log_file) {
        std::fprintf(log_file.get(), "This is a log entry.\n");
        std::fflush(log_file.get());
        std::cout << "Wrote to log.txt" << std::endl;
    } else {
        std::cerr << "Failed to open log.txt" << std::endl;
    }

    // 当 log_file 超出作用域,lambda deleter 会被调用,关闭文件
    std::cout << "End of unique_ptr custom deleter example." << std::endl;
}

这种方式的优点是,你不需要编写一个完整的类,只需要提供一个清理函数。它利用了

unique_ptr
本身已经处理好的所有权语义(包括移动),非常方便。对于复杂一些的资源,比如需要多个参数才能释放的,或者需要进行错误检查的,自定义RAII类可能更清晰。但对于简单的清理任务,自定义删除器是首选。

总的来说,无论是自定义RAII类还是利用

unique_ptr
的自定义删除器,核心都是将资源的生命周期与对象的生命周期绑定,从而实现自动、安全的资源管理。这是C++强大而优雅的特性之一。

热门AI工具

更多
DeepSeek

DeepSeek是一款面向对话、写作、编程和推理场景的AI大模型工具。

咔片AIPPT

一款在线AI演示文稿制作工具,可根据主题和内容需求辅助生成PPT结构与页面,提高演示材料制作效率。

豆包大模型

豆包大模型是一款由字节跳动推出的企业级大语言模型服务平台。

UpDream
UpDream Hot

一款AI视频创作工具,主要用于哔哩哔哩推出的自研AI视频创作工具,适合需要提升相关任务效率的用户。

AionClaw
AionClaw Hot

AionClaw是一款面向办公、创作和编程任务的AI桌面智能体。

Loomy
Loomy Hot

一款AI工具,主要用于科大讯飞发布的桌面级 AI 助理,比 OpenClaw 更易用、更安全!,适合需要提升相关任务效率的用户。

讯飞绘文

讯飞绘文是一款由科大讯飞推出的一站式 AIGC 内容运营平台。

WorkBuddy

一款AI办公效率工具,主要用于腾讯云推出的AI原生桌面智能体工作台,适合需要提升相关任务效率的用户。

Atoms
Atoms Hot

Atoms是一款AI智能体工具,第一支自动构建真实业务的 AI 团队。

相关专题

更多
resource是什么文件
resource是什么文件

Resource文件是一种特殊类型的文件,它通常用于存储应用程序或操作系统中的各种资源信息。它们在应用程序开发中起着关键作用,并在跨平台开发和国际化方面提供支持。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

320

2023.12.20

fclose函数的用法
fclose函数的用法

fclose是一个C语言和C++中的标准库函数,用于关闭一个已经打开的文件,是文件操作中非常重要的一个函数,用于将文件流与底层文件系统分离,释放相关的资源。更多关于fclose函数的相关问题,详情请看本专题下面的文章。php中文网欢迎大家前来学习。

822

2023.11.30

go语言goto的用法
go语言goto的用法

本专题整合了go语言goto的用法,阅读专题下面的文章了解更多详细内容。

2877

2025.09.05

c++怎么把double转成int
c++怎么把double转成int

本专题整合了 c++ double相关教程,阅读专题下面的文章了解更多详细内容。

4148

2025.08.29

C++中int、float和double的区别
C++中int、float和double的区别

本专题整合了c++中int和double的区别,阅读专题下面的文章了解更多详细内容。

784

2025.10.23

C++ 智能指针与现代内存管理
C++ 智能指针与现代内存管理

深入讲解 C++ 现代内存管理的核心工具——智能指针,涵盖 unique_ptr 独占所有权语义、shared_ptr 引用计数机制与循环引用问题、weak_ptr 弱引用的应用场景、make_unique/make_shared 工厂函数的性能优势、自定义删除器的编写、RAII 资源管理思想的实践,以及从裸指针迁移到智能指针的重构策略,帮助开发者编写安全无泄漏的现代 C++ 代码。

359

2026.04.23

硬盘接口类型介绍
硬盘接口类型介绍

硬盘接口类型有IDE、SATA、SCSI、Fibre Channel、USB、eSATA、mSATA、PCIe等等。详细介绍:1、IDE接口是一种并行接口,主要用于连接硬盘和光驱等设备,它主要有两种类型:ATA和ATAPI,IDE接口已经逐渐被SATA接口;2、SATA接口是一种串行接口,相较于IDE接口,它具有更高的传输速度、更低的功耗和更小的体积;3、SCSI接口等等。

3168

2023.10.19

PHP接口编写教程
PHP接口编写教程

本专题整合了PHP接口编写教程,阅读专题下面的文章了解更多详细内容。

5029

2025.10.17

Gradle插件开发入门教程
Gradle插件开发入门教程

本专题介绍Gradle插件开发的基本流程,涵盖插件类编写、自定义任务注册、扩展参数设计和插件应用方法,帮助你理解插件如何组织构建逻辑,减少多个项目之间的重复配置。

60

2026.10.22

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn