首页 > 编程语言 >C++ | 每一个C++程序员都应该知道的RAII

C++ | 每一个C++程序员都应该知道的RAII

时间:2023-10-29 14:35:54浏览次数:44  
标签:std Resource RAII C++ 程序员 内存 ptr

导读:RAII是C++中一种管理资源、避免资源泄漏的惯用法,利用栈的特点来实现。本文较为详细介绍了RAII的原理、使用方法和优点,并且通过实例讲解了RAII在C++ STL中的应用,如智能指针和互斥锁等,在最后进行了编程实践。本文适合对C++编程有一定了解的开发者阅读。

1. 什么是RAII

RAII是Resource Acquisition Is Initialization的缩写,即“资源获取即初始化”。它是C++语言的一种管理资源、避免资源泄漏的惯用法,利用栈的特点来实现,这一概念最早由Bjarne Stroustrup提出。在函数中由栈管理的临时对象,在函数结束时会自动析构,从而自动释放资源,因此,我们可以通过构造函数获取资源,通过析构函数释放资源。即:


Object() {
    // acquire resource in constructor
}

~Object() {
    // release resource in destructor
}

RAII总结如下:

  • 将每一种资源封装在一个RAII类中:

    • 所有资源在构造函数中获取,例如:分配内存、打开文件、建立数据库连接等;如果无法完成则在构造函数中抛出异常;
    • 所有资源在析构函数中释放,例如:释放内存、关闭文件、销毁数据库连接等;不应该抛出任何异常。
  • 通过RAII类实例获取资源:

    • 具有自动生命管理周期或临时对象生命周期
    • 其生命周期与第一种绑定。

2. 为什么要使用RAII

我们知道,在C++中,通过new运算符动态申请内存,例如:

Foo* ptr = new Foo(1);

// ...
delete ptr;

在这段代码中,new运算符在计算机内存的堆上申请了一块Foo类型的内存,然后将其地址赋值给存储在栈上的指针ptr。为了能够释放内存资源,我们需要使用完new运算符申请的内存后,手动调用delete运算符释放内存。

但是,情况并不总是如此简单。

Foo* ptr = new Foo(1);

f(ptr);  // -->① may throw exception
if(ptr->g()) {
    // ... --> ② may forget to delete ptr
    return;
}
// ...
delete ptr;

如上面这个例子,我们可能会遇到以下几种情况:

  1. 忘记delete释放内存。比如释放原指针指向的内存前就改变了指针的指向。
  2. 程序抛出异常后导致无法delete。比如上面的①处,如果f函数抛出异常,没有机会运行delete,从而导致内存泄漏。
  3. 需求变更后,修改了函数,新增了分支,提前返回,却没有delete;现实情况代码复杂的话可能没有这么显而易见。

而通过RAII这样一种机制,我们可以使其自动释放内存。

3. C++ STL中RAII的应用

3.1 智能指针

智能指针是RAII的一种实现,它是一种模板类,用于管理动态分配的对象。智能指针的主要作用是自动释放内存,从而避免内存泄漏。C++11中提供了三种智能指针:unique_ptr、shared_ptr和weak_ptr。它们的详细原理将在之后的文章中介绍。这里我们以unique_ptr为例,它的构造函数如下:

template< class T, class Deleter = std::default_delete<T> > class unique_ptr;

unique_ptr的析构函数会自动释放内存,因此,我们可以通过unique_ptr来管理动态分配的内存,从而避免内存泄漏。例如:

std::unique_ptr<int> ptr = std::make_unique<int>(1); // release memory when ptr is out of scope

3.2 互斥锁

在多线程编程中,std::lock_guard, std::unique_lock, std::shared_lock等也利用了RAII的原理,用于管理互斥锁。当这些类的等对象创建时,会自动获取互斥锁;当对象销毁时,会自动释放互斥锁。

std::lock_guard的构造函数如下:

template< class Mutex > class lock_guard;

std::lock_guard的析构函数会自动释放互斥锁,因此,我们可以通过std::lock_guard来管理互斥锁,从而避免忘记释放互斥锁。例如:


std::mutex mtx;
std::lock_guard<std::mutex> lock(mtx); // unlock when lock is out of scope

不使用RAII的情况下,我们需要手动释放互斥锁,如下所示:

std::mutex mtx;
mtx.lock();
// ...
mtx.unlock();

3.3 文件操作

std::ifstream, std::ofstream等C++标准库的IO操作都是RAII的实现。

3.4 事务处理

