智能指针类模板:auto_ptr、unique_ptr、shared_ptr的原理与使用

news2025/1/10 20:32:25

1. 什么是智能指针

智能指针是行为类似于指针的类对象,通常用于管理动态内存分配。C++程序通常手动动态分配堆内存,但如果动态分配的内存没有释放,则会发生内存泄漏。
例如代码段1.1。

// 代码段1.1
void demo()
{
   double *pd = new double;
   *pd = 25.5;
}

因为使用了new关键字申请动态内存,因此每次调用这个函数,都会从堆中分配一段内存,但直到函数运行结束,都没有释放分配的内存,因此产生了内存泄漏。
图示内存泄漏
解决该问题的方法为在函数return前添加一段代码delete pd;

另一种常见的产生内存泄漏的情况是在函数出现异常的时候。例如代码段1.2。

// 代码段1.2
void demo()
{
   double *pd = new double;
   if (weird_thing()) {
   	throw_exception();
   }
   *pd = 25.5;
   delete pd;
}

当出现异常时,不会执行delete语句,因此发生内存泄漏。
上面的代码中,pd只是普通的指针。如果使用对象来完成pd的功能,那么我们就可以利用对象的构造函数和析构函数,来完成动态申请和释放内存的操作。在创建对象时,自动调用构造函数申请内存;在函数返回时,对象过期,也会自动调用析构函数释放内存。实现上述功能的模板类就是智能指针。

C++98提供的解决方案是模板auto_ptr,但它也有自己的缺陷,因此C++11将其摒弃,并提供了unique_ptr和shared_ptr作为另外两种解决方案。

2. 智能指针的常规使用

先从机制最简单的auto_ptr讲起。我们提取auto_ptr的实现代码,为了方便可读性,此处略作删减。具体源码可以自行到memory头文件中查阅。

// 代码段2.1
template <class _Ty> class auto_ptr {
private:
    _Ty* _Myptr; // the wrapped object pointer

public:
    // 构造函数:根据已有的指针构造auto_ptr,并保存到
    explicit auto_ptr(_Ty* _Ptr = nullptr) noexcept : _Myptr(_Ptr) {
    }

    // 析构函数:销毁申请的内存空间
    ~auto_ptr() noexcept {
        delete _Myptr;
    }

    // return wrapped pointer
    _NODISCARD _Ty* get() const noexcept {
        return _Myptr;
    }

	// 重载*运算符:取指针的值
    _NODISCARD _Ty& operator*() const noexcept {
#if _ITERATOR_DEBUG_LEVEL == 2
        _STL_VERIFY(_Myptr, "auto_ptr not dereferencable");
#endif
        return *get();
    }

    // 重载->运算符:访问结构体成员
    _NODISCARD _Ty* operator->() const noexcept { // return pointer to class object
#if _ITERATOR_DEBUG_LEVEL == 2
        _STL_VERIFY(_Myptr, "auto_ptr not dereferencable");
#endif
        return get();
    }

    ...
};

注:

  1. noexcept是C++11中引入的关键字,其含义是程序员向编译器保证该函数不会发射异常。
  2. _NODISCARD定义为就是C++17中的新属性[[nodiscard]],定义在函数前表示该函数的返回值非void,调用该函数时最好使用一个变量或对象来保存返回值,否则会报warning。
  3. _STL_VERIFY(_Myptr, "xxxx")的作用是判断指针_Myptr如果为空,则抛出错误"Expression: xxxx",否则不做任何操作。

因此,可以对代码段1.1进行改写,将pd更换成auto_ptr。

// 代码段2.2
void demo()
{
	auto_ptr<double> apd(new double);
	*apd = 25.5;
}

new double会返回new申请的动态内存的指针,作为参数传递给auto_ptr的构造函数,构造函数会用该指针初始化私有成员_Myptr。
在这里插入图片描述
但是,auto_ptr有一个重大缺陷,严重影响了它的使用的安全性,下面我们做详细介绍。

3. auto_ptr的缺陷

如果auto_ptr只完成上述功能,那么会有一个严重的问题。例如代码段3.1,创建两个auto_ptr对象p1、p2,并将p2指向p1的同一块内存空间。

// 代码段3.1
void demo()
{
	auto_ptr<double> p1(new double);
	auto_ptr<double> p2;
	p2 = p1;
}

但这种做法实际上是不能被接受的,因为在函数运行结束时,程序将试图释放这块内存空间两次——一次是在p2过期调用析构函数时,一次是在p1过期调用析构函数时。也就是说,如果将代码段3.1改写成new-delete方式,与代码段3.2等价。代码段3.2运行时会直接报错。

