相信不少同学都在面试中都被问到过c++智能指针的问题,接踵而至的必定是循环引用了,而我每次的答案都是一招鲜:因为它们都在互相等待对方先释放,所以造成内存泄漏。面试官很满意,我也很满意。
但是为啥要等到对方先释放?在我内心也曾有个问号。。
智能指针起源
所谓人类进步的阶梯就是懒,曾几何时,就有人想,我把new 运算符返回的指针p交给一个对象“托管”,而不用操心在哪里去释放这个指针p,由这个托管者自动的在合适的时机进行指针p的释放,这样也不怕自己忘记释放指针p了。而且,我这边操作托管者,要跟操作原来的指针一毛一样,这样才方便。即假设托管指针p的对象叫做shared_ptr,那么
*shared_ptr 所指,便是 *p 所向!
第一章 auto_ptr
最早的智能指针类,实现了*运算符。你用它实例化出来一个类对象,用起来就好像指针一样。对象的特性自然是在作用域结束时,自动调用析构函数实现自我销毁。如此一来,攻城狮们便可不再操心何时释放指针,而可以随心所欲的摸鱼啦。
但其有个致命缺陷:因为其缺省的复制和赋值构造函数,带来的同一个对象被多个智能指针托管,释放的时候便混乱异常,一不小心就重复释放了,使得用户使用起来战战兢兢,老早就被淘汰了。
第二章 unique_ptr
为了规避auto_ptr可以随意复制和赋值构造的缺陷,就推出了unique_ptr, 其实就是给auto_ptr简单换了个名字,并禁用掉其默认的复制和赋值构造函数,好嘛,大家都别用了<(`^′)>,不写代码,就不会有bug!
第三章 shared_ptr
只有一个unique_ptr,对于省吃俭用的高级攻城狮们来说自然是不够的,于是就有人站了出来喊了一声:我们要搞共享经济——砰!基于引用计数的shared_ptr横空出世了,每多一个人持有这个对象,引用计数就加一;每少一个人持有这个对象,引用计数就减一。引用计数为零时,才做真正的销毁操作。
很快,梦魇悄然而至,“我的shared_ptr怎么没有释放?”——一位焦头烂额的格子少年路过。。
起初,没有人在意这场灾难,这不过是一个指针的丢失、一个bug,一个服务器的宕机,直到这场灾难和每个人息息相关......
"你看这个人写的代码,它好像一坨狗屎"
#include <iostream>
class B;
class A {
public:
A() {
std::cout << "A" << std::endl;
}
~A() {
std::cout << "~A" << std::endl;
}
public:
std::shared_ptr<B> ptr;
};
class B {
public:
B() {
std::cout << "B" << std::endl;
}
~B() {
std::cout << "~B" << std::endl;
}
public:
std::shared_ptr<A> ptr;
};
void fun() {
std::shared_ptr<A> pa(new A());
std::shared_ptr<B> pb(new B());
pa->ptr = pb;
pb->ptr = pa;
}
int main()
{
fun();
return 0;
}
运行结果我不敢看: (*/ω\*)
纳尼?A和B的析构函数都没有被调用,妥妥的内存泄漏了!
事后,据某位亲身经历这次事件的大牛回忆说:“喔,当时的内存布局是这个样子的!”
“对象A同时被pa和对象B中的ptr两个智能指针托管,所以引用计数为2;对象B同时被pb和对象
A中的ptr两个智能指针托管,所以引用计数也为2。那么当fun函数执行完,栈对象pb、pa依次开始执行这样的析构函数:”
// 大牛随手写的析构函数伪代码
// Copyright 2022 DaNiu. All rights reserved.
if (--ref_cnt == 0) delete obj;
“紧接着内存布局就变成了这样:”
“pa和pb已经销毁了,然而对象A和B,已经迷失在这浩瀚内存中,亘古难灭。。。吾称之为循环引用!”
路人震惊:“嘶!随着这样的迷失越来越多,这片天地再也无人可搞对象!!!”
゜゜(´O`) ゜゜。:“不要啊!我还没有搞过对象呢。”
......
庆幸的是,不就之后,有人就在shared_ptr出世的那方世界的一个不起眼的角落里,发现了一个小家伙,伴shared_ptr而生。
终章 weak_ptr
为了解决shared_ptr在在循环引用中存在的资源泄漏问题,weak_ptr在这种场景下应用而生,weak_ptr指向的智能指针对象,其引用计数不会加一,也就不会存在无法释放的问题了。
解决的方法就是,把A和B其中的一个ptr改成weak_ptr。