0基础C语言自学教程——第七节 初始指针
目录
内存和地址
指针引入
指针和指针类型
野指针
1. 指针未初始化
2. 指针越界访问
3. 指针指向的空间释放
如何规避野指针
指针运算
二级指针
指针是什么?
在讲解指针之前,我们需要探讨一个概念,那就是内存。
内存和地址
我们把计算机中的内存看作一条长街上的一排房屋。比如繁华大道上的多少多少号。每个房子可以容纳数据,并通过多少多少号来标识。
而在计算机内存中,计算机的内存由数以亿万计的位(比特位)组成,每一个位可以容纳0或者1.
如图,计算机由数以万计可以容纳1或者0的“房屋”组成。
而8个这样的比特位组成一个字节,每一个地址通过一个字节来标识。
所以,我们也可以这样理解:把计算机中的内存划分成为一个一个小的内存单元,也就是字节。
为了存储更大的值,我们通常将2个或者更多的字节组合在一起,形成一个新的、更大的内存单位。比如,我们之前所说的,一个int类型占用4个字节(32位),一个double类型占用8个字节等。而如果是4个或者8个字节,通常都有且仅有一个地址。
而内存单元是有编号的,而这些编号,通俗一样上就是我们所说的地址。
每个地址标识一个字节,那我们就可以给(2^32Byte == 2^32/1024KB ==2^32/1024/1024MB==2^32/1024/1024/1024GB == 4GB)4G的空闲进行编址
在这些类型中,就可以通过0或者1来存储值。那么如果找到了这些元素的地址,就可以确定我们想要找的值了。
(plus:为什么会产生0和1?这里简单说一下,对于32位平台 - 有32根地址线 - 通电与否 - 代表着0/1。64位同理,有64跟地址线 -> 电信号就转换为数字信号)
但是,我们如果仅仅通过记住地址来去访问,是不是有点傻...所以,我们就引出了指针。
指针引入
指针理解的2个要点:
1. 指针是内存中一个最小单元(或者最大,取决于机器)的编号,也就是地址。(指针即地址)
2. 平时口语中说的指针,通常指的是指针变量,是用来存放内存地址的变量。
举一个例子来看看:
如上面的图片所示,存储单元a中存储着数据100,它的地址位0x001132,我们把这个地址存储在p里面,那么,这里的p就是指针变量。p里面存放的就是a的地址。
理解了这么一对关系,实际上就抓住了关键。
我们再来明确一下各个量的含义:
0x001132是一个编号,也就是地址,而地址就是指针。
在这里的p,就是我们通常意义上的指针变量。
我们来用代码演示一下:
我们所打印出来的00B8FD18就是内存编号,就是a的地址。
我们还可以通过内存来查看:
这里的0x00EFFDCC就是它的地址,而0a 00 00 00就是它存储的值。
0a 00 00 00表示的是16进制数字,其中,0a表示一个字节,00 00 00 分别又表示着三个字节。
(我们只看前面四个字节的内容。后面的cccc表示未定义。具体的我们会在函数的栈帧的创建和销毁中去讲解)
那我如果这样:
那么这里的p便是我们所说的指针变量,其存储的就是a的地址。
我们打印p,可以看到,打印的就是a的地址。
上面的
int* p = &a.
以为p的类型为int*,而p是指针变量,存储的是a的地址。
那么,一个指针占多大内存呢?
我们在32位平台上来看一看:
可以看出,在32位平台上,一个指针占4个字节的大小。
对于64位平台,读者可以自行调试,其应占用8个字节的大小。
指针和指针类型
我们知道,数据的类型有整型、浮点型等等。那么对于指针有没有类型呢?
准确来说,是有的。
我们在上面定义指针类型的时候,我们留意到,是用int* 来定义的。依次类推,我们在定义其他类型指针的时候,也是用这种方法:
例如:
short* p1 = NULL; char* p2 = NULL; int* p3 = NULL; float* p4 = NULL; double* p5 = NULL;
所以,char*里存放的是char类型的数据;
int* 里存放的是int类型的数据;
以此类推...
那么同理,在对指针进行解引用的时候,是什么类型就能访问相应类型字节的大小的空间。
比如,对char*类型的指针进行解引用,就只能访问一个字节;
int* 类型就只能访问四个字节。
....以此类推。
可以在内存监视中去查看。
野指针
什么叫野指针?
就是说指针所指向的位置是随机的、未确定的、没有明确限制的。
写出野指针会造成非常严重的后果。如果你的编译器语法检查足够严格,那么程序会直接崩溃掉。
那么,造成野指针的原因是什么呢?
1. 指针未初始化举个简单的例子,
举个简单例子
int* p;
这样定义一个未初始化的指针,如果其在未初始化的时候使用它,那么就会造成崩溃。
请看
int* p;
这样定义一个未初始化的指针,如果其在未初始化的时候使用它,那么就会造成崩溃。
请看:
为什么会这样?
原因很简单。因为 你的p不知道指向的是内存中的哪一块空间。它是随机的。在它不知道指向哪一块空间的情况下,对其解引用,就会造成 “不知道访问的是哪一块随机的地址” 这样一种尴尬。而这显然是不允许的。
2. 指针越界访问
指针的越界访问,与上面本质上是一样的。都是指针不知道指向了哪一块随机的地址。
举个例子:
int a[] = {0,1,2,3,4}; int* p = a; p--;
一旦这样写,p就飞到九霄云外去了,不再在数组的“限制(控制)范围之内”
这个时候,p就是野指针。
3. 指针指向的空间释放
在动态开辟使用空间时,在free()以后,其free的只是指针后面的内容,而该指针本身就变成了野指针。(会在动态开辟内容章节详细讲解)
来看:
int* p = (int*)malloc(sizeof(int)*n); free(p);
此时,p就是一个野指针。
所以通常情况下,我们都会再将p置空。
即
p = NULL;
如何规避野指针
几条建议:
1. 指针初始化
2. 小心指针越界
3. 指针指向空间释放即使置NULL
4. 避免返回局部变量的地址(这一条我们将会在下一章详细讲解)
5. 指针使用之前检查有效性
指针运算
指针与指针之间,可以相减,但是不可以相加、相乘、相除;同时,指针可以加一个或者减去一个常数,但是不能够与常数相乘除。
并且,需要注意的时,两指针相减得到的并不是内存地址的差值,而是相差的数据类型的个数。具体来说,就是
(内存地址差值)/ (数据类型的大小)
另外,指针也可以比较大小。
还需要注意一点:
允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,
但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。
二级指针
这个其实很简单,也很好理解,说白了,就是一个指向指针的指针。
我们来看:
int a = 10; int* p = &a; int** pp = &p;
就是这个意思。
这里的pp就是一个二级指针。