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

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

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

当(weird_thing()返回true)时,delete将不被执行,因此将导致内存泄露。

如何避免这种问题?有人会说,这还不简单,直接在return;之前加上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 ())return; str = *ps; // delete ps; NO LONGER NEEDEDreturn;
}

2. shared_ptr简单介绍

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

在语句#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还有更聪明的地方。
有时候,会将一个智能指针赋给另一个并不会留下危险的悬挂指针。假设有如下函数定义:

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. weak_ptr的简单介绍

weak_ptr 是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象. 进行该对象的内存管理的是那个强引用的 shared_ptr. weak_ptr只是提供了对管理对象的一个访问手段.

weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少.

定义在 memory 文件中(非memory.h), 命名空间为 std.

weak_ptr成员函数

weak_ptr 没有重载*和->但可以使用 lock 获得一个可用的 shared_ptr 对象. 注意, weak_ptr 在使用前需要检查合法性.

expired 用于检测所管理的对象是否已经释放, 如果已经释放, 返回 true; 否则返回 false.
lock 用于获取所管理的对象的强引用(shared_ptr). 如果 expired 为 true, 返回一个空的 shared_ptr; 否则返回一个 shared_ptr, 其内部对象指向与 weak_ptr 相同.
use_count 返回与 shared_ptr 共享的对象的引用计数.
reset 将 weak_ptr 置空.
weak_ptr 支持拷贝或赋值, 但不会影响对应的 shared_ptr 内部对象的计数.

weak_ptr的使用更为复杂一点,它可以指向shared_ptr指针指向的对象内存,却并不拥有该内存,而使用weak_ptr成员lock,则可返回其指向内存的一个share_ptr对象,且在所指对象内存已经无效时,返回指针空值nullptr。

注意:weak_ptr并不拥有资源的所有权,所以不能直接使用资源。
可以从一个weak_ptr构造一个shared_ptr以取得共享资源的所有权。

weak_ptr 使用实例:

#define _CRT_SECURE_NO_WARNINGS#include <iostream>
#include <string>
#include <memory>
#include <vector>
#include <map>void check(std::weak_ptr<int> &wp)
{std::shared_ptr<int> sp = wp.lock(); // 转换为shared_ptr<int>if (sp != nullptr){std::cout << "still: " << *sp << std::endl;} else{std::cout << "still: " << "pointer is invalid" << std::endl;}
}void mytest()
{std::shared_ptr<int> sp1(new int(22));std::shared_ptr<int> sp2 = sp1;std::weak_ptr<int> wp = sp1; // 指向shared_ptr<int>所指对象std::cout << "count: " << wp.use_count() << std::endl; // count: 2std::cout << *sp1 << std::endl; // 22std::cout << *sp2 << std::endl; // 22check(wp); // still: 22sp1.reset();std::cout << "count: " << wp.use_count() << std::endl; // count: 1std::cout << *sp2 << std::endl; // 22check(wp); // still: 22sp2.reset();std::cout << "count: " << wp.use_count() << std::endl; // count: 0check(wp); // still: pointer is invalidreturn;
}int main()
{mytest();system("pause");return 0;
}

使用 weak_ptr 解决 shared_ptr 因循环引有不能释放资源的问题

使用 shared_ptr 时, shared_ptr 为强引用, 如果存在循环引用, 将导致内存泄露. 而 weak_ptr 为弱引用, 可以避免此问题, 其原理:
  对于弱引用来说, 当引用的对象活着的时候弱引用不一定存在. 仅仅是当它存在的时候的一个引用, 弱引用并不修改该对象的引用计数, 这意味这弱引用它并不对对象的内存进行管理.
  weak_ptr 在功能上类似于普通指针, 然而一个比较大的区别是, 弱引用能检测到所管理的对象是否已经被释放, 从而避免访问非法内存。
注意: 虽然通过弱引用指针可以有效的解除循环引用, 但这种方式必须在程序员能预见会出现循环引用的情况下才能使用, 也可以是说这个仅仅是一种编译期的解决方案, 如果程序在运行过程中出现了循环引用, 还是会造成内存泄漏.

