原文链接:智能指针之shared_ptr
一、shared_ptr类
- 头文件:#include<memory>
- 智能指针,是一个模板。创建智能指针时,必须提供指针所指的类型
- 如果当做提条件判断,则是检测其是否为空
shared_ptr<string> p1; //指向string
shared_ptr<list<int>> p2;//指向int的list
if(p1 && p1->empty())
*p1="h1";
二、shared_ptr类的操作
- shared_ptr类的默认初始化:
- 如果不初始化一个shared_ptr类对象,那么该对象默认初始化为一个空指针
三、make_shared函数
- 最安全的分配和使用动态内存的方法就是调用该函数
- 此函数在内存中动态分配对象并初始化,返回此对象的shared_ptr
//指向一个值为42的int的shared_ptr
shared_ptr<int> p = make_shared<int>(42);
//p2指向一个值为10个'9'的string
shared_ptr<string> p2=make_shared<string>(10, '9');
//p3指向一个值初始化为0的int数
shared_ptr<int> p3 = make_shared<int>()
- 配合auto使用:make_shared函数可以赋值给auto,这样比较简单
auto p=make_shared<vector<string>>();
四、shared_ptr的拷贝、赋值与引用计数
- 引用计数:shared_ptr类所指向的对象都有一个引用计数
- 但对shared_ptr类进行拷贝时,计数器就会增加。例如:当用一个shared_ptr初始化另一个shared_ptr、或者它作为参数传递给一个函数以及作为函数的返回值,它所关联的计数器就会增加
- 当我们给让shared_ptr指向另一个对象或者shared_ptr销毁时,原对象的计数器就会递减
- 一旦一个shared_ptr的计数器为0,就会自动释放该对象的内存
auto p=make_shared<int>(42); //p指向一个引用者
auto q(p); //用p初始化q,那么p所指的对象计数器加1
auto r=make_shared<int>(42);
r=q;
- 将q赋值给r,那么:
- r原来所指的对象引用计数变为0,然后自动释放内存
- q所指的对象的引用计数+1
五、shared_ptr的自动销毁对象内存机制
- 由上面可知,当指向一个对象的最后一个shared_ptr对象被销毁时,shared_ptr类会自动销毁此对象。shared_ptr类是通过析构函数来完成销毁工作的
- 内存浪费:因为只有在销毁掉最后一个shared_ptr时,该指针所指向的内存才会释放,因此如果你忘记了销毁程序不再需要的shared_ptr,程序仍然正在执行,那么就造成内存浪费
六、shared_ptr与作用域的关系
- shared_ptr类所指向的内存何时被释放,与shared_ptr类的生存周期有关
-
演示案例:
- 首先我们定义下面的函数返回一个指向于一个值的share_ptr指针
shared_ptr<Foo> factory(T arg)
{
return make_share<Foo>(arg);//返回一个share_ptr类型的智能指针
}
- 情景一:例如下面函数调用factory函数来生成一个shared_ptr指针,但是p一旦离开了作用域(use_factory函数),那么p指针就失效了,因此p所指向的内存地址也就自动释放了
//函数结束之后,p就自动释放它所指向的对象的内存
void use_factory(T arg)
{
shared_ptr<Foo> p=factory(arg);
}
- 情景二:下面的函数也是 factory函数来生成一个shared_ptr指针,但是p指针通过返回值返回了,所以,如果有另一个shared_ptr指针调用了该函数,那么该p所指向的内存地址不会随着use_factory函数的调用而释放
auto use_factory(T arg)
{
shared_ptr<Foo> p=factory(arg);
return p;
}
七、shared_ptr与new的使用
使用规则:
- ①我们可以使用将shared_ptr类对象指向一个new所申请的动态内存
- ②new申请的动态内存的使用、释放等规则仍然符合shared_ptr类的使用规则
使用语法:
- 因为智能指针的构造函数是explicit的。因此:我们不能将一个内置指针隐式地转换为一个智能指针,必须使用直接初始化形式来初始化一个智能指针
shared_ptr<int> p=new int(1024); //错误
shared_ptr<int> p2(new int(1024)); //正确:使用直接初始化
- 动态内存作为返回值时的使用手法:限于上面的使用语法,一个返回shared_ptr的函数不能在其返回语句中隐式转换为一个普通指针
shared_ptr<int> clone(int p)
{
return new int(p); //错误
}
shared_ptr<int> clone(int p)
{
return shared_ptr<int>(new int(p)); //正确
}
shared_ptr类的其他使用语法:
八、shared_ptr类的函数传参使用
当一个函数的参数是shared_ptr类时,有以下规则:
- 函数的调用是传值调用
- 调用函数时,该shared_ptr类所指向的对象引用计数加1。但是函数调用完成之后,shared_ptr类自动释放,对象的引用计数又减1
void process(shared_ptr<int> ptr){ ... }
shared_ptr<int> p(new int(42)); //初始化一个智能指针对象p
process(p); //p所指的对象引用计数加1
//process函数调用之后,p所指的引用计数减1
int i=*p; //正确
函数参数使用时与new的关系:
- 因为shared_ptr类会在生存周期结束之后,将引用计数减1,当引用计数为0时,会释放内存空间
- 下面是一个特殊的应用场景,需要注意
void process(shared_ptr<int> ptr){ ... }
int *x(new int(1024));
process(x); //错误,不能将int*转换为一个shared_ptr<int>
process(shared_ptr<int>(x)); //合法的,但是process函数返回之后内存会被释放
int j=*x; //错误,x所指的内存已经被释放了
九、get函数的使用
- shared_prt类的get函数返回一个内置指针,指向智能指针所管理的对象
- 此函数的设计情况:我们需要向不能使用智能指针的代码传递一个内置指针
- get函数将内存的访问权限传递给一个指针,但是之后代码不会delete该内存的情况下,对get函数的使用才是最安全的
- 永远不要用get初始化另一个智能指针或者为另一个智能指针赋值
shared_ptr<int> p(new int(42)); //引用计数变为1
int *q=p.get(); //正确:使用q需要注意,不要让它管理的指针被释放
{//新语句块
shared_ptr<int>(q); //用q初始化一个智能指针对象
} //语句块结束之后,智能指针对象释放它所指的内存空间
int foo=*p;//错误的,p所指的内存已经被释放了
十、reset、unique函数的使用
- reset函数会将shared_prt类原先所指的内存对象引用计数减1,并且指向于一块新的内存
shared_ptr<int> p;
p=new int(1024); //错误:不能将一个指针赋予shared_ptr
p=reset(new int(1034)); //正确,p指向一个新对象
- reset函数与unqie函数配合使用:在改变对象之前,检查自己是否为当前对象的唯一用户
shared_ptr<string> p=make_shared<string>("Hello");
if(!p.unique()) //p所指向的对象还有别的智能指针所指
p.reset(new string(*p)); //现在可以放心的改变p了
*p+=newVal; //p所指向的对象只有自己一个智能指针,现在可以放心的改变对象的值了
十一、异常处理
当程序发生异常时,我们可以捕获异常来将资源被正确的释放。但是如果没有对异常进行处理,则有以下规则:
- shared_ptr的异常处理:如果程序发生异常,并且过早的结束了,那么智能指针也能确保在内存不再需要时将其释放
- new的异常处理:如果释放内存在异常终止之后,那么就造成内存浪费、
voif func()
{
shared_ptr<int> sp(new int(42));
...//此时抛出异常,未捕获,函数终止
}//shared_ptr仍然会自动释放内存
voif func()
{
int *ip=new int(42);
...//此时抛出异常,未捕获
delete ip; //在退出之前释放内存,此语句没有执行到,导致内存浪费
}
十二、重置shared_prt类删除器
- 概念:前面介绍过,当shared_ptr生命周期结束时,会调用默认的析构函数来释放(delete)自己所指向的内存空间。但是我们可以使用shared_prt的语法来指定删除器函数,那么在shared_ptr生命周期结束时就会自动调用这个函数
- 关于删除器的一个效率问题可以参阅此篇文章中的“效率与灵活性”:
演示案例:
- 下面演示一个shared_ptr指定删除器函数以及避免内存泄露的案例
- 错误情景:我们调用f函数来打开一个网络连接,但是在f函数调用之后没有关闭这个连接。因此就会造成内存的泄露
struct destination; //连接的对象
struct connection; //连接需要的信息
connection connect(destbination*); //打开连接
void disconnect(connection); //关闭连接
void f(destination &d)
{
connection c=connect(&d);//打开一个连接
....//使用这个连接
//如果在f函数退出之前忘记调用disconnect函数,那么该连接就没有关闭
}
- 正确情景:现在我们定义一个新的函数“end_connection”,并且配合shared_ptr类的使用。shared_ptr指定了一个删除器函数“end_connection”。因此下面的代码能够保证在f函数的各种调用结束时,保证连接正确的关闭
void end_connection(connection *p)
{
disconnection (*p);
}
void f(destination &d)
{
connection c=connect(&d);
shared_ptr<connection> p(&c,end_connection);
....//使用这个连接
//当f函数退出或者异常退出,p都会调用end_connection函数
}