带你刷笔试关的小怪|详解指针习题和面试题【C语言/指针/进阶】

前言

通过前面的学习,已经对各类指针有所了解,要想掌握C的利器——指针,做题能让我们快速掌握指针的用法。下面将详解指针习题(作为复习)和指针面试题(重点)。
友情链接:详解指针【上】 详解指针【中】 详解指针【下】(时间紧的话就看上篇吧,思路解析是最详细的)

9. 指针和数组笔试题解析

复习回顾

  1. sizeof()计算的是对象所占内存的大小(单位:字节),而与它指向的东西无关。即sizeof(指针)就是指针的大小,和它指向的东西的大小无关
  2. 数组名一般指首元素地址,它表示为整个数组有两种情况:① sizeof(数组名)、② &数组名。对于第一种情况,括号内只能是数组名才表示整个数组。

一维数组

//一维数组
int a[] = {1,2,3,4};
printf("%d\n",sizeof(a));//4*4=16 ①

printf("%d\n",sizeof(a+0));//4/8(32/64位)
//a+0是第一个元素的地址,地址大小是4/8字节
printf("%d\n",sizeof(*a));//4
//a是首元素地址,*a是第一个元素的大小
printf("%d\n",sizeof(a+1));//4/8(32/64位)
//同二
printf("%d\n",sizeof(a[1]));//4
//同三
printf("%d\n",sizeof(&a));//4/8 ② 不要认为是16
//*****&数组名也是一个地址,地址大小是4/8字节*****//
printf("%d\n",sizeof(*&a));//16 ①
//对地址解引用,得到整个地址,*和&在一起有抵消的效果
//此时*&a和a等价,本语句和第一条等价
printf("%d\n",sizeof(&a+1));//4/8 ② 不要认为是16
//*****&a是数组地址,对地址加1,跳过一个数组,还是地址,只是不指向a数组了
//地址大小是4/8字节*****//
printf("%d\n",sizeof(&a[0]));//4/8 下标为0的元素的地址
printf("%d\n",sizeof(&a[0]+1));//4/8 下标为2的元素的地址

字符数组

char arr[] = {'a','b','c','d','e','f'};
printf("%d\n", sizeof(arr));//6
printf("%d\n", sizeof(arr+0));//4/8 下标为0的地址
printf("%d\n", sizeof(*arr));//1 首元素的大小
printf("%d\n", sizeof(arr[1]));//1 下标为1的元素的大小
printf("%d\n", sizeof(&arr));//4/8 数组地址
printf("%d\n", sizeof(&arr+1));//4/8 跳过整个数组地址后的地址
printf("%d\n", sizeof(&arr[0]+1));//4/8 下标为1的元素的地址
char arr[] = {'a','b','c','d','e','f'};// a, b, c, d, e ,f
printf("%d\n", strlen(arr));//随机值 因为strlen()遇到'\0'才停止
printf("%d\n", strlen(arr+0));//同上
printf("%d\n", strlen(*arr));//*arr是'a',strlen()认为
//它的ASCII值97是一个地址,这个地址系统不会分配给用户,造成非法访问内存
printf("%d\n", strlen(arr[1]));//同上
printf("%d\n", strlen(&arr));//随机值 同一
printf("%d\n", strlen(&arr+1));//随机值 地址跳过了一整个数组
printf("%d\n", strlen(&arr[0]+1));//随机值 地址跳过了一个元素

弄懂了前面的代码,试着分析一下吧:

//字符串常量以字符数组的形式存放在内存中
char arr[] = "abcdef";//a,b,c,d,e,f,\0
printf("%d\n", sizeof(arr));//7
printf("%d\n", sizeof(arr+0));//4/8
printf("%d\n", sizeof(*arr));//1
printf("%d\n", sizeof(arr[1]));//1
printf("%d\n", sizeof(&arr));//4/8
printf("%d\n", sizeof(&arr+1));//4/8
printf("%d\n", sizeof(&arr[0]+1));//4/8

printf("%d\n", strlen(arr));//6
printf("%d\n", strlen(arr+0));//6
printf("%d\n", strlen(*arr));//错误
printf("%d\n", strlen(arr[1]));//错误
printf("%d\n", strlen(&arr));//6
printf("%d\n", strlen(&arr+1));//随机值 跳过了整个数组,跳过了'\0'
printf("%d\n", strlen(&arr[0]+1));//5 从第二个元素开始

体会

  1. 操作符sizeof()只和它的对象的内存大小有关,和对象指向的东西无关
  2. 库函数strlen(),求字符串长度只能将地址传入,统计'\0'之前的元素个数