class CB;
class CA;class CA
{
public:CA() {}~CA() { PRINT_FUN(); }void Register(const std::shared_ptr<CB>& sp){m_spb = sp;}private:std::weak_ptr<CB> m_spb;
};class CB
{
public:CB() {};~CB() { PRINT_FUN(); };void Register(const std::shared_ptr<CA>& sp){m_spa = sp;}private:std::shared_ptr<CA> m_spa;
};std::shared_ptr<CA> spa(new CA);
std::shared_ptr<CB> spb(new CB);spb->Register(spa);
spa->Register(spb);
printf("%d\n", spb.use_count()); // 1
printf("%d\n", spa.use_count()); // 2

另一个循环依赖的例子,来自<C++标准库(第2版)>

class Person : public enable_shared_from_this<Person>
{
public:Person(const string& name): m_name{ name }{}~Person(){cout << "release " << m_name << endl;}string getName() const{return m_name;}void setFather(shared_ptr<Person> f){m_father = f;if (f){f->m_kids.push_back(shared_from_this());}}void setMother(shared_ptr<Person> m){m_mother = m;if (m){m->m_kids.push_back(shared_from_this());}}shared_ptr<Person> getKid(size_t idx){if (idx < m_kids.size()){weak_ptr<Person> p = m_kids.at(idx);if (!p.expired()){return p.lock();}}return nullptr;}private:string                        m_name;shared_ptr<Person>            m_father;shared_ptr<Person>            m_mother;//vector<shared_ptr<Person>>    m_kids; // 循环依赖vector<weak_ptr<Person>>      m_kids;
};// 测试代码
shared_ptr<Person> jack{ make_shared<Person>("Jack") };
shared_ptr<Person> lucy{ make_shared<Person>("Lucy") };
shared_ptr<Person> john{ make_shared<Person>("John") };
john->setFather(jack);
john->setMother(lucy);auto p = jack->getKid(0);
if (p)
{cout << p->getName() << endl;
}

附源码实现:

template<class _Ty>
class weak_ptr: public _Ptr_base<_Ty>
{    // class for pointer to reference counted resourcetypedef typename _Ptr_base<_Ty>::_Elem _Elem;public:weak_ptr(){    // construct empty weak_ptr object}template<class _Ty2>weak_ptr(const shared_ptr<_Ty2>& _Other,typename enable_if<is_convertible<_Ty2 *, _Ty *>::value,void *>::type * = 0){    // construct weak_ptr object for resource owned by _Otherthis->_Resetw(_Other);}weak_ptr(const weak_ptr& _Other){    // construct weak_ptr object for resource pointed to by _Otherthis->_Resetw(_Other);}template<class _Ty2>weak_ptr(const weak_ptr<_Ty2>& _Other,typename enable_if<is_convertible<_Ty2 *, _Ty *>::value,void *>::type * = 0){    // construct weak_ptr object for resource pointed to by _Otherthis->_Resetw(_Other);}~weak_ptr(){    // release resourcethis->_Decwref();}weak_ptr& operator=(const weak_ptr& _Right){    // assign from _Rightthis->_Resetw(_Right);return (*this);}template<class _Ty2>weak_ptr& operator=(const weak_ptr<_Ty2>& _Right){    // assign from _Rightthis->_Resetw(_Right);return (*this);}template<class _Ty2>weak_ptr& operator=(shared_ptr<_Ty2>& _Right){    // assign from _Rightthis->_Resetw(_Right);return (*this);}void reset(){    // release resource, convert to null weak_ptr objectthis->_Resetw();}void swap(weak_ptr& _Other){    // swap pointersthis->_Swap(_Other);}bool expired() const{    // return true if resource no longer existsreturn (this->_Expired());}shared_ptr<_Ty> lock() const{    // convert to shared_ptrreturn (shared_ptr<_Elem>(*this, false));}
};

6. 如何选择智能指针?

在掌握了这几种智能指针后,大家可能会想另一个问题:在实际应用中,应使用哪种智能指针呢?
下面给出几个使用指南。

