当前位置: 首页 > news >正文

C/C++学习笔记 资源获取是初始化 (RAII) 理解

一、RAII概述

        也许RAII 的一个更好的名称是“范围绑定资源”,因为您将资源的生命周期与局部变量的生命周期联系起来,并且局部变量的生命周期在它超出范围时结束。

        现代 C++ 通过在堆栈上声明对象来尽可能避免使用堆内存。当资源对于堆栈来说太大时,它应该由一个对象拥有。当对象被初始化时,它会获取它拥有的资源。然后该对象负责在其析构函数中释放资源。拥有对象本身在堆栈上声明。对象拥有资源的原则也称为“资源获取即初始化”或RAII。

        当拥有资源的堆栈对象超出范围时,会自动调用其析构函数。这样,C++ 中的垃圾回收与对象的生命周期密切相关,并且是确定性的。资源总是在程序中的已知点释放,您可以控制该点。只有像 C++ 中那样的确定性析构函数才能平等地处理内存和非内存资源。

        RAII 保证资源可用于任何可能访问对象的函数(资源可用性是类不变量,消除了多余的运行时测试)。它还保证所有资源在其控制对象的生命周期结束时以获取相反的顺序释放。同样,如果资源获取失败(构造函数异常退出),则每个完全构造的成员和基础子对象获取的所有资源都以初始化的相反顺序释放。这利用了核心语言特性(对象生存期、范围退出、初始化顺序和堆栈展开)) 以消除资源泄漏并保证异常安全。该技术的另一个名称是范围绑定资源管理(SBRM),在基本用例之后,RAII 对象的生命周期由于范围退出而结束。

        RAII可以总结如下:

        将每个资源封装到一个类中,其中构造函数获取资源并建立所有类不变量,如果无法完成则抛出异常,析构函数释放资源并且从不抛出异常;

        始终通过 RAII 类的实例使用资源,本身具有自动存储期限或临时生命周期,或具有受自动或临时对象的生命周期限制的生命周期。

        RAII 类包含三个部分:

        资源在析构函数中被放弃(例如关闭文件)

        类的实例是堆栈分配的

        在构造函数中获取资源(例如打开文件)。这部分是可选的。

二、示例1:确保实例分配在堆栈

class OpenFile {
public:
    OpenFile(const char* filename){
        //失败则抛出异常
        _file.open(filename);
    }
    
    ~OpenFile(){
        _file.close();
    }
    
    std::string readLine() {
        return _file.readLine();
    }
    
private:
    File _file;
};


OpenFile f("boo.txt");
//异常安全,并且没有必要进行关闭
loadFromFile(f);

         确保实例分配在堆栈上而不是堆上

std::string firstLineOf(const char* filename){
    OpenFile f("boo.txt"); //stack allocated
    return f.readLine();
    //文件在这里关闭。 `f` 超出范围并运行析构函数。
}

std::string firstLineOf(const char* filename){
    OpenFile* f = new OpenFile("boo.txt"); //heap allocated
    return f->readLine();
    //析构函数永远不会运行,因为 `f` 永远不会被删除
}

三、示例2:坏的示例和好的示例

std::mutex m;
 
//这是一个坏的示例
void bad() 
{
    m.lock();                    // acquire the mutex
    f();                         // if f() throws an exception, the mutex is never released
    if(!everything_ok()) return; // early return, the mutex is never released
    m.unlock();                  // if bad() reaches this statement, the mutex is released
}

//这是一个好的示例
void good()
{
    std::lock_guard<std::mutex> lk(m); // RAII class: mutex acquisition is initialization
    f();                               // if f() throws an exception, the mutex is released
    if(!everything_ok()) return;       // early return, the mutex is released
}                                      // if good() returns normally, the mutex is released

四、示例3:使用智能指针的示例

1、简单对象

        下面的例子展示了一个简单的对象w。它在函数范围的堆栈上声明,并在函数块的末尾被销毁。该对象w不拥有任何资源(例如堆分配的内存)。它唯一的成员g本身在堆栈上声明,并且与 . 一起超出范围w。widget析构函数中不需要特殊代码。

class widget {
private:
    gadget g;   // 生命周期自动绑定到封闭对象
public:
    void draw();
};

void functionUsingWidget () {
    widget w;   // 生命周期自动绑定到封闭范围构造 w,包括 w.g 成员
    // ...
    w.draw();
    // ...
} // w 和 w.g 自动异常安全的自动销毁和释放,就像“finally { w.dispose(); w.g.dispose(); }”

2、手动释放资源

        在以下示例中,w拥有内存资源,因此必须在其析构函数中包含代码才能释放内存。

