文章目录
前言
本文将会向你介绍C++智能指针,重在介绍智能指针的发展历程以及shared_ptr的模拟实现
为什么要有智能指针
先观察以下代码,并思考
如果p1的new抛异常、p2的new抛异常、div除法函数如果除0会怎样?
cpp
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void Func()
{
int* p1 = new int;
int* p2 = new int;
cout << div() << endl;
cout << 1 << endl;
delete p1;
delete p2;
}
int main()
{
try
{
Func();
}
catch (exception& e)
{
cout << e.what() << endl;
}
return 0;
}
之前可能只是new出来,忘记释放了,导致内存泄露,而异常出来后,情况就复杂了,异常会改变执行流,直接往catch处跳了,不会执行delete语句,会出现内存泄漏的问题,像new出错抛异常有时又是我们规避不了的,那该怎么办呢?
->采用RAII思想管理资源(把管理一份的资源的责任托管给了一个对象,不需要手动地去delete)
智能指针的使用及原理
RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。智能指针是RAII思想的一种实现
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:
不需要显式地释放资源。
采用这种方式,对象所需的资源在其生命期内始终保持有效。
使用RAII思想可以设计出SmartPtr类,这也是智能指针的雏形
cpp
template<class T>
class SmartPtr
{
public:
SmartPtr(T* ptr)
:_ptr(ptr)
{}
~SmartPtr()
{
cout << "delete: " << _ptr << endl;
delete _ptr;
}
T& operator*()
{
return *_ptr;
}
T& operator->()
{
return _ptr;
}
private:
T* _ptr;
};
当一个指针赋值给另一个指针时,我们需要的是浅拷贝,就是想让两个指针
指向同一块空间,但是指向了同一块空间就会出现析构两次的问题
auto_ptr
C++98就已经在库中实现了auto_ptr, auto_ptr的理念是既然有析构两次的风险,那么把A指针赋值给B指针后,A指针就销毁不能用了,即管理权转移,会把被拷贝对象的资源管理权转移给拷贝对象,这时我们再访问A指针,对于一些不懂auto_ptr的人,可是大灾难
cpp
template<class T>
class auto_ptr
{
auto_ptr(T* ptr)
:_ptr(ptr)
{}
~auto_ptr()
{
cout << "delete: " << _ptr << endl;
delete _ptr;
}
T& operator*()
{
return *_ptr;
}
T& operator->()
{
return _ptr;
}
//auto_ptr解决两次析构的理念
auto_ptr(auto_ptr<T>& ap)
:_ptr(ap._ptr)
{
ap._ptr = nullptr;
}
private:
T* _ptr;
};
unique_ptr
后来,为了填auto_ptr的坑,C++11推出了新的智能指针:unique_ptr Unique_ptr的理念就比较绝了,既然是拷贝、赋值导致的析构两次的问题 Unique_ptr直接就把拷贝和赋值给禁了 明显地还没到不死不休的情况,把拷贝和赋值给禁了就太绝了,因此auto_ptr 在很多公司中明令禁止使用
cpp
template<class T>
class unique_ptr
{
unique_ptr(T* ptr)
:_ptr(ptr)
{}
~unique_ptr()
{
cout << "delete: " << _ptr << endl;
delete _ptr;
}
T& operator*()
{
return *_ptr;
}
T& operator->()
{
return _ptr;
}
unique_ptr(unique_ptr<T>& up) = delete;
unique_ptr<T>& operator=(unique_ptr<T>& up) = delete;
private:
T* _ptr;
};
shared_ptr
再后来,C++11也就有了Shared_ptr,shared_ptr的理念是使用引用计数的思想,对析构作修改,如果有两个智能指针同时对一个资源的管理,此时引用计数为2,实际上每次析构的时候只会让引用计数--,实际只会释放一次,这样就很好地解决了赋值、拷贝析构两次的问题
shared_ptr需要重点掌握,因为面试中hr可能要求手撕
shared_ptr的重点在拷贝与赋值上,在前一代智能指针的基础上,增加了一个引用计数,引用计数资源也是由一个指针进行管理
赋值重载中我们需要注意,当 sp1=sp2,我们是需要将sp1管理资源的引用计数进行- -的,同时对sp2、sp4、sp5共同管理资源的引用计数++
还需要判断是否是自己给自己赋值,即sp1 = sp1,sp2 = sp5(本质上也是自己给自己赋值)
cpp
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
, _count(new int(1))
{}
~shared_ptr()
{
if (--(*_count) == 0)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
delete _count;
}
}
shared_ptr(const shared_ptr<T>& sp)
: _ptr(sp._ptr)
, _count(sp._count)
{
++(*_count);
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
//判断是否是给自己赋值
if (_ptr == sp._ptr)
{
return *this; //最好是返回左操作数
}
//减少赋值对象的引用计数
if (--(*_count) == 0)
{
delete _ptr;
delete _count;
}
_ptr = sp._ptr;
_count = sp._count;
//加加此时的引用计数
++(*_count);
return *this;
}
T& operator*()
{
return *_ptr;
}
T& operator->()
{
return _ptr;
}
int use_count() const
{
return *_count;
}
T* get() const
{
return *_ptr;
}
private:
T* _ptr;
int* _count; //引用计数
};
循环引用
尽管shared_ptr已经很完美了,但仍然是有缺陷的
cpp
struct ListNode
{
int _data;
shared_ptr<ListNode> prev;
shared_ptr<ListNode> next;
~ListNode() { cout << "~ListNode()" << endl; }
};
int main()
{
shared_ptr<ListNode> node1(new ListNode);
shared_ptr<ListNode> node2(new ListNode);
node1->next = node2;
//node2->prev = node1;
return 0;
}
如果屏蔽掉node2->prev = node1;node1->next = node2; 是不会构成循环引用的,此时node1,node2都可以正常释放
如果构成循环引用,node1与node2都得不到释放,从而内存泄漏
以下就是循环引用问题。这样双方都不会进行析构
当两份空间还没连接起来,只有node1与node2指向空间,引用计数都为1,当连接后,它们的引用计数都变为2了,当main函数调用完,node2先析构,此时引用计数变为1,node1再析构,引用计数也减为1,但是这两份空间不会释放,因为引用计数还没有减到0,那么什么时候才会释放,要node2的prev析构后,node1空间才会析构...如图所示
解决方法一:在使用智能指针的时候直接跳过循环引用这个坑
解决方法二:换成弱指针weak_ptr
weak_ptr
weak ptr不是RAII思想的智能指针,只是专门用来解决share_ptr的循环引用问题的,weak_ptr的拷贝与赋值是支持shared_ptr进行拷贝与赋值的
cpp
template<class T>
class weak_ptr
{
public:
weak_ptr()
:_ptr(nullptr)
{}
weak_ptr(const shared_ptr<T>& sp)
:_ptr(sp.get())
{}
weak_ptr<T>& operator=(const shared_ptr<T>& sp)
{
_ptr = sp.get();
return *this;
}
T& operator*()
{
return *_ptr;
}
T& operator->()
{
return _ptr;
}
private:
T* _ptr;
};
这样就解决了循环引用问题,node1与node2节点都进行析构了
小结
今日的分享就到这里了,重点掌握智能指针的发展历程中每个智能指针的缺陷是啥,以及需要掌握shared_ptr的手撕