(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类似。

C++的4种智能指针剖析使用相关推荐

  1. C++ STL 四种智能指针

    文章目录 0.前言 1.unique_ptr 2.auto_ptr 3.shared_ptr 3.1 shared_ptr 简介 3.2 通过辅助类模拟实现 shared_ptr 4.weak_ptr ...

  2. c++几种智能指针比较

    unique_ptr持有对对象的独有权--两个unique_ptr不能指向一个对象,即 unique_ptr 不共享它所管理的对象.它无法复制到其他 unique_ptr,无法通过值传递到函数,也无法 ...

  3. C++智能指针剖析(上)std::auto_ptr与boost::scoped_ptr

    1. 引入 C++语言中的动态内存分配没有自动回收机制,动态开辟的空间需要用户自己来维护,在出函数作用域或者程序正常退出前必须释放掉. 即程序员每次 new 出来的内存都要手动 delete,否则会造 ...

  4. C++智能指针(二)模拟实现三种智能指针

    https://blog.csdn.net/nou_camp/article/details/70186721 在上一篇博客中提到了Auto_ptr(C++智能指针(一)),下面进行模拟实现Auto_ ...

  5. C++ 四种智能指针详解

    智能指针出现的原因:主要解决的是堆内存分配释放,忘记释放内存引发的内存泄漏问题,智能指针最主要的事情就是让程序员无需去注意内存释放,内存释放的问题放在智能指针内部进行处理. 智能指针有四种,包括aut ...

  6. C++中的三种智能指针分析(RAII思想)

    2019独角兽企业重金招聘Python工程师标准>>> 智能指针 首先我们在理解智能指针之前我们先了解一下什么是RAII思想.RAII(Resource Acquisition Is ...

  7. C++ 几种智能指针的简单实现

    #pragma once // 智能指针 // 定义个类来封装资源的分配和释放,在构造 函数完成资源的分配和初始化,在析构函数完成资源的 // 清理,可以 保证资源的正确初始化和释放. // 这里简单 ...

  8. STL(五)之智能指针剖析

    C++标准库(五)之智能指针源码剖析 _Mutex_base template<_Lock_policy _Lp> class _Mutex_base { protected: enum ...

  9. C++学习笔记:三种智能指针【Share、Unique、Weak】【Cherno】

    老规矩,先问题后文章: 为什么要有智能指针以及它的好处是什么.智能指针是在怎么工作的.智能指针的操作: 1:为什么要有智能指针: 要知道,内存泄漏是非常严重的后果,如果new出来的空间没在合适的时间d ...

最新文章

  1. java安全接口调用_步骤3:调用Java接口
  2. 报错解决——uwsgi错误invalid request block size
  3. 自己写一个最简单的bootloader_jz2440
  4. SimpleAdapter与listview,gridview的组合用法
  5. 大数据技术基础_【基础】大数据技术栈介绍
  6. windows下程序开机自启动(任务计划程序)
  7. getParameterValues用法
  8. node静态服务器优缺点_使用 Node.js 的优势和劣势都有哪些?
  9. 卸载Android app弹出浏览器的一种实现
  10. 基于php的选课系统设计(含源文件)
  11. 吾爱电脑数据恢复工具箱 v 2.0
  12. python cursor游标_python tuble、lambda及cursor游标相关
  13. excel最常用的八个函数
  14. 服务器系统能装cad吗,服务器主机用CAD画图吗
  15. Matlab导入数据(一定有用!!)
  16. 12.图形裁剪——二维裁剪+三维裁剪+Sutherland-Cohen裁剪算法+中点分割算法
  17. 全网最详细桥接老式无线路由器教程
  18. 中餐和西餐菜名大全(zt)
  19. ValueError: empty range for randrange() (0, 0, 0)
  20. 你见过最水的程序员是啥样的?

热门文章

  1. bucket java_桶排序(BucketSort)(java)
  2. nginx配置文件映射外网服务器
  3. ORA-01810: format code appears twice
  4. Spring Security + OAuth2.0
  5. matlab 解函数方程,MATLAB程序设计教程(7)—MATLAB解方程与函数极值
  6. 丝芙兰(Sephora)和悦诗风吟(Innisfree)如何用“购物篮”改善顾客购物体验
  7. ROS2和ROS1启动launch的一些分享(关于如何查阅文档的具体说明)
  8. 苹果11计算机怎么打开语音,iPhone11怎么打开Siri 语音唤醒方法介绍
  9. 敏友的【敏捷个人】有感(1): 敏捷个人-阅读笔记 by 人月神话
  10. 史玉柱深度剖析:民营的13种“死法”