C++杂记——RAII (Resource Acquisition Is Initialization)

什么是RAII

RAII(Resource Acquisition Is Initialization,资源获取即初始化)是一种C++编程习惯和原则,旨在通过资源的生命周期管理来保证资源的安全和有效使用。RAII的核心思想是将资源的获取和释放绑定到对象的生命周期。当对象被创建时,它会分配相应的资源;当对象的生命周期结束(例如超出作用域时),其析构函数会自动被调用,从而释放资源。

它使得我们得以规避“裸new操作”和“裸delete操作”的风险,避免裸new和裸delete可以使我们的代码远离各种潜在风险,避免资源泄漏。

RAII的优势

  • 自动资源管理:通过RAII,程序员不需要手动释放资源,减少了内存泄漏等问题的出现。
  • 异常安全:RAII使得在出现异常时,资源仍能安全释放,避免了资源的泄露。
  • 清晰的资源控制:通过对象的生命周期管理,代码更加整洁,意图更加明确。

比如:网络套接字、互斥锁、文件句柄和内存等等,它们属于系统资源。由于系统的资源是有限的,所以,我们在编程使用系统资源时,都必须遵循一个步骤:

  1. 申请资源;
  2. 使用资源;
  3. 释放资源。

第一步和第二步缺一不可,因为资源必须要申请才能使用的,使用完成以后,必须要释放,如果不释放的话,就会造成资源泄漏。

RAII 原理

当局部变量离开其作用域时,这个变量也就被销毁了;当这个变量是类对象时,这个时候就会自动调用这个类的析构函数。

由于系统的资源不具有自动释放的功能,而C++中的类具有自动调用析构函数的功能。如果把资源用类进行封装起来,对资源操作都封装在类的内部,在析构函数中进行释放资源。当定义的局部变量的生命结束时,它的析构函数就会自动的被调用,如此,就不用显式调用释放资源的操作了。

RAII的应用

STL(标准模板库)在设计上充分利用了RAII原则。以下是RAII在STL中的几个典型应用:

  • std::vector 和 std::string:
    这些容器自动管理内存。在创建一个std::vector或std::string对象时,它们会分配适量的内存并在对象析构时自动释放。
    当std::vector中的元素超出它的生命周期时,它的析构函数会依次调用每个元素的析构函数,从而确保没有资源泄漏。

  • 智能指针:
    STL中的智能指针(如std::unique_ptr和std::shared_ptr)也遵循RAII原则。它们在对象被创建时申请资源(例如动态分配的内存),并在对象被销毁时自动释放资源。
    智能指针提供了一种管理动态对象生命周期的安全方式,避免了传统指针可能导致的内存管理错误。

  • 容器迭代器:
    STL中的迭代器通常是用对象来表示的,这些对象也遵循RAII原则。比如std::lock_guard在锁的使用中,保证锁在使用时被持有,而当锁对象超出作用域时,锁会自动释放。

  • 互斥体包装器
    std::lock_guard是一个互斥体封装器,它提供了一个方便的RAII风格机制,用于在作用域块的持续时间内拥有一个互斥体。

当一个lock_guard对象被创建时,它会尝试获取它被赋予的互斥体的所有权。当控制离开创建lock_guard对象的作用域时,lock_guard会被销毁,互斥体也会被释放。

例子

自动释放资源

自动计算函数耗时

#ifndef _CODE_SNIPPET_RAII_H_
#define _CODE_SNIPPET_RAII_H_
#include <sys/time.h>

#include <chrono>
#include <ctime>
#include <iostream>
#include <string>

using llong = long long;
using namespace std::chrono;
using std::cout;
using std::endl;

class TimerLog
{
public:
    TimerLog(const std::string tag)
    {
        m_begin_ = high_resolution_clock::now();
        m_tag_ = tag;
    }

    void Reset()
    {
        m_begin_ = high_resolution_clock::now();
    }

    llong Elapsed()
    {
        return static_cast<llong>(duration_cast<std::chrono::milliseconds>(
                                      high_resolution_clock::now() - m_begin_)
                                      .count());
    }

    ~TimerLog()
    {
        auto time = duration_cast<std::chrono::milliseconds>(
                        std::chrono::high_resolution_clock::now() - m_begin_)
                        .count();
        std::cout << "time { " << m_tag_ << " } " << static_cast<double>(time)
                  << " ms" << std::endl;
    }

private:
    std::chrono::time_point<std::chrono::high_resolution_clock> m_begin_;
    std::string m_tag_;
};
#define CALL_SCOPE_TIME(x) TimerLog t(x)
void TestTimerLog();
#endif
#include "RAII.h"
#include <thread>

void TestTimerLog()
{
    auto func = []() {
        for (int i = 0; i < 2; ++i) {
            cout << "i " << i << endl;
            std::this_thread::sleep_for(std::chrono::milliseconds(1));
        }
    };
    {
        CALL_SCOPE_TIME("func");
        func();
    }
}

测试结果:

$ ./CPP/cpp_snippet 
i 0
i 1
time { func } 2 ms

注意

虑到封装的是资源,资源很多时候是不具备拷贝语义的,所以,在实际实现过程中需要控制其复制过程(比如lock_guard直接不可复制),让资源的一切操作都在自己的控制当中。

总结

RAII是一种有效的资源管理机制,极大地提升了C++程序的安全性和稳定性。在STL中,RAII得到广泛应用,通过智能指针、容器及其迭代器等技术,使得资源管理变得更加简单和安全。这不仅减少了人为错误,也支持了异常安全的编程习惯。

参考

RAII

posted @ 2025-03-02 13:30  main_c  阅读(3)  评论(0)    收藏  举报  来源