【C++】2.10智能指针详解

一、使用

1. 抛异常的安全性

cpp 复制代码
int calc(int a, int b) {
    if (b == 0) {
        throw("by 0");
    } else {
        return a / b;
    }
}
void func() {
    int* p1 = new int[10];
    calc(1, 0);
}
  • 在这个函数中,calc 抛出异常,int 数组就不会被析构,造成内存泄露。

  • 除非在每一个 new 后面都跟着一个 delete[] p1;。

2. RAII 和智能指针设计思路

  • 利用对象生命周期管理资源的获取和释放,将资源托管给类的析构函数。

  • 并且重载了 *、-> 等运算符进行调用,使其表现得像指针。

cpp 复制代码
int calc(int a, int b) {
    if (b == 0) {
        throw("by 0");
    } else {
        return a / b;
    }
}
void func() {
    SmartPtr<int> p1 = new int[10];
    calc(1, 0);
}
  • 异常时,智能指针对象一定会析构,就能带着其管理的 int* 资源一起析构。

3. C++ 标准库的智能指针

  • 拷贝问题:由于智能指针是仿指针,因此拷贝也需要仿照指针进行浅拷贝,但这会带来析构两次的问题。
  1. auto_ptr : C++98 的智能指针,极其不推荐使用,因为它会让被拷贝的源指针悬空。
    https://media/image1.png

  2. unique_ptr : 不能拷贝,除非被 move。

  3. shared_ptr: 用引用计数实现拷贝。

    cpp 复制代码
    shared_ptr<int> p1(new int[10]);
    shared_ptr<int> p2 = p1;
    cout << p1.use_count();
    • 还可以通过 use_count() 查看引用计数。

二、shared_ptr 实现

1. 成员变量

cpp 复制代码
T* _ptr;
int* _count;
  • 由于静态变量会统计所有智能指针的个数,而智能指针可能会指向不同的资源,因此不能用静态变量来计数。

  • 需要使用动态开辟的指针,当构造时就开辟新的空间来存储计数。

2. 构造函数

cpp 复制代码
shared_ptr(const shared_ptr<T>& sp)
    :_ptr(sp._ptr)
    ,_count(sp._count) {
    (*_count)++;
}
shared_ptr(T* ptr)
    :_ptr(ptr) {
    _count = new int(1);
}
  • 拷贝构造时,引用计数加一。

  • 用原始指针构造时,新开一块空间对 _count 进行初始化计数。

3. 析构函数

cpp 复制代码
~shared_ptr() {
    if (--(*_count) == 0) {
        delete _ptr;
        delete _count;
    }
}
  • 只有引用计数到达 0 时才释放管理的资源。

4. 解引用操作

cpp 复制代码
T& operator*() {
    return *_ptr;
}
T* operator->() {
    return _ptr;
}

5. 获取引用计数

cpp 复制代码
int use_count() {
    return *_count;
}

6. 赋值运算符重载(重点)

  • 需要先处理当前智能指针管理的资源(如果引用计数到 0 则析构)。

  • 然后接收新的资源,并对新的引用计数加一。

cpp 复制代码
shared_ptr<T>& operator=(const shared_ptr<T>& sp) {
    if (sp._ptr != _ptr) {
        if (--(*_count) == 0) {
            delete _ptr;
            delete _count;
        }
        _count = sp._count;
        _ptr = sp._ptr;
        ++(*_count);
    }
    return *this;
}

