C++11智能指针(二)

简介: C++11智能指针

4.2 线程安全问题

存在问题


当前模拟实现的shared_ptr还存在线程安全的问题,由于管理同一个资源的多个对象的引用计数是共享的,因此多个线程可能会同时对同一个引用计数进行自增或自减操作,而自增和自减操作都不是原子操作,因此需要通过加锁来对引用计数进行保护,否则就会导致线程安全问题


如下面代码中用一个shared_ptr管理一个整型变量,然后用两个线程分别对这个shared_ptr对象进行1000次拷贝操作,这些对象被拷贝出来后又会立即被销毁

802414ecf209428dbff0861ae71a6d86.png



在这个过程中两个线程会不断对引用计数进行自增和自减操作,理论上最终两个线程执行完毕后引用计数的值应该是1,因为拷贝出来的对象都被销毁了,只剩下最初的shared_ptr对象还在管理这个整型变量,但每次运行程序得到引用计数的值可能都是不一样的,根本原因就是因为对引用计数的自增和自减不是原子操作


加锁解决问题


引用计数的线程安全问题,本质就是要让对引用计数的自增和自减操作变成一个原子操作,因此可以对引用计数的操作进行加锁保护


在shared_ptr类中新增互斥锁成员变量,为了让管理同一个资源的多个线程访问到的是同一个互斥锁,管理不同资源的线程访问到的是不同的互斥锁,因此互斥锁也需要在堆区创建

在调用拷贝构造函数和拷贝赋值函数时,除了需要将对应的资源和引用计数的地址交给当前对象管理之外,还需要将对应的互斥锁的地址也交给当前对象

当一个资源对应的引用计数减为0时,除了需要将对应的资源和引用计数进行释放,还需要将对应的互斥锁进行释放

为了简化代码逻辑,可以将拷贝构造函数和拷贝赋值函数中引用计数的自增操作提取出来,封装成AddRef()函数,将拷贝赋值函数和析构函数中引用计数的自减操作提取出来,封装成ReleaseRef()函数,只需对AddRef()和ReleaseRef()函数进行加锁保护即可

#include <mutex>
using std::mutex;
using std::unique_lock;
namespace bjy
{
  template<class T>
  class shared_ptr
  {
  public:
    shared_ptr(T* ptr = nullptr) :_ptr(ptr), _pCount(new size_t(1)), _pMtx(new mutex) {}
    ~shared_ptr() { ReleaseRef(); }
    shared_ptr(shared_ptr<T>& sp) :_ptr(sp._ptr), _pCount(sp._pCount), _pMtx(sp._pMtx) {
      AddRef();
    }
    shared_ptr<T>& operator=(shared_ptr<T>& sp) {
      if (_ptr != sp._ptr) {//管理同一块空间的对象之间不需进行赋值操作
        ReleaseRef();
        _ptr = sp._ptr;
        _pCount = sp._pCount;
        _pMtx = sp._pMtx;
        AddRef();
      }
      return *this;
    }
    size_t GetCount() { return *_pCount; }
    T& operator*() { return *_ptr; }
    T* operator->() { return _ptr; };
  private:
    void AddRef() {
      unique_lock<mutex> lock(*_pMtx);
      ++(*_pCount);
    }
    void ReleaseRef()//flag为true表示引用计数已为0,需要删除锁
    {
      bool flag = false;
      {
        unique_lock<mutex> lock(*_pMtx);
        if (--(*_pCount) == 0) //引用计数完成--
        {
          if (nullptr != _ptr) {
            delete _ptr;
            _ptr = nullptr;
          }
          delete _pCount;
          _pCount = nullptr;
          flag = true;
        }
      }
      if (flag == true) delete _pMtx;
    }
  private:
    T* _ptr;
    size_t* _pCount;
    mutex* _pMtx;
  };
}

cc0ae3ad72c44f03b4e52a6437cdc6de.png