// 代码段3.2
void demo()
{
	double* p1 = new double;
	double* p2;
	p2 = p1;
	delete p2;
	delete p1;
}

为了解决这个问题,auto_ptr类模板制定了一个”所有权(ownership)“的概念。对于一个特定的对象,只允许有一个auto_ptr拥有它。使用=赋值号的的时候,发生所有权的转移,将对象的所有权从旧的auto_ptr转移给新的auto_ptr,并将就得auto_ptr置为nullptr,这样在释放内存空间时,不会出现好几个auto_ptr试图释放同一块内存空间得情况。

通过重载运算符=,实现上述功能,具体代码为代码段3.3。

	// return wrapped pointer and give up ownership
    _Ty* release() noexcept {
        _Ty* _Tmp = _Myptr;
        _Myptr    = nullptr;
        return _Tmp;
    }

	// 重置_Myptr的值:如果传入的地址与_Myptr指向的地址不同,
	// 则先释放掉_Myptr指向的空间,再让其指向传入的地址空间
    void reset(_Ty* _Ptr = nullptr) { 
        if (_Ptr != _Myptr) {
            delete _Myptr;
        }

        _Myptr = _Ptr;
    }

	// 重载=运算符:让右值指向nullptr,并将右值的赋给左值
    auto_ptr& operator=(auto_ptr& _Right) noexcept {
        reset(_Right.release());
        return *this;
    }

重载=运算符后,代码段3.1就可以编译通过并成功运行了。但是如果我们想在p1的所有权转移之后再访问p1的地址,就会报错。如代码段3.4。

// 代码段3.4
void demo()
{
	auto_ptr<double> p1(new double);
	*p1 = 25.5;
	cout << *p1 << endl;	// 正常打印25.5
	auto_ptr<double> p2;
	p2 = p1;
	cout << *p1 << endl;	// 报错:auto_ptr not dereferencable
}

执行单步调试可以看出,在执行完语句p2 = p1之后,p1就会被置为nullptr,无法再被访问。

auto_ptr的拷贝构造函数与赋值有着同样的问题,因此也采用相同的方法解决,即调用拷贝构造函数时会转移所有权。此处不再赘述。

4. C++11新策:shared_ptr和unique_ptr

由于上文所述的种种缺陷,在C++11中弃置了auto_ptr,并实现了shared_ptr和unique_ptr,分别使用两种方法来解决该问题。

unique_ptr

unique_ptr延续了auto_ptr的所有权机制, unique_ptr所指向的对象只能有一个unique_ptr指针,因此unique_ptr不支持普通的拷贝和赋值操作。

将代码段3.1中的auto_ptr直接改成unique_ptr,则赋值语句会直接在编译时报语法错误。编译阶段的错误比潜在的程序崩溃更安全。

// 代码段4.1
void demo()
{
	unique_ptr<double> p1(new double);
	unique_ptr<double> p2;
	p2 = p1; // 编译错误
}

但是,unique_ptr并不是禁止赋值操作。如果赋值号的右值是临时的右值,赋值后不会留下悬空指针,那么这种赋值操作是被允许的。如代码段4.2。

// 代码段4.2
void demo()
{
	unique_ptr<double> p3;
	p3 = unique_ptr<double>(new double);	// allowed
}

代码段4.2是可以成功编译运行的,因为赋值号的右值是一个临时右值,它调用了unique_ptr的构造函数创建了一个临时对象,并在所有权转让给p3后被销毁。因此不会留下悬空指针。

unique_ptr通过C++11新增的移动构造函数和引用区分安全和不安全的用法。

此外,相比于auto_ptr,unique_ptr可用于数组的变体。C++中,new和delete配对,new []和delete [],但auto_ptr中只实现了delete而没有实现delete [],因此只能使用new分配内存。unique_ptr实现了delete和delete [],因此可以使用new []初始化数组。

// 代码段4.3
	unique_ptr<double[]> pd(new double(5));

shared_ptr

shared_ptr通过引用计数(reference counting)的方式来解决多个智能指针指向同一块内存空间的问题。引用计数记录了指向同一块内存的智能指针的个数,发生赋值操作或复制时,计数加1;每当一个指针过期时,计数减1。当最后一个指针过期时,才调用delete释放内存空间。

因此,当程序需要多个指针指向同一个对象时,使用shared_ptr。

但shared_ptr存在的一个问题是,在多线程情况下,它不是线程安全的。这是因为shared_ptr的内存模型中包含两个指针——指向对象的指针和指向引用计数的指针。
shared_ptr的内存模型
无论是对象本身还是对象的引用计数都是可以被多个shared_ptr共享的,因此是临界资源。而赋值操作本身两个步骤才能完成:①智能指针指向对象②计数加1。这并不是一个原子操作(即一步就能完成的操作)。因此在多线程情况下可能引发安全问题,甚至带来悬空指针。如代码段4.4。