//字符串常量以字符数组的形式存放在内存中
char *p = "abcdef";//指针变量p
printf("%d\n", sizeof(p));//4/8 指针变量大小
printf("%d\n", sizeof(p+1));//4/8 'b'的地址
printf("%d\n", sizeof(*p));//1 'a'
printf("%d\n", sizeof(p[0]));//1 'a'
printf("%d\n", sizeof(&p));//4/8 指针变量p的地址
printf("%d\n", sizeof(&p+1));//4/8 跳过一个数组的地址
printf("%d\n", sizeof(&p[0]+1));//4/8 'b'的地址

printf("%d\n", strlen(p));//6
printf("%d\n", strlen(p+1));//5
printf("%d\n", strlen(*p));//错误
printf("%d\n", strlen(p[0]));//错误
printf("%d\n", strlen(&p));//**随机值 指针变量p的地址,和字符串常量无关**
printf("%d\n", strlen(&p+1));//随机值
printf("%d\n", strlen(&p[0]+1));//5

二维数组

将二维数组看成若干一维数组组成

int a[3][4] = {0};
printf("%d\n",sizeof(a));//48 整个数组
printf("%d\n",sizeof(a[0][0]));//4/8 元素大小
printf("%d\n",sizeof(a[0]));//16 第一行的所有元素
printf("%d\n",sizeof(a[0]+1));//4/8 **a[0]作为第一行的数组名,不符合①的形式**
**//也没有取它的地址,所以a[0]是第一行首元素地址,+1就是第二行元素的地址**
printf("%d\n",sizeof(*(a[0]+1)));//4/8 第一行第二个元素
printf("%d\n",sizeof(a+1));//4/8 **第二行的地址**
printf("%d\n",sizeof(*(a+1)));//16 **第二行的元素**
printf("%d\n",sizeof(&a[0]+1));//4/8 第二行的地址 // &和[]有抵消作用
printf("%d\n",sizeof(*(&a[0]+1)));//16 **第二行的元素**
printf("%d\n",sizeof(*a));//16 第一行
printf("%d\n",sizeof(a[3]));//16

sizeof(a[3]) 的理解(这里数组越界了)

对于sizeof()操作符,只是求对象所占内存的大小,并没有访问它的内存,而是根据类型得出结果的。也就是说,sizeof()求的是对象的数据类型的大小。如:

int a = 0;
int sz = sizeof(a);
int SZ = sizeof(int);
//这两种写法是等价的

对于sizeof(a[3])a[3]的数据类型是int[4],所以即使越界了(其实访问内存才算越界,这里只是强调),也不影响sizeof()计算它的大小。这和strlen()是不一样的,后者有访问内存,编译器可能会报错。

10. 指针笔试题

笔试题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;
}
//程序的结果是什么?

解读

  1. 对于指针变量ptr,若不强制类型转换,那么它的类型是 int(*)[5],若要对它进行加减整数操作,单位长度是一整个数组的长度,ptr = (int *)(&a + 1),首先跳过一整个数组,然后强转,强转不会影响指针指向的位置。
  2. 强制类型转换为(int *)后,访问内存的权限是一个元素的长度

在这里插入图片描述

笔试题2

//考点:指针加整数的意义
struct Test
{
int Num;
char *pcName;
short sDate;
char cha[2];
short sBa[4];
}*p;
//假设p 的值为0x100000。 如下表表达式的值分别为多少?
//已知,结构体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;
}

解读

  1. p+0x100000 =20 + 0x100000 = 0x100014
  2. (unsigned long)p,将p强转为长整型,长整型+1,就是单纯+1
  3. (unsigned int*)p,强转为指针,指针+1,+4或+8
  4. 考点:指针类型决定访问内存的权限

笔试题3

int main()
{
int a[4] = { 11, 22, 33, 44 };
int *ptr1 = (int *)(&a + 1);
int *ptr2 = (int *)((int)a + 1);
printf( "%x,%x", ptr1[-1], *ptr2);
return 0;
}

解读

  1. 对于ptr1ptr1首先存了&a+1的地址,指向数组最后一个元素后的地址,强转为(int*)型,ptr[-1]等价于*(ptr1 - 1 )
  2. 对于ptr2a是首元素地址,(int)a表示将首元素地址强制类型转换为int型数据,然后加一;再对它强制类型转换为(int*)型,又变成一个地址了,再对它进行解引用时,访问的字节数是从它开始往后总共四个字节

在这里插入图片描述

