0


C语言进阶——指针进阶试题讲解(万字长文详解)

🌇🌆🌃本文已收录至:C语言——梦想系列_Yohifo的博客-CSDN博客

更多知识尽在此专栏中!


文章内各颜色含义

  • 其他重点
  • 数组 a / arr
  • 数组内元素 / 指针指向元素
  • sizeof
  • 目标量
  • strlen
  • 指针p

🌇前言

指针,一块存储其他内存块地址的空间,不仅能监管别人的地址信息,还拥有属于自己的地址。在取地址操作符(&)与解引用操作符(*)的“双重折磨”下,很多人对指针望而生畏,常常会掉进不规范使用指针而引发错误的大坑中。本文旨在通过众多例子来带大家理解指针(主要包含sizeof、strlen和多道指针笔试题),本文篇幅可能较长,请系好安全带,跟我走!


🌇正文

相信大家对这么一句话还有印象吧:数组名就是首元素地址。既然数组名就是地址,而且我们对数组的认识也比较深刻,那么我们可以从数组开始,带大家逐步深入理解指针。

注意:本文中所有测试用题都是在x86环境下运行的,更换环境会造成结果差异。

🌆一维数组(题组一)

作为我们的热身题,题组一还是比较简单的,它不仅是一维数组,而且还是 int 型,因此只会包含 sizeof 的例题(strlen 需要 char型 数据),下面是源码,大家可以先试着想一下,然后与我后面的讲解作对比,或者直接到自己的电脑上跑一下看正确答案。

//题组一
//只包含整型的数组
#include<stdio.h>
int main()
{
    int a[] = { 1,2,3,4 };
    printf("%d\n", sizeof(a));//?
    printf("%d\n", sizeof(a + 0));//?
    printf("%d\n", sizeof(*a));//?
    printf("%d\n", sizeof(a + 1));//?
    printf("%d\n", sizeof(a[1]));//?
    printf("%d\n", sizeof(&a));//?
    printf("%d\n", sizeof(*&a));//?
    printf("%d\n", sizeof(&a + 1));//?
    printf("%d\n", sizeof(&a[0]));//?
    printf("%d\n", sizeof(&a[0] + 1));//?
    return 0;
}

🌃讲解

怎么样?是否能读懂程序,如果有不理解的地方也不用担心,下面跟我一起来看详解吧!

题1:

首先我们来看看整型数组 arr,数组大小未定义,但根据后面存放的元素来看,数组中存放了四个int 型的元素(1,2,3,4),而我们的 sizeof 是计算大小的一个操作符(关于sizeof),作为语法规定的操作符,它能计算出各种变量的大小,比如 sizeof(int) 就为4。而当我们把数组名放入 sizeof 中时,数组名就不再代表首元素地址,而是代表整个数组的大小。综上所述,我们的题1就是计算整个数组所占空间的大小(即元素数 * 类型大小),自然就是16字节了。

注:此处有错误,数组名为a,不影响理解。

题2:

只有纯数组名的情况下 sizeof 才会计算整个数组的大小,那么任何对数组名的操作都会影响这一定律,即使是a+0 ,此时数组名代表首元素地址,地址+0表示未偏移,因此还是代表首元素地址,地址就是指针,因此此时 sizeof 计算的其实是一个指针的大小,指针大小不受类型的影响,指针在32位平台(x86环境)下是4字节,而在64位平台(x64环境)下是8字节,因此这题结果为4或8,我的环境是x86,最终结果为4字节。

题3:

第三题就比较简单了,数组名就是首元素地址,对其进行解引用操作( * ),即 *a 可以得到它指向的元素值,当然此题是1,而1是一个 int 型数据,放在 sizeof 中其实就是求 int 的大小,显而易见为4字节。

题4:

第4题与第二题逻辑一致,不过第四题是 a+1 指向的是数组内第二个元素,本质上也是地址(指针),根据平台不同,大小也会不同,这里是x86环境,经过 sizeof(a+1) 的运算后,最终大小为4字节。

题5:

题5相对来说就比较熟悉了,经典的数组元素访问形式,数组名+下标,下标从0开始,这里访问到了数组中的第二个元素,就是2,跟前面题3一样,sizeof(a[1]) 也是计算一个整型元素的大小,毋庸置疑,大小为4字节。

题6:

题6是对数组名进行取地址操作,前面说过数组名本身就是一个指针(地址),对其进行取地址会取到数组名自身的地址,而地址就是指针,sizeof(&a) 就变得和题2、题4一样求指针的大小了,具体是4还是8得看平台,我们这里是4字节。

