C语言基础|【C语言入门必看】指针

指针

文章目录

  • 一、指针是什么
    • 1.内存划分
    • 2.指针和指针变量
  • 二、指针和指针类型
    • 1.指针解引用方面
    • 2.指针加减整数
  • 三、野指针
    • 1.野指针定义
    • 2.野指针成因
    • 3.如何规避野指针
  • 四、指针运算
    • 1.指针±整数
    • 2.指针-指针
    • 3.指针的关系运算
  • 五、指针和数组
  • 六、二级指针
  • 七、指针数组

一、指针是什么 1.内存划分 内存是一块很大的空间,由一个个小的占1个字节(bit)的内存单元组成,每一个内存单元对应着一个地址,即对内存单元的编号。
现实中,每个人有身份证,可以通过身份证找到这个人,内存单元也是如此,可以通过地址找到该内存单元。
内存,看图:
C语言基础|【C语言入门必看】指针
文章图片

2.指针和指针变量 上代码:
#include int main() { int a = 10; //在内存中开辟一块空间 int *pa = &a; //这里我们对变量a,取出它的地址,可以使用&操作符。 //将a的地址存放在p变量中,p就是一个指针变量。 return 0; }

我们定义一个变量a,在内存中会分配出4个字节的空间给 a 。变量a有4个字节且第一个字节的地址是 0x0012FF43 ,它就代表着变量a的地址。通过该地址我们可以找到变量a,也可以说该地址指向了变量a,因此地址被形象化地称为指针。
那什么是指针变量呢?
没错,就是存放指针(地址)的变量,上述代码中,int *p = &a; 含义:以int* 类型创建变量pa,并取a的地址(指针)存进pa。变量pa中存储的是地址(指针),因此称它为指针变量。当然创建变量pa,也需在内存空间中分配空间。
如图:
C语言基础|【C语言入门必看】指针
文章图片

总结:
1.指针即地址,地址即指针。
2.指针变量是存放地址的变量,其中的内容都被当作地址处理。
注:指针变量经常被人们简称为指针,我们要去从语境中区分他人说的是指针还是指针变量。
那么就有问题来了:
  • 一个小的单元到底是多大?
  • 如何编址?
从上图可知,一个小的单元是一个字节。为什么是一个字节呢?
【C语言基础|【C语言入门必看】指针】对于32位的机器,假设有32根地址线,那么假设每根地址线在寻址的是产生一个电信号正电/负电(1或者0)。
那32根地址线有多少种0、1组合呢?
一根地址线有2种,那32根则有232种,即从32个全0到32个全1。
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 ... 11111111 11111111 11111111 11111110 11111111 11111111 11111111 11111111

若每一个这样的二进制序列作为内存单元的编号,则232个地址可以管理232个 内存单元,因此有232个内存单元可供使用,为了更加直观表达,转化为十进制232=4 294 967 296。
计算机存储容量单位换算(从左往右÷):
C语言基础|【C语言入门必看】指针
文章图片

若每个内存单元大小是1个bit,4294967296/8/1024/1024/1024=0.5GB
这样的话,管理的空间实在太小了,且一个char类型的变量就得花掉8个地址,int类型就花掉32个地址,实在太浪费了。
因此,一个地址管理一个char类型更合理,即每个内存单元大小是1个byte,转化后是4GB
那每个内存单元是一个kb呢?
不行,因为当你创建一个char类型,内存就得申请1个kb的空间,这就显得小题大做。
同样的逻辑,也可以推导64位机器编址空间。
这里我们就明白:
  • 在32位的机器上,地址是32个0或者1组成二进制序列,那地址就得用4个字节的空间来存储,所以一个指针变量的大小就应该是4个字节。
  • 那如果在64位机器上,如果有64个地址线,那一个指针变量的大小是8个字节,才能存放一个地址。
总结:
  • 指针变量是用来存放地址的,地址是唯一标示一块地址空间的。
  • 指针的大小在32位平台是4个字节,在64位平台是8个字节。
