一. 前言

昨晚整理博客时突然发现指针还少了一篇没写,今天就顺便来补一补。上回书说到,emmm忘记了,没事,我们直接进入本期的内容:

本期我们带来了几道指针相关笔试题的解析,还算是相对比较轻松的。话不多说,让我们来看看吧

哦对了,如果对前两期有兴趣的话可以点击以下链接进行跳转:

【C语言】深度理解指针(上)
【C语言】深度理解指针(中)

二. 经典笔试题详解✨

1. 笔试题NO.1

//程序的结果是什么?int main(){int a[5] = { 1, 2, 3, 4, 5 };int* ptr = (int*)(&a + 1);printf("%d,%d", *(a + 1), *(ptr - 1));return 0;}

答案是2,5

  • 首先,在*(a+1)中,数组名a单独出现,代表首元素地址,则a+1为第二个元素地址,解引用后就是2。这相比大家都没有问题。

  • 关键在于第二个,&a是取出的是整个数组的地址,+1后指针就越过数组指向下一个位置。但是由于强转为整形指针赋给了ptr,因此ptr-1其实是移动了一个整形4个字节的大小而不是移动了整个数组的大小,因此(ptr-1)就回到了最后一个元素,解引用后即为5。

2. 笔试题NO.2

//程序的结果是什么?struct Test{int Num;char* pcName;short sDate;char cha[2];short sBa[4];}*p;//假设p 的值为0x100000。 如下表表达式的值分别为多少?int main(){printf("%p\n", p + 0x1);printf("%p\n", (unsigned long)p + 0x1);printf("%p\n", (unsigned int*)p + 0x1);return 0;}

答案是:
10000014
10000001
10000004

  • 首先第一步:通过计算可以得出Test结构体类型的大小为20个字节(不会求的可以查阅鄙人上期相关内容)。

  • 然后看第一个:p是一个结构体类型的指针,0x代表16进制,因此p+1就是指针向后移动了一个结构体类型20个字节。这里可能会有疑问,移动了20个字节,那不应该是10000020吗?这里需要注意的是,%p打印出来的地址是以16进制的方式来表示的,而20=16+4,用16进制来表示正好就是14,所以答案是10000014而不是10000020。

  • 然后第二个:将p强制类型转换为无符号长整形。虽说进行了强制,但也只是强转,并没有改变变量p在内存中存放的数据。由于p变成了无符号长整形,所以p+1就不再是指针的加减了,而是普通的算术加减,p+1的值就为10000001。

  • 第三个也很好办,将p强转为无符号整形指针,p+1就向后移动了4个字节,也就是一个无符号整形的大小,最后的值为10000004。

3. 笔试题NO.3

//在小端机器上,程序的结果是什么?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;}

答案是4和2000000。这道题还是挺有意思的,不急,听我慢慢道来。

  • 第一个答案应该没有疑问:&a取出的是整个数组的地址,+1后越过数组指向下一位置,强转成整形指针后赋给ptr1,而ptr1[-1]就相当于*(ptr-1)。发现了没有,与我们第一道题一模一样,最后的答案就是最后一个元素4。

  • 有意思的是第二个:a为数组名,代表首元素地址,将a强转为整形然后+1后再强转为整形指针赋给ptr2,与上一题同理,最终ptr2实际上只偏移了1个字节,即指向第一个元素的第二个字节处。那么第一个元素1的第二个字节到底是什么呢?这就要取决于多字节数据在内存中的存储方式了,分为以下两种:

大端模式(big endian):将数据的低字节保存到内存的高地址处
小端模式(little endian):将数据的低字节保存到内存的低地址处
注:一般我们日常使用的计算机都是以小端模式进行存储

那么,根据题目的要求,我们可以画出数组在小端机器上内存的存储情况(数据的表示均为16进制):

  • 由于ptr2是整形指针,解引用就向后访问4个字节依次取出00,00,00,02。而由于我们是小端模式,低地址为数据的低字节位,因此最终*ptr的值就为02000000,与答案相符。

4. 笔试题NO.4

//程序的结果是什么?#include int main(){int a[3][2] = { (0, 1), (2, 3), (4, 5) };int *p;p = a[0];printf( "%d", p[0]); return 0;}

