C++中的智能指针

目录

背景

裸指针

智能指针

原理

智能指针

auto_ptr

unique_ptr

       1. unique_ptr禁止拷贝构造(copy constructor)和赋值运算(=)

          1.1 C++提供了标准库函数move()

         1.2.如果unique_ptr是一个临时右值

       2. unique_ptr可用于数组 

shared_ptr

     

      环状引用问题

weak_ptr

    注意:

总结


 

背景

  • 裸指针

如果编程中直接使用裸指针,就需要程序员手动的去allocate和release资源了。假设有这样一段代码:

void f(){
  int* a = new int(100);  //申请资源
   ....
  delete a; //release a所指向的对象
}

这段程序乍看起来没有什么问题,new和delete有成对地出现,应该不会发生内存泄漏的情况吧。那如果程序员忘记写delete了;或者是像上面一样有加delete,但这new和delete中间的"...."区域内是否有可能出现过早的return语句呢,亦或是这块"...."区域的语句抛出了异常,无论是哪一种原因,最后其实都没有做delete,那这种情况下内存泄露就发生了。

那怎么避免这种问题呢?最直接的想法可能就是“既然前面说的问题是因为程序可能会提前退出导致的,那就在程序每一个可能会退出的地方,都加上一个delete,以进行全面的防守”。这样子做当然没有问题,但是越到后面,可能就越难维护了,假设后面的人,在这段程序里又加了一些内容,使得程序可能又新增了一个会提前退出的地方,但是后面的人不知道这个函数里还有着对资源管理的事情要做,而忘记了加delete,还是会引入问题。所以单纯地依赖在f()里总是会手动地执行其delete语句不是一个很好的做法。

  • 智能指针

那前面说的问题还有其他更好的解决办法吗?答案是:有。这个方法就是采用智能指针(smart pointer). 

智能指针能够解决前面说的“因为忘记delete或者是提前退出导致的内存泄漏问题”,在介绍智能指针是如何使用之前,还是需要了解一下智能指针是怎么解决掉前面说的那些问题的,它能解决这个问题的原理是什么。

  • 原理

智能指针是基于这样一个思想:以对象来管理资源。简单来说就是,把资源放进一个对象里面,将资源释放的动作放在析构函数里,我们便可以依赖C++的析构函数自动调用机制确保资源被释放。

“以对象来管理资源”的观念常被称为“资源取得时机便是初始化时机”(Resource Acquisition Is Initialization; RAII)”,这里面有两个关键的想法:

  • 获得资源后立刻放进管理对象内:每一笔资源在获得的同时立刻被放进管理对象当中
  • 管理对象运用析构函数确保资源被释放:不论流程上如何控制,一旦管理对象被销毁,其析构函数自然会被调用,其所管理的资源也就会被释放掉

没错,智能指针正是一个类(模板类,因为指针类型很多),它所管理的资源就是指针,可以简单地理解为就是在这个类的析构函数里有做一个delete的动作。智能指针这个模板类重载了一些指针相关的运算操作符(比如“*”,“->”),让程序员在操作这个对象时就像是操作普通指针一样。所以智能指针不仅使得人们可以像使用普通指针那样去使用它,还可以防止对内存的不当使用。

这里再多说几句:以对象来管理资源,其实不仅仅是内存是资源,其他的比如:互斥锁(Mutex)、文件描述器(File descriptor) 等其实也是资源,一旦你用了它,将来就必须要还给系统,如果不这样,就会发生各种事故。资源交给对象来管理,只是不同的资源所对应的管理对象不一样,比如互斥锁的管理对象一般使用的是lock_guard、unique_lock这些类的对象(C++ 多线程 (mutex & conition_variable篇)),但是基本原理是一样的,就是利用了C++对象的构造函数和析构函数的自动调用机制,防止资源泄漏发生。

 

智能指针

前面介绍了智能指针的大概原理,接下来看看C++里提供的智能指针有哪些,以及用法。

