C++11智能指针
目录
一,智能指针简介
1.使用场景:
在某些场景下面,我们可能会因为忘记对一个资源进行释放而导致内存泄漏问题,也可能因为程序的抛异常操作导致资源没有被释放而导致内存泄漏问题。对于前者我们可以通过对代码的规范书写来规避,但是后者却防不胜防。所以在这里便出现了智能指针,通过智能指针便可以很好的规避上面的两种问题。
2.智能指针的特点:
智能指针的特点是啥呢?智能指针的特点有两个:
1.RAII(资源申请后立即初始化)。
2.像指针一样使用。
3.智能指针的原理:
智能指针的原理便是通过RAII生成一个对象,然后通过这个对象的生命周期来管理指针。这样便可以不用显示的调用delete来释放资源了。也可以避免再抛异常的情况下跳过释放步骤。
4.智能指针的分类:
在C++11里面有以下几个智能指针:
1.auto_ptr
2.unique_ptr
3.shared_ptr
4.weak_ptr
在接下来的内容里我也会重点讲解这些智能指针的实现思想以及缺陷。
二,智能指针的实现
声明:
以下的智能指针的实现都是为了避免在多个指针指向同一块资源的情况下对同一块资源进行多次析构导致内存错误。
一,auto_ptr
实现思想:在赋值或者拷贝构造时将资源管理权转移给赋值对象,再将自己变为nullptr。
缺点:程序员可能不知道某个智能指针的管理权被转移而去访问该指针导致访问空错误。
实现:
template<class T> class auto_ptr { public: //生命周期管理资源 auto_ptr( T* ptr) :_ptr(ptr) {} ~auto_ptr() { cout << "~auto_ptr()" << endl;//只是为了演示 delete _ptr; } //像指针一样 T& operator*() { return *_ptr; } T* operator->() { return _ptr; } //管理权转移 auto_ptr( auto_ptr<T>& sp)//拷贝构造 :_ptr(sp._ptr) { sp._ptr = nullptr;//不要放到初始化列表中,初始化列表只初始化this对象 } auto_ptr& operator =(auto_ptr<T>sp)//赋值 { _ptr = sp._ptr; sp._ptr = nullptr; return *this; } private: T* _ptr; };
测试:
int main() { smart_ptr::auto_ptr<int> sp1(new int); smart_ptr::auto_ptr<int> sp2(new int); *sp2 = 1; cout << *sp2 << endl; sp1 = sp2; //*sp2 = 2; return 0; }
结果:
但是当你无意间再次访问sp2时:
int main() { smart_ptr::auto_ptr<int> sp1(new int); smart_ptr::auto_ptr<int> sp2(new int); *sp2 = 1; cout << *sp2 << endl; sp1 = sp2; *sp2 = 2; return 0; }
便会出错:
二,unique_ptr
实现思想:取消掉赋值和拷贝。
缺点:拷贝场景下非常不适用,只能让一个资源被一个指针管理。
实现方法:将拷贝构造函数和赋值重载取消掉。
实现:
template<class T> class unique_ptr { public: //生命周期管理资源 unique_ptr(T* ptr) :_ptr(ptr) {} ~unique_ptr() { cout << "~unique_ptr()" << endl;//只是为了演示 delete _ptr; } //像指针一样 T& operator*() { return *_ptr; } T* operator->() { return _ptr; } //禁用拷贝构造和赋值重载 unique_ptr(unique_ptr<T>& sp) = delete; unique_ptr& operator =(const unique_ptr<T>& sp) = delete; private: T* _ptr; }; }
在不使用拷贝和赋值的情况下:
void test_unique_ptr() { smart_ptr::unique_ptr<int> sp1(new int); smart_ptr::unique_ptr<int> sp2(new int); *sp2 = 1; cout << *sp2 << endl; //sp1 = sp2; //*sp2 = 2; }
成功运行:
使用赋值以后:
void test_unique_ptr() { smart_ptr::unique_ptr<int> sp1(new int); smart_ptr::unique_ptr<int> sp2(new int); *sp2 = 1; cout << *sp2 << endl; sp1 = sp2; //*sp2 = 2; }
代码报错:
三,shared_ptr
在既要实现拷贝构造又要不能重复析构。shared_ptr便可以完美的实现这个操作。
思想:引用计数
缺陷:害怕循环引用
代码:
template<class T> class shared_ptr { public: shared_ptr() :_ptr(nullptr) ,_pcount(new int(0)) {} //RAII shared_ptr(T* ptr) :_ptr(ptr) ,_pcount(new int(1)) {} ~shared_ptr() { if (--(*_pcount )== 0) { cout << "~shared_ptr()" << endl;//为了演示 delete _ptr; delete _pcount; } } //像指针一样 T& operator*() { return *_ptr; } T* operator->() { return _ptr; } //拷贝和赋值 shared_ptr(const shared_ptr<T>& sp) :_ptr(sp._ptr) , _pcount(sp._pcount) { ++(*_pcount); } shared_ptr<T>& operator = (const shared_ptr<T>& sp) { if (_ptr!=sp._ptr)//当两个指针指向的资源不一样时 //指向一样的资源时因为使赋值,所以*pcount的数量是不会变的。 { --(*_pcount); if (*_pcount == 0) { delete _ptr; } _ptr = sp._ptr; _pcount = sp._pcount; ++(*_pcount); } return *this; } private: T* _ptr; int* _pcount; }; }
测试代码:
void test_shared_ptr() { smart_ptr::shared_ptr<int> sp1(new int); smart_ptr::shared_ptr<int> sp2; //赋值+拷贝 sp2 = sp1; smart_ptr::shared_ptr<int> sp3(sp2); int a = 0; }
结果:
四,weak_ptr
shared_ptr虽然很好用,并且解决了多次析构和拷贝的问题。但是,在一种场景下面shared_ptr也会导致内存泄露的问题。比如以下场景:
struct ListNode//搞一个对象,对象的成员是shared_ptr对象 { smart_ptr::shared_ptr<ListNode>next; smart_ptr::shared_ptr<ListNode>pre; }; void test_shared_ptr2()//搞出两个类型为shared_ptr<ListNode>类型的对象,并将其连接起来 { smart_ptr::shared_ptr<ListNode>sp1 = new ListNode; smart_ptr::shared_ptr<ListNode>sp2 = new ListNode; sp1->next = sp2; sp2->pre = sp1; } int main() { test_shared_ptr2(); return 0; }
示意图如下:
在这种场景下:在这种情况下,堆上的资源里面和外面栈上开辟的sp1和sp2便会同时管理两块块空间:sp1和pre管理一块,sp2和next管理一块。这样便会带来内存泄露的问题:因为next决定着pre的生死,pre又决定着next的生死。这样就会变得非常不好释放空间,导致内存泄漏。
解决方式如下:
实现一个weak_ptr,weak_ptr是专门用来解决这种场景下的问题的。
实现思想如下:
1.weak_ptr不遵循RAII.
2.在weak_ptr里面实现shared_ptr的拷贝和赋值。
3.像指针一样。
4.在weak_ptr里面不实现引用计数。
代码如下:
template<class T> class weak_ptr { public: weak_ptr() :_ptr(nullptr) {} //实现shared_ptr对象的拷贝和赋值 weak_ptr(const shared_ptr<T>& sp) : _ptr(sp.get()) {} weak_ptr<T>& operator =(const shared_ptr<T>& sp) { _ptr = sp.get(); return *this; } //像指针一样 T& operator*() { return *_ptr; } T* operator->() { return _ptr; } private: T* _ptr; }; }
测试代码:
struct ListNode { smart_ptr::weak_ptr<ListNode>next; smart_ptr::weak_ptr<ListNode>pre; }; void test_shared_ptr2() { smart_ptr::weak_ptr<ListNode>sp1 (new ListNode); smart_ptr::weak_ptr<ListNode>sp2 ( new ListNode); sp1->next = sp2; sp2->pre = sp1; int a = 0; } int main() { test_shared_ptr2(); return 0; }
结果:
成功搞定。
在这里将next和pre改为weak_ptr就相当于不再让资源的内部成员决定对方资源的存亡问题。这样也就解决了循环引用的问题。
三,定制删除器
在前面实现的析构函数中,我们使用的都是delete ptr的方式来释放资源。
~shared_ptr() { if (--(*_pcount )== 0) { cout << "~shared_ptr()" << endl;//为了演示 delete _ptr; delete _pcount; } }
但是这种方式在我们开一个int[]资源时便会出现错误:
void test_shared_ptr3() { smart_ptr::shared_ptr<list<string>>sp1(new list<string>[10]);//开的是多个list对象 } int main() { test_shared_ptr3(); return 0; }
再开多个list对象时,便不可以再用delete来做释放工作了,而是要用delete[]来释放资源。这里要强调一个匹配使用。所以我们可以传入一个定制删除器来解决这个问题,定制删除器如下:
?1,是smart_ptr的一个成员:因为类型难以定义,但是知道返回值和参数类型,所以使用包装器。
function<void(T*)>_del;//删除器,用包装器实现不定义类型
?2,要有缺省值,要实现两个构造
缺省值:
function<void(T*)>_del = [](T* ptr) {delete ptr;};
两个构造:不穿删除方法就用缺省值的删除方法
shared_ptr(T* ptr) :_ptr(ptr) ,_pcount(new int(1)) {} shared_ptr(T* ptr, function<void(T*)>del) :_ptr(ptr) , _pcount(new int(1)) , _del(del) {}
3,传删除方法时记得使用lamada表达式,仿函数,函数指针中的一种。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!