一、智能指针的使用及原理
1. 1、RAII
RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:
1.不需要显式地释放资源。
2.采用这种方式,对象所需的资源在其生命期内始终保持有效。
下面介绍3种智能指针,智能指针最重要的就是拷贝构造。
1.2 auto_ptr
C++98中推出了一个auto_ptr
当初设计的不是很好,不建议使用,为什么?
因为拷贝时,管理权转移,被拷贝的对象置为空,要拷贝的对象接替了被拷贝的对象的空间
ap2拷贝ap1,ap1被置为空,ap2接受ap1原来的空间
核心思想代码就是
auto_ptr(auto_ptr<T>& sp)
:_ptr(sp._ptr)
{
// 管理权转移
sp._ptr = nullptr;
}
1.3 unique_ptr
C++11后出现了unique_ptr
unique_ptr的实现原理:简单粗暴的防止拷贝。因此直接把拷贝与赋值禁掉了
unique_ptr构造的常用使用写法:
struct Date
{
int _year;
int _month;
int _day;
Date(int year = 1, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
{}
~Date()
{
cout << "~Date()" << endl;
}
};
template<class T>
class DeleteArray
{
public:
void operator()(T* ptr)
{
delete[] ptr;
}
};
class Fclose
{
public:
void operator()(FILE* ptr)
{
cout << "fclose:" << ptr << endl;
fclose(ptr);
}
};
智能指针对空间的托管
int main()
{
// 定制删除器
unique_ptr<Date, DeleteArray<Date>> up2(new Date[5]);
有特化版本
//unique_ptr<Date[]> up2(new Date[5]);
unique_ptr<FILE, Fclose> up3(fopen("test.cpp", "r"));
return 0;
}
其余的接口可以看文档是怎么写的
1.4 shared_ptr
C++11推出了shared_ptr,另外shared_ptr可以进行拷贝
shared_ptr允许多个智能指针可以指向同一块资源,并且能够保证共享的资源只会被释放一次,因此是程序不会崩溃掉。
shared_ptr的原理:
是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。例如:教室里最后一个人关灯。
1. shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享。
2. 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。
3. 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;
4. 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。
为什么智能指针要推出拷贝构造这个功能?就比如下面的容器存储的类型是shared_ptr就不得不进行拷贝了。
vector<shared_ptr<Date>> v;
shared_ptr构造的常用使用:
智能指针对空间的托管:
shared_ptr<Date> sp1(new Date);
//虽然在文档没表现但是也可以这么玩
shared_ptr<Date[]> sp4(new Date[5]);
shared_ptr<FILE> sp5(fopen("test.cpp", "r"), Fclose());
make_shared则是C++11引入的一个模板函数,用于更高效地创建shared_ptr实例
std::make_shared:
用于创建std::shared_ptr的实例,并自动管理其指向的对象的生命周期。
在一次内存分配中同时创建对象和控制块,减少内存碎片和分配开销。
使用:
shared_ptr<Date> sp6 = make_shared<Date>(2024, 8, 5);
模拟shared_ptr:
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr = nullptr)
: _ptr(ptr)
, _pcount(new int(1))
{}
template<class D>
shared_ptr(T* ptr, D del)
: _ptr(ptr)
, _pcount(new int(1))
, _del(del)
{}
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _pcount(sp._pcount)
{
++(*_pcount);
}
void release()
{
if (--(*_pcount) == 0)
{
//进行回调
_del(_ptr);
delete _pcount;
_ptr = nullptr;
_pcount = nullptr;
}
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (_ptr != sp._ptr)
{
release();
_ptr = sp._ptr;
_pcount = sp._pcount;
++(*_pcount);
}
return *this;
}
~shared_ptr()
{
release();
}
T* get() const
{
return _ptr;
}
int use_count() const
{
return *_pcount;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
int* _pcount;
// 不要思维固定用D _del,使用包装器也行
function<void(T* ptr)> _del = [](T* ptr) {delete ptr; };
};
shared_ptr的循环引用:
假设我们要使用定义一个双向链表,如果我们想要让创建出来的链表的节点都定义成shared_ptr智能指针,那么也需要将节点内的_pre和_next的类型都转换成shared_ptr的类型。如果定义成普通指针,那么就不能赋值给shared_ptr的智能指针。
struct ListNode
{
int _data;
//ListNode* _next;
//ListNode* _prev;
shared_ptr<ListNode> _next;
shared_ptr<ListNode> _prev;
};
int main()
{
// 循环引用 -- 内存泄露
shared_ptr<ListNode> n1(new ListNode);
shared_ptr<ListNode> n2(new ListNode);
n1->_next = n2;
n2->_prev = n1;
return 0;
}
为什么会内存泄漏 ?
node1和node2析构后,两个节点引用计数减到1(node1为左节点,node2为右节点)导致内存无法释放。
循环引用分析:
1、右节点什么时候释放呢,左节点_next管着,左节点_next析构,右节点就释放了
2、左节点_next什么时候析构呢,左边节点释放,左节点_next就析构
3、左节点什么时候释放,右节点的_prev管着,右节点的_prev析构了,左边的节点就释放了
4、右节点的_prev什么时候析构呢,右节点释放,右节点_prev就析构了
最后导致左节点与右节点无法释放,发生内存泄漏
那么如何解决?
解决方案:在引用计数的场景下,把节点中的_prev和_next改成weak_ptr就可以了
weak_ptr:弱指针不支持管理资源,不支持RAII
原理:
node1->_next = node2;node2->_prev = node1;时weak_ptr的_next和_prev不会增加node1和node2的引用计数。
模拟一个简单的weak_ptr,这里的weak_ptr就是把shared_ptr的指针拿过来,没有发生计数。
严格来说是要计数的,来判断weak_ptr是否过期。
template<class T>
class weak_ptr
{
public:
weak_ptr()
{}
// 拷贝构造
weak_ptr(const shared_ptr<T>& sp)
:_ptr(sp.get())
{}
weak_ptr<T>& operator=(const shared_ptr<T>& sp)
{
_ptr = sp.get();
return *this;
}
private:
T* _ptr = nullptr;
};