C++提供的智能指针有如下几种:auto_ptr(C++11中已经摒弃)、shared_ptr、weak_ptr和unique_ptr,就像前面说的,它们都是模板类。关于这些智能指针:

  1. 要创建智能指针对象,必须包含头文件<memory>;
  2. 所有的指针类都有一个explicit构造函数,该函数将指针作为参数
  3. 可以通过智能指针类提供的get()方法获取智能指针所指向的原始资源
  4. 使用通常的模板语法来实例化所需类型的指针(weak_ptr的构造方法稍微特殊点,后面会介绍),比如:
auto_ptr<int> a(new int(100));
unique_ptr<int> a(new int(100));
shared_ptr<int> a(new int(100));
weak_ptr<int> b = a; 

为什么会有多种不同类型的智能指针呢?接下来介绍一下这些不同类型的智能指针的差异

auto_ptr

auto_ptr是C++ 98提出来的,C++11中已经摒弃了。它的用法如下:

void f(){
 auto_ptr<int> a(new int(100));
 ....
//后面就不用再手动去加delete了
}

当通过copy构造函数或者时赋值运算符复制它们,它们会变成NULL,而复制所得的指针将取得资源的唯一拥有权

void f(){
  auto_ptr<int> a(new int(100)); //a指向new int返回的地址
  auto_ptr<int> b(a); //现在b指向对象,a被置为NULL
  a = b; //现在a指向对象,b置为NULL
}

这意味着受auto_ptr管理的资源没有一个以上的auto_ptr可以同时指向它。但是在auto_ptr放弃了对象的所有权后,这样就留下了一个悬挂指针,使用者后面还有可能使用它来访问该对象(比如上面的例子中,最后再做一个“*b”的操作),但显然这会导致问题.

 

unique_ptr

独占式指针,即同一时刻只能有一个指针指向同一个对象。如果程序不需要多个指向同一个对象的指针,则可使用unique_ptr。

 这一点看起来和auto_ptr是一样的,都是对于一个资源来说,只有一个指针指向它。但是它们之间有如下差别:

       1. unique_ptr禁止拷贝构造(copy constructor)和赋值运算(=)

对于unique_ptr来说,编译器会认为下面#3这一步是非法操作,直接编译阶段就会报错,这样就避免了a不再指向有效数据的问题,所以从这一点来讲,unique_ptr比auto_ptr更安全。

void f_unique() {
	unique_ptr<int> a(new int(100)); //#1
	unique_ptr<int> b;               //#2
	b = a;                           //#3 not allowed
}

但如果真的想把unique_ptr赋给另一个的话,其实也不是不行,如下两种情况可以做到将一个unique_ptr赋给另一个: 

          1.1 C++提供了标准库函数move()

比如想将上面的a赋给b,则可以使用move()

void f_unique() {
	unique_ptr<int> a(new int(100));
	cout << "Address: " << a.get() << "; *a = " << *a << endl;
	unique_ptr<int> b;
	b = move(a);                          //not allowed
	cout << "Address: " << b.get() << "; *b = " << *b << endl;

	cout << "Address: " << a.get() << endl;
}

 5966f9f9e47e42c4bdf98d21d5d56a18.png

         1.2.如果unique_ptr是一个临时右值

如果unique_ptr是一个临时右值,编译器允许这样做;如果unique_ptr将存在一段时间,编译器将禁止这样做。

如下面这段示例,demo返回的是一个临时的unique_ptr,然后a接管了原本返回的unique_ptr所拥有的对象,而返回的unique_ptr被销毁。这样的话,没有机会使用返回的临时unique_ptr对象来访问无效的数据,这样的赋值行为想象上也应该要是合理的

unique_ptr<int> demo(int a) {
	unique_ptr<int> res(new int(a));
	return res;
}

void testDemo() {
	unique_ptr<int> a;
	a = demo(10);
	cout << "Address: " << a.get() << "; *a = " << *a << endl;
}

       2. unique_ptr可用于数组 

  unique_ptr<int []> a(new int(100));

但是auto_ptr不可以,因为auto_ptr里使用的是delete而没有使用delete [],所以只能与new一起使用,无法与delete []一起使用,所以无法用于数组。而unique_ptr里有使用new []和delete []的版本。

 

shared_ptr

