内存管理

    • 定义:动态分配内存的区域

    • c++会设计到的两个有关内存管理器的操作

      1. 让内存管理器分配一个某大小的内存块

        分配内存要考虑程序当前已经有多少未分配的内存

        内存不足时要从操作系统申请新的内存;内存充足时,从可用内存里取出一块合适大小的内存,将其标记为已用,再将其返回给要求内存的代码

      2. 让内存管理器释放一个之前分配过的内存块

        对于连续未使用的内存块,内存管理器需要将其合并成一块,以便可以满足后续较大内存的分配

    • 定义:函数调用过程中产生的本地变量和调用数据的区域
    • 大致执行过程:
      • 当函数调用另一个函数时,会把参数也压入栈里,然后把下一行汇编指令的地址压入栈,并跳转到新的函数。新的函数进入后,首先做一些必须的保存工作,然后会调整栈指针,分配出本地变量所需的空间,随后执行函数中的代码,并在执行完毕之后,根据调用者压入栈的地址,返回到调用者未执行的代码中继续执行
    • 优点:
      1. 栈上的分配十分简单,只需移动栈指针
      2. 栈上的释放十分简单,函数执行结束时只需移动栈指针
      3. 由于后进先出的执行过程,不可能出现内存碎片
  • RAII(Resource Acquisition Is Initialization)

    • 定义:RAII 依赖于栈和析构函数,对所有的资源进行管理
    • 栈方便且风险低,为什么使用RAII?
      1. 对象占用内存很大
      2. 在编译期不能确定对象的大小
      3. 对象是函数的返回值,但由于特殊原因(如对象切片),不应使用对象的值进行返回
    • 用途:
      1. 在析构函数里释放内存
      2. 关闭文件
      3. 释放同步锁
      4. 释放其他重要的系统资源

智能指针

  • 什么是智能指针?
    1. 完全实践RAII,包装裸指针
    2. 行为类似常规指针,但负责自动释放所指对象
    3. 一个模板
    4. 使用动态内存
  • 为什么使用智能指针?
    • 能够自动适应各种复杂的情况,防止误用指针导致的隐患。如,内存泄漏
  • 三种智能指针。都定义在<memory>
    1. unique_ptr。独占所指对象
    2. shared_ptr。允许多个指针指向同一对象
    3. weak_ptr。一种弱引用,指向shared_ptr所管理的对象
  • 建议
    1. 内置指针仅用于范围、循环或助手函数有限的小代码块中,这些代码块的性能非常关键,而且不存在混淆所有权的可能性
    2. 在单独的代码行上创建智能指针,而不要在参数列表中创建智能指针,这样就不会因为参数列表分配规则而发生可能的资源泄漏

unique_ptr

  • 操作

    unique_ptr<T> u1
    unique_ptr<T,D>u2
    空unique_ptr,可以指向类型为T的对象。
    u1自己调用delete释放它的指针;u2自己调用一个类型为D的可调用对象来释放它的指针
    unique_ptr<T,D> u(d) 空unique_ptr,指向类型为T的对象,用类型为D的对象d代替delete
    u = nullptr 释放u指向的对象,将u置空
    u.release() 返回指针,u放弃对指针的控制权,并将u置空
    u.reset() 释放u指向的对象
    u.reset(q)
    u.reset(nullptr)
    如果提供内置指针q,令u指向q指向的对象。否则将u置空

    其中,T是对象类型,D为删除器
    更多的参见shared_ptr

  • 特性:

    1. 一个unique_ptr只能指向一个对象,且当unique_ptr被销毁时,它所指向的对象也被销毁
    2. unique_ptr离开作⽤域时,若其指向对象,则将其所指对象销毁(默认delete)
    3. unique_ptr不是指针,而是对象
    4. 定义unique_ptr时,需要将其绑定到一个new返回的指针
    5. 不能对unique_ptr调用delete。它会自动管理初始化时的指针,在离开作用域时析构释放内存
    6. 不支持加减运算,不能随意移动指针地址
    7. 不支持普通的拷贝或赋值
  • 所有权

    1. 一个unique_ptr只能指向一个对象,且当unique_ptr被销毁时,它所指向的对象也被销毁。为了实现这个目的,unique_ptr使用了move语义,且禁止拷贝赋值,必须使用std::move()显示地声明所有权转移
    2. 尽量不要对 unique_ptr 执行赋值操作

