为什么要引入智能指针?
首先我们来看下面这个例子:
1 | void test() |
- 在test函数中new一个四字节的空间,
- 判断if条件的语句为真,抛出异常
- main函数直接catch 捕获异常,函数返回0
- try 执行了直接执行catch,程序结束,以至于没有执行delete_ptr释放空间,导致内存泄漏。
理解智能指针需要从下面三个层次:
- 从较浅的层面看,智能指针是利用了一种叫做RAII(资源获取即初始化)的技术对普通的指针进行封装,这使得智能指针实质是一个对象,行为表现的却像一个指针。
- 智能指针的作用是防止忘记调用delete释放内存和程序异常的进入catch块忘记释放内存。另外指针的释放时机也是非常有考究的,多次释放同一个指针会造成程序崩溃,这些都可以通过智能指针来解决。
- 智能指针还有一个作用是把值语义转换成引用语义。
std::unique_ptr
- 性质
- 对其持有的对内存具有唯一拥有权。
- 对象销毁时会释放其持有的堆内存。
- 使用注意尽量使用方式3去创建,因为形式3更安全。
1
2
3
4
5
6
7//1
unique_ptr<int> sp1(new int(1));
//2
unique_ptr<int> sp2;
sp2.reset(new int(1));
//3
unique_ptr<int> sp3 = make_unique<int>(1);
Q:为什么形式3更安全?
A:参考《effective modern c++》
鉴于std::auto_ptr的前车之鉴,std::unique_ptr禁止赋值语义,为了达到这个效果,std::unique_ptr类的拷贝构造函数和赋值运算符被标记为delete。
但是可以通过移动构造函数来将堆内存转移。
1 | unique_ptr<int> func(int val){ |
std::unique_ptr不仅可以持有一个堆对象,还可以持有一组堆对象。
1 | //1 |
加入堆内存对象内部还有需要回收的资源,我们还可以自定义智能指针的资源释放函数。
例如:
1 |
|
shared_ptr
unique_ptr对持有的资源具有独占性,shared_ptr持有的资源在多个shared_ptr
之间共享,每多一个shared_ptr对资源的引用,资源引用计数将增加1,每个指向该资源的
shared_ptr对象析构时,资源引用计数减一,最后一个shared_ptr对象析构时,发现资源
计数为0,将释放其持有的资源。多个线程之间,递增和减少资源的引用计数是安全的(不意味着
多个线程同时操纵资源对象是安全的)。shared_ptr使用use_count()来获取当前持有资源的
引用计数。除了上面描述的,基本上使用方法和unique_ptr相似。
1 | //1 |
再看下面这段代码:
1 |
|
输出结果:
1 | A() |
实际开发中,有时候需要在类中返回包裹当前对象(this)的一个std::shared_ptr
对象给外部使用,C++ 新标准也为我们考虑到了这一点,有如此需求的类只要继承自
std::enable_shared_from_this 模板对象即可。用法如下:
1 | class A : public std::enable_shared_from_this<A> |
上述代码中,类 A 的继承 std::enable_shared_from_this 并提供一个
getSelf() 方法返回自身的 std::shared_ptr 对象,在 getSelf() 中
调用 shared_from_this() 即可。
陷阱一:不应该共享栈对象的 this 给智能指针对象
陷阱二:避免 std::enable_shared_from_this 的循环引用问题
weak_ptr
std::weak_ptr 是一个不控制资源生命周期的智能指针,是对对象的一种弱引用,
只是提供了对其管理的资源的一个访问手段,引入它的目的为协助 std::shared_ptr
工作。
std::weak_ptr 可以从一个 std::shared_ptr 或另一个std::weak_ptr 对象构造,
std::shared_ptr 可以直接赋值给 std::weak_ptr ,也可以通过 std::weak_ptr
的 lock() 函数来获得 std::shared_ptr。它的构造和析构不会引起引用计数的增
加或减少。std::weak_ptr 可用来解决 std::shared_ptr 相互引用时的死锁问题
(即两个std::shared_ptr 相互引用,那么这两个指针的引用计数永远不可能下降
为 0, 资源永远不会释放)。
1 |
|
既然,std::weak_ptr 不管理对象的生命周期,那么其引用的对象可能在某个时刻被销毁了
,如何得知呢?
std::weak_ptr 提供了一个 expired() 方法来做这一项检测,返回 true,
说明其引用的资源已经不存在了;返回 false,说明该资源仍然存在,这个时候可以使用
std::weak_ptr 的 lock() 方法得到一个std::shared_ptr 对象然后继续操作资源,以下
代码演示了该用法:
1 | //tmpConn_ 是一个 std::weak_ptr<TcpConnection> 对象 |
有读者可能对上述代码产生疑问,既然使用了 std::weak_ptr 的 expired() 方
法判断了对象是否存在,为什么不直接使用 std::weak_ptr 对象对引用资源进行
操作呢?实际上这是行不通的,std::weak_ptr 类没有重写operator-> 和 operator*
方法,因此不能 std::shared_ptr 或 std::unique_ptr 一样直接操作对象,
同时 std::weak_ptr 类也没有重写 operator! 操作,因此也不能通过 std::weak_ptr*
对象直接判断其引用的资源是否存在。
之所以weak_ptr 不增加引用资源的引用计数不管理资源的生命周期,是因为,即使它实现
了以上说的几个方法,调用它们也是不安全的,因为在调用期间,引用的资源可能恰好被
销毁了,这会造成棘手的错误和麻烦。
正确使用场景是那些资源如果可能就使用,如果不可使用则不用的场景,它不参与资源的生
命周期管理。例如,网络分层结构中,Session 对象(会话对象)利用 Connection 对象
(连接对象)提供的服务工作,但是 Session 对象不管理 Connection 对象的生命周期,
Session 管理 Connection 的生命周期是不合理的,因为网络底层出错会导致 Connection
对象被销毁,此时 Session 对象如果强行持有 Connection 对象与事实矛盾。