RAII与三五零法则

RAII与三/五/零法则

RAII

什么是RAII?


RAII的全称是资源获取即初始化 (Resource Acquisition Is Initialization)

​ 它的核心思想是将资源的管理与对象的生命周期绑定在一起。当一个对象被创建时,它自动获取资源;当对象被销毁时,它负责释放资源。这种方法的优势在于可以确保资源在适当的时候被正确释放,从而避免了资源泄漏。

为什么需要RAII?

void array_test(const size_t size)
{
    int* arr = new int[size];
    /* Process the array */
    delete[] arr;
}
void process(int* arr) noexcept(false);

void array_test(const size_t size)
{
    int* arr = new int[size];
    process(arr);
    delete[] arr;
}

如果上面的例子中process函数抛出异常会导致delete[] arr;无法执行造成内存泄漏。可以用try-catch结构捕获异常之后释放内存。

void array_test(const size_t size)
{
    int* arr = new int[size];
    try
    {
        process(arr);
    }
    catch (...)
    {
        delete[] arr;
        return;
    }
    delete[] arr;
}

但是在有多个资源的情况下这样手动管理会很麻烦,代码逻辑也会变得复杂。

我们可以利用RAII将资源获取也就是申请内存的操作放到对象的初始化中,相应地,把资源回收也就是释放内存的操作放到析构函数中。

struct dyn_array
{
    size_t size = 0;
    int* ptr = nullptr;

    dyn_array() = default; // Default(empty) initialization
    explicit dyn_array(const size_t size) :size(size), ptr(new int[size]) {} // Initialize with a given size
    ~dyn_array() noexcept { delete[] ptr; } // Free the memory
};

void array_test(const size_t size)
{
    dyn_array darr(size);
    process(darr.ptr);
} // Memory freed no matter how this function exits

​ 这样就可以简化资源管理的操作。因为C++对象有明确的生存周期期,这个函数无论是正常退出还是因抛出异常而退出,对象darr的作用域(生存期)都将结束,导致其析构函数被调用,资源成功释放,省去了各种手动判断释放资源的步骤。

三法则和零法则

什么是三法则?

​ 如果你需要定义一个类的析构函数,那么你可能也需要定义它的拷贝构造函数和拷贝赋值运算符。因为这些都是涉及到资源管理的操作,需要确保资源的正确释放和管理。如果你手动管理资源,而没有定义拷贝构造函数和拷贝赋值运算符,那么可能会导致资源的浅拷贝问题。

为什么不能用编译器自带的拷贝构造函数和拷贝赋值运算符?

void array_test(const size_t size)
{
    dyn_array darr(size); // Allocated memory (darr.ptr)
    {
        // Copy initialization, copy.ptr = darr.ptr
        dyn_array copy = darr;
        process(copy.ptr);
    } // copy.ptr is freed
    process(darr.ptr); // Oops, now darr.ptr points to garbage
} // Double oops, the pointer is doubly freed...

​ 复制品copy跟原数组darr指向了同一片内存,当复制品的生存期结束时,复制品将那个数组销毁了,因此process(darr.ptr);会出现异常。在darr的生存期结束时,它又会释放它保留的指针,这导致了内存的二次释放。

​ 所以在这种情况下编译器自带的拷贝构造函数和拷贝赋值运算符并不靠谱,需要自己实现

dyn_array(const dyn_array& other) :size(other.size)
{
        ptr = new int[other.size];
        std::copy_n(other.ptr, size, ptr);
}

dyn_array& operator=(const dyn_array& other)
{
        if (&other != this)
        {
            delete[] ptr;
            size = other.size;
            ptr = new int[other.size];
            std::copy_n(other.ptr, size, ptr);
        }
        return *this;
}

​ 与其对应的就是零法则,如果你并不需要自己实现析构函数,那么就一个特殊函数都不要实现,让编译器帮你做完所有事情。

什么时候需要手动实现析构函数?

类涉及到动态分配的资源,比如内存、文件句柄、数据库连接等,要手动编写析构函数来确保这些资源在对象被销毁时被正确释放。##

五法则

在三法则的基础上加入移动构造函数和移动赋值运算符。这是因为在 C++11 引入了右值引用和移动语义,通过移动资源可以避免不必要的拷贝操作,提高性能。

void array_test(const size_t size)
{
    dyn_array darr;
    darr = dyn_array(size);
}

函数的第二行构造了一个dyn_array临时量,之后把这个临时量赋值给darr。如果仅有前面的三法则定义的话,这会调用复制赋值运算符。可是,既然我们知道赋的值是一个临时量,我们为什么非要重新开辟一片内存空间再把临时数组里面的数据复制一遍呢?如果能有一种办法把临时量里面存的指针“偷过来”就好了。

当然有这种方法,这里就要用到移动赋值运算符了。

dyn_array& operator=(dyn_array&& other) noexcept
{
        delete[] ptr;
        size = other.size;
        // Following line is equivalent to
        //     ptr = other.ptr;
        //     other.ptr = nullptr;
        ptr = std::exchange(other.ptr, nullptr);
        return *this;
}
dyn_array(dyn_array&& other) noexcept :
        size(other.size),
        ptr(std::exchange(other.ptr, nullptr)) {}

原博客

posted @   紫冰凌  阅读(68)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 【杭电多校比赛记录】2025“钉耙编程”中国大学生算法设计春季联赛(1)
点击右上角即可分享
微信分享提示