在ReleaseRef()函数中,当引用计数被减为0时需要释放互斥锁资源,但不能在临界区中释放互斥锁,因为后面还需要进行解锁操作,因此代码中借助了一个flag变量,通过flag变量来判断解锁后是否需要释放互斥锁资源

shared_ptr只需要保证引用计数的线程安全问题,而不需要保证资源的线程安全问题,就像原生指针管理一块内存空间一样,原生指针只需要指向这块空间,而这块空间的线程安全问题应该由这块空间的操作者来保证

4.3 定制删除器

定制删除器的用法


当智能指针对象的生命周期结束时,所有的智能指针默认都是以 delete 的方式将资源释放,但是智能指针并不是只管理以 new 方式申请到的内存空间,智能指针管理的也可能是以 new[] 的方式申请到的空间,或管理的是一个文件指针

#include <iostream>
struct ListNode 
{
  ListNode* _prev;
  ListNode* _next;
  size_t _value;
  ~ListNode() { std::cout << "~ListNode()" << std::endl; }
};
int main()
{
  std::shared_ptr<ListNode> sp1(new ListNode[10]);//err
  std::shared_ptr<FILE> sp2(std::fopen("test.cpp","w"));//err
  return 0;
}

以 new[] 的方式申请到的内存空间必须以 delete[] 的方式进行释放,而文件指针必须通过调用 fclose 函数进行释放


这时需要定制删除器来控制释放资源的方式,C++标准库中的shared_ptr提供了如下构造函数

template <class U, class D>
shared_ptr (U* p, D del);

参数p:需要让智能指针管理的资源的地址

参数del:删除器,为一个可调用对象,如函数指针、仿函数、lambda表达式以及被包装器包装后的可调用对象

#include <iostream>
struct ListNode 
{
  ListNode* _prev;
  ListNode* _next;
  size_t _value;
  ~ListNode() { std::cout << "~ListNode()" << std::endl; }
};
template<class T>
struct DelArrary
{
  void operator()(const T* ptr) {
    std::cout << "delete[]: " << ptr << std::endl;
    delete[] ptr;
  }
};
int main()
{
  std::shared_ptr<ListNode> sp1(new ListNode[10], DelArrary<ListNode>());
  std::shared_ptr<FILE> sp2(fopen("test.cpp", "w"), [](FILE* ptr) {
    std::cout << "fclose: " << ptr << std::endl;
    fclose(ptr);
  });
  return 0;
}


定制删除器的模拟实现


C++标准库中实现shared_ptr时是分成了很多类的,因此C++标准库中可以将删除器的类型设置为构造函数的模板参数,然后将删除器的类型在各个类之间进行传递。

但本次模拟实现是直接用一个类来模拟实现shared_ptr的,因此不能将删除器的类型设置为构造函数的模板参数。因为删除器不是在构造函数中调用的,而是需要在ReleaseRef()函数中进行调用,因此势必需要用一个成员变量将删除器保存下来,而在定义这个成员变量时就需要指定删除器的类型,因此模拟实现的时候不能将删除器的类型设置为构造函数的模板参数

要在当前模拟实现的shared_ptr的基础上支持定制删除器,就只能给shared_ptr类再增加一个模板参数,在构造shared_ptr对象时就需要指定删除器的类型。然后增加一个支持传入删除器的构造函数,在构造对象时将删除器保存下来,在需要释放资源的时候调用该删除器进行释放即可。最好在设置一个默认的删除器,若用户定义shared_ptr对象时不传入删除器,则默认以delete的方式释放资源

