C++从入门到起飞之——智能指针!
1. 智能指针的使用场景
下面程序中我们可以看到,new了以后,我们也delete了,但是因为抛异常导,后面的delete没有得到执行,所以就内存泄漏了,所以我们需要new以后捕获异常,捕获到异常后delete内存,再把异常抛出,但是因为new本身也可能抛异常,连续的两个new和下面的Divide都可能会抛异常,让我们处理起来麻烦。智能指针放到这样的场景里面就让问题简单多了。
#include <iostream>using namespace std;double Divide(int a, int b)
{// 当b == 0时抛出异常 if (b == 0){throw "Divide by zero condition!";}else{return (double)a / (double)b;}
}
void Func()
{// 这⾥可以看到如果发⽣除0错误抛出异常,另外下⾯的array和array2没有得到释放。 // 所以这⾥捕获异常后并不处理异常,异常还是交给外⾯处理,这⾥捕获了再重新抛出去。 // 但是如果array2new的时候抛异常呢,就还需要套⼀层捕获释放逻辑,这⾥更好解决⽅案 // 是智能指针,否则代码太戳了 int* array1 = new int[10];int* array2 = new int[10]; // 抛异常呢 try{int len, time;cin >> len >> time;cout << Divide(len, time) << endl;}catch (...){cout << "delete []" << array1 << endl;cout << "delete []" << array2 << endl;delete[] array1;delete[] array2;throw; // 异常重新抛出,捕获到什么抛出什么 }// ...cout << "delete []" << array1 << endl;delete[] array1;cout << "delete []" << array2 << endl;delete[] array2;
}
int main()
{try{Func();}catch (const char* errmsg){cout << errmsg << endl;}catch (const exception& e){cout << e.what() << endl;}catch (...){cout << "未知异常" << endl;}return 0;
}
2. RAII和智能指针的设计思路
RAII是Resource Acquisition Is Initialization的缩写,他是⼀种管理资源的类的设计思想,本质是⼀种利用对象生命周期来管理获取到的动态资源,避免资源泄漏,这里的资源可以是内存、文件指针、网络连接、互斥锁等等。RAII在获取资源时把资源委托给⼀个对象,接着控制对资源的访问, 资源在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源,这样保障了资源的正常释放,避免资源泄漏问题。
智能指针类除了满足RAII的设计思路,还要方便资源的访问,所以智能指针类还会想迭代器类⼀
样,重载 operator*/operator->/operator[] 等运算符,方便访问资源。
template<class T>
class smart_ptr
{
public:smart_ptr(T* ptr) :_ptr(ptr){}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}T& operator[](size_t i){return _ptr[i];}~smart_ptr(){delete[] _ptr;}
private:T* _ptr;
};void Func()
{这⾥使⽤RAII的智能指针类管理new出来的数组以后,程序简单多了 smart_ptr<int> p1 = new int[10];smart_ptr<pair<string,int>> p2 = new pair<string, int>[10];int len, time;cin >> len >> time;cout << Divide(len, time) << endl;
}
3. C++标准库智能指针的使用
C++标准库中的智能指针都在<memory>这个头文件下面,我们包含<memory>就可以是使用了,
智能指针有好几种,除了weak_ptr他们都符合RAII和像指针⼀样访问的行为,原理上而言主要是解
决智能指针拷贝时的思路不同。
> auto_ptr
auto_ptr是C++98时设计出来的智能指针,他的特点是拷贝时把被拷贝对象的资源的管理权转移给
拷贝对象,这是⼀个非常糟糕的设计,因为他会到被拷贝对象悬空,访问报错的问题,C++11设计
出新的智能指针后,强烈建议不要使用auto_ptr。其他C++11出来之前很多公司也是明令禁止使用
这个智能指针的。
#include <iostream>
#include <memory>using namespace std;struct Date
{int _year;int _month;int _day;Date(int year = 1, int month = 1, int day = 1):_year(year), _month(month), _day(day){}~Date(){cout << "~Date()" << endl;}
};int main()
{auto_ptr<Date> p1(new Date);auto_ptr<Date> p2 = p1;return 0;
}
被拷贝对象p1直接就为空了,p1是一个左值,如果再访问程序就会报错 !!
> unique_ptr
unique_ptr是C++11设计出来的智能指针,他的名字翻译出来是唯⼀指针,他的特点的不支持拷贝,只支持移动。如果不需要拷贝的场景就非常建议使用他。
unique_ptr<Date> p1(new Date);//无法拷贝//unique_ptr<Date> p2 = p1;//可以move移动构造转移资源unique_ptr<Date> p2(move(p1));
虽然move移动转移资源后,p1是空的,但是p1本就是左值,使用者显然是知道move可能会带来的后果的。所以,move左值的时候一定要谨慎。
下面简单封装一下unique_ptr:
#include <iostream>
#include <memory>using namespace std;namespace my_unique_ptr
{template<class T>class unique_ptr{public://删除拷贝构造和拷贝赋值unique_ptr(const unique_ptr<T>& uptr) = delete;unique_ptr<T>& operator=(const unique_ptr<T>& uptr) = delete;//构造函数explicit unique_ptr(T* ptr) :_ptr(ptr){}//移动构造unique_ptr(unique_ptr<T>&& uptr) :_ptr(uptr._ptr){uptr._ptr = nullptr;}//移动赋值unique_ptr<T>& operator=(const unique_ptr<T>&& uptr){//释放当前资源if (_ptr) delete _ptr;_ptr = uptr._ptr;uptr._ptr = nullptr;return *this;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}T& operator[](size_t i){return _ptr[i];}const T& operator[](size_t i) const{return _ptr[i];}~unique_ptr(){if(_ptr){delete[] _ptr;cout << "~unique_ptr()" << endl;}}private:T* _ptr;};
}int main()
{my_unique_ptr::unique_ptr<int> p1(new int[10]);my_unique_ptr::unique_ptr<int> p2(new int[10]);for (int i = 0; i < 10; i++){p1[i] = i;p2[i] = i;}my_unique_ptr::unique_ptr<int> p3(move(p1));my_unique_ptr::unique_ptr<int> p4 = move(p2);for (int i = 0; i < 10; i++){cout << p3[i] << " ";}cout << endl;return 0;
}
> shared_ptr
shared_ptr是C++11设计出来的智能指针,他的名字翻译出来是共享指针,他的特点是支持拷贝, 也支持移动。如果需要拷贝的场景就需要使用他了。底层是用引用计数的方式实现的。
我们先来简单使用一下:
int main()
{shared_ptr<int> sp1(new int(10));shared_ptr<int> sp2(new int[10]);//支持拷贝shared_ptr<int> sp3 = sp1;return 0;
}
我们知道智能指针如果支持拷贝,那一定是浅拷贝。因为智能指针的本质是方便我们访问和管理我们new的资源。如果是深拷贝,那就违背智能指针的初衷了!!不过,浅拷贝又会引发两个问题:多个指针管理同一片资源,资源的释放时机,资源的释放次数。所以,我们要确保这份资源只析构一次,并且只有在没有指针管理这份资源的时候才释放!!
因此,在share_ptr的底层中使用了引用计数的方式解决这些问题!!原理就是,用一个计数器管理一篇资源,计数器的数目代表有多少个指针管理这片资源。那我们该怎么实现呢??在share_ptr中,我们new一个count,用这个count来维护。这里不能使用static的原因就是静态变量是属于整个类实例化出来的对象的,因此只有一个计数器是无法管理多份资源的!!
下面简单封装了一下share_ptr:
template<class T>
class share_ptr
{
public://构造share_ptr(T* ptr):_ptr(ptr),_pcnt(new int(1)){}//析构~share_ptr(){if (--(*_pcnt) == 0){delete _ptr;delete _pcnt;}}//拷贝构造share_ptr(const share_ptr<T>& sptr):_ptr(sptr._ptr), _pcnt(sptr._pcnt){++(*_pcnt);}//拷贝赋值share_ptr<T>& operator=(const share_ptr<T>& sptr){// 如果指向同一份资源就不要赋值了if (_ptr != sptr._ptr){//当前引用计数--if (--(*_pcnt) == 0){delete _ptr;delete _pcnt;}//更改资源_ptr = sptr._ptr;_pcnt = sptr._pcnt;(*_pcnt)++;}return *this;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}T& operator[](size_t i){return _ptr[i];}private:T* _ptr;int* _pcnt;
};
> 删除器
无论是unique_ptr还是share_ptr在默认析构时都是使用delete释放资源的,所以如果智能指针管理的资源不是new出来的,程序在释放资源时就会崩溃。
//这样写程序就会崩溃
shared_ptr<Date> sp1(new Date[10]);
unique_ptr<Date> sp2(new Date[10]);
为了解决这个问题,shared_ptr支持在构造的时候给定一个删除器,这个删除器本质就是一个可调用对象,比如:仿函数,函数指针,lambda表达式……而我们的智能指针在大多数情况下用的都是new所以就特化了一个delete[]的版本,我们只需要如下传递模版参数即可,很方便。
shared_ptr<Date[]> sp1(new Date[10]);
unique_ptr<Date[]> sp2(new Date[10]);
而传删除器是一种通用的方法【这里直接传lambda表达式是真的香】。
shared_ptr<Date[]> sp1(new Date[10], [](Date* ptr) {delete[] ptr;});shared_ptr<FILE> sp3(fopen("code1.cpp", "r"),[](FILE* ptr) {fclose(ptr);});
对于特化的new来说,unique_ptr和shared_ptr使用删除器都一样,但是如果是其他情况就不一样了。因为unique_ptr和shared_ptr对删除器的设计有所不同,shared_ptr是在类内部定义的构造函数模版,我们直接传对象编译器就会自动推导类型。但是,unique_ptr却是在类模版多定义了一个模版参数来支持删除器。所以,在使用unique_ptr传删除器的时候,使用仿函数类型会比较方便,因为仿函数类型可以直接定义对象。但是lambda表达式和函数指针是不能直接定义对象的,所以还是要在后面传递一个实例化的对象。
//仿函数
unique_ptr<Date, DeleteArray<Date>> up2(new Date[5]);
//函数指针
unique_ptr<Date, void(*)(Date*)> up3(new Date[5], DeleteArrayFunc<Date>);
//lambda
auto delArrOBJ = [](Date* ptr) {delete[] ptr; };
unique_ptr<Date, decltype(delArrOBJ)> up4(new Date[5], delArrOBJ);
下面我们就自己实现一下shared_ptr的删除器:
template<class T>class shared_ptr{public://构造(没有传删除器就走这个构造)shared_ptr(T* ptr):_ptr(ptr),_pcnt(new int(1)){}template<class D>shared_ptr(T* ptr, D del): _ptr(ptr),_pcnt(new int(1)),_del(del){}//析构~shared_ptr(){if (--(*_pcnt) == 0){_del(_ptr);delete _pcnt;}}//拷贝构造shared_ptr(const shared_ptr<T>& sptr):_ptr(sptr._ptr), _pcnt(sptr._pcnt){++(*_pcnt);}//拷贝赋值shared_ptr<T>& operator=(const shared_ptr<T>& sptr){// 如果指向同一份资源就不要赋值了if (_ptr != sptr._ptr){//当前引用计数--if (--(*_pcnt) == 0){delete _ptr;delete _pcnt;}//更改资源_ptr = sptr._ptr;_pcnt = sptr._pcnt;(*_pcnt)++;}return *this;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}T& operator[](size_t i){return _ptr[i];}private:T* _ptr;int* _pcnt;function<void(T*)> _del = [](T* ptr) {delete ptr; };};
}
> 使用的小细节
• shared_ptr 除了支持用指向资源的指针构造,还支持 make_shared 用初始化资源对象的值直接构造。
• shared_ptr 和 unique_ptr 都支持了operator bool的类型转换,如果智能指针对象是⼀个空对象没有管理资源,则返回false,否则返回true,意味着我们可以直接把智能指针对象给if判断是否空。
• shared_ptr 和 unique_ptr 都得构造函数都使用explicit【在编程语言中,“explicit”作为关键字,表示需要显式转换的数据类型,需手动调用转换函数,而非自动转换】修饰,防止普通指针隐式类型转换成智能指针对象。
4. shared_ptr和weak_ptr
4.1 shared_ptr循环引用问题
shared_ptr在大多数资源管理的场景下都可以很好的解决,但是在循环引用的场景下却会发生资源无法释放,内存泄漏的问题!
在如下场景就会造成循环引用,导致内存泄漏!!
struct ListNode
{int _data;std::shared_ptr<ListNode> _next;std::shared_ptr<ListNode> _prev;~ListNode(){cout << "~ListNode()" << endl;}
};
int main()
{// 循环引⽤ -- 内存泄露 std::shared_ptr<ListNode> n1(new ListNode);std::shared_ptr<ListNode> n2(new ListNode);n1->_next = n2;n2->_prev = n1;return 0;
}
在上图的分析中,next管理着prev指向的资源,而prev也管理着next指向的资源。但是,它们互相指向,也就是说,它们之中一定要有一个释放的话,其条件都是对方要先释放。可是,它们之间相互依赖,彼此制衡,形成一个环状的资源释放链,最终导致它们之间谁都无法释放!!
4.2 weak_ptr
为了解决上述问题,C++11中就引入了weak_ptr。weak_ptr既不支持RAII【资源请求立即初始化】,也不支持访问资源。所以,我们看文档时发现weak_ptr不支持构造时绑定资源,只支持绑定到share_ptr,但是绑定到share_ptr时并不增加计数器的数目。因此,weak_ptr可以解决上述问题。
struct ListNode
{int _data;//std::shared_ptr<listnode> _next;//std::shared_ptr<listnode> _prev;std::weak_ptr<ListNode> _next;std::weak_ptr<ListNode> _prev;~ListNode(){cout << "~ListNode()" << endl;}
};
weak_ptr也没有重载operator*和operator->等,因为他不参与资源管理,那么如果他绑定的
shared_ptr已经释放了资源,那么他去访问资源就是很危险的。weak_ptr支持expired检查指向的
资源是否过期,use_count也可获取shared_ptr的引用计数,weak_ptr想访问资源时,可以调用
lock返回⼀个管理资源的shared_ptr,如果资源已经被释放,返回的shared_ptr是⼀个空对象,如
果资源没有释放,则通过返回的shared_ptr访问资源是安全的。
int main()
{std::shared_ptr<string> sp1(new string("111111"));std::shared_ptr<string> sp2(sp1);std::weak_ptr<string> wp = sp1;cout << wp.expired() << endl;cout << wp.use_count() << endl;// sp1和sp2都指向了其他资源,则weak_ptr就过期了 sp1 = make_shared<string>("222222");cout << wp.expired() << endl;cout << wp.use_count() << endl;sp2 = make_shared<string>("333333");cout << wp.expired() << endl;cout << wp.use_count() << endl;wp = sp1;auto sp3 = wp.lock();cout << wp.expired() << endl;cout << wp.use_count() << endl;*sp3 += "###";cout << *sp1 << endl;return 0;
}