数据库事务处理中,如果在事务结束时没有提交或回滚,就会导致数据库连接一直被占用,从而导致数据库连接池耗尽。因此,我们需要在事务结束时自动提交或回滚,从而释放数据库连接。这一过程也可以通过RAII来实现。

3.5 其他

RAII还可以用于管理其他资源,比如网络连接、线程等。

4. RAII的编程实践

基于RAII实现资源池的自动回收机制:

ResourcePool为资源池类,可以创建指定数量的资源,并提供获取和释放资源的接口。

ResourceWrapper为资源包装类,用于获取资源,并在对象销毁时自动释放资源。

Resource为资源类,用于模拟资源,通过id来标识,其构造函数和析构函数分别用于获取和释放资源。

代码实现如下:

#include <iostream>
#include <vector>
#include <deque>
constexpr int kErrorId = -1;
template<typename T>
class ResourcePool {
public:
    ResourcePool(int size) {
        for (int i = 0; i < size; ++i) {
            pool_.emplace_back(i);
        }
    }

    T getResource() {
        if (pool_.empty()) {
            return T();
        }
        T resource = std::move(pool_.front());
        pool_.pop_front();
        std::cout<< "Resource " << resource.ID() << " is acquired." << std::endl;
        return resource;
    }

    void releaseResource(T&& resource) {
        if (resource.ID() == kErrorId) {
          return;
        }
        std::cout << "Resource " << resource.ID() << " is released." << std::endl;
        pool_.emplace_back(std::forward<T>(resource));
    }

private:
    std::deque<T> pool_;
};

template<typename T>
class ResourceWrapper {
public:
    ResourceWrapper(ResourcePool<T>& pool) : pool_(pool), resource_(pool_.getResource()) {
      if(resource_.ID() == kErrorId) {
        throw std::runtime_error("Resource is not available now.");
      }
    }

    ~ResourceWrapper() { // release resource when object is destroyed.
        pool_.releaseResource(std::move(resource_)); 
    }
private:
    ResourcePool<T>& pool_;
    T resource_;
};


class Resource {
public:
    constexpr explicit Resource(int id) : id_(id) {
      std::cout << "Resource " << id_ << " is created." << std::endl;
    }
    Resource(): id_(kErrorId) {}
    ~Resource() = default;
    int ID() const {
        return id_;
    }
    Resource(const Resource& other) noexcept : id_(other.id_) {}

    Resource& operator=(const Resource& other) noexcept {
      id_ = other.id_;
      return *this;
    }

    Resource(Resource&& other) noexcept : id_(other.id_) {
      other.id_ = kErrorId;
    }

    Resource& operator=(Resource&& other) noexcept {
      id_ = other.id_;
      other.id_ = kErrorId;
      return *this;
    }
private:
    int id_;
};

constexpr int kPoolSize = 3;
ResourcePool<Resource> pool(kPoolSize); // Resource pool with 3 resources in global scope.

void RequestRourceTest() {
    std::vector<ResourceWrapper<Resource>> resources;
    constexpr int kResourcesNum = 3;
    resources.reserve(kResourcesNum);
    for (int i = 0; i < kResourcesNum; ++i) {
        resources.emplace_back(pool);
    }
}

int main() {
    RequestRourceTest();
    return 0;
}

运行输出结果如下:

Resource 0 is created.
Resource 1 is created.
Resource 2 is created.
Resource 0 is acquired.
Resource 1 is acquired.
Resource 2 is acquired.
Resource 0 is released.
Resource 1 is released.
Resource 2 is released.

5. 总结

在本文中,我们介绍了C++中的RAII技术,它是一种管理资源的方法,可以帮助我们避免内存泄漏和资源泄漏等问题。RAII技术的核心思想是将资源的获取和释放绑定在对象的生命周期中,这样可以确保资源在不再需要时被正确释放。我们还介绍了如何使用RAII技术来管理动态内存、文件句柄和互斥锁等资源,并提供了一些示例代码来说明如何实现RAII类。最后,我们还讨论了RAII技术的一些注意事项和最佳实践,以帮助开发人员编写更安全、更可靠的代码。希望本文能够帮助您更好地理解和应用RAII技术。

在本文的编程实践中,还使用了std::move()、std::forward()、noexcept等诸多现代C++技术,更多细节和不足之处,将在之后的文章中进行进一步探讨。

参考:

  1. Effective C++, Item 13: Use objects to manage resources. Scott Meyers.
  2. https://en.cppreference.com/w/cpp/language/raii

