C++-智能指针

引入

我们知道除了静态内存和栈内存外,每个程序还有一个内存池,这部分内存被称为自由空间或者堆。程序用堆来存储动态分配的对象即那些在程序运行时分配的对象,当动态对象不再使用时,我们的代码必须显式的销毁它们。

为了更加容易(更加安全)的使用动态内存,引入了智能指针的概念。智能指针的行为类似常规指针,重要的区别是它负责自动释放所指向的对象。标准库提供的两种智能指针的区别在于管理底层指针的方法不同, shared_ptr允许多个指针指向同一个对象,unique_ptr则“独占”所指向的对象。标准库还定义了一种名为weak_ptr的伴随类,它是一种弱引用,指向shared_ptr所管理的对象,这三种智能指针都定义在memory头文件中。

shared_ptr

shared_ptr实现共享式拥有概念。
多个智能指针可以指向相同对象,该对象和其相关资源会在“最后一个引用被销毁”时候释放。从名字share就可以看出了资源可以被多个指针共享,它使用计数机制来表明资源被几个指针共享。可以通过成员函数use_count()来查看资源的所有者个数。

我们可以认为每个shared_ptr都有一个关联的计数器,通常称其为引用计数,无论何时我们拷贝一个shared_ptr,计数器都会递增。当我们给shared_ptr赋予一个新值或是shared_ptr被销毁(例如一个局部的shared_ptr离开其作用域)时,计数器就会递减,一旦一个shared_ptr的计数器变为0,它就会自动释放自己所管理的对象。

成员函数

1
2
3
4
5
6
make_shared<T>(args)//返回一个shared_ptr,指向一个动态分配的类型为T的对象,使用args初始化此对象
use_count() //返回引用计数的个数
unique() //返回是否是独占所有权(use_count 为 1)
swap(p,q) //交换两个shared_ptr对象(即交换所拥有的对象)
reset() //放弃内部对象的所有权或拥有对象的变更, 会引起原有对象的引用计数的减少
get() //返回内部对象(指针), 由于已经重载了()方法, 因此和直接使用对象是一样的

简单例子

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
int main()
{
string *s1 = new string("s1");

shared_ptr<string> ps1(s1);
shared_ptr<string> ps2;
ps2 = ps1;

cout << ps1.use_count()<<endl; //2
cout<<ps2.use_count()<<endl; //2
cout << ps1.unique()<<endl; //0

string *s3 = new string("s3");
shared_ptr<string> ps3(s3);

cout << (ps1.get()) << endl; //033AEB48
cout << ps3.get() << endl; //033B2C50
swap(ps1, ps3); //交换所拥有的对象
cout << (ps1.get())<<endl; //033B2C50
cout << ps3.get() << endl; //033AEB48

cout << ps1.use_count()<<endl; //1
cout << ps2.use_count() << endl;//2
ps2 = ps1;
cout << ps1.use_count()<<endl; //2
cout << ps2.use_count() << endl;//2
ps1.reset(); //放弃ps1的拥有权,引用计数的减少
cout << ps1.use_count()<<endl; //0
cout << ps2.use_count()<<endl; //1
}

unique_ptr

unique_ptr实现独占式拥有或严格拥有概念,保证同一时间内只有一个智能指针可以指向该对象。它对于避免资源泄露(例如“以new创建对象后因为发生异常而忘记调用delete”)特别有用。

另外unique_ptr还有更聪明的地方:当程序试图将一个 unique_ptr 赋值给另一个时,如果源 unique_ptr 是个临时右值,编译器允许这么做;如果源 unique_ptr 将存在一段时间,编译器将禁止这么做,比如:

1
2
3
4
5
unique_ptr<string> pu1(new string ("hello world")); 
unique_ptr<string> pu2;
pu2 = pu1; // #1 不允许
unique_ptr<string> pu3;
pu3 = unique_ptr<string>(new string ("You")); // #2 允许

其中#1留下悬挂的 unique_ptr(pu1),这可能导致危害。而#2不会留下悬挂的 unique_ptr,因为它调用 unique_ptr 的构造函数,该构造函数创建的临时对象在其所有权让给 pu3 后就会被销毁。

:如果确实想执行类似与#1的操作,要安全的重用这种指针,可给它赋新值。C++ 有一个标准库函数std::move(),让你能够将一个 unique_ptr 赋给另一个。尽管转移所有权后还是有可能出现原有指针调用(调用就崩溃)的情况。但是这个语法能强调你是在转移所有权,让你清晰的知道自己在做什么,从而不乱调用原有指针。操作如下:

1
2
3
4
5
6
7
8
9
int main()
{
unique_ptr<string> pu1(new string("hello world"));
unique_ptr<string> pu2;
pu2 = move(pu1);
cout << *pu1; //抛出异常,程序崩溃
cout << *pu2; //hello world

}

成员函数

1
2
3
4
5
6
7
8
9
make_unique<T>(args)//c++14才引入的,c++11没有,用法与make_shared异曲同工
release()
/*释放,调用后智能指针和其所指向对象的联系再无联系,但是该内存仍然存在有效。它会返回裸指针,但是该智能指针被置空。
返回的裸指针我们可以手工delete来释放,也可以用来初始化另外一个智能指针,或者给另外一个智能指针赋值。*/
swap(p,q) //交换两个unique_ptr对象(即交换所拥有的对象)
reset()
/*reset()不带参数情况:释放智能指针所指向的对象(释放因为它是独占,而不像shared_ptr还需要考虑引用计数),并将智能指针置空。
reset()带参数时:释放智能指针所指向的对象,并将该智能指针指向新对象。*/
get() //返回内部对象(指针), 由于已经重载了()方法, 因此和直接使用对象是一样的

在C++11自制make_unique

1
2
3
4
template<typename T, typename ...Args>
std::unique_ptr<T> make_unique( Args&& ...args ) {
return std::unique_ptr<T>( new T( std::forward<Args>(args)... ) );
}

weak_ptr

share_ptr虽然已经很好用了,但是有一点share_ptr智能指针还是有内存泄露的情况,当两个对象相互使用一个shared_ptr成员变量指向对方,会造成循环引用,使引用计数失效,从而导致内存泄漏。

weak_ptr 是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象. 进行该对象的内存管理的是那个强引用的shared_ptr, weak_ptr只是提供了对管理对象的一个访问手段。weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少。weak_ptr是用来解决shared_ptr相互引用时的死锁问题,如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。它是对对象的一种弱引用,不会增加对象的引用计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_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
class B;	//声明
class A
{
public:
shared_ptr<B> pb_;
~A()
{
cout << "A delete\n";
}
};

class B
{
public:
shared_ptr<A> pa_;
~B()
{
cout << "B delete\n";
}
};

void fun()
{
shared_ptr<B> pb(new B());
shared_ptr<A> pa(new A());
cout << pb.use_count() << endl; //1
cout << pa.use_count() << endl; //1
pb->pa_ = pa;
pa->pb_ = pb;
cout << pb.use_count() << endl; //2
cout << pa.use_count() << endl; //2
}

int main()
{
fun();
return 0;
}

可以看到fun函数中pa ,pb之间互相引用,两个资源的引用计数为2,当要跳出函数时,智能指针pa,pb析构时两个资源引用计数会减1,但是两者引用计数还是为1,导致跳出函数时资源没有被释放(A、B的析构函数没有被调用)运行结果没有输出析构函数的内容,造成内存泄露。

如果把其中一个改为weak_ptr就可以了,我们把类A里面的 shared_ptr pb_,改为 weak_ptr pb_ ,运行结果如下:

1
2
3
4
5
6
1
1
1
2
B delete
A delete

这样的话,资源B的引用开始就只有1,当pb析构时,B的计数变为0,B得到释放,B释放的同时也会使A的计数减1,同时pa析构时使A的计数减1,那么A的计数为0,A得到释放。

注意:我们不能通过weak_ptr直接访问对象的方法,比如B对象中有一个方法print(),我们不能这样访问,pa->pb_->print(),因为pb_是一个weak_ptr,应该先把它转化为shared_ptr,如:

1
2
shared_ptr<B> p = pa->pb_.lock();//因为weak_ptr 没有重载*和->,但可以使用lock()获得一个可用的 shared_ptr 对象. 
p->print();

成员函数

1
2
3
4
lock()      //用于获取所管理的对象的强引用(shared_ptr). 如果 expired 为 true, 返回一个空的 shared_ptr; 否则返回一个 shared_ptr, 其内部对象指向与 weak_ptr 相同.
expired() //用于检测所管理的对象是否已经释放, 如果已经释放, 返回 true; 否则返回 false
use_count() //返回与 shared_ptr 共享的对象的引用计数.
reset() //将 weak_ptr 置空.

weak_ptr 支持拷贝或赋值, 但不会影响对应的 shared_ptr 内部对象的计数。

如果一块内存被shared_ptr和weak_ptr同时引用,当所有shared_ptr析构了之后,不管还有没有weak_ptr引用该内存,内存也会被释放。所以weak_ptr不保证它指向的内存一定是有效的,在使用之前使用函数lock()检查weak_ptr是否为空指针。