【C语言】数组与指针常见笔试题讲解(2)

0. 前言

Hello,大家好呀!今天我们继续来讲解数组与指针相关笔试题

在讲解之前,让来我们来回顾一下上篇博客讲的内容:

【C语言】数组与指针常见笔试题讲解(1)-CSDN博客

1.

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

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

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

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

本期博客会把剩下的二维数组和指针笔试题,以及指针运算相关笔试题进行讲解。 

1. 预备知识

在讲解二维数组和指针相关笔试题之前,我们先带同学们回顾一下二维数组在内存中的存储

这个知识~

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

像⼀维数组⼀样,我们如果想研究⼆维数组在内存中的存储⽅式,我们也是可以打印出数组所有元素的地址的。

代码如下:

#include <stdio.h>

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

输出的结果: 

​ 

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

另外,我们曾经在:【C语言】深入理解指针-CSDN博客
讲过:二维数组的数组起始可以看作是一维数组的数组,也就是二维数组的每个元素是一个一维数组。那么二维数组的首元素就是第一行,是个一维数组。

如下图所示:

1.2 总结 

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

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

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

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

#include <stdio.h>

int 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;
}

 大家可以先思考一下这些题,你们认为输出结果是什么,一会儿会进行讲解~

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]指的就是第四行数组名
那这里或许很多同学都会认为它这里没有第四行数组,数组越界了,会报错!

实际上,它会不会报错呢?🤔
实际上是不会的。
因为我们之前讲过,sizeof去计算的时候,他压根不会计算表达式里面的值,它不会真实访问第四行的,所以不会存在越界访问。
a[3]这个通过类型就可以推断出来的,这就是第四行的数组名吗?

数组名单独放在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 <stdio.h>

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 + 1) 相信大家都没问题,它表示的是数组的第二个元素。
  
我们来重点讲一下*(ptr - 1)。

先来看 &a。对数组名进行取地址操作, 取出的是整个数组的地址,虽然数值上与数组首元素相等,但他们是不同的类型。在这里,它类型为 i n t ( ∗ ) [ 5 ] int(*)[5]int(∗)[5]。
  
再看 &a + 1 ,这里取出的是整个数组的地址,+1 即跳过整个数组。最后,再强制类型转换成 i n t intint * 类型
  
ptr - 1 此时,指针已经跳过了整个数组,因为此时的 p t r ptrptr 已经是 i n t intint* 类型, - 1,后退 4 个字节,即指向数组最后一个元素。
  
*(ptr - 1) 最后再进行解引用,取出数组最后一个元素

3.1.2 运行结果: 

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 得到的不是将p的地址100000加上20得到100020,
因为它这个0x本质上是个16进制的数字,那+20之后就变成100014。

具体换算过程如下:

2. 这里需要注意的是: 
很多同学误以为这里整型+1是跳过一个元素,实际上是错的,为什么呢?接下来我给大家详细解释一下~

分析:这里的结构体指针类型被强制转换为unsigned long,它就不是一个指针类型了。因为我把p转换为unsigned long类型,让它是一个无符号的整型,整型+1加几?

比如说:500+1它的结果是多少,就是501,因为就是整形的加法

因为这不是指针,我们说只有指针+1才跳过1个元素的大小,但现在是unsigned long,是无符号长整型, 
就是整型,整型+1就是+1。因为只有整型指针+1我才跳过一个整型元素的大小,一个结构体指针+1我才跳过一个结构体,一个字符指针+1我才跳过一个字符,而现在我是unsigned long,+1就是+1。它只是简单地将地址增加一个字节。所以打印结果就是100001。

3. 这题我们发现,我们这是把这个结构体指针强制类型转换为unsigned int*, 那整型指针+1是加几?
就是假整型指针的大小,当前平台是32位,指针大小是4个字节,所以就变成100004。
所以最终打印地址结果就是1000004。

3.2.2 vs测试结果:

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

x86运行结果:

 这里或许有同学会有疑问,为什么要前面加上 00 呢?

因为我们这里是x86环境,一个指针要打够32个bit位,
我们之前讲过操作符时讲过,32个bit位等于8个十六进制位,
一个十六进制位是等于4个二进制位,
所以8个十六进制位就等于32个二进制位。
所以打印的时候,前面会加上00。