二、指针和指针类型 变量有intchar等不同的类型,可知指针变量也有不同的类型。
int a = 10; int* pa = &a;

  • *代表pa是个指针。
  • int 代表指针pa所指向的变量类型是int类型
这时,我们的小脑袋瓜就会想到:无论什么类型的指针变量,在32位平台下不都是4个字节吗?那为什么还要区分类型呢?
C语言中,指针变量是区分类型的,它这么干有意义的,主要体现在两方面:
  1. 指针解引用
  2. 指针加减整数
1.指针解引用方面
int a = 0x11223344; //十六进制,每一个数字可转化成4个bit int* pa = &a; *pa = 0;

我们创建并初始化一个变量a,并用指针变量pa指向a,然后对pa解引用把a赋为0,调试并在内存中发现:
C语言基础|【C语言入门必看】指针
文章图片

接着,我们更改指针变量的类型,int* pa改为char* pa
C语言基础|【C语言入门必看】指针
文章图片

对比下,我们发现同样*pa的操作,解引用访问int*创建的pa可以修改4个字节的内容,而解引用访问char*创建的pa只能修改一个字节的内容。
说明:
意义1:指针类型决定了指针解引用访问时,一共可以访问几个字节(访问内存的大小)。
char* -指针解引用访问1个字节 int* -指针解引用访问4个字节 一一对应:char*指向是char型,char型恰好1个字节,int*同理。

2.指针加减整数 现在我们用不同类型的指针分别指向同一个变量a,并对指针+1。如:
C语言基础|【C语言入门必看】指针
文章图片

可见,int*型的指针+1向后跳了4个字节;char*型的指针+1向后跳了1个字节。
说明:
指针类型决定了(指针±整数)的步长(跳过几个字节)。
总结:
指针类型决定了:
  1. 指针解引用访问几个字节(访问的内存空间大小)
  2. 指针类型决定了指针±整数跳过几个字节(步长)
这样的话,我们用不同类型的指针,就可以实现跳过不同的字节,实现不同的访问方式。看下面两段代码:
int arr[10] = { 0 }; int i = 0; int* pa = arr; for (i = 0; i < 10; i++) { *(pa + i) = 1; }

int arr[10] = { 0 }; int i = 0; char* pa = &arr; for (i = 0; i < 40; i++) { *(pa + i) = 1; }

调试并在内存中可见:
C语言基础|【C语言入门必看】指针
文章图片

第①种以4个字节为一跳;第②种以1个字节为一跳。
看图:
C语言基础|【C语言入门必看】指针
文章图片

三、野指针 1.野指针定义 概念:野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
2.野指针成因 1.指针未初始化
int *p; //局部变量指针未初始化,默认为随机值 *p = 20;

定义指针时就要对其初始化,即指向一个变量的地址,若暂时无,可先初始化为空指针NULL。
2.指针越界访问
int arr[10] = { 0 }; int i = 0; int* p = &arr; for (i = 0; i <= 10; i++)//当指针指向的范围超出数组arr的范围时,p就是野指针 { *p = i; p++; }