namespace bjy
{
  //默认的删除器
  template<class T>
  struct Delete
  {
    void operator()(const T* ptr) {
      delete ptr;
    }
  };
  template<class T, class D = Delete<T>>
  class shared_ptr
  {
  private:
    void ReleaseRef()
    {
      _pmutex->lock();
      bool flag = false;
      if (--(*_pcount) == 0) //将管理的资源对应的引用计数--
      {
        if (_ptr != nullptr)
        {
          cout << "delete: " << _ptr << endl;
          _del(_ptr); //使用定制删除器释放资源
          _ptr = nullptr;
        }
        delete _pcount;
        _pcount = nullptr;
        flag = true;
      }
      _pmutex->unlock();
      if (flag == true)
      {
        delete _pmutex;
      }
    }
    //...
  public:
    shared_ptr(T* ptr, D del)
      : _ptr(ptr)
      , _pcount(new int(1))
      , _pmutex(new mutex)
      , _del(del)
    {}
    //...
  private:
    T* _ptr;        //管理的资源
    int* _pcount;   //管理的资源对应的引用计数
    mutex* _pmutex; //管理的资源对应的互斥锁
    D _del;         //管理的资源对应的删除器
  };
}


若传入的删除器是仿函数,那么需要在构造shared_ptr对象时指明仿函数的类型

若传入的删除器是一个lambda表达式更为麻烦,因为lambda表达式的类型不太容易获取。可以将lambda表达式的类型指明为一个包装器类型,让编译器传参时自行进行推演,也可以先用auto接收lambda表达式,然后再用decltype来声明删除器的类型

template<class T>
struct DelArr
{
  void operator()(const T* ptr) {
    cout << "delete[]: " << ptr << endl;
    delete[] ptr;
  }
};
int main()
{
  //仿函数示例
  cl::shared_ptr<ListNode, DelArr<ListNode>> sp1(new ListNode[10], DelArr<ListNode>());
  //lambda示例1
  cl::shared_ptr<FILE, function<void(FILE*)>> sp2(fopen("test.cpp", "r"), [](FILE* ptr) {
    cout << "fclose: " << ptr << endl;
    fclose(ptr);
  });
  //lambda示例2
  auto f = [](FILE* ptr) {
    cout << "fclose: " << ptr << endl;
    fclose(ptr);
  };
  cl::shared_ptr<FILE, decltype(f)> sp3(fopen("test.cpp", "w"), f);
  return 0;
}

五、std::weak_ptr

该类型指针通常不单独使用(没有实际用处),只能和shared_ptr搭配使用。可以将weak_ptr视为shared_ptr指针的一种辅助工具

借助weak_ptr类型指针,可以获取shared_ptr指针的一些状态信息,如有多少指向相同的shared_ptr指针,shared_ptr指针指向的堆内存是否已经被释放等

当weak_ptr类型指针的指向和shared_ptr指针相同时,weak_ptr并不会使资源的引用计数加1

当weak_ptr指针被释放时,之前所指堆内存的引用计数也不会因此而减1,即weak_ptr并不会影响所指堆内存空间的引用计数

weak_ptr<T>模板类中没有重载 * 和 -> 运算符,weak_ptr 类型指针只能访问所指的堆内存,而无法修改它

shared_ptr的循环引用问题


shared_ptr的循环引用问题在一些特定的场景下才会产生。如定义如下的结点类,并在结点类的析构函数中打印一句提示语句,便于判断结点是否正确释放。以 new 的方式在堆上构建两个结点,并将这两个结点连接起来,在程序的最后以 delete 的方式释放这两个结点

struct ListNode
{
  ListNode* _next;
  ListNode* _prev;
  int _val;
  ~ListNode()
  {
    cout << "~ListNode()" << endl;
  }
};
int main()
{
  ListNode* node1 = new ListNode;
  ListNode* node2 = new ListNode;
  node1->_next = node2;
  node2->_prev = node1;
  //...
  delete node1;
  delete node2;
  return 0;
}


上述程序没有问题,两个结点都能够正确释放。为了防止程序中途返回或抛异常等原因导致结点未被释放,将这两个结点分别交给两个shared_ptr对象进行管理,这时为了让连接节点的赋值操作能够执行,就需要把ListNode类中的_next和_prev成员变量的类型也改为shared_ptr类型

struct ListNode
{
  std::shared_ptr<ListNode> _next;
  std::shared_ptr<ListNode> _prev;
  size_t _val;
  ~ListNode()
  {
    cout << "~ListNode()" << endl;
  }
};
int main()
{
  std::shared_ptr<ListNode> node1(new ListNode);
  std::shared_ptr<ListNode> node2(new ListNode);
  node1->_next = node2;
  node2->_prev = node1;
  //...
  return 0;
}

