动态内存的使用很容易出问题,因为确保在正确的时间释放内存是极为困难的。有时我们会忘记释放内存产生内存泄漏,有时提前释放了内存,再使用指针去引用内存就会报错。
为了更容易(同时也更安全)地使用动态内存,新的标准库提供了两种智能指针类型来管理动态对象。智能指针的行为类似常规指针,区别在于它负责自动释放所指向的对象。这两种智能指针的区别在于管理底层指针的方式:shared_ptr允许多个shared_ptr类型指针指向同一个对象;unique_ptr则“独占”所指向的对象。标准库还定义了一个名为weak_ptr的伴随类,它是一种弱引用,指向shared_ptr所管理的对象。这三种类型都定义在memory头文件中。
1 shared_ptr 类
类似vector,智能指针也是模板。因此,当定义智能指针时,必须在尖括号内给出类型,如下所示:
shared_ptr p1; // shared_ptr,可以指向string类型的对象
shared_ptr> p1; // shared_ptr,可以指向int类型的list的对象
默认初始化的智能指针中保存着一个空指针。智能指针的使用方式与普通指针类似,解引用一个智能指针返回它指向的对象。
下面列出了 shared_ptr 和 unique_ptr 都支持的操作。
shared_ptr sp //空shared_ptr智能指针,可以指向类型为T的对象
unique_ptr up //空unique_ptr智能指针,可以指向类型为T的对象
p //将p用作一个条件判断,若p指向一个对象,则为ture
*p //解引用p,获得它指向的对象
p->mem //等价于(*p).mem
p.get() //返回p中保存的指针
swap(p,q) //交换p和q中的指针
p.swap(q)
下面列出了 shared_ptr 独有的操作。
make_shared(args) //返回一个shared_ptr,指向一个动态分配的类型为T的对象。使用args初始化此对象
shared_ptr p(q) //p是shared_ptr q的拷贝;此操作会递增q中的引用计数。q中的指针必须能转换成T*
p = q //p和q都是shared_ptr,所保存的指针必须能相互转换。此操作会递减p中的引用计数,递增q中的引用计数。若p中的引用计数变为0,则将其管理的原内存释放
p.unique() //若p.use_count()为1,返回true;否则返回false
p.use_count() //返回与p共享对象的智能指针数量;可能很慢,主要用于调试
下面介绍一些改变 shared_ptr 的其他方法:
p.reset () //若p是唯一指向其对象的shared_ptr,reset会释放此对象。
p.reset(q) //若传递了可选的参数内置指针q,会令P指向q,否则会将P置为空。
p.reset(q, d) //若还传递了参数d,将会调用d而不是delete 来释放q
make_shared函数
最安全的分配和使用动态内存的方法是调用一个名为make_shared的标准库函数。 此函数在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr。与智能指针一样,make_shared也定义在头文件memory中。
当要用make_shared时,必须指定想要创建的对象的类型。定义方式与模板类相同, 在函数名之后跟一个尖括号,在其中给出类型:
//指向一个值为42的int的shared_ptr
shared_ptr p3 = make_shared(42);
//p4 指向一个值为"9999999999"的string
shared_ptr p4 = make_shared(10,'9');
//p5指向一个值初始化的int
shared_ptr p5 = make_shared();
当然,我们通常用auto定义一个对象来保存make_shared的结果,这种方式较为简单:
//p6指向一个动态分配的空vector
auto p6 = make_shared();
shared_ptr的拷贝和赋值
我们可以认为每个shared_ptr都有一个关联的计数器,通常称其为引用计数(reference count)。无论何时我们拷贝一个shared_ptr,例如,当用一个 shared_ptr初始化另一个shared_ptr,或将它作为参数传递给一个函数以及作为函数的返回值时,它所关联的引用计数就会递增。而当我们给shared_ptr赋予一个新值或者shared_ptr被销毁时,引用计数就会递减。
一旦一个shared_ptr的计数器变为0,它就会自动释放自己所管理的对象:
auto p = make_shared (42); //p指向的对象只有p一个引用者
auto q(p); //p和q指向相同对象,此对象有两个引用者
auto r = make_shared (42); //r指向的int只有一个
r = q; //给r赋值,令它指向另一个地址
//递增q指向的对象的引用计数
//递减r原来指向的对象的引用计数
//r原来指向的对象已没有引用者,会自动释放
此例中我们分配了一个int,将其指针保存在r中。接下来,我们将一个新值赋予r。在此情况下,r是唯一指向此int的shared_ptr,在把q赋给r的过程中,此int被自动释放。
shared_ptr自动销毀所管理的对象……
当指向一个对象的最后一个shared_ptr被销毁时,Shared_ptr类会自动销毁此 对象。它是通过另一个特殊的成员函数——析构函数完成销毁工作的。shared_ptr的析构函数会递减它所指向的对象的引用计数。如果引用计数变为0,
shared_ptr的析构函数就会销毁对象,并释放它占用的内存。
shared_ptr和new结合使用
我们还可以用new返回的指针来初始化智能指针,如下所示:
shared_ptr p2(new int (42)); //p2 指向一个值为 42 的 int
接受指针参数的智能指针构造函数是explicit的。因此,我们不能将一个内置指针隐式转换为一个智能指针,必须使用直接初始化形式来初始化一个智能指针:
shared_ptr pi = new int (1024); //错误:必须使用直接初始化形式
shared_ptr p2(new int(1024)); //正确:使用了直接初始化形式
出于相同的原因,一个返回shared_ptr的函数不能在其返回语句中隐式转换一个普通指针:
shared_ptr clone(int p)
{
return new int(p); //错误:隐式转换为 shared_ptr
}
我们必须将shared_ptr显式绑定到一个想要返回的指针上:
shared_ptr clone(int p)
{
return shared_ptr(new int(p)); //正确:显式地用int*创建shared_ptr
}
不要混合使用普通指针和智能指针
考虑下面对 shared_ptr 进行操作的函数:
//在函数被调用时 ptr 被创建并初始化
void process(shared_ptr ptr)
{
//使用ptr
}//ptr离开作用域,被销毁
int main()
{
shared_ptr p( new int (42) ) ; //引用计数为 1
process (p);//拷贝p会递增它的引用计数;在process中引用计数值为2
int i = *p; //正确:引用计数值为1
}
下面考虑混合使用普通指针和智能指针的情况。虽然不能传递给process —个内置指针,但可以传递给它一个(临时的) shared_ptr,这个shared_ptr是用一个内置指针显式构造的。但是,这样做很可能会导致错误:
int *x(new int(1024)); //危险:x是一个普通指针,不是一个智能指针
process (x);// 错误:不能将 int*转换为一个 shared_ptr
process ( shared_ptr (x) ); // 合法的,但内存会被释放!
int j = *x; //未定义的:x是一个空悬指针!
在上面的调用中,我们将一个临时shared_ptr传递给process。当这个调用所在的表达式结束时,这个临时对象就被销毁了。销毁这个临时变量会递减引用计数,此时引用计数就变为0 了。因此,当临时对象被销毁时,它所指向的内存会被释放。但x继续指向(已经释放的)内存,从而变成一个空悬指针。如果试图使用x的值,其行为是未定义的。
2 unique_ptr 类
一个unique_ptr “拥有”它所指向的对象。与shared_ptr不同,某个时刻只能有一个unique_ptr指向一个给定对象。当unique_ptr被销毁时,它所指向的对象也被销毁。
与shared_ptr不同,没有类似make_shared的标准库函数返回一个unique_ptr。当我们定义一个unique_ptr时,需要将其绑定到一个new返回的指针上。类似shared_ptr,初始化unique_ptr必须采用直接初始化形式:
unique_ptr p1; //指向一个double的unique_ptr
unique_ptr p2(new int(42)); //p2指向一个值为42的int
由于一个unique_ptr拥有它指向的对象,因此unique_ptr不支持普通的拷贝或 赋值操作:
unique_ptr p1(new string("Stegosaurus"));
unique_ptr p2 (p1); //错误:unique_ptr 不支持拷贝
unique_ptr p3;
p3 = p2; //错误:unique_ptr不支持赋值
下面列出了unique_ptr特有的操作。
unique_ptr u1 //空unique_ptr,可以指向类型为T的对象。u1会使用delete来释放它的指针
unique_ptr u2 //u2会使用一个类型为D的可调用对象来释放它的指针
unique_ptr u(d) //空unique_ptr,指向类型为T的对象,用类型为D的对象d替代delete
u = nullptr //释放u指向的对象,将u置为空
u.release() //u放弃对指针的控制权,返回指针,并将u置为空
u.reset() //释放u指向的对象
u.reset(q) //如果提供了内置指针q,另u指向这个对象;否则将u置为空'
u.reset(nullptr)
虽然我们不能拷贝或赋值unique_ptr,但可以通过调用release或reset将指针的 所有权从一个(非const)unique_ptr转移给另一个unique:
//将所有权从pl (指向string Stegosaurus)转移给p2
unique_ptr p2(p1, release()); //release 将 p1 置为空
unique_ptr p3(new string("Trex"));
//将所有权从p3转移给p2
p2.reset(p3.release()); //reset 释放了 p2 原来指向的内存
release成员返回unique_ptr当前保存的指针并将其置为空。因此,p2被初始化为 p1原来保存的指针,而p1被置为空。
调用release会切断unique_ptr和它原来管理的对象间的联系,如果我们不用另一个智能指针来保存release返回的指针,我们的程序就要负责资源的释放:
p2.release(); //错误:p2不会释放内存,而且我们丢失了指针
auto p = p2.release(); //正确,但我们必须记得 delete(p)
传递unique_ptr参数和返回unique_ptr
不能拷贝unique_ptr的规则有一个例外:我们可以拷贝或赋值一个将要被销毁的 unique_ptr。最常见的例子是从函数返回一个unique_ptr:
unique_ptr clone(int p)
{
// 正确:从 int*创建一个 unique_ptr
return unique_ptr(new int(p));
}
还可以返回一个局部对象的拷贝:
unique_ptr clone (int p)
{
unique_ptr ret(new int (p));
//…
return ret;
}
对于上面两段代码,编译器都知道要返回的对象将要被销毁。在此情况下,编译器执行一种特殊的“拷贝”,在《C++ Primer》13.6.2节(第473页)中有介绍。
3 weak_ptr 类
weak_ptr是一种不控制所指向对象生存期的智能指针,它指向由一个shared_ptr管理的对象。将一个weak_ptr绑定到一个shared_ptr不会改变 shared_ptr的引用计数。一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放。即使有weak_ptr指向对象,对象也还是会被释放,因此,weak_ptr的名字抓住了这种智能指针“弱”共享对象的特点。
下面列出了weak_ptr的操作。
weak_ptr w //空weak_ptr可以指向类型为T的对象
weak_ptr w(sp) //与shared_ptr sp指向相同对象的weak_ptr。T必须能转换为sp指向的S型
w = p //p可以是一个shared_ptr或一个weak_ptr。赋值后w与p共享对象
w.reset() //将W置为空
w.use_count() //与w共享对象的shared ptr的数量
w.expired() //若 w.use_count()为0,返回true,否贝y返回 false
w.lock() //如果expired为true,返回一个空shared ptr:否则返回一个 指向w的对象的shared_ptr
当我们创建一个weak_ptr时,要用一个shared_ptr来初始化它:
auto p = make_shared(42);
weak_ptr wp(p); // wp弱共享p; p的引用计数未改变
本例中wp和p指向相同的对象。由于是弱共享,创建wp不会改变p的引用计数;wp指向的对象可能被释放掉。
由于对象可能不存在,我们不能使用weak_ptr直接访问对象,而必须调用lock。 此函数检查weak_ptr指向的对象是否仍存在。如果存在,lock返回一个指向共享对象 的shared_ptr。与任何其他shared_ptr类似,只要此shared_ptr存在,它所指向的底层对象也就会一直存在。例如:
if ( shared_ptr np = wp.lock() )
{
// 如果 np 不为空则条件成立
//在if中,np与p共享对象
}
在这段代码中,只有当lock调用返回true时我们才会进入if语句体。在if中,使用 np访问共享对象是安全的。
参考
《C++ Primer 第5版》