C++ 智能指针
文章目录
1.引入智能指针的原因
由于C++没有GC(垃圾回收器),程序员从堆上申请的资源,打开的文件,创建的套接字需要我们手动释放和关闭
使用投异常机制可能会出现两个问题:
- 异常安全问题。申请完资源,由于异常需要捕捉,使得执行流不会按顺序进行,导致资源还没有释放和关闭,就跳到别的地方执行了
- 由于需要程序员手动释放和关闭,可能会有忘记释放和关闭的问题
这样就会导致资源的泄漏。计算机的资源是有限的,导致我们可以使用的资源越来越少
总结:智能指针是针对我们从堆上申请的空间,比如:new和malloc出来的空间。使得智能指针来帮我们释放空间,不需要我们来手动释放了
2.智能指针的原理
2.1 RAII思想理解
- RAII是一种了
利用对象生命周期来控制程序资源
(如:内存,文件,套接字,互斥量等)的技术- 在对象构造时获得资源,使得堆资源的控制在对象的生命周期内都有效,最后在对象析构的时候释放资源
- 实际上我们是将资源的管理托管给了一个对象。这样做有两个好处:
- 不需要显示的释放资源
- 资源的控制在对象生命周期内都有效
举例:利用RAII实现一个最简单的智能指针
#pragma once #include<iostream> using namespace std; template<class T> class SmatrPrt { private: T* _ptr; public: //将外面申请的资源,托管给类的成员 SmatrPrt(T* ptr = nullptr) :_ptr(ptr) {} //在对象析构时,自动释放资源 ~SmatrPrt() { if (_ptr) { cout << "delete ptr" << endl; delete _ptr; } } }; #include "SmartPtr.h" void test() { //将申请的资源托管给了sp对象 //当对象空间释放会调用析构函数,释放资源 SmatrPrt<int> sp(new int); } int main() { test(); system("pause"); return 0; }
2.2 智能指针原理剖析
- 智能指针的原理**利用了RAII思想,并且还需要让智能指针具有指针的功能。**即,需要重载operator*和operator->的函数
- 之后在使用指针时,只需要像指针一样控制对象即可
#pragma once #include<iostream> using namespace std; template<class T> class SmatrPrt { private: T* _ptr; public: //将外面申请的资源,托管给类的成员 SmatrPrt(T* ptr = nullptr) :_ptr(ptr) {} T& operator*() { return *_ptr; } T* operator->() { return _ptr; } //在对象析构时,自动释放资源 ~SmatrPrt() { if (_ptr) { cout << "delete ptr" << endl; delete _ptr; } } }; #include "SmartPtr.h" void test(){ SmatrPrt<int> sp(new int); //像指针一样操作对象 *sp = 20; cout << *sp << endl; } int main(){ test(); system("pause"); return 0; }
总结:智能指针
- 利用RAII思想,实现自动释放资源
- 重载operator*和operator->,使其具有和指针一样的行为和功能
3.智能指针的分类
智能指针分为:auto_ptr、unique_ptr、shared_ptr三种
智能指针都包含在memory的库中,要使用智能指针必须包含这个库
上面的代码有一个bug:
原因:用sp1拷贝构造sp2,由于是浅拷贝。会使得两个对象的成员变量指向同一块空间。两个对象在析构时,导致一块空间释放了两次。所以程序会奔溃
随着C++的发展,有三个解决方案,一个方案对应着一种智能指针
- 将指针的管理权转移给另外一个对象。对应C++98的auto_ptr
- 防止拷贝。对应C++11的unique_ptr
- 引用计数。对应C++11的shared_ptr
3.1 auto_ptr的理解
auto_ptr的原理是:将资源的管理权由一个对象转移给另外一个对象
但是这样会有一个问题,如果重新访问ap1,就会出现问题。这种方式在实际在编程中用得少
扩展:模拟实现auto_ptr:
//赋值防止自己给自己赋值,还需要释放当前对象以前的资源 template<class T> class AutoPtr { private: T* _ptr; public: AutoPtr(T* ptr = nullptr) :_ptr(ptr) {} //拷贝构造 //将ap资源的管理权交当前对象 AutoPtr(const AutoPtr<T>& ap) { _ptr = ap._ptr; ap._ptr = nullptr; } AutoPtr<T>& operator=(const AutoPtr<T>& ap) { //防止自己给自己赋值 if (this != &ap) { //释放之前的资源 if (_ptr) { delete _ptr; } _ptr = ap._ptr; ap._ptr = nullptr; } return *this; } T& operator*() { return *_ptr; } T* operator->() { return _ptr; } //在对象析构时,自动释放资源 ~AutoPtr() { if (_ptr) { cout << "delete ptr" << endl; delete _ptr; } } };
3.2 unique_ptr的理解
针对auto_ptr的不足,C++11根据自己的一些语法,设计了一种更靠谱的智能指针,unique_ptr
unique_ptr的原理:直接简单粗暴,将构造和拷贝构造直接禁止编译器默认生成
扩展:unique_ptr的模拟实现
#pragma once #include<iostream> using namespace std; template<class T> class UniquePtr { private: //C++98,将拷贝构造和赋值,显示声明,设为私有 //外部无法调用 UniquePtr(UniquePtr<T>& up); UniquePtr<T>& operator=(UniquePtr<T>& up); T* _ptr; public: //将外面申请的资源,托管给类的成员 UniquePtr(T* ptr = nullptr) :_ptr(ptr) {} //利用C++11语法,禁止编译器默认生成 UniquePtr(UniquePtr<T>& up) = delete; UniquePtr<T>& operator=(UniquePtr<T>& up) = delete; T& operator*() { return *_ptr; } T* operator->() { return _ptr; } //在对象析构时,自动释放资源 ~UniquePtr() { if (_ptr) { cout << "delete ptr" << endl; delete _ptr; } } };
3.3 shared_ptr的理解
- shared_ptr原理:在类中增加一个成员变量用来计数。每次调用一次拷贝构造和赋值重载函数,即每增加一个管理者,计数加1。没析构一个管理者对象,计数器减1,直到减为0,才真正释放资源
- shared_ptr内部,给每一份资源维护了一个计数器,用来记录该份资源被几个对象共享
- 在对象被销毁,也就是调用析构时,说明不在管理该资源,计数器减1
- 如果引用计数等于0,说明当前对象是最后一个使用该资源的对象,必须释放资源
- 当引用计数不等于0,说明还有对象在管理资源,不能释放资源
对于shared_ptr的思考:
1.计数器如何设置:
- 由于当前计数器属于同一资源的对象,可以将计数器设计成一个指针
- 不能设计成引用,引用要引用外部变量,需要外部管理。也不能设计成静态成员变量,静态成员变量,属于整个类,如果当前类要同时管理其它资源计数器就乱了
2.线程安全问题:
- 由于引用了一个计数器。计数器自增(++),自减(–)和判断的行为不是原子的。当多个线程进入,会导致线程安全问题。从而导致资源没有释放,造成内存泄漏
- 为了保证线程安全,可以在shared_ptr类中,再增加一把锁。由于同一份资源的对象要看到同意把锁,所以,锁也需要声明成指针类型
扩展:shared_ptr的模拟实现
shared_ptr实现中注意:
- 在赋值重载函数中,首先需要判断是否是自己给自己赋值
- 其次,不能直接释放被赋值对象的资源,需要先将计数器减1(当前对象不管理了),当计数器等于0,说明没有其它对象管理当前资源,才将资源释放
- 在析构函数中,也是需要将计数器减为0了,才能将资源释放
- 释放资源时注意,需要先解锁了,再释放锁的资源
- 在计数器自增,自减和判断前需要加锁
#include<mutex> template<class T> class SharedPtr { private: T* _ptr; int* _count; mutex* _mt;//防止多线程安全问题 private: void AddCount() { _mt->lock(); (*_count)++; _mt->unlock(); } void ReleasePtr() { //是否需要删除锁 bool flag = true; //加锁,防止线程安全 _mt->lock(); (*_count)--; if ((*_count) == 0) { delete _ptr; delete _count; flag = false;//锁要在外面删除,需要解锁 } _mt->unlock(); //删除锁 if (flag == false) { cout << "delete" << endl; delete _mt; } } public: //将外面申请的资源,托管给类的成员 SharedPtr(T* ptr = nullptr) :_ptr(ptr) , _count(new int(1))//申请一份资源,初始化为1 , _mt(new mutex) {} SharedPtr(SharedPtr<T>& sp) { _ptr = sp._ptr; _count = sp._count; _mt = sp._mt; AddCount(); } SharedPtr<T>& operator=(SharedPtr<T>& sp) { //防止自己给自己赋值 if (this != &sp) { //是否管理资源 if (_ptr) { ReleasePtr(); } _ptr = sp._ptr; _count = sp._count; _mt = sp._mt; AddCount(); } return *this; } T& operator*() { return *_ptr; } T* operator->() { return _ptr; } //在对象析构时,自动释放资源 ~SharedPtr() { ReleasePtr(); } };
shared_ptr的缺陷:会出现循环引用问题,即当循环引用时,shared_ptr会出现资源没有释放的问题
对于shared_ptr缺陷的解决方法:
- C++11增加了一个weak_ptr专门来解决shared_ptr循环引用的问题
- 原理是:weak_ptr的成员变量是shared_ptr,不在具有RAII的思想,只是具有指针的功能,也就是,不会释放资源,也不会将计数器计数
- 使用weak_ptr前提是,知道是循环引用了
template<class T> class WeakPtr { private: T* _wptr; public: WeakPtr(T* wptr = nullptr) { _wptr = wptr; } //用shared_ptr构造 WeakPtr(const SharedPtr<T>& sp) { _wptr = sp.GetPtr(); } WeakPtr<T>& operator=(const SharedPtr<T>& sp) { _wptr = sp.GetPtr(); return *this; } //具有指针的功能 T* operator->() { return &_wptr } T& operator*() { return *_wptr; } };
上面问题的改造:
shared_ptr进行申请多个对象引发的问题:
shared_ptr类里默认的在析构函数里释放资源使用的是delete。但是当我们申请出来的不是一个对象,而是多个对象时:
于是C++11增加了一个
删除器
,可以根据申请对象的不同,设计删除动作,传入shared_ptr中实际用法是,写一个仿函数类(写一个类,从在operator()函数,函数实现是需要删除资源的动作),实例化对象,传入shared_ptr中:
//删除器 template<class T> class Del { public: void operator()(T* ptr) { delete[] ptr; } }; class DelFle { public: void operator()(FILE* ptr) { fclose(ptr); } }; void test() { Del<int> d; shared_ptr<int> sp1(new int[10], d); DelFle df; shared_ptr<FILE> sp2(fopen("text.txt", "w"), df); }