当前位置:首页 » 《随便一记》 » 正文

【C++】智能指针

9 人参与  2023年04月06日 11:29  分类 : 《随便一记》  评论

点击全文阅读


文章目录

一、引入二、智能指针2.1 智能指针保存与释放资源RAII2.2 智能指针的其他操作2.3 智能指针拷贝问题2.4 auto_ptr管理权转移2.5 unique_ptr防拷贝2.6 shared_ptr引用计数❗️❗️2.6.1 引用计数的实现2.6.2 赋值问题2.6.3 多线程拷贝问题2.6.4 循环引用问题❗️❗️ 2.7 weak_ptr不管资源2.7.1 weak_ptr简单实现 三、定制删除器3.1 模拟实现定制删除器

一、引入

double div(){double a, b;cin >> a >> b;if (b == 0){throw "除0错误";}return a / b;}void fun(){int* p = new int[10];cout << div() << endl;delete[]p;}int main(){try{fun();}catch (const char* errstr){cout << errstr << endl;}catch (...){cout << "未知错误" << endl;}return 0;}

在上一章【C++】异常中为了这种会导致内存泄漏的情况,我们的办法是在fun函数内在try+catch重复throw出异常。

void fun(){int* p = new int[10];try{cout << div() << endl;}catch (...){delete[]p;cout << "delete[]p 1" << endl;throw "除0错误";}cout << "delete[]p 2" << endl;delete[]p;}

在这里插入图片描述
但是如果有两个空间需要释放:

void fun(){int* p1 = new int[10];int* p2 = new int[15];try{cout << div() << endl;}catch (...){delete[]p1;delete[]p2;throw "除0错误";}delete[]p1;delete[]p2;}

我们要知道new也是会抛异常的,如果p1抛出了异常,那么p2就没有被释放掉,造成内存泄漏。
而智能指针就能很好的解决这个问题。

二、智能指针

2.1 智能指针保存与释放资源RAII

