C++智能指针简单剖析

http://blog.csdn.net/lanxuezaipiao/article/details/41603883

导读

最近在补看《C++ Primer Plus》第六版,这的确是本好书,其中关于智能指针的章节解析的非常清晰,一解我以前的多处困惑。C++面试过程中,很多面试官都喜欢问智能指针相关的问题,比如你知道哪些智能指针?shared_ptr的设计原理是什么?如果让你自己设计一个智能指针,你如何完成?等等……。而且在看开源的C++项目时,也能随处看到智能指针的影子。这说明智能指针不仅是面试官爱问的题材,更是非常有实用价值。

下面是我在看智能指针时所做的笔记,希望能够解决你对智能指针的一些困扰。

目录

  1. 智能指针背后的设计思想
  2. C++智能指针简单介绍
  3. 为什么摒弃auto_ptr?
  4. unique_ptr为何优于auto_ptr?
  5. 如何选择智能指针?

正文

1. 智能指针背后的设计思想

我们先来看一个简单的例子:

void remodel(std::string & str)
{std::string * ps = new std::string(str);...if (weird_thing())throw exception();str = *ps; delete ps;return;
}

当出现异常时(weird_thing()返回true),delete将不被执行,因此将导致内存泄露。
如何避免这种问题?有人会说,这还不简单,直接在throw exception();之前加上delete ps;不就行了。是的,你本应如此,问题是很多人都会忘记在适当的地方加上delete语句(连上述代码中最后的那句delete语句也会有很多人忘记吧),如果你要对一个庞大的工程进行review,看是否有这种潜在的内存泄露问题,那就是一场灾难!
这时我们会想:当remodel这样的函数终止(不管是正常终止,还是由于出现了异常而终止),本地变量都将自动从栈内存中删除—因此指针ps占据的内存将被释放,如果ps指向的内存也被自动释放,那该有多好啊。
我们知道析构函数有这个功能。如果ps有一个析构函数,该析构函数将在ps过期时自动释放它指向的内存。但ps的问题在于,它只是一个常规指针,不是有析构凼数的类对象指针。如果它指向的是对象,则可以在对象过期时,让它的析构函数删除指向的内存。

这正是 auto_ptr、unique_ptr和shared_ptr这几个智能指针背后的设计思想。我简单的总结下就是:将基本类型指针封装为类对象指针(这个类肯定是个模板,以适应不同基本类型的需求),并在析构函数里编写delete语句删除指针指向的内存空间。

因此,要转换remodel()函数,应按下面3个步骤进行:

  • 包含头义件memory(智能指针所在的头文件);
  • 将指向string的指针替换为指向string的智能指针对象;
  • 删除delete语句。

下面是使用auto_ptr修改该函数的结果:

# include <memory>
void remodel (std::string & str)
{std::auto_ptr<std::string> ps (new std::string(str));...if (weird_thing ())throw exception(); str = *ps; // delete ps; NO LONGER NEEDEDreturn;
}

2. C++智能指针简单介绍

STL一共给我们提供了四种智能指针:auto_ptr、unique_ptr、shared_ptr和weak_ptr(本文章暂不讨论)。
模板auto_ptr是C++98提供的解决方案,C+11已将将其摒弃,并提供了另外两种解决方案。然而,虽然auto_ptr被摒弃,但它已使用了好多年:同时,如果您的编译器不支持其他两种解决力案,auto_ptr将是唯一的选择。

使用注意点

  • 所有的智能指针类都有一个explicit构造函数,以指针作为参数。比如auto_ptr的类模板原型为:
    templet<class T>
    class auto_ptr {explicit auto_ptr(X* p = 0) ; ...
    };

    因此不能自动将指针转换为智能指针对象,必须显式调用:

    shared_ptr<double> pd; 
    double *p_reg = new double;
    pd = p_reg;                               // not allowed (implicit conversion)
    pd = shared_ptr<double>(p_reg);           // allowed (explicit conversion)
    shared_ptr<double> pshared = p_reg;       // not allowed (implicit conversion)
    shared_ptr<double> pshared(p_reg);        // allowed (explicit conversion)
  • 对全部三种智能指针都应避免的一点:
    string vacation("I wandered lonely as a cloud.");
    shared_ptr<string> pvac(&vacation);   // No
    pvac过期时,程序将把delete运算符用于非堆内存,这是错误的。

使用举例