题7:
无论是 * 还是 & ,计算顺序都是从右到左,因此 &a 实际上就是先取出整个数组的地址,再对其进行解引用操作,经过解引用,a 为数组名,此时 sizeof(&a) 就相当于 sizeof(a) ,回到了我们的题1,属于特殊情况之一,计算出的是整个数组的大小,为 4 * 4 = 16字节。其实当出现 & 这种组合时,它俩就抵消了,直接把它们省去就行了,不过 & 不行,因为逻辑上讲不通。

题8:
在第8题中,我们先是取出来整个数组的地址,然后对其进行+1操作,这样就跳过了整个数组,本质上仍然是指针,虽然此时已经跳出了数组,但并没有构成越界行为,因为 sizeof 中的表达式不会进行运算,当它执行此条语句时,只会跑到对应位置看看是什么类型,并不会改变原指针的指向位置,更不会进行解引用,因此没有越界行为。sizeof(&a+1) 仍在计算指针类型的大小,在这里(x86环境下)是4字节。

题9:

下标引用操作符 [ ] 优先级非常高,当数组名与其结合并附有下标时,a[0] 就表示数组中的第一个元素 1,再对其进行 &,取出它的地址,指针就是地址,本题绕了个弯,最终计算的仍是指针的大小,因此 sizeof(&a[0]) 对指针进行求值,最终值为4字节。

题10:

本题组的最后一个题了,首先根据第9题的经验,可知 &a[0] 是第一个元素的地址,对其进行 +1操作,使其向后偏移4个字节(元素类型为 int , 指针类型为 int*)后指向第二个元素 2 ,虽然进行了偏移,但仍然无法改变它是一个地址(指针)的事实,因此 sizeof(&a[0] + 1) 也是在计算指针的大小,这里的结果是4字节 。

**以上就是对题组一的全部讲解,可以对着看看自己答对了几道题。 **

🌆字符数组(题组二)

经过前面的热身后我们可以挑战更难的题目了,题组二中涉及到了 sizeof 和 strlen ,数组类型也变为了 char 型,一样的,先给大家看看题目源码,可以试着自己做做。

//题组二
//含有多字符的数组
#include<stdio.h>
#include<string.h>
int main()
{
    char arr[] = { 'a','b','c','d','e','f' };
    printf("%d\n", sizeof(arr));//?
    printf("%d\n", sizeof(arr + 0));//?
    printf("%d\n", sizeof(*arr));//?
    printf("%d\n", sizeof(arr[1]));//?
    printf("%d\n", sizeof(&arr));//?
    printf("%d\n", sizeof(&arr + 1));//?
    printf("%d\n", sizeof(&arr[0] + 1));//?

    printf("%d\n", strlen(arr));//?
    printf("%d\n", strlen(arr + 0));//?
    printf("%d\n", strlen(*arr));//?
    printf("%d\n", strlen(arr[1]));//?
    printf("%d\n", strlen(&arr));//?
    printf("%d\n", strlen(&arr + 1));//?
    printf("%d\n", strlen(&arr[0] + 1));//?
    return 0;
}

🌃讲解

题组二中有 sizeof 操作符和 strlen 库函数,题目比较多,没关系,看我一个一个解析!

题1:

第一题是在 sizeof 中放入了数组名 arr,属于特殊情况之一,sizeof(arr) 计算的是整个数组所占空间的大小,即数组元素数 * 类型大小,可见数组内有6个元素(不是字符串,不包含'\0'),类型为char型(1字节),因此数组大小为 6 * 1 = 6字节。

题2:

第二题中的数组名 arr 发生了变化,虽然只是 +0 ,但它此时已经不属于特殊情况之一,而是代表数组首元素地址,既然是地址,sizeof(arr+0) 计算的实际上就是指针的大小,这里(x86环境,为32位平台)是4字节。

题3:

第三题,对数组名 arr 进行了解引用操作,同样的不再具备特殊情况的属性,而是数组中第一个元素的地址,经过解引用后,可以得到元素的具体值,即单个字符 'a' ,sizeof 根据类型计算大小,char 型是 1 个字节,因此 sizeof(*arr) 最终结果为1字节。

题4:

本题与上题基本一致,题3是通过指针进行解引用,而我们的题4是通过数组的形式得到元素具体值,下标为1,表示数组中的第二个元素,即字符 b,同样的大小也为1字节,经过 sizeof(arr[1]) 计算后,最终结果为1字节。

题5:
在第五题中,我们对数组名 arr 进行了取地址操作,取出了数组名的地址,本质上是一个指针,指向整个数组,sizeof(&arr) 求的就是一个指针的大小,具体为4字节(x86环境)。

题6:

这题跟上一题差不多,当我们对数组名取地址,得到整个数组的地址后,对其进行+1操作,会偏移到数组最后一个元素的后一块空间,sizeof 看到 &arr+1 后知道这是一个指针类型,大小为4字节。至于是否越界?答案是否,sizeof 中的表达式不进行运算,它只需要知道数据类型就行了,因此不会出现越界行为。

题7:

这是本套题组中的最后一道 sizeof 相关题,先看题目,首先 [ ] 的优先级很高,与数组名 arr 进行结合后表示数组中的第一个元素 a,在对其进行取地址,得到一个地址(此时的地址是首元素地址),对地址+1后指向第二个元素 b,同样也是一个指针,sizeof(&arr[0]+1) 计算的也是指针类型的大小,具体为4字节。

**题组二的 sizeof 部分结束了,下面来看看 strlen 部分: **

题8:

第八题是我们接触的第一道 strlen 函数题,strlen 是一个库函数,作用是计算字符串的长度,原理为指针不断向后偏移,直到遇到结束标志 '\0' ,然后返回统计到的字符个数(strlen 官方介绍),回顾题目,我们得到了一个存放6个字符的数组 arr (没有字符串结束标志),那么当 strlen 进行偏移统计时,无法找到结束标志,会一直往后找,直到找到结束标志 '\0' ,当然返回的长度肯定与原数组长度对不上,我们称这个返回数为随机数,当然现在编译器很聪明,会报一个警告。经过不断寻找后,结果为随机数。

题9:
在第九题中,我们对数组首元素地址指向了+0的操作(相当于没加),此时 strlen(arr+0) 统计字符串长度的效果和 strlen(arr) 完全一样,即两个题求得的随机数都一样,反正都没有结束标志,根据前题判断,此题结果为随机数。

题10:

第十题就比较特殊了,对数组名进行解引用,得到的是首元素 a,标准规定,在使用 strlen 时需要传入一个地址。而此时的 *arr 是一个具体元素值,类型不匹配,运行会报错,因此此题并不会求出运算结果。

题11:
上一题(*arr)是通过指针+解引用的方式访问元素 a,而这题(arr[1])是通过数组名 +下标的方式访问元素 b,两者都是访问元素,得到的是一个具体元素值,传给 strlen 进行运算同样会报错,无法得出结果!

题12:
让我们结束报错代码的学习,在第十二题中,取出来数组名的地址(&arr),此时 strlen 能正常接收并进行运算,而&arr 与arr 指向的地址一致,可以间接看成这题是题8的翻版,因此所得到的随机数也一致,应该就是19了(测试后发现不是,果然是随机数~),当然不同环境具体值有差异,但肯定和原数组长度对不上。

题13:

此题就是在题12的基础上+1,当然因为是 &arr+1,跳过的是整个数组,也就是说 &arr+1 从数组尾元素后一个位置开始,传递给 strlen ,本来一样的随机数,跳过了一整个数组(6个元素),因此第十三题得到的最终值是随机数 - 6。

题14:
本套题组的最后一题,一样的,arr[0] 得到的首元素 a,取出它的地址,在它的地址基础上+1,此时 &arr[0]+1 表示元素 b 的地址,也就是说,我们跳过了一个元素,这样一来,strlen 的最终值就是随机值 - 1,因为起点从 a 变成了 b,二者差1。

**至此我们的题组二已经全部讲解完毕了,相对于题组一,题目增加了很多,难度也是提升了一个阶梯,不过也都还好,相信在看完我的讲解后,能对这些题目有更深的理解。 **

🌆字符串数组(题组三)

在题组二中我们接触了存储在数组中的多字符(无'\0'),而在题组三中数组中的内容变成了字符串(有'\0'),既然多了一个结束标志 '\0' ,两组题的差异会有多大呢?下面是题目源码,一起来看看吧!

//题组三
//含有字符串的数组
 #include<stdio.h>
#include<string.h>
int main()
{
    char arr[] = "abcdef";
    printf("%d\n", sizeof(arr));//?
    printf("%d\n", sizeof(arr + 0));//?
    printf("%d\n", sizeof(*arr));//?
    printf("%d\n", sizeof(arr[1]));//?
    printf("%d\n", sizeof(&arr));//?
    printf("%d\n", sizeof(&arr + 1));//?
    printf("%d\n", sizeof(&arr[0] + 1));//?

    printf("%d\n", strlen(arr));//?
    printf("%d\n", strlen(arr + 0));//?
    printf("%d\n", strlen(*arr));//?
    printf("%d\n", strlen(arr[1]));//?
    printf("%d\n", strlen(&arr));//?
    printf("%d\n", strlen(&arr + 1));//?
    printf("%d\n", strlen(&arr[0] + 1));//?
    return 0;
}

🌃讲解

**现在看这些题是否已经有点感觉了?不至于像前面一样一头雾水,一起来看看各题讲解及答案吧 **

