🌆指针(题组四)
在本套题组中,我们将真正从指针的角度出发,通过 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; }
在这关中我们遇到了结构体指针,首先我们有一个结构体,大小为 4*1+4*1+2*1+2*1+2*4 =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、指针指向与解引用等知识点,难度也是逐级递进,坚持做到每道题都有配图和讲解(希望这样能让大家得到更好的理解),正因为如此,使得本文的篇幅偏长,如果有不对的地方,欢迎随时指出,我愿洗耳恭听。
如果你觉得本文写的还不错的话,期待留下一个小小的赞👍,你的支持是我分享的最大动力!
如果本文有不足或错误的地方,随时欢迎指出,我会在第一时间改正。