🌇前言
指针,一块存储其他内存块地址的空间,不仅能监管别人的地址信息,还拥有属于自己的地址。在取地址操作符(&)与解引用操作符(*)的“双重折磨”下,很多人对指针望而生畏,常常会掉进不规范使用指针而引发错误的大坑中。本文旨在通过众多例子来带大家理解指针(主要包含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。
以上就是题组三的全部讲解,主要就是考察字符串,理解了也很简单。