前言
大家好,本篇博客将为大家展示一些曾经考过的关于指针的经典笔试题,里面有些题目的难度还是不小的,所以希望大家可以认真理解;如果你点开了本篇博客,麻烦各位大佬一键三连,多多支持,感谢;OK,下面进入正文部分。
1. sizeof和strlen的对比
在讨论下面的题之前。我们需要先来看看sizeof和strlen的区别;
1.1 sizeof
在学习操作符的时候,我们学习了 sizeof , sizeof 计算变量所占内存内存空间大小的,单位是字节,如果操作数是类型的话,计算的是使用类型创建的变量所占内存空间的大小。 sizeof 只关注占⽤内存空间的大小,不在乎内存中存放什么数据。
#include <stdio.h>
int main()
{
char arr1[3] = {'a', 'b', 'c'};
char arr2[] = "abc";
printf("%d\n", strlen(arr1));
printf("%d\n", strlen(arr2));
printf("%d\n", sizeof(arr1));
printf("%d\n", sizeof(arr1));
return 0;
}
1.2 strlen
strlen函数是C语言库函数,用来求字符串的长度;关于strlen函数,它的函数原型如下
统计的是从strlen函数的参数str中这个地址开始向后, “\0”之前字符串中字符的个数。 strlen 函数会⼀直向后找“\0”字符,直到找到为止,所以可能存在越界查找。
#include <stdio.h>
int main()
{
char arr1[3] = {'a', 'b', 'c'};
char arr2[] = "abc";
printf("%d\n", strlen(arr1));
printf("%d\n", strlen(arr2));
printf("%d\n", sizeof(arr1));
printf("%d\n", sizeof(arr1));
return 0;
}
大家可以看到,上面有两个数组,那么arr1中是没有“\0”的。所以在求arr1的长度时,就会存在越界查找,最终得到的是一个随机整数。
上面这张图,展现了它们二者的区别,大家需要注意其中的一些注意事项。
在我们了解了它们二者的区别后,接下来我们来看一些题目
2. 数组和指针笔试题解析
2.1 一维数组
int a[] = { 1,2,3,4 };
printf("%d\n", sizeof(a));
printf("%d\n", sizeof(a + 0));
printf("%d\n", sizeof(*a));
printf("%d\n", sizeof(a + 1));
printf("%d\n", sizeof(a[1]));
printf("%d\n", sizeof(&a));
printf("%d\n", sizeof(*&a));
printf("%d\n", sizeof(&a + 1));
printf("%d\n", sizeof(&a[0]));
printf("%d\n", sizeof(&a[0] + 1));
大家先来看上面的代码,最好自己去做一下,再来看下面的解析;
这里我先来为大家解释这道题,后面为大家展示运行结果;
首先,这道题上来创建了一个一维数组;
第一行代码,是我们前面强调过的问题,sizeof数组名表示计算整个数组的大小,那么这是一个整型数组,每个元素的大小为4个字节,那么整个数组有4个元素,最终结果就应该为16;
第二行代码,大家注意sizeof后面不是数组名,那么就按照一般情况去,这里a就表示首元素地址,那么a+0还是表示首元素的地址,那么就相当于就一个地址的大小,假设我们是在X86环境下进行测试,那么结果就为4,单位是字节;
第三行代码,它对a进行了解引用操作,首先我们知道,这里的a表示首元素的地址,那么对首元素的地址解引用,得到的就是它指向的元素,观察数组,就是首元素1,那么这里相当于求1这个整形数据的大小,那么结果就为4了;
第四行代码,这里大家发现它和第二行比较相似,这里给出的是a+1,那这里a本来是首元素的地址,让指针+1,相当于向后移动一位,那么就指向了第二个元素,但是大家注意,这里并没有进行解引用操作,也就意味着这里还是一个地址,那么在X86的环境下,结果就是4个字节;
第五行代码,这个比较简单,a[1]就表示数组的第二个元素,即元素2,那么相当于求整型数据2的大小,那结果就为4字节;
第六行代码,这个也是前面重点强调过的问题,我们说过“&数组名”,表示取出整个数组的地址,在这道题里,其实不管它是什么地址,只要是地址,那么它的大小就只与编译环境有关,这里默认X86环境,那么结果就是4字节;
第七行代码,这里可能有同学存在疑问了,如果说&数组名是取出的是整个元素的地址,那么对它解引用是不是就得到数组的所有元素呢?其实答案就是可以得到数组中的所有元素,这个代码有两种理解方式;第一种理解方式:*和&相互抵消,那么就相当于sizeof(a),那结果就为16;第二种理解方式:我们知道&a表示取出整个元素的地址,那么它的类型就是int(*)[4],这是一个数组指针类型,那么对这个指针解引用时,就会得到整个数组的所有元素,那么结果同样也为16;
第八行代码,我们知道&a表示取出整个元素的地址,&a+1会跳过整个数组;但是大家注意,跳过一个数组后,它依然是一个地址,那么在X86环境下结果就为4字节;
第九行代码,这个大家应该很容易理解,a[0]就是首元素1,取出它的地址,那结果就是4字节(X86环境)
第十行代码,其实与第四行代码是等价的,最终得到了一个地址,大小为4字节;
上面给大家展示了运行的结果,这里是在X86环境下的编译结果,仅供参考;
2.2 字符数组
2.2.1 代码1
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));
这里我们再来看字符数组的题目;还是一样,大家最好先自己做一下,再来看下面的解析;
这道题上来先创建了一个字符数组;
第一行代码,大家可以看到,还是sizeof数组名的形式,求的是整个数组的大小,那么结果就不用多说,为6字节;
第二行代码,sizeof后不是数组名,那么求的就是一个地址的大小,结果为4字节(X86);
第三行代码,sizeof后不是数组名,那么*arr就表示首元素,也就是字符a,所以结果为1字节;
第四行代码,这个大家应该都会,就是数组的第二个元素,那么大小就是1字节;
第五行代码,“&数组名”表示取出整个数组的地址,那么它终究还是一个地址。它是一个数组指针。大小自然为4字节(X86);
第六行代码,这也是我们前面说过的问题,&arr表示取出整个数组的地址,当&arr+1时,我们说过,它会跳过整个数组,那么我们最终得到的还是一个地址,那么大小就是4字节(X86);
第七行代码,这个也比较简单,&arr[0]就表示首元素的地址,那么给它+1就表示第二个元素的地址,那么还是相当于求一个地址的大小,那结果还是4字节(X86);
上面就是代码的运行结果,这里是在X86环境下的编译结果,仅供参考;
2.2.2 代码2
char arr[] = { 'a','b','c','d','e','f' };
printf("%d\n", strlen(arr));
printf("%d\n", strlen(arr + 0));
printf("%d\n", strlen(*arr));
printf("%d\n", strlen(arr[1]));
printf("%d\n", strlen(&arr));
printf("%d\n", strlen(&arr + 1));
printf("%d\n", strlen(&arr[0] + 1));
下面咱们继续,现在把sizeof换成strlen,那这个结果会发生什么变化呢?
第一行代码,这里想求出这个字符数组的字符个数,但是大家要注意,这里没有“\0”,那么我们知道,strlen是遇到“\0”才会停下来,所以这里就会造成越界,所以我们会得到一个随机的整数;
第二行代码,这里其实和第一行代码是等价的,arr+0还是首元素地址,那么strlen从首元素向后数,没有遇到“\0”,所以结果还是随机的;
第三行代码,这里给的是*arr。那我们知道,arr表示首元素地址,那么*arr就是首元素,就是字符a,我们知道字符a的ASCII码值为97,所以相当于将97的地址传给了strlen,那么strlen将得到野指针,这个代码是存在问题的;
第四行代码,这里其实和第三行是一样的,将‘b’的ASCII码值98传给了strlen函数,那么还是会得到野指针,代码是有问题的;
第五行代码,&arr表示取出整个数组的地址,那么这个地址实际上就是首元素地址,那就相当于strlen从首元素开始向后数,但是因为没有”\0“。所以还是得到随机值;
第六行代码,和第五行代码类似,只不过是跳过整个数组后开始数,因为没有”\0“,所以结果还是随机值,但是这里的随机值是比第五行的随机值小6;
第七行代码,还是与上面类似,这个是从第二个元素开始数的,那么得到的随机值会比第五行的随机值小1;
下面为大家展示一下,在X86环境下的运行结果,为了不使程序崩溃,我会将那两行错误代码注释掉;
2.2.3 代码3
char arr[] = "abcdef";
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))
这里还是创建了一个字符数组,而这个数组是有“\0”的,所以在分析的时候要注意;
第一行代码,还是sizeof数组名的形式,那么求的就是整个数组的大小,那么这里大家注意,‘\0’也是一个字符,所以在计算的时候不能忘了它,那么最终的结果就是7字节;
第二行代码,sizeof后不是数组名,那么arr+0表示首元素地址,既然是地址,那就是4字节(X86);
第三行代码,sizeof后不是数组名,*arr就表示首元素,那么就是字符a,那么它是字符型变量,大小为1字节;
第四行代码,这里比较简单,arr[1]就是数组的第二个元素,也就是字符b,大小为1字节;
第五行代码,&arr表示取出整个数组的地址,实际上就是首元素地址,那么既然是地址,大小就是4字节(X86);
第六行代码,&arr+1要跳过整个数组,最终还是个地址,结果就是4字节(X86);
第七行代码,&arr[0]+1表示第二个元素的地址,那么它还是一个地址,答案就是4字节(X86);
下面我们还是将sizeof换成strlen,看看有什么变化;
2.2.4 代码4
char arr[] = "abcdef";
printf("%d\n", strlen(arr));
printf("%d\n", strlen(arr+0));
printf("%d\n", strlen(*arr));
printf("%d\n", strlen(arr[1]));
printf("%d\n", strlen(&arr));
printf("%d\n", strlen(&arr+1));
printf("%d\n", strlen(&arr[0]+1));
第一行代码,这里是求数组的元素个数,那么有“\0”的存在,strlen就能求出数组的元素个数,我们知道strlen是数到“\0”为止的,所以“\0”之前有6个元素,那么结果就是6;
第二行代码,这里其实和第一行代码是等价的,arr+0还是首元素地址,那么strlen从首元素向后数,遇到“\0”停下,那么结果还是6;
第三行代码,这里给的是*arr,那我们知道,arr表示首元素地址,那么*arr就是首元素,就是字符a,我们知道字符a的ASCII码值为97,所以相当于将97的地址传给了strlen,那么strlen将得到野指针,这个代码是存在问题的;
第四行代码,同第三行代码,代码本身存在错误;
第五行代码,&arr表示取出整个数组的地址,实际上就是首元素的地址,那么就是从首元素开始往后数,一共有6个元素;
第六行代码,这里大家注意&arr+1表示跳过整个数组,那么strlen将从跳过后的地址开始往后数,那后面是没有“\0”的,那么strlen就不知道在哪儿停下,所以结果将是一个随机整数;
第七行代码,我们知道&arr[0]是首元素地址,那么&arr[0]+1就是第二个元素的地址,所以strlen就会从第二个元素开始往后数,一直到“\0”为止,所以结果就为5;
下面我们来看运行结果;
OK,上面讨论了一些数组的题,下面我们来看指针的相关内容;
2.2.5 代码5
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));
这里大家注意,它用字符指针接受了字符串的地址,这个字符指针中存放的字符串首元素的地址,也就是‘a’的地址;
第一行代码,sizeof后面是一个指针变量,实际上就是个地址,那么地址的大小取决于编译环境,这里我默认X86环境,那么结果就是4字节;
第二行代码,p是首字符的地址,那么p+1就是第二个元素的地址,那么它还是一个地址,结果为4字节(X86);
第三行代码,p是数组名,表示首字符地址,那么*p就得到了首元素,所以就相当于sizeof(a),那么a是一个字符变量,那么结果就是1字节;
第四行代码,这个比较明显,p[0]就是首元素,所以和第三行等价,结果为1字节;
第五行代码,&p表示取出指针变量p本身的地址,那么它的形式其实就是二级指针,那二级指针本质上也是地址,所以大小就是4字节(X86);
第六行代码,&p+1就表示跳过p指针变量后的地址,那么是地址,大小就是4字节(X86);
第七行代码,&p[0]其实可以写成&*(p+0),那么就是首字符的地址,那么&p[0]+1就是第二个元素的地址,那么既然是地址,大小就是4字节(X86);
2.2.6 代码6
char *p = "abcdef";
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));
这里我们将上面的sizeof换成strlen,看看结果会发生什么变化;
第一行代码,我们知道p是首字符地址,那么将它传给strlen,strlen就会从首字符开始往后数,直到遇到“\0”,那么这行代码结果就是6;
第二行代码,p+1表示第二个字符的地址,那么从第二个字符开始往后数,得到的结果就是5;
第三行代码,*p表示的是首字符,那么就是将字符’a‘传给了strlen,我们知道字符a的ASCII码值为97,所以相当于将97的地址传给了strlen,那么strlen将得到野指针,这与上面说过的那个问题是一样的,这个代码是本身就是存在问题的;
第四行代码,p[0]其实就是*(p+0),那么得到的就是字符’a‘,这样与上面的情况就一样了,代码本身就有问题;
第五行代码,&p表示取出指针变量p的地址,那么这就与原本的字符串没啥关系了,将它传给strlen,那么就会从p本身的起始地址开始往后数,我们并不知道p本身存放的地址是什么,strlen就不知道在哪儿停,所以结果就是随机值;
第六行代码,本质上和第五行代码类似,结果也是随机的;
第七行代码,&p[0]相当于&*(p+0),那么得到的就是首字符地址,那么&p[0]+1就是第二个元素的地址,将它传给strlen,就从第二个元素开始往后数,那么结果就是5;
2.3 二维数组
上面说完了字符数组和一维数组,下面我们增加一下难度,来看看二维数组的考察;
int a[3][4] = {0};
printf("%d\n",sizeof(a));
printf("%d\n",sizeof(a[0][0]));
printf("%d\n",sizeof(a[0]));
printf("%d\n",sizeof(a[0]+1));
printf("%d\n",sizeof(*(a[0]+1)));
printf("%d\n",sizeof(a+1));
printf("%d\n",sizeof(*(a+1)));
printf("%d\n",sizeof(&a[0]+1));
printf("%d\n",sizeof(*(&a[0]+1)));
printf("%d\n",sizeof(*a));
printf("%d\n",sizeof(a[3]));
这里大家还是最好先自己做一遍,然后再来看解析;
第一行代码,sizeof数组名求整个数组的大小,那么这个二维数组3行4列,一共有12个整型元素,那么结果就是48字节;
第二行代码,这个是常规题目了,a[0][0]表示第一行第一列的元素,在这儿就是0,那么0是整型,大小就是4字节;
第三行代码,我们知道二维数组是由一维数组构成的,a[0]本质上就是第一行的一维数组的数组名,那么这里就是sizeof数组名的表达形式,求的是第一行一维数组的大小,第一行有4个整型元素,那么结果就是16字节;
第四行代码,这里大家注意,数组名没有单独放在sizeof内部,所以它表示数组首元素的地址,大家注意这里说到的数组是第一行的一维数组,其实就是&a[0][0],那么+1后就是&a[0][1],那么既然是地址,结果就为4字节(X86);
第五行代码,根据第四行代码,我们知道这里就是元素a[0][1],大小就是4字节;
第六行代码,这里大家注意,a作为数组名没有单独放在sizeof内部,那么它就表示这个二维数组的首元素地址,也就是第一行的地址,那么a+1跳过一行,得到的是第二行的地址,它是一个数组指针int(*)[4],既然是地址,结果就是4字节(X86);
第七行代码,这里有两种理解方式;第一种理解:上面刚说a+1是数组指针,那么对其解引用,得到的就是第二行的一维数组,那么这就相当于求第二行的大小,那么结果就是16字节;第二种理解方法:我们知道,*(a+1)可以等价为a[1],而a[1]就是第二行的数组名,那么这里就是sizeof数组名的形式,这样还是在求第二行的大小。结果殊途同归,还是16字节;
第八行代码,这里大家看到,a[0]是第一行的数组名,&a[0]就表示取出第一行的地址,那么&a[0]+1就是第二行的地址,结果就是4字节(X86);这里大家可以发现,这其实和第六行代码是同理的;
第九行代码,上面说了,&a[0]+1是第二行的地址,那么对其解引用就得到了第二行,第二行的大小是16字节;
第十行代码,这里数组名a 没有单独放在sizeof内部,那么就表示首元素地址,本质就是第一行的地址,所以对其解引用得到第一行,计算的就是第一行的大小,为16字节;这里大家需要知道这个等价关系:*a == *(a+0) == a[0],a[0]就是第一行的数组名,单独放在sizeof内部,,求的就是第一行的大小;
第十一行代码,这里可能有同学有疑问了,这个二维数组一共就三行呀,a[3]表示第四行了,这是不是越界访问了?那么事实上这里是不存在越界访问的问题的,大家要知道sizeof后的表达式是不计算的,sizeof是根据表达式的类型去计算大小的,所以我们只需要知道a[3]的类型就OK了,那么a[3]是第四行的数组名,单独放在sizeof内部,计算的就是第四行的大小,为16字节;
3. 指针运算笔试题解析
3.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;
}
大家可以先自己思考一下,上面的代码运行结果是什么;
这道题是比较简单的,这里创建了指针ptr来接受地址,这个地址是&a+1,是从首元素地址跳过整个数组后的地址;打印代码中,a+1就是第二个元素的地址,*(a+1)就是第二个元素,ptr-1是数组最后一个元素的地址,*(ptr-1)就得到元素5;
3.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);
printf("%p\n", (unsigned long)p + 0x1);
printf("%p\n", (unsigned int*)p + 0x1);
return 0;
}
这道题大家乍一看挺复杂的,但是实际上就是在考察指针+-整数,有的同学可能会被结构体的内容唬住,但其实我们不需要去关注结构体的内容,题目已经提示了结构体的大小为20字节;上来创建了一个结构体指针,它接受的地址是0x100000;前面我们学过整型指针+1跳过4字节,字符指针+1跳过1字节,那么这里大家类比一下,结构体指针+1就应该跳过一个结构体的大小,那么在本题中就是跳过20字节;那么打印代码中,第一行,0x1是十六进制的表示方法,本质就是1,那么p+1就需要跳过20字节,那么得到的地址用十六进制表示为0x100014,这里我们使用"%p"打印,需要补齐一个地址的长度,结果就是00100014;下面再看第二行,这里使用了强制类型转换,将结构体指针p强制转换为unsigned long类型,那么这个时候给p+1,就是给一个整型+1,那么结果就是00100001;第三行代码,这里是将p强制转换为一个整型指针,那么p+1就要跳过一个整型的大小,也就是4字节,结果就是00100004;
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;
}
这道题代码比较少,但是也有坑,主要的坑就在第一行,数组里逗号表达式,这里大家要回想起来:逗号表达式的结果是最后一个表达式的结果;所以这个数组的内容就是{1,3,5},这一点大家一定要清楚,再往下就比较简单了,创建了一个整型指针来接受数组第一行的地址,那么p[0]其实就是*(p+0),就是*p,那么*p在这里就是a[0][0],就是元素1,所以打印的结果是1;
3.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;
}
这道题稍微有些复杂,这里用一张图为大家解释,大家请看;
大家仔细看这张图,我们知道“指针-指针”得到的是两个指针之间的元素个数,那么通过上面的图,大家可以看到两个指针相差4个元素,但是大家注意,这里是低地址减高地址,所以结果是-4。这是“%d”的打印结果;那么“%p”的打印结果是什么呢,其实打印的就是-4的补码(16进制),那么结果就是FFFFFFFC;
本题主要考察的是对数组指针的理解,大家需要注意;
3.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;
}
这道题和第一题类似,但是难度要高于第一题;我们来看,这里创建了一个二维数组,&aa表示取出整个数组的地址,它的类型是int(*)[2][5],那么&aa+1就要跳过整个数组;所以ptr1-1就是最后一个元素的地址,那么对其解引用就得到10;下面继续看ptr2,我们知道*(aa+1)等价于aa[1],那么a[1]就是第二行的地址,那么ptr2就是元素6的地址,这里大家需要注意前面的(int*)进行了强制类型转换,本来它是一个数组指针,现在转换成了整型指针,所以ptr2-1就是元素5的地址,对其解引用得到元素5;
3.6 题目六
#include <stdio.h>
int main()
{
char *a[] = {"work","at","alibaba"};
char**pa = a;
pa++;
printf("%s\n", *pa);
return 0;
}
这道题考察指针数组和二级指针,首先上来创建了一个字符指针数组,里面存了三个字符串的首字符地址,下面创建了一个二级指针来存放一级指针的地址,这个一级指针是数组名,表示的是首元素地址,大家来看下面的解析图;
4. 总结
本篇博客为大家展示了一些关于数组和指针的笔试题,内容稍微有些多,但是希望大家可以认真对待每一道题,这些题目对数组和指针的理解很关键,而且后面如果你要参加笔试,本篇博客的内容会对你有帮助,最后希望本篇内容可以让大家深入理解并掌握数组和指针的内容,感谢阅读!