【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++异常的处理方式🔍


相关文章
|
13天前
|
存储 安全 C++
C++中的引用和指针:区别与应用
引用和指针在C++中都有其独特的优势和应用场景。引用更适合简洁、安全的代码,而指针提供了更大的灵活性和动态内存管理的能力。在实际编程中,根据需求选择适当的类型,能够编写出高效、可维护的代码。理解并正确使用这两种类型,是掌握C++编程的关键一步。
19 1
|
13天前
|
数据采集 存储 编译器
this指针如何使C++成员指针可调用
本文介绍了C++中的this指针,它是一个隐藏的指针,用于在成员函数中访问对象实例的成员。文章通过代码示例阐述了this指针的工作原理,以及如何使用指向成员变量和成员函数的指针。此外,还提供了一个多线程爬虫示例,展示this指针如何使成员指针在对象实例上调用,同时利用代理IP和多线程提升爬取效率。
this指针如何使C++成员指针可调用
|
1天前
|
安全 C++ 开发者
C++一分钟之-RAII资源获取即初始化
【6月更文挑战第24天】RAII是C++中一种关键的资源管理技术,它利用对象生命周期自动获取和释放资源,减少内存泄漏。通过构造函数获取资源,析构函数释放资源,确保异常安全。优势包括自动性、异常安全和代码清晰。使用智能指针如`std::unique_ptr`和`std::shared_ptr`,以及标准库容器,可以避免手动管理。自定义RAII类适用于非内存资源。代码示例展示了智能指针和自定义RAII类如何工作。掌握RAII能提升程序的可靠性和可维护性。
15 6
|
1天前
|
存储 Java C#
C++语言模板类对原生指针的封装与模拟
C++|智能指针的智能性和指针性:模板类对原生指针的封装与模拟
|
1天前
|
设计模式 C++ 开发者
C++一分钟之-智能指针:unique_ptr与shared_ptr
【6月更文挑战第24天】C++智能指针`unique_ptr`和`shared_ptr`管理内存,防止泄漏。`unique_ptr`独占资源,离开作用域自动释放;`shared_ptr`通过引用计数共享所有权,最后一个副本销毁时释放资源。常见问题包括`unique_ptr`复制、`shared_ptr`循环引用和裸指针转换。避免这些问题需使用移动语义、`weak_ptr`和明智转换裸指针。示例展示了如何使用它们管理资源。正确使用能提升代码安全性和效率。
13 2
|
6天前
|
存储 算法 安全
C++一分钟之-数组与指针基础
【6月更文挑战第19天】在C++中,数组和指针是核心概念,数组是连续内存存储相同类型的数据,而指针是存储内存地址的变量。数组名等同于指向其首元素的常量指针。常见问题包括数组越界、尝试改变固定大小数组、不正确的指针算术以及忘记释放动态内存。使用动态分配和智能指针可避免这些问题。示例代码展示了安全访问和管理内存的方法,强调了实践的重要性。
23 3
|
10天前
|
编译器 Linux C++
C++智能指针
**C++智能指针是RAII技术的体现,用于自动管理动态内存,防止内存泄漏。主要有三种类型:已废弃的std::auto_ptr、不可复制的std::unique_ptr和可共享的std::shared_ptr。std::unique_ptr通过禁止拷贝和赋值确保唯一所有权,而std::shared_ptr使用引用计数来协调多个指针对同一资源的共享。在C++17中,std::auto_ptr因设计缺陷被移除。**
|
18天前
|
存储 编译器 C++
C++中的指针
C++中的指针
11 1
|
20天前
|
存储 安全 编译器
C++进阶之路:何为引用、内联函数、auto与指针空值nullptr关键字
C++进阶之路:何为引用、内联函数、auto与指针空值nullptr关键字
13 2
|
23天前
|
C++ 存储 Java
C++ 引用和指针:内存地址、创建方法及应用解析
'markdown'C++ 中的引用是现有变量的别名,用 `&` 创建。例如:`string &meal = food;`。指针通过 `&` 获取变量内存地址,用 `*` 创建。指针变量存储地址,如 `string *ptr = &food;`。引用不可为空且不可变,指针可为空且可变,适用于动态内存和复杂数据结构。两者在函数参数传递和效率提升方面各有优势。 ```