这时程序运行结束后两个结点都没有被释放,但若是去掉连接结点时的两句代码中的任意一句,那么这两个结点就都能够正确释放,根本原因就是因为这两句连接结点的代码导致了循环引用


当以new的方式申请到两个ListNode结点并交给两个智能指针管理后,这两个资源对应的引用计数都被加到了1


20e0d84a752a440d8926f66a22bc30a9.png


将这两个结点连接起来后,资源1当中的_next成员与node2一同管理资源2,资源2中的_prev成员与node1一同管理资源1,此时这两个资源对应的引用计数都被加到了2


7714e9d5e9d74ff08a2b9fbed61f118c.png


当出了main()函数的作用域后,node1和node2的生命周期都结束了,因此这两个资源对应的引用计数都减到了1

e412ef5110544a548af9639a46cea65f.png



当资源对应的引用计数减为0时对应的资源才会被释放,因此资源1的释放取决于资源2中的_prev成员,而资源2的释放取决于资源1中的_next成员

而资源1当中的_next成员的释放又取决于资源1,资源2当中的_prev成员的释放又取决于资源2,于是这就变成了一个死循环,最终导致资源无法释放

而若连接结点时只进行一个连接操作,那么当node1和node2的生命周期结束时,就会有一个资源对应的引用计数被减为0,此时这个资源就会被释放,这个释放后另一个资源的引用计数也会被减为0,最终两个资源就都被释放了,这就是为什么只进行一个连接操作时这两个结点就都能够正确释放的原因


weak_ptr解决循环引用问题


weak_ptr是C++11中引入的智能指针,weak_ptr不是用来管理资源的释放的,主要是用来解决shared_ptr的循环引用问题的。weak_ptr支持用shared_ptr对象来构造weak_ptr对象,构造出来的weak_ptr对象与shared_ptr对象管理同一个资源,但不会增加这块资源对应的引用计数


将ListNode中的_next和_prev成员的类型换成weak_ptr就不会导致循环引用问题了,此时当node1和node2生命周期结束时两个资源对应的引用计数就都会被减为0,进而释放这两个结点的资源

struct ListNode
{
  std::weak_ptr<ListNode> _next;
  std::weak_ptr<ListNode> _prev;
  size_t _val = 10;
  ~ListNode()
  {
    cout << "~ListNode()" << endl;
  }
};
int main()
{
  std::shared_ptr<ListNode> node1(new ListNode);
  std::shared_ptr<ListNode> node2(new ListNode);
  cout << node1.use_count() << endl;
  cout << node2.use_count() << endl;
  node1->_next = node2;
  node2->_prev = node1;
  //...
  cout << node1.use_count() << endl;
  cout << node2.use_count() << endl;
  return 0;
}


ca32631c44524ec688f0440ebb40338b.png


weak_ptr的模拟实现

namespace bjy
{
  template<class T>
  class weak_ptr
  {
  public:
    weak_ptr(const weak_ptr<T>& wp)noexcept :_ptr(ptr) {}
    weak_ptr(const shared_ptr<T>& sp)noexcept :_ptr(sp.get()) {}
    weak_ptr& operator=(const shared_ptr<T>& sp) {
      _ptr = sp.get();
      return *this;
    }
    T& operator*() { return *_ptr; };
    T* operator->() { return _ptr; };
  private:
    T* _ptr;
  };
}

利用shared_ptr的成员函数get()获取裸指针


六、C++11与Boost中智能指针的关系

C++98中产生了第一个智能指针auto_ptr

C++ boost库给出了更实用的scoped_ptr、shared_ptr和weak_ptr

C++TR1,引入了boost中的shared_ptr等,不过TR1并不是标准版

C++11,引入了boost库中的unique_ptr、shared_ptr和weak_ptr。unique_ptr对应的是boost库中的scoped_ptr,并且C++11中的智能指针的实现原理是参考boost中实现的