// 代码段4.4
shared_ptr<double> gx(new double(1));

线程A:
void demoA()
{
	shared_ptr<double> pa;
	pa = gx;
}

线程B:
void demoB()
{
	shared_ptr<double> pb(new double(2));
	gx = pb;
}

上述代码中包括两个线程A、B。三个shared_ptr,其中gx为全局变量(线程A、B均可访问),pa为线程A局部变量,pb为线程B局部变量。假设有如下情况

  1. 线程A执行pa = gx,即读gx。但该赋值操作只来得及完成步骤①指针指向对象,尚未完成步骤②引用计数加1,这时切换成了线程B;
  2. 线程B执行gx = gb,即写gx。该赋值操作完成了步骤①和②。线程B运行结束并退出,释放了pb的空间并减少
  3. 继续执行线程A,由于此时object 1的引用计数已经变成0,则会释放该对象,导致pa成为悬空指针。

shared_ptr的线程不安全场景

boost官方文档中有如下结论:
3. 同一个shared_ptr被多个线程“读”是安全的;
4. 同一个shared_ptr被多个线程“写”是不安全的;
5. 共享引用计数的不同的shared_ptr被多个线程”写“ 是安全的

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

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

相关文章

使用STM32F103的串口实现IAP程序升级功能

使用STM32F103的串口实现IAP程序升级功能 &#x1f3ac;IAP程序烧录全过程演示&#xff1a; ✨这几天折腾IAP升级功能&#xff0c;狂补了很多相关BootLoader相关的知识。本来最想实现IAP升级程序的方式是&#xff0c;基于SPI通讯的SD卡&#xff0c;借助挂载的FatFS文件系统&am…

C++中的内存分区

目录 操作系统的内存区域 C内存分区模型 1. 程序运行前 2. 程序运行后 3. new 操作符的使用 操作系统的内存区域 text段&#xff1a;存储程序的二进制指令&#xff0c;即程序源码编译后的二进制代码data段&#xff1a;存储已被初始化的全局变量、常量bss段&#xff1a;存储…

ES-工作原理

前言 ​ 搜索引擎是对数据的检索&#xff0c;而数据总体分为两种&#xff1a;结构化数据和非结构化数据。而对于结构化数据&#xff0c;因为他们具有特定的结构&#xff0c;所以一般都是可以通过关系型数据库MySQL/oracle的二维表的方式存储和搜索&#xff0c;也可以建立索引。…

Redis的简单使用 (实现Session持久化)

&#x1f389;&#x1f389;&#x1f389;点进来你就是我的人了博主主页&#xff1a;&#x1f648;&#x1f648;&#x1f648;戳一戳,欢迎大佬指点! 欢迎志同道合的朋友一起加油喔&#x1f93a;&#x1f93a;&#x1f93a; 目录 一、Redis数据类型的使用 1. 字符串&#xff…

Redis【入门篇】---- Redis的Java客户端-Jedis

Redis【入门篇】---- Redis的Java客户端-Jedis 1. Jedis快速入门2. Jedis连接池1. 创建Jedis连接池2. 改造原始代码 在Redis官网中提供了各种语言的客户端&#xff0c;地址&#xff1a;https://redis.io/docs/clients/ 其中Java客户端也包含很多&#xff1a; 标记为❤的就是推荐…

密码学证明方案寒武纪大爆发——扩容、透明性和隐私的变革潜力

1. 引言 前序博客有&#xff1a; ZKP大爆炸 本文主要参考&#xff1a; StarkWare 2023年6月博客 Cambrian Explosion of Cryptographic Proofs----The transformative potential for scalability, transparency, and privacy2023年3月Eli Ben-Sasson在The 13th BIU Winter …

JavaWeb之Cookie和Session

文章目录 CookieCookie基本介绍Cookie的基本使用Cookie的创建从服务器获取CookieCookie值的修改方案一方案二 浏览器查看CookieCookie声明控制Cookie有效路径Path的设置 SessionSession基本介绍Session的创建和获取&#xff08;id号&#xff0c;是否为新&#xff09;Session域数…

【SQL server关键字】

目录&#xff1a; 前言一、CREATE -- 创建二、INSERT INTO VALUES -- 插入数据三、SELECT FROM -- 查找数据1.SEKECT简单了解2.函数的使用3.选择列表与group by子句的对应4.exists子查询 四、UPDATE SET -- 更改数据五、ALTER -- 修改属性六、JOIN ON-- 链接多个表1. join初…