结果

在这里插入图片描述

笔试题4

#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. 这里的初始化语句有猫腻,需要睁大眼睛!!
  2. 实际上数组存放的是{1,3},{5,0},{0,0},为什么呢
  3. 因为最外层大括号里的圆括号,圆括号里的是逗号,它们是逗号表达式,最后一个才为它的值。
int a[3][2] = { (0, 1), (2, 3), (4, 5) };
//等价于
int a[3][2] = { 1, 3, 5 };//未初始化完全,自动补0
//等价于
int a[3][2] = { {1, 3}, {5, 0}, {0, 0} };

  1. p指向第一行第一个元素,结果为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;
}

解读

在这里插入图片描述

笔试题6

int main()
{
  int aa[2][5] = {10,9,8,7,6,5,4,3,2,1};
  int *ptr1 = (int *)(&aa + 1);
  int *ptr2 = (int *)(*(aa + 1));
  printf( "%d,%d", *(ptr1 - 1), *(ptr2 - 1));
  return 0;
}

体会

(写在最后)对指针加减整数操作时,应该首先想到指针的类型

  1. 这题和笔试题3很像,只不过这里是二维数组
  2. &aa+1,跳过一整个数组,ptr1指向最后一个元素后的内存,强转为(int *)再减一,向前移动一个元素的长度,即指向最后一个元素
  3. aa是第一行首元素地址,它代表一整行,aa+1跳过一行指向第二行;*(aa + 1)使之转化为指向列的指针,强转为(int *)再减一,往前移动一个元素的长度,即指向前一行的最后一个元素

在这里插入图片描述

笔试题7

#include <stdio.h>
int main()
{
char *a[] = {"work","at","alibaba"};
char**pa = a;
pa++;
printf("%s\n", *pa);
return 0;
}

解读

  1. 常量字符串,在内存中是以字符数组的形式储存的,而数组名(即数组首元素地址)存放的是第一个字符的地址,也就是说,a这个数组是一个指针数组,存放着三个常量字符串的首元素地址,即"w""a""a"的地址。

在这里插入图片描述

  1. 而二级指针变量pa存放着a的地址,a的地址是指针数组的首元素地址,也就是常量字符串"work"的地址,加一则指向"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);
printf("%s\n", *--*++cpp+3);
printf("%s\n", *cpp[-2]+3);
printf("%s\n", cpp[-1][-1]+1);
return 0;
}

解读

  1. 先看图,cpp存放着cp的首元素地址,而它指向常量字符串"FIRST""F"的地址

    在这里插入图片描述

  2. 对于**++cpp++的优先级更高,且cpp指向cp数组的首元素,自加1则指向下一个元素c+2,第一层解引用得到数组c"P"地址,第二层解引用得到字符串"POINT"

  3. 对于*++cpp,先自增/自减,然后是解引用,最后是+3,此时不要忘记了cpp在上一个语句已经发生了变化(下面的语句也是一样的),已经指向数组cp的第二个元素了。首先是*++cppcpp先自增1,指向数组cp的第三个元素c+1,然后解引用得到它的值,c+1,下面用c+1作为*++cpp的结果处理

  4. 对于*--(c+1):先自减1,即*c,得到数组c第一个元素的值,即常量字符串"ENTER""E"的地址,对它+3(注意此时还是在对地址操作),指针向后移动三个单位长度(单位长度由指针类型决定),即指向"E"的地址,打印结果为:ER

    在这里插入图片描述

  5. 对于*cpp[-2]+3cpp首先与[ ]结合,等价于*(*(cpp-2))+3。思路同上,注意进行运算时,先看看这个变量是指针还是普通变量,指针的类型如何?试着分析一下吧。

    在这里插入图片描述

  6. 对于cpp[-1][-1]+1,先转化为指针形式,它等价于(*(cpp-1)-1)+1,此处和第五点的不同之处是这里有对数组cp的元素的值运算。

    在这里插入图片描述

    结果如下

    在这里插入图片描述

结语

指针并没有人们所说的那么难,最重要的是理解指针是如何运作的。以及各类指针的共同点和区别,它的类型决定了什么?在对指针进行加减运算时,是否要注意指针的类型?会当凌绝顶,一览众山小。未知对我们来说都是有难度的,只要学会并理解了它,我们以后才能更好地使用它。
如果这篇文章对你有帮助的话,请给作者一些鼓励吧~
欢迎读者指正!

posted @ 2022-12-06 22:31  shawyxy  阅读(109)  评论(0编辑  收藏  举报