目录
前言
为了解决抛异常所造成的内存泄漏等问题~秒懂C++之异常-CSDN博客~我们来学习智能指针的相关用法~
智能指针的使用及原理
RAII
RAII ( Resource Acquisition Is Initialization )是一种 利用对象生命周期来控制程序资源 (如内存、文件句柄、网络连接、互斥量等等)的简单技术。
在对象构造时获取资源 ,接着控制对资源的访问使之在对象的生命周期内始终保持有效, 最后在 对象析构的时候释放资源 。借此,我们实际上把管理一份资源的责任托管给了一个对象。
cpp
template <class T>
class SmartPtr
{
public:
SmartPtr(T* ptr = nullptr)
: _ptr(ptr)
{}
~SmartPtr()
{
if (_ptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
}
T& operator*() {return *_ptr;}
T* operator->() {return _ptr;}
private:
T* _ptr;
};
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void Func()
{
SmartPtr<int> sp1(new int);
SmartPtr<int> sp2(new int);
cout << div() << endl;
}
int main()
{
try {
Func();
}
catch (const exception& e)
{
cout << e.what() << endl;
}
return 0;
}
可以看到即使触发了异常最后也可以清理资源~避免内存泄漏~
RAII也可以和正常指针一样解引用与->访问其内容~
RAII弊端
智能指针的拷贝都是默认生成的,而默认生成的拷贝构造都为浅拷贝这就代表拷贝后会有两个指针指向同一处空间,最终析构两次造成报错~
其实多个指针指向同一处空间进行访问并没什么大碍,迭代器也这样。不过智能指针除了访问还有释放的职能,这才导致出现问题~
std::auto_ptr
这个稍微提一下就好了,因为它是一个及其失败的设计!
过于浮夸了,直接把人家资源给掠夺了~
std::unique_ptr
unique_ptr 的实现原理:简单粗暴的防拷贝,下面简化模拟实现了一份 UniquePtr 来了解它的原
理
cpp
namespace lj
{
template <class T>
class unique_ptr
{
public:
unique_ptr(T* ptr = nullptr)
: _ptr(ptr)
{}
~unique_ptr()
{
if (_ptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
}
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
unique_ptr(const unique_ptr<T>& sp) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& sp) = delete;
private:
T* _ptr;
};
}
就是利用关键字delete防止生成默认拷贝与默认赋值,不过个人感觉实用性不大~
std::shared_ptr
shared_ptr 的原理:是通过引用计数的方式来实现多个 shared_ptr 对象之间共享资源 。
- shared_ptr 在其内部, 给每个资源都维护了着一份计数,用来记录该份资源被几个对象共 享
- 在 对象被销毁时 ( 也就是析构函数调用 ) ,就说明自己不使用该资源了,对象的引用计数减一
- 如果引用计数是 0 ,就说明自己是最后一个使用该资源的对象, 必须释放该资源
- 如果不是 0 ,就说明除了自己还有其他对象在使用该份资源, 不能释放该资源 ,否则其他对象就成野指针了
shared_ptr中如何合理利用引用计数是一个难点~
普通int count计数肯定是不行的,那静态成员呢?
cpp
namespace lj
{
template<class T>
class shared_ptr
{
public:
// RAII
shared_ptr(T* ptr)
:_ptr(ptr)
{
_count = 1;
}
// sp2(sp1)
shared_ptr(const shared_ptr<T>& sp)
{
_ptr = sp._ptr;
++_count;
}
~shared_ptr()
{
if (--_count == 0)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
}
int use_count()
{
return _count;
}
// 像指针一样
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
static int _count;
};
template<class T>
int shared_ptr<T>::_count = 0;
}
一开始还好,但当sp3被构建出来后会把count重新初始化为1,因为静态成员是属于类中的所有对象,这样一来就打乱了原本的计数。本来是想要释放两处空间的结果只释放了sp3的,由此看来静态也不合适~
我们想要的是给每一个资源都配上一个引用计数,而非全部资源共用一个!
cpp
namespace lj
{
template<class T>
class shared_ptr
{
public:
// RAII
shared_ptr(T* ptr)
:_ptr(ptr)
,_pcount(new int(1))
{
}
// sp2(sp1)
shared_ptr(const shared_ptr<T>& sp)
{
_ptr = sp._ptr;
_pcount = sp._pcount;
++*_pcount;
}
~shared_ptr()
{
if (--(*_pcount) == 0)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
delete _pcount;
}
}
int use_count()
{
return *_pcount;
}
// 像指针一样
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
T* _pcount;
};
}
最终给出的解决方案是在每一个对象中存一个指向计数的指针~
除了拷贝构造,赋值拷贝也要给上~
cpp
// sp1 = sp4
// sp4 = sp4;
// sp1 = sp2;
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
//if (this != &sp)
if (_ptr != sp._ptr)//防止自身赋值,也防止与拷贝自身的赋值,即指向同一资源的不赋值
{
release();
_ptr = sp._ptr;
_pcount = sp._pcount;
// 拷贝时++计数
++(*_pcount);
}
return *this;
}
void release()
{
// 说明最后一个管理对象析构了,可以释放资源了
if (--(*_pcount) == 0)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
delete _pcount;
}
}
shared_ptr弊端
cpp
struct ListNode
{
int _val;
/*struct ListNode* _next;
struct ListNode* _prev;*/
//这里也要改为智能指针
//n1->_next为内置类型,n2为自定义类型
lj::shared_ptr<ListNode> _next;
lj::shared_ptr<ListNode> _prev;
ListNode(int val = 0)
:_val(val)
,_next(nullptr)
,_prev(nullptr)
{}
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
int main()
{
/*ListNode* n1 = new ListNode(10);
ListNode* n2 = new ListNode(10);*/
//库里的std::shared不支持隐式类型转化
//std::shared_ptr<ListNode> n1 = new ListNode(10);
lj::shared_ptr<ListNode> n1(new ListNode(10));
lj::shared_ptr<ListNode> n2(new ListNode(20));
n1->_next = n2;
n2->_prev = n1;
return 0;
}
当我们把智能指针应用到双链表时会出现循环引用的弊端~
完全就是死循环了,永远释放不了~
ps:_next作为节点成员,节点什么时候释放,它就什么时候析构~
为了解决这种特定场景下的问题,我们又引入了新的智能指针,weak_ptr
std::weak_ptr
weak指针其实也就是在shared指针的基础上放弃了对引用计数的职能~
cpp
struct ListNode
{
int _val;
lj::weak_ptr<ListNode> _next;
lj::weak_ptr<ListNode> _prev;
ListNode(int val = 0)
:_val(val)
//weak指针无法用普通指针构造
/*, _next(nullptr)
, _prev(nullptr)*/
{}
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
int main()
{
lj::shared_ptr<ListNode> n1(new ListNode(10));
lj::shared_ptr<ListNode> n2(new ListNode(20));
cout << n1.use_count() << endl;
cout << n2.use_count() << endl;
n1->_next = n2;
n2->_prev = n1;
cout << n1.use_count() << endl;
cout << n2.use_count() << endl;
return 0;
}
cpp
namespace lj
{
template<class T>
class shared_ptr
{
public:
// RAII
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
,_pcount(new int(1))
{
}
// sp2(sp1)
shared_ptr(const shared_ptr<T>& sp)
{
_ptr = sp._ptr;
_pcount = sp._pcount;
++*_pcount;
}
// sp1 = sp4
// sp4 = sp4;
// sp1 = sp2;
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
//if (this != &sp)
if (_ptr != sp._ptr)//防止自身赋值,也防止与拷贝自身的赋值,即指向同一资源的不赋值
{
release();
_ptr = sp._ptr;
_pcount = sp._pcount;
// 拷贝时++计数
++(*_pcount);
}
return *this;
}
void release()
{
// 说明最后一个管理对象析构了,可以释放资源了
if (--(*_pcount) == 0)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
delete _pcount;
}
}
~shared_ptr()
{
release();
}
int use_count()
{
return *_pcount;
}
// 像指针一样
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
//方便weak指针拿到shared指针
T* get() const
{
return _ptr;
}
private:
T* _ptr;
int* _pcount;
};
}
namespace lj
{
// 不支持RAII,不参与资源管理
template<class T>
class weak_ptr
{
public:
// RAII
// 取消了引用计数
weak_ptr()
:_ptr(nullptr)
{}
//用share指针作拷贝
weak_ptr(const shared_ptr<T>& sp)
{
_ptr = sp.get();
}
//用share指针作赋值重载
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;
};
}
我们把互相牵制的_next与_prev指针变为weak指针,使得其无法进行引用计数,这样就可以把资源成功释放~
最终在weak指针的作用下解决了循环引用的问题~
扩展(删除器)
在了解删除器之前我们需要认识一点:[ ] 是要匹配使用的,如果new用了[ ] 那么delete也要使用[ ] ,否则就会报错~
而且也不是所有被智能指针修饰的资源都是delete,例如打开文件的时候我们反而是要用close,
所以一般是搞关于删除的仿函数。
cpp
template<class T>
struct DeleteArray
{
void operator()(T* ptr)
{
cout << "delete[]" << ptr << endl;
delete[] ptr;
}
};
int main()
{
std::shared_ptr<ListNode> p1(new ListNode(10));
//C11下作出的改进优化
//std::shared_ptr<ListNode[]> p2(new ListNode[10]);
std::shared_ptr<ListNode> p2(new ListNode[10], DeleteArray<ListNode>());
return 0;
}
不过比起仿函数,我们前面所学的lambda表达式会更好用一点~
cpp
int main()
{
std::shared_ptr<ListNode> p1(new ListNode(10));
//C11下作出的改进优化
//std::shared_ptr<ListNode[]> p2(new ListNode[10]);
//std::shared_ptr<ListNode> p2(new ListNode[10], DeleteArray<ListNode>());
std::shared_ptr<FILE> p3(fopen("Test.cpp", "r"), [](FILE* ptr) {fclose(ptr); });
std::shared_ptr<ListNode> p2(new ListNode[10], [](ListNode* ptr){ delete[] ptr; });
return 0;
}
认识到删除器后,我们再来对自己实现的shared指针进行改进调整~
所以我们再引入前面所学的包装器function,解决这个参数问题~
cpp
#include<functional>
namespace lj
{
template<class T>
class shared_ptr
{
public:
// RAII
template<class D>
shared_ptr(T* ptr, D del)
:_ptr(ptr)
, _pcount(new int(1))
, _del(del)
{
}
// sp2(sp1)
shared_ptr(const shared_ptr<T>& sp)
{
_ptr = sp._ptr;
_pcount = sp._pcount;
++*_pcount;
}
// sp1 = sp4
// sp4 = sp4;
// sp1 = sp2;
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
//if (this != &sp)
if (_ptr != sp._ptr)//防止自身赋值,也防止与拷贝自身的赋值,即指向同一资源的不赋值
{
release();
_ptr = sp._ptr;
_pcount = sp._pcount;
// 拷贝时++计数
++(*_pcount);
}
return *this;
}
void release()
{
// 说明最后一个管理对象析构了,可以释放资源了
if (--(*_pcount) == 0)
{
cout << "delete:" << _ptr << endl;
//delete _ptr;
_del(_ptr);
delete _pcount;
}
}
~shared_ptr()
{
release();
}
int use_count()
{
return *_pcount;
}
// 像指针一样
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
//方便weak指针拿到shared指针
T* get() const
{
return _ptr;
}
private:
T* _ptr;
int* _pcount;
function<void(T*)> _del = [](T* ptr) {delete ptr; };
};
}