#include <iostream>
#include <string>
#include <memory>class report
{
private:std::string str;
public:report(const std::string s) : str(s) {std::cout << "Object created.\n";}~report() {std::cout << "Object deleted.\n";}void comment() const {std::cout << str << "\n";}
};int main() {{std::auto_ptr<report> ps(new report("using auto ptr"));ps->comment();}{std::shared_ptr<report> ps(new report("using shared ptr"));ps->comment();}{std::unique_ptr<report> ps(new report("using unique ptr"));ps->comment();}return 0;
}

3. 为什么摒弃auto_ptr?

先来看下面的赋值语句:

auto_ptr< string> ps (new string ("I reigned lonely as a cloud.”);
auto_ptr<string> vocation; 
vocaticn = ps;

上述赋值语句将完成什么工作呢?如果ps和vocation是常规指针,则两个指针将指向同一个string对象。这是不能接受的,因为程序将试图删除同一个对象两次——一次是ps过期时,另一次是vocation过期时。要避免这种问题,方法有多种:

  • 定义陚值运算符,使之执行深复制。这样两个指针将指向不同的对象,其中的一个对象是另一个对象的副本,缺点是浪费空间,所以智能指针都未采用此方案。
  • 建立所有权(ownership)概念。对于特定的对象,只能有一个智能指针可拥有,这样只有拥有对象的智能指针的构造函数会删除该对象。然后让赋值操作转让所有权。这就是用于auto_ptr和uniqiie_ptr 的策略,但unique_ptr的策略更严格。
  • 创建智能更高的指针,跟踪引用特定对象的智能指针数。这称为引用计数。例如,赋值时,计数将加1,而指针过期时,计数将减1,。当减为0时才调用delete。这是shared_ptr采用的策略。

当然,同样的策略也适用于复制构造函数。
每种方法都有其用途,但为何说要摒弃auto_ptr呢?
下面举个例子来说明。

#include <iostream>
#include <string>
#include <memory>
using namespace std;int main() {auto_ptr<string> films[5] ={auto_ptr<string> (new string("Fowl Balls")),auto_ptr<string> (new string("Duck Walks")),auto_ptr<string> (new string("Chicken Runs")),auto_ptr<string> (new string("Turkey Errors")),auto_ptr<string> (new string("Goose Eggs"))};auto_ptr<string> pwin;pwin = films[2]; // films[2] loses ownership. 将所有权从films[2]转让给pwin,此时films[2]不再引用该字符串从而变成空指针cout << "The nominees for best avian baseballl film are\n";for(int i = 0; i < 5; ++i)cout << *films[i] << endl;cout << "The winner is " << *pwin << endl;cin.get();return 0;
}

运行下发现程序崩溃了,原因在上面注释已经说的很清楚,films[2]已经是空指针了,下面输出访问空指针当然会崩溃了。但这里如果把auto_ptr换成shared_ptr或unique_ptr后,程序就不会崩溃,原因如下:

  • 使用shared_ptr时运行正常,因为shared_ptr采用引用计数,pwin和films[2]都指向同一块内存,在释放空间时因为事先要判断引用计数值的大小因此不会出现多次删除一个对象的错误。
  • 使用unique_ptr时编译出错,与auto_ptr一样,unique_ptr也采用所有权模型,但在使用unique_ptr时,程序不会等到运行阶段崩溃,而在编译器因下述代码行出现错误:
    unique_ptr<string> pwin;
    pwin = films[2]; // films[2] loses ownership.
    指导你发现潜在的内存错误。

这就是为何要摒弃auto_ptr的原因,一句话总结就是:避免潜在的内存崩溃问题。

4. unique_ptr为何优于auto_ptr?

可能大家认为前面的例子已经说明了unique_ptr为何优于auto_ptr,也就是安全问题,下面再叙述的清晰一点。
请看下面的语句:

请看下面的语句:

auto_ptr<string> p1(new string ("auto") ; //#1
auto_ptr<string> p2;                       //#2
p2 = p1;   

在语句#3中,p2接管string对象的所有权后,p1的所有权将被剥夺。前面说过,这是好事,可防止p1和p2的析构函数试图刪同—个对象;
但如果程序随后试图使用p1,这将是件坏事,因为p1不再指向有效的数据。

下面来看使用unique_ptr的情况:

unique_ptr<string> p3 (new string ("auto");   //#4
unique_ptr<string> p4;                       //#5
p4 = p3;    

编译器认为语句#6非法,避免了p3不再指向有效数据的问题。因此,unique_ptr比auto_ptr更安全。

但unique_ptr还有更聪明的地方。
有时候,会将一个智能指针赋给另一个并不会留下危险的悬挂指针。假设有如下函数定义:

unique_ptr<string> demo(const char * s)
{unique_ptr<string> temp (new string (s)); return temp;
}

并假设编写了如下代码:

unique_ptr<string> ps;
ps = demo('Uniquely special");

demo()返回一个临时unique_ptr,然后ps接管了原本归返回的unique_ptr所有的对象,而返回时临时的 unique_ptr 被销毁,也就是说没有机会使用 unique_ptr 来访问无效的数据,换句话来说,这种赋值是不会出现任何问题的,即没有理由禁止这种赋值。实际上,编译器确实允许这种赋值,这正是unique_ptr更聪明的地方。

总之,党程序试图将一个 unique_ptr 赋值给另一个时,如果源 unique_ptr 是个临时右值,编译器允许这么做;如果源 unique_ptr 将存在一段时间,编译器将禁止这么做,比如:

unique_ptr<string> pu1(new string ("hello world"));
unique_ptr<string> pu2;
pu2 = pu1;                                      // #1 not allowed
unique_ptr<string> pu3;
pu3 = unique_ptr<string>(new string ("You"));   // #2 allowed

其中#1留下悬挂的unique_ptr(pu1),这可能导致危害。而#2不会留下悬挂的unique_ptr,因为它调用 unique_ptr 的构造函数,该构造函数创建的临时对象在其所有权让给 pu3 后就会被销毁。这种随情况而已的行为表明,unique_ptr 优于允许两种赋值的auto_ptr 。

当然,您可能确实想执行类似于#1的操作,仅当以非智能的方式使用摒弃的智能指针时(如解除引用时),这种赋值才不安全。要安全的重用这种指针,可给它赋新值。C++有一个标准库函数std::move(),让你能够将一个unique_ptr赋给另一个。下面是一个使用前述demo()函数的例子,该函数返回一个unique_ptr<string>对象:
使用move后,原来的指针仍转让所有权变成空指针,可以对其重新赋值。

unique_ptr<string> ps1, ps2;
ps1 = demo("hello");
ps2 = move(ps1);
ps1 = demo("alexia");
cout << *ps2 << *ps1 << endl;

5. 如何选择智能指针?

在掌握了这几种智能指针后,应使用哪种智能指针呢?
(1)如果程序要使用多个指向同一个对象的指针,应选择shared_ptr。这样的情况包括:
有一个指针数组,并使用一些辅助指针来标示特定的元素,如最大的元素和最小的元素;
两个对象包含都指向第三个对象的指针;
STL容器包含指针。
很多STL算法都支持复制和赋值操作,这些操作可用于shared_ptr,但不能用于unique_ptr(编译器发出warning)和auto_ptr(行为不确定)。如果你的编译器没有提供shared_ptr,可使用Boost库提供的shared_ptr。

(2)如果程序不需要多个指向同一个对象的指针,则可使用unique_ptr。如果函数使用new分配内存,并返还指向该内存的指针,将其返回类型声明为unique_ptr是不错的选择。这样,所有权转让给接受返回值的unique_ptr,而该智能指针将负责调用delete。可将unique_ptr存储到STL容器在那个,只要不调用将一个unique_ptr复制或赋给另一个算法(如sort())。例如,可在程序中石油类似于下面的代码段。

unique_ptr<int> make_int(int n)
{return unique_ptr<int>(new int(n));
}
void show(unique_ptr<int> &p1)
{cout << *a << ' ';
}
int main()
{...vector<unique_ptr<int> > vp(size);for(int i = 0; i < vp.size(); i++)vp[i] = make_int(rand() % 1000);              // copy temporary unique_ptrvp.push_back(make_int(rand() % 1000));     // ok because arg is temporaryfor_each(vp.begin(), vp.end(), show);           // use for_each()...
}

其中push_back调用没有问题,因为它返回一个临时unique_ptr,该unique_ptr被赋给vp中的一个unique_ptr。另外,如果按值而不是按引用给show()传递对象,for_each()将非法,因为这将导致使用一个来自vp的非临时unique_ptr初始化pi,而这是不允许的。前面说过,编译器将发现错误使用unique_ptr的企图。
在unique_ptr为右值时,可将其赋给shared_ptr,这与将一个unique_ptr赋给一个需要满足的条件相同。与前面一样,在下面的代码中,make_int()的返回类型为unique_ptr<int>:

unique_ptr<int> pup(make_int(rand() % 1000));   // ok
shared_ptr<int> spp(pup);                       // not allowed, pup as lvalue
shared_ptr<int> spr(make_int(rand() % 1000));   // ok
模板shared_ptr包含一个显式构造函数,可用于将右值unique_ptr转换为shared_ptr。shared_ptr将接管原来归unique_ptr所有的对象。
在满足unique_ptr要求的条件时,也可使用auto_ptr,但unique_ptr是更好的选择。如果你的编译器没有unique_ptr,可考虑使用Boost库提供的scoped_ptr,它与unique_ptr类似。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/384149.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

非常可乐——BFS

【题目描述】 大家一定觉的运动以后喝可乐是一件很惬意的事情&#xff0c;但是seeyou却不这么认为。因为每次当seeyou买了可乐以后&#xff0c;阿牛就要求和seeyou一起分享这一瓶可乐&#xff0c;而且一定要喝的和seeyou一样多。但seeyou的手中只有两个杯子&#xff0c;它们的容…

整型数据存储

//代码1 #include<stdio.h> int main() {char a -1;signed char b -1;unsigned char c -1;printf("a %d, b %d, c %d", a, b, c);return 0; } 1000 0000 0000 0001 -> -1源码 1111 1111 1111 1110 -> -1反码 1111 1111 1111 1111 -> -1补码 对于…

聊聊gcc参数中的-I, -L和-l

http://blog.csdn.net/stpeace/article/details/49408665 在本文中&#xff0c; 我们来聊聊gcc中三个常见的参数&#xff0c; 也即-I, -L和-l 一. 先说 -I (注意是大写的i) 我们先来看简单的程序&#xff1a; main.c: [cpp] view plaincopy #include <stdio.h> #incl…

Pots——BFS

【题目描述】 You are given two pots, having the volume of A and B liters respectively. The following operations can be performed: FILL(i) fill the pot i (1 ≤ i ≤ 2) from the tap; DROP(i) empty the pot i to the drain; POUR(i,j) pour from pot i to pot j;…

HDU - 4578Transformation——线段树+区间加法修改+区间乘法修改+区间置数+区间和查询+区间平方和查询+区间立方和查询

【题目描述】 HDU - 4578Transformation Problem Description Yuanfang is puzzled with the question below: There are n integers, a1, a2, …, an. The initial values of them are 0. There are four kinds of operations. Operation 1: Add c to each number between ax …

[C++基础]034_C++模板编程里的主版本模板类、全特化、偏特化(C++ Type Traits)

http://www.cnblogs.com/alephsoul-alephsoul/archive/2012/10/18/2728753.html 1. 主版本模板类 首先我们来看一段初学者都能看懂&#xff0c;应用了模板的程序&#xff1a; 1 #include <iostream>2 using namespace std;3 4 template<class T1, class T2>5 clas…

自定义类型: 结构体,枚举,联合

1.结构体 个人认为结构体和数组特别相似&#xff0c;只不过结构体和数组的区别在于结构体的成员可以是不同类型&#xff0c;而数组成员类型是相同的。 &#xff08;1&#xff09;结构体的声明 struct tag {成员列表//至少得有一个成员 }值列表;//值列表可以省略 struct {int a…

详解C++中的函数调用和下标以及成员访问运算符的重载

http://www.jb51.net/article/78436.htm 这篇文章主要介绍了详解C中的函数调用和下标以及成员访问运算符,讲到了这些二元运算符使用的语法及重载,需要的朋友可以参考下函数调用 使用括号调用的函数调用运算符是二元运算符。 语法 ?1primary-expression ( expression-list )备…

A计划——BFS

【题目描述】 可怜的公主在一次次被魔王掳走一次次被骑士们救回来之后&#xff0c;而今&#xff0c;不幸的她再一次面临生命的考验。魔王已经发出消息说将在T时刻吃掉公主&#xff0c;因为他听信谣言说吃公主的肉也能长生不老。年迈的国王正是心急如焚&#xff0c;告招天下勇士…

使用openssl的md5库

http://blog.csdn.net/sinat_35297665/article/details/78244523 在linux机器上&#xff0c;有一个命令可以计算出文件的md5值&#xff0c;那就是md5sum&#xff0c;如果没有的话&#xff0c;就需要安装RPM包&#xff1a;coreutils。 现在我们使用openssl的库也可以方便的计算出…

主席树入门

今天学习了一下主席树&#xff08;名字这么强的嘛&#xff09; 虽然直接理解起来不容易&#xff0c;但是这种解决问题的思想其实并不陌生。 我们可以首先来看维护整个区间第K大的线段树 我们将[l,r]区间内数字的多少用线段树进行维护&#xff0c;这样的话为了求取区间第k大&…

Socket网络编程--小小网盘程序(1)

http://www.cnblogs.com/wunaozai/p/3886588.html 这个系列是准备讲基于Linux Socket进行文件传输。简单的文件传输就是客户端可以上传文件&#xff0c;可以从服务器端下载文件。就这么两个功能如果再加上身份验证&#xff0c;就成了FTP服务器了&#xff0c;如果对用户的操作再…

使用 Verdaccio 构建自己的私有 npm 仓库

前言 无论你是公司的开发者&#xff0c;还是个人开发者&#xff0c;你可能都听说过或者使用过 npm&#xff0c;这是一个使用广泛的 JavaScript 包管理器。但是&#xff0c;你是否遇到过以下的问题&#xff1a;你需要一个私有的包存放地方&#xff0c;或者你需要在离线环境下使…

HDU - 4348To the moon——主席树+区间修改

HDU - 4348To the moon 【题目描述】 【题目分析】 题目中说明每次更新后时间都会加1&#xff0c;而且还会需要查询以前的区间&#xff0c;还会需要返回以前的时间&#xff0c;所以是很裸的主席树。区间查询的话我们同样需要用到lazy标记 通过这道题我发现线段树的操作还是很灵…

进入一个目录需要那些权限

1.文件访问者的分类 文件的访问者具体可分为以下几类&#xff1a; (1)拥有者 (2)组拥有者 (3)其他用户 这些都代表什么意思呢&#xff1f; 其中r表示只读&#xff0c;w表示只写&#xff0c;x表示可执行&#xff0c;第一个字母代表了文件的类型&#xff0c;其中文件类型可以分为…

Socket网络编程--小小网盘程序(2)

http://www.cnblogs.com/wunaozai/p/3887728.html 这一节将不会介绍太多的技术的问题&#xff0c;这节主要是搭建一个小小的框架&#xff0c;为了方便接下来的继续编写扩展程序。本次会在上一小节的基础上加上一个身份验证的功能。 因为网盘程序不像聊天程序&#xff0c;网盘是…

Linux下的重要目录

1.bin目录 主要防止系统下的各种必备可执行文件 2./proc 目录 这个目录相当于Windows下的计算机系统信息查看以及进程动态查看&#xff0c;可以查看计算机信息&#xff0c;用来存放当前计算机上的进程信息 3./sys 目录 (1)其中block目录用于存放块设备文件 (2)bus存放总线类型…

HDU - 6278 Just $h$-index主席树+二分

HDU - 6278 Just hhh-index 【题目描述】 【题目分析】 题目要求在区间[l,r][l,r][l,r]内大于h的数不少于h个&#xff0c;对于这种最大化问题&#xff0c;我们应该想到二分。 最小情况显然是1.最大情况显然是r−l1r-l1r−l1&#xff0c;对于一个hhh&#xff0c;我们如何判断能…

Socket网络编程--小小网盘程序(3)

http://www.cnblogs.com/wunaozai/p/3891062.html 接上一小节&#xff0c;这次增加另外的两张表&#xff0c;用于记录用户是保存那些文件。增加传上来的文件的文件指纹&#xff0c;使用MD5表示。 两张表如下定义: 1 create table files(2 fid int,3 filename varchar(64),4 md…

LInux下du, df, top, free, pstack, su, sudo, adduser, password命令

1.du命令&#xff1a;du [选项] 文件 (1)功能该命令是显示指定文件以及下的所有文件占用系统数据块的情况&#xff0c;如果没有文件&#xff0c;默认为是当前工作目录 -a    显示所有文件对系统数据块的使用情况 -b    显示数据块大小时以字节为基本单位 -c    除了显…