C++ 智能指针详解

2023-11-20

点击蓝字

8bb66403bfc0380ccd952a86fac14091.png

关注我们

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

我们知道除了静态内存和栈内存外,每个程序还有一个内存池,这部分内存被称为自由空间或者堆。

程序用堆来存储动态分配的对象即那些在程序运行时分配的对象,当动态对象不再使用时,我们的代码必须显式的销毁它们。

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

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

为了更加容易(更加安全)的使用动态内存,引入了智能指针的概念。智能指针的行为类似常规指针,重要的区别是它负责自动释放所指向的对象。

标准库提供的两种智能指针的区别在于管理底层指针的方法不同,shared_ptr允许多个指针指向同一个对象,unique_ptr则“独占”所指向的对象。

标准库还定义了一种名为weak_ptr的伴随类,它是一种弱引用,指向shared_ptr所管理的对象,这三种智能指针都定义在memory头文件中。

shared_ptr类

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

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

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

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

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

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

b41ec6383b694281fa4c303ae1681f16.png
这里写图片描述

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

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用其参数来构造给定类型的对象,如果我们不传递任何参数,对象就会进行值初始化

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的析构函数会递减它所指向的对象的引用计数。

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

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<int> *pv = new vector<int>{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的指针。

内存耗尽: 
虽然现代计算机通常都配备大容量内村,但是自由空间被耗尽的情况还是有可能发生。一旦一个程序用光了它所有可用的空间,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赋予指针,这样就清楚的指出指针不指向任何对象。 

动态内存的一个基本问题是可能多个指针指向相同的内存

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

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

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

1f837c52fc242339777c01e372ce26bf.png
这里写图片描述

不要混合使用普通指针和智能指针  
如果混合使用的话,智能指针自动释放之后,普通指针有时就会变成悬空指针,当将一个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

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

智能指针和异常  
如果使用智能指针,即使程序块过早结束,智能指针也能确保在内存不再需要时将其释放,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分配的内存,记住传递给它一个删除器

unique_ptr

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

下表是unique的操作: 

da364b98a8ac806a1658fb2de3f3101f.png
这里写图片描述

虽然我们不能拷贝或者赋值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<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类型的对象时,必须提供一个指定类型的可调用对象删除器。

weak_ptr

weak_ptr 是一种不控制所指向对象生存期的智能指针,它指向一个由shared_ptr管理的对象,将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。

一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放,即使有weak_ptr指向对象,对象还是会被释放。 
weak_ptr的操作  

e796204aa45c9bde0cac07b99e5e1dec.png
这里写图片描述

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

scoped_ptr

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

*声明:本文于网络整理,版权归原作者所有,如来源信息有误或侵犯权益,请联系我们删除或授权事宜。

5b85629c2e654bcb65a67a66601436c8.png

5fab5371ffe9d1159ef80b5229af3b34.gif

戳“阅读原文”我们一起进步

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

C++ 智能指针详解 的相关文章

随机推荐

  • 吴恩达机器学习笔记系列(五)——梯度下降

    一 gradient descent 梯度下降 1 概念 线性回归的目的就是找出使得误差 损失函数 最小的参数值 可以用梯度下降来确定 参数的大小 梯度下降是一种迭代方法 能够求解局部最小值 结果与初始点的选取有关 为了找到最小值 从某一点
  • ADFS 4.0 证书更新

    ADFS 4 0 证书更新 由于公网证书的过期 需要重新更新ADFS的服务通信证书 证书要求 带私钥 PFX格式 更换流程 证书安装到 证书 计算机 个人 安装后点开证书能看到 你有一个与证书对应的私钥 右击证书 gt 所有任务 gt 管理
  • 【uboot内核适配学习】uboot 修改默认ip

    1 修改默认ip作用 设备出场的时候都需要默认的ip 2 修改措施 找到uboot芯片配置文件 不同芯片厂家适配的文件必定是不一样的 位置也可能不一样 define CONFIG ETHADDR 00 40 5c 26 0a 5b MAC地
  • 面经——小米面经(2021春招)

    摘自 小米面经 2021春招 感谢小米 感谢雷总 感谢上官可编程 作者 阿波罗啦啦啦啦 发布时间 2021 05 01 11 08 41 网址 https blog csdn net weixin 44933419 article deta
  • 博客营销

    1 博客营销有什么价值 应注意什么 1 博客可以直接带来潜在用户 2 博客营销的价值体现在降低网站推广费用方面 3 博客文章内容为用户通过搜索引擎获取信息提供了机会 4 博客文章可以方便地增加企业网站的链接数量 5 可以实现更低的成本对读者
  • 指标体系建设

    1 背景 结合业务场景将多个不同指标和维度进行组合 从而针对某一真实业务场景进行数据分析和决策导向 并能在整体业务变化中发现和定位问题 2 概念理解与示例分析 2 1 指标体系 指标体系 名称 分类 解析 作用 示例 指标 结果型指标 时机
  • 汉诺塔问题 java

    汉诺塔问题 public class HanoiTower 编写一个main方法 public static void main String args Tower t1 new Tower t1 move 5 A B C 汉诺塔问题的解决
  • React事件处理及事件流

    React事件处理 React事件处理是通过将事件处理器绑定到组建上处理事件 事件触发的同时更新组建的内部状态 内部状态更新会触发组件的重绘 React 元素的事件处理和 DOM 元素的事件处理很相似 但语法上的略有区别 在React中事件
  • 如何删除gitee仓库下的文件

    有时我们可能在上传项目到github或者gitee时 忘记忽略了某个文件 就直接push上去了 最后发现上传多了 如何删除掉远程仓库中的文件呢 注 在github上我们只能删除仓库 无法删除文件夹或文件 所以只能通过命令 2 打开GitBa
  • Python 程序设计习题(4) —— 列表与元组

    目录 1 Python 习题部分 2 Python 习题讲解 列表 元组 其他 1 Python 习题部分 要想学习一门语言 便少不了练习 故附上部分 Python 习题 供大家学习参考 如有错误之处 还望指正 1 二年级一班举行了数学考试
  • springboot项目中application.properties无法变成小树叶问题解决

    1 检查我们的resources目录的状态 看看是不是处在普通文件夹的状态 如果是的话 我们需要重新mark一下 右键点击文件夹 选择mark directory as resources root 此时我们发现配置文件变成了小树叶 2 如
  • ScheduledThreadPoolExecutor周期定时任务异常处理踩坑的问题!!

    问题原因 在公司写项目的时候 有一个周期定时任务的需求 就想着阿里巴巴开发手册里不是说不能用Executors去创建线程池 因为存在如下问题 FixedThreadPool和SingleThreadPool 允许的请求队列长度为 Integ
  • NVME Format Command 个人笔记

    Format NVM command NVM Command Set Specific This command is used by the host to change the LBA data size and or metadata
  • 【图像压缩】QOI图像格式详解

    最近听说一种图像格式比较流行 想起我曾经是做图像压缩的emmmm 就来研究一下 QOI Quite OK Image Format 很好的图像格式 git链接 能快速地无损压缩图像 原理也非常简单 没有各种变换 直接空域处理 而无损压缩 自
  • linux入门系列20--Web服务之LNMP架构实战

    作为本入门系列最后一篇文章 将演示如何在CentOS7环境下搭建LNMP环境来构建个人博客网站 常见搭建网站的方式有LAMP LNMP IIS Nginx Tomcat等等 本文演示比较流行的基于LNMP方式来搭建动态WEB网站 正如前文
  • VScode配置C++(win11)以及Vscode的一些使用问题

    目录 一 下载VScode 省略 二 下载编译器 mingw 三 配置 vscode 四 补充 配置好后 输出中文会乱码 五 文件参数讲解 六 多文件编译 修改task json 七 中文问题 一 下载VScode 省略 二 下载编译器 m
  • 测试设计提升之路

    当前软件行业中有很多职位 其中开发与测试可以说是TOP2热门 测试相对开发来说入门容易 但要快速达到巅峰 我们需要掌握一些方法与套路 测试工作是一个繁琐的工作 一个人的精气神有限 在规定的时间内需要掌握多种技术 而且要达到精通非常困难 就测
  • java 使用匿名内部类的方式创建线程并设置和获取线程名字

    有些方法需要传入接口的实例或者抽象类的实例对象 比如Thread有一个构造方法 Thread Runnable target 这时可以可以自定义类实现Runnable接口 重写接口中的方法 将自定义类的对象传入构造方法中 也可以使用匿名内部
  • K8S学习--Kubeadm-7--Ansible二进制部署

    K8S学习 Kubeadm 安装 kubernetes 1 组件简介 K8S学习 Kubeadm 安装 kubernetes 2 安装部署 K8S学习 Kubeadm 3 dashboard部署和升级 K8S学习 Kubeadm 4 测试运
  • C++ 智能指针详解

    点击蓝字 关注我们 参考资料 C Primer中文版 第五版 我们知道除了静态内存和栈内存外 每个程序还有一个内存池 这部分内存被称为自由空间或者堆 程序用堆来存储动态分配的对象即那些在程序运行时分配的对象 当动态对象不再使用时 我们的代码