文章目录

  • ▶️1.预备知识:
    • ▶️1.1二维数组是怎么存储的?
    • ▶️1.1 总结
  • ▶️2.二维数组和指针相关笔试题
    • ▶️2.1二维数组和指针相关笔试题讲解
    • ▶️2.2 VS运行结果演示
    • ▶️2.3总结
  • ▶️3.指针运算笔试题解析
    • ▶️3.1题目1:
    • ▶️3.1.1 题目1讲解:
    • ▶️3.1.2 vs测试结果:
    • ▶️3.2 题目2:
    • ▶️3.2.1 题目2讲解:
    • ▶️3.2.2 vs测试结果:
    • ▶️3.3 题目3:
    • ▶️3.3.1 题目3讲解:
    • ▶️3.3.2 vs测试结果:
    • ▶️3.4 题目4:
    • ▶️3.4.1 题目4讲解:
    • ▶️3.4.2 vs测试结果:
      • ▶️3.4.2 VS警告:
    • ▶️3.5 题目5:
    • ▶️3.5.1 题目5讲解:
    • ▶️3.5.2 vs测试结果:
    • ▶️3.6 题目6:
    • ▶️3.6.1 题目6讲解:
    • ▶️3.6.2 vs测试结果:
    • ▶️3.7 题目7:
    • ▶️3.7.1 题目7讲解:
    • ▶️3.7.2 vs测试结果:
  • ▶️4.总结

Hello,大家好呀!今天我们继续来讲解C语言指针相关笔试题!!!
在讲解之前,让来我们来回顾一下上次博客:C语言-数组&&指针笔试题讲解(1)-干货满满!!!
讲了什么来吧:
1.

strlen和sizeof的对比:
1.sizeof是操作符。strlen是库函数,需要包含头文件string.h

2.sizeof是计算操作数所占内存的大小,单位是字节。strlen是求字符串长度的,统计的是\0之前字符的隔个数。

3.sizeof操作符是不关心内存中存放什么数据,而strlen函数是关注内存中是否有\0,如果没有\0,就会持续往后找,可能会越界。

2. 整型数组和字符数组和字符指针相关笔试题举例分析和讲解。



那今天博主会把剩下的二维数组和指针笔试题,以及指针运算相关笔试题全面进行讲解。
讲的内容如下图所示:


▶️1.预备知识:

在讲解二维数组和指针相关笔试题之前,我们先给大家讲一下二维数组在内存中的存储知识~

▶️1.1二维数组是怎么存储的?

其实它像一维数组一样,我们如果想研究二维数组在内存中的存储方式,是可以打印数组中所有元素。

代码如下:

#include int main() {int a[3][5] = { 0 };for (int i = 0; i < 3; i++) {for (int j = 0; j < 5; j++) {printf("a[%d][%d]=%p\n",i,j, &a[i][j]);}}return 0;}

输出的结果:

分析代码: 从输出的结果来看,二维数组中每一行内部的每个元素都是相邻的,地址之间相差4个字节,跨行位置的两个元素 (如:arr[0][4]和arr[1][0]) 之间也是差4个字节。
因此我们可以得出以下结论: 二维数组中的每个元素都是连续存放的。

另外,我们曾经在:C语言-指针讲解(3)
讲过:二维数组的数组起始可以看作是一维数组的数组,也就是二维数组的每个元素是一个一维数组。那么二维数组的首元素就是第一行,是个一维数组。
如下图所示:

分析: 我们根据上图,可以知道第一行一维数组的类型就是int[5],所以第一行的地址就是数组指针类型int(*)[5]



▶️1.1 总结

二维数组中的每个元素都是连续存放的。

好了,当我们介绍了这些前置知识后,那我们就开始讲解一下二位数组和指针相关的笔试题吧~

▶️2.二维数组和指针相关笔试题

接下来,我们来看一下二维数组和指针相关的笔试题~
题目如下:

