指针的最后一部分笔记,涉及到一些面试题。其中几个题是非常典型的,值得仔细思考分析。
数组
一维数组
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));
return 0;
}
int main()
{
char arr[] = "abcdef";
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;
}
不做完全的解释,只挑几个比较有特点的。
printf("%d\n", sizeof(arr+0)); : 没有在sizeof内部也没有&数组名,因此是首元素地址,首元素地址+0之后指向的是首元素,也没有解引用,那么这里就是个地址。是地址,大小就是4/8;
printf("%d\n", strlen(*arr)); :对数组地址解引用,那么就是对首元素地址解引用,得到的值是'a'。‘a’ 作为地址传参给strlen,实际传的是字符’a’对应的ASCII码值作为地址,这里就非法了。
printf("%d\n", strlen(&arr + 1)); : &arr,取到的是整个数组的地址,+1之后跳过数组,因此这里是随机值。
再来一组:
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));
return 0;
}
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;
}
printf("%d\n", sizeof(p + 1)); :p指向的是字符串abcdef的首元素地址,也就是a的地址,p+1需要看p的类型是什么。这里可以知道p的类型是char*,所以p+1跳过一个字节,指向b的地址。既然是地址,那么大小就是4/8。
printf("%d\n", sizeof(&p + 1)); :&p取到的是p变量的地址,类型应该是char**,这里&p+1跳过的是p的地址,然后既然还是地址,大小就是4/8。
printf("%d\n", strlen(&p)); :&p得到的是p变量的地址,作为参数传给strlen时,就是从p所占空间的起始位置开始向后找\0,因此是个随机值。
printf("%d\n", strlen(&p + 1));:和上面一样,跳过了p变量,还是随机值。这里需要注意的是,这个随机值和上面的随机值没什么关系。
二维数组
处理二维数组的时候,时刻牢记二维数组的首元素是一维数组。
看一下代码:
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]));
return 0;
}
一个一个过一遍。
printf("%d\n", sizeof(a)); :数组的大小,3*4*4 = 48。
printf("%d\n", sizeof(a[0][0])); :数组中第一行第一列元素的大小,4。
printf("%d\n", sizeof(a[0])); :arr[0] 这里就是一个数组名,既然是sizeof(数组名),那么就是整个数组的大小。这里记住,二维数组的元素是一维数组,那么这个一维数组的大小就是4*4=16。
printf("%d\n", sizeof(a[0] + 1)); :这里sizeof内部不是数组名,那么a[0]就是首元素地址。这个地址指向a[0][0],+1之后就是a[0][1],地址的大小4/8。
printf("%d\n", sizeof(*(a[0] + 1))); :对上面的地址解引用,大小就是4。
printf("%d\n", sizeof(a + 1)); :a在这里代表首元素,首元素是个一维数组,+1之后跳过整个一维数组,因此这里a+1指向的是二维数组里的第二个元素的首地址,也就是a[1][0]。既然是地址,大小就是4/8。
printf("%d\n", sizeof(*(a + 1))); :对上面的地址解引用,大小16。这里注意,*(a+1) == a[1],因此计算的是二维数组里第二个元素的大小,因此是16。
printf("%d\n", sizeof(&a[0] + 1)); :取到的是第一个元素的地址,+1之后到第二个元素,指向的是第二行的首地址。大小4/8。
printf("%d\n", sizeof(*(&a[0] + 1)));:这里是对第二个元素整体解引用,那么大小就是16。
printf("%d\n", sizeof(*a)); :这里是第一行解引用,相当于*(a+0),那么就是a[0],大小16。
printf("%d\n", sizeof(a[3]));:虽然越界,但不改变a[3]的类型,还是16。
一些笔试题
题目 1
#include <stdio.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;
}
分析一下。&a这个是&数组名了,那么就是整个数组的地址,+1之后跳过整个数组,因此ptr指向的是5之后的位置。*(a+1) == a[1],也就是2, ptr-1指向5,解引用之后就是5。因此打印2,5。
题目 2
struct Test
{
int Num;
char *pcName;
short sDate;
char cha[2];
short sBa[4];
}*p = (struct Test*)0x100000;
int main()
{
printf("%p\n", p + 0x1);
printf("%p\n", (unsigned long)p + 0x1);
printf("%p\n", (unsigned int*)p + 0x1);
return 0;
}
这里结构体的大小是20个字节。同样,分析一下。
printf("%p\n", (unsigned long)p + 0x1);:p是一个结构体指针,然后被赋值为0x100000。p+0x1 == p+1 跳过了整个结构体,也就是20个字节。所以结果是0x100014。注意20写成16进制是14。
printf("%p\n", (unsigned long)p + 0x1);:这里p被强制转换成一个unsigned long型数字,+1那就是+1了。结果是0x100001
printf("%p\n", (unsigned int*)p + 0x1);:这里p是一个unsigned int*类型的指针,+1之后就是跳过4个字节,结果是0x100004
题目 3
int main()
{
int a[3][2] = { (0, 1), (2, 3), (4, 5) };
int *p;
p = a[0];
printf("%d", p[0]);
return 0;
}
这里需要注意的是数组内的元素是逗号表达式,因此实际等价于:
int a[3][2] = { 1, 3, 5 };
然后p=a[0] 就是指向首地址的,写成指针可以是p = *(a+0)。打印的时候就是*(*(a+0)+0),这个表达式 == a[0][0],所以结果是1。
题目 4
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;
}
这里看一下,a是一个5*5的数组,而p是一个数组指针,指向的数组有4个int型的元素。执行p = a之后,就是把a的地址强制给p,注意这里p的类型是int(*)[4]。
&p[4][2]-&a[4][2],这里是两个地址做差,结果是两个地址之间的元素个数。那么就需要知道这两个地址都指向哪里。先看简单的,a[4][2]是第5行第3个元素的首地址,按照内存中的顺序,是第23个元素的首地址。p[4][2]是第4行第4个元素(注意这里p[4][0]是第4行第2个元素,内存中是第17个元素),所以p[4][2]在内存里是第19个元素。做差之后差值是-4。所以按照%d打印的时候,结果就是-4。-4按照%p打印,则是FFFFFFFFC,注意负数的补码就好了。
题目 5
#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 :跳过整个数组,指向后一个元素,也就是10之后的地址;
aa + 1:跳过首元素,指向后一个元素的首地址,也就是6;
那么结果就很清楚了,是10,5。
题目 6
#include <stdio.h>
int main()
{
char *a[] = {"work","at","alibaba"};
char**pa = a;
pa++;
printf("%s\n", *pa);
return 0;
}
先看一下,a是一个指针数组,指针的类型是char*,数组的内容分别是 work,at,alibaba的首地址。现在使用二级指针pa指向a,也就是数组的首地址给了pa。pa++,这里就是跳过第一个元素,那么pa就指向了第二个元素的首地址。打印结果就是at。
题目 7
这个题目难度比较大,需要仔细分析,最好画图。
#include <stdio.h>
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;
}
char *c[] = {"ENTER","NEW","POINT","FIRST"}; :c是指针数组,存储的内容是"ENTER",“NEW”,“POINT”,“FIRST” 这几个字符串的首地址。
char**cp[] = {c+3,c+2,c+1,c}; :cp是二级指针数组,内容是指向按顺序是指向"FIRST",“POINT”,“NEW”,“ENTER”
char***cpp = cp; :cpp是三级指针,指向cp。
到这里,画一个图理解一下:

**++cpp:cpp跳过一个元素,指向下一个,也就是c+2:

此时解引用拿到的是c+2,c+2就是P的地址,再次解引用就得到了POINT这个字符串的首地址。所以:printf("%s\n", **++cpp); 就打印了POINT。
*--*++cpp+3:先捋一下优先级。自加和自减的还有解引用的优先级都比加法高,所以就先看前面的。注意,上一行执行结束之后cpp试试指向c+2的。此时再++:

解引用之后拿到的是c+1,然后执行–,那么这里实际执行的是(c+1)–,结果是c。既然结果是c,那么指向也就变了:

再次解引用,拿到的就是E的地址。这里注意还有一个+3,那么就是跳过3个字符,ENTER跳过3个字符打印,结果就是ER。所以printf("%s\n", *--*++cpp+3);打印结果是ER。
*cpp[-2]+3:这里换一种写法比较容易理解,**(cpp-2)+3。画一下图:

再经过两次解引用,拿到字符串F,跳过3个字母之后,最后打印ST。
printf("%s\n", cpp[-1][-1]+1); :这里还是稍微改造一下写法:cpp[-1][-1]+1 可以写成 *(*(cpp-1)-1) +1。这里一定要清楚,上面的语句执行不改变cpp的指向。所以,cpp-1执行之后:

解引用之后拿到的是C+2。这里再次执行C+2 -1 = C+1,又一次改变了指向:

解引用之后拿到了N,N+1跳过一个字母,所以打印是EW。
深入理解指针与数组在面试中的常见问题
文章详细解析了指针和数组在C语言中的sizeof和strlen应用,涉及一维和二维数组示例,以及面试中常出现的指针相关题目,帮助读者掌握指针操作的基本原理。
123





