本文将介绍智能指针用法的一些平时可能没注意的细节(关于智能指针的基本用法可以参考前面的博文)。
1.unique_ptr和shared_ptr在构造上的一点差异
unique_ptr支持动态数组,而shared_ptr不能直接支持动态数组。std::unique_ptr<int []> ptr(new int[10]);合法,而std::shared_ptr<int []> ptr(new int[10]);是不合法的。
如果通过std::shared_ptr来构造动态数组,则需要显式指定删除器,比如下面的代码:
std::shared_ptr<int> p(new int[10], [](int* p){delete[] p;}); //指定delete[]
也可以用std::default_delete作为删除器:
std::shared_ptr<int> p(new int[10], std::default_delete<int[]>);
我们可以封装一个make_shared_array方法让shared_ptr支持数组:
template<typename T> shared_ptr<T> make_shared_array(size_t size) { return shared_ptr<T>(new T[size], default_delete<T[]>()); }
测试代码:
std::shared_ptr<int> p = make_shared_array<int>(10); std::shared_ptr<char> p = make_shared_array<char>(10);
unique_ptr缺少一个类似于make_shared的make_unique方法,不过在c++14中会增加make_unique方法。其实要实现一个make_unique方法是比较简单的:
//支持普通指针 template<class T, class... Args> inline typename enable_if<!is_array<T>::value, unique_ptr<T> >::type make_unique(Args&&... args) { return unique_ptr<T>(new T(std::forward<Args>(args)...)); } //支持动态数组 template<class T> inline typename enable_if<is_array<T>::value && extent<T>::value==0, unique_ptr<T> >::type make_unique(size_t size) { typedef typename remove_extent<T>::type U; return unique_ptr<T>(new U[size]()); } //过滤掉定长数组的情况 template<class T, class... Args> typename enable_if<extent<T>::value != 0, void>::type make_unique(Args&&...) = delete;
实现思路很简单,如果不是数组则直接创建unique_ptr,如果是数组的话,先判断是否为定长数组,如果为定长数组则编译不通过;非定长数组时,获取数组中的元素类型,再根据入参size创建动态数组的unique_ptr。extent<T>::value用来获取数组的长度,如果获取值为0,则不到说明不是定长数组。
2.shared_ptr和unique_ptr指定删除器方式的一点差异
unique_ptr指定删除器和std:: shared_ptr是有差别的,比如下面的写法:
std:: shared_ptr<int> ptr(new int(1), [](int*p){delete p;}); //正确
std::unique_ptr<int> ptr(new int(1), [](int*p){delete p;}); //错误
std::unique_ptr指定删除器的时候需要确定删除器的类型,所以不能直接像shared_ptr指定删除器,可以这样写:
std::unique_ptr<int, void(*)(int*)> ptr(new int(1), [](int*p){delete p;});
上面这种写法在lambda没有捕获变量的情况下是正确的,如果捕获了变量则会编译报错:
std::unique_ptr<int, void(*)(int*)> ptr(new int(1), [&](int*p){delete p;}); //错误,因为捕获了变量
为什么lambda捕获了变量作为unique_ptr就会报错呢,因为lambda在没有捕获变量的情况下是可以直接转换为函数指针的,捕获了就不能转换为函数指针。
如果希望unique_ptr的删除器支持lambda,可以这样写:
std::unique_ptr<int, std::function<void(int*)>> ptr(new int(1), [&](int*p){delete p;});
我们还可以自定义unique_ptr的删除器,比如下面的代码:
#include <memory> #include <functional> using namespace std; struct MyDeleter { void operator()(int*p) { cout<<"delete"<<endl; delete p; } }; int main() { std::unique_ptr<int, MyDeleter> p(new int(1)); return 0; }
3.通过智能指针管理第三方库分配的内存
智能指针可以很方便的管理当前程序库动态分配的内存,还可以用来管理第三方库分配的内存。第三方库分配的内存一般需要通过第三方库提供的释放接口才能释放,由于第三方库返回出来的指针一般都是原始指针,如果用完之后没有调用第三方库的释放接口,就很容易造成内存泄露。比如下面的代码:
void* p = GetHandle()->Create(); //do something… GetHandle()->Release(p);
这段代码实际上是不安全的,在使用第三方库分配的内存过程中,可能忘记调用Release接口,还有可能中间不小心返回了,还有可能中间发生了异常,导致无法调用Release接口,这时用智能指针去管理第三方库的内存就很合适了,只要出了作用域内存就会自动释放,不用显式去调用释放接口了,不用担心中途返回或者发生异常导致无法调用释放接口的问题。
void* p = GetHandle()->Create(); std::shared_ptr<void> sp(p, [this](void*p){ GetHandle()->Release(p);});
上面这段代码就可以保证任何时候都能正确释放第三方库分配的内存。虽然能解决问题,但还是有些繁琐,因为每个第三方库分配内存的地方都要调用这段代码,比较繁琐,我们可以将这段代码提炼出来作为一个公共函数,简化调用。
std::shared_ptr<void> Guard(void* p) { return std::shared_ptr<void> sp(p, [this](void*p){ GetHandle()->Release(p);}); } void* p = GetHandle()->Create(); auto sp = Guard(p); //do something…
上面的代码通过Guard函数做了简化,用起来比较方便,但仍然不够安全,因为有可能使用者可能会这样写:
void* p = GetHandle()->Create(); Guard(p); //危险,这句结束之后p就被释放了 //do something…
这样写是有问题的,会导致访问野指针,因为Guard(p);是一个右值,如果不赋值给一个指针的话,Guard(p);这句结束之后,就会释放,导致p提前释放了,后面就会访问野指针的内容。auto sp = Guard(p);需要一个赋值操作,忘记赋值就会导致指针提前释放,这种写法仍然不够安全。我们可以定义一个宏来解决这个问题:
#define GUARD(P) std::shared_ptr<void> p##p(p, [](void*p){ GetHandle()->Release(p);}); void* p = GetHandle()->Create(); GUARD(p); //安全
也可以用unique_ptr来管理第三方的内存:
#define GUARD(P) std::unique_ptr<void, void(*)(int*)> p##p(p, [](void*p){ GetHandle()->Release(p);});
通过宏定义方式我们可以避免智能指针忘记赋值,即方便又安全。
c++11 boost技术交流群:296561497,欢迎大家来交流技术。