//二维数组相关题目#includeint main() {int a[3][4] = { 0 };printf("%zd\n", sizeof(a));//1.输出结果是什么?printf("%zd\n", sizeof(a[0][0]));//2.输出结果是什么?printf("%zd\n", sizeof(a[0]));//3.输出结果是什么?printf("%zd\n", sizeof(a[0] + 1));//4.输出结果是什么?printf("%zd\n", sizeof(*(a[0] + 1)));//5.输出结果是什么?printf("%zd\n", sizeof(a + 1));//6.输出结果是什么?printf("%zd\n", sizeof(*(a + 1)));//7.输出结果是什么?printf("%zd\n", sizeof(&a[0] + 1));//8.输出结果是什么?printf("%zd\n", sizeof(*(&a[0] + 1)));//9.输出结果是什么?printf("%zd\n", sizeof(*a));//10.输出结果是什么?printf("%zd\n", sizeof(a[3]));//11.输出结果是什么?return 0;}

大家可以先思考一下这11道题的输出结果是什么,一会博主会进行讲解~

▶️2.1二维数组和指针相关笔试题讲解

如下图所示:

1.我们知道,sizeof(数组名)是计算整个数组的大小。
那从上图,我们得知,这个二维数组一共有12个元素,并且每个元素占的是4个字节,那这里总共占了12*4个字节。所以它的大小是48。
2.从上图:我们知道a[0][0]是第一行第一个元素,大小是4个字节。

如下图所示

3.虽然这个二维数组在我们假想中是一个多行多列的形式。
但事实上我们刚刚就介绍过二维数组在内存中是连续存放的,也就是像上图这样存储。
我们可以把这个二维数组每一行看作是一个一维数组,然后我们给每一行的一维数组都起个名,分别是a[0],a[1],a[2]
所以说,这里a[0]实际上就是第一行的数组名,然后这里的数组名单独放在sizeof内部了,计算的是第一行的大小,而且第一行是有4个整型,所以它的大小是16个字节。
4.这里的a[0]是第一行这个数组的数组名。
但是这个数组名并非是单独放在sizeof内部,所以数组名表示数组首元素的地址,也就是a[0][0]的地址。
那a[0]+1是第一行第二个元素(a[0][1]),是地址它的大小就是4/8个字节。
5.从上题我们知道**a[0]+1是第一行第二个元素(a[0][1])的地址。**
所以,*(a[0]+1)拿到的就是第一行第二个元素,大小是4个字节。

如下图所示:

6.这里我们发现a没有单独放在sizeof内部,没有&,数组名a就是首元素的地址,也就是第一行的地址。
所以a+1,就是第二行的地址。它的大小也就是4/8。
这里可能有同学对此表示疑惑,为什么a+1是第二行的地址呢?
因为a是数组首元素的地址,而从上图,我们得知首行(第一行)是四个整型元素的数组,所以a的类型为int ( * )[4],是个数组指针类型。
那我们想一想,如果a是这个类型的话,+1是不是要跳过这个4个整型的数组啊。 就是把第一行跳过去指向第二行,如上图绿色部分显示。所以它的大小为4/8个字节。
7.因为我们知道a=int( * )[4],那a+1=int( * )[4],它们的类型本质上都是个数组指针类型。
对一个数组指针进行解引用操作实际上就是访问一个数组的大小,是不是这个道理?对于一个数组指针+1跳过一个数组,对于一个数组指针+1就是得到一个数组。
所以sizeof(a+1)这里的**a+1**指向的是第二行的地址,那*(a+1)就是第二行的元素,它的大小16。
这里还有第二种解读方式: 这里的*(a+1) ==a[1] ,因为a[1]恰好是第二行的数组名,数组名单独放在sizeof的内部,所以它的值也是16。

如下图所示:

8.我们之前讲过这个&数组名 取出的是整个数组的地址。
那同理a[0]是第一行的数组名,&a[0]取出的是第一行的地址,那&a[0]+1得到的就是第二行的地址。是地址的话大小就是4/8。
需要注意的是:这里的a+1==&a[0]+1,因为它们本质上都是第一行的地址+1,指向的是第二行的地址。
9.我们从上题可以得知:&a[0]+1得到的就是第二行的地址。
*(&a[0]+1)就是指向的就是第二行,它的类型也是int ( * )[4],对其解引用得到的是第二行的元素,大小是16。

