1、问题引入

在C++中,静态内存和栈内存外,还有一部分内存称为堆程序用堆来存储动态分配的对象即那些在程序运行时分配的对象,当动态对象不再使用时,我们的代码必须显式的销毁它们。在C++中一般使用“new”:在动态内存中为对象分配一块空间并返回一个指向该对象的指针,“delete”:指向一个动态独享的指针,销毁对象,并释放与之关联的内存。

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

为了更加容易(更加安全)的使用动态内存,引入了智能指针的概念。智能指针的行为类似常规指针,重要的区别是它负责自动释放所指向的对象。C++中常用的智能指针有shared_ptr(多个指针指向同一对象)、unique_ptr(独占所指的对象)、weak_ptr(伴随类,弱引用)、auto_ptr(局部指针C++11已弃用),位于头文件memory中。实际上智能指针还有boost::scoped_ptr()、boost::scoped_array、boost::shared_array等。

2、shared_ptr类

shared_ptr允许多个指针指向同一对象,资源可以被多个指针所共享。创建智能指针时必须提供额外的信息,指针可以指向的类型:

shared_ptr<string> p1;
shared_ptr<list<int>> p2;

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

if(p1  && p1->empty()){*p1 = "hi";           // 如果p1指向一个空string,解引用p1,将一个新值赋予string
}

(1)shared_ptr的操作

如下表所示是shared_ptr和unique_ptr都支持的操作:

如下表所示是shared_ptr特有的操作:

注:make_shared函数

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

shared_ptr<int> p3 = make_shared<int>(42);
shared_ptr<string> p4 = make_shared<string>(10,'9');
shared_ptr<int> p5 = make_shared<int>();

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

(2)shared_ptr的拷贝和赋值

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

auto p = make_shared<int>(42);
auto q(p);

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

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

总结:引用计数

增:

  • 拷贝:拷贝一个shared_ptr;
  • 初始化:用一个shared_ptr初始化另一个shared_ptr;
  • 参数:作为参数传递给一个函数
  • 返回值:作为函数返回值

减:

  • 赋新值:给shared_ptr赋予一个新值
  • 销毁:shared_ptr被销毁后

(3)shared_ptr自动销毁所管理的对象并自动释放相关联的内存

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

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

注:delete之后重置指针值

在使用new申请了动态内存后,在使用delete之后,指针就变成了空悬指针,即指向一块曾经保存数据对象但现在已经无效的内存的地址。有一种方法可以避免悬空指针的问题:在指针即将要离开其作用于之前释放掉它所关联的内存 如果我们需要保留指针可以在delete之后将nullptr赋予指针,这样就清楚的指出指针不指向任何对象。

(4)shared_ptr和new结合使用

如果我们不初始化一个智能指针,它就会被初始化成一个空指针,接受指针参数的职能指针是explicit的,因此我们不能将一个内置指针隐式转换为一个智能指针,必须直接初始化形式来初始化一个智能指针。

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

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

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

(5)不要混合使用普通指针和智能指针

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

shared_ptr<int> 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。

(6)其他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;//现在我们知道自己是唯一的用户,可以改变对象的值
}

(6)智能指针陷阱:

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

3、unique_ptr

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

例:

unique_ptr<string>  p1(new  string(“hello”));
unique_ptr<string>  p2(p1);           // 错误:unique_ptr不支持拷贝
unique_ptr<string>  p3;
p3 = p2;                   // 错误:unique_ptr不支持赋值

下表是unique的操作:

注意u.release()和u.reset()的区别:

(1)u.release()是释放u的对象,但是u所指的对象还存在在内存中,并未被释放,需要用delete来释放内存。

(2)u.reset()是是否了u所指的对象。

例:

unique_ptr<string>  p1(new  string(“hello”));
auto p = p1.release();
delete  p;          // 用release()后需要用delete来释放

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

例:

