承接移动语义,来更新智能指针,上文说到 unique_ptr
也会用到移动语义的东西。由于 C++ 语言没有自动内存回收机制,程序员每次 new
出来的内存都要手动 delete
,一旦忘记 delete
或者程序过早退出而没有 delete
,导致内存泄漏总是不好的。内存泄漏指由于疏忽或错误造成程序未能释放已经不再使用的内存,从而造成了内存的浪费。
所以,能不能有一种东西的存在,使用 new
创建对象后,对象作用域消失时,能不能自动析构并调用 delete
?手动管理过程繁琐且容易出错,那么尝试通过智能指针来实现这一管理过程。
unique_ptr
std::unique_ptr
是通过指针独占式拥有或严格拥有并管理另一对象,建立了所有权的概念,一个智能指针只能拥有一个对象,一个对象也只能被一个智能指针所拥有,这样智能指针消失时,就会 delete
对象,并在 unique_ptr
离开作用域或拥有的最后一个资源被销毁时释放。
赋值时,对象的所有权会转让,也就是移动赋值,因为所有权独享所以删除了拷贝构造,只能通过 move
实现移动所有权。应用场景:对象延迟销毁,可以将对象转移到另一个线程释放,不影响主线程。来看一个例子:
1 |
|
智能指针和指针一样,可以解引用,或者访问类成员。不过智能指针指向的对象必须位于堆区,否则 delete
时会发生错误。
智能指针的构造函数有 explicit
修饰,因此不能发生隐式转换,也就是说,下面的语句是错误的:
1 | double* pr = new double; |
然后顺手看了官网的 API
,比较常用的是 release
, swap
和 reset
方法:
1 |
|
shared_ptr
shared_ptr
使用引用计数,记录多个指针可以指向一个对象,即多个 shared_ptr
共同管理一个对象,仅当最后一个指针消失时,调用 delete
。
因此,shared_ptr
对象除了包括一个所拥有对象的指针外,还必须包括一个引用计数代理对象的指针,通过计数实现对象的最后一个拥有着有责任销毁对象,并清理与该对象相关的所有资源;且时间上的开销主要在初始化和拷贝操作上,因为不能像移动语义那样移动所有权。但这也造就了 shared_ptr
独特的应用场景:
- 共享权不明,多个对象管理同一个内存。举个例子,实验室的所有人共同拥有空调,空调的所有权不属于某一个人,但最后一个人走的时候要关空调。
- 如果一个对象的复制操作很费时,同时我们又需要在函数间传递这个对象,我们往往会选择传递指向这个对象的指针来代替传递对象本身,以此来避免对象的复制操作。既然选择使用指针,那么使用
shared_ptr
是一个更好的选择,即起到了向函数传递对象的作用,又不用为释放对象操心。 shared_ptr
支持在构造的时候传入一个定制删除器,替代delete
在生命周期结束时调用,以此实现RAII
的思想,即资源的有效期与持有资源的对象的生命期严格绑定,将资源的获取放在类的构造函数里,资源的释放放在类的析构函数里。在类的生存期结束的时候,析构函数会被自动调用,对应的资源将会释放。- 把指针存入标准库容器时,如果容器中保存的是普通指针,当我们在清空某个容器时,先要释放容器中指针所指向的资源,然后才能清空这些指针本身。如果普通指针替换成相应
shared_ptr
,容器的clear()
清空shared_ptr
,而随着shared_ptr
的释放,它会自动释放它所管理的资源,而无需我们主动去释放。
1 | // tranditionally |
weak_ptr
weak_ptr
允许共享但不拥有某对象,指向 shared_ptr
指针指向的对象的内存,却并不拥有该内存。在使用的时候检查一下指针的有效性。一旦最后一个共享该对象的智能指针失去了所有权,任何 weak_ptr
的 lock()
方法返回 nullptr
。因此,在 default
和 copy
构造函数之外,weak_ptr
只提供接受一个 shared_ptr
的构造函数。
1 |
|
可以应用于对象可能失效的场景,可打破循环引用 cycles of references
的问题。
内存泄漏
题外话,我们可以通过观察来发现程序是否存在内存泄漏:程序长时间运行后内存占用率一直不断的缓慢的上升,而实际上在你的逻辑中并没有这么多的内存需求。至于手动解决内存泄漏,一般的方法是:
- 查询
new
与delete
,看看内存的申请与释放是不是成对释放的 - 如果依旧发生内存泄露,可以通过记录申请与释放的对象数目是否一致来判断。在类中追加一个静态变量
static int count
,在构造函数中执行count++
,在析构函数中执行count--
,通过在程序结束前将所有类析构,之后输出静态变量,看count
的值是否为 0。 - 检查类中申请的空间是否完全释放,尤其是存在继承父类的情况,看看子类中是否调用了父类的析构函数,有可能会因为子类析构时没有释放父类中申请的内存空间,这里建议将父类的析构函数声明为虚函数,否则可能不会调用子类的析构函数。
- 对于函数中申请的临时空间,认真检查,是否存在提前跳出函数的地方没有释放内存。