JDBC 望舒客栈项目 万字详解

目录 一、前言 二、项目结构 三、准备工作 1.建立子包 : 2.导入jar包 : 3.工具类 : 1 Utility工具类 2 JDBCUtilsDruid工具类 4.导入配置文件 : 5.引入BasicDAO : 四、项目主体 1.界面显示 : 1 代码演示 2 运行测试 2.用户登录 : 1 创建员工表employee 2 创建Ja…

JavaScript 事件加载有哪些应用场景?

&#x1f482; 个人网站:【海拥】【游戏大全】【神级源码资源网】&#x1f91f; 前端学习课程&#xff1a;&#x1f449;【28个案例趣学前端】【400个JS面试题】&#x1f485; 寻找学习交流、摸鱼划水的小伙伴&#xff0c;请点击【摸鱼学习交流群】 目录 前言什么是JavaScript事…

VUE L MVVM模型 ③

目录 文章有误请指正&#xff0c;如果觉得对你有用&#xff0c;请点三连一波&#xff0c;蟹蟹支持✨ V u e j s Vuejs Vuejs M V V M MVVM MVVM模型Data与El的2种写法总结 文章有误请指正&#xff0c;如果觉得对你有用&#xff0c;请点三连一波&#xff0c;蟹蟹支持✨ ⡖⠒⠒⠒…

多线程中的wait和notify

1、wait和notify 由于线程之间是抢占式执行的&#xff0c;所以线程之间的执行先后顺序难以预知。但实际上是希望合理的协调多个线程之间的执行先后顺序。 完成这个协调工作&#xff0c;主要涉及到三个方法 *wait()/wait(long timeout);让当前线程进入等待状态。 *notify()/n…

【每日算法 数据结构(C++)】—— 02 | 数组的并交集(解题思路、流程图、代码片段)

文章目录 01 | &#x1f451; 题目描述02 | &#x1f50b; 解题思路交集并集 03 | &#x1f9e2; 代码片段交集并集 When you feel like giving up, remember why you started. 当你想放弃时&#xff0c;请记住为什么你开始 01 | &#x1f451; 题目描述 给你两个数组&#xff…

学习机组过程中的疑难问题与解决 -----(1)

本文章是在学习计算机组成原理过程中个人感觉需要理解与记忆的问题&#xff0c;还有一些在学习过程中自己产生的疑问以及解答,本文章可能排版不良&#xff0c;精力有限&#xff0c;还请见谅 第一章&#xff1a; &#xff08;1&#xff09;MAR的位数对应着存储单元的个数&#…

前端三剑客简介

文章目录 css的导入方式CSS选择器 JavascriptJavascript的引入方式输出语句变量数据类型javascript对象String对象 BOM对象DOM对象 W3C标准&#xff1a;网页主要由三部分组成 结构&#xff1a;html表现&#xff1a;css&#xff0c;层叠样式表行为&#xff1a;JavaScript css的…

VulnHub项目:Nagini

1、靶机地址&#xff1a; HarryPotter: Nagini ~ VulnHub 本篇为哈利波特死亡圣器系列的第二部&#xff0c;该靶机中存在3个魂器&#xff0c;依次找到它们&#xff0c;摧毁它们&#xff01; 2、渗透过程 首先收集靶机ip和确定攻击机ip&#xff0c;同时探测靶机开放端口 存在…

一个简单案例理解为什么在多线程的应用中要使用互斥锁

需求:使用10个线程,同时对一个值count进行加一操作,每个线程对count加100000次,最终使得count1000000 第一版代码:不加锁 ​​​lock.c #include<stdio.h> #include<pthread.h>#define THREAD_COUNT 10void *thread_callback(void *arg){int *pcount(int*)arg;in…

Unity例子——第一人称视角的角色控制器

本文是为了前文Unity四元组的举例示范&#xff0c;为了让读者更好地理解。 效果是实现一个可以由鼠标进行方向操作&#xff0c;键盘进行移动操作的任务。 此为效果视频&#xff1a; 1687597097844 下面进行教学&#xff1a; 一、搭建简单场景 新建一个场景&#xff0c;放置一…

SonarQube(sonar-scanner)+GitLab(gitlab-runner)实现提交代码自动扫描项目代码

安装gitlab-runner 插件挂载目录 mkdir -p /data/gitlab-runner/configdocker run -d --name gitlab-runner \ -v /data/gitlab-runner/config:/etc/gitlab-runner \ -v /var/run/docker.sock:/var/run/docker.sock \ --restart always \ --privilegedtrue \ gitlab/gitlab-r…