shared_ptr是计数型智能指针,同一时刻可以有多个指针指向同一个对象,并在无人指向它时自动删除该资源。它的大概原理是:shared_ptr内部有一个计数器,每当新增一个指向同一个对象的指针时,那么这个计数器会加1;反之,每有一个指针被释放,计数器减1,如果计数器为0时,shared_ptr就会释放指向的对象。

所以可见shared_ptr和前面所说的auto_ptr、unique_ptr最明显的不同就是:shared_ptr允许多个指针同时指向一个对象,而auto_ptr、unique_ptr都只允许同一时刻只能有一个指针指向同一个对象。

如果程序要使用多个指向同一个对象的指针,则应该选择shared_ptr。如下这段程序所示, a,b,c这三个shared_ptr都同时指向了同一块内存

void f() {
	shared_ptr<int> a(new int(100));
	cout << "Adress: " <<a.get() << "; *a = " << *a << endl;

	shared_ptr<int> b(a);
	cout << "Adress: " << b.get() << "; *b = " << *b << endl;

	shared_ptr<int> c = a;
	cout << "Adress: " << c.get() << "; *c = " << *c << endl;
	return;
}


80fa8776eac84648b253899c4c3ed2ec.png

     

      环状引用问题

环状引用是指两个或多个对象之间相互引用,形成一个闭环的情况。在面对环状引用时,shared_ptr无法打破这种情况,会引起内存泄漏。

假设有这样一段代码:

class A;
class B;

class A {
  public:
	shared_ptr<B> b_ptr;
};

class B {
  public:
	shared_ptr<A> a_ptr;
};

void testCycles() {
	shared_ptr<A> a = make_shared<A>(); //#1
	shared_ptr<B> b = make_shared<B>(); //#2

	a->b_ptr = b;  //#3
	b->a_ptr = a;  //#4

	return;
}

1.经过上面的#1和#2后,指向a和b的shared_ptr引用计数各自为1,即当前各有一个shared_ptr有分别指向a和b

2. 经过#3和#4后,a->b_ptr指向了b,   b->a_ptr指向a, 因为a和b有互指,则各自的引用计数变为2,如下图所示:

b24ee3b221494350b4f45ac13737cfe8.png

3. 当a和b都脱离作用域,各自调用自己的析构函数后,a和b各自的引用计数减为1,但是a和b的资源还没有release,a还有指向b,b也还有指向a.

4. a释放,就需要b的a_ptr释放,b释放,就需要a的b_ptr释放。因此a和b互相约束着对方的析构,最后都没法析构,导致内存泄漏。

这有点儿“死锁”的味道了。所以环状引用会导致对象的引用计数无法减为0,从而导致内存泄漏。为了解决环状引用的问题,可以使用weak_ptr来打破环状引用。weak_ptr是一种弱引用,它不会增加对象的引用计数,也不会阻止对象的销毁。下面介绍一下weak_ptr。

 

weak_ptr

weak_ptr是用来解决shared_ptr相互引用导致的内存泄漏问题,是shared_ptr的辅助类。具体来说,就是在环状引用中,将其中一个对象的指针使用weak_ptr来引用另一个对象,而不是使用shared_ptr。这样,在两个对象相互引用时,当所有的shared_ptr都释放了所指向的对象,即使还有weak_ptr指向该对象,对象也会被销毁,不会导致引用计数无法降为0,从而避免了内存泄漏的问题

因此,上面那段示例代码可以将A or B里的某一方改为weak_ptr:

class A;
class B;

class A {

public:
	shared_ptr<B> b_ptr;
};

class B {

public:
	weak_ptr<A> a_ptr;
};

void testCycles() {
	shared_ptr<A> a = make_shared<A>();
	shared_ptr<B> b = make_shared<B>();

	a->b_ptr = b;
	b->a_ptr = a;

	return;
}

    注意:

    1. weak_ptr不参与资源的管理和释放,可以使用shared_ptr对象来构造weak_ptr对象,但是不能直接使用指针来构造weak_ptr对象,如下的语法是非法的

weak_ptr<int> a(new int(100));

   2. weak_ptr没有operator*函数和operator->成员函数,不具有一般指针的行为。

 

总结

