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

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

🌇前言


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

37812580a56ae9c0b80831439bc2558.png



🌇正文


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


注意:本文中所有测试用题都是在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,不影响理解。

4673fab82bd1f8024e620559d71e85e.png



题2:


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

4ef61739e435d49f4a31cc857efc4dc.png



题3:


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

dcb86456b0af541aadebf4672540e63.png



题4:


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

00483991edbe3e3b8dc22b8cb5a841a.png



题5:


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

3924cea409a975ec3ea077ec816daa4.png



题6:

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


860694afbd17d2e475c5e1bf33d6739.png


题7:

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


4c074d48f0a2deaf376e72d98feab27.png


题8:

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


1d74b1ead8327dc204a18c5f20dbac9.png


题9:


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

ecfbf63b9ef4ff060eca5c41c195206.png



题10:


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

87451decad17bf0e84df0b6adb958fa.png



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


🌆字符数组(题组二)


经过前面的热身后我们可以挑战更难的题目了,题组二中涉及到了 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字节。

b6462ac1ac830adf391a07ae5d329ac.png



题2:


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


c6f3ebbe2127af4672334fe8b78f609.png


题3:


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


a4821379860618faeac9762837bcd35.png


题4:


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

bf44e0830cd0d251141d0442655180f.png



题5:

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


6bf51969cc17293baf01909d8a09c0f.png


题6:


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

009631d33271974f17e8c4dfbd55e5a.png



题7:


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

dcb4b6a37c16f3d730583c26b417cec.png



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


题8:


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

f257fa3c1fecec55c2f69007383a3bc.png



题9:

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


1a6299d7d25e123bbddfdb8a6662a79.png


题10:


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


f978cc4931142fe0e264790f0b5836b.png


题11:

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


bb321b75ae729ec2d9dc89f8c7bacec.png


题12:

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

d7713fc7d46d0d48250de8591bc7a59.png



题13:


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

785940b8285733eff56ce4ebe7405e0.png

题14:

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

582442b8f6cec0d43c7cc08574273b9.png

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


🌆字符串数组(题组三)


在题组二中我们接触了存储在数组中的多字符(无'\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。

51b9d36c75b4ee4b921603547a05791.png



题2:


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


0773fda647a06bf421bd52d6ca962ff.png


题3:

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


b11523376bff58687688142a375cdee.png


题4:

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


6eee04c9a898b193d92752a587a996c.png


题5:

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


c4cb5af1120c45c78a845cca38fec90.png


题6:


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


7331767d8e06300d5020a10f722c3df.png


题7:


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

e479bff5e3b8fc4eb03744bf8bc4557.png



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


题8:


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


62680ae28ffc6854dee04a1adb59643.png


题9:


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

c64871c4b59354e48e59a433505c95c.png



题10:


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

ccd996cde96501a86b2398f499b13bf.png



题11:


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


09a5759a014b2bb31f97c1be085b5e5.png


题12:


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

8acffb825f9091895d6c6ee2784a629.png



题13:


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

3104ba20eaf9acdec93594314636b8f.png



题14:


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


467813158c3d857f019c120847c204c.png


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


目录
相关文章
|
2月前
|
C语言
【c语言】指针就该这么学(1)
本文详细介绍了C语言中的指针概念及其基本操作。首先通过生活中的例子解释了指针的概念,即内存地址。接着,文章逐步讲解了指针变量的定义、取地址操作符`&`、解引用操作符`*`、指针变量的大小以及不同类型的指针变量的意义。此外,还介绍了`const`修饰符在指针中的应用,指针的运算(包括指针加减整数、指针相减和指针的大小比较),以及野指针的概念和如何规避野指针。最后,通过具体的代码示例帮助读者更好地理解和掌握指针的使用方法。
61 0
|
24天前
|
存储 NoSQL 编译器
【C语言】指针的神秘探险:从入门到精通的奇幻之旅 !
指针是一个变量,它存储另一个变量的内存地址。换句话说,指针“指向”存储在内存中的某个数据。
77 3
【C语言】指针的神秘探险:从入门到精通的奇幻之旅 !
|
24天前
|
存储 编译器 C语言
【C语言】指针大小知多少 ?一场探寻C语言深处的冒险 !
在C语言中,指针的大小(即指针变量占用的内存大小)是由计算机的体系结构(例如32位还是64位)和编译器决定的。
48 9
|
24天前
|
安全 程序员 C语言
【C语言】指针的爱恨纠葛:常量指针vs指向常量的指针
在C语言中,“常量指针”和“指向常量的指针”是两个重要的指针概念。它们在控制指针的行为和数据的可修改性方面发挥着关键作用。理解这两个概念有助于编写更安全、有效的代码。本文将深入探讨这两个概念,包括定义、语法、实际应用、复杂示例、最佳实践以及常见问题。
42 7
|
1月前
|
存储 C语言
C语言如何使用结构体和指针来操作动态分配的内存
在C语言中,通过定义结构体并使用指向该结构体的指针,可以对动态分配的内存进行操作。首先利用 `malloc` 或 `calloc` 分配内存,然后通过指针访问和修改结构体成员,最后用 `free` 释放内存,实现资源的有效管理。
120 13
|
27天前
|
存储 程序员 编译器
C 语言数组与指针的深度剖析与应用
在C语言中,数组与指针是核心概念,二者既独立又紧密相连。数组是在连续内存中存储相同类型数据的结构,而指针则存储内存地址,二者结合可在数据处理、函数传参等方面发挥巨大作用。掌握它们的特性和关系,对于优化程序性能、灵活处理数据结构至关重要。
|
27天前
|
算法 C语言
C语言中的文件操作技巧,涵盖文件的打开与关闭、读取与写入、文件指针移动及注意事项
本文深入讲解了C语言中的文件操作技巧,涵盖文件的打开与关闭、读取与写入、文件指针移动及注意事项,通过实例演示了文件操作的基本流程,帮助读者掌握这一重要技能,提升程序开发能力。
99 3
|
28天前
|
存储 算法 程序员
C 语言指针详解 —— 内存操控的魔法棒
《C 语言指针详解》深入浅出地讲解了指针的概念、使用方法及其在内存操作中的重要作用,被誉为程序员手中的“内存操控魔法棒”。本书适合C语言初学者及希望深化理解指针机制的开发者阅读。
|
1月前
|
存储 C语言 开发者
C 语言指针与内存管理
C语言中的指针与内存管理是编程的核心概念。指针用于存储变量的内存地址,实现数据的间接访问和操作;内存管理涉及动态分配(如malloc、free函数)和释放内存,确保程序高效运行并避免内存泄漏。掌握这两者对于编写高质量的C语言程序至关重要。
58 11
|
27天前
|
程序员 C语言
C语言中的指针既强大又具挑战性,它像一把钥匙,开启程序世界的隐秘之门
C语言中的指针既强大又具挑战性,它像一把钥匙,开启程序世界的隐秘之门。本文深入探讨了指针的基本概念、声明方式、动态内存分配、函数参数传递、指针运算及与数组和函数的关系,强调了正确使用指针的重要性,并鼓励读者通过实践掌握这一关键技能。
38 1