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

0. 前言

小伙伴们大家好!通过之前的学习,相信大家对数组,指针有了基本的认识,数组和指针在C语言中是非常重要的概念,也是未来找工作笔试中经常出现的考点。本文将对一些常见的数组与指针相关的笔试题进行详细解析,帮助您更好地理解和掌握这部分知识。

1. 数组试题

1.1 整型数组:

题目如下:

大家不妨先思考一下这些题,看看它们的输出结果是什么?

int main()
{
	int a[] = { 1,2,3,4 };		
	printf("%zd\n", sizeof(a));
	printf("%zd\n", sizeof(a + 0));		
	printf("%zd\n", sizeof(*a));
	printf("%zd\n", sizeof(a + 1));		
	printf("%zd\n", sizeof(a[1]));
	printf("%zd\n", sizeof(&a));			
	printf("%zd\n", sizeof(*&a));		
	printf("%zd\n", sizeof(&a + 1));
	printf("%zd\n", sizeof(&a[0]));
	printf("%zd\n", sizeof(&a[0] + 1));			                
	return 0;
}

🌟🌟知识回顾:

在学习操作符的时候,我们学习过 sizeof,sizeof 计算的是变量所占内存内存空间⼤⼩的,单位是字节,如果操作数是类型的话 sizeof(类型),计算的是使⽤类型创建的变量所占内存空间的⼤⼩

我们要注意的是:sizeof 只关注占⽤内存空间的⼤⼩,不在乎内存中存放什么数据。

另外,在数组章节讲过的易错点,就是对于 “数组名” 的理解

数组名一般是数组首元素(第一个元素)的地址,但是有两个例外,

1. sizeof(数组名) - 数组名表示整个数组,计算的是整个数组的大小,单位是字节

2. &数组名 - 数组名表示整个数组,取出的是整个数组的地址

除此之外,所以的数组名是数组首元素(第一个元素)的地址。

我们来分析一下:

int main()
{
	int a[] = { 1,2,3,4 };//数组a有4个元素:1,2,3,4

	printf("%zd\n", sizeof(a));//16 -- sizeof括号内单独放数组名a, 
    数组名表示整个数组,计算的是整个数组的大小, 4个元素,每个元素都是整型,
    整型占4个字节,4*4=16字节
	
    printf("%zd\n", sizeof(a + 0));//a是首元素的地址-类型是int*, 
    a+0 还是首元素的地址,是地址大小就是4/8
	
    printf("%zd\n", sizeof(*a));//a是首元素的地址,*a就是首元素,大小就是4个字节
	其实可以理解为:*a == a[0] == *(a+0)
	
    printf("%zd\n", sizeof(a + 1));//a是首元素的地址,
    类型是int*,a+1跳过1个整型,a+1就是第二个元素的地址,4/8
	
    printf("%zd\n", sizeof(a[1]));//a[1]就是第二个元素,大小4个字节
	printf("%zd\n", sizeof(&a));//&a是数组的地址,数组的地址也是地址,
    是地址大小就是4/8个字节
	
    printf("%zd\n", sizeof(*&a));
    //1. *& 互相抵消了,sizeof(*&a) = sizeof(a) -16
	//2. &a 是数组的地址,类型是int(*)[4],对数组指针解引用访问的是数组, 
    计算的是数组的大小 -16
	
    printf("%zd\n", sizeof(&a + 1));//&a+1是跳过整个数组后的那个位置的地址,
    是地址就是4/8个字节
	printf("%zd\n", sizeof(&a[0])); //首元素的地址,大小4/8个字节
	printf("%zd\n", sizeof(&a[0] + 1));//&a[0] + 1 -- 数组第二个元素的地址,
    大小是4/8个字节

	return 0;
}

那我们分析的是否正确呢,接下来我们分别用vs的x64环境和x86环境来测试一下运行结果~

在X64环境下,是64位,所以地址大小为8 

我们再看这个,在X86环境,是32位,所以地址大小为4,这也就是为什么指针大小是4或者8的原因 

1.2 字符数组

代码1:

现在我们把整型数组换成字符数组了,大家思考下面程序会输出什么结果? 

待会我们会统一讲解一下。

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));

	return 0;
}

 同样的道理,我们发现还是求sizeof, 也就是变量/类型所占空间的大小。

分析:

int main()
{
    //数组arr包含6个字符:'a','b','c','d','e','f'
	char arr[] = { 'a','b','c','d','e','f' };

	printf("%d\n", sizeof(arr));//数组名单独放在sizeof内部了,计算的是数组的大小,
	//单位是字节,6个元素,每个元素都是字符类型,
	//char占1个字节 6*1=6
	printf("%d\n", sizeof(arr + 0));//arr是数组名表示首元素的地址,
    arr+0还是首元素的地址,
    是地址就是4/8个字节
	printf("%d\n", sizeof(*arr));//arr是首元素的地址,*arr就是首元素,
    大小就是1个字节
	//*arr -- arr[0] - *(arr+0)
	printf("%d\n", sizeof(arr[1]));//arr[1] 是第二个元素,大小也是1个字节
	printf("%d\n", sizeof(&arr));//&arr 是数组地址,数组的地址也是地址,
    大小是4/8个字节
	//&arr -- char (*)[6]
	printf("%d\n", sizeof(&arr + 1));//&arr+1, 跳过整个数组,指向了数组后边的空间,
    4/8个字节
	printf("%d\n", sizeof(&arr[0] + 1));//第二个元素的地址,是地址就是4/8字节

	return 0;
}

 X64环境下代码运行如下 :

 X86环境下代码运行如下 : 

代码2:

#include <stdio.h>
#include <string.h>

int main()
{
	char arr[] = { 'a','b','c','d','e','f' };
	printf("%zd\n", strlen(arr));//1.输出结果是什么呢?
	printf("%zd\n", strlen(arr + 0));//2.输出结果是什么呢?
	printf("%zd\n", strlen(*arr));//3.输出结果是什么呢?
	printf("%zd\n", strlen(arr[1]));//4.输出结果是什么呢?
	printf("%zd\n", strlen(&arr));//5.输出结果是什么呢?
	printf("%zd\n", strlen(&arr + 1));//6.输出结果是什么呢?
	printf("%zd\n", strlen(&arr[0] + 1));//7.输出结果是什么呢?
	return 0;
}

现在把sizeof换成strlen,那结果会发生什么变化呢? 

🌟🌟知识回顾:

strlen 是C语⾔库函数,功能是求字符串长度。函数原型如下:

size_t strlen ( const char * str );

统计的是从 strlen 函数的参数 str 中这个地址开始向后, \0 之前字符串中字符的个数。

strlen 函数会⼀直向后找 \0 字符,直到找到为⽌,所以可能存在越界查找。

使⽤时需要包含头⽂件 <string.h>

🌟大家可以思考思考,待会我们会统一讲解一下。

 

分析:

1.如图所示:


我们发现,这个arr数组它是没有\0的,也就是说当这个strlen函数拿到首元素字符a的地址,会一直往后找\0。
它有可能会在内存中的某个位置找到\0,然后统计\0之前的字符个数,因此我们是不确定它的值是多少,因此它是个随机值。

2.同样地,这个输出结果也是随机值,因为arr+0,意思就是首元素地址跳过0个元素,本质上还是首元素的地址。
所以strlen函数拿到的也是首元素a的地址,然后向后面找\0,但是arr数组里面没有\0,因此它本质上还是个随机值。

3. arr是首元素的地址,*arr是首元素,就是'a','a'的ascii码值是97,就相当于把97作为地址传递给了strlen,strlen得到的就是一个非法访问的地址, 代码是有问题的 

4.同样地,我们发现arr[1]就是数组的第二个元素。也就是传过去的是字符b,字符b的ASCLL码值是98,也就是传过去的地址是98。也会属于非法访问,所以它的输出结果同样也会出现报错。

5.我们发现&arr的类型是char (*p)[6],然后如果把它传到strlen函数里面的话,它的参数类型就强制转换为char *。站在strlen函数的角度,它依然拿到的是arr起始位置的地址,也就是从字符a向后数的,数到的结果还是随机值。所以它的结果跟第一道题和第二道题输出结果是一样的,都是随机值。

6.如图所示,我们发现,&arr取出的是数组的地址,而&arr+1跳过一个数组,指向的是字符f后面的地址,也就是从f后面的地址往后去找\0,所以从这里往后数,那个内存放什么,我们就更不知道了,但是这个地方得到的随机值跟前面那些随机值是不一样的,因为它们中间相差了6个字符。

