c++编程(26)——智能指针

欢迎来到博主的专栏:c++编程
博主ID:代码小豪

文章目录

    • 智能指针
      • 什么是智能指针?
    • auto_ptr
    • unique_ptr
    • share_ptr
      • shared_ptr缺陷
    • weak_ptr

智能指针

什么是智能指针?

智能指针是c++中关于动态内存管理的重要一环,在智能指针之前,管理动态内存的方式都是使用new、delete关键字。那么只能指针的作用是什么呢?

首先,智能指针是一个具有指针特性的对象,而非指针,其主要功能是利用对象的生命周期来管理资源。该技术有个专业名词,为RAII。

RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内
存、文件句柄、网络连接、互斥量等等)的简单技术。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在
对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做
法有两大好处:
不需要显式地释放资源。
采用这种方式,对象所需的资源在其生命期内始终保持有效。

简而言之,该技术的原理是利用对象的生命周期结束时调用析构函数的特性,在析构函数中将资源进行销毁。
在这里插入图片描述
在这里插入图片描述
到这也许就有人感到疑问了,我自己new出来的资源,自己delete释放不就好了吗?何必搞出一个指针呢?这个方法在以前确实是即高效,又节省空间的方法(因为创建对象是有开销的)。但自从c++推出异常机制之后,这个方法就不那么好用了。

在一个现代一个较大的编程项目当中,对异常进行处理是非常重要的,而由于一旦异常被抛出,程序会直接退出当前的函数栈帧,不再执行后续的程序,

这就会导致一个非常尴尬的事情发生。即我们可能会在函数头new了一个资源,在函数结束的末尾delete掉了这些资源,但是由于函数中间部分出现了异常,程序直接退出了该函数,导致无法执行到函数末尾。那么这个new出来的资源就没有进行释放,导致了内存泄漏。

而智能指针则是为了解决由抛异常导致的内存泄漏的救世主。由于抛异常会退出函数栈帧,而函数栈帧退出时又会顺带将局部对象进行析构,智能指针的析构又会顺带将资源进行销毁,这就完美的解决了问题。

auto_ptr

auto_ptr是c++98标准推出的第一个智能指针类,其定义在标准库<memory>当中。智能指针无论怎么讲解其作用,无非也就是讲RAII而已,因此博主采取模拟标准库中的常见智能指针的方式,细讲这些智能指针的不同。

auto_ptr是c++标准库推出的第一个智能指针类,其主要作用就是模仿指针的功能(解引用(*)和解引用(->))。以及RAII特性,并且采用了管理权转移的方式拷贝智能指针。

我们先写一个自定义类,以突出auto_ptr的各个功能。

class data
{
public:~data(){cout << "~data()" << endl;}int _year;int _month;int _day;
};

data类对象在析构时,会在屏幕当中打印“~data()”,以检测data类的资源是否被正确释放。

template<class T>class auto_ptr{public:auto_ptr(T* ptr=nullptr){_ptr = ptr;}T& operator*(){return *_ptr;}T* operator->(){return &(*_ptr);}~auto_ptr(){cout << "delte" << endl;delete _ptr;}private:T* _ptr;};

