从稍微懂一点开始的C++学习之路1: 智能指针
从稍微懂一点开始的C++学习之路1 智能指针
因为之前一直是搞qt的,没有搞过纯c++,所以现在算得上是刚开始学纯C++。C++的大部分语法其实我都懂,主要的是一些规范,还有内存回收等一些细节地方纯C++和Qt之间还是有蛮大差距的,现在底层框架需要用纯C++进行开发,所以这里我就重新学一下C++这门语言。
基础的语法就不再去详细了解了,现在从一些机制开始聊起,看到哪算哪,感觉有意思的就记一下。
今天聊聊智能指针
在实际的 C++ 开发中,我们经常会遇到诸如程序运行中突然崩溃、程序运行所用内存越来越多最终不得不重启等问题,这些问题往往都是内存资源管理不当造成的。比如:
- 有些内存资源已经被释放,但指向它的指针并没有改变指向(成为了野指针),并且后续还在使用;
- 有些内存资源已经被释放,后期又试图再释放一次(重复释放同一块内存会导致程序运行崩溃);
- 没有及时释放不再使用的内存资源,造成内存泄漏,程序占用的内存资源越来越多。
针对以上这些情况,很多程序员认为 C++ 语言应该提供更友好的内存管理机制,这样就可以将精力集中于开发项目的各个功能上。
C++ 智能指针底层是采用引用计数的方式实现的。简单的理解,智能指针在申请堆内存空间的同时,会为其配备一个整形值(初始值为 1),每当有新对象使用此堆内存时,该整形值 +1;反之,每当使用此堆内存的对象被释放时,该整形值减 1。当堆空间对应的整形值为 0 时,即表明不再有对象使用它,该堆空间就会被释放掉。
注意本文的智能指针需要用到
- #include <memory>
- using namespace std
请不要忘记这点
1.shared_ptr
shared_ptr就是可以共享的指针,和 unique_ptr、weak_ptr 不同之处在于,多个 shared_ptr 智能指针可以共同使用同一块堆内存。并且,由于该类型智能指针在实现上采用的是引用计数机制,即便有一个 shared_ptr 指针放弃了堆内存的“使用权”(引用计数减 1),也不会影响其他指向同一堆内存的 shared_ptr 指针(只有引用计数为 0 时,堆内存才会被自动释放)。
1、shared_ptr智能指针的创建
shared_ptr<T> 类模板中,提供了多种实用的构造函数,这里给读者列举了几个常用的构造函数(以构建指向 int 类型数据的智能指针为例)。
1) 通过如下 2 种方式,可以构造出 shared_ptr<T> 类型的空智能指针:
- std::shared_ptr<int> p1; //不传入任何实参
- std::shared_ptr<int> p2(nullptr); //传入空指针 nullptr
注意,空的 shared_ptr 指针,其初始引用计数为 0,而不是 1。
2) 在构建 shared_ptr 智能指针,也可以明确其指向。例如:
- std::shared_ptr<int> p3(new int(10));
由此,我们就成功构建了一个 shared_ptr 智能指针,其指向一块存有 10 这个 int 类型数据的堆内存空间。
同时,C++11 标准中还提供了 std::make_shared<T> 模板函数,其可以用于初始化 shared_ptr 智能指针,例如:
- std::shared_ptr<int> p3 = std::make_shared<int>(10);
以上 2 种方式创建的 p3 是完全相同。
3) 除此之外,shared_ptr<T> 模板还提供有相应的拷贝构造函数和移动构造函数,例如:
- //调用拷贝构造函数
- std::shared_ptr<int> p4(p3);//或者 std::shared_ptr<int> p4 = p3;
- //调用移动构造函数
- std::shared_ptr<int> p5(std::move(p4)); //或者 std::shared_ptr<int> p5 = std::move(p4);
如上所示,p3 和 p4 都是 shared_ptr 类型的智能指针,因此可以用 p3 来初始化 p4,由于 p3 是左值,因此会调用拷贝构造函数。需要注意的是,如果 p3 为空智能指针,则 p4 也为空智能指针,其引用计数初始值为 0;反之,则表明 p4 和 p3 指向同一块堆内存,同时该堆空间的引用计数会加 1。
而对于 std::move(p4) 来说,该函数会强制将 p4 转换成对应的右值,因此初始化 p5 调用的是移动构造函数。另外和调用拷贝构造函数不同,用 std::move(p4) 初始化 p5,会使得 p5 拥有了 p4 的堆内存,而 p4 则变成了空智能指针。
注意,同一普通指针不能同时为多个 shared_ptr 对象赋值,否则会导致程序发生异常。例如:
int* ptr = new int;
std::shared_ptr<int> p1(ptr);
std::shared_ptr<int> p2(ptr);//错误
4) 在初始化 shared_ptr 智能指针时,还可以自定义所指堆内存的释放规则,这样当堆内存的引用计数为 0 时,会优先调用我们自定义的释放规则。
在某些场景中,自定义释放规则是很有必要的。比如,对于申请的动态数组来说,shared_ptr 指针默认的释放规则是不支持释放数组的,只能自定义对应的释放规则,才能正确地释放申请的堆内存。
对于申请的动态数组,释放规则可以使用 C++11 标准中提供的 default_delete<T> 模板类,我们也可以自定义释放规则:
//指定 default_delete 作为释放规则
std::shared_ptr<int> p6(new int[10], std::default_delete<int[]>()); //自定义释放规则
void deleteInt(int*p) {
delete []p;
}
//初始化智能指针,并自定义释放规则
std::shared_ptr<int> p7(new int[10], deleteInt);
使用了动态生存期的资源的类:
程序使用动态内存处于以下三种原因:
1.程序不知道自己需要使用多少对象
2.程序不知道所需对象的准确类型
3.程序需要在多个对象间共享数据
在Effective c++中提到,最好是使用这种智能指针!
那我们如果直接管理内存呢!
玩的明白那当然也行,但是很容易发生野指针和空指针的问题,这个还是需要好好斟酌一下,接受新事物并没有什么大不了的,不是吗?
动态内存的管理非常容易出错,也就是使用new 和 delete管理动态内存存在三个常见问题
1.忘记delete内存。忘记释放动态内存会导致人们常说的内存泄漏问题,因为这种内存被占用后指针就丢失了,被占用的内存就永远不可能归还给自由空间了。查找内存泄漏错误是非常困难的,因为一般引用程序都是在运行了很长事件之后,真正耗尽内存的时候才会检测到这种错误。
2.使用已经释放掉的对象,通过释放内存后将指针置空,有时候可以检测到这种错误。
3.同一块内存释放两次。当有两恶搞指针指向相同的动态分配对象时,可能发生这种错误。如果对其中一个指针进行了delete操作,对象的内存就被归还给自由空间了。如果我们在delete第二次,就会破坏空指针的内存,自由空间就可能被破坏。
查找和维护这些错误是非常困难的,所以尽量多使用只能指针。
坚持只是用智能指针,就可以避免所有问题。对于一块内存,只有在没有任何智能指针指向它的情况下,智能指针才会自动释放它。
delete之后重置指针值?
delete之后,指针就变成空悬指针了!就是一块曾经有数据,但是现在是空白内存的指针。而且这个还不好排查,因为这个空悬指针可能还不是nullptr,没有办法检查。
解决办法如果这个指针在很多地方被使用了的话,delete这个指针的时候还需要将这个指针置为nullptr,这样就可以方便找出空悬指针了(当然了vs的debug很容易看得出空悬指针,但毕竟程序里也需要判断嘛)
但是重置指针只提供了有限的保护,约等于没有保护
如果多个指针指向相同的内存,在delete内存之后重置指针的方法只对这个指针有效。
比如我们有个A和B两个指针,是指向相同的内存,我们现在释放A之后,B的内存相当于也被释放掉了。
这个时候我们会将A置为 nullptr,确实是保护了A,但问题是B 现在的内存也被清理掉了,相当于是凭空产生了B这么个空悬指针,没有置为 nullptr。
但问题是我们有时候会有很多指针共享内存,我怎么知道谁被释放了谁没被释放?指针多了,这就是很难控制的了。
不要将普通指针和智能指针混用
准确的说不是不让混用,是不让你拿普通指针来生成临时的智能指针,可能会导致一些无法预料的错误,来看两段简单的代码。
假设我们有一个函数,是这样的
void test(shared_ptr<int> t1)
{
//用到t1
}
//函数结束,参数t1被释放,t1的计数减一
正常来说,我们是这样调用函数的
shared_ptr<int> int_t(new int(1234)); //此处计数为1
test(int_t); //在函数内部,此int_t的计数为2,函数结束了之后,int_t的计数就变为了1
int i = *int_t; //正确的,此时int_t的计数为1
如果我们混用,会发生什么?
int *normal_ptr(new int(1234)); //一个普通指针
//process(normal_ptr) //错误,int*无法转换成一个shared_ptr
process(shared_ptr<int>(normal_ptr)); //这是合法的,但是在process函数内部,这个智能指针的计数为一,函数结束之后,智能指针的计数为0,并且将normal_ptr释放了
int j = *x; //此时的x是一个空悬指针,这样调用会导致意想不到的错误。
这个例子就将混用的危害显而易见的标识了
当shared_ptr绑定到一个普通指针时,内存的管理职责交给shared_ptr就行了。一旦我们这么做了,就不应该再用内置的指针来访为 shared_ptr所指向 的内存了。
从稍微懂一点开始的C++学习之路1: 智能指针的更多相关文章
- oc学习之路-----搞死指针之内存存储int类型
关于每个数据类型个字节在内存中的存储地址(以int为例) 先上图 如题,为什么说好的*p = &c是1啊,为什么是513呢,一开始,我也觉得挺惊讶的,后面听老师分析了一下才知道怎么回事,但是还 ...
- osg(OpenSceneGraph)学习笔记1:智能指针osg::ref_ptr<>
OSG的智能指针,osg::ref_ptr<> osg::Referenced类管理引用计数内存块,osg::ref_ptr需要使用以它为基类的其它类作为模板参数. osg::ref_pt ...
- C++中的智能指针、轻量级指针、强弱指针学习笔记
一.智能指针学习总结 1.一个非const引用无法指向一个临时变量,但是const引用是可以的! 2.C++中的delete和C中的free()类似,delete NULL不会报"doubl ...
- 我的nodejs学习之路1
距离上次写文章类东西已经有4-5年了,猛然写东西有种提笔忘字的感觉. 言归正传,这是一篇记录我自己学习nodejs的文章,在写下这篇文章的时候我也不是什么大牛,也不是很了解nodejs这项技术.之所以 ...
- 嵌入式linux的学习之路[转]
我认为的一条学习嵌入式Linux的路: 1)学习 Linux系统安装. 常用命令.应用程序安装. 2) 学习 Linux 下的 C 编程.这本书必学<UNIX 环境高级编程>.<UN ...
- 【C++深入浅出】智能指针之auto_ptr学习
起: C++98标准加入auto_ptr,即智能指针,C++11加入shared_ptr和weak_ptr两种智能指针,先从auto_ptr的定义学习一下auto_ptr的用法. template& ...
- Android学习之路——简易版微信为例(一)
这是“Android学习之路”系列文章的开篇,可能会让大家有些失望——这篇文章中我们不介绍简易版微信的实现(不过不是标题党哦,我会在后续博文中一步步实现这个应用程序的).这里主要是和广大园友们聊聊一个 ...
- 初次踏上GUI编程之路(有点意思,详细介绍了菜鸟的学习之路)
初次踏上GUI编程之路 —— 我的Qt学习方法及对Qt认识的不断转变 -> 开始接触GUI与开始接触Qt: 话说,我第一次看见“Qt”这一个名词,好像是在CSDN网站的主页上吧,因为CSDN好像 ...
- Linux学习之路(一)
导语: 早前为了方便日常开发,建立跟生产环境类型的环境的时候考虑使用docker作为模拟生产环境,结果没想到给自己的学习挖了一个大坑.其他关于docker容器技术的坑先不在这里赘述,有时间的话在其他文 ...
- 初级dba学习之路参考
今天周一拖着疲惫的身躯 11点才离开公司,回到家估计写完这篇博客就要17号了. 一个人走在回家的路上,很黑,突然很多感触,一个人在北京拼搏,不敢停止学习的脚步,因为只要停下来就会感觉到孤独. 回顾一下 ...
随机推荐
- Java删除word合并单元格时的重复值
Spire.Doc提供了Table.applyVerticalMerge()方法来垂直合并word文档里面的表格单元格,Table.applyHorizontalMerge()方法来水平合并表格单元格 ...
- WMS 相比于 ERP 系统有哪些优势?
WMS与ERP系统是两个不同的系统,不存储优势的比较!WMS是仓库管理系统(Warehouse Management System) 的缩写,ERP是Enterprise Resource Plann ...
- 实验02_Proteus仿真数码管显示报告
一.原理总结 利用两个寄存器 R4 和 R5 来存储两个数码管的显示数字,R4 用来存储前一个数码管显示数字,而 R5 用来存储后一个数码管显示数字,利用左移操作 RLC 取 A 中首位放入 C ...
- 知识图谱顶会论文(ACL-2022) PKGC:预训练模型是否有利于KGC?可靠的评估和合理的方法
PKGC:预训练模型是否有利于KGC?可靠的评估和合理的方法 论文地址:Do Pre-trained Models Benefit Knowledge Graph Completion? A Reli ...
- 驱动开发:内核运用LoadImage屏蔽驱动
在笔者上一篇文章<驱动开发:内核监视LoadImage映像回调>中LyShark简单介绍了如何通过PsSetLoadImageNotifyRoutine函数注册回调来监视驱动模块的加载,注 ...
- 十、Pod的init containers
Pod 的 init Containers Pod 我们可以分为两类,一种属于自主式 Pod ,还有一种属于控制器管理的 Pod . 一.Pod 的 initContainers 基本概念: Pod ...
- DQL-模糊查询
DQL-模糊查询 模糊查询即模糊检索,是指搜索系统自动按照用户输入关键词的同义词进行模糊检索,从而得出较多的检索结果.与之相反的是"精准搜索".模糊检索也可以说是同义词检索,这里的 ...
- Linux网络管理命令
Linux网络管理命令 ifconfig 用于配置网卡ip地址信息等网络参数或显示网络接口状态,类似于windows的ipconfig命令. 可以用这个工具来临时性的配置网卡的IP地址.掩码.广播地址 ...
- Selenium4+Python3系列(五) - 多窗口处理之句柄切换
写在前面 感觉到很惭愧呀,因为居然在Selenium+Java系列中没有写过多窗口处理及句柄切换的文章,不过也无妨,不管什么语言,其思路是一样的,下面我们来演示,使用python语言来实现窗口句柄的切 ...
- Vue2基础知识学习
Vue2基础知识学习 01.初识 new Vue({ el: '#root', //用于指定当前Vue实例为哪个容器服务,值通常为css选择器符 data () { return { } } }); ...