@目录
一、简介
动态内存的使用很容易出问题,因为确保在正确的时间释放内存是极其困难的。有时我们会忘记释放内存,在这种情况下就会产生内存泄漏;有时在尚有指针引用内存的情况下我们就释放了它,在这种情况下就会产生引用非法内存的指针。
为了更容易(同时也更安全)地使用动态内存,新的标准库提供了两种智能指针(smartpointer)类型来管理动态对象。智能指针的行为类似常规指针,重要的区别是它负责自动释放所指向的对象。新标准库提供的这两种智能指针的区别在于管理底层指针的方式:
-
shared_ ptr
允许多个指针指向同一个对象; -
unique_ ptr
则“独占”所指向的对象。
标准库还定义了一个名为weak_ ptr的伴随类,它是一种弱引用,指向shared_ ptr 所管理的对象。这三种类型都定义在memory头文件中。
1.1 程序使用动态内的原因:
- 程序不知道自己需要使用多少对象
- 程序不知道所需对象的准确类型
- 程序需要在多个对象间共享数据
二、shared_ ptr 类
类似vector,智能指针也是模板。因此,当我们创建一个智能指针时,必须提供额外的信息:——指针可以指向的类型。与vector一样,我们在尖括号内给出类型,之后是所定义的这种智能指针的名字:
shared_ptr<string> p1; // shared_ ptr, 可以指向string
shared_ ptr<list<int>> p2; // shared_ ptr,可以指向int的list
默认初始化的智能指针中保存着一个空指针。
2.1 make_ shared 函数
最安全的分配和使用动态内存的方法是调用一个名为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指向一个值初始化的(参见3.3.1节,第88页)int,即,值为0
shared_ ptr<int> p5 = make_shared<int>();
类似顺序容器的emplace成员,make_ shared 用其参数来构造给定类型的对象。例如,调用make_ shared
2.2 shared_ptr 的拷贝和赋值
当进行拷贝或赋值操作时,每个shared_ptr都会记录有多少个其他shared_ptr指向相同的对象:
auto p = make_shared<int>(42); // p指向的对象只有p一个引用者
auto q(p); // p和q指向相同对象,此对象有两个引用者.
我们可以认为每个shared_ ptr都有一个关联的计数器,通常称其为引用计数( reference count)。无论何时我们拷贝一个shared_ptr,计数器都会递增。例如,当用一个shared_ ptr初始化另一个shared_ptr,或将它作为参数传递给一个函数以及作为函数的返回值时,它所关联的计数器就会递增。当我们给shared_ptr赋予一个新值或是shared_ ptr被销毁(例如一个局部的shared_ptr 离开其作用域)时,计数器就会递减。
一但一个shared_ptr的计数器变为0,它就会自动释放自己所管理的对象:
auto r = make_ shared<int>(42); // r指向的int只有一个引用者
r=q; //给r赋值,令它指向另一个地址
//递增q指向的对象的引用计数
//递减r原来指向的对象的引用计数
// r原来指向的对象已没有引用者,会自动释放
2.3 shared_ ptr 和 new 结合使用‘
推荐使用make_shared而不是new
如果我们不初始化一个智能指针,它就会被初始化为一个空指针。如表12.3所示,我们还可以用new返回的指针来初始化智能指针:
shared_ ptr<double> pl; // shared_ptr 指向一个double的空指针
shared_ ptr<int> p2 (new int(42)); // p2指向一个值为42的int
接受指针参数的智能指针构造函数是explicit的。因此,我们不能将一个内置指针隐式转换为一个智能指针,必须使用直接初始化形式来初始化一个智能指针:
shared_ ptr<int> p1 = new int(1024); // 错误:必须使用直接初始化形式
shared_ ptr<int> p2 (new int (1024)) ;//正确:使用了直接初始化形式
p1的初始化隐式地要求编译器用一个new返回的int*来创建一个shared_ptr。由于我们不能进行内置指针到智能指针间的隐式转换,因此这条初始化语句是错误的。出于相同的原因,一个返回shared_ptr 的函数不能在其返回语句中隐式转换一个普通指针:
shared_ _ptr<int> clone(int p) {
return new int(p); //错误:隐式转换为shared ptr<int>
}
我们必须将shared_ ptr 显式绑定到一个想要返回的指针上:
shared_ ptr<int> clone (int p) {
//正确:显式地用int*创建shared_ ptr<int>
return shared_ ptr<int> (new int(p) ) ;
}
2.4 不要混合使用普通指针和智能指针......
shared_ ptr可以协调对象的析构,但这仅限于其自身的拷贝(也是shared_ ptr)之间。这也是为什么我们推荐使用make_ shared 而不是new的原因(因为new出来的是一个普通指针,在进行传递时可能引发错误,可以看下面的例子)。这样,我们就能在分配对象的同时就将shared_ ptr 与之绑定,从而避免了无意中将同一块内存绑定到多个独立创建的shared_ptr上。
考虑下面对shared_ ptr 进行操作的函数:
//在函数被调用时ptr被创建并初始化
void process (shared_ptr<int> ptr){
//使用ptr
} // ptr离开作用域,被销毁
process的参数是传值方式传递的,因此实参会被拷贝到ptr中。拷贝一个shared_ ptr会递增其引用计数,因此,在process运行过程中,引用计数值至少为2。当process结束时,ptr 的引用计数会递减,但不会变为0。因此,当局部变量ptr被销毁时,ptr指向的内存不会被释放。
使用此函数的正确方法是传递给它一个sharedptr:
shared_ptr<int> p(new int(42)); // 引用计数为1
process(p); //拷贝P会递增它的引用计数;在process中引用计数值为2
inti=*p;//正确:引用计数值为1
虽然不能传递给process 一个内置指针,但可以传递给它一个(临时的)shared_ptr,这个shared_ptr是用一个内置指针显式构造的。但是,这样做很可能会导致错误:
int *x(new int (1024)) ;
//危险: x是一个普通指针,不是一个智能指针
process(x); //错误:不能将int*转换为一个shared ptr<int>
process(shared_ptr<int>(x)); // 合法的,但内存会被释放!
intj=*x;//未定义的:x是一个空悬指针!
在上面的调用中,我们将一个临时 shared_ptr 传递给process。当这个调用所在的表达式结束时,这个临时对象就被销毁了。销毁这个临时变量会递减引用计数,此时引用计数就变为0了。因此,当临时对象被销毁时,它所指向的内存会被释放。
但x继续指向(已经释放的)内存,从而变成一个空悬指针。如果试图使用x的值,其行为是未定义的。
当将一个shared_ ptr绑定到一个普通指针时,我们就将内存的管理责任交给了这个shared_ ptr。旦这样做了 ,我们就不应该再使用内置指针来访问shared ptr 所指向的内存了。
2.5 不要使用get初始化另一个智能指针或为智能指针赋值
智能指针类型定义了一个名为get的函数(参见表12.1), 它返回一个内置指针,指向智能指针管理的对象。此函数是为了这样一种情况而设计的我们需要向不能使用智能指针的代码传递一个内置指针。使用get返回的指针的代码不能delete此指针。
虽然编译器不会给出错误信息,但将另一个智能指针也绑定到get返回的指针上是错误的: (因为此时相当于把内置指针赋给了智能指针,当然会出错了)
shared_ptr<int> p(new int(42)); //引用计数为1
int *q = p.get(); //正确:但使用q时要注意,不要让它管理的指针被释放
{
//新程序块
//未定义:两个独立的shared_ ptr指向相同的内存
shared_ ptr<int>(q) ;
} //程序块结束,q被销毁,它指向的内存被释放
int foo = *p; //未定义: p指向的内存已经被释放了
在本例中,p和q指向相同的内存。由于它们是相互独立创建的,因此各自的引用计数都是1。当q所在的程序块结束时,q被销毁,这会导致q指向的内存被释放。从而p变成一个空悬指针,意味着当我们试图使用p时,将发生未定义的行为。而且,当p被销毁时这块内存会被第二次delete。
2.6 reset()操作
我们可以用reset来将一个新的指针赋予一个shared_ptr:
p = new int (1024) ;
//错误:不能将一个指针赋予shared_ptr
p.reset(new int(1024)); // 正确: p指向一个新对象
与赋值类似,reset会更新引用计数,如果需要的话,会释放p指向的对象。reset成员经常与unique一起使用,来控制多个shared_ ptr 共享的对象。在改变底层对象之前,我们检查自己是否是当前对象仅有的用户。如果不是,在改变之前要制作一份新的拷贝:
if (!p.unique())
p.reset (new string(*p)); // 我们不是唯一用户;分配新的拷贝
*p += newVal; //现在我们知道自己是唯一的用户,可以改变对象的值
2.7 使用我们自己的释放操作
struct destination; //表示我们正在连接什么
struct connection; //使用连接所需的信息
connection connect(destination*); //打开连接
void disconnect(connection); //关闭给定的连接
默认情况下,shared_ptr假定它们指向的是动态内存。因此,当一个shared_ptr被销毁时,它默认地对它管理的指针进行delete操作。为了用shared_ptr来管理一个connection,我们必须首先定义一个函数来代替delete。这个删除器(deleter) 函数必须能够完成对shared_ ptr中保存的指针进行释放的操作。在本例中,我们的删除器必须接受单个类型为connection*的参数:
void end_connection (connection *p){ disconnect (*p); }
当我们创建一个shared_ ptr时,可以传递一个(可选的)指向删除器函数的参数
void f(destination &d /*其他参数*/)
{
connection C = connect(&d) ;
shared_ptr<connection> p(&C, end_ connection) ;
//使用连接
//当f退出时(即使是由于异常而退出), connection会被正确关闭
}
当p被销毁时,它不会对自己保存的指针执行delete,而是调用end_connection.接下来,end_connection 会调用disconnect,从而确保连接被关闭。如果f正常退出,那么p的销毁会作为结束处理的一部分。如果发生了异常,P同样会被销毁,从而连接被关闭。
三、unique_ptr类
3.1 与shared_ptr的区别及基本操作
与shared_ptr不同,某个时刻只能有一个unique_ ptr指向一个给定对象。当unique_ ptr被销毁时,它所指向的对象也被销毁。表12.4 列出了unique_ ptr特有的操作。与shared_ ptr相同的操作列在表12.1中。
与shared_ ptr不同,没有类似make_ shared 的标准库函数返回一个unique_ ptr。当我们定义一个unique_ptr 时,需要将其绑定到一个new返回的针上。类似shared_ ptr,初始化unique_ ptr必须采用直接初始化形式:
unique_ptr<double> p1; //可以指向一个double的unique_ _ptr
unique_ptr<int> 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不支持赋值
3.2 release()和reset()
虽然我们不能拷贝或赋值unique_ptr, 但可以通过调用release或reset将指针的所有权从一个(非const) unique_ptr 转移给另一个unique:
//将所有权从p1 (指向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被置为空。
reset
成员接受一个可选的指针参数,令unique_ptr 重新指向给定的指针。如果unique_ptr不为空,它原来指向的对象被释放。因此,对p2调用reset释放了用"Stegosaurus"初始化的string所使用的内存,将p3对指针的所有权转移给p2,并将p3置为空。
调用release会切断unique_ptr 和它原来管理的对象间的联系。release 返回的指针通常被用来初始化另一个智能指针或给另一个智能指针赋值。在本例中,管理内存的责任简单地从一个智能指针转移给另一个。但是,如果我们不用另一个智能指针来保存release返回的指针,我们的程序就要负责资源的释放:
p2. release() ; //错误: p2不会释放内存,而且我们丢失了指针
auto P = p2.release () ; //正确,但我们必须记得delete(p)
3.3 传递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;
}
对于两段代码,编译器都知道要返回的对象将要被销毁。在此情况下,编译器执行一种殊的“拷贝”。
3.4 向unique_ ptr传递删除器
类似shared_ptr,unique_ptr默认情况下用delete 释放它指向的对象。与
shared_ptr一样,我们可以重载一个unique_ ptr 中默认的删除器。但是,unique_ ptr管理删除器的方式与shared_ ptr 不同。重载一个unique_ ptr中的删除器会影响到unique_ptr 类型以及如何构造(或reset)该类型的对象。与重载关联容器的比较操作类似,我们必须在尖括号中unique_ptr 指向类型之后提供删除器类型。在创建或reset一个这种unique_ ptr类型的对象时,必须提供一个指定类型的可调用对象(删除器):
// p指向一个类型为objT的对象,并使用一个类型为delT的对象释放objT对象
//它会调用一个名为fcn的delT类型对象
unique_ ptr<objT, delT> P (new objT,fcn) ;
作为一个更具体的例子,我们将重写连接程序,用unique_ ptr 来代替shared_ ptr,如下所示:
void f(destination &d /*其他需要的参数*/)
connection c = connect(&d); // 打开连接
//当p被销毁时,连接将会关闭
unique_ ptr<connection, decltype(end_ connection) *>
p(&C,end_connection) ;
//使用连接
//当f退出时(即使是由于异常而退出), connection会被正确关闭
}
在本例中我们使用了decltype来指明函数指针类型。由于decltype (end_connection) 返回一个函数类型,所以我们必须添加一个*来指出我们
正在使用该类型的一个指针。
四、weak_ptr类
4.1 创建对象
weak_ ptr (见表12.5)是一种不控制所指向对象生存期的智能指针,它指向由一个shared ptr管理的对象。将一个weak_ ptr绑定到一个shared_ ptr不会改变shared_ ptr的引用计数。一旦最后一个指向对象的shared_ptr 被销毁,对象就会被释放。即使有weak_ ptr指向对象,对象也还是会被释放。因此,weak_ ptr的名字抓住了这种智能指针“弱”共享对象的特点。
当我们创建一个 weak_ ptr 时,要用一个shared_ ptr来初始化它:
auto p = make_shared<int>(42);
weak_ptr<int> wp(p); // wp弱共享p; p的引用计数未改变
本例中wp和P指向相同的对象。由于是弱共享,创建wp不会改变p的引用计数; wp指向的对象可能被释放掉。
4.2 访问对象
由于对象可能不存在,我们不能使用weak_ ptr直接访问对象,而必须调用lock。此函数检查weak_ptr 指向的对象是否仍存在。如果存在,lock 返回一个指向共享对象的shared_ ptr。 与任何其他shared_ ptr类似,只要此shared_ ptr 存在,它所指向的底层对象也就会一直存在。例如:
if (shared_ptr<int> np = wp.1ock()) { //如果np不为空则条件成立
//在if中,np与p共享对象
}
在这段代码中,只有当lock调用返回true时我们才会进入if语句体。在if中,使用np访问共享对象是安全的。