由于auto_ptr实现了operator*和operator->。因此我们可以像操作指针一样操作auto_ptr。

	void test_auto_ptr(){auto_ptr<data> ap1(new data);(*ap1)._year = 2024;ap1->_month = 9;(*ap1)._day = 10;cout << ap1->_year<<" " << ap1->_month <<" " << ap1->_day << endl;//2024 9 10}

运行程序,可以观察到data类的资源在函数结束时被释放。
在这里插入图片描述
c++的指针允许指针之间进行拷贝,那么auto_ptr也理应可以这么做。但如果我们尝试让auto_ptr之间进行拷贝,会发现程序运行会发生报错。

在这里插入图片描述
报错的原因也很简单,由于ap1,ap2,ap3共同指向同一块资源,当程序结束后,ap1,ap2,ap3会发生析构,由于ap3析构时,会将delete掉指向的资源,此时轮到ap2进行析构了,ap2将指向的资源调用delete,此时尴尬的事情就发生了,由于ap2指向的资源在ap3析构时就已经释放了,对已释放的资源重复delete会导致程序报错。

auto_ptr的解决方案也非常简单,既然多个对象指向同一块资源会报错。那就在拷贝的时候,让被拷贝的对象转让出资源的管理权就行了,实现如下:

auto_ptr(auto_ptr<T>& aptr)
{delete _ptr;_ptr = aptr._ptr;aptr._ptr = nullptr;
}auto_ptr& operator=(auto_ptr<T>& aptr)
{delete _ptr;_ptr = aptr._ptr;aptr._ptr = nullptr;return *this;
}

从上面的代码可以得知,当auto_ptr对象发生拷贝时,被拷贝的对象会转移资源,给拷贝的对象。这样就只有拷贝的对象才能管理这片资源。当生命结束,调用析构时,也只有一个指针释放管理的资源。

但是这么做有一个缺点,那就是之前创建的智能指针不能用了。

auto_ptr<data> ap1(new data);
auto_ptr<data> ap2(ap1);//拷贝ap1
auto_ptr<data> ap3;
ap3 = ap2;//拷贝ap2
(*ap1)._year = 2024;//errpr.ap1已丢失管理权
ap1->_month = 9;//error
(*ap1)._day = 10;//error

unique_ptr

总体而言,auto_ptr并不好用,因为既然auto_ptr不允许多个对象指向同一个资源,那么又何必设计出auto_ptr的拷贝构造呢?总给人一种画蛇添足的感觉,而且如果auto_ptr一不注意发生了拷贝(比如传参调用函数)。那么源对象就会丢失资源,

于是c++11推出了unique_ptr,该对象就和它的名字一样,禁止其他unique_ptr对其进行拷贝。由于unique_ptr的功能和auto_ptr除了不能拷贝之外,区别并不是很大,因此博主就不模拟实现unique_ptr了。

尝试运行下面的程序(unique_ptr是标准库中的)

void test_unique_ptr()
{unique_ptr<data>(new data[5]);
}

此时程序会因为调用了unique_ptr的析构函数而报错。
在这里插入图片描述
分析原因,原来是因为释放数组资源时,要用detele[]。而unique_ptr的析构函数在默认情况下使用的是delete,这就是报错的原因。

解决方法:
在c++标准库中,存在unique_ptr模板关于数组类型的特化版本。

non-specialized	
template <class T, class D = default_delete<T>> class unique_ptr;
array specialization	
template <class T, class D> class unique_ptr<T[],D>;

使用该特化版本可以解决delete关键字无法释放数组资源的问题。

void test_unique_ptr()
{unique_ptr<data[]>up1(new data[5]);
}

那么这种情况呢?

unique_ptr<FILE>up2(fopen("test.txt", "r"));

虽然运行这段代码没有问题,但是up2的释放方式不对,对于file*的指针使用delete是不对的,而是应该fclose才行,但是我们不能对库进行修改,那么该怎么办呢?

c++允许我们自定义一个释放资源的仿函数,称之为定制删除器。这个仿函数通过unique_ptr的第二模板参数传给unique_ptr。

array specialization	
template <class T, class D> class unique_ptr<T[],D>;

我们写一个针对FILE*类型的指针的定制删除器,并将其命名为closefile。

class closefile
{
public:void operator()(FILE* ptr){fclose(ptr);}
};

最后将closefile传递给unique_ptr。

unique_ptr<FILE,closefile> up2(fopen("test.txt", "w"));

share_ptr

share_ptr允许拷贝其他智能指针,而且采用的策略与auto_ptr不同,因此具有不同的特性。

share_ptr采用的是引用计数的内存管理方法,其思想如下:
(1)每一块资源都有对应的计数,比如,指针a和指针b同时管理一块资源,则其计数为1
(2)每增加一个管理资源的指针,对应的计数就加1
(3)没减少一个管理资源的指针,对应的计数就减1
(4)当资源的对应计数变为0时,将该资源进行释放

由于标准库中的shared_ptr拥有定制删除器,因此我们要在shared_ptr的成员加多一个定制删除器,并且实现一个默认情况下使用的定制删除删除器。

template<class T>
struct default_deletor//默认删除器
{void operator()(T* ptr){delete ptr;}
};template<class T>
struct default_deletor<T[]>//默认删除器的针对释放数组的特化版本
{void operator()(T* ptr){delete[] ptr;}
};
template<class T>
class shared_ptr
{
public:template<class D>shared_ptr(T* ptr,const D&deletor=default_deletor<T*>):_ptr(ptr),_pcount(new int(1)),_del(deletor){}T& operator*() { return *_ptr; }T* operator->() { return &this->operator*(); }
private:int* _pcount;//计数T* _ptr;function<void(T*)> _del;//定制删除器
};

首先是share_ptr的拷贝构造函数,其方法如下:
与被拷贝者共同管理同一块资源,该资源计数加1.
在这里插入图片描述

shared_ptr(const shared_ptr& sptr):_ptr(sptr->_ptr),_pcount(sptr->_pcount)//与被拷贝者共用同一个计数
{(*_pcount)++;//计数++
}

析构函数的思路如下:
当shared_ptr被析构时,实际上就是shared_ptr不再管理对应资源,那么该资源相应的计数-1,如果该资源的计数变为0,就代表没有智能指针继续管理该资源,为了防止丢失该资源导致内存泄漏,我们就需要对该资源进行释放。

~shared_ptr()
{remove();
}
void remove()
{--(*_pcount);if (*_pcount == 0){//如果计数为0,就要释放资源_del(_ptr);delete _pcount;_pcount = nullptr;_ptr = nullptr;}
}

赋值拷贝的思路如下:
(1)由于赋值之前,shared_ptr有可能已经存在管理的资源了,因此要先移除该shared_ptr管理的资源
(2)和拷贝构造一样,让新管理的资源的计数加1
在这里插入图片描述

要注意,shared_ptr要判断一下是否重复赋值,重复赋值是指,将管理相同资源的shared_ptr进行赋值操作,一方面是避免增加不必要的时间开销,第二则是有可能导致资源被释放。

shared_ptr& operator=(const shared_ptr& sptr)
{if (_ptr != sptr._ptr)//避免重复赋值{remove();_ptr = sptr._ptr;_pcount = sptr._pcount;(*_pcount)++;return *this;}
}

然后我们可以设计以count函数,方便查看shared_ptr的计数。

shared_ptr缺陷

shared_ptr在循环指向的情况下,会导致无法正确析构智能指针的情况出现。比如我们可以尝试让shared_ptr来作为链表的指针。

template<class T>
struct listnode
{T _val;shared_ptr<listnode> next;shared_ptr<listnode> prev;
};

设计一个节点left和节点right,让它们头尾相接。

void test_list()
{shared_ptr<listnode<data>> left(new listnode<data>);shared_ptr<listnode<data>> right(new listnode<data>);left->next = right;right->prev = left;
}

运行这段代码,可以发现left和right没有正常析构,因为data在析构时,会在屏幕上打印“”~data“”。

原因在于:
在这里插入图片描述
left指针指和right->prev共同指向同一块资源,因此该资源的计数为2。右边资源也同理。

当left指针和right指针被析构时,这两个资源的计数都会变为1.
在这里插入图片描述
此时,如果left资源想要释放,就要让right资源调用right->prev的析构函数。(因为right->prev是一个smart-ptr)。此时尴尬的事情发生了,控制right资源的智能指针已经被销毁了,也就说明我们没有其他方式可以控制right资源。因此left和right彼此之间都无法释放空间,这就导致了内存泄漏。

weak_ptr

为了解决这个问题,标准库又设计了一个weak_ptr。当weak_ptr的生命周期结束时,不会去释放指向的空间。它仅仅只是作为一个指针,指向一个由shared_ptr管理的资源。将weak_ptr指向shared_ptr时,不会增加shared_ptr管理的资源的引用计数。

这么做的好处在于,如果用weak_ptr代替shared_ptr作为链表之间的链接时(不仅是链表,循环指向的场景都能使用weak_ptr解决问题。)。不会增加shared_ptr的引用计数,因此当shared_ptr被销毁时,管理的资源也会被正确释放(上面的情况则是由于循环指向导致计数增加,不会被正确释放)

因此如果使用智能指针来控制链表,正确的做法是让weak_ptr作为节点之间的链接,而非shared_ptr。

template<class T>
struct listnode
{T _val;weak_ptr<listnode<T>> next;weak_ptr<listnode<T>> prev;
};

此时再让节点之间循环指向。情况则会变成
在这里插入图片描述

void test_list()
{std::shared_ptr<listnode<data>> left(new listnode<data>);std::shared_ptr<listnode<data>> right(new listnode<data>);left->next = right;right->prev = left;
}

运行这段代码,发现left和right被成功析构。
在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

力扣718-最长重复子数组(Java详细题解)

题目链接&#xff1a;718. 最长重复子数组 - 力扣&#xff08;LeetCode&#xff09; 前情提要&#xff1a; 因为本人最近都来刷dp类的题目所以该题就默认用dp方法来做。 dp五部曲。 1.确定dp数组和i下标的含义。 2.确定递推公式。 3.dp初始化。 4.确定dp的遍历顺序。 5…

Linux嵌入式相机 — 项目总结

main函数执行流程 1、初始化触摸屏 Touch_screen_Init();struct tsdev *ts NULL; ts ts_setup(NULL, 0); //以阻塞打开2、初始化 LCD LCD_Init(void); 通过 ioctl 函数获取 LCD 的固定参数、可变参数&#xff0c;得到分辨率、bpp、一行的长度&#xff08;以字节为单位&a…

【MATLAB源码-第225期】基于matlab的计算器GUI设计仿真,能够实现基础运算,三角函数以及幂运算

操作环境&#xff1a; MATLAB 2022a 1、算法描述 界面布局 计算器界面的主要元素分为几大部分&#xff1a;显示屏、功能按钮、数字按钮和操作符按钮。 显示屏 显示屏&#xff08;Edit Text&#xff09;&#xff1a;位于界面顶部中央&#xff0c;用于显示用户输入的表达式和…

【激励广告带来的广告收入与用户留存率的双重提升】

激励广告带来的广告收入与用户留存率的双重提升 ) 随着移动应用市场的竞争加剧&#xff0c;如何通过广告变现成为众多开发者关注的焦点。其中&#xff0c;激励广告&#xff08;Rewarded Ads&#xff09;凭借其用户友好、互动性强等特点&#xff0c;逐渐成为开发者的首选。那些…

Java——Static与final修饰的变量与方法(总结)

前言&#xff1a; Java语法学过一遍之后&#xff0c;我相信大多数和我一样脑瓜子嗡嗡的&#xff0c;甚至有点乱了&#xff0c;这时候应该自己把之前的能总结的&#xff0c;或者不熟悉的都要总结一遍&#xff0c;以便于后期的学习&#xff01;&#xff01; static修饰的成员变量…

[附源码]SpringBoot+VUE+Java实现人脸识别系统

今天带来一款优秀的项目&#xff1a;java人脸识别系统源码 。 系统采用的流行的前后端分离结构&#xff0c;内含功能包括 “人脸数数据录入”&#xff0c;“人脸管理”&#xff0c;“摄像头识别” 如果您有任何问题&#xff0c;也请联系小编&#xff0c;小编是经验丰富的程序员…

数码好物抢先看!2024有什么好用又实惠的好物推荐!

在数字科技日新月异的今天&#xff0c;各种数码好物层出不穷&#xff0c;它们以其先进的技术、创新的功能以及不断提升的性能&#xff0c;为我们的生活带来了极大的便利和乐趣。对于消费者来说&#xff0c;在众多的数码产品中挑选出好用又实惠的好物&#xff0c;无疑是一件既令…

Spring Controller

服务器控制 响应架构 Spring Boot 内集成了 Tomcat 服务器&#xff0c;也可以外接 Tomcat 服务器。通过控制层接收浏览器的 URL 请求进行操作并返回数据。 底层和浏览器的信息交互仍旧由 servlet 完成&#xff0c;服务器整体架构如下&#xff1a; Server&#xff1a; Tomcat…

电机知识总结

一.直流无刷电机&#xff08;BLDC&#xff09; 27N30P指有27个槽&#xff0c;30的极数&#xff0c;它的极对数&#xff1a;30/215,所以是15对极。 N必须是3的倍数&#xff0c;P必须是偶数&#xff0c; 电角度是电气特性&#xff0c;机械角度是空间特性&#xff0c;必须指明是谁…

Selenium等待机制:理解并应用显式等待与隐式等待,解决页面加载慢的问题

目录 引言 等待机制的重要性 显式等待&#xff08;Explicit Wait&#xff09; 原理 应用方式 代码示例 优点与缺点 隐式等待&#xff08;Implicit Wait&#xff09; 原理 应用方式 代码示例 优点与缺点 解决页面加载慢的问题 1. 合理设置等待时间 2. 优先使用显…

数据三维可视化技术的应用场景

数据三维可视化技术作为一种强大的工具&#xff0c;已经在各个领域展现出了巨大的应用潜力。它不仅提供了直观、生动的数据展示方式&#xff0c;还让用户能够更深入地理解数据间的关联和趋势。下面将探讨数据三维可视化技术的应用范围及其在不同领域中的重要性。 数据三维可视化…

控价服务如何判断高低

在当今竞争激烈的市场环境中&#xff0c;品牌控价成为企业发展的关键一环。许多品牌选择与第三方控价公司合作&#xff0c;借助其专业的电商价格监测系统&#xff0c;既能节省人力成本&#xff0c;又能获得高质量的服务。然而&#xff0c;如何判断第三方控价服务系统的优劣呢&a…

VirtualBox7.1.0 安装 Ubuntu22.04.5 虚拟机

环境 &#xff08;1&#xff09;宿主机系统&#xff1a;Windows10 &#xff08;2&#xff09;虚拟机软件&#xff1a;VirtualBox7.1.0 &#xff08;3&#xff09;虚拟机系统&#xff1a;Ubuntu 22.04.5 LTS (Jammy Jellyfish) 步骤 &#xff08;1&#xff09;第一步 &…

2024年最新版TypeScript学习笔记——泛型、接口、枚举、自定义类型等知识点

今天带来的是来自尚硅谷禹神2024年8月最新的TS课程的学习笔记&#xff0c;不得不说禹神讲的是真的超级棒&#xff01; 文章目录 TS入门JS中的困扰静态类型检查编译TS命令行编译自动化编译 类型检查变量和函数类型检查字面量类型检查 类型推断类型声明声明对象类型声明函数类型…

个人驾校预约管理系统设计与实现

个人驾校预约管理系统设计与实现 摘 要 传统办法管理信息首先需要花费的时间比较多&#xff0c;其次数据出错率比较高&#xff0c;而且对错误的数据进行更改也比较困难&#xff0c;最后&#xff0c;检索数据费事费力。因此&#xff0c;在计算机上安装个人驾校预约管理系统软件…

3.js - THREE.CubeTextureLoader() 添加环境纹理,以创建立方体贴图

使用 THREE.CubeTextureLoader() 添加环境纹理&#xff0c;以创建立方体贴图 不使用 THREE.CubeTextureLoader() 的时候 源码 import * as THREE from three import { OrbitControls } from three/examples/jsm/controls/OrbitControls import { RGBELoader } from three/exam…

SHAP 模型可视化 + 参数搜索策略在轴承故障诊断中的应用

往期精彩内容&#xff1a; Python-凯斯西储大学&#xff08;CWRU&#xff09;轴承数据解读与分类处理 Python轴承故障诊断入门教学-CSDN博客 Python轴承故障诊断 (13)基于故障信号特征提取的超强机器学习识别模型-CSDN博客 Python轴承故障诊断 (14)高创新故障识别模型-CSDN…

【鸿蒙】HarmonyOS NEXT开发快速入门教程之ArkTS语法装饰器(上)

文章目录 前言一、ArkTS基本介绍1、 ArkTS组成2、组件参数和属性2.1、区分参数和属性的含义2.2、父子组件嵌套 二、装饰器语法1.State2.Prop3.Link4.Watch5.Provide和Consume6.Observed和ObjectLink代码示例&#xff1a;示例1&#xff1a;&#xff08;不使用Observed和ObjectLi…

新媒体运营

一、新媒体运营的概念 1.新媒体 2.新媒体运营的五大方向 用户运营 产品运营 。。。 二、新媒体的岗位职责及要求 三、新媒体平台

数仓工具:datax

datax可以理解为sqoop的优化版&#xff0c; 速度比sqoop快 因为sqoop底层是map任务&#xff0c;而datax底层是基于内存 DataX 是一个异构数据源离线同步工具&#xff0c;致力于实现包括关系型数据库(MySQL、Oracle等)、HDFS、Hive、ODPS、HBase、FTP等各种异构数据源之间稳定…