自学内容网 自学内容网

【C++11】智能指针

智能指针在异常出现之后就大放异彩,避免了执行流乱跳而造成一些资源泄露。

为什么需要智能指针:

当连续的抛异常(非智能指针解决):

观察下面代码的问题:

int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
{
throw invalid_argument("除0错误");
}
return a / b;
}
void Func()
{
// 1、如果p1这里new 抛异常会如何?
// 2、如果p2这里new 抛异常会如何?
// 3、如果div调用这里又会抛异常会如何?
int* p1 = new int;
int* p2 = new int;
cout << div() << endl;
delete p1;
delete p2;
}
int main()
{
try
{
Func();
}
catch (exception& e)
{
cout << e.what() << endl;
}
return 0;
}

在func中,我们可能会有3个地方会抛异常

  1. 当p1抛异常时:我们不需要进行管理,因为资源就没有申请下来,不需要释放
  2. 当p2抛异常时:由于会直接跳到最外层的catch中,所以导致不会释放p1,所以我们就要先释放p1再重新抛出。
void Func()
{
// 2、如果p2这里new 抛异常会如何?
int* p1 = new int;
int* p2 = nullptr;
try 
{
p2 = new int;
}
catch (...)
{
delete p1;
throw;
}
cout << div() << endl;
delete p1;
delete p2;
}
  1. 当div抛异常:我们依然需要进行捕获,将p1,p2进行delete在重新抛出。
void Func()
{
// 3、如果div调用这里又会抛异常会如何?
int* p1 = new int;
int* p2 = nullptr;
try
{
p2 = new int;
}
catch (...)
{
delete p1;
throw;
}
try
{
cout << div() << endl;
}
catch (...)
{
delete p1;
delete p2;
throw;
}
delete p1;
delete p2;
}

虽然这样的方式可以解决问题,但是真的是一点也不优雅!
我们先快速的见一见智能指针,并使用我们自己写的智能指针去解决上述问题。

当连续的抛异常(智能指针解决)

我们先写出一个最简单的智能指针:

template<class T>
class smart_ptr
{
public:
smart_ptr(T* ptr)
: _ptr(ptr)
{}
~smart_ptr()
{
if (_ptr)
{
delete _ptr;
}
}

private:
T* _ptr;
};

没错,十几行即可搞定!

func函数也变的更加优雅。

void Func()
{
smart_ptr<int> sp1 = new int;
smart_ptr<int> sp2 = new int;

cout << div() << endl;
}

智能指针的原理:

RAII:

我们的智能指针是站在RAII思想上的产物。

RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。

这种做法有两大好处:

  • 不需要显式地释放资源。
  • 采用这种方式,对象所需的资源在其生命期内始终保持有效。

然而真正的智能指针不仅仅需要自动析构,还要有与指针一样的行为

指针的行为:

我们在模拟各种STL容器的迭代器时就已经接触过模拟指针行为,无非就是进行操作符重载进而得到期望的行为。

指针的拷贝:

而最繁琐的一步就是当属拷贝,我们知道普通指针有时是需要进行拷贝的,但是这里的拷贝我们是浅拷贝,并非是深拷贝,如果是深拷贝
那么这两个指针都会各自指向一个对象,并非同一个对象,与期望不符。

但是浅拷贝又会引发另一个问题,会导致同一块内存空间释放两次导致err,这里在实现智能指针会有涉及。

那么就有存在一个问题:为什么我们模拟STL迭代器时没有这种释放两次的err发生?明明都是结构那么相似,都是带有一个指针
因为迭代器是进行包装指针的,并不需要释放指针,释放指针指向是容器应该做的,当我们的迭代器销毁时(栈上的),只会销毁迭代器对象,并不会连同他的成员变量指针的指向一同销毁。

而智能指针则是需要管理释放,但不能单纯浅拷贝。

智能指针的模拟:

注意:此处实现的都是极简版本。

auto_ptr:

实际上在C++98已经存在了智能指针,但是很辣鸡…

我们已经说过,单纯的浅拷贝会引发问题。
下段代码是还没实现拷贝的智能指针。

template<class T>
class auto_ptr
{
public:
auto_ptr(T* ptr)
: _ptr(ptr)
{}
~auto_ptr()
{
cout << "delete: " << _ptr << endl;
delete _ptr;
}

T& operator*()
{
return *_ptr;
}

T* operator&()
{
return _ptr;
}

T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};

