0

0

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

P粉602998670

P粉602998670

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

|

621人浏览过

|

来源于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 
    #include 
    #include 
    
    void processData(std::vector* 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> vecPtr = std::make_unique>();
        vecPtr->push_back(10);
        vecPtr->push_back(20);
    
        std::cout << "Vector size (before move): " << vecPtr->size() << std::endl;
    
        // unique_ptr 不能复制,只能移动
        std::unique_ptr> 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
    被销毁或不再指向该资源,引用计数就减少。当引用计数降为零时,资源就会被释放。

    #include 
    #include 
    
    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 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 ptr1 = std::make_shared(1);
        std::cout << "ptr1 created. Use count: " << ptr1.use_count() << std::endl;
    
        {
            std::shared_ptr 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包装器

JTBC网站内容管理系统5.0.3.1
JTBC网站内容管理系统5.0.3.1

JTBC CMS(5.0) 是一款基于PHP和MySQL的内容管理系统原生全栈开发框架,开源协议为AGPLv3,没有任何附加条款。系统可以通过命令行一键安装,源码方面不基于任何第三方框架,不使用任何脚手架,仅依赖一些常见的第三方类库如图表组件等,您只需要了解最基本的前端知识就能很敏捷的进行二次开发,同时我们对于常见的前端功能做了Web Component方式的封装,即便是您仅了解HTML/CSS也

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

    #include 
    #include 
    #include 
    #include 
    
    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 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 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  // For FILE*, fopen, fclose
#include 
#include  // For std::runtime_error
#include    // 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 
#include 
#include 
#include  // 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_ptr(std::fopen("log.txt", "w"), file_closer);
    // 或者用 std::function 包装
    std::unique_ptr> 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++强大而优雅的特性之一。

相关专题

更多
C语言变量命名
C语言变量命名

c语言变量名规则是:1、变量名以英文字母开头;2、变量名中的字母是区分大小写的;3、变量名不能是关键字;4、变量名中不能包含空格、标点符号和类型说明符。php中文网还提供c语言变量的相关下载、相关课程等内容,供大家免费下载使用。

379

2023.06.20

c语言入门自学零基础
c语言入门自学零基础

C语言是当代人学习及生活中的必备基础知识,应用十分广泛,本专题为大家c语言入门自学零基础的相关文章,以及相关课程,感兴趣的朋友千万不要错过了。

608

2023.07.25

c语言运算符的优先级顺序
c语言运算符的优先级顺序

c语言运算符的优先级顺序是括号运算符 > 一元运算符 > 算术运算符 > 移位运算符 > 关系运算符 > 位运算符 > 逻辑运算符 > 赋值运算符 > 逗号运算符。本专题为大家提供c语言运算符相关的各种文章、以及下载和课程。

348

2023.08.02

c语言数据结构
c语言数据结构

数据结构是指将数据按照一定的方式组织和存储的方法。它是计算机科学中的重要概念,用来描述和解决实际问题中的数据组织和处理问题。数据结构可以分为线性结构和非线性结构。线性结构包括数组、链表、堆栈和队列等,而非线性结构包括树和图等。php中文网给大家带来了相关的教程以及文章,欢迎大家前来学习阅读。

255

2023.08.09

c语言random函数用法
c语言random函数用法

c语言random函数用法:1、random.random,随机生成(0,1)之间的浮点数;2、random.randint,随机生成在范围之内的整数,两个参数分别表示上限和下限;3、random.randrange,在指定范围内,按指定基数递增的集合中获得一个随机数;4、random.choice,从序列中随机抽选一个数;5、random.shuffle,随机排序。

585

2023.09.05

c语言const用法
c语言const用法

const是关键字,可以用于声明常量、函数参数中的const修饰符、const修饰函数返回值、const修饰指针。详细介绍:1、声明常量,const关键字可用于声明常量,常量的值在程序运行期间不可修改,常量可以是基本数据类型,如整数、浮点数、字符等,也可是自定义的数据类型;2、函数参数中的const修饰符,const关键字可用于函数的参数中,表示该参数在函数内部不可修改等等。

519

2023.09.20

c语言get函数的用法
c语言get函数的用法

get函数是一个用于从输入流中获取字符的函数。可以从键盘、文件或其他输入设备中读取字符,并将其存储在指定的变量中。本文介绍了get函数的用法以及一些相关的注意事项。希望这篇文章能够帮助你更好地理解和使用get函数 。

632

2023.09.20

c数组初始化的方法
c数组初始化的方法

c语言数组初始化的方法有直接赋值法、不完全初始化法、省略数组长度法和二维数组初始化法。详细介绍:1、直接赋值法,这种方法可以直接将数组的值进行初始化;2、不完全初始化法,。这种方法可以在一定程度上节省内存空间;3、省略数组长度法,这种方法可以让编译器自动计算数组的长度;4、二维数组初始化法等等。

595

2023.09.22

php源码安装教程大全
php源码安装教程大全

本专题整合了php源码安装教程,阅读专题下面的文章了解更多详细内容。

74

2025.12.31

热门下载

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

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Go 教程
Go 教程

共32课时 | 3.2万人学习

Go语言实战之 GraphQL
Go语言实战之 GraphQL

共10课时 | 0.8万人学习

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

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号