(C语言之路-----p5: 指针)
一.什么是指针?
计算机要储存数据,就需要储存空间.这样的空间包括硬盘,高速缓存,内存,寄存器.今天,我们来讲讲内存.内存是一块很大的空间,储存数不胜数的数据,在这些数据被存储了以后,我们要怎么使用它呢?
计算机提供了一个东西来访问内存里的数据,叫指针.每块内存都有一个独属于它的指针,就好像地球上的每块地方都有独属于它的经纬度一样.每个指针对应着一个字节(1byte)大小的内存空间.
那么**,指针又是怎么来的呢?**计算机内有一个地方存储的很多电线,人们称之为地址线.地址线内通有两种电,分别是高频电和低频电,我们用1和0来表示频率的高低,这样地址线就可以表示很多种01序列,而这些01序列就是我们所说的指针啦.我们的计算机通常有32根地址线或者64根地址线(分别对应x86系统和x64系统),那么就可以分别产生232个指针和264个指针.
**指针需不需要空间呢?**指针是由物理电线产生的,不占据内存空间,除非你把指针主动放到一块内存空间里面,这样指针就占据一块内存空间了.
如果把指针放到一块内存空间,那么这块空间至少需要多大?换个说法就是,**指针有多大?**我们前面提到,指针是由地址线产生的,如果用数字表达指针,那么指针就是一长串由0和1排列的数字,0和1在计算机中被称为一个二进制位(或者比特位bit),是计算机最小的储存单位,8个比特位组成1个字节(byte),1024个字节组成1kb,1024kb等于1mb,1mb等于1gb,等等.当然,这里的1mb,1kb,1mb中的b是byte的b,而不是bit的b.那么我们就知道了,一个指针由32位(或64位)比特位组成,大小就是4个(或8个)字节.
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lfOBDSMa-1637247909264)(C:\Users\KAI\AppData\Roaming\Typora\typora-user-images\image-20211118215747917.png)]
**为什么指针指向的空间大小是1个字节呢?**由上文我们可以知道,计算机的地址线可以产生很多个指针,如果是32位机器的话,那么产生了2^32个指针.如果读者平时有了解计算机方面的常识的话,就应该知道计算机内存大小是4gb,8gb,16gb或者32gb(有可能更大),32位的机器内存大小是4gb,如果每个指针指向的空间大小是1bit,那么最多只能让0.5gb的内存空间拥有指针,其余的空间没有指针,就无法正常的访问,那么空间就被浪费掉了,但是,如果让1个指针指向1个字节大小的空间的话,那么能访问的空间就是4gb,刚好不浪费空间,那有人可能会说,为什么指针不一次指向1kb,1mb或者更大的空间呢?答案是一次指向太大的空间会让精确访问数据变的困难,所以一个指针指向一个字节大小的空间.这个设定被延续下来,用到了64位的操作系统.
最后,我们要说的是,我们口中常说的指针其实不是指针,而是指针变量,是用来存放指针的.指针也被称为是地址,指针变量也被称为是地址变量.
二.指针和指针类型
1.指针的初始化
char *pc = NULL;
int *pi = NULL;
short *ps = NULL;
long *pl = NULL;
float *pf = NULL;
double *pd = NULL;
2.指针的类型
上面的代码中,我们发现了指针也分为很多种.我们来分析一下
char *pc,pc是一个指向char类型的指针,那我是怎么知道的呢?在辨别指针类型的时候,有一个技巧,就是我们把这个指针看成一个表达式, *(pc)是对指针进行解引用,解引用是一个char类型的数据,那么这个指针就是指向char类型的指针.我们再举几个例子练习一下:
char *arr[10] ,arr是一个指针数组, *arr[10]表示对数组的10个元素解引用,得到char类型的数据,所以这个数组有10个元素,每个元素都是指向char类型的指针
char (*arr)[10] , arr是一个指向数组的指针, *arr表示对arr解引用, 得到char [10]类型的数组,说明这个指针是指向一个字符数组的指针,这个数组共有10个元素
char **pc, 这个可能有点难理解, * ( *pc),表示对 *pc解引用,说明 *pc是一个指针, *pc表示对pc解引用,表示pc是一个指针,最后得到的结果是一个char类型的数据,pc就是一个指向指针的指针,而被指向的指针是一个指向字符的指针.在后面我们也会提到,这其实是一个二级指针.
在上文我们多次提到( * )解引用符号,那么解引用是什么意思呢?其实解引用就是我们循着指针找到数据的过程,* ( *p)其实就是 * p的值.*p其实就是p指针所指向的值.
除了解引用符号以外,我们再介绍一个符号,取地址( & )符号, 取地址, 作用如其名字一样, 就是取出一个数据的地址,&a就是取出a的地址.
指针分为那么多类型,那么指针类型的意义是什么呢?
其实,不同类型的指针决定了对指针解引用的权限 其次,不同类型的指针往前往后走一步的长度是不同的(长度单位是字节)
2.1 指针解引用
int main()
{
int a = 0x11223344;
int* p1 = &a;
*p1 = 0;
printf("%#x\n", a);
int b = 0x11223344;
char* p2 = (char*)&b;
*p2 = 0;
printf("%#x\n", b);
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HPOCgRZA-1637247909266)(C:\Users\KAI\AppData\Roaming\Typora\typora-user-images\image-20211118220120829.png)]
2.1 指针±整数
int main()
{
int a = 1;
int* p1 = &a;
printf("%p\n", p1);
printf("%p\n", p1 + 1);
printf("%p\n", p1 + 2);
puts("");
char* p2 = (char*)&a;
printf("%p\n", p2);
printf("%p\n", p2 + 1);
printf("%p\n", p2 + 2);
return 0;
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VUQh4Ayr-1637247909267)(C:\Users\KAI\AppData\Roaming\Typora\typora-user-images\image-20211118220543806.png)]
三.野指针
野指针的定义
以下举两个例子,让我们可以更清晰的了解到野指针是如何产生的
1.指针未初始化
#include <stdio.h>
int main()
{
int *p;
*p = 20;
return 0;
}
2.数组越界访问
#include <stdio.h>
int main()
{
int arr[10] = {0};
int *p = arr;
int i = 0;
for(i=0; i<=11; i++)
{
*(p++) = i;
}
return 0;
}
那么,我们该如何解决野指针带来的问题呢?
//如何预防野指针:
//1.将指针初始化成一个特定的值,如果不知道初始化为什么,则把其置为NULL空指针
//2.检查数组下标
//3.函数尽量不返回局部变量的地址
//4.在使用指针之前检查其有效性
//5.如果一个指针不使用时,就把其置为NULL
#include <stdio.h>
int main()
{
int *p = NULL;
int a = 10;
p = &a;
if(p != NULL)
{
*p = 20;
}
return 0;
}
四.指针运算
1.指针±整数
#define N_VALUES 5
float values[N_VALUES];
float *vp;
for (vp = &values[0]; vp < &values[N_VALUES];)
{
*vp++ = 0;
}
在第三节中,我们使用指针±整数,其实这是将指针往前挪动或者往后挪动的意思,挪动的长度就叫步长.
2.指针 - 指针
指针与指针之间只能相减不能相加,两个指针只能位于同一个连续的空间(通常是数组)里面,计算的结果是两个指针指向的元素的距离,单位是(个),注意不是字节.
int my_strlen(char *s)
{
? ? ? char *p = s;
? ? ? while(*p != '\0' )p++;
? ? ? return p-s;
}//利用指针-指针来strlen函数
3.指针的关系运算
同一个数组内的指针可以互相比较,&arr[1] < &arr[2],&arr[i] < &arr[ i + 1], &arr[ j ] > &arr[ j - 1 ].
为什么是这样呢?因为数组元素是在内存中地址从小到大存储的,所以下标小的对应的那个数组元素的地址小于下标大的.
但是,这里有一点要注意的是,C语言标准中,不允许数组首元素与前面的一个地址比较大小,只允许数组的最后一个元素与其后面的那个地址比较大小,虽然标准不允许,但是很多编译器仍然可以这么做,在平时编程的时候不要拿数组首元素与前面的那个地址相比较比较保险.
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rJLOIhyS-1637247909268)(C:\Users\KAI\AppData\Roaming\Typora\typora-user-images\image-20211118225323888.png)]
五.指针和数组
指针(变量)和数组,一个是存放指针,一个是存放元素,这风马牛不相及的两个东西,为什么我要放到一起讨论呢?
在前面的一章介绍操作符的时候其实有提到,一个数组可以通过下标访问,也可以通过指针访问,即
arr[i] --> *(arr + i);
#include <stdio.h>
int main()
{
int arr[] = {1,2,3,4,5,6,7,8,9,0};
int *p = arr;
int sz = sizeof(arr)/sizeof(arr[0]);
for(i=0; i<sz; i++)
{
printf("&arr[%d] = %p <====> p+%d = %p\n", i, &arr[i], i, p+i);
}
return 0;
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-37GpcFOd-1637247909269)(C:\Users\KAI\AppData\Roaming\Typora\typora-user-images\image-20211118230254691.png)]
这是因为数组名就是首元素地址,对首元素地址进行±运算可以找到这个数组的任何一个元素.
当然指针和数组名是有差别的,指针(变量)是一个变量,我们可以对变量的值进行改变,可以 p++,但是数组名是常量,是没办法改变的(我们遇到的arr++实际上是形参的变量接受了实参,此时的arr就是一个指针变量)
&arr[i], i, p+i); ? } ? ?return 0; }
[外链图片转存中...(img-37GpcFOd-1637247909269)]
这是因为数组名就是首元素地址,对首元素地址进行+-运算可以找到这个数组的任何一个元素.
当然指针和数组名是有差别的,指针(变量)是一个变量,我们可以对变量的值进行改变,可以 p++,但是数组名是常量,是没办法改变的(我们遇到的arr++实际上是形参的变量接受了实参,此时的arr就是一个指针变量)
|