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