C++智能指针
- 1.智能指针的意义
- 2.内存泄漏
- 3.RAII
- 4.智能指针的原理和使用
- 5.auto_ptr
- 6.unique_ptr
- 7.shared_ptr
- 8.循环引用分析(weak_ptr)
- 9.定制删除器
🌟🌟hello,各位读者大大们你们好呀🌟🌟
🚀🚀系列专栏:【C++的学习】
📝📝本篇内容:智能指针的意义;内存泄漏;RAII;智能指针的原理和使用;auto_ptr;unique_ptr;shared_ptr;循环引用分析(weak_ptr);定制删除器
⬆⬆⬆⬆上一篇:C++异常
💖💖作者简介:轩情吖,请多多指教(> •̀֊•́ ) ̖́-
1.智能指针的意义
在C语言中,我们能够使用malloc和free,在C++中使用new和delete,但是它们都有一个问题,容易造成内存泄露,说大白话就是假设你开辟了一段空间,但是你忘记了释放了,当你写的代码越多,遗忘的可能就越多,到最后可能就内存不够了,因此C++引入了智能指针来解决这个问题
#include <iostream>
#include <string>
using namespace std;
void Count(int* x, int* y)throw(string)
{if (*y == 0){throw string("y==0");//直接抛出到main函数,这会导致}else{cout<<(*x / *y) << endl;}
}void transform()
{int* x = new int(10);int* y = new int(0);Count(x, y);delete x;delete y;
}void Func1()throw()//C++11之前的写法
{}void Func2()noexcept//C++11之后的写法
{}int main()
{try{transform();}catch(const string& str){cout << str << endl;}return 0;
}
上面的代码我们在C++异常讲过,我们一旦抛出异常后,如果没有在transform中捕获,先进行释放内存,再抛出,就会出现内存泄露
2.内存泄漏
内存泄露一般分为两种:
堆内存泄漏:
堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。
系统资源泄漏:
指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。
3.RAII
首先我们先来了解一下什么事RAII,它的思想是一种利用对象生命对象来控制程序资源的简单技术
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。这样我们就不用显式的释放资源,不会再出现因为遗忘而造成内存泄漏,并且采用这种方式,对象所需的资源在其生命周期内始终保持有效
4.智能指针的原理和使用
我们先来使用一下智能指针
#include <iostream>
#include <memory>//使用智能指针需要包含该头文件
#include <vector>
using namespace std;
int main()
{//处理内置类型shared_ptr<int> ptr(new int(1));//我们最常用的就是这个智能指针类,他本质就是靠类对象的生命周期来实现的具体功能cout << *ptr << endl;//处理自定义类型shared_ptr<vector<int>> ptr1(new vector<int>);ptr1->push_back(10);ptr1->push_back(11);ptr1->push_back(12);ptr1->push_back(13);vector<int>::iterator it = ptr1->begin();while (it != ptr1->end()){cout << *it << endl;it++;}return 0;
}
可以看到我们在使用的时候,也会使用*,->这些指针需要的功能,所以说智能指针类必须要有这些操作符的重载才可以让类对象像指针一样去使用
#include <iostream>
#include <memory>
#include <vector>
using namespace std;
namespace lnb
{template<class T>class SmartPtr{public://构造函数获取资源SmartPtr(T* ptr=nullptr):_ptr(ptr){}//析构函数释放资源~SmartPtr(){delete _ptr;}T* operator->()//保证能够像指针一样使用{return _ptr;}T& operator*(){return *_ptr;}private:T* _ptr;};};int main()
{lnb::SmartPtr<vector<int>> ptr1(new vector<int>);ptr1->push_back(10);ptr1->push_back(11);ptr1->push_back(12);ptr1->push_back(13);vector<int>::iterator it = ptr1->begin();while (it != ptr1->end()){cout << *it << endl;it++;}return 0;
}
在上面的代码中,我写了一个最简单的智能指针的模拟实现,有了指针需要的操作符重载
智能指针的原理简单来说就是两点:RAII特性;重载operator*和opertaor->,具有像指针一样的行为
5.auto_ptr
现在我们来看一下库中一些智能指针的写法和用法,首先先讲一下auto_ptr
☞auto_ptr文档
对于这个智能指针我们就简单的讲一下,因为不是重点,而且这个智能指针设计有缺陷,因此我们需要了解一下
我们之前自写的SmartPtr只考虑了最基本的使用,但是并没有考虑赋值之类的问题,但是这也是指针重要的特性,在我们的官方库中就实现了auto_ptr
为什么会报错呢?可以试一下赋值重载也是一样的,这是因为我们的auto_ptr底层是把指针的管理权转移了,从而导致原来的指针悬空,为nullptr,不能使用,所以说这一版的设计是用缺陷的,具体可以看一下下面的模拟实现
#include <iostream>
#include <vector>
#include <memory>
using namespace std;
namespace lnb
{//模拟实现auto_ptrtemplate<class T>class auto_ptr{public:auto_ptr(T* ptr = nullptr):_ptr(ptr){}~auto_ptr(){if(_ptr)delete _ptr;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}//上面的都是基本操作auto_ptr(auto_ptr<T>& ap)//直接将原本的指针转移走了,导致指针悬空,不能使用{_ptr = ap._ptr;ap._ptr = nullptr;//原本指针悬空}auto_ptr<T>& operator=(auto_ptr<T>& ap){if (ap._ptr != _ptr)//防止是自己赋值给自己{if (_ptr){delete _ptr;}_ptr = ap._ptr;ap._ptr = nullptr;}return *this;}T* get(){//获取底层指针return _ptr;}private:T* _ptr;};
}int main()
{//我们模拟实现的lnb::auto_ptr<int> ptr(new int);lnb::auto_ptr<int> ptr1(ptr);cout <<ptr.get() << endl;cout << ptr1.get() << endl;lnb::auto_ptr<int> ptreq(new int);lnb::auto_ptr<int> ptreq1;ptreq1 = ptreq;cout << ptreq.get() << endl;cout << ptreq1.get() << endl;cout << "----------------------" << endl;//库中提供的std::auto_ptr<int> ptr2(new int);std::auto_ptr<int> ptr3(ptr2);cout << ptr2.get() << endl;cout << ptr3.get() << endl;std::auto_ptr<int> ptreq2(new int);std::auto_ptr<int> ptreq3;ptreq3 = ptreq2;cout << ptreq2.get() << endl;cout << ptreq3.get() << endl;return 0;
}
6.unique_ptr
针对于上述的auto_ptr的问题,因此就出现了unique_ptr来解决,但是其实也没有解决彻底,它的功能就是把拷贝赋值功能给delete了,具体看下面实现
☞unique_ptr文档
#include <iostream>
#include <vector>
#include <memory>
namespace lnb
{//模拟实现unique_ptr//简单粗暴的防拷贝template<class T>class unique_ptr{public:unique_ptr(T* ptr = nullptr):_ptr(ptr){}~unique_ptr(){if (_ptr)delete _ptr;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}//上面的都是基本操作unique_ptr(unique_ptr<T>& up) = delete;//这里和auto_ptr不一样unique_ptr<T>& operator=(unique_ptr<T>& up) = delete;//这里和auto_ptr不一样T* get(){//获取底层指针return _ptr;}private:T* _ptr;};
}int main()
{lnb::unique_ptr<int> up(new int(1));lnb::unique_ptr<int> up1(up);//errorstd::unique_ptr<int> up2(new int(1));std::unique_ptr<int> up3(up2);//errorreturn 0;
}
在我们的C++11出来之前,Boost库中就已经有了scoped_ptr/shared_ptr/weak_ptr
因此在我们C++11中委员会把Boost中的精华吸收了,就出现了unique_ptr/shared_ptr/weak_ptr,Boost中的scoped_ptr就是unique_ptr
7.shared_ptr
在智能指针当中,最重要的是shared_ptr,也是最常用的,它完美解决了拷贝问题
☞shared_ptr
它的基本原理是通过引用计数的方式来实现多个shared_ptr对象之间的共享资源
我们先来看一下它的模拟实现
#include <iostream>
#include <memory>
#include <mutex>
using namespace std;
namespace lnb
{template<class T>class shared_ptr{public:shared_ptr(T* ptr = nullptr):_ptr(ptr), _count(new int(1)), _mtx(new mutex){}shared_ptr(const shared_ptr<T>& sp):_ptr(sp._ptr),_count(sp._count),_mtx(sp._mtx){Add_Count();//如若发生拷贝就增加计数}~shared_ptr(){Release();}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}T* get(){return _ptr;}shared_ptr<T>& operator=(shared_ptr<T>& sp){if (sp._ptr != _ptr){Release();//先把原来的指针释放掉_ptr = sp._ptr;_mtx = sp._mtx;_count = sp._count;Add_Count();}return *this;}int use_count(){return *_count;}private:void Add_Count(){_mtx->lock();(*_count)++;_mtx->unlock();}void Release()//释放时使用{bool flag = false;//使用标志位将锁释放掉_mtx->lock();(*_count)--;if (*_count == 0)//当指针已经没人用的时候,就释放掉{delete _ptr;delete _count;flag = true;//不能将锁直接在这直接释放,因为没有解锁}_mtx->unlock();if (flag){delete _mtx;}}private:T* _ptr=nullptr;//引用计数,使用变量的话,每个对象都各自有,无法改变一个来带动相同类型的引用计数int* _count=nullptr;mutex* _mtx = nullptr;//保证不会有线程安全};}
int main()
{lnb::shared_ptr<int> ptr1(new int(1));cout << *ptr1 << endl;lnb::shared_ptr<int> ptr2(ptr1);cout << *ptr2 << endl;cout << ptr1.use_count() << endl;lnb::shared_ptr<int> ptr3;ptr3 = ptr1;cout << *ptr3 << endl;cout << ptr1.use_count() << endl;return 0;
}
引用计数:
①shared_ptr在其内部,给每个资源都维护了一份计数,用来记录这份资源被几个对象共享
②在对象被销毁时,就说明不使用该份资源了,对象的引用计数会减一
③如果引用计数为0了,说明自己是最后一个使用该资源的对象了,需要释放对象中的所有资源
如果不是0,那就说明还有其他对象还在使用该份资源,还不能释放该资源,否则其他对象就成了野指针
线程安全:
①智能指针对象中的引用是多个指针共享的,两个线程中智能指针的引用计数同时++或–会造成线程不安全问题,这个操作不是原子的,最终的结果就会导致资源没有释放或者程序崩溃,因此我们需要给引用计数的++和–操作进行加锁
②智能指针管理的对象在堆上(指针指向的内容),两个线程同时去访问,会导致有线程安全问题
#include <iostream>
#include <memory>
#include <mutex>
#include <thread>
using namespace std;
namespace lnb
{template<class T>class shared_ptr{public:shared_ptr(T* ptr = nullptr):_ptr(ptr), _count(new int(1)), _mtx(new mutex){}shared_ptr(const shared_ptr<T>& sp):_ptr(sp._ptr),_count(sp._count),_mtx(sp._mtx){Add_Count();//如若发生拷贝就增加计数}~shared_ptr(){Release();}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}T* get(){return _ptr;}shared_ptr<T>& operator=(shared_ptr<T>& sp){if (sp._ptr != _ptr){Release();//先把原来的指针释放掉_ptr = sp._ptr;_mtx = sp._mtx;_count = sp._count;Add_Count();}return *this;}int use_count(){return *_count;}private:void Add_Count(){_mtx->lock();(*_count)++;_mtx->unlock();}void Release()//释放时使用{bool flag = false;//使用标志位将锁释放掉_mtx->lock();(*_count)--;if (*_count == 0)//当指针已经没人用的时候,就释放掉{delete _ptr;delete _count;flag = true;//不能将锁直接在这直接释放,因为没有解锁}_mtx->unlock();if (flag){delete _mtx;}}private:T* _ptr=nullptr;//引用计数,使用变量的话,每个对象都各自有,无法改变一个来带动相同类型的引用计数int* _count=nullptr;mutex* _mtx = nullptr;//保证不会有线程安全};}void Func(const lnb::shared_ptr<int>&ptr)
{int x = 1000000;while (x--){lnb::shared_ptr<int> tmp(ptr);//线程是安全的,类中加锁了(*tmp)++;//对于智能指针指向的内容,不是线程安全的}
}
int main()
{lnb::shared_ptr<int> ptr(new int(0));thread t1(Func, ref(ptr));thread t2(Func, ref(ptr));t1.join();t2.join();cout << ptr.use_count() << endl;cout << *ptr << endl;return 0;
}
8.循环引用分析(weak_ptr)
#include <iostream>
#include <memory>
#include <mutex>
#include <thread>
using namespace std;
namespace lnb
{template<class T>class shared_ptr{public:shared_ptr(T* ptr = nullptr):_ptr(ptr), _count(new int(1)), _mtx(new mutex){}shared_ptr(const shared_ptr<T>& sp):_ptr(sp._ptr),_count(sp._count),_mtx(sp._mtx){Add_Count();//如若发生拷贝就增加计数}~shared_ptr(){Release();}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}T* get(){return _ptr;}shared_ptr<T>& operator=(const shared_ptr<T>& sp){if (sp._ptr != _ptr){Release();//先把原来的指针释放掉_ptr = sp._ptr;_mtx = sp._mtx;_count = sp._count;Add_Count();}return *this;}int use_count(){return *_count;}private:void Add_Count(){_mtx->lock();(*_count)++;_mtx->unlock();}void Release()//释放时使用{bool flag = false;//使用标志位将锁释放掉_mtx->lock();(*_count)--;if (*_count == 0)//当指针已经没人用的时候,就释放掉{delete _ptr;delete _count;flag = true;//不能将锁直接在这直接释放,因为没有解锁}_mtx->unlock();if (flag){delete _mtx;}}private:T* _ptr=nullptr;//引用计数,使用变量的话,每个对象都各自有,无法改变一个来带动相同类型的引用计数int* _count=nullptr;mutex* _mtx = nullptr;//保证不会有线程安全};}struct ListNode
{int _data=0;lnb::shared_ptr<ListNode> _next;lnb::shared_ptr <ListNode> _prev;
};int main()
{lnb::shared_ptr<ListNode> ptr1(new ListNode);//第1句lnb::shared_ptr<ListNode> ptr2(new ListNode);//第2句ptr1->_next = ptr2;//第3句ptr1->_prev = nullptr;//第4句ptr2->_next = nullptr;//第5句ptr2->_prev = ptr1;//第6句return 0;
}
看一下上面的代码,有没有发现什么问题
其实要是不仔细分析每一步,很难发现它的问题,new出来的两个对象到最后都没有成功释放
①我们先看前面两句,如果说没有后面的代码,它们肯定没有问题,最后能够正常释放空间
②那如果只看前四句呢?仔细分析一下的话,其实能够发现最后也能释放干净
③但是如果互相指向的话就会出现无法释放的问题,ptr1的next指向ptr2,ptr2的prev指向ptr1,两个引用计数都变成了2,但是当析构的时候,引用计数减到都只能减到1,_next还指向下一个结点,_prev还指向上一个结点,它们互相都等待对方释放,等next析构了,ptr2就释放了,等prev析构了,ptr1就释放了。但是next是ptr1的成员,只有ptr1释放了,next才能析构,而ptr1由prev管理,prev属于ptr2成员,所以这叫做循环引用,谁都不会释放
因此我们要使用weak_ptr来解决,这样就不会增加ptr1和ptr2的引用计数
weak_ptr不是常规智能指针,不支持RAII,支持像指针一样,专门设计出来,辅助解决shared_ptr的循环引用问题,可以指向资源,但是它不参与管理,不增加引用计数
☞weak_ptr文档
#include <iostream>
#include <memory>
#include <mutex>
#include <thread>
using namespace std;
namespace lnb
{template<class T>class shared_ptr{public:shared_ptr(T* ptr = nullptr):_ptr(ptr), _count(new int(1)), _mtx(new mutex){}shared_ptr(const shared_ptr<T>& sp):_ptr(sp._ptr),_count(sp._count),_mtx(sp._mtx){Add_Count();//如若发生拷贝就增加计数}~shared_ptr(){Release();}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}T* get()const{return _ptr;}shared_ptr<T>& operator=(const shared_ptr<T>& sp){if (sp._ptr != _ptr){Release();//先把原来的指针释放掉_ptr = sp._ptr;_mtx = sp._mtx;_count = sp._count;Add_Count();}return *this;}int use_count()const{return *_count;}private:void Add_Count(){_mtx->lock();(*_count)++;_mtx->unlock();}void Release()//释放时使用{bool flag = false;//使用标志位将锁释放掉_mtx->lock();(*_count)--;if (*_count == 0)//当指针已经没人用的时候,就释放掉{delete _ptr;delete _count;flag = true;//不能将锁直接在这直接释放,因为没有解锁}_mtx->unlock();if (flag){delete _mtx;}}private:T* _ptr=nullptr;//引用计数,使用变量的话,每个对象都各自有,无法改变一个来带动相同类型的引用计数int* _count=nullptr;mutex* _mtx = nullptr;//保证不会有线程安全};//模拟实现weak_ptrtemplate<class T>class weak_ptr{public:weak_ptr():_ptr(nullptr){}weak_ptr<T>& operator=(const lnb::shared_ptr<T>& sp){_ptr = sp.get();return *this;}weak_ptr<T>(const lnb::shared_ptr<T>& sp){_ptr = sp.get();}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;};}struct ListNode
{int _data=0;lnb::weak_ptr<ListNode> _next;lnb::weak_ptr<ListNode> _prev;
};int main()
{lnb::shared_ptr<ListNode> ptr1(new ListNode);lnb::shared_ptr<ListNode> ptr2(new ListNode);ptr1->_next = ptr2;ptr1->_prev = nullptr;//先构造一个shared_ptr再赋值ptr2->_next = nullptr;ptr2->_prev = ptr1;cout << ptr1.use_count() << endl;cout << ptr2.use_count() << endl;return 0;
}
9.定制删除器
为什么需要这个定制删除器呢?这主要是因为new出来的对象不一定是单个的,也可能是一组,或者说是用malloc申请的空间,这样使用delete的话会有问题
#include <iostream>
#include <memory>
#include <mutex>
#include <thread>
#include <functional>
using namespace std;
namespace lnb
{template<class T>class shared_ptr{public:shared_ptr(T* ptr = nullptr):_ptr(ptr), _count(new int(1)), _mtx(new mutex){}template<class T,class U>shared_ptr(T* ptr, U del): _ptr(ptr),_del(del),_count(new int(1)),_mtx(new mutex){}shared_ptr(const shared_ptr<T>& sp):_ptr(sp._ptr),_count(sp._count),_mtx(sp._mtx){Add_Count();//如若发生拷贝就增加计数}~shared_ptr(){Release();}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}T* get()const{return _ptr;}shared_ptr<T>& operator=(const shared_ptr<T>& sp){if (sp._ptr != _ptr){Release();//先把原来的指针释放掉_ptr = sp._ptr;_mtx = sp._mtx;_count = sp._count;Add_Count();}return *this;}int use_count()const{return *_count;}private:void Add_Count(){_mtx->lock();(*_count)++;_mtx->unlock();}void Release()//释放时使用{bool flag = false;//使用标志位将锁释放掉_mtx->lock();(*_count)--;if (*_count == 0)//当指针已经没人用的时候,就释放掉{_del(_ptr);delete _count;flag = true;//不能将锁直接在这直接释放,因为没有解锁}_mtx->unlock();if (flag){delete _mtx;}}private:T* _ptr=nullptr;//引用计数,使用变量的话,每个对象都各自有,无法改变一个来带动相同类型的引用计数int* _count=nullptr;mutex* _mtx = nullptr;//保证不会有线程安全function<void(T*)> _del = [](T* ptr) {delete ptr; };
};//模拟实现weak_ptrtemplate<class T>class weak_ptr{public:weak_ptr():_ptr(nullptr){}weak_ptr<T>& operator=(const lnb::shared_ptr<T>& sp){_ptr = sp.get();return *this;}weak_ptr<T>(const lnb::shared_ptr<T>& sp){_ptr = sp.get();}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;};}struct ListNode
{int _data=0;lnb::weak_ptr<ListNode> _next;lnb::weak_ptr<ListNode> _prev;
};template<class T>
struct Del
{void operator()(T* ptr){delete[] ptr;}};int main()
{lnb::shared_ptr<int> ptr(new int[10],Del<int>());lnb::shared_ptr<int> ptr1(new int(1));return 0;
}
🌸🌸C++智能指针的知识大概就讲到这里啦,博主后续会继续更新更多C++的相关知识,干货满满,如果觉得博主写的还不错的话,希望各位小伙伴不要吝啬手中的三连哦!你们的支持是博主坚持创作的动力!💪💪