内存管理
-
堆
-
定义:动态分配内存的区域
-
c++会设计到的两个有关内存管理器的操作
-
让内存管理器分配一个某大小的内存块
分配内存要考虑程序当前已经有多少未分配的内存
内存不足时要从操作系统申请新的内存;内存充足时,从可用内存里取出一块合适大小的内存,将其标记为已用,再将其返回给要求内存的代码
-
让内存管理器释放一个之前分配过的内存块
对于连续未使用的内存块,内存管理器需要将其合并成一块,以便可以满足后续较大内存的分配
-
-
-
栈
- 定义:函数调用过程中产生的本地变量和调用数据的区域
- 大致执行过程:
- 当函数调用另一个函数时,会把参数也压入栈里,然后把下一行汇编指令的地址压入栈,并跳转到新的函数。新的函数进入后,首先做一些必须的保存工作,然后会调整栈指针,分配出本地变量所需的空间,随后执行函数中的代码,并在执行完毕之后,根据调用者压入栈的地址,返回到调用者未执行的代码中继续执行
- 优点:
- 栈上的分配十分简单,只需移动栈指针
- 栈上的释放十分简单,函数执行结束时只需移动栈指针
- 由于后进先出的执行过程,不可能出现内存碎片
-
RAII(Resource Acquisition Is Initialization)
- 定义:RAII 依赖于栈和析构函数,对所有的资源进行管理
- 栈方便且风险低,为什么使用RAII?
- 对象占用内存很大
- 在编译期不能确定对象的大小
- 对象是函数的返回值,但由于特殊原因(如对象切片),不应使用对象的值进行返回
- 用途:
- 在析构函数里释放内存
- 关闭文件
- 释放同步锁
- 释放其他重要的系统资源
智能指针
- 什么是智能指针?
- 完全实践RAII,包装裸指针
- 行为类似常规指针,但负责自动释放所指对象
- 一个模板
- 使用动态内存
- 为什么使用智能指针?
- 能够自动适应各种复杂的情况,防止误用指针导致的隐患。如,内存泄漏
- 三种智能指针。都定义在<memory>
- unique_ptr。独占所指对象
- shared_ptr。允许多个指针指向同一对象
- weak_ptr。一种弱引用,指向shared_ptr所管理的对象
- 建议
-
内置指针
仅用于范围、循环或助手函数有限的小代码块中,这些代码块的性能
非常关键,而且不存在混淆所有权
的可能性 - 在单独的代码行上创建智能指针,而不要在参数列表中创建智能指针,这样就不会因为参数列表分配规则而发生可能的资源泄漏
-
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 -
特性:
- 一个unique_ptr只能指向一个对象,且当unique_ptr被销毁时,它所指向的对象也被销毁
- unique_ptr离开作⽤域时,若其指向对象,则将其所指对象销毁(默认delete)
- unique_ptr不是指针,而是
对象
- 定义unique_ptr时,需要将其绑定到一个new返回的指针
- 不能对unique_ptr调用delete。它会自动管理初始化时的指针,在离开作用域时析构释放内存
- 不支持加减运算,不能
随意
移动指针地址 - 不支持
普通
的拷贝或赋值
-
所有权
- 一个unique_ptr只能指向一个对象,且当unique_ptr被销毁时,它所指向的对象也被销毁。为了实现这个目的,unique_ptr使用了move语义,且禁止拷贝赋值,必须使用
std::move()
显示地声明所有权转移 - 尽量不要对 unique_ptr 执行赋值操作
- 一个unique_ptr只能指向一个对象,且当unique_ptr被销毁时,它所指向的对象也被销毁。为了实现这个目的,unique_ptr使用了move语义,且禁止拷贝赋值,必须使用
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代替deleteshared_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的所有权可以被
安全共享
。因为它的内部使用引用计数
- 与unique_ptr的所有权不同,shared_ptr的所有权可以被
-
特性
- 当引用计数减少到 0,shared_ptr会自动调用 delete 释放内存
- shared_ptr的实现机制是在拷⻉构造时使⽤同⼀份引⽤计数
- 当对象不再被使用,shared_ptr会自动调用 delete 释放内存
- shared_ptr可以在任何场合替代内置指针,而不用担心资源回收的问题
-
注意事项
-
引用计数的存储和管理都是成本,不如unique_ptr
-
引用计数的变动非常复杂,很难知道其真正释放资源的时机。因此,对象的析构函数不要有非常复杂、严重阻塞的操作
-
无法解决循环引用问题
-
不建议在函数的参数使用shared_ptr,因为成本高
-
同⼀个shared_ptr被多个线程“读”是安全的
-
同⼀个shared_ptr被多个线程“写”是不安全的
在多个线程中同时对⼀个shared_ptr循环执⾏两遍swap。 shared_ptr的swap函数的作⽤就是和另外⼀个shared_ptr交换引⽤对象和引⽤计数,是写操作。执⾏两遍swap之后, shared_ptr引⽤的对象的值应该不变
-
共享引⽤计数的不同的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是一种
-
为什么使用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); }
-
-
注意事项
- 将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数
- 一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放,即使weak_ptr指向对象
- 创建一个weak_ptr时,需要用shared_ptr初始化
- 由于对象可能不存在,不能使用weak_ptr直接访问对象,而是调用lock()
reference
Smart pointers (Modern C++) | Microsoft Learn
罗剑锋的 C++ 实战笔记 (geekbang.org)
C++ Primer 中文版(第 5 版) (豆瓣) (douban.com)
知识星球 | 深度连接铁杆粉丝,运营高品质社群,知识变现的工具 (zsxq.com)