在实际中,应尽量避免直接使用裸指针,而应优先选择智能指针,它们利用对象的生命周期来管理资源,避免了资源泄漏这些问题。并且针对不同的需求选用合适类型的智能指针。

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/355858.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

CRM管理系统有哪些应用价值?

如何杜绝员工飞单、走私单&#xff1f; 如何避免员工离职带走客户&#xff1f; ——crm管理系统 企业要想取得成功&#xff0c;有强大的技术支持和合适的工具辅助&#xff0c;crm管理系统这类工具就是客户管理系统&#xff0c;它是一种先进的管理模式&#xff0c;crm管理系统通…

浅析透明图片显示

1、理解图片构成 上面是一个飞机的透明图片&#xff0c;每个图片都是有一个个像素点构成的&#xff0c;每个像素点都是一个颜色&#xff0c;在内存中占4个字节&#xff0c;由透明度、红、绿、蓝构成。如下图&#xff1a; 该飞机图片飞机图片长51像素&#xff0c;宽63像素。就是…

苹果m1、m2安装blender GIS,解决not Imageio 报错

blender-GIS 能够在地图上生成地形&#xff0c;如下图所示&#xff1a; 使用blenderGIS过程中会有 imageio 找不到的情况&#xff0c; 网上的 imageio 用在苹果电脑的M1、M2芯片上好像还是没有出图&#xff0c;似乎这个 imageio 这个已经是好几年前的代码&#xff0c;没能适配…

Sphinx的原理详解和使用

一、Sphinx介绍 1.1 简介 Sphinx是由俄罗斯人Andrew Aksyonoff开发的一个全文检索引擎。意图为其他应用提供高速、低空间占用、高结果 相关度的全文搜索功能。Sphinx可以非常容易的与SQL数据库和脚本语言集成。当前系统内置MySQL和PostgreSQL 数据库数据源的支持&#xff0c;也…

Vue3 + Express 实现大文件分片上传、断点续传、秒传

目录 前言原理实现项目搭建获取文件文件分片hash计算文件上传文件合并秒传&断点续传 完整代码 前言 在日常开发中&#xff0c;文件上传是常见的操作之一。文件上传技术使得用户可以方便地将本地文件上传到Web服务器上&#xff0c;这在许多场景下都是必需的&#xff0c;比如…

比特币ETF通过为BiFinance带来全新机遇

2013年7月&#xff0c;Winklevoss兄弟提交了首个比特币交易所交易基金&#xff08;ETF&#xff09;申请&#xff0c;随后多家公司纷纷效仿&#xff0c;但美国证券交易委员会&#xff08;SEC&#xff09;均以“容易受到市场操纵”为由驳回了这些申请。时至2024年伊始&#xff0c…

TCP_拥塞控制

引言 24年春节马上就要到了&#xff0c;作为开车党&#xff0c;最大的期盼就是顺利回家过年不要堵车。梦想是美好的&#xff0c;但现实是骨感的&#xff0c;拥堵的道路让人苦不堪言。 在网络世界中&#xff0c;类似于堵车的问题也存在&#xff0c;而TCP&#xff08;Transmissi…

环形链表找入环点----链表OJ---三指针

https://leetcode.cn/problems/linked-list-cycle-ii/description/?envTypestudy-plan-v2&envIdtop-100-liked 首先&#xff0c;需要判断是否有环&#xff0c;而这里我们不单纯判断是否有环&#xff0c;还要为下一步做准备&#xff0c;需要让slow指针和fast都从头结点开始…

【数据结构1-1】线性表

线性表是最简单、最基本的一种数据结构&#xff0c;线性表示多个具有相同类型数据“串在一起”&#xff0c;每个元素有前驱&#xff08;前一个元素&#xff09;和后继&#xff08;后一个元素&#xff09;。根据不同的特性&#xff0c;线性表也分为数组&#xff08;vector&#…

MySQL 学习记录

基本常识 row-size-limitsblob&#xff1a; BLOB and TEXT columns cannot have DEFAULT values.Instances of BLOB or TEXT columns in the result of a query that is processed using a temporary table causes the server to use a table on disk rather than in memory b…

C++11——新的类功能与可变参数模板

