0


【C语言】 - 指针进阶 - 经典笔试八题


前言

指针往往是变幻莫测,灵活多变,只有深入理解其本质才能有深刻的体会。

下面通过八道经典笔试题,来一起深入理解指针的神奇之处。


笔试题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。


总结

指针往往比较灵活且多变,空想很难有想出答案,最好的方法就是:画图。

结合题目的要求找到对应关系,当把所有的关系理清时,会有一种拨开迷雾见青山的快感。

标签: c语言

本文转载自: https://blog.csdn.net/m0_63059866/article/details/123381610
版权归原作者 yy_上上谦 所有, 如有侵权,请联系我们删除。

“【C语言】 - 指针进阶 - 经典笔试八题”的评论:

还没有评论