1.1 从C++智能指针获取的裸指针变成悬垂指针下面所讨论的C++踩坑悬垂指针,以及之后的Rust避坑悬垂指针,并不是暗示C++不如Rust好,而仅仅是为了提升自学者入门Rust的动力而已。 代码通过智能指针管理一个整数,当智能指针被销毁后,原先获取的裸指针仍然指向已释放的内存,导致悬垂指针的产生。最后,代码尝试访问这个悬垂指针指向的值,展示了未定义行为的可能结果。 通过这个示例,可以清楚地看到从智能指针获取的裸指针在智能指针生存期结束后如何变成悬垂指针,从而引发潜在的风险。因此,在使用智能指针时,应谨慎管理裸指针的使用,避免悬垂指针的产生。 1.2 从Rust智能指针获取引用避坑悬垂指针Rust如何避坑上面从C++智能指针获取的裸指针变成悬垂指针的问题?通过运用引用来避坑,如代码清单1-2所示。 1.3 从Rust智能指针获取的裸指针变成悬垂指针在Rust中,如果使用不慎,也会踩类似C++那样将从智能指针获取的裸指针变成悬垂指针的坑,如代码清单1-3所示。
又被使用指的是:指针存在(悬垂指针被引用)。这个引用的结果是不可预测的,因为不知道会发生什么。由于大多数的堆内存其实都是C++对象,所以利用的核心思路就是分配堆去占坑,占的坑中有自己构造的虚表。 首先要说明2个概念: 1.悬垂指针:悬垂指针是指一类不指向任何合法的或者有效的(即与指针的含义不符)的对象的指针。 比如一个对象的指针,如果这个对象已经被释放或者回收但是 指针没有进行任何的修改仍然执行已被释放的内存,这个指针就叫做悬垂指针 2.UAF漏洞:Use-After-Free是一种内存破坏漏洞,简单的说 ,漏洞的原因是使用了悬垂指针。
所有权系统 - 防止悬垂指针 4. 借用检查器 - 防止数据竞争 5. 生命周期系统 - 防止悬垂引用 6. RAII 机制 - 自动资源管理 7. 智能指针 - 安全的共享所有权 8. 项目概述 1.1 项目目标 本项目通过13 个实际演示,全面展示 Rust 的内存安全特性: ✅ 所有权系统防止悬垂指针 ✅ 借用检查器防止数据竞争 ✅ 生命周期系统防止悬垂引用 ✅ RAII 机制防止内存泄漏 内存安全问题概览 2.1 常见的内存安全问题 问题类型 描述 危害 其他语言 悬垂指针 指向已释放内存的指针 程序崩溃、安全漏洞 C/C++ 常见 双重释放 释放同一块内存两次 程序崩溃、内存损坏 C/ 悬垂指针问题 C/C++ 代码(危险): // ❌ C:可能出现悬垂指针 char* create_string() { char str[] = "临时数据"; return str; // 悬垂指针!
4、悬垂指针 由悬垂指针可以引出野指针,垃圾内存以及内存黑洞等相关概念,我们一点点来看: 悬垂指针:当所指向的对象被释放或者收回,但是对该指针没有做任何修改,以至于该指针仍旧指向已经回收的内存地址,此情况下该指针便称为悬垂指针 (也称迷途指针)。 一般以如下形式(C++)可以避免悬垂指针: delete cp; cp = NULL; //此处一定要记得! 野指针:其产生是由于某些指针在首次使用之前没有进行必要的初始化,这类指针就称为野指针。 垃圾内存:针对于某个指针来说,常常指不合法的内存,某些内存free或delete后没有将其设置为NULL,这样的内存就是垃圾内存。 内存“黑洞”:和上面三个完全不同的概念,特指对一个指针施加free或delete前就把这个指针设置为NULL,这样之后,这块内存并不属于编译器,它只属于某个变量的合法访问区域,但这个访问的指针已经不存在了
指针 指针作为C语言的核心部分,相比较其他的内容相对比较难懂一下,应用的方式多样,变化较多,导致很多的同学非常苦恼,那么接下来,我将陪你来共同揭开指针的神秘面纱; 指针的含义 大家不要把指针想的太难,指针其实就是通过地址找到对应的变量的位置 ,然后我们可以对地址进行解引用来访问变量的内存,来获取值的一种间接方法;我们通常说指针指向哪里哪里,就是指针变量里面存的其他变量(或者常量)的地址;即 指针->地址->内存; 普通变量指针 首先,我们现来看看指针的构成以及表达形式 ; 数组指针 数值指针,其主体是指针,他就是个指针,不过是有点不同而已,这个指针指向的是数组的地址,在此之前我们需要了解数组的地址; 数组的地址; 我们通常说数组的地址是数组名,是数组的首元素地址,也确实是这样 字符指针 含义 字符指针就是指针指向了字符或者字符串,因为字符串可以看成是一个字符数组,所以字符串指针与数组指针大致可以类比; 字符指针打印字符串 这里我使用了三种打印字符串的方式,从结果上看,很明显打出来的字符串都是相同的 指针数组 含义 指针数组,主体是数组,不同的是里面存的是指针,是地址; 指针数组打印二维数组 值得注意的是指针数组的[]前面的*p是没有括号的,我们可以这样看,他是一个数组,数组名是p,数组元素个数是3
这样的类具有指针的所有缺陷,如悬垂指针、内存泄漏等,但无需特殊的复制控制。 缺点:容易导致悬垂指针和内存泄漏问题。 智能指针通过引用计数来防止悬垂指针的出现。当智能指针的引用计数降为0时,它会自动删除所指向的对象。 ,避免了悬垂指针和内存泄漏问题。 使用这些智能指针可以大大简化内存管理,并减少内存泄漏和悬垂指针的风险。 封装和抽象指针管理逻辑:通过封装和抽象来隐藏指针的细节,使代码更加清晰和易于维护。
很不幸的是,对于这种逻辑错误开发者往往没有特别好的手段可以规避掉,二次delete一个悬垂指针行为是未定义的,也就是说错误是有可能被隐藏的。 当然“delete后置空指针”这种教条能流传这么久,肯定是有它的道理的。 关于到底什么时候需要置空指针,关键之处在于搞清楚置空指针到底解决了什么问题。 先来理一下nullptr和野指针/悬垂指针的区别: 解引用: nullptr:未定义 野指针/悬垂指针:未定义 delete nullptr:良好定义,delete什么也不用做 野指针/悬垂指针:未定义 值:nullptr:明确 野指针/悬垂指针:未定义,无法确定 可以发现nullptr最大的优势在于值是明确的,也就是说分辨一个指针是不是nullptr比分辨一个指针是不是野指针/悬垂指针要容易得多。 至此,我们至少可以得出一个结论,如果对象是和持有其的指针一同销毁的,那置空指针就是脱裤子放屁。
这种自由虽带来高性能,但也容易埋下内存泄漏、悬垂指针等隐患。 4.2 悬垂指针指针指向的内存已被释放:cpp复制编辑int* p = new int(5);delete p;*p = 10; // 未定义行为4.3 野指针指针未初始化,指向未知地址:cpp复制编辑 七、现代 C++ 的智能指针C++11 引入智能指针机制,自动管理动态内存,防止内存泄漏与悬垂指针。7.1 std::unique_ptr独占所有权,不能拷贝,只能移动。 delete 后赋为 nullptr 避免裸指针,尽量使用智能指针 多用 RAII 模式封装资源管理 十一、性能优化与策略 内存复用:重复使用大块内存减少频繁分配。 回顾重点: 栈 vs 堆 vs 静态区的分配区别 new/delete 与构造析构的配对使用 常见错误:悬垂指针、内存泄漏、越界访问 智能指针(unique_ptr、shared_ptr)的使用
普通变量和指针变量 共性 PS: 可见这4个函数的汇编指令完全一致,无论是什么类型的指针变量,对指针变量的读写跟普通变量没有任何区别,所谓的指向只是描述指针变量的值时多少而已,就读写而言,指针变量跟普通变量没有任何区别 空指针和野指针 野指针:定义了一个指针变量,如果没有进行初始化,系统就会有可能随机赋值一个地址给这个指针变量,也就是说,这个指向指向一个未知的区域。 空指针:空指针不是指向常数0,只指向地址0,即NULL,其实换句话说,指针的本质就是地址嘛,空指针就是指针本身的值(地址)为0空指针的作用是防止野指针的出现,因为我们不能知道野指针到底指向哪里,所以我们也无法判断一个指针是否是野指针 PS: 区分指针数组int *a[3]和数组指针int (*a)[3],前者时存放指针的数组,后者是指向数组的指针。 这样两者的区别就豁然开朗了,数组指针只是一个指针变量,似乎是C语言里专门用来指向二维数组的,它占有内存中一个指针的存储空间。指针数组是多个指针变量,以数组形式存在内存当中,占有多个指针的存储空间。
前言: 本文主要讲解指针进阶部分的内容,分为字符指针,指针数组,数组指针,函数指针。 int *arr[5];//存放整型地址的指针数组 char *arr[6];//存放字符类型的指针数组 数组指针 概念辨析 我们类比一下: 整型指针:指向整型变量的指针,存放整型变量的地址的指针。 数组指针:指向数组的指针,存放数组地址的指针。 形式辨析 int * p; 首先*表示这是一个指针,命名为p,然后指向的是int类型的指针,数组指针也一样 int(*p) [5]; 上面的形式就是数组指针,我们需要先用()把*和指针名括起来,然后剩下的就是指针指向的类型 数组接受时,行可以省略,但是列不能 指针接收,必须用数组指针来接收。 函数指针 函数指针就是指向函数的指针。
普通变量和指针变量 共性 PS: 可见这4个函数的汇编指令完全一致,无论是什么类型的指针变量,对指针变量的读写跟普通变量没有任何区别,所谓的指向只是描述指针变量的值时多少而已,就读写而言,指针变量跟普通变量没有任何区别 空指针和野指针 野指针:定义了一个指针变量,如果没有进行初始化,系统就会有可能随机赋值一个地址给这个指针变量,也就是说,这个指向指向一个未知的区域。 空指针:空指针不是指向常数0,只指向地址0,即NULL,其实换句话说,指针的本质就是地址嘛,空指针就是指针本身的值(地址)为0空指针的作用是防止野指针的出现,因为我们不能知道野指针到底指向哪里,所以我们也无法判断一个指针是否是野指针 指针变量的+-运算 指针变量的加减运算:也就是做地址偏移,不同 的指针类型偏移的步长不同。 图片 图片 PS: 区分指针数组int *a[3]和数组指针int (*a)[3],前者时存放指针的数组,后者是指向数组的指针。
4、智能指针 智能指针是由于在有指针成员的类中,指针所指向的对象是共享的,防止出现悬垂指针而提出的一种管理指针的办法。 再看,可能出现悬垂指针的情况: int *ip = new int(42); HasPtr ptr(ip, 10); delete ip; ptr.set_ptr_val(0); //Disaster! 这里ip和ptr中的指针指向了同一对象,删除了该对象时,ptr中的指针不再指向有效对象,但是你又不知道该对象不在了,所以,这样就出现了悬垂指针。 所以,定义智能指针能有效地解决这个问题,为了避免多个指针共享一个对象时撤销出现的悬垂指针问题,定义智能指针类的主要功能就是来保证在撤销指向对象的最后一个指针时才删除该对象。 为了统计指向共享对象的指针的数量,引入使用计数,用其跟踪该类有多少个对象共享同一指针,但使用计数为0时,删除对象。在设计上,将使用计数设计成一个单独的类,用来封装使用计数和相关指针。
概念 我们把指向数组的指针叫做数组指针,后面还会学到指针数组,这两个是不一样的,根据中学语文偏正词组的知识可以知道,前者是指针,后者是数组。 注意:二维数组名a不可以赋值给一般指针变量p,只能赋值给二维数组的行指针变量。 行指针变量 行地址和列地址 先看一个代码。 行指针是一种特殊的指针变量,专门指向一维数组。 行指针定义: int a[2][3]; int (*p)[3]; 不可写成 int (*p)[2],因为二维数组a每行有四个元素。 不可写成 int *p[4],此为指针数组的定义。 : p=a[0]; 或: p=*a; 或: p=&a[0][0]; 用法同一般指针变量。
常量指针:指向常量的指针 在指针定义语句的类型前加const,表示该指针指向一个常量。 const int a=666; const int * p=&a; 常量a只能被访问而不能被改写,但指向常量a的常量指针可以改写。 指针常量 在指针定义语句的指针名前加const,表示该指针是常量。 int a; int * const p=&a; 指针常量在定义时必须初始化,且不允许修改,但其指向的地址的值可以修改,即p不可改写而*p可以改写。 常量指针常量:指向常量的指针常量 在定义时必须初始化。 const int a=666; const int * const p=&a; 很简单,p和*p都不能改写。
解引用 return 0; } 二、指针和指针变量 指针:地址 指针变量:变量-存放地址 指针变量用来存放地址的,指针变量并不完全等同指针 四、指针类型的意义(为什么不用ptr_t p代表所有指针) 1.指针解引用的时候有多大权限 (如果一个指针代替所有的话,解引用时的字节与变量定义类型不同) 2.指针类型决定了指针向前或向后走一步有多大 指针未初始化 2.指针越界访问造成野指针 3.指针指向的空间释放 1. 1.指针初始化 如果明确知道指针指向哪里就直接赋值地址,如果不知道指针应该指向哪里, 可以给指针赋值NULL。 七、空指针 空指针是一个特殊的数据类型,它的值定义为NULL。空指针不同于NULL的整数表示,它是一个指针变量的特殊值,表示该指针变量不指向任何有效的内存地址。
指针数组 数组指针 &数组名 与 数组名 数组指针的使用 数组传参、指针参数 一维数组传参 二维数组传参 一级指针传参 二级指针传参 函数指针 结语 前言 回想之前,我们学了指针的一些基础 指针与结构体 指针的大小是固定的4/8个字节(32位平台/64位平台)。 指针是有类型,指针的类型决定了指针的±整数的步长,指针解引用操作的时候的权限。 指针的运算。 有了前面的一些指针的基础之后呢,我们将从这篇博客开始,开始一起探究指针的高级主题:指针的进阶,将通过不止一篇的博客讲完这部分的内容,下面一起来看一看> ---- 字符指针 在指针的类型中我们知道有一种指针类型为字符指针 指针数组 在前面的时候,我们就认识了指针数组,指针数组是一个存放指针的数组。 有的,下面,我们来看看什么是数组指针 数组指针 数组指针是指针?还是数组? 答案是:指针。 整形指针: int * pint; 能够指向整形数据的指针。
2.修饰基本数据类型,如 NSInteger、BOOL、int、float 等;3.修饰对象类型时,不增加其引用计数;4.会产生悬垂指针(悬垂指针:assign 修饰的对象在被释放之后,指针仍然指向原对象地址 这时候如果继续通过指针访问原对象的话,会由于悬垂指针的原因产生内存泄漏或程序异常)。 weak 1.ARC 下才能使用。2.修饰弱引用,不增加对象引用计数,主要可以用于避免循环引用。 3.weak 修饰的对象在被释放之后,会自动将指针置为 nil,不会产生悬垂指针。 unsafe_unretained 1.MRC 下经常使用,ARC 下基本不用。 2.同 weak,区别就在于 unsafe_unretained 会产生悬垂指针。 retain 1.MRC 下使用,ARC 下基本使用 strong。 但是它会产生悬垂指针。
悬垂引用 在具有指针的语言中,很容易通过释放内存时保留指向它的指针而错误地生成一个 悬垂指针(dangling pointer),所谓悬垂指针是其指向的内存可能已经被分配给其它持有者。 相比之下,在 Rust 中编译器确保引用永远也不会变成悬垂状态:当你拥有一些数据的引用,编译器确保数据不会在其引用之前离开作用域。 让我们尝试创建一个悬垂引用,Rust 会通过一个编译时错误来避免: fn main() { let reference_to_nothing = dangle(); } fn dangle() return type contains a borrowed value, but there is no value for it to be borrowed from 让我们仔细看看在dangle(悬垂
Rust通过借用检查器对值的生命周期进行检查,其目的是为了避免出现悬垂指针。这点很容易理解,我们通过一段简单的代码来看一下。 在代码的第5行,b将所有权出借给了a,而在第7行我们想使用a时,b的生命周期已经结束,也就是说,从第7行开始,a成为了一个悬垂指针。因此这段代码会报一个编译错误。 ? 因为这样明显会造成悬垂指针。试想当你没有任何输入参数时返回了引用,那么引用本身的值在函数返回时必然会被析构,返回的引用也就成了悬垂指针。 first_sentence先于结构体实例i被定义,因此i的生命周期是短于first_sentence的,如果反过来,i的生命周期长于first_sentence即长于part,那么在part被析构以后,i.part就会成为悬垂指针 其实只要记住一个原则就可以了,那就是:生命周期参数的目的是帮助借用检查器验证引用的合法性,避免出现悬垂指针。 Rust还有几个深坑,我们下次继续。
1.智能指针是什么 简单来说,智能指针是一个类,它对普通指针进行封装,使智能指针类对象具有普通指针类型一样的操作。 具体而言,复制对象时,副本和原对象都指向同一存储区域,如果通过一个副本改变其所指的值,则通过另一对象访问的值也会改变.所不同的是,智能指针能够对内存进行进行自动管理,避免出现悬垂指针等情况。 而智能指针也致力于解决这种问题,使程序员专注于指针的使用而把内存管理交给智能指针。 我们先来看看普通指针的悬垂指针问题。 于是悬垂指针就形成了,程序崩溃也“指日可待”。我们通过代码+图来来探求悬垂指针的解决方法。 如果有个办法让ptr1知道,除了它自己外还有两个指针指向基础对象,而它不应该删除基础对象,那么悬垂指针的问题就得以解决了。如下图: ? ? 那么何时才可以删除基础对象呢?