智能指针

1. 什么是智能指针?

智能指针是行为类似于指针的类对象,但这种对象还有其他功能。

2. 为什么设计智能指针?

引例:

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 语句删除指针指向的内存空间,使得指针过期时自动销毁,解决内存泄漏问题。

注意:模板 auto_ptr 是 C++98 提供的解决方案,C++11 已将其摒弃,并提供另外两种解决方案:unique_ptr 和 shared_ptr 。

3. 怎么使用智能指针?

要转换 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 NEEDED
    return;
}

4. 都有哪些智能指针?

实际上,一共有 4 种:auto_ptr、unique_ptr、shared_ptr 和 weak_ptr(本文暂不讨论)。

5. 有哪些注意事项?

  • 所有的智能指针类都有一个 explicit 构造函数,该构造函数以指针作为参数。比如 auto_ptr 的类模板原型为:
    template<class X> class auto_ptr {
    public:
      explicit auto_ptr(X* p = 0) throw(); 
      ...
    };
    
    因此不需要自动将指针转换为智能指针对象:
    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 运算符用于非堆内存,这是错误的。

6. 为何摒弃 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 和 unique_ptr 的策略,但 unique_ptr 的策略更严格。
  • 创建智能更高的指针,跟踪引用特定对象的智能指针数。这称为引用计数(reference counting)。例如,赋值时,计数将加 1 ,而指针过期时,计数将减 1 。仅当最后一个指针过期(计数减为 0) 时,才调用 delete 。这是 shared_ptr 采用的策略。

当然,同样的策略也适用于复制构造函数。

每种方法都有其用途,但为何说要摒弃 auto_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;
}

运行后会发现程序会崩溃。书上说错误地使用 auto_ptr 可能会导致问题(这种代码的行为是不确定的,其行为可能随系统而异)。

这里的问题在于,pwin = films[2]; 将所有权从 films[2] 转让给了 pwin 。这导致 films[2] 不再引用该字符串。在 auto_ptr 放弃对象的所有权后,便可能使用它来访问该对象。当程序打印 films[2] 指向的字符串时,却发现这是一个空指针,这显然是个讨厌的意外。

如果使用 shared_ptr 代替 auto_ptr (这要求编译器支持 C++ 新增的 shared_ptr 类),则程序将正常运行。如果使用 unique_ptr ,结果又会如何呢?与 suto_ptr 一样,unique_ptr 也采用所有权模型。但使用 unique_ptr 时,程序不会等到运行阶段崩溃,而是在编译阶段报错。

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

7. unique_ptr 为何优于 auto_ptr ?

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

请看下面的语句:

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

在语句 #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

编译器认为语句 #6 非法,避免了 p3 不再指向有效数据的问题。因此,unique_ptr 比 auto_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 。

这也是禁止(只是一种建议,编译器并不禁止)在容器对象中使用 auto_ptr ,但允许使用 unique_ptr 的原因。如果容器算法试图对包含 unique_ptr 的容器执行类似于语句 #1 的操作,将导致编译错误;如果算法试图执行类似于语句 #2 的操作,则不会有任何问题。而对于 auto_ptr ,类似于语句 #1 的操作可能导致不确定的行为和神秘的崩溃。

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

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

使用 move 后,原来的指针仍转让所有权变成空指针,可以对其重新赋值。

到这里,你可能会问:unique_ptr 如何能够区分安全和不安全的用法呢?答案是它使用了 C++11 新增的移动构造函数和右值引用。详情本文不讨论。

相比于 auto_ptr ,unique_ptr 还有另一个优点。它有一个可用于数组的变体。模板 auto_ptr 使用 delete 而不是 delete[] ,因此只能与 new 一起使用,而不能与 new[] 一起使用。但 unique_ptr 有使用 new[] 和 delete[] 的版本。

警告:使用 new 分配内存时,才能使用 auto_ptr 和 shared_ptr ,使用 new[] 分配内存时,不能使用它们。不使用 new 分配内存时,不能使用 auto_ptr 或 shared_ptr ;不使用 new 或 new[] 分配内存时,不能使用 unique_ptr 。

8. 如何选择智能指针?

在掌握了这几种智能指针后,大家可能会想另一个问题:在实际应用中,该如何选择使用哪种智能指针呢?

(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() )。例如,可在程序中使用类似于下面的代码段,这里假设程序包含正确的 include 和 using 语句:

unique_ptr<int> make_int(int n)
{
    return unique_ptr<int>(new int(n));
}
void show(unique_ptr<int> &p1)              // pass by reference
{
    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_ptr
    vp.push_back(make_int(rand() % 1000));     // ok because arg is temporary
    for_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 类似。

参考资料:

《C++ Primer Plus》第 6 版中文版
C++智能指针简单剖析

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容

  • 导读## 最近在补看《C++ Primer Plus》第六版,这的确是本好书,其中关于智能指针的章节解析的非常清晰...
    小敏纸阅读 1,989评论 1 12
  • 原作者:Babu_Abdulsalam 本文翻译自CodeProject,转载请注明出处。 引入### Ooops...
    卡巴拉的树阅读 30,063评论 13 74
  • C++智能指针 原文链接:http://blog.csdn.net/xiaohu2022/article/deta...
    小白将阅读 6,850评论 2 21
  • C++ 智能指针详解 一、简介由于 C++ 语言没有自动内存回收机制,程序员每次 new 出来的内存都要手动 de...
    yangqi916阅读 1,360评论 0 2
  • 本文根据众多互联网博客内容整理后形成,引用内容的版权归原始作者所有,仅限于学习研究使用,不得用于任何商业用途。 智...
    深红的眼眸阅读 684评论 0 0