7.如图所示,我们发现&arr[0]+1得到的是字符b的地址,然后把b的地址传给strlen函数,从b的位置一直往后数找\0,所以它这里的输出结果依然是随机值。只不过它的随机值跟第一题和第二题输出结果的随机值 少了个1。

x86环境运行结果:

x64环境运行结果: 

 我们从x64环境和x86环境中也知道,除了第三和第四题代码错误之外,

其他题的输出结果均为随机值。 说明我们的分析是正确的。 

代码3:

#include <stdio.h>
int main()
{

   char arr[] = "abcdef";
   printf("%zd\n", sizeof(arr));//1.输出结果是什么?
   printf("%zd\n", sizeof(arr + 0));//2.输出结果是什么?
   printf("%zd\n", sizeof(*arr));//3.输出结果是什么?
   printf("%zd\n", sizeof(arr[1]));//4.输出结果是什么?
   printf("%zd\n", sizeof(&arr));//5.输出结果是什么?
   printf("%zd\n", sizeof(&arr + 1));//6.输出结果是什么?
   printf("%zd\n", sizeof(&arr[0] + 1));//7.输出结果是什么?
   return 0;
}

 这里需要注意的是: 由于arr里面存放的是字符串,

我们要知道\0是字符串的结束标志,所以里面默认存放的是有\0的。

它的内存布局是这样的:

大家可以先思考这几道题,待会我们会统一讲解一下。 

分析:

1.我们知道sizeof(数组名)是计算整个数组的大小。
那这里数组里面有7个元素,而且该数组为char类型,所以每个元素是占一个字节,那这里就总共占了7个字节。
2.这里的arr+0 表示arr跳过0个元素,本质上还是首元素的地址,所以大小就是4/8个字节。
3.arr表示数组首元素的地址,*arr就是首元素,大小就是1字节
4.arr[1]是第二个元素,大小也是1字节。
5.&arr是数组的地址,但是也是地址,是地址大小就是4/8个字节。 

这里我们重点看一下第六题和第七题 

6.如图所示,我们发现&arr+1指向的是\0后面的地址,因此我们可以推导出&arr+1就是跳过整个数组的那个地址。大小为4/8个字节。

7.同样地,我们从上图发现&arr[0]+1指向的是数组中第二个元素的地址,所以它的大小为4/8个字节。 

通过我们的分析,我们用vs测试一下结果,看看分析的是否正确~

x86环境运行结果:

 x64环境运行结果

代码4:
 

int main()
{
	char arr[] = "abcdef";
	printf("%zd\n", strlen(arr));//1.输出结果是什么呢?
	printf("%zd\n", strlen(arr + 0));//2.输出结果是什么呢?
	printf("%zd\n", strlen(*arr));//3.输出结果是什么呢?
	printf("%zd\n", strlen(arr[1]));//4.输出结果是什么呢?
	printf("%zd\n", strlen(&arr));//5.输出结果是什么呢?
	printf("%zd\n", strlen(&arr + 1));//6.输出结果是什么呢?
	printf("%zd\n", strlen(&arr[0] + 1));//7.输出结果是什么呢?

	return 0;
}

大家不妨先思考这几道题,待会我们会统一讲解一下。 

分析:

1.我们发现strlen(arr) 本质上就是把首元素的地址a传给strlen函数,然后strlen就往后找\0,统计\0之前的个数,从上图,我们能直观地分析到\0之前有6个元素个数,因此它的输出结果是6。
2.同样地,我们发现arr+0 还是首元素跳过0个元素,本质上还是把首元素的地址传给strlen函数,统计\0之前的字符个数,所以它的输出结果依然是6。
3.这里我们前面已经介绍过,* arr就是数组首元素a,它的ASCII码值为97当做地址传给strlen函数,但是这个地址属于是非法访问,因此会出现报错情况。
4.同样地,arr[1]就是数组首元素a,它的Ascll码值为98当做地址传给strlen函数,但是这个地址属于是非法访问,因此会出现报错情况。
5.我们前面已经介绍过这里的&arr本质上它的类型就是char (* p)[7],当我们把这个数组地址传入strlen函数内部,会把它强转为char*类型。
站在strlen函数的角度,它依然拿到的是arr起始位置的地址,也就是从字符a的地址向后数的,直到遇到\0为止,所以它的输出结果也是6。

 6.如图所示,我们发现,&arr+1是指向\0的后面,因此它后面内存放的是什么,以及后面是否有\0,我们是不知道的。因此它的输出是个随机值。