//将所有权从p1(指向string Stegosaurus)转移给p2
unique_ptr<string> p2(p1.release());//release将p1置为空
unique_ptr<string>p3(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

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

例:返回一个局部对象的拷贝

unique_ptr<int> clone(int p)
{unique_ptr<int> ret(new int(p));return ret;
}

注:

  • 向后兼容:auto_ptr :标准库的较早版本包含了一个名为auto_ptr的类,它具有uniqued_ptr的部分特性,但不是全部。
  • 用unique_ptr传递删除器:unique_ptr默认使用delete释放它指向的对象,我们可以重载一个unique_ptr中默认的删除器。我们必须在尖括号中unique_ptr指向类型之后提供删除器类型。在创建或reset一个这种unique_ptr类型的对象时,必须提供一个指定类型的可调用对象删除器。

4、weak_ptr

weak_ptr是一种不控制所指向对象生存期的智能指针,它指向一个由shared_ptr管理的对象,将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放,即使有weak_ptr指向对象,对象还是会被释放。

weak_ptr是指向shared_ptr的对象,然而shared_ptr也可以用shared_ptr来接收,那么为什么还要用weak_ptr来接收呢?实际上,使用weak_ptr可以防止用户访问一个不再存在的对象,同时使用weak_ptr一般意味着weak_ptr所指的对象可能会被销毁。

weak_ptr的操作:

由于对象可能不存在,我们不能使用weak_ptr直接访问对象,而必须调用lock,此函数检查weak_ptr指向的对象是否存在。如果存在,lock返回一个指向共享对象的shared_ptr,如果不存在,lock将返回一个空指针。

例:


if(shared_ptr<int>  np  =  wp.lock())    {       // 若np不为空则条件成立// 在if中,np和p共享对象
}

5、auto_ptr

auto_ptr 是C++标准库提供的类模板,auto_ptr对象通过初始化指向由new创建的动态内存,它是这块内存的拥有者,一块内存不能同时被分给两个拥有者。当auto_ptr对象生命周期结束时,其析构函数会将auto_ptr对象拥有的动态内存自动释放。即使发生异常,通过异常的栈展开过程也能将动态内存释放。auto_ptr不支持new 数组。

(1)初始化auto_ptr

1) 构造函数

1] 将已存在的指向动态内存的普通指针作为参数来构造

int* p = new int(33);
auto_ptr<int> api(p);

2] 直接构造智能指针

auto_ptr< int > api( new int( 33 ) );

2) 拷贝构造

利用已经存在的智能指针来构造新的智能指针:

auto_ptr< string > pstr_auto( new string( "Brontosaurus" ) );
auto_ptr< string > pstr_auto2( pstr_auto );  //利用pstr_auto来构造pstr_auto2

因为一块动态内存只能由一个智能指针独享,所以在拷贝构造或赋值时都会发生拥有权转移的过程。在此拷贝构造过程中,pstr_auto将失去对字符串内存的所有权,而pstr_auto2将其获得。对象销毁时,pstr_auto2负责内存的自动销毁。

3) 赋值

利用已经存在的智能指针来构造新的智能指针

auto_ptr< int > p1( new int( 1024 ) );
auto_ptr< int > p2( new int( 2048 ) );
p1 = p2;

在赋值之前,由p1 指向的对象被删除。赋值之后,p1 拥有int 型对象的所有权。该对象值为2048。p2不再被用来指向该对象。

4)直接定义空的auto_ptr

通常的指针在定义的时候若不指向任何对象,我们用Null给其赋值。对于智能指针,因为构造函数有默认值0,我们可以直接定义空的auto_ptr如下:

auto_ptr< int > p_auto_int;  //不指向任何对象

5)防止两个auto_ptr对象拥有同一个对象(一块内存)

因为auto_ptr的所有权独有,所以下面的代码会造成混乱。

int* p = new int(0);
auto_ptr<int> ap1(p);
auto_ptr<int> ap2(p);

因为ap1与ap2都认为指针p是归它管的,在析构时都试图删除p, 两次删除同一个对象的行为在C++标准中是未定义的。所以我们必须防止这样使用auto_ptr。

6)警惕智能指针作为参数!

a、按值传递时,函数调用过程中在函数的作用域中会产生一个局部对象来接收传入的auto_ptr(拷贝构造),这样,传入的实参auto_ptr就失去了其对原对象的所有权,而该对象会在函数退出时被局部auto_ptr删除。如下例:

void f(auto_ptr<int> ap)
{cout<<*ap;}
auto_ptr<int> ap1(new int(0));
f(ap1);
cout<<*ap1; //错误,经过f(ap1)函数调用,ap1已经不再拥有任何对象了。

b、引用或指针时,不会存在上面的拷贝过程。但我们并不知道在函数中对传入的auto_ptr做了什么,如果当中某些操作使其失去了对对象的所有权,那么这还是可能会导致致命的执行期错误。