题1:
第一题一如既往的从数组名开始,记住,凡是在 sizeof 中单独出现的数组名,都不再代表数组首元素地址,而是代表整个元素数量,经过 sizeof 计算后得出整个数组所占空间的大小。当然因为此时数组中存放的是一个字符串,除了各字符外还额外存放了一个字符串结束标志 '\0' ,这也算字符,sizeof 在计算大小时会将此标志也算入其中,因此本题的最终大小为 (6+1) * 1 = 7 字节,因为数组类型为 char,所以需要乘上1。

题2:

第二题中对数组名 arr 执行了操作,此时数组名 arr 已不再单独存放于 sizeof 中,因此数组名 arr 就是首元素 a 的地址,对其地址执行+0操作,相当于没有偏移还是指向首元素 a,此时 arr+0 ,就是一个地址,指针就是地址,sizeof(arr+0) 在计算时相当于是在计算一个指针大小,指针不论类型,都是4字节(x86环境),因此此题的答案为4字节。

题3:
对数组名 arr 解引用后,得到的数组中首元素的具体值 a,为 char 型元素,因此 sizeof(*arr) 相当于在计算一个字符型数据的大小,毋庸置疑,大小为1字节。

题4:
数组名 arr 与 [ ] 结合,再通过下标1访问到数组第二个元素 b,与题3一样,arr[1] 得到的也是数组中元素的具体值 b,sizeof(arr[1]) 实际计算的就是 char 类型的大小,为1字节。

题5:
第五题中对数组名 arr 执行了取地址的操作,属于另一种特殊情况:&数组名,数组名不再是首元素地址,取出的是整个数组的地址,进行操作时,会跳过整个数组。此时我们得到的是整个数组的地址(虽然和首元素地址一样,但本质上不同),既然我们此时得到的是一个地址,而且是放入sizeof 中的地址,那么 sizeof(&arr) 计算的就是指针大小,在当前平台(x86) 下为4字节。

题6:

此题目标量是在题5的基础上+1,前面说过 &arr 取出的是整个数组的地址,对其进行+1,会跳过整个数组(包括结束标志 '\0' ),虽然指向了越出数组的空间,但并未构成越界,因为 sizeof 中的表达式不会真正运算,只是去看一下是什么类型的数据,很显然此时是一个指针类型的数据,因此大小为4字节(当前为x86环境)。

题7:

第七题,首先我们获得数组首元素值 a,再对其进行取地址操作,此时的地址不代表整个数组的地址,因此+1只会跳到下一个元素 b 的地址处。所以说 &arr[0]+1 仍是一个地址,sizeof 计算时会当作指针处理,最终结果为4字节(x86环境下)。

**下面来看看 strlen 关于字符串的运算吧! **

题8:

strlen 得配合字符串使用,因为字符串中自带结束标志,所以这部分就和 strlen 很配对,不过如果传的不是地址,同样会报错。下面来看看这题,传递的是数组名 arr,因为没在 sizeof 中,因此此时相当于是数组首元素地址,strlen 会根据这个地址逐个往后比对,直到遇到结束标志,显然在此数组中元素个数是6,最终结果也正是6。

题9:

题9是在上一题的基础上进行操作,不过因为 arr+0 相较于 arr 无任何偏移,因此这两个地址的指向空间都一样,都是首元素 a,当然因为指向一样,strlen(arr+0) 在计算时起点也一样,因此最终结果是一样的,都是6。

题10:

这里对数组名 arr 执行了解引用操作,导致此时 *arr 为数组首元素具体值 a ,把一个具体值传递给 strlen 是不可行的,且 a 的ASCII码为97,当97被当作地址传递给 strlen 时,所代表的是为操作系统分配的内存地址,普通用户访问会报错,所以此题没有结果,运行时会报错。

题11:

第十题是以数组的形式访问元素,arr[1] 表示数组中的第二个元素 b,当 b 被转化为98传递给 strlen 时,运行同样会报错,一样的得不到结果。

题12:

题11中取出了数组名 arr 的地址,此时地址仍与首元素地址一致,因为传递的是一个地址,交给 strlen 处理时合法,又因为和首元素地址一致,strlen 的起点和题8、题9一致,综上 strlen(&arr) 所得出的长度与它们一致,都是6。

题13:

在这题中,我们对 &arr 进行+1操作,因为 &arr 取出的是整个数组的地址,移动步长为整个数组,&arr+1会跳过整个数组,指向整个数组尾元素的后一块空间,从这块空间开始往后比对,因为谁也不知道什么时候能遇到结束标志 \0 ,所以 strlen(&arr+1) 的结果为随机值。

题14:

本套题组的最后一题,首先 arr[0] 代表首元素 a,取出它的地址,再执行+1操作,跳过一个元素,最终 &arr[0]+1 指向元素 b,传给 strlen 进行计算,此时起点为元素 b,相较于题8少了一个元素,因此本题的计算值为 6 - 1 = 5。

以上就是题组三的全部讲解,主要就是考察字符串,理解了也很简单。

🌆指针(题组四)

在本套题组中,我们将真正从指针的角度出发,通过 sizeof 和 strlen 的不断磨练来拿捏指针,因为指针指向的存储空间是连续的(类似于数组的存储方式),所以本套题组中也有很多知识点与上面重复,先来看看源码吧!

//题组四
//指针登场
 #include<stdio.h>
#include<string.h>
int main()
{
    char* p = "abcdef";
    printf("%d\n", sizeof(p));//?
    printf("%d\n", sizeof(p + 1));//?
    printf("%d\n", sizeof(*p));//?
    printf("%d\n", sizeof(p[0]));//?
    printf("%d\n", sizeof(&p));//?
    printf("%d\n", sizeof(&p + 1));//?
    printf("%d\n", sizeof(&p[0] + 1));//?

    printf("%d\n", strlen(p));//?
    printf("%d\n", strlen(p + 1));//?
    printf("%d\n", strlen(*p));//?
    printf("%d\n", strlen(p[0]));//?
    printf("%d\n", strlen(&p));//?
    printf("%d\n", strlen(&p + 1));//?
    printf("%d\n", strlen(&p[0] + 1));//?
    return 0;
}

🌃讲解

涉及指针的题多多少少带有些难度,配上其在内存中的布局情况图会更好理解。

题1:

在本套题组中,我们得到的是一个指向字符串的字符型指针 p ,因为字符串在内存中是连续存储的,可以通过首字符地址打印出整个数组。当一级指针 p 指向字符串时,就是指向字符串中的首字符地址(此时的指针 p 有点像数组名),当将指针 p 放入 sizeof 中时,计算的是一个指针所占空间的大小(p 并不是数组名),指针大小在这里(x86环境)是4字节。

题2:
第二题中,将字符串型指针 p 向后移动了一位,使其指向字符 b,此时 p+1 依旧是一个指针,sizeof 计算的仍然是指针的大小,结果为4字节。

题3:

第三题中对字符指针 p 进行解引用,因为此时 p 指向字符 a,对其解引用后就能得到字符 a,因为a 是一个字符型数据,sizeof(*p) 计算的就是一个字符串型数据的大小, 结果为1字节。

题4:

前面说过,在数组 arr 中,*arr 与 arr[0] 效果一样,都是访问数组首元素,这也从侧面说明了指针在访问元素时有两种方式,这里的 p[0] 跟 *p 一样,也能访问到字符 a,sizeof 此时也是在计算 char 型数据的大小,即1字节。

题5:

p 作为一个字符型指针,对其进行取地址操作后,能得到指针 p 的地址,此时相当于一个二级指针,只要是指针类型,在传给 sizeof 计算后,结果都为4字节(x86环境下)。

题6:

对字符指针 p 的地址+1,指向指针 p 的后一块空间,既然 &p+1 指向其他空间,sizeof 在计算时认为这是一个指针类型变量,因此结果为4字节(x86环境)。

题7:
第七题中的 p[0] 表示字符 a,对其进行取地址操作,取出字符 a 的地址(相当于 p),再执行+1操作,此时 &p[0]+1 指向字符 b ,为指针类型的数据,sizeof 计算值为4字节。

关于指针的 sizeof 计算题到此就结束了,下面来看看指针关于 strlen 的运算题。

题8:

前面说过字符串在内存中也是连续存储的,同时自带一个结束标志 \0 ,将指向字符串的指针 p 传入 strlen 中计算,符合传地址的规定,同时也有结束标志,显然 strlen(p) 结果为6。

题9:

字符指针 p 指向首字符 a,对 p+1 后,指向第二个字符 b,此时将strlen(p+1) 计算时的起点不是从字符 a 开始,而且从字符 b 开始,这样一来,计算出的长度就会-1,最终值变为5。

题10:

此题对字符指针 p 进行了解引用操作,得到了首字符 a 的具体值,将 a(97) 传给 strlen 时,strlen(a) 会去访问不能访问的空间(这片空间属于操作系统),运行会报错,此题不会出结果。

题11:
跟上一题差不多,p[0] 得到的也是首字符 a,同样会把 a 的ASCII值传给 strlen,strlen 会依据这个地址去访问空间,但是肯定会访问错误,毕竟操作系统不是谁都能访问。

题12:

对指向字符串的字符指针 p 进行取地址操作,得到 p 的地址,将其传给 strlen ,strlen(&p) 会从指针 p 处开始向后比对,因为谁也不知道后面是否有结束标志,因此结果为一个随机数。

题13:
十三题中对 p 的地址执行了+1操作,使其向后移动4字节(因为 &p 指向一个 p,+1移动一个指针p),当 strlen(&p+1) 开始运算时,起始地址和上一题已经不一样了,最终得到的随机值也不一样。

题14:

目标值我们都已经很了解了,先是取出首字符 a 的地址,对它的地址+1,指向下一个字符 b,当 strlen 从此处开始往后比对时,最终长度会-1,跟题8的思路一致,结果都是5。

本套题组已经全部讲解完毕,其中有些题目涉及到了二级指针,如果不画图的话,是很难理清思路的,所以在处理类似题目时,一定要画图理解!

🌆二维数组(题组五)

这是最后一套题组,为二维数组,其中也会涉及到二级指针、数组指针等概念,此套题组中的二维数组为 int 型,因此只会涉及到 sizeof ,下面来看看源码吧!

//二维数组
#include<stdio.h>
int main()
{
    int arr[3][4] = { 0 };
    printf("%d\n", sizeof(arr));//?
    printf("%d\n", sizeof(arr[0][0]));//?
    printf("%d\n", sizeof(arr[0]));//?
    printf("%d\n", sizeof(arr[0] + 1));//?
    printf("%d\n", sizeof(*(arr[0] + 1)));//?
    printf("%d\n", sizeof(arr + 1));//?
    printf("%d\n", sizeof(*(arr + 1)));//?
    printf("%d\n", sizeof(&arr[0] + 1));//?
    printf("%d\n", sizeof(*(&arr[0] + 1)));//?
    printf("%d\n", sizeof(*arr));//?
    printf("%d\n", sizeof(arr[3]));//?
    return 0;
}

🌃讲解

二维数组中有行和列,对于单行的计算需要仔细思考,对于单行首地址+1的操作也要慎重。

题1:

在第一题中,往 sizeof 中放入了一个数组名 a,此时数组名代表整个数组,sizeof(a) 计算时需要计算元素数 * 类型的值,也就是 12 * 4 = 48,最终结果为48字节。

题2:

数组名 arr 与[0] 结合,访问第一行,arr[0] 再与 [0] 结合,访问第一行中的第一个元素,此元素为 int 型,sizeof(arr[0][0]) 计算结果为4字节。

题3:

前面说过,数组名 arr 与 [0] 结合,访问到的是二维数组中的第一行,其中 arr[0] 为第一行元素的首地址(数组名是首元素地址,在 sizeof 中代表整个数组的大小),此时 arr[0] 可以看作这个数组(将这个二维数组看作三个数组的结合体,一个数组中放四个元素)的数组名,放在 sizeof 中表示整行数组的大小,因此 sizeof(arr[0]) 计算的就是一整行数组所占空间大小,为元素数 * 类型,即 4 * 4 = 16字节。

题4:

arr[0] 表示二维数组中首行数组的首地址,+1后会跳过一个元素,指向首行数组的第二个元素,此时 arr[0]+1 是一个地址,地址就是指针,sizeof(arr[0]+1) 计算的就是一个指针类型的大小,这里是4字节(x86)。

题5:

第五题实际上就是对上一题的地址进行了解引用操作,访问到首行二元素的具体值,为一个 int 型数据,sizeof(*(arr[0]+1)) 结果4字节。

题6:

数组名 arr 代表首行元素的首元素地址,对此地址+1,会跳过整行元素,此时 arr+1 是一个指向第二行元素首地址的二级指针,sizeof(arr+1) 对地址进行计算,相当于在计算一个指针类型的大小,结果为4字节(x86环境)。

题7:
从上图中可以看出,arr+1 是一个 int* 的数据,对其进行解引用后,得到一个指向第二行首元素的地址,也就是第二行元素的首地址,单独放在 sizeof 中,代表整个第二行元素的大小,sizeof(*(arr+1)) 计算的就是整个第二行元素所占空间的大小,为元素数 * 类型大小,即4 * 4 = 16字节。

题8:

arr[0] 为首行首元素地址,取地址后得到指向第二行首元素指针的指针(int型),对此二级指针执行+1操作,使其跳过一个int数据,指向 arr[0] 的下一块空间,因为 sizeof 内部表达式不会进行运算,所以没有越界。并且 sizeof 只需要知道类型就能计算出大小,显然这里是一个指针类型,大小为4字节。

题9:

前面说过,&arr[0]+1 是一个指向第二行元素首地址的二级指针,类型为 int* ,解引用后得到第二行首元素的地址,相当于第二行元素的数组名,放在 sizeof 中代表整行元素的长度(大小),经过 sizeof 计算所占空间大小后,结果为 4 * 4 = 16字节。

