c++ 智能指针 - shared_ptr

各类型数据的内存占用及释放

  • 静态内存
    保存局部的static对象、类static数据成员及定义在任何函数之外的变量;
    由编译器自动创建和销毁;
    static对象在使用之前分配,在程序结束时销毁。
  • 栈内存
    保存定义在函数内的非static变量;
    仅在其定义的程序块运行时才存在。
  • 动态内存(堆)
    存储动态分配的对象,即那些在程序运行时分配的对象;
    动态对象的生存期由程序来控制,也就是说,当动态对象不再使用时,必须显式进行销毁。

智能指针

为了更容易更方便的使用动态内存,c++提供了两种智能指针shared_ptr和unique_ptr来管理动态对象,智能指针负责自动释放所管理的对象,避免因为不合适的释放导致的内存泄露和访问已释放内存的bug;

智能指针也是模板,创建一个智能指针时,必须提供额外的类型信息,如shared_ptr p1;

智能指针的使用方式和普通指针类似,解引用一个智能指针返回它指向的对象,也可以在一个条件判断中使用智能指针,判断它是否为空;

shared_ptr和unique_ptr都支持的操作:

shared_ptr<T> sp;
unique_ptr<T> up;                 空智能指针,可以指向类型为T的对象

p                                 将p作为一个条件判断,若p不为空则为true

*p                                解引用p,获取它指向的对象

p.get()                           返回p中保存的指针。要小心使用,若智能指针释放了对象,返回的指针指向的对象也就消失了

swap(p,q)
p.swap(q)                         交换p和q中的指针

shared_ptr独有的操作:

make_shared<T>(args)             返回一个shared_ptr,指向一个动态分配的类型为T的对象,使用参数args初始化此对象

shared_ptr<T> p(q)               p是shared_ptr的拷贝;此操作会递增q中的计数器。q中的指针必须能够转换为T^* 

p  = q                       p和q都是shared_ptr,所保存的指针必须能够相互转换。此操作会递减p的引用计数,                                       同时递增q的引用计数;若p的引用计数递减后变为0,则将其管理的原内存释放    

p.unique()                  若p.use_count() == 1,返回true,表示对p所指向的内存只有一份引用计数,否则返回false
p.use_count()               返回与p共享对象的智能指针数量。可能很慢,主要用于调试

shared_ptr

shared_ptr允许多个指针指向同一个对象;

make_ptr

最安全的分配和使用动态内存的方法是调用std::make_shared函数,该函数在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr。函数头文件 #include

make_ptr用其参数来构造给定类型的对象,但必须要求传递的参数与T的某个构造函数可以匹配;

shared_ptr p3 = make_shared(43);
shared_ptr p4 = make_shared(10,’9’);//p4指向”9999999999”
shared_ptr p5 = make_shared();//p5指向一个值初始化的int,即指向一个值为0的int

通常使用auto定义一个对象来保存make_shared的结果,如
auto p6 = make_shared

使用new出来的指针初始化shared_ptr

使用内置指针初始化shared_ptr的构造函数是explicit的,不允许隐式进行转换,必须使用直接初始化的形式。
shared_ptr p1 = new int(42);//错误,必须使用直接初始化形式
shared_ptr p2(new int(42));//正确,使用了直接初始化形式

shared_ptr clone(int p)
{
return new int(p);//错误,不允许隐式转换为shared_ptr
return make_shared(p);//正确
return shared_ptr(new int(p));//正确,显式进行转换
}

但是由于下面所述的更多的原因,不建议混合使用普通指针和shared_ptr,所以还是建议使用make_shared函数分配和使用动态内存。

shared_ptr的拷贝和赋值

每个shared_ptr都有一个关联的计数器,称之为引用计数;

当用一个shared_ptr初始化另一个shared_ptr、将其作为参数传递给另一个参数、将shared_ptr作为返回值时,它所关联的引用计数都会增加;

当给shared_ptr赋予一个新值、shared_ptr被销毁、局部shared_ptr对象离开作用域,它所关联的引用计数都会递减;

shared_ptr自动销毁所管理的对象

shared_ptr的析构函数会递减它所指向的对象的引用计数,如果引用计数变为0,则shared_ptr的析构函数会销毁对象并释放对象所占用的内存。

shared_ptr自动释放所关联的对象占用的内存

当和shared_ptr关联的动态对象的引用计数变为0时,shared_ptr会自动调用该动态对象的析构函数或指定函数释放动态对象占用的内存。这种特性会使得动态内存的管理更加方便,不易出现内存泄露和重复delete的bug。

shared_ptr在无用之后仍然被保留的一种情况是:将shared_ptr放入了容器中,而后不再需要全部的元素,那么要求对不需要的元素显式进行erase操作,否则还是会造成内存泄露。

智能指针的释放

默认情况下,一个用来初始化智能指针的普通指针必须指向动态内存,因为智能指针默认使用delete释放它关联的对象。

可以将智能指针绑定到一个指向其他类型资源的指针上,但这样做的时候,需要自己提供函数来替换delete。

定义和改变shared_ptr的其他方法:

shared_ptr<T> p(q)    智能指针p管理普通指针q指向的对象,但要求q必须指向new分配的内存,且可以转换为T的指针
shared_ptr<T> p(u)    p从unique_ptr那里接管了对象的所有权,且将u置为空
shared_ptr<T> p(q,d)  p接管了内置指针q所指向的对象的所有权,q必须转换为T的指针。p将使用可调用对象d来代替delete
shared<T> p(p2,d)   p是shared_ptr q的拷贝,唯一的区别是p将用可调用对象d来代替delete
p.reset();p.reset(q);p.reset(q,d)     若p是唯一指向对象的shared_ptr,reset会释放此对象;若传递了可选参数q,则会令p指向q,否则会将p置为空;若还传递了参数d,将会调用d而不是delete来释放q

不用混用智能指针和普通指针

shared_ptr可以协调对象的析构,但这仅限于其自身的拷贝(也是shared_ptr)之间,但如果混用了智能指针与普通指针,则有发生bug的风险。

void process(shared_ptr ptr)
{
//处理ptr
}//ptr离开作用域,被销毁

shared_ptr p(new int(42));//引用计数为1
process(p);//由于process函数的形参为值传递方式,所以在函数内部时,处理的是p的副本,对p进行了复制,那么在process函数内部的引用计数为2,一旦函数结束,内部的副本被销毁,引用计数变为1,不影响正常的数据访问
int j = *p;//正确

int *x(new int(42));
process(x);//错误,不允许隐式转换
process(shared_ptr p(x));//可以正确调用,但是一旦函数指向结束,临时对象会被释放,此时引用计数变为0,所指向的对象的内存会被释放
int j = *x;//错误,会发生未定义的行为,因为x此时是一个空悬指针

当将一个shared_ptr绑定到一个普通指针时,我们就将内存管理的责任交给了shared_ptr。一旦这样做了之后,就不应该再用内置指针来访问shared_ptr所指向的内存了。

不用使用get初始化另一个智能指针或为智能指针赋值

智能指针有一个get函数,获取智能指针管理的对象的内置指针。这个函数设计的初衷是为了调用不能以智能指针为参数的函数。

但必须注意的是:使用get函数返回的指针的代码不能delete此指针,特别是,永远不要用get初始化另一个智能指针或为另一个智能指针赋值。

shared_ptr p(new int(42));
int *q = p.get();
{
//新程序块
//未定义:两个智能指针指向同一块内存
shared_ptr p1(q);
}//程序结束时,p1被销毁,它所指向的内存也被释放
int j = *p;//未定义的行为,因为此时p指向的内存已经被释放了

reset函数

p.reset(new int(1024));//p指向了新的对象

reset经常和unique一起使用,在控制多个shared_ptr共享的对象。在改变底层对象之前,要检查是否我们未当前对象仅有的用户,如果不是的话,在改变之前需要制作一份新的拷贝。
if(! p.unique())
{
p.reset(new int(*p));
}
*p += n;

智能指针与异常处理

代码发生异常的时候,中间退出,那么对于new出来的对象如果没有显式释放的话就造成了内存泄露。可以用智能指针避免这一现象。
void f()
{
shared_ptr sp(new int(42));
//发生了异常,且未在f中被捕获,程序退出
}//函数结束时,sp会被析构,对象的引用计数减1变为0,自动释放对象所占用的内存

使用自己的析构操作

默认情况下,shared_ptr认为指向动态内存,且shared_ptr释放时会调用它管理的指针的delete操作。

但如果shared_ptr管理的是自定义类型,且该类型没有良好设计的构造函数时,那么使用shared_ptr依然会造成内存泄露,这种情况下可以定义一个其他的函数来代替delete操作。

void MyDelete
{
//自定义的析构函数
}

void f()
{
myType* t = new myType();
shared_ptr p(t,MyDelete);
}//函数结束或者发生异常时,会析构p,此时会调用MyDelete函数释放内存

如果智能指针管理的不是new出来的对象,那么需要自己定义一个删除器。
void MyDelete
{
//自定义操作
}

void f(input& d)
{
myType t = myType(d);//非new出来的对象
shared_ptr p(&t,MyDelete);
}//函数结束或者发生异常时,会析构p,此时会调用MyDelete函数进行指定的操作

总结及注意事项

智能指针可以提供对动态分配的内存安全而又方便的管理,但这是建立在正确使用的前提下的。为了正确使用智能指针,必须坚持一些基本原则:

  • 不使用相同的内置指针值初始化或reset多个智能指针;
  • 不delete get函数返回的指针;
  • 不使用get()初始化或reset另一个智能指针;
  • 如果使用了get()返回的指针,那么切记当最后一个智能指针被销毁后,你的内置指针就变为无效了;
  • 如果使用智能指针管理的是资源而不是new分配的内存,记住传递给它一个删除器用于对对象进行特殊处理。

使用了动态生存期资源的类

程序使用动态内存是出于下述三种原因之一:
1.程序不知道自己需要使用多少对象;这种原因的使得出现了容器
2.程序不知道所需对象的准确类型;暂时不知道
3.程序需要在多个对象间共享数据;这种原因造成了shared_ptr

猜你喜欢

转载自blog.csdn.net/cdknight_happy/article/details/79895334