C++智能指针1——shared_ptr类

动态内存与智能指针

在C++中,动态内存的管理是通过一对运算符来完成的:

  • new,在动态内存中为对象分配空间并返回一个指向该对象的指针,我们可以选择对对象进行初始化;
  • delete,接受一个动态对象的指针,销毁该对象,并释放与之关联的内存。

动态内存的使用很容易出问题,因为确保在正确的时间释放内存是极其困难的。有时我们会忘记释放内存,在这种情况下就会产生内存泄漏;有时在尚有指针引用内存的情况下我们就释放了它,在这种情况下就会产生引用非法内存的指针。

为了更容易(同时也更安全)地使用动态内存,新的标准库提供了两种智能指针类型来管理动态对象。智能指针的行为类似常规指针,重要的区别是它负责自动释放所指向的对象。

新标准库提供的这两种智能指针的区别在于管理底层指针的方式

  1. shared_ptr 允许多个指针指向同一个对象;
  2. unique ptr 则“独占”所指向的对象
  3. 标准库还定义了一个名为weak ptr的伴随类,它是一种弱引用,指向shared ptr所管理的对象。

这三种类型都定义在memory头文件中。

shared_ptr类

类似vector,智能指针也是模板。

因此,当我们创建一个智能指针时,必须提供额外的信息——指针可以指向的类型。

与vector一样,我们在尖括号内给出类型,之后是所定义的这种智能指针的名字

shared ptr<string> pl; // shared_ptr,可以指向string
shared ptr<list<int>> p2; // shared ptr,可以指向int的list

默认初始化的智能指针中保存着一个空指针。

智能指针的使用方式与普通指针类似。

解引用一个智能指针返回它指向的对象。如果在一个条件判断中使用智能指针,效果就是检测它是否为空:

//如果p1不为空,检查它是否指向一个空string
if (pl && pl->empty())
*p1="hi"; //如果pl指向一个空string,解引用p1,将一个新值赋予string
shared_ptr和unique_ptrz都支持的操作
shared_ptr<T> sp
unique_ptr<T> up
空智能指针,可以指向类型为T的对象
p将p用作一个条件判断,若p指向一个对象,则为true
*p解引用p,获得它指向的对象
p->mem等价于(*p).mem
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(g)p是shared ptrq的拷贝;此操作会递增g中的计数器。q中的指针必须能转换为T*
p=gp和q都是shared ptr,所保存的指针必须能相互转换。此操作会递减p的引用计数,递增g的引用计数;若p的引用计数变为0,则将其管理的原内存释放
p.unique()若p.use_count()为1,返回true;否则返回false
p.use_count()返回与p共享对象的智能指针数量:可能很慢,主要用于调试


 

make_shared 函数

最安全的分配和使用动态内存的方法是调用一个名为make_shared的标准库函数。

此函数在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr。

与智能指针一样,make_shared也定义在头文件memory中。

当要用make_shared时,必须指定想要创建的对象的类型。定义方式与模板类相同,在函数名之后跟一个尖括号,在其中给出类型:

//指向一个值为42的int的shared ptr
shared_ptr<int> p3 =make_shared<int>(42);
// p4指向一个值为"9999999999"的string
shared_ptr<string> p4 = make_shared<string>(10, '9');
//p5指向一个值初始化的int,即,值为0
shared_ptr<int>p5 make_shared<int>();

类似顺序容器的 emplace成员,make_shared用其参数来构造给定类型的对象。

例如,调用make_shared<string>时传递的参数必须与string的某个构造函数相匹配,调用make_shared<int>时传递的参数必须能用来初始化一个int,依此类推。如果我们不传递任何参数,对象就会进行值初始化

当然,我们通常用auto定义一个对象来保存make shared的结果,这种方式较为简单:

// p6指向一个动态分配的空 vector<string>
auto p6 = make_shared<vector<string>>();

shared_ptr的拷贝和赋值

当进行拷贝或赋值操作时,每个shared_ptr都会记录有多少个其他shared_ptr指向相同的对象:

auto p = make shared<int>(42);// p指向的对象只有ρ一个引用者
auto q(p);// p和q指向相同对象,此对象有两个引用者

我们可以认为每个shared_ptr都有一个关联的计数器,通常称其为引用计数

无论何时我们拷贝一个 shared_ptr,计数器都会递增。

例如,shared_ptr初始化另一个shared_ptr,或将它作为参数传递给某个函数以及作为函数的返回值时,它所关联的计数器就会递增。