三、定制删除器

  • 由于智能指针指向的资源不一定是 new 出来的单个 T 类型对象,还可能是 T 数组,甚至是打开的文件句柄。

  • 直接用 delete 或 delete[] 释放可能会出错。

  1. 模板特化

    • 对于数组,可以使用 shared_ptr<Date[]>,库会进行模板特化,调用 delete[]。

      复制代码
      shared_ptr<Date[]> d1(new Date[10]);
  2. 仿函数

    • 对于文件等资源,delete 无法释放。

      复制代码
      shared_ptr<FILE> p3(fopen("源.cpp", "r")); // 错误,会造成内存泄露
    • 需要定义删除器。

      cpp 复制代码
      struct del {
          void operator()(FILE* fp) const {
              if (fp) {
                  fclose(fp);
                  std::cout << "文件已关闭" << std::endl;
              }
          }
      };
      shared_ptr<FILE> p3(fopen("源.cpp", "r"), del());
  3. unique_ptr 的删除器

    • unique_ptr 的删除器类型需要在模板参数中指定,而不是在构造函数中传入。

      复制代码
      unique_ptr<FILE, del> p4(fopen("源.cpp", "r"));
    • 也可以使用 lambda 表达式,但需要用 decltype 推导其类型。

      复制代码
      auto fclosefunc = [](FILE* ptr) { fclose(ptr); };
      unique_ptr<FILE, decltype(fclosefunc)> p5(fopen("源.cpp", "r"), fclosefunc);
  4. 模拟实现定制删除器

    • 由于需要在构造函数中接收仿函数,可以用 function 包装器来存储。

      复制代码
      function<void(T*)> _del = [](T* t) { delete t; };
    • 提供两个构造函数重载,区分是否传入自定义删除器。

      cpp 复制代码
      shared_ptr(T* ptr)
          :_ptr(ptr) {
          _count = new int(1);
      }
      template <class D>
      shared_ptr(T* ptr, D del)
          : _ptr(ptr)
          , _del(del) {
          _count = new int(1);
      }
    • 析构时调用包装器存储的函数。

      cpp 复制代码
      ~shared_ptr() {
          if (--(*_count) == 0) {
              _del(_ptr);
              delete _count;
          }
      }
  5. make_shared

    • 由于 shared_ptr 需要为对象和控制块(包含引用计数等)分别开辟空间,可能会产生内存碎片。

    • make_shared 可以将这两个部分一次性连续开辟,减少碎片。

      cpp

      复制代码
      shared_ptr<Date> p6 = make_shared<Date>();

四、循环引用

  • 考虑在双向链表中使用 shared_ptr。

    cpp 复制代码
    struct ListNode {
        int _data;
        ListNode* _next;
        ListNode* _prev;
        ~ListNode() {
            cout << "~ListNode()" << endl;
        }
    };
    shared_ptr<bit::ListNode> p1(new bit::ListNode);
    shared_ptr<bit::ListNode> p2(new bit::ListNode);
    p1->_next = p2; // 错误,类型不匹配
  • 在链表的节点里,如果要用智能指针管理,由于 _next 为普通指针,p2 为智能指针,因此无法直接赋值。

  • 需要将节点内的指针也改为智能指针类型。

    cpp 复制代码
    struct ListNode {
        int _data;
        std::shared_ptr<ListNode> _next;
        std::shared_ptr<ListNode> _prev;
        ~ListNode() {
            cout << "~ListNode()" << endl;
        }
    };
  • 但是,这样做会导致内存泄露!

  • 原因分析:

    • 第一个节点的资源由 p1 和 p2 的 _prev 指向,引用计数为 2。

    • 第二个节点的资源由 p2 和 p1 的 _next 指向,引用计数也为 2。

    • 函数结束时,p1 和 p2 先析构,两个节点的引用计数都变为 1。

    • 接着,第一个节点要析构,需要等待第二个节点的 _prev 析构。

    • 第二个节点要析构,需要等待第一个节点的 _next 析构。

    • 这就陷入了循环等待,导致两个节点都无法被释放。

  • 解决方案:weak_ptr

    • 将节点内的指针改为 weak_ptr。

      cpp 复制代码
      struct ListNode {
          int _data;
          std::weak_ptr<ListNode> _next;
          std::weak_ptr<ListNode> _prev;
          ~ListNode() {
              cout << "~ListNode()" << endl;
          }
      };
    • weak_ptr 指向资源但不会增加引用计数,从而打破了循环引用,资源可以正常析构。

相关推荐
小白男神3 天前
MySQL进阶学习四(存储过程、游标、触发器)
mysql
这个DBA有点耶3 天前
MVCC深入:Read View、版本链与快照读——InnoDB并发控制的内核
数据库·mysql·架构
DBA_G3 天前
从地面到云霄:GBase数据库在民航三大场景的落地实践
数据库
自由能燃气设备3 天前
商用全预混低氮冷凝锅炉免费方案vs付费方案对比+选型避坑指南
大数据·数据库·人工智能
科创致远3 天前
科创致远 ESOP 系统核心效能与实战价值展示
大数据·数据库·人工智能·精益工程
IT大白鼠3 天前
Redis 系列 · 第 01 篇——认知入门:Redis 是什么
redis·nosql
彧azz3 天前
Linux 环境下 Redis 学习总结:数据类型、持久化、锁、事务、主从与缓存问题
linux·redis·笔记·学习·面试
kybs19913 天前
全球灾害数据分析可视化 毕业设计-附源码66794
vue.js·spring boot·mysql·安全·django·c#·asp.net
2601_962218613 天前
万象生鲜系统称重自动多退少补算法解决生鲜非标品痛点
大数据·数据库·人工智能·python·算法
程序猿_极客3 天前
【免费】分享一套优质的基于SSM的校园失物招领系统的设计与实现,源码+文档+视频详解(讲解)
java·mysql·ssm·课程设计·失物招领系统