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被置为NULLa = 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)); //#1unique_ptr<int> b;               //#2b = 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 allowedcout << "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>(); //#1shared_ptr<B> b = make_shared<B>(); //#2a->b_ptr = b;  //#3b->a_ptr = a;  //#4return;
}

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->成员函数,不具有一般指针的行为。

 

总结

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

 

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【思科】 GRE VPN 的实验配置

【思科】GRE VPN 的实验配置 前言报文格式 实验需求配置拓扑GRE配置步骤R1基础配置GRE 配置 ISP_R2基础配置 R3基础配置GRE 配置 PCPC1PC2 抓包检查OSPF建立GRE隧道建立 配置文档 前言 VPN &#xff1a;&#xff08;Virtual Private Network&#xff09;&#xff0c;即“虚拟专…

【C++进阶】STL容器--list使用迭代器问题分析

目录 前言 1. list的基本使用 1.1 list构造函数 1.2 list迭代器 1.3 list capacity 1.4 list元素访问 1.5 list 修改操作 insert erase swap resize clear 2. list失效迭代器问题 3. list使用算法库函数问题 总结 前言 list&#xff08;链表&#xff09;在C中非常重要…

Qt开源版 vs 商业版 详细比较!!!!

简单整理Qt开源版与商业版有哪些差别&#xff0c;仅供参考。 简单对比 开源版商业版许可证大部分采用对商业使用不友好的LGPLv3具备商业许可证保护代码专有许可证相关大部分模块使用LGPLv3和部分模块使用GPL组成仅第三方开源组件使用Qt的其他许可证Qt模块功能支持支持技术支持…

探秘Photoshop | 一站式了解所有相关信息

Photoshop是迄今为止世界上最强大的图像编辑软件&#xff0c;它已成为许多涉及图像处理的行业标准。软件技术一天行千里&#xff0c; Photoshop也在不断更新&#xff0c;从1990年开始发布&#xff0c; photoshop1.0到最新的 2018Photoshop... 几乎每隔一年&#xff0c;Photosho…

python爬虫demo——爬取历史平均房价

简单爬取历史房价 需求 爬取的网站汇聚数据的城市房价 https://fangjia.gotohui.com/ 功能 选择城市 https://fangjia.gotohui.com/fjdata-3 需要爬取年份的数据&#xff0c;等等 https://fangjia.gotohui.com/years/3/2018/ 使用bs4模块 使用bs4模块快速定义需要爬取的…

css圆形弹出菜单按钮效果

<!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8" /><meta name"viewport" content"widthdevice-width, initial-scale1.0" /><title>圆形弹出菜单按钮</title><link rel&qu…

PC电脑端的小程序顶部自定义标题失效的原因

windows客户端不被支持:navigationStyle:custom!! navigationStylestringdefault导航栏样式&#xff0c;仅支持以下值&#xff1a; default 默认样式 custom 自定义导航栏&#xff0c;只保留右上角胶囊按钮。iOS/Android 微信客户端 7.0.0&#xff0c;Windows 微信客户端不支…

laravel框架项目对接小程序实战经验回顾

一.对接小程序总结 1.状态转换带来的问题&#xff0c;如下 问题原因&#xff1a;由于status 传参赋值层级较多&#xff0c;导致后续查询是数组但是传参是字符串&#xff0c; 解决方案&#xff1a;互斥的地方赋值为空数组&#xff0c;有状态冲突的地方unset掉不需要的参数 2参…

【服务器APP】利用HBuilder X把网页打包成APP

目录 &#x1f33a;1. 概述 &#x1f33c;1.1 新建项目 &#x1f33c;1.2 基础配置 &#x1f33c;1.3 图标配置 &#x1f33c;1.4 启动界面配置 &#x1f33c;1.5 模块配置 &#x1f33c;1.6 打包成APP &#x1f33a;1. 概述 探讨如何将网页转化为APP&#xff0c;这似乎…

RabbitMQ快速实战

目录 什么是消息队列&#xff1f; 消息队列的优势 应用解耦 异步提速 削峰填谷 总结 主流MQ产品特点比较 Rabbitmq快速上手 创建用户admin Exchange和Queue Connection和Channel RabbitMQ中的核心概念总结 什么是消息队列&#xff1f; MQ全称Message Queue&#xf…

Mysql-事务(隔离级别,事务底层原理,MVCC)

什么是事务&#xff1f;有哪些特性&#xff1f; 事务&#xff1a;事务指的是逻辑上的一组操作&#xff0c;组成这组操作的各个单元要么全都成功&#xff0c;要么全都失败。 事务特性&#xff1a; 原子性&#xff08;Atomicity&#xff09;&#xff1a; 原子性是指事务是一个不…

低代码平台:业务开发“加速器”

一、现状 低代码开发平台要让每个人&#xff0c;包括开发者和普通业务人员&#xff0c;都能够成为企业数字化过程中的主导者和构建者&#xff01;让普通人更容易上手&#xff01; 基于这一目标&#xff0c;应用需求多的云服务商成为低代码投资的主要来源。一家云服务商如谷歌云…