【C++11(三)】智能指针详解--RAII思想&循环引用问题

简介: 【C++11(三)】智能指针详解--RAII思想&循环引用问题

1. 前言

相信学C++的同学或多或少的听说过

智能指针这个词,博主刚听见这个词时

,觉得它应该很复杂,并且很高大上,但不

管是多牛的东西,都是人写出来的,是可

学习的!不要怀着害怕的心理来学习它

本章重点:

本篇文章着重讲解智能指针的发展历史
中出现过的auto_ptr,unique_ptr以及主
角shared_ptr.并且会介绍什么是RAII思想
以及为什么要有智能指针这一话题,最后
会给大家分析shared_ptr的循环引用问题
以及定制删除器的基本概念


2. 为什么要有智能指针?

在写代码时,我们经常在堆上申请空间

但是偶尔会忘记释放空间,会造成内存

泄漏问题,当然,这不是最重要的,在某些

场景下即使你释放了也会有问题:

int div()
{
  int a, b;
  cin >> a >> b;
  if (b == 0)
  throw invalid_argument("除0错误");
  return a / b;
}
void Func()
{
  // 1、如果p1这里new 抛异常会如何?
  // 2、如果p2这里new 抛异常会如何?
  // 3、如果div调用这里又会抛异常会如何?
  int* p1 = new int;
  int* p2 = new int;
  cout << div() << endl;
  delete p1;
  delete p2;
}
int main()
{
  try
  {
    Func();
  }
  catch (exception& e)
  {
    cout << e.what() << endl;
  }
  return 0;
}

在上面代码的这种场景中,不管是使用
new还是调用div函数都有抛异常的风险
并且程序一旦抛异常就会直接跳到catch
处,所以上面的代码一旦抛异常就代表着
delete p1和p2并不会执行,也就会出现
内存泄漏的问题!这个问题不使用智能
指针是很难解决的!!!


3. RAII思想以及智能指针的设计

  1. RAII思想

RAII思想是一种 利用对象生命周期来控制程序资源 (如内存、文件句柄、网络连接、互斥量等等)的简单技术。在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象

这种做法有两种好处:

  • 不需要显式地释放资源
  • 对象所需的资源在其生命期内始终有效
  1. 智能指针的基本设计

现在我们来写一个类,构造函数的

时候创造资源,析构函数的时候释放

资源,当对象出了作用域会自动调用析构!

// 使用RAII思想设计的SmartPtr类
template<class T>
class SmartPtr {
public:
SmartPtr(T* ptr = nullptr)
  : _ptr(ptr)
{}
~SmartPtr()
{
  if(_ptr!=nullptr)
    delete _ptr;
}
T& operator*() {return *_ptr;}
T* operator->() {return _ptr;}
private:
    T* _ptr;
};

现在我们来使用一下它:

SmartPtr<int> sp1(new int(10));
*sp = 20;

当然,重载了->是给自定义类型用的


4. C++智能指针的发展历史

首先,我们要清楚智能指针的一个大坑
那就是当一个指针赋值给另外一个指针
时,我们需要的是浅拷贝,因为我们就是想
让两个指针指向同一块空间,但是指向了
同一块空间就会有析构函数调用两次的风险
由于这一个大坑,智能指针进行了很多次迭代

  1. 在C++98的时候就已经在库中实现
    了智能指针了,它就是 auto_ptr

既然智能指针是随着历史不断发展的

就证明它前面的版本写的不咋滴[doge]

事实也是如此,auto_ptr是这样实现的,

既然有析构两次的风险,那么当我把A

指针赋值给B指针后,A指针就销毁不能用

了,对于不了解auto_ptr的人来说这无疑是

一个巨大的风险!

auto_ptr<int> ap1(new int(10));
auto_ptr<int> ap2(ap1);
//此时ap1已经失效了!
  1. 有了这一大坑后,C++11推出了全新
    的智能指针: unique_ptr

unique_ptr的做法比auto_ptr还绝

智能指针不是拷贝有问题吗?那么

unique_ptr就禁用了拷贝和赋值,