当我们给 shared_ptr赋予一个新值或是shared_ptr被销毁(例如局部的shared ptr离开其作用域时,计数器就会递减

一旦一个shared ptr的计数器变为0,它就会自动释放自己所管理的对象;

auto r =make_shared<int>(42);//τ指向的int只有一个引用者
x=q;//给r赋值,令它指向另一个地址
// 递增q指向的对象的引用计数
// 递减上原来指向的对象的引用计数
// r原来指向的对象已没有引用者,会自动释放


此例中我们分配了一个int,将其指针保存在x中。接下来,我们将一个新值赋予r。在此情况下,x是唯一指向此int的shared ptr,在把q赋给r的过程中,此int被自动释放。

到底是用一个计数器还是其他数据结构来记录有多少指针共享对象,完全由标准库的具体实现来决定。

关键是智能指针类能记录有多少个 shared_ptr 指向相同的对象,并能在恰当的时候自动释放对象。

shared_ptr自动销毁所管理的对象……

当指向一个对象的最后一个shared_ptr被销毁时,shared ptr类会自动销毁此对象。

它是通过另一个特殊的成员函数——析构函数完成销毁工作的。

类似于构造函数,每个类都有一个析构函数。

就像构造函数控制初始化一样,析构函数控制此类型的对象销毁时做什么操作。

析构函数一般用来释放对象所分配的资源。

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

…shared_ptr 还会自动释放相关联的内存

当动态对象不再被使用时,shared_ptr类会自动释放动态对象,这一特性使得动态内存的使用变得非常容易。

例如,我们可能有一个函数,它返回一个share_ptr,指向一个Foo类型的动态分配的对象,对象是通过一个类型为的参数进行初始化的:

// factory返回一个 shared_ptr,指向一个动态分配的对象
shared_ptr<Foo> factory(T arg)
{
//恰当地处理arg
// shared_ptr负责释放内存
return make_shared<Foo>(arg);
}

由于factory返回一个shared_ptr,所以我们可以确保它分配的对象会在恰当的时制被释放。

例如,下面的函数将factory返回的 shared_ptr保存在局部变量中:

void use_factory(T arg)
{
shared_ptr<Foo> p = factory(arg);
// 使用P
// p离开了作用城,它指向的内存会被自动释放掉
}

由于p是use_factory的局部变量,在use_factory结束时它将被销毁。当p被销毁时,将递减其引用计数并检查它是否为0.

在此例中,p是唯一引用factory返回的内存的对象。由于p将要销毁,p指向的这个对象也会被销毁,所占用的内存会被释放。

但如果有其他shared_ptr也指向这块内存,它就不会被释放掉:

shared ptr<Foo> use factory(T arg)
{
shared_ptr<Foo> p = factory(arg);
return p; // 当我们返回p时,引用计数进行了递增操作 函数返值
// 使用P
//p离开了作用域,但它指向的内存不会被释放掉

}

在此版本中,use_factory中的return语句向此函数的调用者返回一个p的拷贝。

一个shared_ptr会增加所管理对象的引用计数值。现在当p被销毁时,它所指向的内存还有其他使用者。

对于一块内存,shared ptr类保证只要有任何shared_ptr对象引用它,它就不会被释放掉。

由于在最后一个shared ptr销毁前内存都不会释放,保证shared_ptr在无用之后不再保留就非常重要了。

如果你忘记了销毁程序不再需要的shared_ptr,程序仍会正确执行,但会浪费内存。

share_ptr 在无用之后仍然保留的一种可能情况是,你将shared ptr存放在一个容器中,随后重排了容器,从而不再需要某些元素。在这种情况下,你应该确保用erase删除那些不再需要的shared_ptr元素。

如果你将 shared_ptr存放于一个容器中,而后不再需要全部元素,而只使用其中一部分,要记得用erase删除不再需要的那些元素。

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

程序使用动态内存出于以下三种原因之一;

  1.  程序不知道自己需要使用多少对象
  2. 程序不知道所需对象的准确类型
  3. 程序需要在多个对象间共享数据 

容器类是出于第一种原因而使用动态内存的典型例子,

在本节中,我们将定义一个类,它使用动态内存是为了让我个对象能共享相同的底层数据。

到目前为止,我们使用过的类中,分配的资源都与对应对象生存期一致。

例如,每个vector“拥有”其自己的元素。当我们拷贝一个vector时,原vector和副本vector
中的元素是相互分离的:

vector<string> v1; // 空vector
{// 新作用域
vector<string> v2 = {"a", "an", "the"};
v1 =v2; //从v2拷贝元素到v1中
} // v2被销毁,其中的元素也被销毁
// v1有三个元素,是原来v2中元素的拷贝

由一个vector分配的元素只有当这个vector存在时才存在。当一个vector被销毁时,这个vector中的元素也都被销毁。

但某些类分配的资源具有与原对象相独立的生存期。

例如,假定我们希望定义一个名为Blob的类,保存一组元素。与容器不同,我们希望Blob对象的不同拷贝之间共享相同的元素。即,当我们拷贝一个Blob时,原Blob对象及其拷贝应该引用相同的底层元素。

一般而言,如果两个对象共享底层的数据,当某个对象被销毁时,我们不能单方面地销毁底层数据;

Blob<string> b1; // 空Blob
{ // 新作用域
Blob<string> b2 = {"a", "an", "the"};
b1= b2;// b1和b2共享相同的元素

}// b2被销毁了,但b2中的元素不能销毁
// b1指向最初由b2创建的元素

在此例中,b1和b2共享相同的元素。当b2离开作用域时,这些元素必须保留,因为b1仍然在使用它们。

使用动态内存的一个常见原因是允许多个对象共享相同的状态

shared_ptr和new结合使用

如前所述,如果我们不初始化一个智能指针,它就会被初始化为一个空指针。

我们还可以用new返回的指针来初始化智能指针:

shared_ptr<double> pl;// shared ptr可以指向一个double
shared_ptr<int> p2(new int(42));// p2指向一个值为42的int

接受指针参数的智能指针构造函数是explicit的。因此我们不能将一个内置指针隐式转换为一个智能指针,必须使用直接初始化形式来初始化一个智能指针:
 

shared ptr<int> p1 = new int(1024); //错误:必须使用直接初始化形式

shared_ptr<int> p2(new int(1024)); // 正确:使用了直接初始化形式

p1的初始化隐式地要求编译器用一个new返回的int*来创建一个shared_ptr。由于我们不能进行内置指针到智能指针间的隐式转换,因此这条初始化语句是错误的。
 

出于相同的原因,一个返回shared ptr的函数不能在其返回语句中隐式转换一个普通指针

shared ptr<int> clone(int p) 
{
return new int(p);//错误:隐式转换为shared ptr<int>
}

我们必须将shared ptr显式绑定到一个想要返回的指针上:

shared_ptr<int> clone(int p) 
{
//正确:显式地用int*创建shared_ptr<int>
return shared_ptr<int>(new int(p));
}
定义和改变shared_ptr的其他方法
shared_ptr<T> p(q)p管理内置指针q所指向的对象;q必须指向new分配的内存,且能够转换为T*类型
shared_ptr<T> p(u)p从unique_ptr u那里接管了对象的所有权;将u置为空
shared_ptr<T> p(q, d)p接管了内置指针q所指向的对象的所有权。q必须能转换为T*类型。p将使用可调用对象d来代替delete
shared_ptr<T> p(p2, d)p是shared_ptr p2的拷贝,唯一的区别是p将用可调用对象d来代替 delete

p.reset()

p.reset(q)

p.reset(q, d) 

若p是唯一指向其对象的shared ptr, reset 会释放此对象。若传递了可选的参数内置指针g,会令p指向q,否则会将p置为空。若还传递了参数d,将会调用d而不是delete来释放q

不要混合使用普通指针和智能指针……

shared_ptr可以协调对象的析构,但这仅限于其自身的拷贝(也是shared pte)之间,这也是为什么我们推荐使用make_shared而不是new的原因。

这样,我们就能在分配对象的同时就将shared_ptr与之绑定,从而避免了无意中将同一块内存绑定到多个独立创建的shared_ptr上。

考虑下面对shared_ptr进行操作的函数,

//在函数被调用时ptr被创建并初始化
void process(shared_ptr<int> ptr)
{
// 使用 ptr
} //ptr离开作用域,被销毁

process的参数是传值方式传递的,因此实参会被拷贝到ptr中。

拷贝一个shared ptr 会递增其引用计数,因此,在process运行过程中,引用计数值至少为2。

当process结束时,ptr的引用计数会递减,但不会变为0。

因此,当局部变量ptr被销毁时,ptr指向的内存不会被释放。
使用此函数的正确方法是传递给它一个shared_ptr:

shared ptr<int> p(new int(42));// 引用计数为1
process(p);// 拷贝p会递增它的引用计数;在process中引用计数值为2

int i=*p; //正确:引用计数值为1

虽然不能传递给process一个内置指针,但可以传递给它一个(临时的)shared_ptr,这个 shared_ptr是用一个内置指针显式构造的。但是,这样做很可能会导致错误:

int *x(new int(1024)); //危险:x是一个普通指针,不是一个智能指针
process(x);// 错误:不能将 int*转换为一个 shared ptr<int>
process(shared ptr<int>(x));//合法的,但内存会被释放!
int j=*x; //未定义的:x是一个空悬指针!

在上面的调用中,我们将一个临时shared_ptr传递给process。

当这个调用所在的表达式结束时,这个临时对象就被销毁了。

销毁这个临时变量会递减引用计数,此时引用计数就变为0了。

因此,当临时对象被销毁时,它所指向的内存会被释放。
但x继续指向(已经释放的)内存,从而变成一个空悬指针。如果试图使用x的值,其行为是未定义的。

当将一个 shared_ptr绑定到一个普通指针时,我们就将内存的管理责任交给了这个shared_ptr。
指向的内存了。

一旦这样做了,我们就不应该再使用内置指针来访问shared_ptr所指向的内存了。


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

智能指针类型定义了一个名为get的函数,它返回一个内置指针指向智能指针管理的对象。

此函数是为了这样一种情况而设计的:我们需要向不能使用智能指针的代码传递一个内置指针。使用get返回的指针的代码不能delete此指针。

虽然编译器不会给出错误信息,但将另一个智能指针也绑定到 get 返回的指针上是错误的:

shared ptr<int> p(new int(42)); //引用计数为1
int *q = p.get();// 正确:但使用q时要注意,不要让它管理的指针被释放
{// 新程序块
//未定义:两个独立的shared ptr指向相同的内存
shared_ptr<int>(q);
}//程序块结束,q被销毁,它指向的内存被释放
int foo =*p;// 未定义:p指向的内存已经被释放了

在本例中,p和g指向相同的内存。由于它们是相互独立创建的,因此各自的引用计数都是1。

当q所在的程序块结束时,q被销毁,这会导致q指向的内存被释放。从而p变成一个空悬指针,意味着当我们试图使用p时,将发生未定义的行为。而且,当p被销毁时,这块内存会被第二次delete。

get用来将指针的访问权限传递给代码,你只有在确定代码不会delete指针的情况下,才能使用 get。特别是,永远不要用 get初始化另一个智能指针或者为另一个智能指针赋值

其他shared_ptr操作

shared_ptr还定义了其他一些操作。

我们可以用reset来将一个新的指针赋予一个shared_ptr;

p =new int(1024); // 错误:不能将一个指针赋予shared_ptr
p.reset (new int(1024));//正确:p指向一个新对象

与赋值类似,reset会更新引用计数,如果需要的话,会释放p指向的对象。

reset成员经常与 unique_ptr起使用,来控制多个shared_ptr共享的对象。

在改变底层对象拷贝之 前,我们检查自己是否是当前对象仅有的用户。如果不是,在改变之前要制作一份新的

if (!p.unique())
p.reset(new string(*p));//我们不是唯一用户;分配新的拷贝
*p +=newVal; //现在我们知道自己是唯一的用户,可以改变对象的值

智能指针和异常

使用异常处理的程序能在异常发生后令程序流程继续,我们注意到,这种程序需要确保在异常发生后资源能被正确地释放,一个简单的确保资源被释放的方法是使用智能指针。

如果使用智能指针,即使程序块过早结束,智能指针类也能确保在内存不再需要时将其释放,

void f()
{
shared ptr<int> sp(new int(42));//分配一个新对象
//这段代码抛出一个异常,且在f中未被捕获
//在函数结束时shared_ptr自动释放内存
}

函数的退出有两种可能,正常处理结束或者发生了异常,无论哪种情况,局部对象都会被销毁。

在上面的程序中,sp是一个 shared_ptr,因此sp销毁时会检查引用计数。

在此例中,sp是指向这块内存的唯一指针,因此内存会被释放掉。
与之相对的,当发生异常时,我们直接管理的内存是不会自动释放的。如果使用内置指针管理内存,且在new之后在对应的delete之前发生了异常,则内存不会被释放:

void f()
{
int *ip = new int(42); //动态分配一个新对象
// 这段代码抛出一个异常,且在f中未被捕获
delete ip; //在退出之前释放内存
}

如果在new和delete之间发生异常,且异常未在f中被捕获,则内存就永远不会被释放了。在函数f之外没有指针指向这块内存,因此就无法释放它了。

 智能指针绑定非动态内存

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

我们可以将智能指针绑定到一个指向其他类型的资源的指针上,但是为了这样做,必须提供自己的操作来替代delete。

当智能指针绑定到非动态分配的内存时,就必须提供自定义的删除器来代替delete

当使用std::shared_ptr时,你也可以为其提供一个自定义的删除器。下面是一个使用std::shared_ptr来管理int数组,并附带自定义删除器的例子:

#include <iostream>  
#include <memory>  
  
// 自定义删除器,用于释放int数组  
struct ArrayDeleter {  
    void operator()(int* ptr) const {  
        delete[] ptr; // 使用delete[]而不是delete来释放数组  
    }  
};  
  
int main() {  
    // 使用自定义删除器的shared_ptr来管理int数组  
    std::shared_ptr<int[]> intArrayPtr(new int[10], ArrayDeleter());  
  
    // 使用intArrayPtr就像使用普通指针一样  
    for (int i = 0; i < 10; ++i) {  
        intArrayPtr[i] = i; // 可以直接像数组一样使用shared_ptr  
    }  
  
    // 输出数组内容  
    for (int i = 0; i < 10; ++i) {  
        std::cout << intArrayPtr[i] << ' ';  
    }  
    std::cout << std::endl;  
  
    // 当最后一个shared_ptr离开作用域或被重置时,ArrayDeleter会被调用,释放数组内存  
    // 不需要显式调用delete[]  
  
    return 0;  
}

在这个例子中,我们定义了一个名为ArrayDeleter的结构体,它重载了operator()以便接受一个int*指针,并释放这个指针指向的数组。然后,我们使用这个自定义删除器以及new int[10]来初始化一个std::shared_ptr<int[]>智能指针。std::shared_ptr会负责在最后一个引用该智能指针的对象被销毁时调用我们提供的删除器。

注意,当我们创建std::shared_ptr时,除了传递原始指针外,我们还传递了ArrayDeleter()的实例,即删除器的构造函数的调用。这样,当std::shared_ptr需要释放内存时,它会调用这个删除器。

此外,由于std::shared_ptr内部会维护一个引用计数,所以只有当最后一个指向该数组的std::shared_ptr被销毁时,数组才会被释放。如果程序中有多个std::shared_ptr实例指向同一个数组,并且它们中的任何一个被销毁,数组都不会被释放,直到最后一个std::shared_ptr也被销毁。

注意:智能指针陷阱

  • 智能指针可以提供对动态分配的内存安全而又方便的管理,但这建立在正确使用的前提下。为了正确使用智能指针,我们必须坚持一些基本规范:
  • 不使用相同的内置指针值初始化(或reset)多个智能指针。
  • 不delete get()返回的指针。
  • 不使用get()初始化或reset另一个智能指针。
  • 如果你使用get()返回的指针,记住当最后一个对应的智能指针销毁后,你的指针就变为无效了。
  • 如果你使用智能指针管理的资源不是new分配的内存,记住传递给它一个删除器
     
  • 34
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
shared_ptrC++中的智能指针,它采用引用计数的方法来实现释放指针所指向的资源。当使用shared_ptr时,它会记录有多少个shared_ptr指向同一个对象,只有当最后一个shared_ptr被销毁时,该对象的内存才会被释放。因此,shared_ptr可以自动管理内存,不需要手动释放。 在代码中,使用shared_ptr可以像普通指针一样操作对象。当需要创建一个shared_ptr对象时,可以使用std::make_shared函数来构造,如下所示: ``` std::shared_ptr<int> sharedPtr1 = std::make_shared<int>(10); ``` 可以通过将shared_ptr赋值给其他shared_ptr来共享资源,如下所示: ``` std::shared_ptr<int> sharedPtr2 = sharedPtr1; ``` 当所有的shared_ptr都被销毁时,内存会自动释放。可以使用use_count()函数获取shared_ptr的引用计数,如下所示: ``` std::cout << "sharedPtr2 use count: " << sharedPtr2.use_count() << std::endl; ``` 当引用计数为0时,内存会被自动释放。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [C++智能指针shared_ptr分析](https://download.csdn.net/download/weixin_38705004/13788082)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* *3* [C++11中的智能指针unique_ptrshared_ptr和weak_ptr详解](https://blog.csdn.net/chenlycly/article/details/130918547)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值