题10:

在二维数组中,*arr 与 arr[0] 所代表的含义一致,因此此题实际上就是在求首行元素所占空间的大小,sizeof(*arr) 就为 4 * 4 = 16字节。

题11:

arr[3] 并不在二维数组中,sizeof 在计算时不会去操作那块空间,它只需要知道那片空间的类型就能计算出所占空间的大小,虽然 arr[3] 已经越出了数组,但是编译器在开辟空间时,会多往后面延申空间,此时 sizeof 会根据前面的数据判断 arr[3] 的类型,这里为指针(第三行数组名),计算结果和前面一样,都是16字节。

🌆小结

至此 sizeof 和 strlen 有关指针的相关题目已经全部讲解完毕了(目前全篇字数已经达到了1w2)

数组名的意义:

  • 1.sizeof(数组名),数组名为整个数组的大小(即元素数量)。
  • 2.&数组名,数组名表示整个数组的地址,对其操作是以整个数组为单位。
  • 3.除以上两种情况外,数组名都是数组首元素地址。

指针的意义:

  • 1.无论类型,指针大小在x86环境下都为4字节,在x64环境下为8字节。
  • 2.strlen 根据指针进行计算时,如果没有结束标志,则输出为随机值。
  • 3.&一级指针,并对其进行+1操作,会跳过整个一级指针。
  • 4.在二维数组中,指向某行元素的一级指针,可以看某行元素的首地址。
  • 5.二维数组的后继空间会依据前面数组的特征进行分配,比如题组五中的题11。


🌆八大指针笔试题

** 我们已经走出指针新手村了,下面可以去打怪升级了。目前有八大恶魔阻挡我们前行,要想获得真正的指针之力,需要把它们全部解决掉,话不多说,让我们直接开始闯关!**

🌃第一关

//笔试题一
int main()
{
    int a[5] = { 1, 2, 3, 4, 5 };
    int* ptr = (int*)(&a + 1);
    printf("%d,%d", *(a + 1), *(ptr - 1));
    return 0;
}

在第一关中,我们得到了一个大小为5的整型数组a,取出整个数组的地址,对其加1,此时 &a+1 指向数组尾元素的下一块空间,强制类型转化为 int* 并将其赋给 ptr。单独数组名+1,指向的是第二个元素,解引用后得到元素的具体值 2;对 ptr 减一后其指向第五个元素,解引用后得到其具体值 5,因此最终打印结果为 2,5。

🌃第二关

//笔试题二
struct Test
{
    int Num;
    char* pcName;
    short sDate;
    char cha[2];
    short sBa[4];
}*p;
int main()
{
    printf("%p\n", p + 0x1);
    printf("%p\n", (unsigned long)p + 0x1);
    printf("%p\n", (unsigned int*)p + 0x1);
    return 0;
}

在这关中我们遇到了结构体指针,首先我们有一个结构体,大小为 41+41+21+21+24 =20字节,并创建了一个结构体指针 p (默认为0)指向此结构体,%p 是按十六进制型式打印地址,0x1 相当于1,对指针 p+1 会跳过整个结构体,即加20字节;将结构体指针强制类型转换为 unsigned long 后,相当于一个普通整型数据,+1就是单纯的加1字节;p 强制类型转换为 unsigned int 后,对其+1表示增加4字节,因为此时是整型指针,步长为4字节。 又因为十六进制中,20表现为14,因此最终打印结果为 00000014、00000001、00000004。

🌃第三关

//笔试题三
int main()
{
    int a[4] = { 1, 2, 3, 4 };
    int* ptr1 = (int*)(&a + 1);
    int* ptr2 = (int*)((int)a + 1);
    printf("%x,%x", ptr1[-1], *ptr2);
    return 0;
}

第三关与第一关有些许相似,同样的,&a+1 指向尾元素下一块空间,强制类型转换后赋给指针 ptr1,因为 a 为数组名,是首元素地址,是一个十六进制数,强制类型转换为整型后+1,只会取到十六进制中的前两位数,因为是小端存储,所以 int(a) 为1,int(a)+1 为2,2 在内存中存储为 02000000,再将其强制类型转换为 int* ,此时 ptr2 中存储的值就是 02000000。输出时,%x 表示按十六进制打印,与 %p 不同的是 %x 打印时会省略前面的 0,综上所述,最终打印结果为 4,2000000。

🌃第四关

//笔试题四
#include <stdio.h>
int main()
{
    int a[3][2] = { (0, 1), (2, 3), (4, 5) };
    int* p;
    p = a[0];
    printf("%d", p[0]);
    return 0;
}

