动态内存的使用很容易出问题,因为确保在正确的时间释放内存是极为困难的。有时我们会忘记释放内存产生内存泄漏,有时提前释放了内存,再使用指针去引用内存就会报错。

为了更容易(同时也更安全)地使用动态内存,新的标准库提供了两种智能指针类型来管理动态对象。智能指针的行为类似常规指针,区别在于它负责自动释放所指向的对象。这两种智能指针的区别在于管理底层指针的方式: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. 不要混合使用普通指针和智能指针

使用一个内置指针来访问一个智能指针所负责的对象是很危险的,因为我们无法知道对象何时会被销毁。

考虑下面对 shared_ptr 进行操作的函数:

//在函数被调用时 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 访问共享对象是安全的。

参考

《C++ Primer 第5版》

[C++11新特性] 智能指针详解的更多相关文章

  1. [转]C++ 智能指针详解

    转自:http://blog.csdn.net/xt_xiaotian/article/details/5714477 C++ 智能指针详解 一.简介 由于 C++ 语言没有自动内存回收机制,程序员每 ...

  2. C++ 智能指针详解(转)

    C++ 智能指针详解   一.简介 由于 C++ 语言没有自动内存回收机制,程序员每次 new 出来的内存都要手动 delete.程序员忘记 delete,流程太复杂,最终导致没有 delete,异常 ...

  3. 【C++】智能指针详解(一):智能指针的引入

    智能指针是C++中一种利用RAII机制(后面解释),通过对象来管理指针的一种方式. 在C++中,动态开辟的内存需要我们自己去维护,在出函数作用域或程序异常退出之前,我们必须手动释放掉它,否则的话就会引 ...

  4. C++11 unique_ptr智能指针详解

    在<C++11 shared_ptr智能指针>的基础上,本节继续讲解 C++11 标准提供的另一种智能指针,即 unique_ptr 智能指针. 作为智能指针的一种,unique_ptr ...

  5. 【C++】智能指针详解

    转自:https://blog.csdn.net/flowing_wind/article/details/81301001 参考资料:<C++ Primer中文版 第五版>我们知道除了静 ...

  6. C++智能指针详解

    本文出自http://mxdxm.iteye.com/ 一.简介 由于 C++ 语言没有自动内存回收机制,程序员每次 new 出来的内存都要手动 delete.程序员忘记 delete,流程太复杂,最 ...

  7. 【转】C++ 智能指针详解

    一.简介 由于 C++ 语言没有自动内存回收机制,程序员每次 new 出来的内存都要手动 delete.程序员忘记 delete,流程太复杂,最终导致没有 delete,异常导致程序过早退出,没有执行 ...

  8. C++新特性---智能指针

    智能指针:     为什么需要智能指针?         1. malloc出来的空间,没有进行释放,存在内存泄漏的问题.          2. 异常安全问题.如果在malloc和free之间如果存 ...

  9. Swift 3 新特性和迁移详解

    写在前面 Swift 3.0 正式版发布了差不多快一个月了,断断续续的把手上和 Swift 相关的迁移到了Swift 3.0.所以写点小总结. 背景 代码量(4万行) 首先,我是今年年初才开始入手 S ...

随机推荐

  1. Openwrt挂载NTFS硬盘提示“只读”错误的解决方法!

    Openwrt是基于Linux代码编写,只支持NTFS格式硬盘的只读权限,否则当挂载的NTFS硬盘写入超过2M左右,就会出现"error:read-only file system" ...

  2. 【Nginx】基本数据结构

    整型的封装 typedef intptr_t ngx_int _t;//有符号整型 typedef uintptr_t ngx_uint_t;//无符号整型 字符串的封装 typedef struct ...

  3. SQL 连接(JOIN)

    SQL 连接(JOIN) SQL join 用于把来自两个或多个表的行结合起来. SQL JOIN SQL JOIN 子句用于把来自两个或多个表的行结合起来,基于这些表之间的共同字段. 最常见的 JO ...

  4. Android开发——本地验证码的简易实现

    0.  前言   验证码无处不在.有人问我,你知道达芬奇password以下是什么吗,对.答案就是达芬奇验证码. 验证码一个最基本的作用就是防止恶意暴力破解登录,防止不间断的登录尝试,事实上能够在se ...

  5. 数据库分表和分库的原理及基于thinkPHP的实现方法

    为什么要分表,分库: 当我们的数据表数据量,訪问量非常大.或者是使用频繁的时候,一个数据表已经不能承受如此大的数据訪问和存储,所以,为了减轻数据库的负担,加快数据的存储,就须要将一张表分成多张,及将一 ...

  6. SQL yog过期后教你怎么让他不过期

    打开注册表  安装sqlyog后,进入注册表编辑器,进入\HEYK_CURRENT_USER\Software\,找到以{}括起来的那项直接干掉! 1, regedit 2,修改 3,

  7. Vs2017添加引用时报错 未能正确加载“ReferenceManagerPackage”包。

    Vs2017添加引用时报错未能正确加载“ReferenceManagerPackage”包. 最近新装了2017,开始前几天还好, 可是最近在添加引用时,报错 -------------------- ...

  8. Linux下kill命令的学习,(主要根据man手册进行的翻译)

    名字      kill -终止一个进程 格式     kill  [-s signal | -p]  [--] pid ..                                      ...

  9. PHP出现Warning: A non-numeric value encountered问题的原因及解决方法

    本文介绍php出现Warning: A non-numeric value encountered问题,用实例分析出现这种错误的原因,并提供避免及解决问题的方法. <?php error_rep ...

  10. Delphi全角转半角

    function ToDBC( input :String):WideString;varc:WideString;i:Integer;beginc := input;for i:=1 to Leng ...