c++11新特性:智能指针

为什么要引入智能指针?

首先我们来看下面这个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void test()
{
int*_ptr=new int(1);
if(_ptr)
{
throw 1;
}
delete _ptr;
}

int main()
{
try
{
test();
}
catch(...)
{}
return 0;
}
  1. 在test函数中new一个四字节的空间,
  2. 判断if条件的语句为真,抛出异常
  3. main函数直接catch 捕获异常,函数返回0
  4. try 执行了直接执行catch,程序结束,以至于没有执行delete_ptr释放空间,导致内存泄漏。

理解智能指针需要从下面三个层次:

  1. 从较浅的层面看,智能指针是利用了一种叫做RAII(资源获取即初始化)的技术对普通的指针进行封装,这使得智能指针实质是一个对象,行为表现的却像一个指针。
  2. 智能指针的作用是防止忘记调用delete释放内存和程序异常的进入catch块忘记释放内存。另外指针的释放时机也是非常有考究的,多次释放同一个指针会造成程序崩溃,这些都可以通过智能指针来解决。
  3. 智能指针还有一个作用是把值语义转换成引用语义。

std::unique_ptr

  • 性质
  1. 对其持有的对内存具有唯一拥有权。
  2. 对象销毁时会释放其持有的堆内存。
  • 使用
    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);
    注意尽量使用方式3去创建,因为形式3更安全。

Q:为什么形式3更安全?
A:参考《effective modern c++》

鉴于std::auto_ptr的前车之鉴,std::unique_ptr禁止赋值语义,为了达到这个效果,std::unique_ptr类的拷贝构造函数和赋值运算符被标记为delete。
但是可以通过移动构造函数来将堆内存转移。

1
2
3
4
5
6
7
8
9
unique_ptr<int> func(int val){
return unique_ptr<int>(new int(val));
}
int main(){
unique_ptr<int> sp = func(12);
unique_ptr<int> sp2 = make_unique<int>(12);
unique_ptr<int> sp3 = move(sp2);
return 0;
}

std::unique_ptr不仅可以持有一个堆对象,还可以持有一组堆对象。

1
2
3
4
5
6
7
//1
unique_ptr<int[]> p(new int[10]);
//2
unique_ptr<int[]> sp2;
sp2.reset(new int[10]);
//3
unique_ptr<int[]> sp3(make_unique<int[]>(10));

加入堆内存对象内部还有需要回收的资源,我们还可以自定义智能指针的资源释放函数。
例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
#include <memory>
class Socket
{
public:
Socket(){}
~Socket(){}
//关闭资源句柄
void close(){}
};
int main()
{
auto deletor = [](Socket* pSocket) {
//关闭句柄
pSocket->close();
delete pSocket;
};
std::unique_ptr<Socket, void(*)(Socket * pSocket)> spSocket(new Socket(), deletor);
return 0;
}

shared_ptr

unique_ptr对持有的资源具有独占性,shared_ptr持有的资源在多个shared_ptr
之间共享,每多一个shared_ptr对资源的引用,资源引用计数将增加1,每个指向该资源的
shared_ptr对象析构时,资源引用计数减一,最后一个shared_ptr对象析构时,发现资源
计数为0,将释放其持有的资源。多个线程之间,递增和减少资源的引用计数是安全的(不意味着
多个线程同时操纵资源对象是安全的)。shared_ptr使用use_count()来获取当前持有资源的
引用计数。除了上面描述的,基本上使用方法和unique_ptr相似。

1
2
3
4
5
6
7
//1
shared_ptr<int> sp1(new int(1));
//2
shared_ptr<int> sp2;
sp2.reset(new int(1));
//3
shared_ptr<int> sp3 = make_shared<int>(1);

再看下面这段代码:

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

class A{
public:
A(){
cout << "A()" << endl;
}
~A(){
cout << "~A()" << endl;
}
};
int main(){

shared_ptr<A> sp1(new A());
cout << sp1.use_count() << endl;

shared_ptr<A> sp2(sp1);
cout << sp1.use_count() << endl;

sp2.reset();
cout << sp1.use_count() << endl;

{
shared_ptr<A> sp3 = sp1;
cout << sp1.use_count() << endl;
}

cout << sp1.use_count() << endl;
return 0;
}

输出结果:

1
2
3
4
5
6
A()
1
2
3
2
~A()

实际开发中,有时候需要在类中返回包裹当前对象(this)的一个std::shared_ptr
对象给外部使用,C++ 新标准也为我们考虑到了这一点,有如此需求的类只要继承自
std::enable_shared_from_this 模板对象即可。用法如下:

1
2
3
4
5
6
7
8
9
10
11
12
class A : public std::enable_shared_from_this<A>
{
public:
A(){
cout << "A()" << endl;
}
~A(){
cout << "~A()" << endl;
}
shared_ptr<A> getSelf(){
return shared_from_this();
}

上述代码中,类 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
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
#include <iostream>
#include <memory>
using namespace std;
int main()
{
//创建一个shared_ptr对象
shared_ptr<int> sp1(new int(123));
cout << "use count: " << sp1.use_count() << endl;

//通过构造函数得到一个weak_ptr对象
weak_ptr<int> sp2(sp1);
cout << "use count: " << sp1.use_count() << endl;

//通过赋值运算符得到一个weak_ptr对象
weak_ptr<int> sp3 = sp1;
cout << "use count: " << sp1.use_count() << endl;

//通过一个weak_ptr对象得到另外一个weak_ptr对象
weak_ptr<int> sp4 = sp2;
cout << "use count: " << sp1.use_count() << endl;

return 0;
}
//output:
// use count: 1
// use count: 1
// use count: 1
// use count: 1

既然,std::weak_ptr 不管理对象的生命周期,那么其引用的对象可能在某个时刻被销毁了
,如何得知呢?
std::weak_ptr 提供了一个 expired() 方法来做这一项检测,返回 true,
说明其引用的资源已经不存在了;返回 false,说明该资源仍然存在,这个时候可以使用
std::weak_ptr 的 lock() 方法得到一个std::shared_ptr 对象然后继续操作资源,以下
代码演示了该用法:

1
2
3
4
5
6
7
8
//tmpConn_ 是一个 std::weak_ptr<TcpConnection> 对象
//tmpConn_引用的TcpConnection已经销毁,直接返回
if (tmpConn_.expired())
return;
std::shared_ptr<TcpConnection> conn = tmpConn_.lock();
if (conn){
//操作conn
}

有读者可能对上述代码产生疑问,既然使用了 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 对象与事实矛盾。