携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第12天,点击查看活动详情
前言
在学习了数组和指针后,有可能会认为就这就这,实际上目前也只是学了,跟熟练掌握还差的远呢,需要不断练习来熟悉。墙裂建议还没弄懂数组和指针初阶、进阶的读者先去补一补对应知识,不然可能有些地方会一脸懵逼。
本文就来分享一波作者的C指针刷题心得见解。本篇属于笔试面试真题刷题篇第二篇,也是完结篇。
笔者水平有限,难免存在纰漏,欢迎指正交流。
二维数组
题目汇总
先看看题目,想一想再看后面的讲解。
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]));
讲解
printf("%d\n",sizeof(a));
一个道理,数组名单独放入sizeof中,代表整个数组,数组a含有12个int元素,计算得到48。
printf("%d\n",sizeof(a[0][0]));
数组第一个元素,类型为int,计算得到4。
printf("%d\n",sizeof(a[0]));
首先,我们在《指针进阶》博文中讲过,高维数组如n维数组,实际上是以n-1维数组维元素的一维数组,比如二维数组其实就是一维数组为元素的一维数组,这里的a[0]实际上就是一个一维数组数组名。
如图,逻辑上分行分列模拟二维数组(实际存储时不是这样的)
a[0]就是第一行数组的数组名,单独放在sizeof中,代表第一行数组,计算得到16。
printf("%d\n",sizeof(a[0]+1));
这里a[0]代表第一行数组首元素地址,也就是&a[0][0],加个1就是向后偏移一位,也就是&a[0][1],是指针,计算得到4或8。
printf("%d\n",sizeof(*(a[0]+1)));
就相当于*(&a[0][1]),即a[0][1],计算得到4。
printf("%d\n",sizeof(a+1));
a这里代表首元素地址,也就是数组&a[0],再加个1就是&a[1],是指针,计算得到4或8。
printf("%d\n",sizeof(*(a+1)));
相当于*&a[1]也就是a[1],是第二行数组数组名,单独出现在sizeof中代表整个数组,计算得到16。
printf("%d\n",sizeof(&a[0]+1));
取出的是第一行数组的地址,再加1就相当于&a[1],是指针,计算得到4或8。
printf("%d\n",sizeof(*(&a[0]+1)));
相当于*&a[1]即a[1],是第二行数组数组名,单独出现在sizeof中代表整个数组,计算得到16。
printf("%d\n",sizeof(*a));
a代表&a[0],*a就相当于
*&a[0],也就是a[0],是第1行数组数组名,单独出现在sizeof中代表整个数组,计算得到16。
printf("%d\n",sizeof(a[3]));
看起来a[3]不是越界访问了吗,是不是会报警告呀?
还记得前面梳理过的sizeof和strlen区别吗?
sizeof只关注对应类型的变量占用内存空间的大小,而不在乎内存中放的是啥,并且在sizeof内并不会真的去访问a[3]。你看嘛,a[3]是不是相当于*(a+3),这是指针偏移后再解引用访问,a代表首元素地址也就对应的是int(*)[4]类型,a+3就是向高地址移动跳过三个int[4],而我们创建的二维数组也就只有三个int[4],这一跳就跳到了它后面,本来它后面的空间并没有分配给我们,我们是不应该也是不能访问的,可是架不住指针的权限大呀,间接访问还不行吗,越界是越界,但是通过指针是可以访问的。
不过呢,可以访问但是sizeof并不会去访问,而是根据a[3]对应的类型计算这样类型的变量要占多大内存空间,a[3]相当于“第四行”数组的数组名,虽然不存在,可以假设其存在,因此计算得到16。
指针笔试题
第一题
题目
int main()
{
int a[5] = { 1, 2, 3, 4, 5 };
int *ptr = (int *)(&a + 1);
printf( "%d,%d", *(a + 1), *(ptr - 1));
return 0;
}
讲解
&a取出整个数组地址,对应指针类型为
int(*)[5],加个1跳过数组a指向数组后面一位,强转成int*后赋给了ptr,ptr-1就是向低地址方向移动一个单位,此时单位为4字节,正好跳过一个int型变量。a代表首元素地址,加个1就是第二个元素的地址,指向第二个元素。打印结果为2,5。如图所示:
第二题
题目
struct Test
{
int Num;
char *pcName;
short sDate;
char cha[2];
short sBa[4];
}*p;
//假设p 的值为0x100000。 如下表表达式的值分别为多少?
//已知,当下系统为x86(32位),结构体Test类型的变量大小是20个字节
int main()
{
printf("%p\n", p + 0x1);
printf("%p\n", (unsigned long)p + 0x1);
printf("%p\n", (unsigned int*)p + 0x1);
return 0;
}
讲解
0x1其实就是1,回顾一下前面讲的指针+-整数运算,步长与数据类型有关,如果类型为结构体指针struct Test*,其步长就为20,如果类型为unsigned int*,其步长就为4,所以第一个和第三个的打印结果就显而易见了:0x100014,0x100004。注意啦,第二个是强转成unsigned long类型,是整数类型,单位变成1了,所以结果就为0x100001。
第三题
题目
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;
}
讲解
&a取出整个数组地址,加个1跳到后面一位去了,再强转成int赋给了ptr1,ptr1[-1]实际上就是(ptr1-1),也就是向前跳了一个int变量后解引用,就得到4,打印是以十六进制显示,所以是0x00000004。
a代表数组首元素地址,类型为int*,但是它被强转成了int型,此时+1的单位就是1,也就是只向后移动了一个字节,那我们一个字节一个字节来看,首先我们使用的机器用的是小端存储,倒着存倒着取,如图所示,看图就很清晰啦,得到的就是0x02000000。
小端存储不清楚的可以去这篇博文了解一下[深入浅出C语言]浅析整型 - 掘金 (juejin.cn)
第四题
题目
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。a[0]作为第一行数组数组名,代表首元素地址即&a[0][0],p[0]相当于*(p+0),也就是*p,即
*&a[0][0],最后就是a[0][0]值为1。
第五题
题目
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;
}
讲解
a是二维数组数组名,代表首元素地址也就是a[0]的地址,指针类型为int(*)[5],用p去接收a其实类型不兼容,但是可以把值放到p去,只是要访问时会有所差异。
如图所示,指针相减得到间隔元素数量,小减大得负数,所以是-4,只是要注意打印用的转换说明不同,%p打印十六进制地址而%d打印十进制整数,-4对应补码为11111111111111111111111111111100,对应十六进制为0xfffffffc,而且地址没有原反补码之说,直接取出值打印即可。打印结果就是0xfffffffc,-4。
第六题
题目
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取到整个数组地址,加个1就跳到数组后面一位,强转成int*后赋值给ptr1,ptr1-1就向前跳过一个int变量,解引用就得到10。
aa代表二维数组首元素地址,也就是&aa[0],加个1向后就跳过一个int[5]数组,即&aa[1],解引用就是aa[1],代表其首元素地址即&aa[1][0],强不强转都是int*类型,赋给了ptr2,ptr2-1就向前跳过了一个int变量,再解引用就得到5。
第七题
题目
int main()
{
char *a[] = {"work","at","alibaba"};
char**pa = a;
pa++;
printf("%s\n", *pa);
return 0;
}
讲解
这种题画图很重要,先把图画出来,剩下的就很容易观察分析了。
解引用后是指向字符串"at"的指针,用%s作为转换说明来打印就得到"at"。
第八题
题目
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;
}
讲解
先画一画大体的图:
看第一个, ++cpp就是&cp[1],解引用一次得到cp[1],再解引用cp[1]即(c+2)即c[2],按%s一打印得到"POINT"。
看第二个,一步一步来,先是*++cpp即cp[2]即c+1,再--让cp[2]变成了c,解引用得到c[0],指向对应字符串首字符,再+3向后移动三位,再打印字符串就打印剩下的字符了,得到"ER"。
看第三个,cpp[-2]即*(cpp-2)即c+3,再解引用得到c[3],指向对应字符串首字符,再+3向后移动三位,再打印字符串就打印剩下的字符了,得到"ST"。
看第四个,cpp[-1][-1]即*(*(cpp-1)-1)得到c[1],指向对应字符串首字符,再+1向后移动一位,再打印字符串就打印剩下的字符了,得到"EW"。
说实话这第八题我觉得是这里出现的最难的题了,能把这样的题给弄透彻的话,其实指针和数组这一块基本上就没什么大问题了。
以上就是本文全部内容了,感谢观看,你的支持就是对我最大的鼓励~