您当前的位置: 首页 >  ar

惊鸿一博

暂无认证

  • 2浏览

    0关注

    535博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文

C++_智能指针shared_ptr、unique_ptr、weak_ptr、auto_ptr总结

惊鸿一博 发布时间:2021-05-10 07:46:51 ,浏览量:2

目录

1 介绍

1.1 内存四区模型

2 智能指针

3 shared_ptr类

3.1 make_shared函数:

3.2 shared_ptr的拷贝和赋值

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

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

3.5 内存耗尽

3.6 shared_ptr和new结合使用

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

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

3.9 其他shared_ptr操作

3.10 智能指针和异常

使用我们自己的释放操作

4 unique_ptr

4.1 unique的操作

不能拷贝unique_ptr,但可以用在函数返回值中

unique_ptr可做为容器元素

用unique_ptr传递删除器

5 weak_ptr的原理

6 auto_ptr(C++11已放弃)

7 总结及源码位置

boost::scoped_ptr

参考:

参考资料:《C++ Primer中文版 第五版》

1 介绍

我们知道除了静态内存和栈内存外,每个程序还有一个内存池,这部分内存被称为自由空间或者堆。程序用堆来存储动态分配的对象即那些在程序运行时分配的对象,当动态对象不再使用时,我们的代码必须显式的销毁它们。

在C++中,动态内存的管理是用一对运算符完成的:new和delete,new:在动态内存中为对象分配一块空间并返回一个指向该对象的指针,delete:指向一个动态独享的指针,销毁对象,并释放与之关联的内存。

1.1 内存四区模型