7.如图所示,我们更能直观地发现,&arr[0]+1指向的是字符b的地址,当我们把字符b的地址传入strlen函数,
会统计字符b的地址到\0之间的字符个数。 所以它的输出结果就是5。 

由于我们发现第三题和第四题的结果输出是报错的,

我们先用注释把它屏蔽掉,再拿vs来运行此代码。 

用x64环境下运行此代码: 

 我们发现第六题输出结果是27,是个随机值。 

代码5:

#include <stdio.h>
int main()
{
	char* p = "abcdef";
	printf("%zd\n", sizeof(p));//1.输出结果是什么
	printf("%zd\n", sizeof(p + 1));//2.输出结果是什么
	printf("%zd\n", sizeof(*p));//3.输出结果是什么
	printf("%zd\n", sizeof(p[0]));//4.输出结果是什么
	printf("%zd\n", sizeof(&p));//5.输出结果是什么
	printf("%zd\n", sizeof(&p + 1));//6.输出结果是什么
	printf("%zd\n", sizeof(&p[0] + 1));//7.输出结果是什么

	return 0;
}

 这里我们要注意的是 char* p = "abcdef" 这段的代码所表达的含义

这里的"abcdef"本质上是一个常量字符串,这里的常量字符串的首字符的地址是放到指针变量p里面去。假设a的地址为0x0012ff40,那么p的地址放的也是0x0012ff40,正因为有这个地址才能找到它。

大家不妨先思考这几道题,待会我们会统一讲解一下。

分析:

1.我们发现:p是一个指针变量,大小是4/8字节。
2.由于我们知道p指向的是a的地址,那么p+1指向的是a的地址,假设p的地址是0xff12ff40,那么p+1指向的产生b的地址,也就是0xff12ff41–>‘b’,所以p+1是字符b的地址,大小同样是4/8个字节。
3.我们知道p指向的是a的地址,那* p指向的是字符a,类型是char *, 所以 * p是首字符,大小为1个字节。

4.这道题可能难倒了不少同学,对于p[0]可能理解比较困难

我们可以用两种方式来解读:

① 如图所示,我们发现,这个字符串画出来也像数组一样,连续放到内存空间里面,假设我们把这个常量字符串想象成一个数组,那它有下标,a的下标为0,b的下标为1,c的下标为2,以此类推,可以得知f的下标为5。数组名也是数组首元素的地址,那么p可以理解为后面这个数组名,那p[0]访问数组下标为0的元素。也就是字符a,同样也是占了一个字节。
② p[0]从计算的角度被转换为*(p+0)是这个a的地址,那(p+0)也是这个位置的地址。
那解引用不就是对这个a访问吗?也就是占了一个字节。

 

 5.我们知道p是一个指针变量,那么&p相当于是个二级指针,是p的地址,既然是地址,那地址大小就是4/8个字节。

6.&p + 1是指向p指针变量后面的空间,也是地址,是4/8个字节

7. p[0]是首字符a,那我们把它的地址取出来,就是a的地址,+1,那也就是b的地址。同样地,它的大小是占4/8个字节。 

我们用vs来测试一下,看看我们分析的结果是否正确?
我们分别以x64和x86环境来演示一下

x64环境:

x86环境:

 

代码6:

#include <stdio.h>
#include <string.h>

int main() {
	
	//字符数组3.2
	char* p = "abcdef";
	printf("%zd\n", strlen(p));//1.输出结果是什么
	printf("%zd\n", strlen(p + 1));//2.输出结果是什么
	printf("%zd\n", strlen(*p));//3.输出结果是什么
	printf("%zd\n", strlen(p[0]));//4.输出结果是什么
	printf("%zd\n", strlen(&p));//5.输出结果是什么
	printf("%zd\n", strlen(&p + 1));//6.输出结果是什么
	printf("%zd\n", strlen(&p[0] + 1));//7.输出结果是什么

	return 0;
}

它的内存布局如图所示: 

 

需要注意的是:这里的内存布局跟上个题目是一样的,同样指针变量p拿到的是字符串中首字符a的地址。