结论:const reference是智能指针作为参数传递的底线。

7)auto_ptr不能初始化为指向非动态内存

原因很简单,delete 表达式会被应用在不是动态分配的指针上这将导致未定义的程序行为。

8)auto_ptr常用的成员函数

a、get()

返回auto_ptr指向的那个对象的内存地址。

b、 reset()

重新设置auto_ptr指向的对象。类似于赋值操作,但赋值操作不允许将一个普通指针直接赋给auto_ptr,而reset()允许。

注:reset(0)可以释放对象,销毁内存。

c、release()

返回auto_ptr指向的那个对象的内存地址,并释放对这个对象的所有权。

用此函数初始化auto_ptr时可以避免两个auto_ptr对象拥有同一个对象的情况(与get函数相比)。

6、scoped_ptr

scoped_ptr是一个类似于auto_ptr的智能指针,它包装了new操作符在堆上分配的动态对象,能够保证动态创建的对象在任何时候都可以被正确的删除。但是scoped_ptr的所有权更加严格,不能转让,一旦scoped_pstr获取了对象的管理权,你就无法再从它那里取回来。正如scoped_ptr(局部指针)名字的含义:这个智能指针只能在作用域里使用,不希望被转让。

scoped和weak_ptr的区别就是,给出了拷贝和赋值操作的声明并没有给出具体实现,并且将这两个操作定义成私有的,这样就保证scoped_ptr不能使用拷贝来构造新的对象也不能执行赋值操作,更加安全,但有了”++”“–”以及“*”“->”这些操作,比weak_ptr能实现更多功能。

(1)scoped_ptr用法

scoped_ptr的用法与普通的指针几乎没什么区别;最大的差别在于你不必再记得在指针上调用delete,还有就是scoped_ptr不允许复制。典型的指针操作(operator* 和 operator->)都被重载了,并提供了和裸指针一样的语法。用scoped_ptr和用裸指针一样快,也没有大小上的增加,因此它们可以广泛使用。

(2)成员函数

1)explicit scoped_ptr(T* p=0) 
       构造函数,存储p的一份拷贝。注意,p 必须是用operator new分配的,或者是null. 在构造的时候,不要求T必须是一个完整的类型。当指针p是调用某个分配函数的结果而不是直接调用new得到的时候很有用:因为这个类型不必是完整的,只需要类型T的一个前向声明就可以了。这个构造函数不会抛出异常。

2)~scoped_ptr() 
       删除指针所指向的对象。类型T在被销毁时必须是一个完整的类型。如果scoped_ptr在它被析构时并没有保存资源,它就什么都不做。这个析构函数不会抛出异常。

3)void reset(T* p=0); 
       重置一个 scoped_ptr 就是删除它已保存的指针,如果它有的话,并重新保存p. 通常,资源的生存期管理应该完全由scoped_ptr自己处理,但是在极少数时候,资源需要在scoped_ptr的析构之前释放,或者scoped_ptr要处理它原有资源之外的另外一个资源。这时,就可以用reset,但一定要尽量少用它。(过多地使用它通常表示有设计方面的问题) 这个函数不会抛出异常。

4)T& operator*() const; 
       该运算符返回一个智能指针中存储的指针所指向的对象的引用。由于不允许空的引用,所以解引用一个拥有空指针的scoped_ptr将导致未定义行为。如果不能肯定所含指针是否有效,就用函数get替代解引用。这个函数不会抛出异常。

5)T* operator->() const; 
       返回智能指针所保存的指针。如果保存的指针为空,则调用这个函数会导致未定义行为。如果不能肯定指针是否空的,最好使用函数get。这个函数不会抛出异常。

6)T* get() const; 
       返回保存的指针。应该小心地使用get,因为它可以直接操作裸指针。但是,get使得你可以测试保存的指针是否为空。这个函数不会抛出异常。get通常在调用那些需要裸指针的函数时使用。

7)operator unspecified_bool_type() const 
       返回scoped_ptr是否为非空。返回值的类型是未指明的,但这个类型可被用于Boolean的上下文(boolean context)中。在if语句中最好使用这个类型转换函数,而不要用get去测试scoped_ptr的有效性。

8)void swap(scoped_ptr& b) 
       交换两个scoped_ptr的内容。这个函数不会抛出异常。

参考:https://blog.csdn.net/flowing_wind/article/details/81301001

