shared_ptr 类
类似 vector,智能指针也是模板。因此,当我们创建一个智能指针时,必须提供额外的信息 —— 指针可以指向的类型。与 vector 一样,我们在尖括号内给出类型,之后是所定义的这种智能指针的名字:
shared_ptr<string> p1; // shared_ptr,可以指向 string
shared_ptr<list<int>> p2; // shared_ptr,可以指向 int 的 list
默认初始化的智能指针中保存着一个空指针。
智能指针的使用方式与普通指针类似。解引用一个智能指针返回它指向的对象。如果在一个条件判断中使用智能指针,效果就是检测它是否为空:
// 如果 p1 不为空,检查它是否指向一个空 string
if (p1 && p1->empty()){
*p1="hi"; // 如果 p1 指向一个空 string,解引用 p1,将一个新值赋予 string
}
shared_ptr 和 unique_ptr 都支持的操作
操作 | 说明 |
---|---|
shared_ptr<T> sp unique_ptr<T> up |
空智能指针,可以指向类型为 T 的对象 |
p | 将 p 用作一个条件判断,若 p 指向一个对象,则为 true |
*p | 解引用 p,获得它指向的对象 |
p->mem | 等价于(*p).mem |
p.get() | 返回 p 中保存的指针。要小心使用,若智能指针释放了其对象,返回的指针所指向的对象也就消失了 |
swap(p, q) p.swap(q) |
交换 p 和 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 共享对象的智能指针数量:可能很慢,主要用于调试 |
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 指向一个值初始化的 int,即,值为 0
shared_ptr<int> p5 = make_shared<int>();
类似顺序容器的 emplace 成员,make_shared 用其参数来构造给定类型的对象。例如,调用 make_shared<string> 时传递的参数必须与 string 的某个构造函数相匹配,调用 make_shared<int> 时传递的参数必须能用来初始化一个 int,依此类推。如果我们不传递任何参数,对象就会进行值初始化。
当然,我们通常用 auto 定义一个对象来保存 make_shared 的结果,这种方式较为简单:
// p6 指向一个动态分配的空 vector<string>
auto p6 = make_shared<vector<string>>();
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 原来指向的对象已没有引用者,会自动释放
此例中我们分配了一个 int,将其指针保存在 r 中。接下来,,我们将一个新值赋予 r。在此情况下,r 是唯一指向此 int 的 shared_ptr,在把 q 赋给 r 的过程中,此 int 被自动释放。
注意:到底是用一个计数器还是其他数据结构来记录有多少指针共享对象,完全由标准库的具体实现来决定。关键是智能指针类能记录有多少个 shared_ptr 指向相同的对象,并能在恰当的时候自动释放对象。
shared_ptr 自动销毁所管理的对象 ……
当指向一个对象的最后一个 shared_ptr 被销毁时,shared_ptr 类会自动销毁此对象。它是通过析构函数完成销毁工作的。
析构函数一般用来释放对象所分配的资源。例如,string 的构造函数(以及其他 string 成员)会分配内存来保存构成 string 的字符,string 的析构函数就负责释放这些内存。类似的, vector 的若干操作都会分配内存来保存其元素。vector 的析构函数就负责销毁这些元素,并释放它们所占用的内存。
shared_ptr 的析构函数会递减它所指向的对象的引用计数。如果引用计数变为 0,shared_ptr 的析构函数就会销毁对象,并释放它占用的内存。
…… shared_ptr 还会自动释放相关联的内存
当动态对象不再被使用时,shared_ptr 类会自动释放动态对象,这一特性使得动态内存的使用变得非常容易。例如,我们可能有一个函数,它返回一个 shared_ptr,指向一个 Foo 类型的动态分配的对象,对象是通过一个类型为 T 的参数进行初始化的:
// factory 返回一个 shared_ptr,指向一个动态分配的对象
shared_ptr<Foo> factory(T arg) {
// 恰当地处理 arg
// shared_ptr 负责释放内存
return make_shared<Foo>(arg);
}
由于 factory 返回一个 shared_ptr,所以我们可以确保它分配的对象会在恰当的时刻被释放。例如,下面的函数将 factory 返回的 shared_ptr 保存在局部变量中:
void use_factory(T arg) {
shared_ptr<Foo> p = factory(arg);
// 使用 p
} // p 离开了作用域,它指向的内存会被自动释放掉
由于 p 是 use_factory 的局部变量,在 use_factory 结束时它将被销毁。当 p 被销毁时,将递减其引用计数并检查它是否为 0。在此例中,p 是唯一引用 factory 返回的内存的对象。由于 p 将要销毁,p 指向的这个对象也会被销毁,所占用的内存会被释放。
但如果有其他 shared_ptr 也指向这块内存,它就不会被释放掉:
void use_factory(T arg) {
shared_ptr<Foo> p = factory(arg);
// 使用 p
return p; // 当我们返回 p 时,引用计数进行了递增操作
} // p 离开了作用域,但它指向的内存不会被释放掉
在此版本中,use_factory 中的 return 语句向此函数的调用者返回一个 p 的拷贝。拷贝一个 shared_ptr 会增加所管理对象的引用计数值。现在当 p 被销毁时,它所指向的内存还有其他使用者。对于一块内存, shared_ptr 类保证只要有任何 shared_ptr 对象引用它,它就不会被释放掉。
由于在最后一个 shared_ptr 销毁前内存都不会释放,保证 shared_ptr 在无用之后不再保留就非常重要了。如果你忘记了销毁程序不再需要的 shared_ptr,程序仍会正确执行,但会浪费内存。shared_ptr 在无用之后仍然保留的一种可能情况是,你将 shared_ptr 存放在一个容器中,随后重排了容器,从而不再需要某些元素。在这种情况下,你应该确保用 erase 删除那些不再需要的 shared_ptr 元素。
注意:如果你将 shared_ptr 存放于一个容器中,而后不再需要全部元素,而只使用其中一部分,要记得用 erase 删除不再需要的那些元素。
shared_ptr 和 new 结合使用
如前所述,如果我们不初始化一个智能指针,它就会被初始化为一个空指针。我们还可以用 new 返回的指针来初始化智能指针:
shared_ptr<double>p1; // 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));
}
默认情况下,一个用来初始化智能指针的普通指针必须指向动态内存,因为智能指针默认使用 delete 释放它所关联的对象。我们可以将智能指针绑定到一个指向其他类型的资源的指针上,但是为了这样做,必须提供自己的操作来替代 delete。
定义和改变 shared_ptr 的其他方法 | 说明 |
---|---|
shared_ptr<T> p(q) | p 管理内置指针 q 所指向的对象;q 必须指向 new 分配的内存,且能够转换为 T* 类型 |
shared_ptr<T> p(u) | 从 unique_ptr u 那里接管了对象的所有权;将 u 置为空 |
shared_ptr<T> p(q, d) | p 接管了内置指针 q 所指向的对象的所有权。q 必须能转换为 T* 类型。p 将使用可调用对象 d 来代替 delete |
shared_ptr<T> p(p2, d) | p 是 shared_ptr p2 的拷贝,唯一的区别是 p 将用可调用对象 d 来代替 delete |
p.reset() p.reset(q) p.reset(q, d) |
若 p 是唯一指向其对象的 shared_ptr,reset 会释放此对象。若传递了可选的参数内置指针 q,会令 p 指向 q,否则会将 p 置为空。若还传递了参数 d,将会调用 d 而不是 delete 来释放 q |
不要混合使用普通指针和智能指针
shared_ptr 可以协调对象的析构,但这仅限于其自身的拷贝(也是 shared_ptr)之间。这也是为什么我们推荐使用 make_shared 而不是 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指向的内存不会被释放。
使用此函数的正确方法是传递给它一个 shared_ptr:
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 的值,其行为是未定义的。
当将一个 shared_ptr 绑定到一个普通指针时,我们就将内存的管理责任交给了这个 shared_ptr。一旦这样做了,我们就不应该再使用内置指针来访问 shared_ptr 所指向的内存了。
警告:使用一个内置指针来访问一个智能指针所负责的对象是很危险的,因为我们无法知道对象何时会被销毁。
……也不要使用 get 初始化另一个智能指针或为智能指针赋值
智能指针类型定义了一个名为 get 的函数,它返回一个内置指针,指向智能指针管理的对象。此函数是为了这样一种情况而设计的:我们需要向不能使用智
能指针的代码传递一个内置指针。使用 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。
警告:get 用来将指针的访问权限传递给代码,你只有在确定代码不会 delete 指针的情况下,才能使用get。特别是,永远不要用 get 初始化另一个智能指针或者为另一个智能指针赋值。
其他 shared_ptr 操作
shared_ptr 还定义了其他一些操作。我们可以用 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; // 现在我们知道自己是唯一的用户,可以改变对象的值
智能指针和异常
程序需要确保在异常发生后资源能被正确地释放。一个简单的确保资源被释放的方法是使用智能指针。
如果使用智能指针,即使程序块过早结束,智能指针类也能确保在内存不再需要时将其释放:
void f() {
shared_ptr<int> sp(new int(42)); // 分配一个新对象
// 这段代码抛出一个异常,且在 f 中未被捕获
} // 在函数结束时 shared_ptr 自动释放内存
函数的退出有两种可能,正常处理结束或者发生了异常,无论哪种情况,局部对象都会被销毁。在上面的程序中,sp 是一个 shared_ptr,因此 sp 销毁时会检查引用计数。在此例中,sp 是指向这块内存的唯一指针,因此内存会被释放掉。
与之相对的,当发生异常时,我们直接管理的内存是不会自动释放的。如果使用内置指针管理内存,且在 new 之后在对应的 delete 之前发生了异常,则内存不会被释放:
void f() {
int *ip = new int(42); // 动态分配一个新对象
// 这段代码抛出一个异常,且在 f 中未被捕获
delete ip; // 在退出之前释放内存
}
如果在 new 和 delete 之间发生异常,且异常未在 f 中被捕获,则内存就永远不会被释放了。在函数 f 之外没有指针指向这块内存,因此就无法释放它了。
智能指针和哑类
包括所有标准库类在内的很多 C++ 类都定义了析构函数,负责清理对象使用的资源。但是,不是所有的类都是这样良好定义的。特别是那些为 C 和 C++ 两种语言设计的类,通常都要求用户显式地释放所使用的任何资源。
那些分配了资源,而又没有定义析构函数来释放这些资源的类,可能会遇到与使用动态内存相同的错误 —— 程序员非常容易忘记释放资源。类似的,如果在资源分配和释放之间发生了异常,程序也会发生资源泄漏。
与管理动态内存类似,我们通常可以使用类似的技术来管理不具有良好定义的析构函数的类。例如,假定我们正在使用一个 C 和 C++ 都使用的网络库,使用这个库的代码可能是这样的:
struct destination; // 表示我们正在连接什么
struct connection; // 使用连接所需的信息
connection connect(destination*); //打开连接
void disconnect(connection); // 关闭给定的连接
void f(destination &d /* 其他参数 */) {
//获得一个连接;记住使用完后要关闭它
connection c = connect(&d);
// 使用连接
// 如果我们在 f 退出前忘记调用 disconnect,就无法关闭 c 了
}
如果 connection 有一个析构函数,就可以在 f 结束时由析构函数自动关闭连接。但是,connection 没有析构函数。这个问题与我们上一个程序中使用 shared_ptr 避免内存泄漏几乎是等价的。使用 shared_ptr 来保证 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);
// 使用连接
// 当退出时(即使是由于异常而退出),connection 会被正确关闭
}
当 p 被销毁时,它不会对自己保存的指针执行 delete,而是调用 end_connection。接下来,end_connection 会调用 disconnect,从而确保连接被关闭。如果 f 正常退出,那么 p 的销毁会作为结束处理的一部分。如果发生了异常,p 同样会被销毁,从而连接被关闭。
注意:智能指针陷阱
智能指针可以提供对动态分配的内存安全而又方便的管理,但这建立在正确使用的前提下。为了正确使用智能指针,我们必须坚持一些基本规范:
- 不使用相同的内置指针初始化(或 reset)多个智能指针。
- 不 delete get() 返回的指针。
- 不使用 get() 初始化或 reset 另一个智能指针。
- 如果你使用 get() 返回的指针,记住当最后一个对应的智能指针销毁后,你的指针就变为无效了。
- 如果你使用智能指针管理的资源不是 new 分配的内存,记住传递给它一个删除器。
shared_ptr 和动态数组
与 unique_ptr 不同,shared_ptr 不直接支持管理动态数组。如果希望使用 shared_ptr 管理一个动态数组,必须提供自己定义的删除器:
// 为了使用 shared_ptr,必须提供一个删除器
shared_ptr<int> sp(new int[10], [](int *p) { delete[] p; });
sp.reset(); // 使用我们提供的 lambda 释放数组,它使用 delete[]
本例中我们传递给 shared_ptr 一个 lambda 作为删除器,它使用 delete[] 释放数组。
如果未提供删除器,这段代码将是未定义的默认情况下,shared_ptr 使用 delete 销毁它指向的对象。如果此对象是一个动态数组,对其使用 delete 所产生的问题与释放一个动态数组指针时忘记 [] 产生的问题一样。
shared_ptr 不直接支持动态数组管理这一特性会影响我们如何访问数组中的元素:
// shared_ptr 未定义下标运算符,并且不支持指针的算术运算
for (size_t i = 0; i != 10; ++1) {
*(sp.get() + i) = i; // 使用 get 获取一个内置指针
}
shared_ptr 未定义下标运算符,而且智能指针类型不支持指针算术运算。因此,为了访问数组中的元素,必须用 get 获取一个内置指针,然后用它来访问数组元素。
网友评论