C++中RAII的一些思考
引言
在学习智能指针之前,我总听说RAII是c++资源管理的核心思想,但一直停留在字面印象上。直到自己动手写了一个管理int的RAII类,把资源的申请和释放绑进构造与析构里,才算是真正体会到它的妙处。
RAII即Resource Acquisition Is Initialization,资源的获取就是初始化。
在此篇文章中,我将记录下自己对RAII的理解,以供思考,如有不足之处欢迎指正。
正文
构造即获取,析构即释放
RAII的类自己持有资源,本例中就是一块堆上的int:
class RAII_int_t {
private:
int *m_int_p{};//持有的资源指针,默认初始化为nullptr
public:
RAII_int_t()
{
m_int_p = new int{};//构造时申请资源
}
explicit RAII_int_t(const int &value)
{
m_int_p = new int{value};
}
~RAII_int_t()
{
delete m_int_p;//析构时释放资源
}
};
资源在构造函数中获取,在析构函数中释放,资源的生命周期与对象的生命周期完全绑定。
这么看来,只要对象按正常的路径离开作用域,delete就一定会被执行,main里那段代码没有一个delete,却也没有任何泄漏。
用在上一篇文章里写的生命周期视角来看,RAII其实是把栈对象的确定性析构,变成了管理堆资源的工具。
深拷贝的拷贝构造
资源类的拷贝不能只复制指针,否则两个对象析构时会对同一块内存delete两次:
RAII_int_t(const RAII_int_t &other)
{
m_int_p = new int{other.get()};//重新new一块,做深拷贝
}
每个对象持有自己独立的那份int,互相之间的修改不会影响对方。
copy and swap的赋值
赋值运算符这里我用了copy and swap的写法:
RAII_int_t &operator=(RAII_int_t other)//注意参数是按值传入
{
std::swap(m_int_p, other.m_int_p);//交换指针
return *this;
}
按值传参意味着实参先被拷贝构造成一个临时对象,然后我们把自己手里的指针和临时对象交换,函数结束时临时对象析构,顺带把我们原来的旧资源释放掉了。
我认为这个写法非常巧妙:拷贝、交换、释放三件事,只用一个函数和一次析构就完成了,而且天然自赋值安全,异常安全也有保证。
对比手动版本的赋值
operator=(const int &value),那里要先new再delete旧指针,顺序一旦写反就丢了资源,而copy and swap把这份风险交给了拷贝构造和析构函数。
与int的互转
为了让这个类用起来像一个int,我加了类型转换运算符:
operator int() const { return *m_int_p; };//隐式转换为int
[[nodiscard]] int get() const { return *m_int_p; };
有了operator int(),add(t1, t3)这类接受int的函数可以直接传入RAII对象,隐式转换自动完成。
而get()标记了[[nodiscard]],忘记使用返回值时编译器会给出警告。
隐式转换方便但也危险,标准库的智能指针就特意用
explicit修饰了bool转换,防止意外转换。这里为了实验的流畅性保留了隐式版本。
补充要点
三个构造函数都处理了创建路径:默认构造new一个零值int,带值构造按值创建,接受指针的构造则做了一层判空兜底:
explicit RAII_int_t(const int *value_p)
{
if (value_p != nullptr) {
m_int_p = new int{*value_p};//拷贝指针指向的值
}
else {
m_int_p = new int{};//无效指针退化为默认构造
}
}
注意这里即使传入指针也是重新new一份,和拷贝构造一样遵循"每个对象独占一份资源"的原则。
小结
RAII的本质是把资源的生命周期托管给栈上对象的构造与析构,只要对象离开作用域,资源就必然被释放。写完这个类再回头看智能指针,就会发现unique_ptr不过是把同样的思想做得更完善罢了。
用一个小小的int体会一遍资源管理的套路,之后面对文件、内存这些真正的资源时,思路是相通的。
本文所用的测试代码收录于Learncpp,如有不妥之处欢迎指正。
$ 正在加载评论…