class widget
{
private:
    int* data;
public:
    widget(const int size) { data = new int[size]; } // acquire
    ~widget() { delete[] data; } // release
    void do_something() {}
};

void functionUsingWidget() {
    widget w(1000000);  // 生命周期自动绑定到封闭范围构造 w,包括 w.data 成员
    w.do_something();

} //w 和 w.data 的自动销毁和释放

3、使用智能指针

        从 C++11 开始,有一种更好的方法来编写前面的示例:使用标准库中的智能指针。智能指针处理它所拥有的内存的分配和删除。使用智能指针消除了在widget类中显式析构函数的需要。

#include <memory>
class widget
{
private:
    std::unique_ptr<int[]> data;
public:
    widget(const int size) { data = std::make_unique<int[]>(size); }
    void do_something() {}
};

void functionUsingWidget() {
    widget w(1000000);  // 生命周期自动绑定到封闭范围构造 w,包括 w.data 小工具成员
    // ...
    w.do_something();
    // ...
} // w 和 w.data 的自动销毁和释放

        通过使用智能指针进行内存分配,您可以消除内存泄漏的可能性。此模型适用于其他资源,例如文件句柄或套接字。您可以在类中以类似的方式管理自己的资源。

        C++ 的设计确保对象在超出范围时被销毁。也就是说,它们会随着块的退出而被破坏,与构建的顺序相反。当一个对象被销毁时,它的基础和成员会以特定的顺序被销毁。在任何块之外、在全局范围内声明的对象可能会导致问题。如果全局对象的构造函数抛出异常,可能很难调试。

相关文章:

  • 高项 16 战略管理
  • Vue框架背后的故事
  • (机器学习的矩阵)(向量、矩阵与多元线性回归)
  • 计算摄影——妆造迁移
  • 【物理应用】基于Zernike多项式的大气湍流相位屏的数值模拟附matlab代码
  • 【工具网站推荐】文字转语音
  • 自定义类型:结构体详解
  • 两万字:讲述微信小程序之组件
  • 网际协议IP(计算机网络)
  • 树莓派buster安装ROS完整记录
  • Linux进程状态、进程优先级、环境变量、进程地址空间
  • 面试官:ArrayList扩容机制,你了解吗?
  • 应用链的兴起将带来哪些风险和机遇?未来将会如何发展?
  • 2022年接口测试面试题大全
  • 软件流程和管理(十):配置管理
  • php的引用
  • [译] React v16.8: 含有Hooks的版本
  • 2018天猫双11|这就是阿里云!不止有新技术,更有温暖的社会力量
  • ABAP的include关键字,Java的import, C的include和C4C ABSL 的import比较
  • ECMAScript6(0):ES6简明参考手册
  • Invalidate和postInvalidate的区别
  • Javascript Math对象和Date对象常用方法详解
  • JS学习笔记——闭包
  • Kibana配置logstash,报表一体化
  • MySQL的数据类型
  • Rancher-k8s加速安装文档
  • Travix是如何部署应用程序到Kubernetes上的
  • 表单中readonly的input等标签,禁止光标进入(focus)的几种方式
  • 干货 | 以太坊Mist负责人教你建立无服务器应用
  • 类orAPI - 收藏集 - 掘金
  • 使用Gradle第一次构建Java程序
  • nb
  • shell使用lftp连接ftp和sftp,并可以指定私钥
  • #我与Java虚拟机的故事#连载15:完整阅读的第一本技术书籍
  • (31)对象的克隆
  • (HAL)STM32F103C6T8——软件模拟I2C驱动0.96寸OLED屏幕
  • (libusb) usb口自动刷新
  • (过滤器)Filter和(监听器)listener
  • (源码版)2024美国大学生数学建模E题财产保险的可持续模型详解思路+具体代码季节性时序预测SARIMA天气预测建模
  • (轉貼) 資訊相關科系畢業的學生,未來會是什麼樣子?(Misc)
  • .NET delegate 委托 、 Event 事件
  • .net6使用Sejil可视化日志
  • .sh
  • [ vulhub漏洞复现篇 ] Grafana任意文件读取漏洞CVE-2021-43798
  • [ vulhub漏洞复现篇 ] ThinkPHP 5.0.23-Rce
  • [20171101]rman to destination.txt
  • [Assignment] C++1
  • [BIZ] - 1.金融交易系统特点
  • [BZOJ1053][HAOI2007]反素数ant
  • [C#][DevPress]事件委托的使用
  • [CISCN2019 华东南赛区]Web11
  • [CSS3备忘] transform animation 等
  • [Flexbox] Using order to rearrange flexbox children
  • [GN] DP学习笔记板子
  • [HackMyVM]靶场 Quick3