目录
一、智能指针是什么
在c++中,动态内存的管理式通过一对运算符来完成的:new,在动态内存中为对象分配空间并返回一个指向该对象的指针,我们可以选择对对象进行初始化;delete,接受一个动态对象的指针,销毁该对象,并释放与之关联的内存。动态内存的使用很容易出现问题,因为确保在正确的时间释放内存是极其困难的。有时使用完对象后,忘记释放内存,造成内存泄漏的问题。
所谓的智能指针本质就是一个类模板,它可以创建任意的类型的指针对象,当智能指针对象使用完后,对象就会自动调用析构函数去释放该指针所指向的空间。
下面是智能指针的基本框架,所有的智能指针类模板中都需要包含一个指针对象,构造函数和析构函数。
二、为什么需要智能指针
异常的重新抛出的场景:
void File()
{
string filename;
cin >> filename;
FILE* fout = fopen(filename.c_str(), "r");
if (fout == nullptr) {
string errmsg = "打开文件失败:";
errmsg += filename;
errmsg += "->";
errmsg += strerror(errno);
Exception e(errno, errmsg);
throw e;
}
char ch;
while ((ch = fgetc(fout))!=EOF) {
cout << ch;
}
fclose(fout);
}
double Division(int a, int b)
{
if (b == 0)
{
string errmsg = "Division by zero condition!";
Exception e(100, errmsg);
throw e;
}
else
{
return ((double)a / (double)b);
}
}
void Func()
{
int* p = new int[100];
int len, time;
cin >> len >> time;
try {
cout << Division(len,time) << endl;
File();
}
catch (...)
{
//捕获之后,不是要处理异常,异常由最外层同一处理
//这里捕获异常只是为了处理内存泄漏的问题
delete[]p;
throw;
}
delete[]p;
}
int main()
{
try {
Func();
}
catch (const Exception& e) {
cout << e.what() << endl;
}
catch (...)
{
cout << "未知异常" << endl;
}
return 0;
}
在Func函数中,我们在堆上创建了开一个指针,为了防止函数抛出异常导致最后的析构函数不执行而产生野指针,我们使用了 异常的重新抛出策略。
但是,终究不是个好的方法,如果这类资源较多,那么我们需要大量的 异常重抛 ,而且就算程序不涉及程序处理,大量的堆上空间需要人工释放,容易造成疏漏,这一问题在工程中比较常见。
所以,这时候如果我们实用智能指针,就可以不用再操心内存是否会泄露的问题。
三、智能指针的使用和原理
3.1、RALL
- 不需要显式的释放资源
- 采用这种方式,对象所需的资源在其生命周期内始终保持有效.
我们可以借助RALL思想来写一个简单的 智能指针:
#include<iostream>
using namespace std;
template<class T>
class SmartPtr
{
public:
SmartPtr(T* ptr =nullptr)
:_ptr(ptr)
{}
~SmartPtr()
{
if (_ptr)delete _ptr;
cout<<"~SmartPtr"<<endl;
}
private:
T* _ptr;
};
int main()
{
int* a = new int(1);
SmartPtr<int> sp(a); //将a 指针委托给sp对象管理
SmartPtr<int>sp2(new int(2)); //直接船舰匿名对象给sp2管理
}
3.2 智能指针的原理
template<class T>
class SmartPtr
{
public:
SmartPtr(T* ptr = nullptr)
: _ptr(ptr)
{}
~SmartPtr()
{
if(_ptr)
delete _ptr;
}
T& operator*() {return *_ptr;}
T* operator->() {return _ptr;}
private:
T* _ptr;
};
struct Date
{
int _year;
int _month;
int _day;
};
int main()
{
SmartPtr<int> sp1(new int);
*sp1 = 10;
cout<<*sp1<<endl;
SmartPtr<int> sparray(new Date);
// 需要注意的是这里应该是sparray.operator->()->_year = 2018;
// 本来应该是sparray->->_year这里语法上为了可读性,省略了一个->
sparray->_year = 2018;
sparray->_month = 1;
sparray->_day = 1;
}
3.3、智能指针的分类
上面的SmartPtr 还不可以被称为智能指针,因为它还不具有指针的行为与性质。
指针可以解引用,也可以通过->去访问所指向的空间中的内容,因此智能指针还需要将 *,->重载。
除此之外,如果我们使用了 拷贝或者赋值操作,就会发生浅拷贝的问题,由于二者指向同一块空间,所以在析构的时候也会析构两次,造成错误。
所以,为了解决以上问题,C++提供了几种设计方案实现的智能指针。
C++中存在4种智能指针:auto_ptr,unquie_ptr,shared_ptr,weak_ptr,他们各有优缺点,以及对应的实用场景
3.3.1、auto_ptr
在C++98版本的库种,提供了 auto_ptr 的智能指针:
class Date
{
public:
Date()
:_year(0),_month(0),_day(0)
{}
~Date(){}
int _year;
int _month;
int _day;
};
int main()
{
auto_ptr<Date>ap(new Date);
//拷贝构造
auto_ptr<Date>copy(ap);
ap->_year = 2022;
}
我们发现报错了,发生了非法访问。
这就是auto_ptr 的弊病,当我们使用对象拷贝或者赋值之后,之前的那个对象就被置空(如下图)
在拷贝或者赋值的过程种,auto_ptr 会传递所有权,将资源全部从源指针转移给目标指针,源指针被置空。
虽然这种方法确实解决了 浅拷贝的问题,但是十分局限性也很大,这也就导致了,我们使用auto_ptr的时候要注意,不要对源指针进行访问或者操作。
由于C++98种提供的这个智能指针问题明显,所以在实际工作种哼多公司是明确规定了不能使用auto_ptr的。
auto_ptr具体实现:
template<class T>
class auto_ptr
{
public:
auto_ptr(T* ptr=nullptr)
:_ptr(ptr)
{}
auto_ptr(auto_ptr<T>& ap)
:_ptr(ap._ptr)
{
ap._ptr = nullptr; //管理权转移
}
auto_ptr<T>& operator = (auto_ptr<T>& ap)
{
if (this != *ap) {
delete _ptr;
_ptr = ap._ptr;
ap._ptr = nullptr;
}
return *this;
}
~SmartPtr()
{
if (_ptr)delete _ptr;
}
T& operator *()
{
return *_ptr;
}
T* operator ->()
{
return _ptr;
}
private:
T* _ptr;
};
3.3.2、unique_ptr
在C++11中,C++11y引入了unique_ptr.
unique_ptr的原理很简单,就是一个“得不到就毁掉”的理念,直接把拷贝和赋值禁止了。
对于用不上赋值拷贝的场景的时候,我们选择unique_ptr也是一个不错的选择。
template<class T>
class unique_ptr
{
public:
unique_ptr(T* ptr = nullptr)
:_ptr(ptr)
{}
//防拷贝
unique_ptr(unique_ptr<T>& ap) = delete;
unique_ptr<T>& operator = (unique_ptr<T>& ap) = delete;
~SmartPtr()
{
if (_ptr)delete _ptr;
}
T& operator *()
{
return *_ptr;
}
T* operator ->()
{
return _ptr;
}
private:
T* _ptr;
};
3.3.3、shared_ptr
C++中还提供了shared_ptr。
shared_ptr 是当前最为广泛使用的智能指针,它可以安全的提供拷贝操作。
shared_ptr的原理:
我们可以对一个资源添加一个计数器,让所有管理该资源的智能共用这个计数器,倘若发生拷贝,计数器加一,倘若有析构发生, 计数器减一,当计数器等于0的时候,就把对象析构掉。
- shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享。
- 在对象被销毁时(就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。
- 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;
- 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。
shared_ptr 的模拟实现
template<class T>
class shared_ptr
{
public:
shared_ptr(T*ptr =nullptr)
:_ptr(ptr),_pcount(new int(1))
{}
//拷贝构造
shared_ptr(const T& sp)
_ptr(sp._ptr),_pcount(sp._pcount)
{
++(*_pcount);
}
//赋值拷贝
shared_ptr<T>& operator = (shared_ptr<T>& sp)
{
if (_ptr != sp._ptr) {
if (--(*_pcount) == 0){
delete _pcount;
delete _ptr;
}
_ptr = sp._ptr;
_pcount = sp._pcount;
++(*_pcount);
}
return *this;
}
T& operator *()
{
return *_ptr;
}
T* operator ->()
{
return _ptr;
}
~shared_ptr()
{
if (--(*_pcount) == 0 && _ptr) {
delete _pcount;
delete _ptr;
}
}
private:
T* _ptr;
int* _pcount;
};
我们把 这个 计数器 建在堆上,这样就可以保证各个对象之间保持同步同时计数正确。
拷贝构造
赋值拷贝
赋值拷贝需要注意两点:
- 在被赋值之前的对象需要将自己析构,也就是放弃当前资源的管理权,然后再去被赋值,取得新的管理权。
- 避免自己对自己赋值,按照1中的机制,如果自己对自己赋值,会造成无谓的操作,或者误析构资源。
另一种写法:
shared_ptr<T>& operator=(shared_ptr<T> sp)
{
swap(_ptr, sp._ptr);
swap(_pcount, sp._pcount);
return *this;
}
但是,此时我们的shared_ptr 还面临着 线程安全的问题。
这里我们需要保障的是对于 计数器的 ++ 和 – 造成的线程不安全。对于资源的线程安全问题,这不是智能指针保证的部分
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr = nullptr)
:_ptr(ptr), _pcount(new int(1)), _pmtx(new mutex)
{}
void add_ref()
{
_pmtx->lock();
++(*_pcount);
_pmtx->unlock();
}
void release_ref()
{
bool flag = false;
_pmtx->lock();
if (--(*_pcount) == 0 && _ptr) {
delete _pcount;
delete _ptr;
flag = true;
cout << "释放资源:" << _ptr << endl;
}
_pmtx->unlock();
if (flag)delete _pmtx;
}
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr),_pcount(sp._pcount),_pmtx(sp._pmtx)
{
add_ref();
}
shared_ptr<T>& operator = (const shared_ptr<T>& sp)
{
if (_ptr != sp._ptr) {
if (--(*_pcount) == 0){
delete _pcount;
delete _ptr;
}
_ptr = sp._ptr;
_pcount = sp._pcount;
add_ref();
}
return *this;
}
T& operator *()
{
return *_ptr;
}
T* operator ->()
{
return _ptr;
}
T* get()
{
return _ptr;
}
int use_count()
{
return *_pcount;
}
~shared_ptr()
{
release_ref();
}
private:
T* _ptr;
int* _pcount;
mutex* _pmtx;
};
定制删除器
不管是我们自己实现的shared_ptr还是库中的shared_ptr,我们在析构的时候默认都是 delete _ptr,如果我们托管的类型是 new T[] ,或者 malloc出来的话,就导致类型不是匹配的,无法析构。
为此,shared_ptr提供了 定制删除器,我们可以在构造的时候作为参数传入。如果我们不传参,就默认使用delete
这个自定义删除器可以是函数指针,仿函数,lamber,包装器。
仿函数的删除器
shared_ptr中的析构函数会去调用DelArry仿函数去释放动态数组。
3.2.4、weak_ptr
虽然 shared_ptr 确实已经是一个不错的设计了,但是没有“十全十美”的东西,在一些特别的场景之下shared_ptr 也无能为力:
shared_ptr 的循环引用
我们看下面的场景,我们运行发现,两个节点n1.n2 都没有析构。
在出了作用域之后,首先把 n1,n2 两个对象析构,此时两边计数器均减为1,那么左边节点资源什么时候析构呢, 当n2->prev析构,也就是当右边节点资源析构,那么右边节点资源什么时候析构呢,当n1->_next析构,也就是当左边节点资源析构…我们发现,此时形成了一个类似于“死锁”的情况。
此时我们就要使用 weak_ptr 来解决 循环引用
weak_ptr是一个弱引用,它是为了配合shared_ptr而引入的一种智能指针,是为了解决循环引用而生的,为什么这么说呢,我们可以看看它的构造函数:
我们只能使用 wek_ptr或者 shared_ptr 去初始化它。
我们在会产生循环引用的位置,把shared_ptr换成weak_ptr。 weak_ptr 不是一个RALL智能指针,它不参与资源的管理,他是专门用来解决引用计数的,我们可以使用一个shared_ptr 来初始化一个weak_ptr,但是weak_ptr 不增加引用计数,不参与管理,但是也像指针一样访问修改资源。
实现一个weak_ptr
template<class T>
class weak_ptr
{
public:
weak_ptr()
:_ptr(nullptr)
{}
weak_ptr(shared_ptr<T>& sp)
:_ptr(sp.get()),_pcount(sp.use_count())
{}
weak_ptr(weak_ptr<T>& sp)
:_ptr(sp._ptr), _pcount(sp._pcount)
{}
weak_ptr& operator = (shared_ptr<T>& sp)
{
_ptr = sp.get();
_pcount = sp.use_count();
return *this;
}
weak_ptr& operator = (weak_ptr<T>& sp)
{
_ptr = sp._ptr;
_pcount = sp._pcount;
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
int use_count()
{
return *_pcount;
}
private:
T* _ptr;
int* _pcount;
};