大家可以先思考或猜想一下这七道题的输出结果是什么,等下我们会进行细致的讲解~

分析:

 如下图所示:

1. 由于我们知道abcdef是个常量字符串,字符串中是有\0。
通过上图,p中存放的a的地址,然后从a的地址开始往后访问。所以它的字符串长度为6。
2. 同样的道理,因为我们已经知道p拿到的首元素a的地址,那么p+1相当于拿到的是字符b的地址,然后往后找\0。 

3. 因为p是拿到字符a的地址,那么 *p拿到的是a的字符。
但是我们前面已经讲过,它会把字符a的Ascll码值作为地址传给strlen。这就属于是非法访问了,因此会报错。
4. 同样地,我们之前讲过,因为p[0]== * (p+0)== * p,因为p拿到首字符a的地址,那(p+0)跳过0个元素同样也是拿到a的地址。
对其进行解引用也是拿到字符a,如果把a的ascll码值传入strlen函数同样也会报错。

 5.这道题估计很多同学会误以为它的字符串长度是6。
有很多同学可能以为是从的首元素a的地址往后数。但实际上并不是。

接下来我给大家解释一下:
如图所示:

它实际上是从p的内存空间往后数的,那p这个内存空间放什么我们知道吗?我们刚刚举的0x0012ff40这个地址我们都是假设的。
我们只是假设它放了这个地址,但是具体编译器里面分配了什么地址,我们是不知道的。当然,这里的地址也不是随机的,编译器一定会指派一个有效的地址,但是它地址的值在内存存的是什么,每个字节放的是什么,我们是不知道的,所以我们这里是不能确定答案的。
这里什么时候遇到\0,我们也是不可知的。

因此我们可以得出结论:

&p是p的地址,它是从p的所占空间起始位置开始查找的,因此它是个随机值。

6.从图中,我们可以看出&p指向的是p的内存空间起始位置,然后&p+1就相当于跳过一个p内存空间的地址,它就指向下一个内存空间的起始位置。
那从这个位置往后,它也是个内存空间啊,那它内存是什么?什么时候遇到\0,我们同样也是不知道的,那从这个位置往后数,这也是不可控的。因此它也是个随机值。

 

需要注意的是:
p所占的空间和字符串所占的空间根本就不是一回事,因为p有p的空间,字符串有字符串的空间,只不过我把字符串的首字符地址放在p里面去,所以这两块内存不一定是连续的,也可能是无关的,因此这是两个独立的空间。

7.p[0]是第一个元素,那&p[0]就是把它的地址取出来,就是a的地址,+1,就是从b的地址一直往后数,一直数到\0,所以它的长度是5。 

我们用vs来测试一下,看看我们分析的结果是否正确?
首先,先把分析为报错结果的第三题和第四题的代码先注释掉。

然后分别以x64和x86环境来运行一下此代码
x64环境:

如上图,我们发现第五题和第六题的运行结果分别为6和30。
这里可能有同学对于第五题的运行结果有所疑问,为什么在x64环境下 

printf("%zd\n", strlen(&p);

这行代码的运行结果是6,但其实它这个是随机值。为什么呢?
原因是这样的:

我们通过调试发现恰好那个\0刚好在内存空间p的第七个字节的位置,所以它的字符串长度恰好为6。


但是它跟字符串长度是没有任何关系的。
比如说,我们将它那个常量字符串改为10,它运行起来一样是为6的。

 x86环境:

同样地,我们拿x86环境下运行,第五题和第六题运行结果同样是一个随机值。 

2. 总结:

这里我们对三种不同类型的字符数组都进行了讲解,分别是:

1.多个字符来初始化一个字符数组的。这里面是没有\0。
2.一个字符串来初始一个数组的情况,这里面的字符串是包括了\0。
3.一个字符指针来指向一个常量字符串的情况,它里面虽然是有\0,但是p不是一个数组空间,它是一个变量空间,这个变量存放着p的地址。

另外,这里只是讲一部分的数组和指针的笔试题,还有一部分的笔试题留到下次的博客再讲解~

 如果对你有所帮助的话,欢迎大家一键三连支持,谢谢大家!

对于本次博客的内容,有疑问,不懂的地方,欢迎大家在评论区和我交流~

本期博客就讲到这里,下期再见~🫶

  • 10
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值