美文网首页
C++: RAII是什么——使用对象来管理资源

C++: RAII是什么——使用对象来管理资源

作者: 七昂的技术之旅 | 来源:发表于2023-10-28 00:46 被阅读0次

    导读: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来标识,其构造函数和析构函数分别用于获取和释放资源。

    实现资源管理类需要注意的一些事项:

    • 需要仔细考虑拷贝构造函数和拷贝赋值运算符的实现,若需拷贝,应考虑实现引用计数或对资源进行深拷贝;若无必要,最好将其删除。这里我们使用了=delete进行了删除;

    • 需提供移动构造函数和移动赋值运算符,以便于使用std::move(),转移资源的控制权;

    • 提供获取原始资源的接口。

    代码实现如下:

    #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()) {
                std::cout << "Resource is not available now." << std::endl;
                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() {
            pool_.releaseResource(std::move(resource_));
        }
        ResourceWrapper(const ResourceWrapper& other) = delete;
        ResourceWrapper& operator=(const ResourceWrapper& other) = delete;
    
        ResourceWrapper(ResourceWrapper&& other) noexcept : pool_(other.pool_), resource_(std::move(other.resource_)) {
        }
    
        ResourceWrapper& operator=(ResourceWrapper&& other) noexcept {
          pool_ = other.pool_;
          resource_ = std::move(other.resource_);
          return *this;
        }
        const T& GetRawResource() const noexcept{
            return 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_;
        }
        // delete copy constructor and copy assignment operator
        Resource(const Resource& other) = delete;
        Resource& operator=(const Resource& other) = delete;
    
        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() {
        constexpr int kResourcesNum = 3;
        for (int i = 0; i < kResourcesNum; ++i) {
            ResourceWrapper<Resource> resource_wrapper(pool);
            resource_wrapper.GetRawResource();
        }
    }
    
    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()等诸多现代C++技术,将在之后的文章中进行进一步探讨。

    参考:

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

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

    相关文章

      网友评论

          本文标题:C++: RAII是什么——使用对象来管理资源

          本文链接:https://www.haomeiwen.com/subject/yibpidtx.html