1.什么是智能指针?
智能指针是一个类,这个类的构造函数中传入一个普通指针,析构函数中释放传入的指针。智能指针的类都是栈上的对象,所以当函数(或程序)结束时通过调用析构函数会自动被释放。
智能指针的作用就是为了能更容易和更安全地使用动态内存,减少内存泄漏(忘记释放)的情况发生。(有点像Java虚拟机的作用)
2.智能指针的种类
智能指针在C++11版本之后提供,包含在头文件<memory>中,shared_ptr、unique_ptr、weak_ptr三种。
2.1 shared_ptr的使用
shared_ptr多个指针指向相同的对象。shared_ptr使用引用计数,引用计数就是标记同一块内存同时有多少个shared_ptr智能指针对象指向这个个地址;通过赋值、拷贝等可以让引用计数增加;当一个指向某个内存的智能指针对象被销毁的时候,指向这个内存的所有智能指针的引用计数都减一;当只有一个智能指针对象指向一个内存地址(引用计数为1)也要被销毁的时候,该内存地址就会被系统自动回收。(其实是shared_ptr的析构函数所做的工作是:先将与该智能指针对象共享同一内存地址的所有智能指针对象的引用计数减一[其实,这个引用计数就是在该共享内存上面存着的,只要其中一个对象去修改,那么所有指向它的对象的引用计数就全都改了],减一之后判断是不是0,如果大于零,就不管别的只用销毁该对象即可;如果等于0,说明已经没有任何对象指向该内存空间,所以就要进行堆空间的回收)
shared_ptr内部的引用计数是线程安全的,但是对象的读取需要加锁。
- 初始化。智能指针是个模板类,可以指定类型,传入指针通过构造函数初始化。也可以使用make_shared函数初始化。不能将指针直接赋值给一个智能指针,一个是类,一个是指针。例如std::shared_ptr<int> p4 = new int(1);的写法是错误的;
- 拷贝和赋值。拷贝使得对象的引用计数增加1,赋值使得原对象引用计数减1,当计数为0时,自动释放内存。后来指向的对象引用计数加1,指向后来的对象。
- get函数获取原始指针
- 注意不要用一个原始指针初始化多个shared_ptr,否则会造成二次释放同一内存
- 注意避免循环引用,shared_ptr的一个最大的陷阱是循环引用,循环引用会导致堆内存无法正确释放,导致内存泄漏。
int a = 10;
shared_ptr<int> ptra = make_shared<int>(a);
shared_ptr<int> ptra2(ptra);//拷贝使对象的引用计数加1
cout << ptra.use_count() << " " << ptra2.use_count() << endl; //引用计数都是2
int b = 20;
int *pb = &b;
//shared_ptr<int> ptrb = pb;//error:不能将一个指针直接赋值给一个智能指针(即不能使用拷贝初始化的形式)
shared_ptr<int> ptrb(pb);//正确:可以使用直接初始化的形式
shared_ptr<int> ptrb = make_shared<int>(b);
ptra2 = ptrb; //赋值操作使得原对象引用计数减一,后来对象引用计数加一,即:ptra2的原对象ptra引用计数减一,ptrb引用计数加一
cout << "ptrb的引用计数:" << ptrb.use_count() << endl;//2
cout << "ptra的引用计数:" << ptra.use_count() << endl;//1
cout << "ptra2的引用计数:" << ptra2.use_count() << endl;//2,现在的ptra指向的是ptrb,所以引用计数跟ptrb是一样的
//shared_ptr 与 new 的结合使用
shared_ptr<int> p1(new int(42));//正确:可以使用直接初始化的形式
//shared_ptr<int> p1 = new int(42);//错误:不能使用拷贝初始化的形式将一个指针直接赋值给一个智能指针
2.2 unique_ptr的使用
unique_ptr“唯一”拥有其所指对象,同一时刻只能有一个unique_ptr指向给定对象(通过禁止拷贝语义、只有移动语义来实现)。unique_ptr指针本身的生命周期:从unique_ptr指针创建时开始,直到离开作用域。离开作用域时,若其指向对象,则将其所指对象销毁(默认使用delete操作符,用户可指定其他操作)。unique_ptr指针与其所指对象的关系:在智能指针生命周期内,可以改变智能指针所指对象,如创建智能指针时通过构造函数指定、通过reset方法重新指定、通过release方法释放所有权、通过移动语义转移所有权。
//初始化unique_ptr必须使用直接初始化方式,不能使用拷贝初始化方式,这点跟shared_ptr一样
unique_ptr<int> uptr(new int(21));//绑定动态对象
int c = 321;
int* d = &c;
unique_ptr<int> uptr1(d);//直接初始化
//unique_ptr<int> uptr2 = uptr;//不能赋值
//unique_ptr<int> uptr2(uptr);//不能拷贝
//将所有权从uptr转移给uptr3
//unique_ptr<int> uptr3(uptr.release());//release将uptr置为空
unique_ptr<int> uptr3 = move(uptr);//使用move转换所有权,同上一句
//将所有权从uptr1转换给uptr3(注意:这里的uptr3是有指向的内存的,不为空)
uptr3.reset(uptr1.release());//reset释放了uptr3原来指向的内存
auto p = uptr3.release();
delete p;
2.3 weak_ptr的使用
weak_ptr是为了配合shared_ptr而引入的一种智能指针,它的最大作用在于协助shared_ptr工作,像旁观者那样观测资源的使用情况。
weak_ptr不能控制它所指向对象的生存期。将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。一旦最后一个指向对象的shared_ptr被销毁,那么对象就会被释放。
weak_ptr可以从一个shared_ptr或者另一个weak_ptr对象构造,获得资源的观测权。但weak_ptr没有共享资源,它的构造不会引起指针引用计数的增加。使用weak_ptr的成员函数use_count()可以观测资源的引用计数,另一个成员函数expired()的功能等价于use_count()==0,但更快,表示被观测的资源(也就是shared_ptr的管理的资源)已经不复存在。
weak_ptr可以使用一个非常重要的成员函数lock()从被观测的shared_ptr获得一个可用的shared_ptr对象, 从而操作资源。但当expired()==true的时候,lock()函数将返回一个存储空指针的shared_ptr。
std::shared_ptr<int> sh_ptr = std::make_shared<int>(10);
std::cout << sh_ptr.use_count() << std::endl;//输出1:sh_ptr的引用计数是1
std::weak_ptr<int> wp(sh_ptr);//weak_ptr绑定到一个shared_ptr并不会改变引用计数,所以跟sh_ptr的引用计数是一样的
std::cout << wp.use_count() << std::endl;//1
if(!wp.expired()){//wp的引用计数不为0
std::shared_ptr<int> sh_ptr2 = wp.lock();
//get another shared_ptr:lock返回一个指向wp的对象的shared_ptr,然后赋值给sh_ptr2,使得sh_ptr的引用计数加一
*sh_ptr = 100;
std::cout << wp.use_count() << std::endl;//输出2:wp与它绑定的shared_ptr对象的引用计数是对应的
}
3.使用shared_ptr可能出现的循环引用问题
具体的循环引用问题的讲解,参考:C++ 智能指针的循环引用问题
出现循环引用的例子:两个对象,你中有我,我中有你
#include <iostream>
#include <memory>
using namespace std;
class B;
class A
{
public:
shared_ptr<B> m_b;
};
class B
{
public:
shared_ptr<A> m_a;
};
int main()
{
shared_ptr<A> a(new A);
//new出来的A的引用计数此时为1
shared_ptr<B> b(new B);
//new出来的B的引用计数此时为1
a->m_b = b; //B的引用计数增加为2
b->m_a = a; //A的引用计数增加为2
/*b先出作用域,B的引用计数减少为1,不为0,
所以堆上的B空间没有被释放,且B持有的A也没有机会被析构,A的引用计数也完全没减少
a后出作用域,同理A的引用计数减少为1,不为0,所以堆上A的空间也没有被释放*/
}