template <class T>class SmartPtr{public:// 保存资源SmartPtr(T* ptr): _ptr(ptr){}// 释放资源~SmartPtr(){delete[]_ptr;cout << _ptr << endl;}private:T* _ptr;};

有了这个以后不管谁抛异常我们就不需要再考虑资源回收的问题了:

void fun(){int* p1 = new int[10];SmartPtr<int> sp1(p1);SmartPtr<int> sp2(new int[15]);cout << div() << endl;}

在这里插入图片描述
当我们把堆上的资源交给智能指针后,出了作用域后就会直接释放,就算抛了异常也会出作用域后销毁。
而智能指针的构造函数相当于保存资源、析构函数相当于释放资源

RAII:

RAII是一种利用对象生命周期来控制程序资源的资源。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象
把资源的声明周期和对象的生命周期绑定到一起
这样就有两点好处:

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

而智能指针既然是指针就要像指针一样使用,还要有其他的操作。

2.2 智能指针的其他操作

template <class T>class SmartPtr{public:SmartPtr(T* ptr): _ptr(ptr){}~SmartPtr(){delete[]_ptr;cout << _ptr << endl;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}T& operator[](size_t pos){return _ptr[pos];}private:T* _ptr;};

这样我们就可以像一个指针一样使用它

2.3 智能指针拷贝问题

原生指针的拷贝就是指向同一块空间,但是原生指针销毁的时候不会清理资源,而智能指针就会对同一块空间析构两次

先来看看库中的智能指针怎么解决这个问题的。

2.4 auto_ptr管理权转移

int main(){std::auto_ptr<int> ptr1(new int);std::auto_ptr<int> ptr2(ptr1);return 0;}

在这里插入图片描述
???
在这里插入图片描述
可以看到auto_ptr使用拷贝构造就是把原来的资源转移走
这样就有可能会造成空指针访问

2.5 unique_ptr防拷贝

unique_ptr的方法简单粗暴:直接不让拷贝

int main(){std::unique_ptr<int> ptr1(new int);std::unique_ptr<int> ptr2(ptr1);return 0;}

在这里插入图片描述

2.6 shared_ptr引用计数❗️❗️

当两个指针同时指向一块空间时,就增加一个引用计数。

2.6.1 引用计数的实现

在开辟空间的时候在堆上申请一块空间,指针同时指向资源和堆上的空间,堆上的空间就可以用来计数。
在这里插入图片描述

// SmartPtr.htemplate <class T>class SmartPtr{public:SmartPtr(T* ptr = nullptr): _ptr(ptr), _pcnt(new int(1)){}SmartPtr(const SmartPtr<T>& sp): _ptr(sp._ptr), _pcnt(sp._pcnt){(*_pcnt)++;}~SmartPtr(){(*_pcnt)--;if (*_pcnt == 0){delete _ptr;delete _pcnt;cout << _ptr << endl;}}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}T& operator[](size_t pos){return _ptr[pos];}private:T* _ptr;int* _pcnt;};// SmartpPtr.cppint main(){SmartPtr<int> ptr1(new int);SmartPtr<int> ptr2(ptr1);return 0;}

在这里插入图片描述

2.6.2 赋值问题

这里首先要解决的是自己给自己赋值的问题,不能直接this != &sp,因为有可能是不一样的智能指针指向同一块资源空间
正确写法:if (_ptr != sp->_ptr)
其次就要解决释放左边的空间,这里注意不能直接释放,而是要看引用计数是否为0,为0才能释放。

SmartPtr<T>& operator=(const SmartPtr<T>& sp){if (_ptr != sp._ptr){(*_pcnt)--;if (*_pcnt == 0){delete _pcnt;delete _ptr;}_ptr = sp._ptr;_pcnt = sp._pcnt;(*_pcnt)++;}return *this;}

2.6.3 多线程拷贝问题

假设我们现在是多线程的情况要对一个智能指针进行拷贝
我们先在智能指针类放一个能获取引用计数的成员函数:

// 获取引用计数值int use_count(){return *_pcnt;}

然后多线程开始不停的用临时的智能指针进行拷贝:

void test(){const int N = 100000;SmartPtr<int> sp1(new int[10]);std::thread t1([&]() {for (int i = 0; i < N; i++){SmartPtr<int> sp2(sp1);}});std::thread t2([&]() {for (int i = 0; i < N; i++){SmartPtr<int> sp3(sp1);}});t1.join();t2.join();cout << sp1.use_count() << endl;}

按道理来说t1和t2线程内部定义的智能指针都是临时对象,拷贝完就会销毁,按道理说最后输出应该为1。但是我们来看看结果:
在这里插入图片描述
不仅如此有时候程序还会崩溃。
原因:

我们知道++--都不是原子性的,假设现在引用计数是1,本来应该是线程1和线程2都会把引用计数++,结果变成3,但是它们同时++,导致结果变成了2,然后线程1和线程2临时对象销毁,引用计数都要–,结果导致引用计数变成0,销毁资源,造成野指针

为了解决这种情况我们就可以进行加锁,让线程串行访问。
所以我们要加一个成员变量(锁)。而因为要同时保护引用计数++--所以必须是同一把锁。
而因为锁是防拷贝的,所以我们要使用指针。
所有引用计数改变的地方都要保护起来。

template <class T>class SmartPtr{public:SmartPtr(T* ptr = nullptr): _ptr(ptr), _pcnt(new int(1)), _pmutex(new std::mutex){}SmartPtr(const SmartPtr<T>& sp): _ptr(sp._ptr), _pcnt(sp._pcnt), _pmutex(sp._pmutex){_pmutex->lock();(*_pcnt)++;_pmutex->unlock();}~SmartPtr(){_pmutex->lock();(*_pcnt)--;_pmutex->unlock();if (*_pcnt == 0){delete _ptr;delete _pcnt;delete _pmutex;//cout << _ptr << endl;}}SmartPtr<T>& operator=(const SmartPtr<T>& sp){if (_ptr != sp._ptr){_pmutex->lock();(*_pcnt)--;_pmutex->unlock();if (*_pcnt == 0){delete _pcnt;delete _ptr;}_ptr = sp._ptr;_pcnt = sp._pcnt;_pmutex = sp._pmutex;_pmutex->lock();(*_pcnt)++;_pmutex->unlock();}return *this;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}T& operator[](size_t pos){return _ptr[pos];}// 获取引用计数值int use_count(){return *_pcnt;}private:T* _ptr;int* _pcnt;std::mutex* _pmutex;};void test(){const int N = 100000;SmartPtr<int> sp1(new int[10]);std::thread t1([&]() {for (int i = 0; i < N; i++){SmartPtr<int> sp2 = sp1;}});std::thread t2([&]() {for (int i = 0; i < N; i++){SmartPtr<int> sp3 = sp1;}});t1.join();t2.join();cout << sp1.use_count() << endl;}

在这里插入图片描述
多次测试结果全部为1。

这里要注意的是share_ptr本身是线程安全的,但是它指向的资源不一定是线程安全的

2.6.4 循环引用问题❗️❗️

现在我们写一个链表链接的代码:

struct ListNode{int val;ListNode* left;ListNode* right;~ListNode(){cout << "~ListNode()" << endl;}};void test(){ListNode* n1 = new ListNode;ListNode* n2 = new ListNode;n1->right = n2;n2->left = n1;delete n1;delete n2;}

在这里插入图片描述
现在我们不想自己delete资源,可以考虑使用智能指针。
而智能指针不能赋值给自定义指针,所以我们要改变指针的类型。

struct ListNode{int val;SmartPtr<ListNode> left;SmartPtr<ListNode> right;~ListNode(){cout << "~ListNode()" << endl;}};void test(){SmartPtr<ListNode> n1 = new ListNode;SmartPtr<ListNode> n2 = new ListNode;n1->right = n2;n2->left = n1;}

在这里插入图片描述
但是我们看到结果并没有释放掉资源,这是怎么回事呢?

在这里插入图片描述
对于n1资源,有n1指向和n2的left指向,所以引用计数为2,n2资源同理。当n1和n2出了作用域,两个的引用计数都变成1。
在这里插入图片描述
但此时right和left是随着对象的销毁才能销毁,但是对象想要销毁,引用计数就要减为0,引用计数减为0,就要指针销毁,这样就成了个死循环。这里要注意的是如果只链接了一个就不会有这种问题。

而为了解决这个问题,我们引入了weak_ptr

2.7 weak_ptr不管资源

这里的weak_ptr没有引用计数,不支持RAII,只指向资源,不管理资源。

struct ListNode{int val;std::weak_ptr<ListNode> left;std::weak_ptr<ListNode> right;~ListNode(){cout << "~ListNode()" << endl;}};void test(){std::shared_ptr<ListNode> n1(new ListNode);std::shared_ptr<ListNode> n2(new ListNode);n1->right = n2;n2->left = n1;}

在这里插入图片描述

2.7.1 weak_ptr简单实现

namespace yyh{template <class T>class weak_ptr{public:weak_ptr(): _ptr(nullptr){}weak_ptr(const SmartPtr<T>& p): _ptr(p.get()){}weak_ptr<T>& operator=(const SmartPtr<T>& p){_ptr = p.get();return *this;}private:T* _ptr;};}struct ListNode{int val;yyh::weak_ptr<ListNode> left;yyh::weak_ptr<ListNode> right;~ListNode(){cout << "~ListNode()" << endl;}};void test(){SmartPtr<ListNode> n1(new ListNode);SmartPtr<ListNode> n2(new ListNode);n1->right = n2;n2->left = n1;}

在这里插入图片描述

三、定制删除器

普通的内置类型确实可以让智能指针自动释放,但是如果不是内置类型呢?

void test(){std::shared_ptr<std::string> n(new std::string[10]);}

这样直接会导致程序崩溃,因为delete类型不匹配([])。
在这里插入图片描述
这里的del就是定制删除器,定制删除器就是一个可调用对象

template <class D>class Delete{public:void operator()(const D* del){delete[]del;cout << "delete[]del" << endl;}};void test(){std::shared_ptr<std::string> n(new std::string[10], Delete<std::string>());}

在这里插入图片描述

3.1 模拟实现定制删除器

这里我们不能像库里那样在构造的时候把参数传进去,因为要删除是在析构函数中,无法从构造函数传递到析构函数。所以我们可以给整个类增加一个模板参数增加一个新的成员变量

而如果要增加一个模板参数,我们为了让前面的代码运行,所以增加一个默认的删除器。

template <class T>class DefultDelete// 默认{public:void operator()(T* ptr){delete ptr;}};template <class D>class Delete{public:void operator()(const D* del){delete[]del;cout << "delete[]del" << endl;}};template <class T, class D = DefultDelete<T>>class SmartPtr{public:SmartPtr(T* ptr = nullptr): _ptr(ptr), _pcnt(new int(1)), _pmutex(new std::mutex){}SmartPtr(const SmartPtr<T>& sp): _ptr(sp._ptr), _pcnt(sp._pcnt), _pmutex(sp._pmutex){_pmutex->lock();(*_pcnt)++;_pmutex->unlock();}~SmartPtr(){_pmutex->lock();(*_pcnt)--;_pmutex->unlock();if (*_pcnt == 0){//delete _ptr;_del(_ptr);delete _pcnt;delete _pmutex;//cout << _ptr << endl;}}SmartPtr<T>& operator=(const SmartPtr<T>& sp){if (_ptr != sp._ptr){_pmutex->lock();(*_pcnt)--;_pmutex->unlock();if (*_pcnt == 0){this->~SmartPtr();//delete _pcnt;//delete _ptr;}_ptr = sp._ptr;_pcnt = sp._pcnt;_pmutex = sp._pmutex;_pmutex->lock();(*_pcnt)++;_pmutex->unlock();}return *this;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}T& operator[](size_t pos){return _ptr[pos];}// 获取引用计数值int use_count(){return *_pcnt;}T* get() const{return _ptr;}private:T* _ptr;int* _pcnt;std::mutex* _pmutex;D _del;};namespace yyh{template <class T>class weak_ptr{public:weak_ptr(): _ptr(nullptr){}weak_ptr(const SmartPtr<T>& p): _ptr(p.get()){}weak_ptr<T>& operator=(const SmartPtr<T>& p){_ptr = p.get();return *this;}private:T* _ptr;};}struct ListNode{int val;yyh::weak_ptr<ListNode> left;yyh::weak_ptr<ListNode> right;~ListNode(){cout << "~ListNode()" << endl;}};void test(){SmartPtr<ListNode> n1(new ListNode);SmartPtr<ListNode, Delete<ListNode>> n2(new ListNode[5]);}

在这里插入图片描述




点击全文阅读


本文链接:http://zhangshiyu.com/post/58596.html

<< 上一篇 下一篇 >>

  • 评论(0)
  • 赞助本站

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

关于我们 | 我要投稿 | 免责申明

Copyright © 2020-2022 ZhangShiYu.com Rights Reserved.豫ICP备2022013469号-1