一个由C/C++编译的程序占用的内存分为以下几个部分:

  • 1、栈区(stack):由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
  • 2、堆区(heap: 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。
  • 3、数据区:主要包括静态全局区和常量区,如果要站在汇编角度细分的话还可以分为很多小的区。
    • 全局区(静态区)(static):全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。 程序结束后由系统释放。
    • 常量区 :常量字符串就是放在这里的。 程序结束后由系统释放。
  • 4、代码区:存放函数体的二进制代码。

动态内存管理经常会出现两种问题:一种是忘记释放内存,会造成内存泄漏;一种是尚有指针引用内存的情况下就释放了它,就会产生引用非法内存的指针。

2 智能指针

为了更加容易(更加安全)的使用动态内存,引入了智能指针的概念。智能指针的行为类似常规指针,重要的区别是它负责自动释放所指向的对象。标准库提供的两种智能指针的区别在于管理底层指针的方法不同,

  • shared_ptr
    • 基于“引用计数”模型实现,多个shared_ptr可指向同一个动态对象,并维护了一个共享的引用计数器,记录了引用同一对象的shared_ptr实例的数量。当最后一个指向动态对象的shared_ptr销毁时,才会自动销毁其所指对象(通过delete操作符,也支持自定义的删除器类型Deleter,以实现个性化的资源释放动作。)。
  • weak_ptr
    • 它是一种弱引用,用于解决仅使用shared_ptr时,造成的循环引用(造成内部对象无法析构,引用计数不能减到0,堆内存不释放,造成内存泄漏)问题,weak_ptr指向一个对象,并不增减该对象的引用计数器(
    • 源码中可看出:boost::weak_ptr必须从一个boost::share_ptr或另一个boost::weak_ptr转换而来,这也说明,进行该对象的内存管理的是那个强引用的boost::share_ptr。boost::weak_ptr只是提供了对管理对象的一个访问手段。weak_ptr指向shared_ptr指针指向的对象的内存,却并不拥有该内存。 ),
    • 为了配合shared_ptr而引入的一种智能指针,它指向一个由shared_ptr管理的对象而不影响所指对象的生命周期,也就是,将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。不论是否有weak_ptr指向,一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放。从这个角度看,weak_ptr更像是shared_ptr的一个助手而不是智能指针。
  • unique_ptr
    • 是一个独享所有权的智能指针,它提供了严格意义上的所有权。它取代了C++98中的auto_ptr。 
      • unique_ptr不支持普通的拷贝或赋值操作,但
        • 可以使用release转移“所有权”,
        • 可以用在函数返回值中,
        • 可以通过move语义作为容器元素使用。
  • auto_ptr (参考:auto_ptr与unique_ptr)
    • 这是C++98标准下的智能指针,它的缺点是在转移所有权后会使运行期不安全。C++11新标准,用unique_ptr来代替auto_ptr原有功能
    • 基于排他所有权模式:两个指针不能指向同一个资源,拷贝或赋值都会改变资源的所有权(unique_ptr则直接禁止了拷贝构造与赋值操作)。auto_ptr 主要有两大问题:
      • 复制和赋值会改变资源的所有权,不符合人的直觉。
      • 在 STL 容器中无法使用auto_ptr ,因为容器内的元素必需支持可复制(copy constructable)和可赋值(assignable)。

这几种智能指针都定义在memory头文件中。

3 shared_ptr类

创建智能指针时必须提供额外的信息,指针可以指向的类型:

shared_ptr p1; shared_ptr p2;

默认初始化的智能指针中保存着一个空指针。 智能指针的使用方式和普通指针类似,解引用一个智能指针返回它指向的对象,在一个条件判断中使用智能指针就是检测它是不是空。

if(p1  && p1->empty())     *p1 = "hi";

如下表所示是shared_ptr和unique_ptr都支持的操作:这里写图片描述

如下表所示是shared_ptr特有的操作:这里写图片描述

3.1 make_shared函数:

最安全的分配和使用动态内存的方法就是调用一个名为make_shared的标准库函数,此函数在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr。头文件和share_ptr相同,在memory中 必须指定想要创建对象的类型,定义格式见下面例子:

shared_ptr p3 = make_shared(42); shared_ptr p4 = make_shared(10,'9'); shared_ptr p5 = make_shared();

make_shared用其参数来构造给定类型的对象,如果我们不传递任何参数,对象就会进行值初始化

3.2 shared_ptr的拷贝和赋值

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

auto p = make_shared(42); auto q(p);

我们可以认为每个shared_ptr都有一个关联的计数器,通常称其为引用计数,无论何时我们拷贝一个shared_ptr,计数器都会递增。当我们给shared_ptr赋予一个新值或是shared_ptr被销毁(例如一个局部的shared_ptr离开其作用域)时,计数器就会递减,一旦一个shared_ptr的计数器变为0,它就会自动释放自己所管理的对象。

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

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

当指向一个对象的最后一个shared_ptr被销毁时,shared_ptr类会自动销毁此对象,它是通过另一个特殊的成员函数-析构函数完成销毁工作的,类似于构造函数,每个类都有一个析构函数。析构函数控制对象销毁时做什么操作。析构函数一般用来释放对象所分配的资源。shared_ptr的析构函数会递减它所指向的对象的引用计数。如果引用计数变为0,shared_ptr的析构函数就会销毁对象,并释放它所占用的内存。

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

当动态对象不再被使用时,shared_ptr类还会自动释放动态对象,这一特性使得动态内存的使用变得非常容易。如果你将shared_ptr存放于一个容器中,而后不再需要全部元素,而只使用其中一部分,要记得用erase删除不再需要的那些元素。

程序使用动态内存的原因: (1)程序不知道自己需要使用多少对象 (2)程序不知道所需对象的准确类型 (3)程序需要在多个对象间共享数据

直接管理内存 C++定义了两个运算符来分配和释放动态内存,new和delete,使用这两个运算符非常容易出错。

使用new动态分配和初始化对象 在自由空间分配的内存是无名的,因此new无法为其分配的对象命名,而是返回一个指向该对象的指针。

int *pi = new int;//pi指向一个动态分配的、未初始化的无名对象

此new表达式在自由空间构造一个int型对象,并返回指向该对象的指针

默认情况下,动态分配的对象是默认初始化的,这意味着内置类型或组合类型的对象的值将是未定义的,而类类型对象将用默认构造函数进行初始化。

string *ps = new string;//初始化为空string int *pi = new int;//pi指向一个未初始化的int

我们可以直接使用直接初始化方式来初始化一个动态分配一个动态分配的对象。我们可以使用传统的构造方式,在新标准下,也可以使用列表初始化

int *pi = new int(1024); string *ps = new string(10,'9'); vector *pv = new vector{0,1,2,3,4,5,6,7,8,9};

也可以对动态分配的对象进行初始化,只需在类型名之后跟一对空括号即可;

动态分配的const对象

const int *pci = new const int(1024); //分配并初始化一个const int const string *pcs = new const string; //分配并默认初始化一个const的空string

类似其他任何const对象,一个动态分配的const对象必须进行初始化。对于一个定义了默认构造函数的类类型,其const动态对象可以隐式初始化,而其他类型的对象就必须显式初始化。由于分配的对象就必须显式初始化。由于分配的对象是const的,new返回的指针就是一个指向const的指针。

3.5 内存耗尽

虽然现代计算机通常都配备大容量内存,但是自由空间被耗尽的情况还是有可能发生。一旦一个程序用光了它所有可用的空间,new表达式就会失败。默认情况下,如果new不能分配所需的内存空间,他会抛出一个bad_alloc的异常,我们可以改变使用new的方式来阻止它抛出异常

//如果分配失败,new返回一个空指针 int *p1 = new int;//如果分配失败,new抛出std::bad_alloc int *p2 = new (nothrow)int;//如果分配失败,new返回一个空指针

我们称这种形式的new为定位new,定位new表达式允许我们向new传递额外的参数,在例子中我们传给它一个由标准库定义的nothrow的对象,如果将nothrow传递给new,我们的意图是告诉它不要抛出异常。如果这种形式的new不能分配所需内存,它会返回一个空指针。bad_alloc和nothrow都在头文件new中。

释放动态内存 为了防止内存耗尽,在动态内存使用完之后,必须将其归还给系统,使用delete归还。

指针值和delete 我们传递给delete的指针必须指向动态内存,或者是一个空指针。释放一块并非new分配的内存或者将相同的指针释放多次,其行为是未定义的。即使delete后面跟的是指向静态分配的对象或者已经释放的空间,编译还是能够通过,实际上是错误的。

动态对象的生存周期直到被释放时为止 由shared_ptr管理的内存在最后一个shared_ptr销毁时会被自动释放,但是通过内置指针类型来管理的内存就不是这样了,内置类型指针管理的动态对象,直到被显式释放之前都是存在的,所以调用这必须记得释放内存。

使用new和delete管理动态内存常出现的问题: (1)忘记delete内存 (2)使用已经释放的对象 (3)同一块内存释放两次

delete之后重置指针值 在delete之后,指针就变成了空悬指针,即指向一块曾经保存数据对象但现在已经无效的内存的地址

有一种方法可以避免悬空指针的问题:在指针即将要离开其作用于之前释放掉它所关联的内存 如果我们需要保留指针可以在delete之后将nullptr赋予指针,这样就清楚的指出指针不指向任何对象。 动态内存的一个基本问题是可能多个指针指向相同的内存

3.6 shared_ptr和new结合使用

如果我们不初始化一个智能指针,它就会被初始化成一个空指针,接受指针参数的智能指针是explicit(在c++种explicit关键字只能用来修饰构造函数。使用explicit可以禁止编译器自动调用拷贝初始化,还可以禁止编译器对拷贝函数的参数进行隐式转换。)的,因此我们不能将一个内置指针隐式转换为一个智能指针,必须直接初始化形式来初始化一个智能指针

shared_ptr p1 = new int(1024);//错误:必须使用直接初始化形式 shared_ptr p2(new int(1024));//正确:使用了直接初始化形式

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

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

如果混合使用的话,智能指针自动释放之后,普通指针有时就会变成悬空指针,当将一个shared_ptr绑定到一个普通指针时,我们就将内存的管理责任交给了这个shared_ptr。一旦这样做了,我们就不应该再使用内置指针来访问shared_ptr所指向的内存了。也不要使用get初始化另一个智能指针或为智能指针赋值。

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

p和q指向相同的一块内存才能,由于是相互独立创建,因此各自的引用计数都是1,当q所在的程序块结束时,q被销毁,这会导致q指向的内存被释放,p这时候就变成一个空悬指针,再次使用时,将发生未定义的行为,当p被销毁时,这块空间会被二次delete

3.9 其他shared_ptr操作

可以使用reset来将一个新的指针赋予一个shared_ptr: 

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

与赋值类似,reset会更新引用计数,如果需要的话,会释放p的对象。reset成员经常和unique一起使用,来控制多个shared_ptr共享的对象。在改变底层对象之前,我们检查自己是否是当前对象仅有的用户。如果不是,在改变之前要制作一份新的拷贝:

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

3.10 智能指针和异常

如果使用智能指针,即使程序块过早结束,智能指针也能确保,在内存不再需要时再将其释放,sp是一个shared_ptr,因此sp销毁时会检测引用计数,当发生异常时,我们直接管理的内存是不会自动释放的。如果使用内置指针管理内存,且在new之后在对应的delete之前发生了异常,则内存不会被释放。

使用我们自己的释放操作

默认情况下,shared_ptr假定他们指向的是动态内存,因此当一个shared_ptr被销毁时,会自动执行delete操作,为了用shared_ptr来管理一个connection,我们必须首先必须定义一个函数来代替delete。这个删除器函数必须能够完成对shared_ptr中保存的指针进行释放的操作。

智能指针陷阱:

  • (1)不使用相同的内置指针值初始化(或reset)多个智能指针。
  • (2)不delete get()返回的指针
  • (3)不使用get()初始化或reset另一个智能指针
  • (4)如果你使用get()返回的指针,记住当最后一个对应的智能指针销毁后,你的指针就变为无效了
  • (5)如果你使用智能指针管理的资源不是new分配的内存,记住传递给它一个删除器
4 unique_ptr

某个时刻只能有一个unique_ptr指向一个给定对象,由于一个unique_ptr拥有它指向的对象,因此unique_ptr不支持普通的拷贝或赋值操作。

unique_ptr是一个独享所有权的智能指针,它提供了严格意义上的所有权。它取代了C++98中的auto_ptr。

4.1 unique的操作

虽然我们不能拷贝或者赋值unique_ptr,但是可以通过调用release或reset将指针所有权从一个(非const)unique_ptr转移给另一个unique

//将所有权从p1(指向string Stegosaurus)转移给p2
unique_ptr p2(p1.release());//release将p1置为空
unique_ptrp3(new string("Trex"));
//将所有权从p3转移到p2
p2.reset(p3.release());//reset释放了p2原来指向的内存

release成员返回unique_ptr当前保存的指针,并将其置为空。因此,p2被初始化为p1原来保存的指针,而p1被置为空。 reset成员接受一个可选的指针参数,令unique_ptr重新指向给定的指针。

调用release会切断unique_ptr和它原来管理的的对象间的联系。release返回的指针通常被用来初始化另一个智能指针或给另一个智能指针赋值。

不能拷贝unique_ptr,但可以用在函数返回值中

我们可以拷贝或赋值一个将要被销毁的unique_ptr.最常见的例子是从函数返回一个unique_ptr.

unique_ptr clone(int p)
{
    //正确:从int*创建一个unique_ptr
    return unique_ptr(new int(p));
}

还可以返回一个局部对象的拷贝:

unique_ptr clone(int p)
{
    unique_ptr ret(new int(p));
    return ret;
}
unique_ptr可做为容器元素

我们知道auto_ptr不可做为容器元素,会导致编译错误。虽然unique_ptr同样不能直接做为容器元素,但可以通过move语意实现。

unique_ptr sp(new int(88) );
vector vec;
vec.push_back(std::move(sp));
// vec.push_back( sp ); error:
// cout ,因此可以把scoped_ptr对象如同指针一样使用。如果scoped_ptr保存的空指针,那么这两个操作的行为未定义。
  • scoped_ptr不能在两个scoped_ptr之间、scoped_ptr与原始指针之间或空指针之间进行了相等或不相等测试。operator ==和operator !=都被声明为私有。
  • 参考:
    • 【C++】智能指针详解
    • C语言之内存四区模型和函数调用模型
    关注
    打赏
    1663399408
    查看更多评论
    0.0387s