【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 数组,甚至是打开的文件句柄。

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

  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;
        }
    };
  • 但是,这样做会导致内存泄露!

  • 原因分析:

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

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

    • 函数结束时,p1p2 先析构,两个节点的引用计数都变为 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 指向资源但不会增加引用计数,从而打破了循环引用,资源可以正常析构。

相关推荐
2401_858286112 小时前
从Redis 8.4.0源码看快速排序(1) 宏函数min和swapcode
c语言·数据库·redis·缓存·快速排序·宏函数
hanqunfeng2 小时前
(一)Redis 7 + ACL 单节点、主从、哨兵、集群构建方法
redis
茁壮成长的露露2 小时前
MongoDB单机安装
数据库·mongodb
qq_406176142 小时前
JS防抖与节流:从原理到实战的性能优化方案
服务器·数据库·php
a***59262 小时前
MySQL数据可视化实战指南
数据库·mysql·信息可视化
Maggie_ssss_supp2 小时前
LINUX-MySQL多表查询
数据库·mysql
lxp1997412 小时前
Mysql短课题全手稿
数据库·mysql
我是一只小青蛙8882 小时前
Python实战:Kingbase数据库高效操作指南
数据库·oracle
龙亘川3 小时前
【课程5.7】代码编写:违建处置指标计算(违建发现率、整改率SQL实现)
数据库·oracle·智慧城市·一网统管平台