C++中智能指针详解相关推荐

  1. C++ 智能指针详解

    智能指针内容很多,重点是基本用法. #include <boost/shared_ptr.hpp> class CBase: public boost::enable_shared_fro ...

  2. C/C++智能指针详解

    系列文章目录 文章目录 系列文章目录 前言 一.什么是智能指针? 二.使用方法 1.shared_ptr 2.unique_ptr 3.weak_ptr 前言 对C/C++学习感兴趣的可以看看这篇文章 ...

  3. c++中的智能指针详解

    智能指针是行为类似于指针的类对象,但是这种对象还有其他的功能.一般用于帮助管理动态内存. 我们需要知道,我们每次在对象中new出一块内存空间都需要在对象销毁时delete掉,否则就会造成内存泄露,这些 ...

  4. 【C++】智能指针详解

    相关博文<C++ STL 四种智能指针> 参考资料:<C++ Primer中文版 第五版> 我们知道除了静态内存和栈内存外,每个程序还有一个内存池,这部分内存被称为自由空间或者 ...

  5. C++-智能指针详解

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

  6. 【C++】unique_ptr独占型智能指针详解

    指针是C/C++区别于其他语言的最强大的语法特性,借助指针,C/C++可以直接操纵内存内容.但是,指针的引入也带来了一些使用上的困难,这要求程序员自己必须手动地对分配申请的内存区进行管理. uniqu ...

  7. C++智能指针详解【C++智能指针】

    自动内存管理 智能指针 什么是 RAII 原理 智能指针的模板(template)实现 auto_ptr auto_ptr 使用 重载函数 operator-> / *语法格式 自实现 auto ...

  8. C++智能指针详解(auto_ptr、unique_ptr、shared_ptr)

    文章目录 1. 智能指针的应用场景 2. 智能指针的介绍 3. 智能指针的使用与原理 3.1 auto_ptr 3.2 unique_ptr 3.3 shared_ptr 3.4 shared_ptr ...

  9. 【C++】智能指针详解及原理简单说明

    文章目录 1.智能指针前提知识 1.1 为什么需要智能指针? 1.2 简单理解内存泄漏 1.3 内存泄漏的分类 1.4 内存泄漏解决方案 2.智能指针的原理 2.1 RAII(资源获取即初始化) 2. ...

最新文章

  1. 从睡姿就可以看出你的性格,据说非常准,快存!
  2. 对象创建的过程细节是怎样的?一起来探讨内存变化细节
  3. 交换机设备登录账号权限1_在交换机中用户权限分为几个级别 分别是什么
  4. Kubernetes基础学习(一)
  5. SpringAOP aspectJ ProceedingJoinPoint 获取当前方法
  6. 八、Vue cli3详解学习笔记
  7. 怎样在linux系统上安装r,Linux系统之路——如何在CentOS7.2安装R(示例代码)
  8. 什么C++程序员,什么Java程序员
  9. 五千的手机和两三千的手机使用起来有什么不一样?有必要买贵的吗?
  10. 数据库开源 | 200人中英文混说数据库开放申请
  11. 用JAVA Excel API 实现对Excel表格的读写更新 (转)
  12. 【动态规划】计蒜客:跳木桩(最长递增子序列的变体)
  13. 关于flash强制更新:早上上班,多台电脑提示未安装flash
  14. Home Barbering Grows In Recession, With Hairy Results
  15. 苹果MacBook Air M2开箱测评 M2版MacBook Air真实体验感受
  16. python e_python 常数e
  17. Ceph Recovery分析
  18. 为何一些人认为从事 IT 行业的人是屌丝男?
  19. 首个AI数字人唱作歌曲刷屏背后:百度APP数字人度晓晓用AI陪伴亿万用户
  20. npm安装vue,在vue/dist目录下没有产生vue.js文件

热门文章

  1. WIN10 qCOW2 下载
  2. 微信 jssdk 语音监听播放结束的坑
  3. stringByAppendingString和stringByAppendingPathComponent
  4. Android 实现APP内应用更新功能(支持Android7.0以上)
  5. Android 更新包与已安装应用的签名不一致
  6. 个人所得税计算C语言实现
  7. 350导热油 shell_导热油320与350的区别,克拉克给你详细解说
  8. sharepoint 2016 学习系列篇(5)-创建一个应用程序网站
  9. Portraiture5人像磨皮润色修饰插件
  10. android7.0 拍照以及图片剪辑--(再次修改)