可以看到,当进行了拷贝构造就会引发释放两次的问题
在这里插入图片描述

他实现了RAII,也实现了模拟指针的行为,但是他的解决拷贝问题的方法是进行管理权转移。

意思就是说既然两个指针同时管理一个会引发问题,那么我拷贝之后就让拷贝的左边进行管理,造成右边的悬空指针。

auto_ptr(auto_ptr<T>& aptr)
{
_ptr = aptr._ptr;
aptr._ptr = nullptr;
}

这样虽然某种意义解决了拷贝问题,但是让不了解底层的人使用是非常坑的。所以不要使用!

余下的3个智能指针是C++11推出的,非常的棒。

unique_ptr:

顾名思义,独一无二的指针,也就是只能有一个人使用,也就是只有一个人有管理权,也就是说不允许进行拷贝!

那我们有什么办法可以禁止拷贝?

方案一:
声明拷贝构造但是不实现,这样虽然我们没有实现拷贝构造,但是外部可能会实现,所以私有即可。

private:
unique_ptr(const unique_ptr<T>& uptr);

方案二:
直接使用C++11更新的delete,直接就可以删除,不管声明为什么访问限定符都可。

unique_ptr(const unique_ptr<T>& uptr) = delete;

完整代码:

template<class T>
class unique_ptr
{
public:
unique_ptr(T* ptr)
: _ptr(ptr)
{}
~unique_ptr()
{
std::cout << "delete: " << _ptr << std::endl;
delete _ptr;
}

T& operator*()
{
return *_ptr;
}

T* operator&()
{
return _ptr;
}
T* operator->()
{
return _ptr;
}
//unique_ptr(const unique_ptr<T>& uptr) = delete;
private:
unique_ptr(const unique_ptr<T>& uptr);
private:
T* _ptr;
};

shared_ptr:

无线程安全:

接下来就是最繁琐的shared_ptr了,他采取了引用计数的方法去解决,引用计数大于1时析构 --计数即可,等于时析构才是真的析构。

可是这样我们就面临一个困境,这个计数器是使用什么类型的呢?他应该存在什么区域?

方案一
当我们的计数器声明为普通成员变量时:
这样每个对象都有一个独立的计数器,很难进行管理起来,所以方案一否决。
方案二
当我们的计数器声明为静态成员变量时:
虽然看似解决了方案一的不足,

下段代码是没有问题的

shared_ptr<int> sptr1 = new int;
shared_ptr<int> sptr2(sptr1);

创建sptr1时静态计数器为1,拷贝后计数器变为2。
析构时先sptr2,让计数器–,再析构sptr1,由于为计数器1了,直接正常析构。
但是却解决不了这样的问题:

shared_ptr<int> sptr1 = new int;
shared_ptr<int> sptr2(sptr1);

shared_ptr<int> sptr3 = new int;

sptr3与以上两个智能指针指向的不是同一个区域,但却共享引用计数,所以这种情况下静态的也不能胜任需求。

方案三:
在堆中new一个计数器,对于指向同一快区域的智能指针使用同一个地址的计数器。

因为有了计数器,所以在构造与析构函数需要改变一下,另外也要添加拷贝与赋值重载。

template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr)
: _ptr(ptr)
, _pcount(new int(1))
{}
~shared_ptr()
{
if ((*_pcount)-- == 1)
{
std::cout << "delete: " << _ptr << std::endl;
delete _ptr;
delete _pcount;
}
}
T& operator*()
{
return *_ptr;
}

T* operator&()
{
return _ptr;
}
shared_ptr(const shared_ptr<T>& sptr)
{
_ptr = sptr._ptr;
_pcount = sptr._pcount;
(*_pcount)++;
}

shared_ptr<T>& operator=(const shared_ptr<T>& sptr)
{
if (_pcount != sptr._pcount 
&& (*_pcount)-- == 1)
{
std::cout << "delete: " << _ptr << std::endl;
delete _ptr;
delete _pcount;
}
return *this;
}
private:
T* _ptr;
int* _pcount;
};

对于拷贝来说,注意先将pcount赋值过去在++,
而对于赋值,我们可以进行一下简单的判断是否为自赋值与引用计数是否符合析构要求即可。

这样一个单线程的极简版就完成了

注意:关于为什么operator=(const Object& obj)的返回值为什么是&而不是const &,博主之前没怎么在意过这个细节,但是这次模拟实现时想到了,我起初是觉得带const最好,但是后来问了很多人与AI得到的结论都是最好不带const。