3.3 题目3:

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

 大家可以先思考一下,你们认为输出结果是什么,一会儿会进行讲解~

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测试一下运行结果,看看我们分析的是否正确

 通过运行结果发现,我们分析的是没有问题的

3.4 题目4:

//假设环境是x86环境,程序输出的结果是啥?

#include <stdio.h>
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行,指向第五行第三个元素的位置。具体的指向位置就是上图蓝色填充部分。
接着往下看,既然我们已经知道这两个地址的指向,那这里&p[4][2] - &a[4][2]本质上就是
考察我们对于 “指针-指针” 的理解。
我们之前介绍过指针-指针得到的是指针之间的元素个数,这里我们发现它们之间的元素个数为4个,但是从图中,我们也可以看出来这个p[4][2]的地址是小于a[4][2]的地址。
所以这个如果我们以%d的形式打印出来的话,输出结果为-4。

需要注意的是: 这里以%d的形式打印和以%p的形式打印是截然不同的。为什么呢?
因为我们要知道,-4它要存到内存存的是补码,内存里面只以%p的形式打印我们认为存的是地址,地址是不存在原反补的概念。地址可以理解成一个无符号数的。
所以内存中存的是补码就直接当成地址被翻译出来了,但如果%d打印的话是打印出它的原码出来。
-4的补码怎么写呢?没关系,我们来看下面的推导过程

 

如果内存中存放的是它的补码,大家想象一下,那我以%p的形式打印的时候,
我们就认为它内存里面存放的是地址,这个是不需要求它的原码出来的,
%p就是认为它是地址,直接把它打印出来就可以的。
又因为%p格式化字符打印一个指针时,它会以十六进制的形式打印指针的值。
也就是说我们把-4的补码转换为十六进制打印出来。具体转换参考下图:

所以最终它的输出结果为FFFFFFFC。因此以%p打印出来的结果为FFFFFFFC。

 3.4.2 vs测试结果:

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

我们发现vs输出结果跟我们分析的结果是一样的

可能有同学看到vs有警告

 

因为:
a这个数组名他表示首元素地址的话,它是第一行这个地址,是五个整型的这个数组。
如果强行赋给p的话,两边的类型是不是有差异的。所以编译器这里报出一个警告,

3.5 题目5: 

#include <stdio.h>
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测试一下运行结果,看看我们分析的是否正确。

总结:

我们发现这一道题其实也不是很难,重点还是要对数组名的理解,

唯有把这些知识点理解透彻了,我们解这种类型的题才能迎刃而解。

 3.6 题目6:

#include <stdio.h>

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

 大家可以先思考一下 这道题的输出结果是什么?

3.6.1 题目6讲解:

分析:
从上图,这里面我们放了几个字符串,数组a的每个元素是char*, 说明这是一个字符指针的数组
我们要知道字符串作为表达式,它的值是不是首字符的地址啊,所以这里给work,at,alibaba这三个字符串的时候,那我们这里是把work的w地址存到里面去,at的a地址存到里面去,alibaba的a地址存到里面去,

需要注意的是: 这是一个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 <stdio.h>

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讲解:

char* c[] = {"ENTER","NEW","POINT","FIRST"};
char** cp[] = {c+3,c+2,c+1,c};
char*** cpp = cp;

题目分析:在讲解这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的值的。
那我们知道++这个运算符优先级是比**的运算符要高的。
因此这个表达式会优先算++, 再算**。
我们更能直观地看出:
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来验证一下,看看运行结果是否跟我们分析的一样吧。

 

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

总结: 通过我们这道题,我们发现这个题代码结合在一起,是一道综合考察指针的运算

指针进行解引用操作的相关运算这样好的经典笔试题

4. 总结

好了,已经把数组和指针常考的笔试题都讲解完毕了。

本期博客我们讲了以下内容:

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

另外,至此为止:我们就把指针的知识点以及相关题目全部讲完了,

大家如果有遗忘的知识点,可以翻看以前的博客,里面有对指针知识点和题目进行详细的讲解。

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

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

 可以给我一个小小的关注,一键三连吗,谢谢大家🫶


完 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值