C++ RAII 与智能指针

什么是RAII

资源获取即初始化(Resource Acquisition Is Initialization, RAII),RAII 是一种资源管理机制,资源的有效期与持有资源的对象生命周期严格绑定,即由对象的构造函数完成资源分配,由析构函数完成资源释放。总结一句话就是:用对象来管理资源。

RAII实现原理

当一个对象离开作用域的时候就会被释放,会调用这个对象类的析构函数,这都是自动管理的,不需要我们手动调用。所以我们可以把资源封装到类的内部,当需要用资源的时候初始化对象即可,当对象被释放的时候资源也会被释放。

看一个小例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#include <iostream>
using namespace std;

class ArrayOperation
{
public :
ArrayOperation()
{
m_Array = new int [10];
}

void InitArray()
{
for (int i = 0; i < 10; ++i)
{
*(m_Array + i) = i;
}
}

void ShowArray()
{
for (int i = 0; i <10; ++i)
{
cout<<m_Array[i]<<endl;
}
}

~ArrayOperation()
{
cout<< "~ArrayOperation is called" <<endl;
if (m_Array != NULL )
{
delete[] m_Array;
m_Array = NULL ;
cout << "m_Array is released" << endl;
}
}

private :
int *m_Array;
};

void test(){
ArrayOperation arrayOp;
arrayOp.InitArray();
arrayOp.ShowArray();
cout << "test end" << endl;
}

int main()
{
test();
cout << "return 0" << endl;
return 0;
}

输出结果如下:
ascen.me_cpp_raii

可以看到在 test 函数结束之后会释放对象 arrayOp,然后会调用 ArrayOperation 的析构函数,释放掉指针。

简单说明一下,创建 arrayOp:

1
2
3
ArrayOperation arrayOp;
//相当于 ArrayOperation arrayOp = ArrayOperation();
//隐式、显示调用的区别

是在栈中分配内存,如果:

1
ArrayOperation* arrayOp = new ArrayOperation();

则是在堆中分配内存,如果不:

1
delete arrayOp;

则不会调用析构函数。

智能指针

auto_ptr

auto_ptr 的特性就是在栈上构建对象,在离开作用范围时会自动析构。

上面的例子可以改成:

1
2
3
4
5
6
7
void test(){
auto_ptr<ArrayOperation> arrayOp(new ArrayOperation);
//相当于auto_ptr<ArrayOperation> arrayOp(new ArrayOperation());
arrayOp->InitArray();
arrayOp->ShowArray();
cout << "test end" << endl;
}

这样的话不需要 delete arrayOp,也会调用析构函数来释放资源。

auto_ptr 有一个问题,就是被销毁时会自动删除它指向的对象,所以不能让多个 auto_ptr 指向同一个对象。如果有多个 auto_ptr 指针指向同一个对象,便会多次删除对象。

但是若通过 copy 构造函数或者 copy assignment 操作符复制它们,原指针就会变成 null,而复制所得的指针将取得资源的唯一控制权(所有权)。注意,是拷贝初始化或者拷贝赋值才会这样,即发生了拷贝行为,原指针会变为 null。

1
2
3
int*p=new int(0);
auto_ptr<int>ap1(p);
auto_ptr<int>ap2(p);

这样 ap1、ap2 都指向 p 所指之物,危险行为,会删除 p 两次。

如果

1
2
3
4
int*p=new int(0);
auto_ptr<int>ap1(p);
auto_ptr<int>ap2 = ap1;
//或者 auto_ptr<int> ap2(ap1);

ap2 是拷贝初始化,ap1 会变为 null,ap2 指向 p 所指之物。

而且因为 auto_ptr 的析构函数中删除指针用的是 delete,而不是 delete[],所以我们不应该用 auto_ptr 来管理一个数组指针!!

shared_ptr

shared_ptr 也是一种智能指针,自动管理对象的释放,并且可以让多个指针指向同一个对象,会有一个类似 iOS ARC 中“引用计数”的东西来记录这个对象被多少指针所引用。如果这个对象的引用计数变为 0,则会自动释放空间。

shared_ptr 跟 auto_ptr 一样自动管理对象的释放,并且没有 auto_ptr 的那些缺点,因为使用了“引用计数”。但是这样也产生了新的问题,那就是循环引用、自引用等问题。

weak_ptr

由于 shared_ptr 有了循环引用等问题,于是就有了 weak_ptr 这种弱引用的指针,不增加引用计数。

这里和 iOS Property 中的机制差不多,很多地方的实现思想都是相似的。

unique_ptr

unique_ptr 是只能有一个 unique_ptr 指向一个对象,因此 unique_ptr 不支持拷贝和赋值。

1
2
3
4
unique_ptr<string> p1(new string("test"));
unique_ptr<string> p2(p1); //拷贝,错误
unique_ptr<string> p3;
p3 = p2; //赋值,错误

不能拷贝或者赋值,但是可以通过 releasereset 来转换指针的所有权。

1
2
3
unique_ptr<string> p2(p1.release); //release将p1置空
unique_ptr<string> p3(new string("test"));
p2.reset(p3.release); //reset释放了p2原来指向的内存

参考

C++中的RAII机制
Effective C++ 第13条款
C++ Primer