前言
嗨,我是firdawn,本章将简单介绍,指针对应的实际意义,以及指针的简单使用和如何避免野指针,下面的图是本章的思维导图,那么,让我们开始吧!
一,内存和地址
1.1 程序运行是在计算机的内存中,而内存通常有8G,16G,那么,这么大的内存空间,计算机是如何管理的呢,原来啊,计算机会将内存划分为一个个小的内存单元进行管理,为每个内存单元分配一个地址,方便数据的查找,删除和存放,而一个内存单元大小是一个字节,相关单位换算如下图,而计算机底层,储存的其实是二进制的信息,也就是0和1组成的二进制序列,所以一个bite(比特)储存的是0或者1。
1.2 那么我们怎么在VS中查看数据在内存中的储存形式呢,我们可以先按F10进入调试窗口,打开VS的内存窗口,在内存搜索框中输入&相对应的变量就可以找到该变量了(&操作符下面我们会介绍,这里先不急),通过内存窗口,我们发现,因为我们要观察的int类型变量x是占4个字节的,所以图中这4个字节的空间存放的都是变量x的数据,由于内存中放的二进制序列展示出来不方便观察,所以会将其以十六进制的形式展示出来,而对于每个地址内的数据,两个十六进制的数字,代表队就是8个bit,也就是一个字节的数据
声明:内存单元的编号 == 地址 == 指针,通常我们口头上说的指针指的是指针变量,但是实际上,在书面表达中指针指的就是地址
二,指针变量和地址
2.1 我们已经知道了,对于每个内存单元都有其唯一的地址,那么如果我们定义了一个变量,该怎么得到他的地址呢,其实我们用&操作符就可以实现,假如这个变量是int类型,那我们将它的地址取出来放到指针变量里,这个变量的类型就是int * ,表示它是一个指针,int 表示这个指针指向的是一个int 类型的数据,那么如果我有一个char类型的数据,存放它的地址的指针变量的类型是什么呢,其实道理是一样的,它的类型为 char * , 表示它是一个指针,char 表示这个指针指向的对象为char 类型。如下图2.1-a
2.2 既然我们已经有了变量指针,那我们如何去使用它呢,通过解引用操作符 * ,就可以通过解引用指针找到对应的变量,如上图2.1-a ,pch中储存了字符变量ch的地址,而通过 *pch ,我们就可以找到ch,换句话说,*pch等价于ch,*px 等价于x。
三,指针变量类型的意义
3.1 对变量进行解引用 ( * ) 操作,得到的其实是其几个字节中最小的那一个地址,在32位平台中指针变量都是4个字节,在64平台中指针变量都是8个字节的大小,假设我们使用的是32位平台,那么,既然指针变量都是4个字节,都是用来储存地址的,我们为何不创造出一个统一的指针类型,它的大小是4个字节,它可以指向所有的变量,这样不是更省事吗,然而,我们对指针的类型进行了区分,为什么要这样做呢,原来,指针的类型,决定了我们对其解引用操作时,能够访问的空间大小,如果是char* 类型的指针,对其解引用,我们只能访问一个字节的空间,如果是int* 类型的指针,对其解引用,我们就能够访问4个字节大小的空间,指针的类型决定了不同的视野,站在不同的视野,它能够访问的空间大小就不同。如下图
3.2 指针除了能进行解引用操作( * )加减访问对应的空间,还能够加减整数,如果 pa是一个int* 类型的指针,那么,pa+1 跳过1个int类型的大小,一次跳过4个字节,指向后面的空间;如果pa是一个char* 类型的指针,那么,pa+1 跳过1个char类型的大小,一次跳过1个字节,指向后面的空间。
3.3 在指针类型中,有一种特殊的指针类型,就是 void* ,它能接收任意类型的变量的地址,但是不能进行解引用操作和指针加减整数的操作,因为不知道它指向的变量的类型,就不知道解引用操作时访问几个字节的空间或者指针加减整数操作时跳过几个字节,它一般用于函数中,当我们不知道要接收到是什么类型变量的地址时,就可以使用void* 来接收。
四,const修饰指针
4.1 当我们有一个指针变量,通过解引用指针我们可以修改对应的变量,如果我们不希望修改对应的变量,我们就可以用const 来修饰这个指针变量,例如:int x = 10; int const* px=&x;这里const放在 * 的左边修饰的是 *px ,那么 *px 就不能被修改,否则会报错,这是语法上的限制,如果我们是int * const px = &x;const放在 * 的右边,修饰的是px,那么px就不能被修改。
五,指针运算
5.1 既然我们有了指针,那么指针能进行哪些运算呢,首先,就是我们提到过的指针加减指数的计算,一个 int* 类型的指针,一次加减整数num,能跳过num个int 类型的大小,指向后面的空间。
5.2 其次,指针能够减指针,得到的是他们之间相差大元素的个数,需要注意的是,指针减指针,需要同一类型的指针进行加减才有意义,并且这两个指针要指向同一块空间。对于,指针加指针是没有实际意义的,就像对于日期,我们进行想减能得到相差的天数,但是日期想加,就没有意义了。
5.3 指针是可以用 > , < , ==,进行比较的,这时候,指针可以看作是一个数值,指针比较大小,进行的就是数值的比较。
六,野指针
6.1 我们在定义一个局部指针变量时,如果不对其初始化,它内部存放的就是随机的地址,这个指针指向的空间不是分配给我们的,我们不清楚这个空间上存放的数据的作用是什么,那我们就原则上不能对其进行修改,那么这个未初始化的指针就是野指针;再举个例子,如果我定义了数组arr[ 10 ],那么我们就不能访问arr[ 15 ]的数据,因为这块空间不是分配给我们的,如果我们访问了,就是数组的越界访问了,但是如果有一个指针,指向的是arr[ 15 ]的地址,那这个指针就是野指针,因为它指向的空间不是属于我的,我们是不能对其修改的。野指针就像一条野狗一样,它是非常危险的,如果一不小心访问了对应的空间,修改了数据,可能就会让计算机丢失数据,当然,计算机内有一些空间上的数据是不能修改的,我们对其进行修改可能就会报错。
6.2 既然野指针这么危险,我们有没有什么办法来解决它呢,其实是有办法的。
1.定义指针变量时同时对其初始化,给它赋值一个有效的地址,如果我们不知道要赋什么值的话,可以赋值NULL,也就是空指针,在cplusplus网站中我们可以知道,NULL其实就是void* 0 ,指向的就是0x00000000 这块空间,而这块空间,我们是无法对其修改的。在使用这个指针变量时,如果我们不确定它是否指向有效的空间,我们就可以对其进行是否为NULL的判断。这就像给指向无效空间的指针加了一个标签,方便我们辨认出,它是否指向有效的空间,这就比原来两眼一黑,无法辨认出这个指针是否指向有效空间好多了。
七,assert断⾔
7.1 如果一个指针为空指针 ,那他指向的就是无效的空间,我们是无法对这个空间上的数据进行修改的,否则电脑会报错,那么如果我们不知道一个指针是否是NULL,我们该怎么避免对NULL的解引用呢,我这时候可以使用assert,assert是一个库函数,使用它要包含头文件,用#include <assert.h> ,它的使用如下图
八,指针的使用和传址调用
8.1 学习了指针,那么我们讲一个使用指针的例子,strlen 的模拟实现,strlen 是一个库函数,可以计算一块连续的空间中’ \0 '之前的字符个数,具体可以参考cplusplus:strlen库函数介绍,代码如下图。
8.2 函数调用有两种,分为传值调用和传址调用,传值调用,传入的是变量的值,在被调用的函数中形参是实参的一份临时拷贝,改变形参不影响实参;传址调用,传入的是变量的地址,对该地址进行解引用 * 操作,能够找到该变量,可以改变实参。