这更像是一种约定成俗的约定,虽然可以使用const返回,但是会造成一些意想不到的错误

  1. 对于链式赋值来说,我们通常希望返回的是一个可修改的对象。
  2. 对于一些特殊情况,我们有时operator=(传递的不可以是const &),需要对传递进来的值进行修改,若是返回const&,对于链式赋值A = B = C,编译器会先进行B = C的赋值,返回B的const &,导致无法继续赋值,连续赋值就不能成立了。

线程安全:

我们进行一下加锁即可,也并不是很难。

要加锁的地方我们有变量的++/–,另外要注意下段代码,在多线程是有可能发生双重释放,因为在*_pcount为1时,因为他不是原子的,导致会有多个线程同时进入,所以也要加锁。在这里插入图片描述

但为了方便加锁,我们可以将析构函数中的重要部分抽出来专门设计一个函数提高复用率与便利。
计数器++也可以专门抽出来。

void Release()
{
_pmtx->lock();
bool flag = false;
if ((*_pcount)-- == 1)
{
flag = true;
std::cout << "delete: " << _ptr << std::endl;
delete _ptr;
delete _pcount;
}
_pmtx->unlock();
if (flag) delete _pmtx;
}

void CountAdd()
{
_pmtx->lock();
(*_pcount)++;
_pmtx->unlock();
}

完整代码:

template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr = nullptr)
: _ptr(ptr)
, _pcount(new int(1))
, _pmtx(new std::mutex)
{}
shared_ptr(shared_ptr<T>& sptr)
{
_ptr = sptr._ptr;
_pcount = sptr._pcount;
_pmtx = sptr._pmtx;
CountAdd();
}
shared_ptr<T>& operator=(const shared_ptr<T>& sptr)
{
if (_pcount != sptr._pcount)
{
Release();

_ptr = sptr._ptr;
_pcount = sptr._pcount;
_pmtx = sptr._pmtx;
CountAdd();
}
return *this;
}
~shared_ptr()
{
Release();
}
void Release()
{
_pmtx->lock();
bool flag = false;
if ((*_pcount)-- == 1)
{
flag = true;
std::cout << "delete: " << _ptr << std::endl;
delete _ptr;
delete _pcount;
}
_pmtx->unlock();
if (flag) delete _pmtx;
}

void CountAdd()
{
_pmtx->lock();
(*_pcount)++;
_pmtx->unlock();
}

T& operator*()
{
return *_ptr;
}

T* operator&()
{
return _ptr;
}

T* GetPtr()
{
return _ptr;
}
int GetCount()
{
return *_pcount;
}
private:
T* _ptr;
int* _pcount;
std::mutex* _pmtx;
};

weak_ptr:

但是我们的shared_ptr其实还有一个大问题。
也就是循环引用的问题:

我们先来看现象:
运行以下代码时会发生以下异样

struct ListNode
{
shared_ptr<ListNode> _prev;
shared_ptr<ListNode> _next;
int val;
~ListNode()
{
std::cout << "~ListNode()" << std::endl;
}
};

void test_shared()
{
shared_ptr<ListNode> n1 = new ListNode;
shared_ptr<ListNode> n2 = new ListNode;

n1->_next = n2;
n2->_prev = n1;
}

在这里插入图片描述
仅仅只打印了这两句话。
这两句话是怎么来的呢?
为什么没有调用ListNode的析构?

当我们将其中节点链接的代码屏蔽掉一句后又正常释放了
在这里插入图片描述
于是我们就需要看一看对象模型是什么样子的了。
在这里插入图片描述

对于n1,我们构造时会先在栈上申请一个n1大小的对象,在去new一个ListNode,
这里我们会调用ListNode的构造函数,由于我们没有显示写出构造函数,于是会调用默认生成的构造。
默认的构造函数对于内置类型不处理,自定义类型调用他自己的构造函数,所以我们的ListNode中的_prev_next需要调用自己的构造函数,new出自己的_ptr_pcount

于是我们就得到了一个完整的n1。

此时需要进行销毁了:自动调用n1的析构,导致n1内的count–,满足析构条件进行资源的释放。
进行delete _pcount,_ptr;
_pcount就自动销毁了,但是_ptr是一个自定义类型的指针,我们需要调用自定义的析构,我们写的自定义析构没有释放资源,但由于成员变量时智能指针,会在销毁前自动析构,于是两个分别进行判断,都满足析构条件,进而全部销毁,此时释放这块ListNode完整整体析构。

