目录
前言
笔试题1
笔试题2
补充
笔试题3
笔试题4
笔试题5
笔试题6
笔试题7
笔试题8
总结
前言
指针往往是变幻莫测,灵活多变,只有深入理解其本质才能有深刻的体会。
下面通过八道经典笔试题,来一起深入理解指针的神奇之处。
笔试题1
程序运行的结果是什么呢?
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 + 1) - 解释:
就不再过多解释。
*(ptr - 1) - 解释·:
&a表示取整个数组的地址,对其加一跳过整个数组,指针指向了红色箭头指向的位置,但是这个指针又被强制类型转换成了整形(int*)类型的指针,并存放在ptr这个整形指针变量里面去了。
此时的ptr指针就是一个整形指针,对其减一就是向前跳动一个整形的地址指向了蓝色箭头的位置,*(ptr - 1)是以整形指针的视角去读取所指空间的内容,也就是从蓝色箭头的指针处向后访问一个整形(4个字节)的长度,所以读到的就是5。
笔试题2
程序运行的结果是什么呢?
备注:
//p是结构体指针
//假设p 的值为0x100000。 如下表表达式的值分别为多少?
//已知,结构体Test类型的变量大小是20个字节struct Test { int Num; char* pcName; short sDate; char cha[2]; short sBa[4]; }*p; int main() { p = (struct Test*)0x100000; printf("%p\n", p + 0x1); printf("%p\n", (unsigned long)p + 0x1); printf("%p\n", (unsigned int*)p + 0x1); return 0; }
公布答案:
为什么会这样呢?
详细解释如下:
计算机内存十一字节划分的,一个字节都有一个对应的地址,地址是由16进制来表示的。
printf("%p\n", p + 0x1); - 解释:
p = (struct Test*)0x100000;
是将0x100000 强制类型转换成(struct Test*)这个结构体的地址。
而我们知道指针的类型决定了指针加减整数的时候跳过的字节数;
如:int*类型的指针 + 1 跳过4个字节,
char* 类型的指针 + 1跳过1个字节,
所以结构体类型的指针 + 1 就跳过一个结构体。
因为这个结构体的大小为20个字节,+1之后指针就向后跳20个字节,20的16进制是14
所以结果就为00100014。
printf("%p\n", (unsigned long)p + 0x1); - 解释:
(unsigned long)p 是将这个结构体的地址强制类型转换成无符号整形
整形数字 + 1就是+1,所以结果是00100001。
printf("%p\n", (unsigned int*)p + 0x1); - 解释:
(unsigned int*)p 是将p强制类型转换成整形指针,整形指针+1就是跳过四个字节,
所以结果是00100004。
补充
大小端存储:
全称:大端字节序存储 小端字节序存储
大端字节序存储:
当一个数据的低字节的数据存放在高地址处,高字节的内容放在了低地址处
这样的存储方式就是大端字节序的存储。
小端字节序存储:
当一个数据的低字节的数据存放在低地址处,高字节的内容放在了高地址处
这样的存储方式就是小端字节序的存储。
写一个小程序来判断当前机器的字节序是大端还是小端:
int check_sys() { int a = 1; return *((char*)&a);//拿到的是低地址 } int main() { int ret = check_sys();//返回1是小端,返回0是大端 if (1 == ret) { printf("小端\n"); //低字节放在了低地址处 } else if (0 == ret) { printf("大端\n"); //高字节放在了低地址处 } return 0; }
临时变量存放在栈区,栈区使用地址的习惯是从高地址到低地址。
32位平台下,1的存储。
二进制:00000000000000000000000000000001
十六进制:00 00 00 01
char* p = (char*)&a; - 将整形指针强制类型转换成字符指针。
*p;访问第一个字节(低字节向高字节读取), 所以 *p 就拿到了低字节的存储数据。小端存储示意图:
大端存储示意图:
笔试题3
程序运行的结果是什么呢?
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; }
公布答案:
为什么会这样呢?
详细解释如下:
注意:本题的输出方式为十六进制输出。
ptr1[-1] - 解释:
与笔试1类似,不再赘述。
*ptr2 - 解释:
a是数组首元素的地址,是十六进制的,强制类型转换成整形(int)之后再 + 1
再强转成整形指针,也就是在上几部操作后,a的地址数值加了1,地址加1
就是向后偏移了一个字节。在VS的环境下:存储方式为小端存储
ptr从 01 指向了后一个字节 00。(int*)((int)a + 1) 再将其强转成整形指针,再向后访问四个字节并以十六进制的方式输出,
所以结果是:2 00 00 00。
以什么形式存储,就以什么形式输出,以小端存储方式存入,就以小端的方式输出。
笔试题4
程序运行的结果是什么呢?
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 0
0 0所以p[0]就为第一个元素1。
笔试题5
程序运行的结果是什么呢?
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; }
公布答案:
为什么会这样呢?
详细解释如下:
p是一个数组指针,p能够指向的数组是4个元素,
a 为二维数组的数组名,二维数组的数组名表示二维数组第一行的地址。
同时二维数组可以理解为一位数组组成的数组。
p = a;
那么p + 1是向后跳了四个整形(int)16个字节。
详解图:
a[4][2]的地址比p[4][2]的地址高4,所以结果为-4;
计算机存数据是以补码的形式存储:
-4的存储
100000000 00000000 00000000 00000100 - 原码
111111111 11111111 11111111 11111011 - 反码
111111111 11111111 11111111 11111100 - 补码
以%p的形式输出,就是以16进制的形式输出,结果为:FF FF FF FC。
笔试题6
程序运行的结果是什么呢?
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是跳过整个二维数组。
可以自己尝试解决。
笔试题7
程序运行的结果是什么呢?
int main() { char* a[] = { "work","at","alibaba" }; char** pa = a; pa++; printf("%s\n", *pa); return 0; }
公布答案:
为什么会这样呢?
详细解释如下:
注意:
形如 char* p = "abcdefg";这样的p为字符指针,存放的是字符串首元素的地址。同时它是 只 读 的 ,是不能被修改的。如果 *p = ‘w’;这样操作的话,编译器报警告。
(访问权限冲突)
a[]就是一个指针数组,里面存放的都是字符指针。
a是数组名是数组首元素的指针,char** pa;说明pa是个指针,这个指针指向的类型是个字符指针。
pa++;就是相当于pa + 1,首元素向后跳动一个字符指针,便是"at"这个字符串的首地址。
笔试题8
程序运行的结果是什么呢?
int main() { char* c[] = { "ENTER","NEW","POINT","FIRST" }; char** cp[] = { c + 3,c + 2,c + 1,c }; char*** cpp = cp; printf("%s\n", **++cpp);//*cp[1] -> *(c + 2) -> c[2] printf("%s\n", *-- * ++cpp + 3); printf("%s\n", *cpp[-2] + 3);//* 的优先级比 +的优先级第 printf("%s\n", cpp[-1][-1] + 1); return 0; }
公布答案:
为什么会这样呢?
详细解释如下:
一开始布局:
c中放的是字符串首地址。
cp中放的是c中元素得地址。
cpp放的是cp首元素地址。
**++cpp - 解释:
补充:操作符的优先级:++ 高于 * 高于 + 高于 -
先对cpp自增一次,此时cpp指向了(c + 2)的位置,一次解引用*操作就拿到了cp数组中的(c + 2),再次解引用*操作就拿到了c数组中第3个元素,第三个是个元素是字符指针,这个字符指针存放的是POINT这个字符串的首地址,%s拿到了POINT首地址向后打印,所以结果为POINT。
*-- * ++cpp + 3 - 解释:
cpp现在原只向的(c + 2)位置上自增1,此时指向了(c + 1)的位置上,解引用*一次便拿到了(c + 1),因为 -- 的优先级比+高,先执行 -- ,(c + 1)自减变成了c,再解引*用一次,就拿到c[]数组的第一个元素,第一个元素存的是ENTER的首地址,再对首地址 + 3,跳过三个字符,指向了E。%s拿到了E的地址向后打印,结果就是ER。
*cpp[-2] + 3 - 解释:
*cpp[-2] + 3 <==> *(*(cpp - 2)) + 3,cpp在指向原来的(c + 1)位置,而(cpp - 2)向前跳动两个char**的元素指向了(c + 3)的位置,但是cpp本身的值没变,这时对(cpp - 2)解引用*一次就拿到了(c + 3),再对(c + 3)解引用*一次就拿到了c[]数组中的最后一个元素,c[]数组中的最后一个元素存的是FIRST字符串的首地址,对其+3就是跳过三个字符,指向了S。%s拿到了S的地址向后打印,结果就是ST。
cpp[-1][-1] + 1 - 解释:cpp[-1][-1] + 1 <==> *(*(cpp - 1) - 1) + 1,cpp在指向原来的(c + 1)位置,(cpp - 1)向前跳动一个char**的元素指向了(c + 2)的位置,但是cpp本身的值没变,这时对(cpp - 1)解引用*一次就
拿到了(c + 2),再对(c + 2) - 1得到了(c + 1),再对(c + 1)解引用*一次就拿到了c[]数组中的第二个元素,c[]数组中的第二个元素存的是NEW这个字符串的首地址,对其 +1就是跳过一个字符,指向了E。%s拿到了E的地址向后打印,结果就是EW。
总结
指针往往比较灵活且多变,空想很难有想出答案,最好的方法就是:画图。
结合题目的要求找到对应关系,当把所有的关系理清时,会有一种拨开迷雾见青山的快感。