你好,我是七昂,计算机科学爱好者,致力于分享C/C++、操作系统等计算机基础知识。希望我们能一起在计算机科学的世界里探索和成长,最终能站得更高,走得更远。如果你有任何问题或者建议,欢迎随时与我交流。感谢你们的支持和关注!

标签:std,Resource,RAII,C++,程序员,内存,ptr
From: https://www.cnblogs.com/qiangz/p/17795846.html

相关文章

  • C和C++的区别
    1.头文件(c++标准头文件都是有命名空间的);2.c++有命名空间,可以解决命名冲突的问题;3.输入与输出1)c++:读入std::cin<<     输出std::cout>>std::endl;2)c:读入:scanf输出:printf4.对空间的开辟和释放c:malloc,calloc,realloc,free......
  • C++---数据结构---队列(queue)
    queue容器queue基本概念概念:Queue是一种先进先出(FirstInFirstOut,FIFO)的数据结构,它有两个出口队列容器允许从一端新增元素,从另一端移除元素队列中只有队头和队尾才可以被外界使用,因此队列不允许有遍历行为队列中进数据称为—入队push队列中出数据称为—出队popque......
  • 【程序员转型】从土木工程到 IT 编程,我的十年转型之路
    大家好,明天是1024        十年前,我还是一名土木工程专业的学生。那时的我,对于未来的职业方向感到迷茫和困惑。然而,在一次偶然的机会中,我接触到了IT编程,并深深地被这个领域所吸引。从那时起,我开始了我的转型之路。转行到IT编程职业并不是一件容易的事情。我需要学习新......
  • 【C++】继承 ⑧ ( 继承 + 组合 模式的类对象 构造函数 和 析构函数 调用规则 )
    文章目录一、继承+组合模式的类对象构造函数和析构函数调用规则1、场景说明2、调用规则二、完整代码示例分析1、代码分析2、代码示例一、继承+组合模式的类对象构造函数和析构函数调用规则1、场景说明如果一个类既继承了基类,又在类中维护了一个其它类型的成员......
  • 房地产行业程序员管理痛点分析
    很久前跟某地产公司程序员聊天,谈到程序员管理的痛点问题,怎么去解决这些痛点。1.大部分骨干力量集中休年假。理由有:回家照顾老人。请病假:职业病方面,比如眼睛痛,头痛,脖子颈椎痛,腰肌劳损,每个人都会有,确实也没办法。你也不好不让人请年假吧?2.按时上下班,下班关手机,任务按科学时间完成。......
  • 35岁大龄程序员失业后,如果回到5年前,会做出哪些改变
    前言35岁的程序员被公司辞退是什么体验?前段时间跟朋友们在讨论几个问题,其中有个非常让我值得深思的,就是说如果回到五年前,我们应该怎么选择或要做什么准备?我觉得我应该会这样做:第一点,我想不要轻易跳槽,尽量转岗到公司的核心业务部门,离开公司所谓的创新型业务部门和非核心业务。这几年......
  • VS and C++ Versions
    VS版本VC版本ToolSet版本MSC版本VisualStudio66 1200VisualStudio20037 1300VisualStudio20058801400VisualStudio20089901500VisualStudio2010101001600VisualStudio2012111101700VisualStudio2013121201800......
  • 《程序员修炼之道:从小工到专家》第三第四章读书笔记
    第三章基本工具第14节纯文本的威力本节是第三章:基本工具,首节内容,章节介绍里有一句话:许多新程序员都会犯下错误,采用单一的强力工具,比如特定的集成开发环境(IDE),而且再也不离开其舒适的界面。这实在是一个错误。我们要乐于超越IDE所施加的各种限制。要做到这一点,唯一的途径是保持......
  • C++_计算机应用开发流程
    开发流程需求-痛点-不满点-构成开发需求需求排期-设计原型开发-编译-测试-发布-安装RedHat(RedhatPackageManager)的包管理规范进行打包,获取到相应的软件RPM发布包,然后使用rpm命令安装makeautotools的工具集打包的。这个工具集包含autoconf、automake等工具......
  • C++ 日期&时间
    C++日期&时间C++标准库没有提供所谓的日期类型。C++继承了C语言用于日期和实际操作的结构和函数。为了使用日期和时间相关的函数和结构,需要在C++程序中引用<ctime>头文件。有四个与时间相关的类型:clock_ttime_tsize_ttm能够把系统时间和日期表示为某种整数。结构类型tm把......