一、深刻理解指针和数组
对于指针和数组,我们必须要要知道的几个核心原则是:
1.sizeof(数组名),数组名代表的是整个数组,计算的是整个数组的大小
2.&数组名,数组名代表的是整个数组。取出的是整个数组的地址
3.除此之外,所有的数组名都是首元素的地址
1.一维数组
#include<stdio.h> int main() { 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 a[] = { 1,2,3,4 };
printf("%d\n", sizeof(a));
//sizeof(数组名),此时数组名代表的是整个数组,计算的是整个数组的大小
//此时a的类型是int [4]
//答案是16,单位是字节
printf("%d\n", sizeof(a + 0));
//数组名不是单独在sizeof内部,也不是取地址,此时代表的是首元素的地址,加上0还是首元素的地址,计算的是这个地址的大小
//a+0的类型是int*
//答案是4/8,单位是字节
printf("%d\n", sizeof(*a));
//a代表的是首元素的地址,对首元素的地址解引用,代表的是首元素,计算的是这个首元素的大小
//*a的类型是int
//答案是4,单位是字节
printf("%d\n", sizeof(a + 1));
//a是首元素地址,加一后代表的是第二个元素的地址
//a+1的类型是int*
//答案是4/8,单位是字节
printf("%d\n", sizeof(a[1]));
//代表的是第二个元素,计算的是第二个元素的大小
//a[1]的类型是int,int的字节就是4个字节
//答案是4,单位是字节
printf("%d\n", sizeof(&a));
//代表是整个数组的地址,地址的大小就是4/8
//从类型的角度分析:&a的类型是int(*)[4],是一个指针变量,也就是4/8个字节
//答案是4/8,单位是字节
//但是在vc6.0上这个编译器上这个算出来的是个16,这是一个bug。理论上应该就是4/8
printf("%d\n", sizeof(*&a));
//&a,代表的是整个数组的地址,然后解引用这个数组的地址,取出来的是一个数组,也就是16
//从类型的角度分析:&a的类型是int(*)[4],解引用后就是去掉这颗*,也就是int [4],这个类型的大小就是16
//答案是16
printf("%d\n", sizeof(&a + 1));
//&a,代表的是整个数组的地址,加一就是这个向后偏移一个数组的长度后的地址,指向数组后面的空间
//虽然这个不属于它的空间,但是它总是一个地址。
//&a的类型是int(*)[4],加一后还是int(*)[4]的类型
//答案是4/8
printf("%d\n", sizeof(&a[0]));
//&a[0]代表的是首元素的地址
//类型是int*
//答案是4/8
printf("%d\n", sizeof(&a[0] + 1));
//&a[0]+1代表的是第二个元素的地址
//类型是int*
//答案是4/8
为了方便我们区别这个4是首元素大小的4还是地址的4,我们将环境改为64位环境,这样如果是指针变量的话,输出的结果就为8
2、字符数组
(1)字符变量存放到数组中
#include<stdio.h> #include<string.h> 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; }
无论如何,我们的核心要点是不会改变的。我们的解析如下
char arr[] = { 'a','b','c','d','e','f' };
printf("%d\n", sizeof(arr));
//sizeof(数组名),次数数组名代表的是整个数组的地址,计算的是整个数组的大小,这个数组只有6个元素,没有\0
//arr的类型是char [6]
//所以答案是6
printf("%d\n", sizeof(arr + 0));
//arr代表的是首元素地址,加0后还是首元素地址,计算的是这个地址的大小
//这个arr+0的类型是char*
//所以答案为4/8
printf("%d\n", sizeof(*arr));
//arr代表的是首元素的地址,解引用后代表的是首元素
//类型是char
//所以答案为1
printf("%d\n", sizeof(arr[1]));
//arr[1]是数组第二个元素
//类型是char
//所以答案为1
printf("%d\n", sizeof(&arr));
//&arr代表的是整个数组的地址
//类型是char(*)[6]
//所以答案为4/8
printf("%d\n", sizeof(&arr + 1));
//&arr是整个数组的地址,加一后是这个数组向后面空间的一个地址。
//类型是char(*)[6]
//答案是4/8
printf("%d\n", sizeof(&arr[0] + 1));
//&arr[0]+1是第二个元素的地址
//答案是4/8
printf("%d\n", strlen(arr));
//arr是数组名,代表的是首元素的地址,从首元素的地址开始算字符串的长度
//为了找到\0,但是我们并不知道数组后面多少个内存才是\0
//所以答案是大于等于6的随机数
printf("%d\n", strlen(arr + 0));
//arr+0是首元素的地址,还是不知道\0究竟在哪里
//所以答案是大于等于6的随机数
printf("%d\n", strlen(*arr));
//arr是首元素的地址,解引用后是第一个元素。第一个元素是字符'a'。类型是char
//字符a的ASCII值是97,97翻译成16进制数是0x00000061也就是这个地址处开始找\0
//但是0x00000061这个地址并不一定分配空间,我们不能直接随便拿一个地址就计算长度,所以这个代码是错的
//这个代码必然会导致程序的崩溃。
printf("%d\n", strlen(arr[1]));
//arr[1]是第一个元素。第一个元素是字符'a'。
//字符a的ASCII值是97,97翻译成16进制数是0x00000061也就是这个地址处开始找\0
//但是0x00000061这个地址并不一定分配空间,我们不能直接随便拿一个地址就计算长度,所以这个代码是错的
//这个代码必然会导致程序的崩溃
printf("%d\n", strlen(&arr));
//&arr,代表的是整个数组的地址,它的类型是char(*)[6]
//而strlen的形参是const char* 很明显类型不匹配,但是只能说是不合理。程序还是可以运行计算出来结果的
//结果仍然是大于等于6的随机值
printf("%d\n", strlen(&arr + 1));
//&arr,代表的是整个数组的地址,加一后是它跳过这个数组以后后面的那个地址,它的类型是char(*)[6]
//而strlen的形参是const char* 很明显类型不匹配,但是只能说是不合理。程序还是可以运行计算出来结果的
//结果是大于等于0的随机值
printf("%d\n", strlen(&arr[0] + 1));
//代表的是第二个元素的地址
//类型是char*
//计算的结果是大于等于5的随机值
但是其实在这里我们可能还会有一个疑问的是
在这段代码中,计算a字符的大小和计算第一个元素的大小不一样。而且如果是c++下,sizeof('a'),计算出来的是1个字节
这是因为c语言标准的问题
(2)字符串存放到字符数组
#include<stdio.h> #include<string.h> 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)); return 0; }
我们要抓住的核心是,这个跟上面的区别是,这个后面是有一个\0的,是七个元素。其他的就是我们的核心原则了
char arr[] = "abcdef";
//需要注意的是这个数组里面其实有七个元素,后面还有一个\0
printf("%d\n", sizeof(arr));
//这个计算的是整个数组的大小
//类型是char [7]
//计算结果为7
printf("%d\n", sizeof(arr + 0));
//代表的是首元素的地址
//类型是char*
//计算结果为4/8
printf("%d\n", sizeof(*arr));
//代表的是首元素
//类型是char
//计算结果为1
printf("%d\n", sizeof(arr[1]));
//代表的是首元素
//类型为char
//计算结果为1
printf("%d\n", sizeof(&arr));
//代表的是整个数组的地址
//类型是char(*)[7]
//计算结果为4/8
printf("%d\n", sizeof(&arr + 1));
//代表的是整个数组后面的空间的地址
//类型是char(*)[7]
//计算结果为4/8
printf("%d\n", sizeof(&arr[0] + 1));
//代表的是第二个元素的地址
//类型是char*
//计算结果为4/8
printf("%d\n", strlen(arr));
//arr代表的是首元素的地址,第七个元素是\0
//类型是char*
//所以结果为6
printf("%d\n", strlen(arr + 0));
//代表的是首元素的地址,第七个元素是\0
//类型是char*
//所以结果为6
//printf("%d\n", strlen(*arr));
//代表的是第一个元素
//类型是char
//第一个元素是'a',ASCII值是97,97这个地址是0x00000061
//但是不能随便拿个地址就去计算字节。去访问
//所以这个代码是错的
//printf("%d\n", strlen(arr[1]));
//代表的是第一个元素
//类型是char
//第一个元素是'a',ASCII值是97,97这个地址是0x00000061
//但是不能随便拿个地址就去计算字节。去访问
//所以这个代码是错的
printf("%d\n", strlen(&arr));
//代表的是整个数组的地址
//类型是char(*)[7]
//虽然类型不匹配,但也是数组的起始地址,也能用。只是不合理
//计算结果是6
printf("%d\n", strlen(&arr + 1));
//整个数组的地址后面的空间
//类型是char(*)[7]
//类型不匹配,但是只是不合理
//由于不知道后面是如何存储的数据,所以结果为随机值
printf("%d\n", strlen(&arr[0] + 1));
//第二个元素的地址
//类型是char*
//结果为5
(3) 字符串存放到一个指针中
#include<stdio.h> #include<string.h> 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)); 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; }
对于这个题,我们还是那三个要点,还需要注意的是,p指向的是a,存放的是a的地址
char* p = "abcdef";
//要注意的是,这个p存放的是a的地址
printf("%d\n", sizeof(p));
//p是一个指针,计算p的大小
//答案是4/8
printf("%d\n", sizeof(p + 1));
//p+1是指向b的地址
//答案是4/8
printf("%d\n", sizeof(*p));
//p是指向a的,p存放的是a的地址,*p的结果就是a这个字符
//类型是char
//计算结果为1
printf("%d\n", sizeof(p[0]));
//p是指向a的,p存放的是a的地址,*p的结果就是a这个字符
//类型是char
//计算结果为1
printf("%d\n", sizeof(&p));
//p是一个指针变量,它的地址是一个二级指针
//还是一个指针,类型为char**
//答案为4/8
printf("%d\n", sizeof(&p + 1));
//&p是一个二级指针,+1后就是指向p变量后面的一个空间
//但是本质还是一个二级指针,类型为char**
//答案为4/8
printf("%d\n", sizeof(&p[0] + 1));
//这个指向的是b这个字符,还是一个指针变量char*
//答案为4/8
printf("%d\n", strlen(p));
//p是一个指针,存放着a的地址
//所以计算结果为6
printf("%d\n", strlen(p + 1));
//p+1指向的是b这个字符
//所以计算结果为5
//printf("%d\n", strlen(*p));
//p指向的是'a',它的ASCII值是97,地址是0x00000061
//不能直接访问这个地址,所以这个代码是错误的
//printf("%d\n", strlen(p[0]));
//p指向的是'a',它的ASCII值是97,地址是0x00000061
//不能直接访问这个地址,所以这个代码是错误的
printf("%d\n", strlen(&p));
//p是一个指针,&p是一个二级指针,类型是char**,指针的类型不一样
//
printf("%d\n", strlen(&p + 1));
//p是一个指针,&p是一个二级指针,类型是char**,指针的类型不一样
printf("%d\n", strlen(&p[0] + 1));
//指向的是b这个字符
//答案是5
这个这个使用的是32位环境
3.二维数组
#include<stdio.h> 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])); }
要时刻注意,我们的核心要点是不会改变的,但是二维数组有一个不同之处是,二维数组的数组名确实是首元素地址,但是二维数组的首元素是第一行这个数组。所以它的数组名是第一行的地址
int a[3][4] = { 0 };
printf("%d\n", sizeof(a));
//这里的a代表的是整个数组,所以计算出来的是4*3*4
//类型是int [3][4]
//答案是48
printf("%d\n", sizeof(a[0][0]));
//a[0][0]是第一行第一列的元素,计算的是首元素的大小
//类型是int
//答案是4
printf("%d\n", sizeof(a[0]));
//a是数组名,代表的是首元素的地址,首元素是整个第一行数组。
//a[0]可以视作*(a+0),也就是对第一行的地址解引用
//得到的是第一行数组,而这个第一行数组又可以看作是一个一维数组的数组名
//sizeof(数组名),此时这个一维数组的数组名代表的是整个数组
//类型是int[4]
//答案是16
printf("%d\n", sizeof(a[0] + 1));
//a[0]是第一行数组,是一个一维数组
//而这个一维数组中,它就相当于一个一维数组的数组名
//代表的是首元素的地址,加一后就是第二个元素的地址
//类型是int*
//答案是4/8
printf("%d\n", sizeof(*(a[0] + 1)));
//a[0]+1是第一行第二个元素的地址
//解引用后就是第一行第二个元素
//类型是int
//答案是4
printf("%d\n", sizeof(a + 1));
//a是数组名,是第一行数组的地址,加一后就是第二行数组的地址
//类型是int(*)[4]
//答案是4/8
printf("%d\n", sizeof(*(a + 1)));
//对第二行的数组解引用,得到的是第二行一维数组的数组名
//sizeof括号里面直接就是一个一维数组的数组名,代表的是整个一维数组
//类型是int[4]
//答案是16
printf("%d\n", sizeof(&a[0] + 1));
//&a[0]是第一行数组的地址,加一后就是第二行数组的地址
//类型是int(*)[4]
//答案是4/8
printf("%d\n", sizeof(*(&a[0] + 1)));
//&a[0]+1是第二行的地址
//解引用后就是第二行的整个数组,代表的是第二行的数组名
//类型是int[4]
//答案是16
printf("%d\n", sizeof(*a));
//a的第一行数组的地址,解引用后就是第一行数组
//类型是int[4]
//答案是16
printf("%d\n", sizeof(a[3]));
//a[3]是第四行的整个数组
//类型是int[4]
//答案是16
二、指针与数组经典笔试题
1.题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; }
这段代码我们画图来分析
最终答案是2和5
2.题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是一个指针变量,是一个结构体指针。它指向的是这个结构体,这个p的值为0x00100000
首先是对结构体加一,跳过整个结构体的字节
结果为0x00100014
然后是将结构体指针强制类型转化为unsigned long类型,这样其实就转化为了普通的正数相加
结果为0x00100001
最后是将这个结构体指针变为了unsigned int* ,是一个指针变量,所指向的数据是unsigned int类型的,是四个字节,所以指针加一就是加四个字节的长度
结果为0x00100004
3.题3
#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; }
4.题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; }
5.题5
#include<stdio.h> 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.题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; }
7.题7
#include <stdio.h> int main() { char* a[] = { "work","at","alibaba" }; char** pa = a; pa++; printf("%s\n", *pa); return 0; }
8.题8
#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; }
这道题比较复杂,我们先画出他们的指针指向分布,如下图所示
有了这个指向分布我们才能继续往下做
首先是**++cpp,对于这个式子,我们要搞清楚优先级,++的优先级比较高,所以先执行++,执行以后,指针的图解变为下图所示
然后我们第一次解引用,找到了cp[1],cp[1]的内容是c+2。c+2所指向的内容是c[2],我们对c[2]再度解引用,得到的是POINT。
然后我们第二次打印的式子中。还有一个前置++,所以我们还需要改变指向,如下图所示
接下来是解引用现在的cpp,得到的是cp[2],cp[2]也就是c+1,但是此时我们又要对cp[2]进行前置--操作,所以c+1,就要变成了c。所以指向就要发生改变改变后的结果如下图所示
接下来又要对这个进行解引用,得到的是c[0],而c[0]是一个字符指针,存放着E的地址,接下来又要+3,得到的是第四个元素E的地址。所以最终的打印结果为ER
接下来的第三个打印,我们此时的指向图如下图所示
我们先使用cpp[-2],这个得到的是cp[0],也就是c+3的地址, 也就是c[3]的地址,然后解引用,得到的是c[3],然后加3,得到的是S的地址,然后打印,最终打印出来的是ST
接下来我们的操作是,cpp[-1][-1],第一次的-1解引用找到的是cp[1],cp[1]指向的是c+2,也就是c[2],然后再次使用[-1],得到的结果是c[1],c[1]存放的是N的地址,然后我们+1,得到的是E的地址,最终打印出来的就是EW
总结
本节讲解了指针和数组的经典笔试面试题,一定要记住那几个原则,以不变应万变
如果对你有帮助,不要忘记点赞加收藏哦!!!
想获得更多优质内容,一定要关注我哦!!!