那为什么我们将_prev,_next进行了指向就不可以正常释放了呢?
在这里插入图片描述
注意:此时由于我们更改指向,间接调用了operator=,导致了析构,也就出现了上边两次析构0x0000…的现象了。

而对于循环引用,当n2销毁时,会先导致n2的引用计数–,但是还未到达销毁条件,此时n1引用计数也–,但同样不会真正销毁,他们两互相牵制了彼此,导致了内存泄漏!
在这里插入图片描述
所以我们需要weak_ptr进行破局,weak_ptr能够破局的最主要原因就在于他的指向不会增加智能指针的引用计数且可以使用智能指针构造weak_ptr,这样就解决了问题。

注意,我们这里是极简版本,库中的实现要复杂得多。
我们的weak_ptr没有RAII的功能,但是有指针的行为。

template<class T>
class weak_ptr
{
public:
weak_ptr() = default;
weak_ptr(const shared_ptr<T>& sptr)
: _ptr(sptr.GetPtr())
{}

T& operator*()
{
return *_ptr;
}

T* operator&()
{
return _ptr;
}
T* operator->()
{
return _ptr;
}

private:
T* _ptr;
};

在这里插入图片描述

定制删除器:

我们有可能也会传入一个不是new出来的,那我们的智能指针总不能无脑delete吧,比如我们传入new Mylass[10],这时就需要delete[],否则会报错。
同样,我们传入FILE时,需要fclose()…
这就需要定制删除器了。

// 仿函数的删除器
template<class T>
struct FreeFunc {
void operator()(T* ptr)
{
std::cout << "free:" << ptr << std::endl;
free(ptr);
}
};
template<class T>
struct DeleteArrayFunc {
void operator()(T* ptr)
{
std::cout << "delete[]" << ptr << std::endl;
delete[] ptr;
}
};
class Myclass
{
public:
Myclass()
: _x(10)
{}
~Myclass()
{
std::cout << "~Myclass()" << std::endl;
}
private:
int _x;
};

int main()
{
FreeFunc<int> freeFunc;
std::shared_ptr<int> sp1((int*)malloc(4), freeFunc);
DeleteArrayFunc<Myclass> deleteArrayFunc;
std::shared_ptr<Myclass> sp2(new Myclass[10], deleteArrayFunc);

std::shared_ptr<FILE> sp5(fopen("test.txt", "w"), [](FILE* p)
{
fclose(p); 
});
return 0;
}

那我们应该怎么实现?
在这里插入图片描述
注意到库中的定义,其实其中的U就是T,我们也可搞一个构造。
但是会有一个问题,这个模板是局部的,我们无法在成员变量中定义,所以需要一个包装器接收传进来的del。
注意我们当不传del时,应该有一个缺省值,避免err的出现。

template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr = nullptr)
: _ptr(ptr)
, _pcount(new int(1))
, _mtx(new mutex)
{}
template<class D>
shared_ptr(T* ptr, D del)
: _ptr(ptr)
, _pcount(new int(1))
, _mtx(new mutex)
, _del(del)
{}
~shared_ptr()
{
Release();
}

void Release()
{
_mtx->lock();
bool flag = false;
if (--(*_pcount) == 0)
{
flag = true;
cout << "delete: " << _ptr << endl;
_del(_ptr);
delete _pcount;
}
_mtx->unlock();
if (flag) delete _mtx;
}

void CountAdd()
{
_mtx->lock();
(*_pcount)++;
_mtx->unlock();
}

T& operator* ()
{
return *_ptr;
}

T* operator&()
{
return _ptr;
}

T* operator->()
{
return _ptr;
}

shared_ptr(const shared_ptr<T>& sptr)
{
_ptr = sptr._ptr;
_pcount = sptr._pcount;
_mtx = sptr._mtx;

CountAdd();
}

shared_ptr<T>& operator=(const shared_ptr<T>& sptr)
{
// process self = self
if (_pcount != sptr._pcount)
{
Release();
_ptr = sptr._ptr;
_pcount = sptr._pcount;
_mtx = sptr._mtx;

CountAdd();
}

return *this;
}

int GetCount() const
{
return *_pcount;
}

T* GetPtr() const
{
return _ptr;
}
private:
T* _ptr;
int* _pcount;
mutex* _mtx;
function<void(T*)> _del = [](T* del) {delete del; };
};

完~


原文地址:https://blog.csdn.net/2301_78636079/article/details/140579033

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!