带你刷笔试关的小怪|详解指针习题和面试题【C语言/指针/进阶】
前言
通过前面的学习,已经对各类指针有所了解,要想掌握C的利器——指针,做题能让我们快速掌握指针的用法。下面将详解指针习题(作为复习)和指针面试题(重点)。
友情链接:详解指针【上】 详解指针【中】 详解指针【下】(时间紧的话就看上篇吧,思路解析是最详细的)
9. 指针和数组笔试题解析
复习回顾
sizeof()
计算的是对象所占内存的大小(单位:字节),而与它指向的东西无关。即sizeof(指针)
就是指针的大小,和它指向的东西的大小无关- 数组名一般指首元素地址,它表示为整个数组有两种情况:①
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 从第二个元素开始
体会
- 操作符
sizeof()
只和它的对象的内存大小有关,和对象指向的东西无关 - 库函数
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;
}
//程序的结果是什么?
解读
- 对于指针变量
ptr
,若不强制类型转换,那么它的类型是int(*)[5]
,若要对它进行加减整数操作,单位长度是一整个数组的长度,ptr = (int *)(&a + 1)
,首先跳过一整个数组,然后强转,强转不会影响指针指向的位置。 - 强制类型转换为
(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;
}
解读
- p+0x100000 =20 + 0x100000 = 0x100014
(unsigned long)p
,将p强转为长整型,长整型+1,就是单纯+1(unsigned int*)p
,强转为指针,指针+1,+4或+8- 考点:指针类型决定访问内存的权限
笔试题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;
}
解读
- 对于
ptr1
,ptr1
首先存了&a+1
的地址,指向数组最后一个元素后的地址,强转为(int*)
型,ptr[-1]
等价于*(ptr1 - 1 )
- 对于
ptr2
,a
是首元素地址,(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,3},{5,0},{0,0}
,为什么呢 - 因为最外层大括号里的圆括号,圆括号里的是逗号,它们是逗号表达式,最后一个才为它的值。
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} };
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;
}
体会
(写在最后)对指针加减整数操作时,应该首先想到指针的类型
- 这题和笔试题3很像,只不过这里是二维数组
&aa+1
,跳过一整个数组,ptr1
指向最后一个元素后的内存,强转为(int *)
再减一,向前移动一个元素的长度,即指向最后一个元素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;
}
解读
- 常量字符串,在内存中是以字符数组的形式储存的,而数组名(即数组首元素地址)存放的是第一个字符的地址,也就是说,
a
这个数组是一个指针数组,存放着三个常量字符串的首元素地址,即"w"
、"a"
和"a"
的地址。
- 而二级指针变量
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;
}
解读
-
先看图,
cpp
存放着cp
的首元素地址,而它指向常量字符串"FIRST"
的"F"
的地址 -
对于
**++cpp
,++
的优先级更高,且cpp
指向cp
数组的首元素,自加1则指向下一个元素c+2
,第一层解引用得到数组c
的"P"
地址,第二层解引用得到字符串"POINT"
-
对于
*++cpp
,先自增/自减,然后是解引用,最后是+3,此时不要忘记了cpp
在上一个语句已经发生了变化(下面的语句也是一样的),已经指向数组cp
的第二个元素了。首先是*++cpp
:cpp
先自增1,指向数组cp
的第三个元素c+1
,然后解引用得到它的值,c+1
,下面用c+1
作为*++cpp
的结果处理 -
对于
*--(c+1)
:先自减1,即*c
,得到数组c
第一个元素的值,即常量字符串"ENTER"
的"E"
的地址,对它+3(注意此时还是在对地址操作),指针向后移动三个单位长度(单位长度由指针类型决定),即指向"E"
的地址,打印结果为:ER -
对于
*cpp[-2]+3
,cpp
首先与[ ]
结合,等价于*(*(cpp-2))+3
。思路同上,注意进行运算时,先看看这个变量是指针还是普通变量,指针的类型如何?试着分析一下吧。 -
对于
cpp[-1][-1]+1
,先转化为指针形式,它等价于(*(cpp-1)-1)+1
,此处和第五点的不同之处是这里有对数组cp
的元素的值运算。结果如下
结语
指针并没有人们所说的那么难,最重要的是理解指针是如何运作的。以及各类指针的共同点和区别,它的类型决定了什么?在对指针进行加减运算时,是否要注意指针的类型?会当凌绝顶,一览众山小。未知对我们来说都是有难度的,只要学会并理解了它,我们以后才能更好地使用它。
如果这篇文章对你有帮助的话,请给作者一些鼓励吧~
欢迎读者指正!