- 指针和数组面试题的解析
- 指针笔试题
———————————————————————————————————————————
首先,我们重新回顾一下:
数组是能够存放一组相同类型的元素,数组的大小取决于数组的元素个数和元素类型。而指针就是地址;指针变量来存放指针的地址,大小是4或8个字节。
指针是指针,数组是数组,两者不等价。
数组名是首元素的地址,这个地址可以存放在指针变量中。我们可以通过指针来遍历数组
数组名在大部分情况下数组名是数组的首元素地址,但是也有两个例外:
sizeof(数组名),这里的数组名是整个数组的大小,计算的是整个数组的大小。
&数组名,这里的数组名也表示整个数组,取出的是整个数组。
1. 指针和数组笔试题解析
1.一维数组
样例题目 :
//一维数组
int a[] = {1,2,3,4};
printf("%d\n",sizeof(a));
printf("%d\n",sizeof(a+0));
printf("%d\n",sizeof(*a));
printf("%d\n",sizeof(a+1));
printf("%d\n",sizeof(a[1]));
printf("%d\n",sizeof(&a));
printf("%d\n",sizeof(*&a));
printf("%d\n",sizeof(&a+1));
printf("%d\n",sizeof(&a[0]));
printf("%d\n",sizeof(&a[0]+1));
解析为:
int main()
{
//一维数组
int a[] = { 1,2,3,4 };
printf("%d\n", sizeof(a));//16
//sizeof(a)就是数组名单独放在sizeof内部,计算的数组总大小,单位是字节
printf("%d\n", sizeof(a + 0));//4或8字节
//sizeof(a+0)这里没有把a单独放在sizeof内部,所以数组名是首元素地址,所以+0还是首元素地址因此为4/8个字节
//a+0是首元素地址,同时注意+0不可以被忽略
printf("%d\n", sizeof(*a));//4
//a是数组首元素地址,相当于 &a[0]
//*a等价于*&a[0],因此等于a[0]
printf("%d\n", sizeof(a + 1));//4或8
//a 是数组首元素的地址,类型是 int*
//a+1 就是跳过一个 int* ,是第二个元素的地址
printf("%d\n", sizeof(a[1]));//4
printf("%d\n", sizeof(&a));//4或者8
//&a 取出的是数组的地址,数组的地址也是地址,大小是4/8个字节
//int (*pa)[4] = &a
printf("%d\n", sizeof(*&a));//16
//*&a == a,sizeof(a)计算的是数组总大小
//或者理解为&a是数组的地址,得到的指针类型是: int(*)[4];(指向数组的指针)
//当对指针进行解引用的时候,访问几个字节取决于指针的类型
//这里*解引用访问数组的指针,解引用访问一个数组的大小,这个数组4个元素每个元素是int类型,所以4*4=16
printf("%d\n", sizeof(&a + 1));//4/8
//《特别注意》 &a 取出的是整个数组的地址,类型是int (*)[4] = &a;
//&a+1 就跳过整个数组,指向的是紧跟数组后的地址
printf("%d\n", sizeof(&a[0]));//4/8
//&a[0] 取出数组首元素的地址
printf("%d\n", sizeof(&a[0] + 1));//4/8
//&a[0]+1 取出数组第二个元素的地址
return 0;
}
注:对于&a+1这一行来说,&a+1跳跃的是整个数组(sizeof计算大小是4/8字节)。
而a+1是跳过一个元素,和&a[0]与&a[0]+1是一回事。
2.字符数组
在此之前需要注意:
1. sizeof 计算的是占用内存空间的大小,单位是字节,不关注内存中到底存放的是什么
2. sizeof 不是函数,是操作符
3. strlen 是函数
4. strlen 是针对字符串,求的是字符串的长度,本质上统计的是 \0 之前出现的字符个数
(strlen才关注\0)
5.strlen的参数为const char* str,如果为数字,则会把数字当成地址产生非法访问的结果。
1. char arr[] = { 'a','b','c','d','e','f' };
样例题目 :
int main()
{
char arr[] = { 'a','b','c','d','e','f' };
printf("%d\n", sizeof(arr));
printf("%d\n", sizeof(arr + 0));
printf("%d\n", sizeof(*arr));
printf("%d\n", sizeof(arr[1]));
printf("%d\n", sizeof(&arr));
printf("%d\n", sizeof(&arr + 1));
printf("%d\n", sizeof(&arr[0] + 1));
printf("%d\n", strlen(arr));
printf("%d\n", strlen(arr + 0));
printf("%d\n", strlen(*arr));
printf("%d\n", strlen(arr[1]));
printf("%d\n", strlen(&arr));
printf("%d\n", strlen(&arr + 1));
printf("%d\n", strlen(&arr[0] + 1));
return 0;
}
解析:
int main()
{
char arr[] = { 'a','b','c','d','e','f' };
printf("%d\n", sizeof(arr));//6
printf("%d\n", sizeof(arr + 0));//4/8
//arr+0 是数组首元素的地址
printf("%d\n", sizeof(*arr));//1
//arr 数组首元素的地址,*arr 表示数组首元素
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));
//随机值
//arr 数组首元素地址
printf("%d\n", strlen(arr + 0));
//随机值
//arr+0 数组首元素地址
printf("%d\n", strlen(*arr));//非法访问—strlen访问97的地址的值
//arr是首元素地址,*arr进行解引用操作 得到a
//而strlen(const char* str) 接受的是字符指针 因此传入a的ASCII值 为97,而后会吧97的地址的值进行访问,但是地址是随意访问的
printf("%d\n", strlen(arr[1]));
//arr[1] 代表数组第二个元素,‘b’- 98 当成地址,形参非法访问
printf("%d\n", strlen(&arr));//随机值 和strlen(arr)是一样的都是随机值
//&arr 数组的地址,传给strlen后从起始位置开始计算
printf("%d\n", strlen(&arr + 1));//随机值-6
//&arr+1 整个数组后的地址————和上面sizeof跨越长度是一样的跨越一个数组,因此从f的下一个元素开始数(之间差了6个)
printf("%d\n", strlen(&arr[0] + 1));//随机值-1
//&arr[0]+1 数组第二个元素的地址 从第二个元素开始数
return 0;
}
图解:
2. char arr[] = "abcdef";
样例题目 :
int main()
{
char arr[] = "abcdef";
printf("%d\n", sizeof(arr));
printf("%d\n", sizeof(arr + 0));
printf("%d\n", sizeof(*arr));
printf("%d\n", sizeof(arr[1]));
printf("%d\n", sizeof(&arr));
printf("%d\n", sizeof(&arr + 1));
printf("%d\n", sizeof(&arr[0] + 1));
printf("%d\n", strlen(arr));
printf("%d\n", strlen(arr + 0));
printf("%d\n", strlen(*arr));
printf("%d\n", strlen(arr[1]));
printf("%d\n", strlen(&arr));
printf("%d\n", strlen(&arr + 1));
printf("%d\n", strlen(&arr[0] + 1));
}
解析:
int main()
{
char arr[] = "abcdef";
printf("%d\n", sizeof(arr));//7 [a b c d e f \0]
//sizeof(arr) 计算的是整个数组的大小,而且sizeof只关注占用内存空间的大小
printf("%d\n", sizeof(arr + 0));//4/8
//arr 数组首元素的地址,*arr 表示数组首元素
printf("%d\n", sizeof(*arr));//1
printf("%d\n", sizeof(arr[1]));//1
printf("%d\n", sizeof(&arr));//4/8
//&arr 取出的是数组的地址
printf("%d\n", sizeof(&arr + 1));//4/8
//&arr+1 跳过一个数组后的地址
printf("%d\n", sizeof(&arr[0] + 1));//4/8
//&arr[0] 数组首元素的地址,+1 数组第二个元素的地址
//strlen 针对字符串,求的是字符串的长度,本质上统计的是 \0 之前出现的字符个数
printf("%d\n", strlen(arr));//6
//arr 首元素的地址
printf("%d\n", strlen(arr + 0));//6
//arr+0 首元素的地址
printf("%d\n", strlen(*arr));//非法访问
//*arr 代表首元素,但是 strlen 函数传入的时候是const char*str,因此将字符的ASCLL码的值作为地址给strlen作为参数
printf("%d\n", strlen(arr[1]));//非法访问
//arr[1] 代表数组第二个元素,但是 strlen 函数传入的时候是const char*str,因此将字符的ASCLL码的值作为地址给strlen作为参数
printf("%d\n", strlen(&arr));//6
//&arr -指针类型为 char(*)[7],之后传给strlen会发生强转,强转为const char *
printf("%d\n", strlen(&arr + 1));//随机值
//&arr+1 数组后的地址,因为不确定所以为随机值
printf("%d\n", strlen(&arr[0] + 1));//5
//&arr[0]+1 数组第二个元素的地址,即b到\0;
}
图解:
3.char *p = "abcdef";
sizeof计算大小
int main()
{
char* p = "abcdef";
printf("%d\n", sizeof(p));
printf("%d\n", sizeof(p + 1));
printf("%d\n", sizeof(*p));
printf("%d\n", sizeof(p[0]));
printf("%d\n", sizeof(&p));
printf("%d\n", sizeof(&p + 1));
printf("%d\n", sizeof(&p[0] + 1));
}
解析:
int main()
{
char* p = "abcdef";
printf("%d\n", sizeof(p));//4/8
//把常量字符串的首元素地址放进*p里去了,也就是a的地址
printf("%d\n", sizeof(p + 1));//4/8
//p 的类型是char*,+1就跳过一个char*,也就是说跳过一个字符的地址,相当于b的地址
printf("%d\n", sizeof(*p));//1
//*p 指针解引用,因此*p表示首字符a
printf("%d\n", sizeof(p[0]));//1
//p[0]相当于*(p+0),表示字符串首元素a
printf("%d\n", sizeof(&p));//4/8
//&p 取出的是指针变量p在内存中的地址 类型是char** //char* *pp=&p
printf("%d\n", sizeof(&p + 1));//4/8
//&p+1 跳过p的地址,但还是一个地址
printf("%d\n", sizeof(&p[0] + 1));//4/8
//&p[0]得到a的地址,&p[0]+1,往后移动一位得到的就是b的地址
}
strlen计算大小
int main()
{
char *p = "abcdef";
printf("%d\n", strlen(p));
printf("%d\n", strlen(p+1));
printf("%d\n", strlen(*p));
printf("%d\n", strlen(p[0]));
printf("%d\n", strlen(&p));
printf("%d\n", strlen(&p+1));
printf("%d\n", strlen(&p[0]+1));
return 0;
}
解析:
int main()
{
char* p = "abcdef";
printf("%d\n", strlen(p));//6
printf("%d\n", strlen(p + 1));//5
printf("%d\n", strlen(*p));//非法访问
printf("%d\n", strlen(p[0]));//非法访问
//p[0]相当于*(p+0),表示字符串首元素a
printf("%d\n", strlen(&p));//随机值
//拿到的是p的地址
printf("%d\n", strlen(&p + 1));//随机值,且与&p无关系
//拿到的是p后面的地址
printf("%d\n", strlen(&p[0] + 1));//5
//&p[0]得到a的地址,&p[0]+1,往后移动一位得到的就是b的地址
return 0;
}
3.二维数组
//二维数组
int main()
{
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]));
}
解析:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include<string.h>
int main()
{
int a[3][4] = { 0 };
printf("%d\n", sizeof(a));//48
//二维数组的数组名a单独放在sizeof内部,计算的是整个数组的大小
printf("%d\n", sizeof(a[0][0]));//4
//a[0][0] 第1行第1个元素
printf("%d\n", sizeof(a[0]));//16
//a[0] 第1行的数组名,数组名单独放在sizeof内部
//计算的是第1行数组的大小,单位是字节,16
printf("%d\n", sizeof(a[0] + 1));//4/8
//a[0]不是单独放在sizeof内部
//arr[0]表示的是首元素地址,即第1行第1个元素的地址 - &a[0][0]
//a[0]+1 是第1行第2个元素的地址 &a[0][1]
printf("%d\n", sizeof(*(a[0] + 1)));//4
//*(a[0]+1) 意思为访问第1行第2个元素a[0][1]
printf("%d\n", sizeof(a + 1));//4/8
//a作为二维数组的数组名并非单独放在sizeof内部,所以表示首元素的地址
//二维数组的首元素是第一行,这里的a就是第一行的地址---int(*)[4]
//a+1是跳过第一行,指向第二行
//注意:这里是第二行的地址而不是第二行第一个首元素的地址
printf("%d\n", sizeof(*(a + 1)));//16
//*(a+1)--->a[1]
printf("%d\n", sizeof(&a[0] + 1));//4/8
//&a[0]是第一行地址,&a[0]+1是第二行的地址
printf("%d\n", sizeof(*(&a[0] + 1)));//16 a[1]
//*(&a[0]+1) 第二行的元素
printf("%d\n", sizeof(*a));//16 *a 就是第一行
//*a --*(a+0) --a[0]
printf("%d\n", sizeof(a[3]));//16
return 0;
}
考考:
int arr[3][4] = { 0 };
printf("%d\n", sizeof(*arr+1));//?
解析:
首先arr不是单独放在sizeof内部,所以arr取的是首元素地址,而arr为二维数组所以首元素为arr[0],之后arr[0]+1,则取的是arr[0][0]+1的地址也就是&arr[0][1],答案则为4/8
其实,二维数组如果没有单独放在sizeof内部就会发生降级, 例如sizeof(a)与sizeof(a+1)和sizeof(a[0]+1)这些事例能充分反应出降级,且上面考考例子就是如此!
4.sizeof的表达式计算
例子:
int main()
{
int a = 5;
short s = 11;
printf("%d\n", sizeof(s = a + 2));
printf("%d\n", s);
return 0;
}
输出结果:
为什么是这样的呢,而不是输出2和7呢?原因是因为sizeof不会计算括号内表达式的值(并且括号内的表达式不会执行,即被忽略),只会关注sizeof括号内部的类型属性的值。
对于这个表达式来说int a=10;a+3;值属性是13,而类型属性是int,大小为4;再来分析上面的例子,代码"sizeof(s=a+2);"s的值属性为7,而s的类型属性为short,大小为2。sizeof只根据类型属性判断大小,不关注sizeof内部的表达式,即不会对s=a+2进行计算。因此下面的printf打印s的值还是11.因此也不会产生越界访问!
总结:
1. sizeof(数组名),这里的数组名表示整个数组,计算的是整个数组的大小。
2.&数组名,这里的数组名表示整个数组,取出的是整个数组的地址。
3.除此之外所有的数组名都表示首元素的地址。
2.指针笔试题
1.
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include<string.h>
int main()
{
int a[5] = { 1, 2, 3, 4, 5 };
int* ptr = (int*)(&a + 1);
printf("%d,%d", *(a + 1), *(ptr - 1));
return 0;
}
结果:
2.
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include<string.h>
struct Test
{
int Num;
char* pcName;
short sDate;
char cha[2];
short sBa[4];
}*p;
//假设p 的值为0x100000。 如下表表达式的值分别为多少?
//已知,结构体Test类型的变量大小是20个字节
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;
}
结果:
整形指针加1,跳过1个整形,跳过4个字节;字符指针加1,跳过1个字符,跳过1个字节。而这里是自己定义的结构体,结构体大小为20,那么这里+1则跳过20个字节。那么p的值为0x100000时
1.p+0x1,对于结构体类型的p进行加1,需要跳过20个字节,对于16进制则为14,因此结果为0x100014。
2. (unsigned long)p + 0x1 将结构体指针类型强制类型转换为unsigned long类型,16进制加减正常+1,因此结果为00100001。
3.(unsigned int*)p+0x1:将结构体指针类型强制类型转换为unsigned int*类型,是一个指针,需要增加一个指针的大小,即+4,结果为00100004。
3.
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
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;
}
结果:
分析:
(int*)(&a+1):首先&a取出整个数组的地址进行加+1,也就是跨越了一个数组,将其转化为int*类型在赋给指针变量ptr。
(int)a+1:a此时为首元素地址(假如a的其实地址为0x0012ff40),a强制类型转换为int类型,+1则正常数字运算,对地址的值进行加1,跳过1个字节,运算后(0x0012ff41)。如果是int*类型的话,+1跳过4个字节,(假如a的其实地址为0x0012ff40,则+1后变成0x0012ff44)
4.
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main()
{
int a[3][2] = { (0, 1), (2, 3), (4, 5) };
// 1 3 5
//逗号表达式的结果是最后一个表达式的结果
int* p;
p = a[0];
printf("%d", p[0]);
return 0;
}
这道题考察了逗号表达式(0,1),(2,3),(4,5)为逗号表达式,取逗号最右边的结果,即1,3,5,此时二维数组变成了a[3][2]={1,3,5};也就是第一行为a[0]={1,3},a[1]={5,0},a[2]={0,0};
a[0]是二维数组第一行的数组名,对a[0]这个数组名没有&,没有单独sizeof,所以a[0]这个数组名表示数组首元素的地址,即a[0][0]的地址,a[0]---->&a[0][0]
因此p=a[0][0],p指向为首元素地址,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[4][2] - &a[4][2]中间相差4个元素,而&p[4][2]为低地址处所以为-4。-4的补码在内存中存储,且被看作为地址:
10000000000000000000000000000100 //原码
111111111111111111111111111111111011 //反码
111111111111111111111111111111111100 //补码
%p转换成16进制输出(每4位二进制位为1位十六进制位),则转换为0xFF FF FF FC
6.
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
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:&aa取出的是整个数组的地址,加1跳过整个数组的大小,指向整个数组的末尾。
*(aa+1):aa是数组首元素的地址,加1后aa+1为数组第2行元素的地址,即aa[1],
然后都-1因为是int*所以指针都往前4个字节,得到结果
7.
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main()
{
char* a[] = { "work","at","alibaba" };
char** pa = a;
pa++;
printf("%s\n", *pa);
return 0;
}
char*a[]数组存放三个元素,分别是字符串a[0],a[1],a[2];a为首元素地址存放在char* *p指针中,pa=a[0],然后pa++,则pa此时指向的是a[1],有首元素地址打印在\0处结束。
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的地址,因此初始先指向cp[0],之后指针cpp到cp[1](也就是红色箭头指向),cp[1]存储的是c+2的地址,因此有了c[2]内容的首元素地址,打印c[2]至'\0'
操作顺序: ++,解引用,再--,解引用,最后解引用位置+3
首先++--的优先级最高根据结合远近,先对cpp进行++,但是注意由于上一题++*cpp,是对cp自加并且将自加的值赋给cpp,因此cpp的位置从上一题的位置开始。++cpp后解引用为*cp[2],cp[2]的内容为c+1的指针(也就是c+1的地址),而c+1的地址-1再解引用此时为c[0],而之后+3则指针指向第三个元素的后面,最后从该位置打印至结束。
操作顺序:cpp[2]-2到cpp[0],对c[0]指向c[3]对c[3]解引用,再向后移动3个单位也就是第三关字母的后面开始进行打印
从上个题目的位置,再-2到cpp[-2]==*(cpp-2)==cp[0],cp[0]存储的是c+3的地址,之后对c+3的地址解引用,先指向首元素地址,再+3,在第三个元素的后面,最后从该位置打印至结束
cpp[-1][-1]==*(cpp-1)[-1]==cp[1][-1]==*((c+2)-1)==*(c+1)==c[1],对cpp-1地址解引用得到c+3,后解引用在-1,解引用得到NEW,指针指向首元素地址,+1改变指针指向,最后打印得到结果
(此坑已完)