C++ 智能指针的魅力你都了解吗
前情提要
我们知道除了静态内存和栈内存外,每个程序还有一个内存池,这部分内存被称为自由空间或者堆。程序用堆来存储动态分配的对象即那些在程序运行时分配的对象,当动态对象不再使用时,我们的代码必须显式的销毁它们。
在C++中,动态内存的管理是用一对运算符完成的:new和delete,ne:在动态内存中为对象分配一块空间并返回一个指向该对象的指针,delete指向一个动态独享的指针,销毁对象,并释放与之关联的内存。
动态内存管理经常会出现两种问题:一种是忘记释放内存,会造成内存泄漏;一种是尚有指针引用内存的情况下就释放了它,就会产生引用非法内存的指针。
为了更加容易(更加安全)的使用动态内存,引入了智能指针的概念。智能指针的行为类似常规指针,重要的区别是它负责自动释放所指向的对象
智能指针的原理
RAII:利用对象生命周期来控制程序资源。主要是通过对象的构造函数来获得资源的管理权,然后再通过析构函数来释放所管理的资源。其原理就是把管理一份资源的责任托管给了一个对象
//RAII template<class T> class SmartPtr { public: //构造函数获取资源管理权 SmartPtr(T* ptr) :_ptr(ptr) {} //通过析构函数释放资源 ~SmartPtr() { if (_ptr) delete _ptr; } private: T* _ptr; };
class A { private: int _a = 10; }; void test() { //错误写法 int* ptr = new int[10]; SmartPtr<int> sp(ptr); //立即初始化--申请资源时就立即绑定资源 SmartPtr<int> sp2(new int); SmartPtr<A> sp3(new A); }
这并不是一个智能指针对象,智能指针应该要满足以下条件
- 实现RAII思想
- 使用方式和指针一样,例如需要支持*解引用和->操作
应在类中添加以下操作的重载
T* operator->() { return _ptr; } T& operator*() { return *_ptr; }
智能指针和普通的指针的区别之一是智能指针不需要手动释放空间
void test() { //智能指针--编译器调用析构自动释放资源--不存在内存泄漏 SmartPtr<A> sp(new A); (*sp)._a = 10; sp->_a = 100; //普通指针--手动释放内存 int* p = new int; A* pa = new A; *p = 1; pa->_a = 10; //return //提前结束普通指针就会导致内存泄漏 delete p; delete pa; }
C++标准库中的智能指针的使用
库中的智能指针分为 auto_ptr、unique_ptr、 share_ptr
他们都需要引入头文件#include <memory>
才能使用
auto_ptr
auto_ptr是一种存在缺陷的智能指针(禁用)
#include <memory> using namespace std; void test() { auto_ptr<int> ap(new int); auto_ptr<int> ap2(new int(2)); *ap = 10; *ap2 = 20; }
auto_ptr指针进行赋值操作时会将资源进行转移,目的是为了防止多个智能指针指向同一块内存资源。但是这种设计显然不符合我们的需求
我们来简单模拟实现auto_ptr,看他底层是如何进行资源权的转移的
//实现auto_ptr template<class T> class Auto_ptr { public: Auto_ptr(T* ptr) :_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 (this != &ap) { if (_ptr) delete _ptr; //资源管理权转移 _ptr = ap._ptr; ap._ptr = nullptr; } return *this; } private: T* _ptr; };
unique_ptr
unique_ptr智能指针是通过防拷贝来解决资源管理权限转移问题----将unique_ptr赋值运算符函数和拷贝构造函数设置为删除函数
void test() { unique_ptr<int> up(new int(10)); unique_ptr<int> up2(up);//error unique_ptr<int> up3(new int(20)); up = up3; //error }
报错原因:拷贝构造和赋值重载函数都是已经删除的函数
底层实现:
template<class T> class Unique_ptr { public: Unique_ptr(T* ptr) :_ptr(ptr) {} Unique_ptr(const Unique_ptr<T>& up) = delete; Unique_ptr<T>& operator=(const Unique_ptr<T>& up) = delete; ~Unique_ptr() { if (_ptr) { delete _ptr; _ptr = nullptr; } } private: T* _ptr; };
shared_ptr
shared_ptr是C++11中新提供的一种智能指针,不仅解决了资源管理权限转移问题,还提供靠谱的拷贝功能
class A { public: int _a = 10; ~A() { cout << "~A()" << endl; } }; void test() { shared_ptr<A> sp(new A); shared_ptr<A> sp2(new A); shared_ptr<A> sp3(sp2);//ok sp3 = sp;//ok sp->_a = 100; sp2->_a = 1000; sp3->_a = 10000; cout << sp->_a << endl; cout << sp2->_a << endl; cout << sp3->_a << endl; }
运行结果:
我们发现申请多少资源就会释放多少资源,此时的sp和sp3共享一份资源,修改sp3也就相等于修改了sp。所以最终都会打印10000。那共享了一份资源,是如何实现资源只释放一次呢?----引用计数
我们可以通过shared_ptr提供的接口use_count()
来查看,当前有多少个智能指针来管理同一份资源
void test() { shared_ptr<A> sp(new A); cout << sp.use_count() << endl;//1 shared_ptr<A> sp2(sp); cout << sp.use_count() << endl;//2 cout << sp2.use_count() << endl;//2 shared_ptr<A> sp3(new A); cout << sp.use_count() << endl;//2 cout << sp2.use_count() << endl;//2 cout << sp3.use_count() << endl;//1 sp3 = sp; sp3 = sp2; cout << sp.use_count() << endl;//2 cout << sp2.use_count() << endl;//2 cout << sp3.use_count() << endl;//2 }
运行截图:之所以中间会有调析构函数,是因为当sp3指向sp时,sp3的引用计数为0,则会调用析构函数来释放资源。此时sp创建的资源就有3个指智能指针来管理
图解:
在实现时,我们应该确保一个资源只对应一个计数器,而不是每个智能指针都有各自的计数器。所以我们可以将资源和计数器绑定在一起,此时指向同一份资源的智能指针,访问的也都是同一个计数器
成员变量:成员变量应该有两个变量,分别是资源指针的变量_ptr和计数器变量_countPtr,他们都是一个指针类型的变量
拷贝构造函数:在拷贝构造函数中,应该将当前对象的指针指向要拷贝的对象的资源,而且还要拷贝它的计数器,最后还需要将计数器进行++
赋值运算符重载:我们不能判断两个对象是否相等,而应该为只要两个对象的资源不同,那么才需要进行赋值。在赋值中,先让当前对象的计数器进行–,如果为0则表示当前对象的资源是只被当前对象管理,则需要释放资源。然后再将当前对象指针修改为要拷贝的对象的资源,并且拷贝它的计数器。最后还要对计数器进行++操作
析构函数:判断当前对象的资源的计数器,先进行–操作,在判断计数器是否为0,如果为0才会将资源释放,不为0则什么都不做
template<class T> class Shared_ptr { public: Shared_ptr(T* ptr) :_ptr(ptr) , _countPtr(new size_t(1))//初始化为1 {} Shared_ptr(const Shared_ptr<T>& sp) :_ptr(sp._ptr) , _countPtr(sp._countPtr) { //计数器累加 ++(*_countPtr); } Shared_ptr<T> operator=(const Shared_ptr<T>& sp) { if (_ptr != sp._ptr) { //本身计数器自减 //计数器为0,则当前对象需要释放资源 if (--(*_countPtr) == 0) { delete _ptr; delete _countPtr; } _ptr = sp._ptr; _countPtr = sp._countPtr; ++(*_countPtr); } return *this; } ~Shared_ptr() { //计数器自减 if (--(*_countPtr) == 0) { delete _ptr; delete _countPtr; _ptr = nullptr; _countPtr = nullptr; } } T& operator*() { return *_ptr; } T* operator->() { return _ptr; } private: T* _ptr; size_t* _countPtr;//计数器指针 };
我们实现的shared_ptr智能指针在多线程的场景下其实是不存在线程安全问题的----引用计数器指针是一个共享变量,多个线程进行修改时会导致计数器混乱。导致资源提前被释放或者会产生内存泄漏问题
我们来看看一下代码, 如果是安全的,那么最后析构函数应该只被调用一次
void fun(const Shared_ptr<A>& sp, int n) { for (int i = 0; i < n; ++i) Shared_ptr<A> copy(sp);//创建copy智能指针 } void test() { Shared_ptr<A> sp(new A); int n = 100000; thread t1(fun, ref(sp), n); thread t2(fun, ref(sp), n); t1.join(); t2.join(); }
运行结果1:我们发现并没有调用对象的析构函数,说明此时产生了内存泄漏的问题
运行结果2:调用两次析构函数,也就说明一份资源被释放两次。
我们可以在类中提供获取计数器的值的接口
size_t getCount() { return *_countPtr; }
然后再代码中运行并获取计数器的值,发现计数器的值并没有为0,所以就不会调用调用析构函数
所以我们可以在修改计数器的地方进行加锁保护。而这个锁不能为全局变量的锁,资源之间不能被有影响,否则当一个资源进行加锁修改时,另一个资源会被收到影响,此时会影响代码的执行效率。应当给每一个计数器提供单独的锁
这里++操作和–操作都进行封装
template<class T> class Shared_ptr { public: Shared_ptr(T* ptr) :_ptr(ptr) , _countPtr(new size_t(1))//初始化为1 , _mtx(new mutex) {} Shared_ptr(const Shared_ptr<T>& sp) :_ptr(sp._ptr) , _countPtr(sp._countPtr) ,_mtx(sp._mtx) { //计数器累加 //++(*_countPtr); addCount(); } Shared_ptr<T> operator=(const Shared_ptr<T>& sp) { if (_ptr != sp._ptr) { //本身计数器自减 //计数器为0,则当前对象需要释放资源 //if (--(*_countPtr) == 0) if (subCount() == 0) { delete _ptr; delete _countPtr; delete _mtx; } _ptr = sp._ptr; _countPtr = sp._countPtr; addCount(); } return *this; } ~Shared_ptr() { //计数器自减 if (subCount() == 0) { delete _ptr; delete _countPtr; delete _mtx; _ptr = nullptr; _countPtr = nullptr; _mtx = nullptr; } } T& operator*() { return *_ptr; } T* operator->() { return _ptr; } size_t getCount() { return *_countPtr; } size_t addCount() { _mtx->lock(); ++(*_countPtr); _mtx->unlock(); return *_countPtr; } size_t subCount() { _mtx->lock(); --(*_countPtr); _mtx->unlock(); return *_countPtr; } private: T* _ptr; size_t* _countPtr;//计数器指针 mutex* _mtx; };
运行结果:我们发现多线程场景下,也是都可以正常释放的
循环引用问题
shared_ptr其实也存在一些小问题,也就是循环引用问题
我们先来看看以下代码
struct ListNode { shared_ptr<ListNode> _next; shared_ptr<ListNode> _prev; int _data; ~ListNode() { cout << "~ListNode()" << endl; } }; void test() { shared_ptr<ListNode> n1(new ListNode); shared_ptr<ListNode> n2(new ListNode); 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; }
运行结果:我们发现并没有释放资源,计数器也在自增
图解:
在C++11中,专门为了解决这个问题,又引入了一种新的智能指针waek_ptr,这种指针称为弱指针。在赋值或者拷贝的时候,计数器并不会进行++。析构时也并不会进行真正资源的释放。waek_ptr不能单独使用,其最大的作用就是解决shared_ptr循环引用的问题。
struct ListNode { weak_ptr<ListNode> _next; weak_ptr<ListNode> _prev; int _data; ~ListNode() { cout << "~ListNode()" << endl; } }; void test() { shared_ptr<ListNode> n1(new ListNode); shared_ptr<ListNode> n2(new ListNode); 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; }
运行结果:
在我们自己实现的shared_ptr中,我们在释放资源时都单单以delete来释放,而在我们申请空间方式中并非就只用new来申请空间们也有可能是用malloc来申请,则此时就应该要用free来释放。所以我们还要给智能指针添加一个删除器
void test() { Shared_ptr<A> sp(new A[100]);//调用析构会报错 }
删除器主要可以通过仿函数来实现
template<class T> struct DeleteDel { void operator()(T* ptr) { delete ptr; } }; template<class T> struct FreeDel { void operator()(T* ptr) { free(ptr); } }; template<class T> struct DeleteArrDel { void operator()(T* ptr) { delete[] ptr; } };
template<class T, class Del = DeleteDel<T>> class Shared_ptr { public: Shared_ptr(T* ptr) :_ptr(ptr) , _countPtr(new size_t(1))//初始化为1 , _mtx(new mutex) {} Shared_ptr(const Shared_ptr<T>& sp) :_ptr(sp._ptr) , _countPtr(sp._countPtr) ,_mtx(sp._mtx) { //计数器累加 //++(*_countPtr); addCount(); } Shared_ptr<T> operator=(const Shared_ptr<T>& sp) { if (_ptr != sp._ptr) { //本身计数器自减 //计数器为0,则当前对象需要释放资源 //if (--(*_countPtr) == 0) if (subCount() == 0) { //delete _ptr; //通过删除器来释放空间 _del(_ptr); delete _countPtr; delete _mtx; } _ptr = sp._ptr; _countPtr = sp._countPtr; addCount(); } return *this; } ~Shared_ptr() { //计数器自减 if (subCount() == 0) { //delete _ptr; //通过删除器来释放空间 _del(_ptr); delete _countPtr; delete _mtx; _ptr = nullptr; _countPtr = nullptr; _mtx = nullptr; } } T& operator*() { return *_ptr; } T* operator->() { return _ptr; } size_t getCount() { return *_countPtr; } size_t addCount() { _mtx->lock(); ++(*_countPtr); _mtx->unlock(); return *_countPtr; } size_t subCount() { _mtx->lock(); --(*_countPtr); _mtx->unlock(); return *_countPtr; } private: T* _ptr; size_t* _countPtr;//计数器指针 mutex* _mtx; Del _del; };
以上就是C++ 一篇文章让你知道智能指针的魅力的详细内容,更多关于C++智能指针的资料请关注我们其它相关文章!