C++ 11中的智能指针
引言
普通指针使用时存在挂起引用以及内存泄漏的问题,C++ 11中引入了智能指针来解决它
std::unique_ptr
std::unique_ptr是std::auto_ptr的替代品,解决了C++ 11之前std::auto_ptr的很多缺漏
简单的看一下std::auto_ptr的复制构造函数
template<typename T>
class auto_ptr {
public:
//Codes..
auto_ptr(auto_ptr& atp) {
m_ptr = atp.m_ptr;
atp.m_ptr = nullptr;
}
private:
T* m_ptr;
};
可以很容易的看出,该函数将指针所有权从一个对象转移到另外一个对象,且将原对象置空。该函数中std::auto_ptr实际上在运用引用去实现移动语义。但若是在转移所有权后仍去访问前一个对象(现在已经被置为空指针),程序可能会崩溃。
std::auto_ptr<int> atp(new int(10));
std::auto_ptr<int> atp2(atp);
//auto _data = atp.data; //undefined behavior
//此时的atp已经为nullptr,“移动函数”将所有权转接给了另外一个对象
庆幸的是C++ 11中引入了一种叫做右值引用的东西,可运用此实现出转移构造函数。如今std::auto_ptr已经被std::unique_ptr所替代
unique_ptr(unique_ptr&& unip)
{
m_ptr = unip.m_ptr;
unip.m_ptr = nullptr;
}
虽然说这个函数与std::auto_ptr中的做法一样,但它只能接受右值作为参数。传递右值,即为转换指针所有权
std::unique_ptr<int> a(new int(10)); //okay
std::unique_ptr<int> b(a); //error
与std::shared_ptr不同,该智能指针用于不能被多个实例共享的内存管理,也就是说,仅有一个实例拥有内存所有权。
对于智能指针而言,声明后会默认置为空。建议使用std::make_unique来替代直接使用new创建实例(C++ 14中加入此方法)。若在new时期智能指针指向的类(也就是模板接受到的参数)抛出了异常,或是类的构造函数失效,这样就会导致new出来的对象被漏掉,导致内存泄漏。
std::unique_ptr<int> unip1; //默认置空
std::unique_ptr<int> unip2(new int(5)); //旧方法
std::unique_ptr<int> unip3 = std::make_unique<int>(10); //C++ 14新规范
//std::unique_ptr<int> unip3 = std::make_unique<int>(new int(10)); //迷惑操作
std::unique_ptr<std::string> unip4 = std::make_unique<std::string>("Pointer");
std::array<int, 10> arr = { 1,2,3,4,5,6,7,8,9,0 };
std::unique_ptr<std::array<int, 10>> unip5 = std::make_unique<std::array<int, 10>>(arr);
提几点make的不足
1.make函数都不允许使用定制删除器,删除器只能在std::unique_ptr或std::shared_ptr的构造函数中传入
2.make函数不能完美的传递initializer_list
//auto unip = std::make_unique<std::vector<int>>({ 1,2,3 }); //错误
std::initializer_list<int> il = { 1,2,3 };
auto unip = std::make_unique<std::vector<int>>(il); //正确
3.当构造函数是私有或者保护时,无法make(只有在编译阶段才会给出报错,而若使用new的旧方法,在敲代码阶段编译器便会指出参数的构造函数不可访问),因为make实际上是类似调用到了构造函数。解决方法是在类中写一个静态成员函数,该函数会创建一个智能指针并返回它
4.对象的内存可能无法及时回收(std::shared_ptr)
要理解这一点首先得知道std::make_shared和直接调用构造函数的内存分配机理
对于std::shared_ptr而言,内部维护了两个指针指向资源对象和控制块,前者即就是智能指针管理的对象,后者专门用于记录引用次数。而若直接采用构造函数分配,只能单独分配控制块,即就是对象与控制块处于两个内存中
![内存](https://img2020.cnblogs.com/blog/2162360/202010/2162360-20201023112120863-2020606159.png)
而若采用std::make_shared方法,能减少内存分配次数,控制块与资源将分配到同块内存中,而在程序运行中,内存分配是代价高昂的操作
![](https://img2020.cnblogs.com/blog/2162360/202010/2162360-20201023112525842-714785688.png)
虽然说使用std::make_shared减少了内存分配次数,提高了性能,但是在回收内存的时候存在一点问题。
我们知道,对象与控制块处于同一块内存上,当对象的引用计数降为0时(强引用次数降为0,与弱引用次数无关,std::weak_ptr只是个旁观者),对象被销毁(调用析构函数),但是对象所在的内存并没有被释放,只有当控制块也被销毁时,这块内存才会被释放。控制块中包含了两个计数,一个为强引用(std::shared_ptr的引用计数),一个为弱引用(std::weak_ptr的引用计数),当两个引用计数都归为0时,控制块的内存才能释放。那么也就是说只要还有一个std::weak_ptr指向控制块(弱引用计数 > 0),控制块便不会被销毁,那么也就是说这块内存也不会被释放(即使std::shared_ptr已经离开作用域了)。
总的来说,通过std::make_shared分配出来的这块内存,只有当最后一个std::shared_ptr与std::weak_ptr都被销毁时,才会被释放
回到std::unique_ptr的构造上来
需要注意的是,接受指针参数的智能指针是explicit的,因此我们不能将一个内置指针隐式转换为一个智能指针
//std::unique_ptr<int> unip = new int(10); //错误
同时std::unique_ptr不允许左值赋值,即不允许拷贝。但是可以通过移动语义转移所有权
std::unique_ptr<int> unip = std::make_unique<int>(10);
//std::unique_ptr<int> copy_unip = unip;
//std::unique_ptr<int> copy_unip(unip);
std::unique_ptr<int> move_unip = std::move(unip); //unip被置空,所有权转到move_unip上
但我们可以拷贝或是赋值一份即将销毁的std::unique_ptr,最常见的例子是从函数返回一个std::unique_ptr
std::unique_ptr<int> clone()
{
std::unique_ptr<int> unip = std::make_unique<int>(10);
return unip; //编译器知道返回的对象即将被销毁,于是编译器执行了一次“特殊的”拷贝
}
除了利用std::make_unique的方法,还可以运用变参模板,简单实现make_unique_SelfCode
//不支持处理数组
//为什么?因为太复杂 我不会
template<typename T, typename ... Ts>
std::unique_ptr<T> make_unique_Selfcode(Ts ... args)
{
return std::unique_ptr<T> {new T{ std::forward<Ts>(args) ... }}; //泛型工厂函数
}
//贴一下完整的make实现 或许以后会分析一下代码实现
// 支持普通指针
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;
C++ 11中包含了花括号初始的方法,叫做initializer_list
小括号调用到了类型的构造函数,包含了隐式转换的规则,而花括号并没有调用到构造函数,而是用到了列表初始化,这种方法当碰到精度降低,范围变窄等情况(narrowing conversion)时,编译器都会给出报错
double num = 3.1415926;
int test1(num); //编译器给出警告,该转换可能会丢失数据
int test2{num}; //编译器给出报错,从double转换到int需要收缩转换
同时,C++ 11也支持使用者在自定义类的初始化时使用列表初始化
class Test
{
public:
Test(const std::initializer_list<int>& i_list)
{
for (auto i : i_list)
vec.emplace_back(i);
}
private:
std::vector<int> vec;
};
int main()
{
Test t{ 1, 2, 3 };
Test t = { 1 ,2 ,3 , 4 };
}
回到std::unique_ptr上, std::unique_ptr对象可以传给左值常量引用常数,因为这样并不会改变内存所有权,也可以使用移动语义进行右值传值
class Test {};
void Change_Right(std::unique_ptr<Test>&& t) {}
void Change_Ref(const std::unique_ptr<Test>& t) {}
int main()
{
auto temp = std::make_unique<Test>();
//Change_Right(temp); //错误 无法将右值引用绑定到左值
Change_Right(std::move(temp));
Change_Ref(temp);
Change_Ref(std::move(temp));
}
对于该智能指针来说,应避免犯以下两个错误(unique与shared同理)
// 1.用同一份指针初始化多个智能指针
int *p = new int(10);
std::unique_ptr<int> unip(p);
std::unique_ptr<int> unip2(p);
// 2.混用普通指针与智能指针
int *p = new int(10);
std::unique_ptr<int> unip(p);
delete p; //程序员在智能指针作用域之前便删除了p,则翻车
std::unique_ptr中有get(), release(), reset()方法
get()可以获得智能指针中管理的资源。release()会返回该其所管理的资源,并释放其所有权。reset()则直接析构其管理的内存,同时reset()还可以接受一个内置指针,使原对象指向新的内存
std::unique_ptr<float> unip = std::make_unique<float>(10);
std::unique_ptr<float> unip2 = std::make_unique<float>(20);
//unip.reset(unip2.get()) //绝对禁止的操作,会导致挂起引用
unip.reset(unip2.release()); //unip析构其管理的内存后,接收unip2转接过来的所有权
unip = std::move(unip2); //与上一行作用相同
说到了reset(),就应该顺便扯一下智能指针的自定义删除器
首先std::shared_ptr的构造与std::unique_ptr存在一点差异。std::unique_ptr支持动态数组,而std::share_ptr不支持动态数组(C++ 17之前)。
std::unique_ptr<int[]> unip(new int[10]); //合法
std::shared_ptr<int[]> shrp(new int [10]); //在C++ 17之前时非法操作,不能传入数组类型作为模板参数
std::shared_ptr<int> shrp2(new int[10]); //可编译,但存在未定义行为
所以,std::share_ptr会使用delete p来释放资源,这对于new int[10] 而言肯定是非法的,对它应使用delete[] p
对此我们有两种解决方法,一种是传入std::default_delete,另外一种是自行构造删除器
std::shared_ptr<int> p(new int[10], std::default_delete<int[]>());
std::shared_ptr<int> shrp2(new int[10], [](int* p) {delete[] p; }); //lambda表达式转换为函数指针
void deleter(int *p)
{
delete[] p;
}
std::shared_ptr<int> shrp2(new int[10], deleter);
//还可以封装一个make_shared_SelfCode的方法
template<typename T>
auto make_shared_SelfCode(size_t size)
{
return std::shared_ptr<T>(new T[size], std::default_delete<T[]>());
}
//允许使用auto作为模板函数的返回值
std::shared_ptr<int> shrp3 = make_shared_SelfCode(10);
虽然说能用了,但存在着几个缺点
1.我们想管理的是int[]类型,但传入的参数模板却为int
2.需要显示提供删除器
3.无法使用make_shared,无法保证异常安全
4.由于没有重载operator[],故需要使用shrp.get().[Index]来获取数组中的元素
在C++ 17中,std::shared_ptr重载了[]运算符,并支持传入int[]类型作为模板参数
std::shared_ptr<int[]> shrp(new int[10]);
shrp[0] = 5;
那么还剩下一个问题,我们还不能使用std::make_shared去实例化指针。所以在C++2a中,新规定便解决了这个问题(虽然我的编译器跑不了)
std::shared_ptr<int[]> shrp = std::make_shared<int[]>(10);
//分配一个管理有10个int元素的动态数组的std::shared_ptr
//同理也有
std::unique_ptr<int[]> unip = std::unique_ptr<int[]>(10);
对于std::shared_ptr来说,自定义一个删除器是比较简单的,而对于std::unique_ptr来说,情况有点不同
std::shared_ptr<int> shrp(new int(10), [](int* p) {delete p; }); //正确
//std::unique_ptr<int> unip(new int(10), [](int* p) {delete p; }); //错误
std::unique_ptr在指定删除器时需要在模板参数中传入删除器的类型
std::unique_ptr<int,void(*)(int*)> unip(new int(10), [](int* p) {delete p; });
如果要在lambda表达式中捕获变量,则编译器将会报错,原因是捕获了变量的lambda表达式无法转换成函数指针,所以我们可以使用std::function来进行包装
//std::unique_ptr<int,void(*)(int*)> unip(new int(10), [&](int* p) {delete p; }); //错误
std::unique_ptr<int, std::function<void(int*)>> unip(new int(10), [&](int* p) {delete p; });
除了用lambda表达式,我们还可以这么干
template<typename T>
void deleter(T* p)
{
delete[] p;
}
//使用decltype类型推断
std::unique_ptr<int, decltype(deleter<int>)*> unip(new int(10), deleter<int>);
//使用typedef取别名
typedef void(*deleter_int)(int*);
std::unique_ptr<int, deleter_int> unip(new int(10), deleter<int>);
//联合typedef与decltype
typedef decltype(deleter<int>)* deleter_int_TD;
std::unique_ptr<int, deleter_int_TD> unip(new int(10), deleter<int>);
//使用using
using deleter_int_U = void(*)(int*);
std::unique_ptr<int, deleter_int_U> unip(new int(10), deleter<int>);
std::shared_ptr
std::shared_ptr与std::unique_ptr类似,都建议采用make方法创建智能指针对象(C++ 11支持)。std::shared_ptr与std::unique_ptr的主要区别在于前者是使用引用计数的智能指针。引用计数的智能指针可以跟踪引用同一个真实指针对象的智能指针实例的数目。这意味着,可以有多个std::shared_ptr实例可以指向同一块动态分配的内存,当最后一个引用对象离开其作用域时,才会释放这块内存。另一个区别为对C风格数组的支持,前文已经提过。
与std::unique_ptr不同,std::shared_ptr支持拷贝构造,也允许左值赋值
std::shared_ptr<int> shrp = std::make_shared<int>(10);
std::shared_ptr<int> shrp2 = shrp;
std::shared_ptr<int> shrp3(shrp2);
std::cout << shrp.use_count() << std::endl; // 3
使用std::shared_ptr管理第三方库
第三方库通常会通过接口提供原始指针,用来管理其中的内存,既然是原始指针,就总会出现忘记使用库中的释放函数,导致内存泄露的情况
void* p = GetHandle()->Create();
//Codes..
//GetHandle()->Release(p); //程序员由于不可抗力导致漏写这行代码了
那么使用智能指针去维护就会显得非常方便(lambda表达式中的this只能在类中成员函数才能使用),以下仅提供代码思路
void* p = GetHandle()->Create();
std::shared_ptr<void> shrp(p, [this](void* p){ GetHandle()->Release(p); });
包装成一个函数
std::shared_ptr<void> GetShared(void* p)
{
std::shared_ptr<void> shrp(p, [this](void* p){ GetHandle()->Release(p); });
return shrp;
}
void* p = GetHandle()->Create();
auto shrp = GetShared(p);
注意用GetShared返回出来的智能指针必须得被一个左值所接受,否则该对象将会被释放,导致后续代码去访问野指针的内容
//使用宏创建
#define GetShared(p) std::shared_ptr<void> shared_##p(p, [](void*p){ GetHandle()->Release(p);});
void* p = GetHandle()->Create();
GetShared(p);
//以上代码相当于 //胶水宏把shared_与参数粘在了一起,成为一个变量名
std::shared_ptr<void> shared_p(p, [](void*p){ GetHandle()->Release(p);});
可以使用use_count()方法来查看std::shared_ptr被引用的次数,而unique()则可以返回一个bool值,用于判断是否是唯一指向当前内存的std::shared_ptr。set()函数则可以释放关联内存块的所有权(相当于std::unique_ptr的release() ),如果是最后一个指向该资源的std::shared_ptr,就释放这块内存。
插一嘴,三种智能指针都有swap(),主要用于交换两个同种类型的指针对象
再来看一眼上文中使用指针指针应该避免犯的错的第一点
int *p = new int(10);
std::shared_ptr<int> shrp(p);
std::shared_ptr<int> shrp2(p);
//导致同一片地址的资源被释放两次
这是因为这两个智能指针是独立初始化的,它们之间并没有通讯共享引用计数。std::shared_ptr的内部实际上使用两个指针,一个用于管理实际的指针,另外一个则指向一个控制块,其中记录了有哪些std::shared_ptr共同管理同一片内存。这是在初始化完成的,所以如果单独初始化两个对象,尽管管理的是同一块内存,它们各自的”控制块“没有互相记录的。但是如果是使用复制构造函数还有赋值运算时,控制块将会同步更新,这样就达到了引用计数的目的。
std::shared_ptr的 “ 死锁 ” 以及 “ 自锁 ”
//照搬知乎上的代码
class Person
{
public:
Person(const string& name):
m_name{name}
{
cout << m_name << " created" << endl;
}
virtual ~Person()
{
cout << m_name << " destoryed" << endl;
}
friend void partnerUp(std::shared_ptr<Person>& shrp1, std::shared_ptr<Person>& shrp2)
{
if (!shrp1 || !shrp2)
return;
shrp1->m_partner = shrp2;
shrp2->m_partner = shrp1;
}
private:
string m_name;
std::shared_ptr<Person> m_partner;
};
int main()
{
{
auto shrp1 = std::make_shared<Person>("Lucy");
auto shrp2 = std::make_shared<Person>("Ricky");
partnerUp(shrp1, shrp2); // 互相设为伙伴
}
return 0;
}
运行之后可以发现控制台只输出了两行created字符,也就是说对象并没有被析构,导致了内存的泄漏。当作用域结束时,理应执行析构函数,但是当析构shrp1时,却发现shrp2内部引用了shrp1,那么就得先析构shrp2,但是发现shrp1中内部又引用了shrp1,互相引用导致了 “ 死锁 ” ,最终导致内存泄漏。“ 自锁 ” 同理。
auto shrp = std::make_shared<Person>("Lucy");
partnerUp(shrp, shrp);
那么这种情况下就需要使用到std::weak_ptr了
std::weak_ptr
std::weak_ptr能够引用std::shared_ptr中的内存,但是它只作为旁观者的存在,并不享有内存的所有权,也就是说使用std::weak_ptr去接受一个std::shared_ptr,并不会增加引用计数,当然也不会影响到std::shared_ptr的析构,有效的阻止了 “ 死锁 ”, “ 自锁 ” 的问题
class Person
{
//Codes..
private:
string m_name;
std::weak_ptr<Person> m_partner; //此时程序能够正常析构了
};
std::weak_ptr一般与std::shared_ptr搭配使用,使用lock()可以返回一个他所监视的std::shared_ptr。使用expired()可以返回一个bool值,若管理对象已删除则返回false,否则返回true
std::shared_ptr<int> shrp = std::make_shared<int>(10);
std::weak_ptr<int> wkp = shrp;
auto shrp2 = wkp.lock();
std::cout<< shrp2.use_count() << std::endl; // 2