一、内存与地址
在正式进入正文之前,我们先聊一个我们十分熟悉的事物:宿舍。
我们刚来大学,是如何找到属于我们的宿舍的呢?几栋楼的哪个门牌号的宿舍。我们设想一下,如果我们没有宿舍的门牌号,我们要怎么才能找到我们的宿舍呢?只能一间一间宿舍进里面看,这样找效率就十分慢。
所以我们需要给宿舍贴上门牌号,能够提高效率,迅速找到对应的宿舍。
对应到计算机中也同样如此。
内存就相当于与整个宿舍楼,内存有8G,16G等等,决定的是内存大小,对应就是宿舍楼大小。
在内存中,我们同样将内存划分为一个个内存单元(一个内存单元就是一个字节),并对每一个内存单元做了一个编号。每一个内存单元可以看成宿舍楼中的一间间宿舍,编号就是宿舍的门牌号。这样CPU就能快速找到对应的内存单元。
我们在二进制中了解到:1字节 = 8比特位,而这一个比特位可以看成宿舍中的一名学生(这个宿舍是8人寝宿舍)。
我们以32位的为例:
在计算机中,我们将内存编号称为地址。在C语言中,又将地址起了一个新的名字:指针。
所以:内存编号 == 地址 == 指针
二、指针与地址
那我们又该怎么理解指针这种数据类型呢?
在上面我们知道,指针 == 地址,联想一下int 类型的的变量,int类型的变量里面存放的是一个int类型的值,那么对于指针变量,里面也是存放了一个指针类型的值(也就是地址)。
我们要先了解,C语言创建变量本质是向内存申请了一块空间。
由于a是int类型的变量,所以a的大小是4个字节。
地址分别是:00EFF9F8 00EFF9F9 00EFF9FA 00EFF9FB
那么我们如何得到a的地址呢?
这就是我们在操作符中了解到的&(取地址操作符)。
此时,我们发现p中的值只有一个----a中四个地址中最小的地址。
结论:&取地址操作符是取出变量最小的地址。
那我们如何通过地址去访问地址中的内容呢?这就需要*(解引用操作符)。
有人可能会想:我直接使用a变量就行,还更简便,为啥要取出a的地址,再用p去访问,麻烦。
这样做可以多出一条路径去使用变量a,使代码更加灵活,在之后多次使用中会理解。
三、指针变量类型意义
1. 拆解指针
我们了解到:指针变量中存放的是地址,但是地址怎么会有int ,char等数据类型呢?
确实,地址本身没有那些数据类型,所以对于int * p;* 表示p变量是一个指针,* 前面的数据类型表示的是p这个变量指向的对象的数据类型,例如:我们这个p变量就是指向一个int类型的对象。
同样,如果我们要得到一个char变量的地址,就创建这样的指针:char* pc;
2. 指针变量意义
接下来,我们具体谈谈这些指针变量类型的意义是什么:
我们先来看看这两个代码:
现象:第一个代码将a的四个字节全部改为了0,第二个代码只将a的第一个字节改为了0
结论:指针的类型决定了指针解引用时有多大的权限(也就是一次操作几个字节)。
3. void* 指针
指针变量类型中有一种特殊的指针类型:void*,可以理解为无具体类型的指针变量。
这种指针变量可以接受任意类型的地址,是不是很好用,但是同时有很大缺陷:无法直接进行解引用和指针+-整数操作。(因为无法确定一次操作几个字节)(可以使用强制类型转换变为其他指针类型后再操作)
我们如果要使用int* p 接受一个char类型的地址,编译器就会发出警告:
但是使用void* p接受就不会:
那我们试试用void* 类型的指针进行解引用:
果真,编译器直接报错,无法执行程序。
同样,我们让void* 类型指针 +- 整数试试:
那么,void* 类型指针到底有啥用呢?
如果我们需要实现泛型编程,我们并不清楚会传来什么类型的地址。这时,我们可以让参数中的指针类型为void*,使得编写的函数能够实现对多种数据的处理。(在进阶指针中讲解)
四、指针运算
1. 指针 +- 整数
我们把指针看成日期,日期 +- 天数结果是什么,还是日期。指针 +- 整数 结果也是指针。
我们先看一个代码:
现象:int*类型的指针+1----跳过4个字节(int类型大小),char*类型指针+1----跳过1个字节(char类型大小)
结论:type * 类型的指针,+i 后,跳过i * sizeof(type)个字节大小(相当于跳过 i 个type类型的数据)。
我们知道数组中的元素在内存中是连续存放的,我们可以使用指针来访问数组中每一个元素:
2. 指针 - 指针
首先,我们思考一下,日期 - 日期的结果是什么?这两个日期之间的天数。
那么指针 - 指针 也是同样的,表示的是这两个指针之间的元素个素(可以为负数)。
我们先来看下面这个代码:
切记:指针 - 指针 不是两个指针地址的值得差值(也就是字节个数)。
由于这个原因,指针 - 指针有两点需要特别注意:
①这两个指针一定要是相同的类型,否则警告。
②这两个指针一定要指向相同的对象,否则结果无意义。
(因为两个变量的内存空间有空隙,结果与编译器有关)
3. 指针的关系运算
我们对于日期的比较,比较的是两个日期的值,指针的比较也同样如此,比较地址值得大小。
五、野指针
野指针:指针指向的位置是不确定的(随机的,不可知的)。
例如:
我们可以将野指针比作野狗,当我们创建了一个野指针,相当于有一只野狗在街上走,谁也不知道这只野狗什么时候会暴起伤人,所以我们应当避免出现野指针。
在编程中,我们使用野指针p就是对一块不属于我们程序员自己的空间进行操作,一旦p指向的地址改变了,我们就无法找到那块空间,也就无法使用那块空间了。
1. 野指针成因
1️⃣指针未初始化
2️⃣指针越界访问
3️⃣指针指向的空间释放
2. 规避野指针
我们知道了野指针如何形成的,那么只需要对症下药就行。
1️⃣指针初始化
在我们创建指针时,如果不知道指针指向哪里,我们可以给指针赋值NULL。
NULL 是C语⾔中定义的⼀个标识符常量,值是0,0也是地址,这个地址是⽆法使⽤的,读写该地址 会报错。
2️⃣小心指针越界
作为程序员,我们应该清楚自己申请了哪些空间,指针也只能访问那些空间,超出范围的空间不进行访问。
3️⃣避免返回局部变量的地址
为了防止造成野指针第三种成因,不要返回局部变量的地址。
4️⃣指针变量不再使⽤时,及时置NULL,指针使⽤之前检查有效性
我们使用指针指向一块地址,就可以通过指针访问这块地址。但是,当我们不在使用那个指针时,可以将该指针赋值为NULL,当指针指向NULL时,无法对该指针进行解引用等操作。同时,在使用指针之前,我们可以判断该指针是否为NULL。
六、const关键字
1. const修饰变量
我们知道,变量是可以被修改的,但是我如果要创建一个变量,这个变量不能被修改怎么办呢?
这时,就是const关键字出手的时候了。const修饰的变量在语法上不允许被修改,只能初始化。
我们说过,指针通过解引用就等价于指针指向的变量,那么我们能不能通过指针修改呢?
答案很明显:能。这跟我们的需求发生了矛盾,我们能不能让指针也无法修改地址中的内容呢?
2. const修饰指针
我们先来看看下面的代码:
当我们在指针前面也加上const修饰后,也无法通过p解引用来修改b的值了。
在语法上:const修饰指针可以放在两个位置:在* 前面,在* 后面。
①在* 前面:就像上面的代码一样,无法通过指针解引用修改地址中的内容,但是可以修改指针指向的对象地址。
②在* 后面:这个就与第一种相反,能够使用指针修改地址中的内容,但是不可以修改指针指向的对象地址。
所以,当我们需要一个指针既不能修改内容,也不能更改指向的对象,可以两个const一起用。
例如:const int* const p;