很显然这也是一个坑,但是在实际

场景下,unique_ptr至少还能被用到

但auto_ptr是很多公司明令禁止使用的!

unique_ptr<int> up1(new int(10));
unique_ptr<int> up2(up1);//这里会直接报错
  1. 经过两次失败的智能指针后,C++11
    还推出了今天的主角: shared_ptr

shared_ptr可堪称完美的智能指针

也是实际中使用的最多的智能指针

它采用的是引用计数的思想,当指向

这份空间的计数是1时才析构,大于1

时就将计数减一,非常的优雅!

由于智能指针在面试时让手撕的概率很大

所以我们会模拟实现它


5. shared_ptr模拟实现

我们使用引用计数的方式来实现

shared_ptr,也就是在原先代码的

基础上增加一个int*成员变量来保存

还有几个指针指向当前空间!

template<class T>
class Smart_Ptr //实现的C++11的shared_ptr版本
{
public:
  Smart_Ptr(T* ptr = nullptr)
    :_ptr(ptr)
    ,_pcount(new int(1))
  {}
  ~Smart_Ptr()
  {
    Release();
  }
  Smart_Ptr(const Smart_Ptr<T>& sp)
    :_ptr(sp._ptr)
    ,_pcount(sp._pcount)
  {
    Addcount();
  }
  Smart_Ptr<T>& operator=(const Smart_Ptr<T>& sp)
  {
    if (_ptr != sp._ptr)
    {
      Release();
      _ptr = sp._ptr;
      _pcount = sp._pcount;
      Addcount();
    }
    return *this;
  }
  void Release()
  {
    if (--(*_pcount) == 0)//销毁最后一个变量时才释放资源
    {
      delete _ptr;
      delete _pcount;
      delete _pmtx;
    }
  }
  void Addcount()
  {
    (*_pcount)++;
  }
  void Subcount()
  {
    Release();
private:
  T* _ptr;
  int* _pcount;
};

我们将计数++贺计数- -特意的提出来
这是因为很多场景下都需要这两个函数.
当计数不为1时就- -计数,当计数为一才
释放资源,并且这样写的好处是相同类型
的指针对象即使指向不同的空间也不会
出错,相反,使用static定义成员指针变量
就会出现上面的这种问题!


6. shared_ptr的循环引用问题

请看下面的代码运行会崩溃:

struct ListNode
{
  int _data;
  shared_ptr<ListNode> prev;
  shared_ptr<ListNode> next;
  ~ListNode(){ cout << "~ListNode()" << endl; }
};
int main()
{
  shared_ptr<ListNode> node1(new ListNode);
  shared_ptr<ListNode> node2(new ListNode);
  node1->next = node2;
  node2->prev = node1;
  return 0;
}

为啥会崩溃?下面我用画图加文字
的方式帮大家分析一下此问题:

现在来进一步分析:当main函数调用完,
node2会先析构,但是此时引用计数是2
所以不会释放空间而是将计数变为1.
然后node1再析构,同上,它的引用计数
也减为一,但是这两份空间并不会释放,
因为要node2的prev释放后,node1的空间
才会释放,那node2的prev什么时候释放?
答案是node2这份空间释放了才会释放
prev,那么node2这份空间什么时候释放?
答案是node1的next释放了它才释放,这
就形成了一个死循环,我等你释放了我才
能释放,对方也在等我释放了对方才能
释放,这就是"循环引用问题"

最好的解决方案就是在使用智能指针

的时候跳过这个坑,不用将智能指针和

这种场景一起使用!!!


7. 定制删除器

使用智能指针时可能会遇见下面的问题:

shared_ptr<int> sp1(new int[10]);

当变量出作用域销毁时即报错

因为new []对应的是delete [].

然而库中写法并不能识别有没有[]

还有一些问题:

shared_ptr<FILE> sp3(fopen("Test.cpp", "r"));

此时智能指针管理的对象并不是堆上

开辟的空间,delete完全没法用,此时需

要使用fclose,所以定制删除器非常重要

在构造函数的地方可以传入一个定制
删除器,也就是一个函数对象,此函数
中有对应的删除方法,请看下面的代码:

shared_ptr<int> sp2(new int[10], [](int* ptr) {delete[] ptr; });
shared_ptr<FILE> sp3(fopen("Test.cpp", "r"), [](FILE* ptr) {fclose(ptr); });

注:定制删除器属于了解的部分


8. 总结以及拓展

智能指针在面试中是常客,经常会被

问到发展历史和shared_ptr的手撕,

学到这里后,C++的所有重要的知识

差不多已经完结了,后面文章更新会慢一点

拓展:weak_ptr的拓展阅读

既然weak_ptr可以解决shared_ptr的

循环引用问题,那么什么是weak_ptr?

有兴趣的同学可以阅读下面这篇文章:

weak_ptr详解


🔎 下期预告:C++异常的处理方式🔍


相关文章
|
3天前
|
存储 安全 编译器
【C++入门 四】学习C++内联函数 | auto关键字 | 基于范围的for循环(C++11) | 指针空值nullptr(C++11)
【C++入门 四】学习C++内联函数 | auto关键字 | 基于范围的for循环(C++11) | 指针空值nullptr(C++11)
|
18天前
|
C++ 容器
【编程技巧】 C++11智能指针
C++11引入了智能指针以自动管理内存,防止内存泄漏和悬挂指针: - `shared_ptr`:引用计数,多所有权,适用于多个对象共享资源。 - `unique_ptr`:独占所有权,更轻量级,适用于单一对象所有者。 - `weak_ptr`:弱引用,不增加引用计数,解决`shared_ptr`循环引用问题。 ## shared_ptr - 支持引用计数,所有者共同负责资源释放。 - 创建方式:空指针、new操作、拷贝构造/移动构造,以及自定义删除器。 - 提供`operator*`和`operator-&gt;`,以及`reset`、`swap`等方法。 ## unique_ptr
228 1
|
19天前
|
存储 安全 编译器
【C++航海王:追寻罗杰的编程之路】引用、内联、auto关键字、基于范围的for、指针空值nullptr
【C++航海王:追寻罗杰的编程之路】引用、内联、auto关键字、基于范围的for、指针空值nullptr
37 5
|
21天前
|
安全 C++ 开发者
C++一分钟之-RAII资源获取即初始化
【6月更文挑战第24天】RAII是C++中一种关键的资源管理技术,它利用对象生命周期自动获取和释放资源,减少内存泄漏。通过构造函数获取资源,析构函数释放资源,确保异常安全。优势包括自动性、异常安全和代码清晰。使用智能指针如`std::unique_ptr`和`std::shared_ptr`,以及标准库容器,可以避免手动管理。自定义RAII类适用于非内存资源。代码示例展示了智能指针和自定义RAII类如何工作。掌握RAII能提升程序的可靠性和可维护性。
27 6
|
21天前
|
存储 Java C#
C++语言模板类对原生指针的封装与模拟
C++|智能指针的智能性和指针性:模板类对原生指针的封装与模拟
|
19天前
|
C++
C++职工管理系统(类继承、文件、指针操作、中文乱码解决)
C++职工管理系统(类继承、文件、指针操作、中文乱码解决)
20 0
C++职工管理系统(类继承、文件、指针操作、中文乱码解决)
|
21天前
|
设计模式 C++ 开发者
C++一分钟之-智能指针:unique_ptr与shared_ptr
【6月更文挑战第24天】C++智能指针`unique_ptr`和`shared_ptr`管理内存,防止泄漏。`unique_ptr`独占资源,离开作用域自动释放;`shared_ptr`通过引用计数共享所有权,最后一个副本销毁时释放资源。常见问题包括`unique_ptr`复制、`shared_ptr`循环引用和裸指针转换。避免这些问题需使用移动语义、`weak_ptr`和明智转换裸指针。示例展示了如何使用它们管理资源。正确使用能提升代码安全性和效率。
22 2
|
3天前
|
安全 编译器 程序员
【C++11】智能指针
【C++11】智能指针
4 0
|
13天前
|
存储 安全 程序员