本关比较简单,就是有个不明显的坑,( )内部的是逗号表达式,比如 (0,1) 实际上为1,这样一来二维数组 a 中的存储情况就变成了 { 1, 3, 5 }, 将首行数组名赋给整型指针 p,p[0] 相当于 *p ,也就是 *(a[0]) -> ((a+0)) -> a[0][0],这关就变成了打印元素 a[0][0] ,也就是元素 1。

🌃第五关

//笔试题五
int main()
{
    int a[5][5];
    int(*p)[4];
    p = a;
    printf("%p,%d\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]);
    return 0;
}

在这关中,int(*p)[4] 是一个数组指针,可以用来存放二维数组,其中 [4] 表示列,因为这里的二维数组 a[5][5] 中有五列数据,强行放入 p 中会导致位置错位,比如 p[1][0] 相当于 a[0][4] ,因此两个相减会计算出中间的元素数。

🌃第六关

//笔试题六
int main()
{
    int aa[2][5] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
    int* ptr1 = (int*)(&aa + 1);
    int* ptr2 = (int*)(*(aa + 1));
    printf("%d,%d", *(ptr1 - 1), *(ptr2 - 1));
    return 0;
}

&数组名,取出的是整个二维数组的地址,+1的移动步长是整个二维数组,此时 ptr1 指向二维数组尾元素的下一块空间;aa 表示第一行数组的数组名,+1的移动步长是第一行数组,因为 aa+1 是int* 型数据,解引用后才能正确指向第二行数组首地址,将其赋给 ptr2。ptr1 - 1 指向尾元素,解引用后得到 10;ptr2 - 1 指向第一行中的尾元素,解引用后得到 5,结果为 10,5。

🌃第七关

//笔试题七
#include <stdio.h>
int main()
{
    char* a[] = { "work","at","alibaba" };
    char** pa = a;
    pa++;
    printf("%s\n", *pa);
    return 0;
}

这关是来自阿里巴巴的面试题,work at alibaba,首先 char* a[ ] 是一个指针数组,是一个存放指针的数组,数组名 a 指向首元素地址,也就是 "work" 中 w 的地址,因为字符串的首地址可以看作指针。创建一个二级指针 pa 指向 a,对 pa++ ,使其跳过一个 a,即由原来的指向 "work" 变为指向 "at" ,对 pa 解引用后打印,就是打印字符串 "at" 。

🌃第八关

//笔试题八
int main()
{
    char* c[] = { "ENTER","NEW","POINT","FIRST" };
    char** cp[] = { c + 3,c + 2,c + 1,c };
    char*** cpp = cp;
    printf("%s\n", **++cpp);
    printf("%s\n", *-- * ++cpp + 3);
    printf("%s\n", *cpp[-2] + 3);
    printf("%s\n", cpp[-1][-1] + 1);
    return 0;
}

作为我们的大Boss,这关的难度可以说是空前绝后,没有两把刷子还理解不了这题。首先题目给了一级指针数组 c,在其中存放了四个字符串,然后有一个二级指针数组 cp ,其中存放了一级指针数组中的各字符串,最后是一个三级指针 cpp,指向二级指针数组 cp ,好了,准备工作已经做完了,下面可以开始做题了。

** 好了,Boss已经打完了,现在你已经学会指针了,可以去各种题目中大杀四方了,指针是把双刃剑,使用需要谨慎,要提防野指针的出现(关于指针的用法)。**


🌇总结

** 历时十余个小时,字数累计15k+,这篇关于指针进阶试题的讲解文章终于算是划上了一个句号。回顾全文,我们从最简单的整型一维数组开始,到三级指针结束,中间穿插了 sizeof、strlen、指针指向与解引用等知识点,难度也是逐级递进,坚持做到每道题都有配图和讲解(希望这样能让大家得到更好的理解),正因为如此,使得本文的篇幅偏长,如果有不对的地方,欢迎随时指出,我愿洗耳恭听。**

** 如果你觉得本文写的还不错的话,期待留下一个小小的赞👍,你的支持是我分享的最大动力!**

** 如果本文有不足或错误的地方,随时欢迎指出,我会在第一时间改正。**

相关文章推荐

C语言初阶——指针_Yohifo的博客-CSDN博客

**C语言进阶——指针进阶_Yohifo的博客-CSDN博客 **

C语言初阶——数组_Yohifo的博客-CSDN博客

标签: c++ c语言 学习

本文转载自: https://blog.csdn.net/weixin_61437787/article/details/126738117
版权归原作者 Yohifo 所有, 如有侵权,请联系我们删除。

“C语言进阶&mdash;&mdash;指针进阶试题讲解(万字长文详解)”的评论:

还没有评论