如下图所示:

10.这里的a表示的是二维数组的数组名
由于它这里没有单独放在sizeof内部,也没有&数组名。
所以数组名a就是数组首元素的地址,也就是第一行的地址,* a就是第一行的,所以它的大小就是16。
这里还需注意一下:* `a = = *(a+0) == a[0] 这三种表达的意思其实是等价的。

如下图所示:

11.如上图,这里的a[3]指的就是第四行,因为只有第四行才能表示成a[3],对不对?
那这里或许很多同学都会认为它这里会报错,实际上,它会不会报错呢?
实际上是不会的。
因为我们之前讲过,sizeof去计算的时候,他压根不会计算表达式里面的值,它不会真实访问第四行的,所以不会存在越界访问。
a[3] - -a[0],这个通过类型就可以推断出来的,是不是一回事呢,a[3]a[0]这不就是表示第几行的数组名吗?数组名单独放在sizeof的内部,它的大小就是16。,它的类型跟a[0]是一样的。

▶️2.2 VS运行结果演示

通过上面的讲解分析,我们不妨拿VS来测试一下结果,看看分析的是否正确把~

这里我们分别以x64和x86环境来进行测试运行用例~
x86运行环境:

x64运行环境:

实际上,从vs运行的结果来看,我们对这11题代码分析的结果是没错的。



▶️2.3总结

这里我们通过讲解二维数组和指针相关笔试题,请
大家再次注意以下知识点的巩固:

1.sizeof(数组名),这里的数组名表示整个数组,计算的是整个数组的大小。
2.&数组名,这里的数组名表示整个数组,取出的是整个数组的地址,它的类型是一个数组指针类型。
3.除了上面两个例子外,其余的数组名都表示数组首元素的地址。



▶️3.指针运算笔试题解析

接下来我们将给大家讲解7道关于指针运算的笔试题。

▶️3.1题目1:

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

大家可以先思考一下这道题的输出结果是什么,一会博主会进行讲解~

▶️3.1.1 题目1讲解:

如下图所示:

分析:根据上图,我们已经知道&a是取出整个数组的地址。那&a+1就是跳过整个数组的地址,也就是指向元素为5后面的地址
另外,我们知道这是一个整型数组,所以&a是类型是int( * )[5],那(&a+1)还是这个类型。
现在要把它的值赋给ptr,因为ptr的类型是int * ,所以我们要对(&a+1)强制类型转换为int*,然后再赋给ptr,所以ptr指向的也是元素为5后面的地址。
那我们再看: 由于ptr是一个整型指针,整型指针+1是向后跳过一个整型,那-1呢?就是向前跳一个整型。
根据我们上面画的图,ptr-1指向的是5的地址,因为它是个整型指针,那我们对其进行解引用,就能访问它里面元素的值,也就是5。
前面* (a+1)输出结果也是同样的道理: 因为a是数组首元素的地址,那a+1就相当于跳过一个整型元素,指向的是第二个元素的地址,对其进行解引用的话,拿到的是数组第二个元素的值,也就是2。

▶️3.1.2 vs测试结果:

我们不妨用vs测试一下,看看我们分析的是否正确吧~

我们就以x64的环境演示一下吧:



▶️3.2 题目2:

//在X86环境下//假设结构体的大小是20个字节//程序输出的结构是啥?struct Test{int Num;char* pcName;short sDate;char cha[2];short sBa[4];}*p = (struct Test*)0x100000;int main(){printf("%p\n", p + 0x1);//1.输出结果是什么?printf("%p\n", (unsigned long)p + 0x1);//2.输出结果是什么?printf("%p\n", (unsigned int*)p + 0x1);//3.输出结果是什么?return 0;}

大家可以先思考一下这道题的输出结果是什么,一会博主会进行讲解~

▶️3.2.1 题目2讲解:

题目分析:这题本质上考察的就是指针运算中的指针±整数
并且我们发现这个指针是个结构体类型的,这个结构体是什么,我们压根不用关心,因为题目已经给出这个结构体的大小为20字节。

1.我们来看一下第一题+1到底跳过多少个字节呢?
这个其实是取决于它的指针类型,我们现在这里是个结构体的指针,所以它+1就是跳过一个结构体的大小,所以这里p+0x1,就相当于加了个20字节。
需要注意的是:这里+1得到的不是100020,因为它这个0x本质上是个16进制的数字,那加20之后就变成100014
具体换算过程如下:

2.这里需要注意的是: 很多同学误以为这里整型+1是跳过一个元素,实际上是错的,为什么呢?接下来我给大家详细解释一下~
分析: 这里的结构体指针类型被强制转换为unsigned long,它就不是一个指针类型了。因为我把p转换为unsigned long类型,让它是一个无符号的整型,整型+1加几?
比如说:500+1它的结果是多少,501还是504" />因为这玩意不是指针,我们说只有指针+1才想着跳过1个元素,但现在我是个unsigned long,是整型啊,整型+1就是+1。因为只有整型指针+1我才跳过一个整型元素的大小,一个结构体指针+1我才跳过一个结构体,一个字符指针+1我才跳过一个字符,而现在我是unsigned long,+1就是+1。它只是简单地将地址增加一个字节。
所以打印结果就是100001
3.这题我们发现,我们这是把这个结构体指针强制类型转换为unsigned int *,那整型指针+1是加几?
是不是加4,所以就变成100001。所以最终打印结果就是1000001

▶️3.2.2 vs测试结果:

我们不妨用一下vs测试一下运行结果看看

x86运行结果:

这里或许有同学对于前面为什么要加上00而产生疑惑?我们就简单讲一下吧~

我们发现前面加了个00,前面的地方加了个00,打印的时候00是用%p,%p前面即使前面是0它也会打印出来的,不会省略的。
因为我们这里是x86环境,一个指针要打够32个bit,我们之前这篇博客:C语言的操作符讲解(上)讲过32bit位等于8个十六进制位,所以打印的时候,前面会加上00
然后我们又知道一个十六进制位是等于4个二进制位,所以8个十六进制位就等于32个二进制位。



▶️3.3 题目3:

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

大家可以先思考一下这道题的输出结果是什么,一会博主会进行讲解~

▶️3.3.1 题目3讲解:

如下图所示:

解读题目: 这道题可能很多人以为这个二维数组的初始化的值如上图所示。
但其实这并不是的,因为我们发现这个地方有个小圆括号,小圆括号括起来就叫逗号表达式。
它的具体用法如上图所示~
虽说我们看着这个数组初始化了一堆数字,但其实它就初始化了3个数字。具体如下图所示:

解答题目: 我们接着往下看,发现这个p是个整型指针。
然后a[0]是二维数组的首行的数组名。那这里的数组名有代表数组首元素的地址的话,也就是元素为1的地址,所以这写a[0],实际上就是a[0][0]的地址,所以p就是妥妥的指向1的地址。
然后接着看下面那个输出结果为p[0] 这个p[0]==*(p+0),这两个是等价的。 而因为这里的p+0是没加的,还是指向1的地址,我们对其进行解应用操作,访问就是里面数组元素1,因此它的输出结果就为1

▶️3.3.2 vs测试结果:

我们不妨用一下vs测试一下运行结果,看看我们分析的是否正确吧~

这里我们就以x64环境演示吧:

通过运行结果发现,我们分析的是没错的~



▶️3.4 题目4:

//假设环境是x86环境,程序输出的结果是啥?#include 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;}

大家可以先思考一下这道题的输出结果是什么,一会博主会进行讲解~

▶️3.4.1 题目4讲解:

如下图所示:

画图分析: 我们知道这个二维数组是五行五列的,因此我们我们就把它画出来。并把二维数组的每行分别以a[0]-a[4]这样标出来。
接着往下看,我们发现p是一个指针,它指向一个四个整型元素的数组。
接着往下看: 我们发现它是把a的首元素的地址交给pp也就指向a[0][0]的地址。

接着看下图:

从上图中: 或许有同学有疑问为什么p[4][2]a[4][2]的地址分别放在这两处? 接下来我将细细讲解~
1.首先那个a[4][2]的地址其实是二维数组的第五行第三个元素的地址。具体的指向位置就是上图蓝色填充部分。
2.同样地: 我们知道p是一个指针,指向一个四个整型元素的数组,所以对p+1,就跳过四个整型元素嘛。
如果我们把指针变量p想象成二维数组的一行,那p[4][2]指向哪里呢?
如果我们按照刚刚那个想法,p是指向一个数组的话,+1跳过一行,再+1跳过一行,再+1跳过一行,再+1跳过一行。
就相当于它跳了4行,指向第五行第三个元素的位置。具体的指向位置就是上图蓝色填充部分。

如下图所示:

1.接着往下看,既然我们已经知道这两个地址的指向,那这里&p[4][2] - &a[4][2]本质上就是考察我们指针-指针的结果是多少。
我们之前这篇博客:C语言指针详解(一)超详细~
介绍过指针-指针得到的是指针之间的元素个数,这里我们发现它们之间的元素个数为4个,但是从图中,我们也可以看出来这个p[4][2]的地址是小于a[4][2]的地址。
所以这个如果我们以%d的形式打印出来的话,输出结果为-4。
2.需要注意的是: 这里以%d的形式打印和以%p的形式打印是截然不同的。为什么呢?
因为我们要知道,-4它要存到内存存的是补码,内存里面只以%p的形式打印我们认为存的是地址,地址是不存在原反补的概念。地址可以理解成一个无符号数的。
所以内存中存的是补码就直接当成地址被翻译出来了,但如果%d打印的话是打印出它的原码出来。
可能有同学忘记了-4的补码是怎么写了,没关系,我们直接看下图~

如果内存中存放的是它的补码,大家想象一下,那我以%p的形式打印的时候,我们就认为它
内存里面存放的是地址,这个是不需要求它的原码出来的,%p就是认为它是地址,直接把它打印出来就可以的。

又因为%p格式化字符打印一个指针时,它会以十六进制的形式打印指针的值。
也就是说我们把-4的补码转换为十六进制打印出来。具体转换参考下图:

所以最终它的输出结果FFFFFFFC。因此以%p打印出来的结果为FFFFFFFC
而如果是%d打印的话,就把补码还原成原码,就是-4嘛。

▶️3.4.2 vs测试结果:

我们不妨用vs测试一下运行结果,看看我们分析的是否正确。

从图中,我们发现vs输出结果跟我们分析的结果是一样的,因此是没毛病的~

▶️3.4.2 VS警告:

如下图所示:
可能有同学看到vs有警告

我们来解释一下:a这个数组名他表示首元素地址的话,它是第一行这个地址,是五个整型的这个数组。如果强行赋给p的话,两边的类型是不是有差异的。所以编译器这里报出一个警告,但是如果你强行赋就赋过去呗对吧,a作为地址会被强制地址p的这个类型,你使用p,就用p的这个视角去走,p就一次加4个整型,就是这么一个道理啊。

总结: 我们用不同的指针在走的时候,你看,你拿a在访问的时候,一行五个元素,一行五个元素,但如果这里是一个数组指针指向四个元素的话,它加1是跳过四个元素,是不是这样的逻辑呀~



▶️3.5 题目5:

#include 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;}

大家可以先思考一下这道题的输出结果是什么,一会博主会进行讲解~

▶️3.5.1 题目5讲解:

如下图所示:

分析题目: 我们根据发现aa数组是一个二行五列的二维数组,*我们也将它的图给画出来。具体看上图
接着我们往下看,&aa是取出整个二维数组的地址,那&aa指向的是第一行首元素的地址。
aa+1它就把整个二维数组都跳过去了,也就是指向10地址后面的地址,跳过去之后,然后强转为整型指针int *再把它赋给ptr1,说明ptr1也是指向那个位置的。
ptr1-1呢?它是个整型指针,向前挪动一个整型,是不是指向10的地址。那对其进行解引用,访问的是不是10的元素。
所以*(ptr-1)的结果就是10。


接着往下看: 我们发现aa是数组首元素的地址,首元素地址就是第一行的地址,也就是aa[0][0]的地址。
aa+1就跳过一行了,因为是数组名是数组首元素的地址,也就是第一行的地址,第一行的地址+1就是第二行的地址
然后第二行的地址解引用,是不是拿到第二行了,其实就相当于第二行的数组名。这里的 * (aa+1)==a[1],a[1]不就相当于拿到第二行吗?相当于拿到第二行的数组名。
所以这个地方a[1]或者 * (aa+1)得到的
虽然是数组名
,但它没有sizeof,又没有单独&,又没有单独放在sizeof内部,所以数组名表示首元素地址,是首元素地址代表的是第二行第一个元素的地址,因为aa[1]是第二行的地址,那数组名代表首元素的地址,就是aa[1][0]的地址,赋给ptr,ptr就是指向6的地址。
需要注意的是* (aa+1)旁边强制类型转换为整型指针,这个是没有意义的,因为它本身就是首元素地址。
这个地方强制类型转换就是迷惑你的,因为两边类型一样了,所以它这个强制类型转换是没有意义的。
接着往后看,那个ptr2作为一个整型指针,向前挪动一个整型,指向的是5的地址,解引用,访问的就是5了,所以它最终的输出结果为5。

▶️3.5.2 vs测试结果:

我们不妨用vs测试一下运行结果,看看我们分析的是否正确。

这里我们以x64环境来演示一下:

总结:我们发现这一道题其实也不是很难,重点还是要对数组名的理解,唯有把这些知识点理解透彻了,我们解这种类型的题才能迎刃而解



▶️3.6 题目6:

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

大家可以先思考一下这道题的输出结果是什么,一会博主会进行讲解~

▶️3.6.1 题目6讲解:

如下图所示:

分析题目:1. 从上图,这里面我们放了几个字符串,数组a的每个元素是char *啊,说明这是一个字符指针的数组啊!
我们要知道字符串作为表达式,它的值是不是首字符的地址啊,所以这里给work,at,alibaba这三个字符串的时候,那我们这里是不是把workw地址存到里面去,ata地址存到里面去,alibabaa地址存到里面去,是这个道理吧。
需要注意的是: 这是一个char*的数组,它的每个元素都是char*的,所以它才能存w的地址,a的地址,a的地址,a是数组名,数组名表示数组首元素的地址,也就是存的是w的地址。
pa就用char **的地址来接收啊,所以pa存的是字符指针数组w首元素的地址。
接着往下看,pa++就相当于跳过一个字符指针数组的元素,指向的是a的地址是不是,对其进行解引用拿到char *的元素为a。并以%s打印的话,最终它的输出结果at

▶️3.6.2 vs测试结果:

我们不妨用vs测试一下运行结果,看看我们分析的是否正确。

总结: 这道题本质上就是考察字符指针数组以及二级指针的相关用法,大家要把这种题理解透彻才行。



▶️3.7 题目7:

#include int main(){char *c[] = {"ENTER","NEW","POINT","FIRST"};char**cp[] = {c+3,c+2,c+1,c};char***cpp = cp;printf("%s\n", **++cpp);//1.输出结果是什么printf("%s\n", *--*++cpp+3);//2.输出结果是什么printf("%s\n", *cpp[-2]+3);//3.输出结果是什么?printf("%s\n", cpp[-1][-1]+1);//4.输出结果是什么?return 0;}

这一道题可能比较难,所以大家不用做,仔细听一下博主是怎么分析这道题的。

▶️3.7.1 题目7讲解:

题目分析:在讲解这4道题目之前,我首先将这三行代码通过注释和画图的方式解析一下~

注释:

char *c[] = {"ENTER","NEW","POINT","FIRST"};/*这个c字符指针数组本质上存的是字符串中首字符的地址,比如:它分别指向数组中这四个字符串中的首字符地址。比如这个c指向字符串中ENTER的首字符地址E,指向字符串中NEW的首字符地址N,指向字符串POINT的首字符地址P,指向字符串中FIRST的首字符地址F。另外这个数组它的每个元素是一个char*的内容*/char**cp[] = {c+3,c+2,c+1,c};/*接下来往下看,它又给了个数组,数组中的每个元素分别是c+3,c+2,c+1,c。这个数组的每个元素是char **,而这些数组元素c的首元素地址是char *的地址,是一级指针的地址,那它的类型是不是char **啊,所以这里我们用二级指针地>>址来接收它。*/char***cpp = cp;//接着我们看,这里数组名代表首元素的地址,就是char **的地址,那我们就要拿个三级指针变量char ***来接收它,这个指针变量叫cpp,它的类型是char ***,它里面放着是cp,cp是这个cp数组的数组名,数组名表示数组首元素的地址,所以它也是指向c+3元素的地址,

根据我们上面三行代码写的注释,那我们也能把它对应的指针所指向的内存图给画出来。
如下:

大家可以先看一下博主画的这个指针所指向的内存图,自己尝试理解消化一下。

好了,如果大家已经理解博主上面画的图,那我们就要对这四道表达式进行计算了,那博主就依次解答这四道题吧。

1.我们先看第一道题,它的是以%s的形式来输出**++cpp的值的。那我们知道++这个运算符优先级是比**的运算符要高的。
因此这个表达式会优先算++,再算**。那我们再看:++cpp这个++是不是有副作用的。比如看下面这个例子~

从上面的例子我们可以知道,++a的意思就相当于a=a+1,它是会让a变化的,那++cpp呢?我们继续看图:

从上图,我们更能直观地看出:
1.cpp原本放着这个这个地址,假设起始地址为0x0012ff40,如果我们对它++cpp++,也就是cpp+1,也就是跳过一个char**元素的大小,也就是把c+3这个元素跳过去了,也就是指向c+2元素的地址。
所以++cpp就让cp不再指向c+3首元素的地址了,而是指向第二个元素c+2的地址了。也就是指向下面的空间去了。当我们++完之后,先解引用一层,我们通过解引用,找到的是c+2的元素。
然后我们再解引用,通过对c+2解引用找到的是char*的值,它里面存的是p的地址。然后p的地址我们以%s的形式打印,最终它的输出结果就为point
再次强调一下: 这一次程序走的过程中++cpp确实是让cpp变了,cpp的值就不再指向cp数组首元素的地址(c+3)了,而是指向cp首元素地址跳过一个元素的地址,也就是指向c+2元素的地址。
因此我们下次我们用cpp的话,要从c+2这个地址开始。

2.首先,我们先解读一下这个表达式*--*++cpp+3
当我们再次执行这个表达式的时候,我依然是先执行++cpp,因为+优先级比较低,所以这个表达式运算顺序是++先算,++旁边的那个*先算,--先算,--左边的那个*先算,最后才是+先算。
然后,我们再仔细分析这道题,我们先给大家一个画个图给大家看看这个表达式的运算逻辑,然后再进行讲解~

我们看图中绿色框框的部分: cpp原本指向c+2的地址,那++cpp,这一次指向也就相当于断了,就相当于跳过一个元素,指向的是c+1的地址。那++之后解引用找到的是c+1的元素。然后进行--的操作,--就是让这里的值-1,这里面本身就是放c+1,
-1之后这里就变成c了,如果这里变成c之后,刚刚这种指向就不存在了,因为它现在是c的元素,我们通过对c解引用找到的是char*的值,也就是它里面存放的是E的地址。
E的地址+3,我们知道+0开始指向E,+1开始指向N,+2指向T,+3指向E,因为我们刚刚拿到是E的地址啊,那E的地址+3是不是跳过3个元素指向第二个E,那从E这里以%s的形式打印出来了,打印出来的值是不是ER

总结: 有没有发现这些题还蛮坑的,前面错了,后面也跟着错。
因为前面错了是会影响后面的,++--操作都会让指针的指向的内容有所改变。所以前面做错后面都会做错,因此我们计算这种题要细心一点才行。

3.这里可能有些同学对于这个表达式:*cpp[-2]+3有点懵,我们来给大家解读一下。
如下图所示:

从图中,我们可以直观地看出来,这个cpp[-2]不是相当于*(cpp-2),又因为前面还有一个*,再加3
所以这个cpp[-2]+3这行代码可以转换成:**(cpp-2)+3。这两个表达式本质上是一样的。
另外,我们通过作图的方式把它这个表达式运算的逻辑搞了出来,具体如下:

从上图: 我们知道cpp原本是指向c+1元素的地址,-1指向c+2的地址,-2指向的是c+3的地址。
它指向的是cp数组中首元素的地址,得到的就是c+3的地址。
需要注意的是 :这个cpp-2那个cpp指向的对象是不变的,只是说它这个表达式得到的是c+3的地址,那之后解引用一次,通过c+3的地址,拿到的是不是c+3的内容?
然后前面又有一个*符号,再解引用一次,找到的是不是char*的值,这里面刚好是FIRST的内容,里面存放的是F的地址。
然后后面+3,就是跳过3个元素,是不是刚好指向里面S的地址如果以%s的形式打印的话,最终的输出结果是ST。

4.这里的cpp[-1][-1],这里可能有同学对于这个表达式也是懵懵懂懂的,因此我们要把这个表达式转换成解引用的形式先。
如下图:

首先呢,我们先把第一个[-1]写成*(cpp-1),然后第二个[-1]就是整体-1之后再解引用。也就是写成*(*(cpp-1)),然后再+1,就是写成*(*(cpp-1))+1的形式。
那这个表达式的运算逻辑是怎么样呢?接下来我将以作图的方式来细细讲解一下。

这里我们再次强调一下: 刚刚cpp[-2]的时候,cpp这个动作是没变的,cpp还是指向(c+1)的地址。
从图中绿色箭头以及圆圈所示: (cpp-1)产生的是这个表达式的地址,这个表达式指向的就是c+2的地址,这里面的地址先解引用,拿到它里面的值c+2。里面存放的是c数组中的第三个元素char *的地址,那它-1,拿到的是不是c的数组中第二个元素char * 的地址,然后我们在外层再进行解引用操作,拿到的是不是第二个元素char * 的值,拿到它里面的值是N的地址啊。
它指向N的地址,那+1是不是相当于跳过一个元素,它指向E,所以如果以%s的形式打印出来就是EW。

▶️3.7.2 vs测试结果:

好了,分析了那么多了~
不妨我们用VS测试一下,看看运行结果是否跟我们分析的一样吧。

X64运行环境:

我们发现VS的运行结果跟我们分析的是一样的,没有任何的问题。

总结: 通过我们这道题,我们发现这个题代码是环环相扣的,考得都是指针的运算,指针进行解引用操作的相关运算。



▶️4.总结

好了,现在博主已经把所有指针的笔试题讲完了。
让我们再次回顾这次博客讲了什么吧。

1.二维数组中的每个元素都是连续存放的。 二维数组的数组起始可以看作是一维数组的数组,也就是二维数组的每个元素是一个一维数组。那么二维数组的首元素就是第一行,是个一维数组。
2.二维数组相关笔试题和讲解
3.指针运算题和讲解

另外,至此为止:我们就把指针的知识点以及相关题目全部讲完了,大家如果有遗忘的知识点,可以翻看博主以前的博客,里面有对指针知识点和题目进行详细的讲解。



最后,如果大家觉得博主这次博客有讲得不好或者不清楚的地方,欢迎私信或者评论区指出。

** 如果觉得博主讲得不错,对你学习指针方面的知识有帮助。**

** 可以给博主一个小小的关注,一键三连吗,谢谢大家!!! **