shared_ptr

  • 操作

    shared_ptr和unique_ptr都支持的操作
    shared_ptr<T> sp
    unique_ptr<T> up
    空智能指针,可以指向类型为T的对象
    p 若p指向一个对象,则为true
    *p 解引用p,获得它所指对象
    p->mem 等价于(*p).mem
    p.get() 返回p中保存的指针
    swap(p,q)
    p.swap(q)
    交换p和q中的指针
    shared_ptr独有的操作
    make_shared<T>(args) 返回一个shared_ptr,指向动态分配的类型为T的对象
    使用args初始化此对象
    shared_ptr<T>p(q) p是shared_ptr q的拷贝
    递增q的引用计数
    q中的指针必须可以转换为T*
    p = q p 和 q都是shared_ptr,所保存的指针必须可以相互转换
    递减p的引用计数,递增q的引用计数
    p.use_count() 返回与p共享对象的智能指针数量
    比较慢,用于调试
    p.unique() 若p.use_count() 为1,返回true,否则false
    定义和改变shared_ptr
    shared_ptr<T> p(q) p管理内置指针q指向的对象,且q必须指向new分配的内存,还能转换为T*类型
    shared_ptr<T> p(u) p从unique_ptr u接管对象的所有权
    将u置空
    shared_ptr<T> p(q,d) p接管内置指针q指向的对象的所有权,q必须能转换为T*类型
    p使用可调用对象d代替delete
    shared_ptr<T> p(p2,d) p是shared_ptr p2的拷贝,p使用可调用对象d代替delete
    p.reset()
    p.reset(q)
    p.reset(q,d)
    若p是唯一指向对象的shared_ptr,reset会释放p的对象
    若传递内置指针q,则令p指向q,否则将p置空
    若还传递d,使用可调用对象d代替delete
  • 所有权

    • 与unique_ptr的所有权不同,shared_ptr的所有权可以被安全共享。因为它的内部使用引用计数
  • 特性

    1. 当引用计数减少到 0,shared_ptr会自动调用 delete 释放内存
    2. shared_ptr的实现机制是在拷⻉构造时使⽤同⼀份引⽤计数
    3. 当对象不再被使用,shared_ptr会自动调用 delete 释放内存
    4. shared_ptr可以在任何场合替代内置指针,而不用担心资源回收的问题
  • 注意事项

    1. 引用计数的存储和管理都是成本,不如unique_ptr

    2. 引用计数的变动非常复杂,很难知道其真正释放资源的时机。因此,对象的析构函数不要有非常复杂、严重阻塞的操作

    3. 无法解决循环引用问题

    4. 不建议在函数的参数使用shared_ptr,因为成本高

    5. 同⼀个shared_ptr被多个线程“读”是安全的

    6. 同⼀个shared_ptr被多个线程“写”是不安全的

      在多个线程中同时对⼀个shared_ptr循环执⾏两遍swap。 shared_ptr的swap函数的作⽤就是和另外⼀个shared_ptr交换引⽤对象和引⽤计数,是写操作。执⾏两遍swap之后, shared_ptr引⽤的对象的值应该不变

    7. 共享引⽤计数的不同的shared_ptr被多个线程”写“ 是安全的

weak_ptr

  • 操作

    weak_ptr
    weak_ptr<T> w 空weak_ptr可以指向类型为T的对象
    weak_ptr<T> w(sp) w与shared_ptr sp指向相同对象
    T必须能转换为sp指向的类型
    w = p p可以是shared_ptr/weak_ptr
    赋值后w 和 p共享对象
    w.reset() 将w置空
    w.use_count() 与w共享对象的shared_ptr的数量
    w.expired() 若w.use_count 为0,返回true,否则false
    w.lock() 若expired为true,返回一个空的shared_ptr
    否则返回一个指向w的对象的shared_ptr
  • 什么是wake_ptr?

    • wake_ptr是一种不控制所指对象生存期的智能指针,指向一个由shared_ptr管理的对象
  • 为什么使用wake_ptr?

    • 解决shared_ptr存在的循环引用问题

      循环引用:

      class Node final
      { 
      public: 
      	using this_type= Node; 
      	using shared_type = std::shared_ptr<this_type>;
      public: 
          shared_type next;	// 使用智能指针来指向下一个节点
      };
      auto n1 = make_shared<Node>(); // 工厂函数创建智能指针 
      auto n2 = make_shared<Node>(); // 工厂函数创建智能指针 
      assert(n1.use_count() == 1);// 引用计数为1
      assert(n2.use_count() == 1); 
      n1->next = n2;	// 两个节点互指,形成了循环引用
      n2->next = n1; 
      assert(n1.use_count() == 2);	// 引用计数为2
      assert(n2.use_count() == 2);    // 无法减到0,无法销毁,导致内存泄漏
      

      解决:

      将shared_ptr改成weak_ptr

      class Node final
      { 
      public: 
      	using this_type= Node; 
      	using shared_type = std::weak_ptr<this_type>;
      public: 
          shared_type next;	// 使用智能指针来指向下一个节点
      };
      //...
      if (!n1->next.expired()) 	// 检查指针是否有效 
      {
      	auto ptr = n1->next.lock(); // lock()获取shared_ptr
          assert(ptr == n2);
      }
      
  • 注意事项

    1. 将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数
    2. 一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放,即使weak_ptr指向对象
    3. 创建一个weak_ptr时,需要用shared_ptr初始化
    4. 由于对象可能不存在,不能使用weak_ptr直接访问对象,而是调用lock()

reference

Smart pointers (Modern C++) | Microsoft Learn

罗剑锋的 C++ 实战笔记 (geekbang.org)

C++ Primer 中文版(第 5 版) (豆瓣) (douban.com)

知识星球 | 深度连接铁杆粉丝,运营高品质社群,知识变现的工具 (zsxq.com)

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。