【C++】 C++11 智能指针
一、智能指针概述
-
C++程序设计中使用堆内存是非常频繁的操作,堆内存的申请和释放都由程序员自己管理。
-
程序员自己管理堆内存可以提高了程序的效率,但是整体来说堆内存的管理是麻烦的,C++11中引入了智能指针的概念,方便管理堆内存
-
使用普通指针,容易造成堆内存泄露(忘记释放),二次释放,程序发生异常时内存泄露等问题等,使用智能指针能更好的管理堆内存
C++
里面的四个智能指针: auto_ptr
, unique_ptr
,shared_ptr
, weak_ptr
其中后三个是C++11
支持,并且第一个已经被C++11
弃用。
二、shared_ptr 共享指针
2.1 shared_ptr 基本原理
-
std::shared_ptr
使用引用计数,每一个shared_ptr
的拷贝都指向相同的内存。再最后一个shared_ptr
析构的时候,内存才会被释放。 -
shared_ptr
共享被管理对象,同一时刻可以有多个shared_ptr
拥有对象的所有权,当最后一个shared_ptr对象销毁时,被管理对象自动销毁。
简单来说,shared_ptr实现包含了两部分:
- 一个指针是所管理的数据的地址
- 一个指针是控制块的地址
- 包括引用计数
- weak_ptr 计数
- 删除器 (Deleter)
- 分配器 (Allocator)
因为不同shared_ptr
指针需要共享相同的内存对象,因此引用计数的存储是在 堆上的。
2.2 shared_ptr 的基本用法
2.2.1 初始化
通过构造函数、std::shared_ptr
辅助函数和reset
方法来初始化shared_ptr
,代码如下:
void test1(){// 智能指针初始化std::shared_ptr<int> p1(new int(1));std::shared_ptr<int> p2 = p1;std::shared_ptr<int> p3;p3.reset(new int(1));if(p3) {std::cout << "p3 is not null" << std::endl;}
}
我们应该优先使用make_shared
来构造智能指针,因为它更高效。
std::shared_ptr<int> sp = std::make_shared<int>(100);
通过reset()
方法,如果没有传入指针,表示将当前管理的指针引用计数减一;如果传入指针,代表减少之前的指针的引用计数,并且管理新的指针,将新的指针的引用计数变为1:
std::shared_ptr<int>sp = std::make_shared<int>(100);
sp.reset();
sp.reset(new int(200));
不能将一个原始指针直接赋值给一个智能指针,例如,下面这种方法是错误的:
std::shared_ptr<int> p = new int(1);
下面是一个使用shared_ptr
的例子,使用use_count()
观察它引用计数的变化:
void test2()
{std::shared_ptr<int> p1;p1.reset(new int(1));std::shared_ptr<int> p2 = p1;// 引用计数此时应该是2std::cout << "p2.use_count() = " << p2.use_count() << std::endl;p1.reset();std::cout << "p1.reset()" << std::endl;;// 引用计数此时应该是1std::cout << "p2.use_count()= " << p2.use_count() << std::endl;if (!p1){std::cout << "p1 is empty" << std::endl;}if (!p2){std::cout << "p2 is empty" << std::endl;}p2.reset();// 引用计数此时应该是0std::cout << "p2.reset()" << std::endl;std::cout << "p2.use_count()= " << p2.use_count() << std::endl;if (!p2){std::cout << "p2 is empty" << std::endl;}
}
2.2.2 获取原始指针
当需要获取原始指针时,可以通过get方法来返回原始指针,代码如下所示:
void test3(){std::shared_ptr<int> ptr(new int(1));int *p = ptr.get();delete p; //注意,这里将重复释放内存,导致程序崩溃
}
p.get()的返回值就相当于一个裸指针的值,不合适的使用这个值,上述陷阱的所有错误都有可能发生,遵守以下几个约定:
- 不要保存
p.get()
的返回值 ,无论是保存为裸指针还是shared_ptr
都是错误的 - 保存为裸指针不知什么时候就会变成空悬指针,保存为
shared_ptr
则产生了独立指针 - 不要
delete p.get()
的返回值 ,会导致对一块内存delete
两次的错误
2.2.3 指定删除器
如果用shared_ptr
管理非new
对象或是没有析构函数的类时,应当为其传递合适的删除器,下面是一个使用自定义删除器的例子,这个删除器只要是可以可调用对象即可。
void DeleteIntPtr(int *p) {std::cout << "call DeleteIntPtr" << std::endl;delete p;
}struct DeleteIntPtr2{void operator()(int *p) {std::cout << "call DeleteIntPtr2" << std::endl;delete p;}
};void test4(){std::shared_ptr<int> ptr(new int(1), DeleteIntPtr);std::shared_ptr<int> ptr2(new int(1), DeleteIntPtr2());std::shared_ptr<int> ptr3(new int(1), [](int *p){std::cout << "call DeleteIntPtr3" << std::endl;delete p;});
}
当我们用shared_ptr管理动态数组时,需要指定删除器,因为shared_ptr
的默认删除器不支持数组对象(C++17
之前),代码如下所示:
std::shared_ptr<int> p3(new int[10], [](int *p) { delete [] p;});
2.2.4 使用shared_ptr要注意的问题
不要用一个原始指针初始化多个shared_ptr,例如下面错误范例:
int *ptr = new int;
shared_ptr<int> p1(ptr);
shared_ptr<int> p2(ptr); // 逻辑错误
不要在函数实参中创建shared_ptr,对于下面的写法:
function(shared_ptr<int>(new int), g()); //有缺陷
因为C++
的函数参数的计算顺序在不同的编译器不同的约定下可能是不一样的,一般是从右到左,但也可能从左到右,所以,可能的过程是先new int
,然后调用g()
,如果恰好g()
发生异常,而shared_ptr
还没有创建, 则int
内存泄漏了,正确的写法应该是先创建智能指针,代码如下:
shared_ptr<int> p(new int);
function(p, g());
通过shared_from_this()
返回this
指针:不要将this指针作为shared_ptr
返回出来,因为this
指针本质上是一个裸指针,因此,这样可能会导致重复析构,看下面的例子。
class A
{
public:std::shared_ptr<A> GetSelf(){return std::shared_ptr<A>(this); // 不要这么做}~A(){std::cout << "Deconstruction A" << std::endl;}
};void test5(){std::shared_ptr<A> ptr(new A());std::shared_ptr<A> ptr2 = ptr->GetSelf(); //此时会多次释放内存
}
在这个例子中,由于用同一个指针(this)构造了两个智能指针sp1和sp2,而他们之间是没有任何关系的,在离开作用域之后this将会被构造的两个智能指针各自析构,导致重复析构的错误。
-
正确返回
this
的shared_ptr
的做法是:让目标类通过std::enable_shared_from_this
类,然后使用基类的成员函数shared_from_this()
来返回this
的shared_ptr
,如下所示: -
因为
std::enable_shared_from_this
类中有一个weak_ptr
,这个weak_ptr
用来观察this
智能指针,调用shared_from_this()
方法是,会调用内部这个weak_ptr
的lock()
方法,将所观察的shared_ptr
返回,避免了重新构造一个shared_ptr
class A : public std::enable_shared_from_this<A>
{
public:std::shared_ptr<A> GetSelf(){return shared_from_this(); // 正确的方式}~A(){std::cout << "Deconstruction A" << std::endl;}
};void test5(){std::shared_ptr<A> ptr(new A());std::shared_ptr<A> ptr2 = ptr->GetSelf(); std::cout << "ptr.use_count() = " << ptr.use_count() << std::endl;
}
避免循环引用。循环引用会导致内存泄漏,比如:
class AA;
class BB;class AA{
public:std::shared_ptr<BB> bb;~AA(){std::cout << "Deconstruction AA" << std::endl;}
};class BB{
public:std::shared_ptr<AA> aa;~BB(){std::cout << "Deconstruction BB" << std::endl;}
};void test6(){std::shared_ptr<AA> aa(new AA());std::shared_ptr<BB> bb(new BB());aa->bb = bb;bb->aa = aa;
}
循环引用导致ap和bp的引用计数为2,在离开作用域之后,aa和bb的引用计数减为1,并不回减为0,导致两个指针都不会被析构,产生内存泄漏。
解决的办法是把A和B任何一个成员变量改为weak_ptr,具体方法见weak_ptr章节
三、unique_ptr 独占指针
3.1 unique_ptr 基本原理
-
unique_ptr
是一个独占型的智能指针,它不允许其他的智能指针共享其内部的指针,不允许通过赋值将一个unique_ptr
赋值给另一个unique_ptr
。 -
unique_ptr
只有一个指针成员,指向所管理的数据的地址。因此一个shared_ptr
对象的大小是它大小的两倍。
std::cout << sizeof(std::shared_ptr<int>) << std::endl; // 8
std::cout << sizeof(std::unique_ptr<int>) << std::endl; // 4
3.2 unique_ptr 的基本使用
3.2.1 unique_ptr 的初始化
unique_ptr
不可以复制和引用,同一时间只能被独占:
void test1(){unique_ptr<int> my_ptr(new int(10));unique_ptr<int> my_other_ptr = my_ptr; // 报错,不能复制
}
unique_ptr不允许复制,但可以通过函数返回给其他的unique_ptr,还可以通过std::move来转移到其他的unique_ptr,这样它本身就不再拥有原来指针的所有权了:
void test1(){unique_ptr<int> my_ptr(new int(10));// unique_ptr<int> my_other_ptr = my_ptr; // 报错,不能复制unique_ptr<int> my_other_ptr = std::move(my_ptr); //正确,移动语义cout << *my_other_ptr << endl;
}
可以使用new
的方式初始化unique_ptr
,或在C++14
之后使用std::make_unique<T>()
来构造
std::unique_ptr<int> my_ptr(new int(10));
std::unique_ptr<int> my_ptr2 = std::make_unique<int>(10);
除了unique_ptr的独占性, unique_ptr和shared_ptr还有一些区别,比如:
- unique_ptr可以指向一个数组,代码如下所示,在
C++17
之后引入了对shared_ptr
的数组支持
void test2(){std::unique_ptr<int []> ptr(new int[10]);ptr[9] = 9;std::shared_ptr<int []> ptr2(new int[10]); //这个是不合法的,C++17以上支持ptr2[9] = 9;std::cout << "ptr[9] = " << ptr[9] << std::endl;std::cout << "ptr2[9] = " << ptr2[9] << std::endl;
}
- unique_ptr指定删除器和shared_ptr有区别
void test3(){std::shared_ptr<int> ptr1(new int(1), [](int *p){delete p;}); // 正确std::unique_ptr<int> ptr2(new int(1), [](int *p){delete p;}); // 错误,unique_ptr不能自定义删除器
}
unique_ptr需要确定删除器的类型,所以不能像shared_ptr那样直接指定删除器,可以这样写:
void test3(){std::shared_ptr<int> ptr1(new int(1), [](int *p){std::cout << " shared_ptr delete p" << std::endl;delete p;}); // 正确// std::unique_ptr<int> ptr2(new int(1), [](int *p){delete p;}); // 错误,unique_ptr不能自定义删除器std::unique_ptr<int, void(*)(int*)> ptr2(new int(1), [](int *p){ std::cout << " unique_ptr delete p" << std::endl;delete p;}); //正确
}
四、weak_ptr 弱引用指针
4.1 weak_ptr 原理
-
weak_ptr 是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象. 进行该对象的内存管理的是那个强引用的shared_ptr, weak_ptr只是提供了对管理对象的一个访问手段。
-
weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从一个shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少
-
weak_ptr是用来解决shared_ptr相互引用时的死锁问题,如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。它是对对象的一种弱引用,不会增加对象的引用计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr
-
weak_ptr没有重载操作符*和->,因为它不共享指针,不能操作资源,主要是为了通过shared_ptr获得资源的监测权,它的构造不会增加引用计数,它的析构也不会减少引用计数,纯粹只是作为一个旁观者来监视shared_ptr中管理的资源是否存在。
-
weak_ptr还可以返回this指针和解决循环引用的问题
4.2 weak_ptr的基本用法
通过use_count()方法获取当前观察资源的引用计数,如下所示:
void test1(){shared_ptr<int> sp(new int(10));weak_ptr<int> wp(sp);cout << wp.use_count() << endl; //结果为1,不增加引用计数
}
通过expired()方法判断所观察资源是否已经释放,如下所示:
void test2(){shared_ptr<int> sp(new int(10));weak_ptr<int> wp(sp);if(wp.expired()){cout << "weak_ptr无效,资源已释放" << std::endl;}else{cout << "weak_ptr有效" << std::endl;}sp.reset();if(wp.expired()){cout << "weak_ptr无效,资源已释放" << std::endl;;}else{cout << "weak_ptr有效" << std::endl;;}
}
通过lock方法获取监视的shared_ptr,如下所示:
std::weak_ptr<int> gw;
void f()
{if(gw.expired()) {cout << "gw无效,资源已释放" << std::endl;}else {auto spt = gw.lock();cout << "gw有效, *spt = " << *spt << endl;}}
void test3(){{std::shared_ptr<int> sp(new int(10));gw = sp;f();}f();
}
4.3 weak_ptr返回this指针
上述shared_ptr
中,通过继承std:::enable_shared_from_this<>
类就可以解决返回this
指针的问题,原因就是基类std:enable_shared_from_this<A>
中有一个std::weak_ptr
观察当前的this
指针,构造一个shared_ptr
指向它,此时原来的shared_ptr
引用计数就会+1
,这样就不会多次构造出shared_ptr
了
class A : public std::enable_shared_from_this<A>
{
public:std::shared_ptr<A> GetSelf(){return shared_from_this(); // 正确的方式}~A(){std::cout << "Deconstruction A" << std::endl;}
};void test5(){std::shared_ptr<A> ptr(new A());std::shared_ptr<A> ptr2 = ptr->GetSelf(); std::cout << "ptr.use_count() = " << ptr.use_count() << std::endl;
}
4.4 weak_ptr解决循环引用问题
在shared_ptr章节提到智能指针循环引用的问题,因为智能指针的循环引用会导致内存泄漏,可以通过weak_ptr解决该问题,只要将A或B的任意一个成员变量改为weak_ptr,因为weak_ptr
不增加引用计数,这样就可以打破引用循环,内存得到正确的释放
class AA;
class BB;class AA{
public:std::weak_ptr<BB> bb; //修改其中一个为weak_ptr~AA(){std::cout << "Deconstruction AA" << std::endl;}
};class BB{
public:std::shared_ptr<AA> aa; ~BB(){std::cout << "Deconstruction BB" << std::endl;}
};void test4(){std::shared_ptr<AA> aa(new AA());std::shared_ptr<BB> bb(new BB());aa->bb = bb;bb->aa = aa;
}
4.5 weak_ptr使用注意事项
weak_ptr在使用前需要检查合法性。
weak_ptr<int> wp;
{shared_ptr<int> sp(new int(1)); //sp.use_count()==1wp = sp; //wp不会改变引用计数,所以sp.use_count()==1shared_ptr<int> sp_ok = wp.lock(); //wp没有重载->操作符。只能这样取所指向的对象
}
shared_ptr<int> sp_null = wp.lock(); //sp_null .use_count()==0;
- 因为上述代码中sp和sp_ok离开了作用域,其容纳的K对象已经被释放了。得到了一个容纳NULL指针的sp_null对象
- 在使用wp前需要调用wp.expired()函数判断一下,因为wp还仍旧存在,虽然引用计数等于0,仍有某处“全局”性的存储块保存着这个计数信息。
- 直到最后一个weak_ptr对象被析构,这块“堆”存储块才能被回收。否则weak_ptr无法直到自己所容纳的那个指针资源的当前状态。
如果shared_ptr sp_ok和weak_ptr wp;属于同一个作用域呢?如下所示:
weak_ptr<int> wp;
shared_ptr<int> sp_ok;
{shared_ptr<int> sp(new int(1)); //sp.use_count()==1wp = sp; //wp不会改变引用计数,所以sp.use_count()==1sp_ok = wp.lock(); //引用计数+1,变成2
}
//离开作用域,引用计数变为1
if(wp.expired()) { cout << "shared_ptr is destroy" << endl;
}
else {cout << "shared_ptr no destroy" << endl;
}
五、shared_ptr线程安全问题
5.1 使用同一个shared_ptr
引用计数本身是安全的,至于智能指针是否安全需要结合实际使用分情况讨论:
情况1:多线程代码操作的是同一个shared_ptr的对象,此时是不安全的,比如std::thread的回调函数,是一个lambda表达式,其中引用捕获了一个shared_ptr
std::thread td([&sp1]()){....});
又或者通过回调函数的参数传入的shared_ptr对象,参数类型引用
void fn(shared_ptr<A>&sp) {...
}
...
std::thread td(fn, sp1);
这时候必然不是线程安全的
5.2 使用不同的shared_ptr
这里指的是管理的数据是同一份,而shared_ptr不是同一个对象,比如多线程回调的lambda的是按值捕获的对象。
std::thread td([sp1]()){....});
另个线程传递的shared_ptr是值传递,而非引用:
void fn(shared_ptr<A>sp) {...
}
...
std::thread td(fn, sp1);
这时候每个线程内看到的sp,他们所管理的是同一份数据,用的是同一个引用计数。但是各自是不同的对象,当发生多线程中修改sp指向的操作的时候,是不会出现非预期的异常行为的。也就是说,如下操作是安全的。
void fn(shared_ptr<A>sp) {...if(..){sp = other_sp;}else {sp = other_sp2;}
}
需要注意:所管理数据的线程安全性问题。显而易见,所管理的对象必然不是线程安全的,必然 sp1、sp2、sp3智能指针实际都是指向对象A, 三个线程同时操作对象A,那对象的数据安全必然是需要对象A自己去保证。
更多资料:https://github.com/0voice