答案是1。

  • 本题不难,就是要注意到数组初始化里面是小括号而不是花括号。初始化列表里面是三条逗号表达式,每个逗号表达式的值都是最后一个表达式的值,即(0,1)==1;(2,3)==3;(4,5)==5。因此整个数组其实就初始化了3个数,如下:

  • 由于p=a[0],因此p就表示第一个一维数组的数组名,因此p[0]就是这个一维数组的第一个元素1。

5. 笔试题NO.5

//程序的结果是什么?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;}

答案是FFFFFFFC和-4。本题最好的解决方式是 画图,通过画图我们可以很明显直观的看出答案:

  • 我们画出如上的示意图,其中&p[4][2]与&a[4][2]的位置如上图所示注意,p是个指向含4个整形元素的数组的指针,因此p每次+1都越过4个整形的空间。

  • 两个指针相减的结果就是两个指针之间的元素个数。我们看图发现&p[4][2]和&a[4][2]之间有4个元素,由于是低地址减去高地址,因此最后的结果就为-4,而-4用%p来打印就是FFFFFFFC(16进制)

6. 笔试题NO.6

//程序的结果是什么?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;}

答案是10和5。

  • &aa为取出的是数组的地址,+1后越过整个二维数组指向下一位置。然后将其强转为int*指针赋给ptr,此时由于ptr是整形指针,-1偏移一个整形的大小指向数组的最后一个元素,即10。

  • *(aa+1)等价于aa[1],是第二个一维数组的数组名,数组名代表首元素地址,因此ptr2-1就偏移了一个整形的大小指向上一个数组的最后一个元素,即5。

7. 笔试题NO.7

//程序的结果是什么?#include int main(){char* a[] = { "work","at","alibaba" };char** pa = a;pa++;printf("%s\n", *pa);return 0;}

答案是at

  • 字符串常量的字面值是其首元素的地址。因此a数组的元素实际上是三个char*类型的指针,每个char*指针指向对应的字符串,如下:

  • a是数组名,代表首元素地址,由于首元素是char*类型的指针,因此用二级指针pa接收。

  • pa是首元素地址,+1后即为第二个元素的地址,解引用后即为第二个元素。结合上图我们可以得出第二个元素就是字符串at的首元素地址,因此按照%s格式打印后结果为at。

8. 笔试题NO.8

//程序的结果是什么?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;}

答案是:
POINT
ER
ST
EW

  • 我的天,这是嘛呀,太吓人了吧这些表达式!不急,我们画个图先:

  • 根据题目我们画出了如上关系图。首先是第一个:cpp指向cp数组的第一个元素,++cpp即指向cp数组第二个元素,因此*++cpp值为c+2。而c+2又指向c[2],因此**++cpp最终就为c[2]。c[2]为字符串”POINT”的首元素地址,按照%s来打印即为POINT。

  • 然后是第二个:需要注意的是,前一条语句中cpp已经++自增一次了,自增后关系图如下:

  • 同理,++cpp即指向cp的第三个元素,解引用后即为c+1,而c+1指向c[1],因此再–后就为c[0],即*–*++cpp的结果为c[0],c[0]是字符串”ENTER”的首元素地址,+3后即指向字符E,用%s打印后结果为ER。

  • 接着是第三个:依旧要根据上条语句的自增自减语句对关系图进行更新:

  • cpp[-2]可以等价为*(cpp-2),即为c+3,再对其解引用后为c[3],c[3]为字符串”FIRST”的首元素地址,+3后就指向字符S,用%s打印后结果为ST。

  • 最后是第四个:由于上一步没有自增自减语句,因此关系图不变:

  • cpp[-1][-1]可以等价为*(*(cpp-1)-1)。cpp-1指向cp第二个元素,解引用后即为c+2。然后c+2指向c数组的第3个元素,-1并解引用后即为c数组的第2个元素c[1]。然后c[1]为字符串”NEW”的首元素地址,+1后即指向字符E,用%s打印后结果为EW。

三. 总结✈

做完这8道题目,不得不说,画图真的是一个非常好的解题方法。有些题目,尽管看似十分复杂,当我们把示意图画出来时,问题就迎刃而解,非常直观。因此 我们在日常分析题目时要擅于画图。
通过这三期的学习,相信我们对指针有了较深的理解,赶紧下去尝试尝试吧,可千万不要一看就会,一写就废


以上,就是本期的全部内容啦

制作不易,能否点个赞再走呢