C++智能指针

本文介绍C++的四个智能指针: auto_ptr, unique_ptr, shared_ptr, weak_ptr 其中后三个是c++11支持,并且第一个已经被C++11弃用。
智能指针和普通指针的区别在于智能指针实际上是对普通指针加了一层封装机制,这样的一层封装机制的目的是为了使得智能指针可以方便的管理一个对象的生命期。

auto_ptr

  • 出现时间最早(C++98),现在(从C++11开始)已被弃用
  • 采用所有权模式,但是限制不足
  • 不能指向对象数组,其析构函数内使用的是delete而非delete[]
  • 不能作为STL标准容器的元素

智能指针出现的思想是使用类来管理资源,无须手动释放资源,减少内存泄露的机会。auto_ptr完成了这一目的。举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <memory>	// 智能指针所在头文件

void remodel(std::string & str)
{
std::string * ps = new std::string(str);
// ...
if (weird_thing())
// 2 当然这里可以手动增加delete语句,但肯定会有部分人忘记,而且异常安全并不总能保证
throw exception(); // 1 当出现异常时,delete将不被执行,因此将导致内存泄露。
str = *ps;
delete ps;
return;
}

void remodel_auto_ptr (std::string & str)
{
std::auto_ptr<std::string> ps(new std::string(str));
// ...
if (weird_thing ())
throw exception();
str = *ps;
// delete ps; NO LONGER NEEDED
return;
} // auto_ptr被销毁的同时,ps指向的内存也会被auto_ptr的析构函数delete掉

但是auto_ptr对资源所有权的剥夺较为隐晦,从而又导致其它问题,即内存访问出错。举例:

1
2
3
4
5
6
7
8
9
10
11
12
void remodel_auto_ptr (std::string & str)
{
std::auto_ptr<std::string> ps(new std::string(str));
// ...
std::auto_ptr<std::string> ps2(new std::string("xxx"));
// ...
ps2 = ps; // 将所有权从ps转让给ps2,此时ps不再引用该字符串从而变成空指针
if (weird_thing ())
throw exception();
str = *ps; // 访问出错
return;
}

以至于后来不得不对auto_ptr重新设计,从而产生了unique_ptrshared_ptr以及weak_ptrunique_ptrshared_ptr分别从独占和共享角度出发对auto_ptr改进,规定了上文代码中ps2 = ps的行为。weak_ptr设计的目的主要用于解决shared_ptr循环引用问题。

unique_ptr

  • 用以代替auto_ptr
  • 采用所有权模式,严格限制
  • 临时右值允许转移所有权

unique_ptr直接不允许上文代码中ps2 = ps的赋值行为,从而避免auto_ptr情况下ps访问错误。但有时确实有使ps2和ps同时能访问某一对象的需求,所以又设计了shared_ptr

shared_ptr

  • 采用共享所有权模式指向对象
  • 使用引用计数机制管理对象被释放的时机

Notes: 指针是指向对象还是引用对象?引用计数机制管理指针指向对象的个数,所以用引用似乎没有问题,但使用引用这个词又怕与引用类型混淆)

多个shared_ptr可以同时指向一个对象,当最后一个shared_ptr离开作用域时,对象内存才会自动释放。 似乎使用unique_ptrshared_ptr非常完美,满足各种需求。但实际上总会出现这样一个问题:每引入一种机制解决现有问题,而又不可避免的带来其它问题。但是代价不一样。shared_ptr会产生循环引用问题,但这种问题发生的频率相比于使用auto_ptr的问题很低。

智能指针的循环引用概念比资源的独占或共享概念较复杂,举例说明之:

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
#include <iostream>
#include <memory>
using namespace std;

class B;
class A
{
public: // 为了省去一些步骤这里 数据成员也声明为public
//weak_ptr<B> pb;
shared_ptr<B> pb;
void doSomthing()
{
// if(pb.lock())
// {
//
// }
}

~A()
{
cout << "kill A\n";
}
};

class B
{
public:
//weak_ptr<A> pa;
shared_ptr<A> pa;
~B()
{
cout <<"kill B\n";
}
};

int main(int argc, char** argv)
{
shared_ptr<A> sa(new A());
shared_ptr<B> sb(new B());
if(sa && sb)
{
sa->pb=sb;
sb->pa=sa;
}
cout<<"sa use count:"<<sa.use_count()<<endl;
return 0;
}

上面A类中包含一个shared_ptr,B类也包含一个shared_ptr。A对象有sa以及sb->pa指向,引用数为2。A对象有sb以及sa->pb指向,引用数也为2。当离开main函数作用域时,sa和sb要被销毁,A和B对象各自引用数减1,由于引用数不为0,均无法被sa->pb和sb->pa删除,造成A、B对象均未被回收,内存泄露。

由此归纳出循环引用产生条件:

  • 至少含两个对象,且其中均为智能指针(强引用指针)
  • 对象中的智能指针同时指向了这些对象,它们之间的指向构成一个环。

打破循环引用的关键在于使环中任一对象的引用数为0,这要求某些对象相互引用时的引用数不能够增加。有如下方法(这些方法是或的关系,选其一即可):

  • 将生命周期最短的类,其中的指针设计为普通指针
  • 将类中的指针设计为弱引用智能指针

上面方法,仅仅是针对必须使用循环引用的情况。大部分情况是我们不小心造成了循环引用,此时修改引用逻辑即可。

weak_ptr

  • 使用weak_ptr引用对象不增加引用数,因此称之为弱引用。
  • 不能直接通过weak_ptr来访问资源。 在需要访问资源的时候weak_ptr为你生成一个shared_ptr,shared_ptr能够保证在shared_ptr没有被释放之前,其所管理的资源是不会被释放的。创建shared_ptr的方法是lock()方法。
  • expired()方法用于检测所管理的对象是否已经释放。如果已经被释放,获取到的是一个空的shared_ptr(NULL)。

计划更新

  • weak_ptr
  • unique_ptr临时允许右值转移所有权
  • shared_ptr使用举例
  • unique_ptr使用举例

参考资料

------ 本文结束------
赞赏此文?求鼓励,求支持!
  • 本文标题: C++智能指针
  • 本文作者: Jiang.G.F
  • 创建于: 2019年12月21日 - 14时12分
  • 更新于: 2020年03月03日 - 11时03分
  • 本文链接: https://gfjiangly.github.io/C++/small_points.html
  • 版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明出处!
0%