注意:


Boost是为C++标准库提供扩展的一些C++程序库的总称。Boost库是一个可移植、提供源代码的C++库,作为标准库的后备,是C++标准化进程的开发引擎之一,是为C++语言标准库提供扩展的一些C++程序库的总称


Boost库由C++标准委员会库工作组成员发起,其中有些内容有望成为下一代C++标准库内容。在C++社区中影响甚大,是不折不扣的“准”标准库


Boost由于其对跨平台的强调,对标准C++的强调,与编写平台无关。但Boost中也有很多是实验性质的东西,在实际的开发中使用需要谨慎



目录
相关文章
|
1月前
|
存储 程序员 C++
深入解析C++中的函数指针与`typedef`的妙用
本文深入解析了C++中的函数指针及其与`typedef`的结合使用。通过图示和代码示例,详细介绍了函数指针的基本概念、声明和使用方法,并展示了如何利用`typedef`简化复杂的函数指针声明,提升代码的可读性和可维护性。
88 1
|
2月前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
208 4
|
3月前
|
存储 安全 编译器
在 C++中,引用和指针的区别
在C++中,引用和指针都是用于间接访问对象的工具,但它们有显著区别。引用是对象的别名,必须在定义时初始化且不可重新绑定;指针是一个变量,可以指向不同对象,也可为空。引用更安全,指针更灵活。
|
3月前
|
存储 C++
c++的指针完整教程
本文提供了一个全面的C++指针教程,包括指针的声明与初始化、访问指针指向的值、指针运算、指针与函数的关系、动态内存分配,以及不同类型指针(如一级指针、二级指针、整型指针、字符指针、数组指针、函数指针、成员指针、void指针)的介绍,还提到了不同位数机器上指针大小的差异。
108 1
|
3月前
|
存储 编译器 C语言
C++入门2——类与对象1(类的定义和this指针)
C++入门2——类与对象1(类的定义和this指针)
60 2
|
3月前
|
存储 安全 编译器
【C++】C++特性揭秘:引用与内联函数 | auto关键字与for循环 | 指针空值(一)
【C++】C++特性揭秘:引用与内联函数 | auto关键字与for循环 | 指针空值
|
3月前
|
存储 C++ 索引
C++函数指针详解
【10月更文挑战第3天】本文介绍了C++中的函数指针概念、定义与应用。函数指针是一种指向函数的特殊指针,其类型取决于函数的返回值与参数类型。定义函数指针需指定返回类型和参数列表,如 `int (*funcPtr)(int, int);`。通过赋值函数名给指针,即可调用该函数,支持两种调用格式:`(*funcPtr)(参数)` 和 `funcPtr(参数)`。函数指针还可作为参数传递给其他函数,增强程序灵活性。此外,也可创建函数指针数组,存储多个函数指针。
116 6
|
4月前
|
编译器 C++
【C++核心】指针和引用案例详解
这篇文章详细讲解了C++中指针和引用的概念、使用场景和操作技巧,包括指针的定义、指针与数组、指针与函数的关系,以及引用的基本使用、注意事项和作为函数参数和返回值的用法。
71 3
|
3月前
|
存储 编译器 程序员
【C++】C++特性揭秘:引用与内联函数 | auto关键字与for循环 | 指针空值(二)
【C++】C++特性揭秘:引用与内联函数 | auto关键字与for循环 | 指针空值
|
4月前
|
C++
C++(十八)Smart Pointer 智能指针简介
智能指针是C++中用于管理动态分配内存的一种机制,通过自动释放不再使用的内存来防止内存泄漏。`auto_ptr`是早期的一种实现,但已被`shared_ptr`和`weak_ptr`取代。这些智能指针基于RAII(Resource Acquisition Is Initialization)原则,即资源获取即初始化。RAII确保对象在其生命周期结束时自动释放资源。通过重载`*`和`-&gt;`运算符,可以方便地访问和操作智能指针所指向的对象。