p++执行10次后,此时的p指向的地址不再属于数组地址,它指向的是后面的内存,属于越界访问,越界访问是非法的。
3. 指针指向的空间释放
int* test() { int a = 10; return &a; //返回a的地址,假设是0x0012ff40 }int main() { int* p = test(); //p存的是a的地址,0x0012ff40 printf("%d\n", *p); return 0; } 显示:10

我们知道函数调用完成,其局部变量会自动释放,虽然指针变量p存的还是原来a的地址,但这块内存已经被编译器归还给操作系统了,若再访问便是非法访问。
但执行程序却打印出10,这是为什么?
这是因为变量a的内存空间归还给操作系统,但并没有对其销毁,而且没有其他函数覆盖它,编译器会对其作一次保留,*p仍然会打印出10。
在打印*p前再调用一次printf()函数:
int* test() { int a = 10; return &a; //返回a的地址,假设是0x0012ff40 }int main() { int* p = test(); //p存的是a的地址,0x0012ff40 printf("hehe\n"); printf("%d\n", *p); return 0; } 显示: hehe 5

因为内存中的栈区分配空间是从高地址到低地址的。调用printf("hehe\n"); 会覆盖原来test()函数所在的空间,此后,*p将会非法访问。
问:为什么只有printf("%d\n", *p); 时,该printf函数不会把test()覆盖呢?
因为传参先于调用,在调用printf()时,*p已经是10了
看图:
C语言基础|【C语言入门必看】指针
文章图片

栈区的空间是先用高地址再到低地址:
C语言基础|【C语言入门必看】指针
文章图片

3.如何规避野指针
  1. 指针初始化
int a = 10; int* pa = &a; //定义指针时就得指向地址 int* pb = NULL; //不知道指向谁,先置为空指针

  1. 小心指针越界:访问数组元素时
  2. 指针指向的空间释放后,立即置为NULL
  3. 避免函数返回局部变量地址
    提一嘴:返回函数静态变量地址是有效的,因为静态变量地址不会销毁。
    疑惑:为什么可以返回局部变量,却不能返回局部变量地址呢?
函数调用完毕,两者都会销毁,返回的局部变量是一份拷贝(将数值拷贝),后续不用对其访问;而返回的局部变量地址,后续若对其访问,则是非法的。
  1. 指针使用之前检查有效性
    空指针不可解引用,所以在使用指针前可以先判断指针是否为空指针NULL。
if(p != NULL) { *p=20; //检验不为空指针,再使用 } //若是空指针,不执行操作

四、指针运算 1. 指针± 整数
2. 指针-指针
3. 指针的关系运算
另外,指针解引用也是指针运算。
但为什么没有指针+指针运算呢?
指针+指针是合法的,但是无意义的。
打个比方,指针是"日期",整数是"天数",指针±整数仍是指针,相当于,日期±天数还是日期。
指针指针是元素的个数,相当于日期日期是天数;日期日期并没什么实在意义,就如同指针指针。
1.指针±整数
#define N_VALUE 5 int main() { float values[N_VALUE]; float* vp = values; for (vp = &values[0]; vp < &values[N_VALUE]; ) { *vp++ = 0; //*(vp++) } return 0; }

程序分析:
*vp++,根据优先级,相当于*(vp++),后置++:先使用vp并解引用,再++。指针++逐次跳过一个类型大小的字节
②当vp指向values[N_VALUE],不满足条件判断,结束循环。
&values[N_VALUE]该地址不属于数组,仅用于判断,并没有访问
看图:
C语言基础|【C语言入门必看】指针
文章图片

2.指针-指针
int arr[10] = { 1,2,3,4,5,6,7,8,9,0 }; printf("%d\n", &arr[9] - &arr[0]); //9 printf("%d\n", &arr[0] - &arr[9]); //-9

得到的数值的绝对值是指针和指针之间元素的个数
为什么不是36呢?
语法规定指针-指针是两个指针之间的元素个数,默认是所占字节大小除以类型大小。
那是不是随便两个指针都可以相减?
当然不是。
int arr[10] = { 1,2,3,4,5,6,7,8,9,0 }; char ch[5] = { 0 }; printf("%d\n", &arr[9] - &ch[0]); //err

程序能正常执行并打印出13。这时候有很大的疑问,&arr[9] - &ch[0] 这两个地址之间有什么关系呢?相减所得的数值(元素个数)是以int类型还是char类型呢?所以得到的数值是没意义的。
从这里我们明白:
  1. 指针-指针的前提:两个指针指向同一块区域(同一个数组)。
  2. 指针-指针:两个地址之间的元素个数。
学习上面的知识,我们可以做出应用:
计算字符串长度-strlen函数
int my_strlen(char* s) { int count = 0; while (*s != '\0') { count++; s++; } return count; }

用指针指针实现
int my_strlen(char* s) { char* start = s; while (*s != '\0') { s++; } return s-start; //指针-指针:两指针之间的元素个数 }

3.指针的关系运算 将指针±整数的代码拿来修改,从末尾向前遍历。
第一种:
for(vp = &values[N_VALUES]; vp > &values[0]; ) { *--vp = 0; }

*--vp:先--后对vp解引用置为0,vp逐次减1并与&value[0]比较
第二种:
for (vp = &values[N_VALUE-1]; vp >= &values[0]; vp--) { *vp = 0; }

区别:最后一次循环,vp指向&value[0]前面的那一块地址,最后不满足判断条件,结束循环。
上图:
C语言基础|【C语言入门必看】指针
文章图片

我们应该避免使用第二种方法,因为C语言语法规定:
允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。
即:允许vpp1比较,但不允许vpp2比较。
原因:p2区域可能存储数组相关信息,如数组元素个数等,所以越界的时候,风险会大,这是编译器不想看到的。
C语言基础|【C语言入门必看】指针
文章图片

地址本质也是一个数值,是可以比较大小的,即指针的关系运算是合理的。
五、指针和数组 指针和数组之间有什么联系呢?
  • 指针变量存储地址,是一个变量,指针的大小固定为4 (32位) / 8 (64位)。
  • 数组是一个相同类型元素的集合,其中元素存放在连续的空间中。数组的大小取决于元素类型和元素个数。
数组的每一块内存单元都是有地址的,而指针就是地址,我们把数组元素的地址赋给指针,通过指针来访问数组,这样便建立了联系。
int arr[10] = { 0 }; printf("%p\n", arr); //0x0012ff40 printf("%p\n", &arr[0]); //0x0012ff40

由此可知,数组名就是数组首元素的地址,但前提是除了以下两种情况
1.sizeof(arr)
2.&arr
这两种情况是整个数组的地址,+1会跳过整个数组,数组章节提过。
顺便提一嘴:
printf("%d", sizeof(&arr));

这种情况,&arr是一个地址,地址的大小固定为4 (32位) / 8 (64位)。
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 }; int* p = arr; for (int i = 0; i < sz; i++) { printf("&arr[%d] = %p <===> p+%d = %p\n", i, &arr[i], i, p + i); }

运行结果:
C语言基础|【C语言入门必看】指针
文章图片

所以p+i其实计算的是数组arr下标为i的地址。那我们就可以直接通过指针来访问数组
六、二级指针 二级指针变量:存放一级指针变量的地址,通过二级指针访问一级指针。
C语言基础|【C语言入门必看】指针
文章图片

对于二级指针的运算有:
  • *ppa 通过对ppa中的地址进行解引用,这样找到的是 pa*ppa 其实访问的就是 pa
int b = 20; *ppa = &b; //等价于 pa = &b;

  • **ppa先通过*ppa找到pa,然后对pa进行解引用操作:*pa,便找到a
**ppa = 30; //等价于*pa = 30; //等价于a = 30;

有n级指针,就得n次解引用才找到变量a
类型中"*"的理解
C语言基础|【C语言入门必看】指针
文章图片

绿框代表是指针变量,红框代表该指针指向的变量类型,"*"有n个,就是n级指针。
如:二级指针ppa前面的int*代表的是ppa指向的变量pa的类型是int*
七、指针数组 指针数组是指针还是数组?
是数组。是存放指针的数组。
我们已经知道整型数组和字符数组。
int arr[5]; //整型数组 - 存放整型变量的数组 char ch[5]; //字符数组 - 存放字符变量的数组

通过类比,我们可以得出指针数组
int* parr[5]; //整型指针数组 - 存放整型指针变量的数组 char* pch[5]; //字符型指针数组 - 存放字符型指针变量的数组

指针数组的使用
int a = 10; int b = 20; int c = 30; //int* pa = &a; //int* pb = &b; //int* pc = &c; //...当需要创建大量指针变量 int* arr[] = { &a,&b,&c }; int i = 0; for (i = 0; i < 3; i++) { printf("%d ", *arr[i]); //printf("%d ", **(arr + i)); }

整型指针数组,数组中每一个元素都是整型变量的地址,因指针变量大小固定是4byte(32位)或8byte(64位),数组大小仅由数组元素个数决定。
C语言基础|【C语言入门必看】指针
文章图片

初阶指针到此结束,后续再深入了解指针。

    推荐阅读