系列文章目录 文章目录 系列文章目录一、新的类功能默认成员函数类成员变量初始化强制生成默认函数的关键字default禁止生成默认函数的关键字delete继承和多态中的final与override关键字 二、可变参数模板递归函数方式展开参数包逗号表达式展开参数包STL容器中的empalce_back与…

写点东西《JWT 与会话身份验证》

写点东西《JWT 与会话身份验证》 身份验证与授权 JWT 与session身份验证 - 基本差异 什么是 JWT&#xff1f; JWT 结构&#xff1a; JWT 工作流程&#xff1a;优势: 安全问题&#xff1a; 处理令牌过期&#xff1a; 基于session的身份验证&#xff08;通常称为基于 cookie 的身…

工程对接大模型流式和非流式对话底层原理解析

文章目录 前言一、非流式输出设计二、stream流式输出设计三、手撸一个流式输出项目总结 前言 之前对接过OpenAi大模型的官方API&#xff0c;可以看到它有一个Stream参数&#xff0c;设置成true的时候就是流式的对话输出&#xff0c;现象就是一段一段的往外崩。 官方手册的地址…

蓝桥杯训练|基础语言Day1 - STL pair vector list stack queue set map容器

学习目标&#xff1a; 博主介绍: 27dCnc 专题 : 算法题入门 &#x1f44d;&#x1f44d;&#x1f44d;&#x1f44d;&#x1f44d;&#x1f44d;&#x1f44d;&#x1f44d;&#x1f44d;&#x1f44d;&#x1f44d;&#x1f44d; ☆*: .&#xff61;. o(≧▽≦)o .&#xff61…

Python爬虫案例展示:实现花猫壁纸数据采集

嗨喽~大家好呀&#xff0c;这里是魔王呐 ❤ ~! python更多源码/资料/解答/教程等 点击此处跳转文末名片免费获取 环境使用: Python 3.10 Pycharm 模块使用: import requests >>> pip install requests win R 输入cmd 输入安装命令 pip install requests 安装即…

Springboot各种请求参数详解

文章目录 请求Postman**为什么需要Postman****什么是Postman****Postman使用教程** 请求参数简单参数实体参数数组参数集合参数![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/eba0ca80e3724412ae4c79af72b859c3.png#pic_center)日期参数json参数路径参数总结 请求…

STM32CubeMX教程31 USB_DEVICE - HID外设_模拟键盘或鼠标

目录 1、准备材料 2、实验目标 3、模拟鼠标实验流程 3.0、前提知识 3.1、CubeMX相关配置 3.1.0、工程基本配置 3.1.1、时钟树配置 3.1.2、外设参数配置 3.1.3、外设中断配置 3.2、生成代码 3.2.0、配置Project Manager页面 3.2.1、设初始化调用流程 3.2.2、外设中…

ESP8266 控制之 : 使用 RingBuffer USART1 和 USART3互传

简介 使用Buffer来避免数据的丢失, 或许你自己在使用串口进行收发时会丢失数据, 现在我们就来简单使用一下RingBuffer创建Rx、Tx的Buffer来避免发送接收丢包或数据丢失问题。 扩展知识 RingBuffer的介绍, 看完大概也就知道了&#xff0c;实在不知道就看看下面的代码 线路连接…

详解操作系统各章大题汇总(死锁资源分配+银行家+进程的PV操作+实时调度+逻辑地址->物理地址+页面置换算法+磁盘调度算法)

文章目录 第三章&#xff1a;死锁资源分配图例一例二 第三章&#xff1a;银行家算法第四章&#xff1a;进程的同步与互斥做题步骤PV操作的代码小心容易和读者写者混 1.交通问题&#xff08;类似读者写者&#xff09;分析代码 2.缓冲区问题&#xff08;第二个缓冲区是复制缓冲区…

实现元素进入界面的平滑效果

先看效果&#xff1a; 实现思路&#xff1a;获取页面中需要加载动画的节点&#xff0c;用元素的animate()方法创建一个动画对象&#xff0c;并传入两个关键帧&#xff0c;接着使用IntersectionObserverAPI创建观察对象&#xff0c;用于观察元素进入页面。当元素进入界面时&…