9.指针和数组笔试题解析
9.1一维数组
看下面一段代码,分析其打印结果:
#define _CRT_SECURE_NO_WARNINGS 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)); return 0; }
以上代码大家可以自行运行一下。这里我们只做分析。
在分析之前,再次强调一下对于数组名的理解:
数组名代表数组的首元素的地址。
但是有两个例外:
1.sizeof(数组名),这里的数组名表示整个数组,计算的是整个数组的大小,单位是字节
2.&(数组名),这里的数组名表示整个数组,取出的是整个数组的地址
下面我们就可以来分析一下代码了:
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> int main() { //一维数组 int a[] = { 1,2,3,4 }; printf("%d\n", sizeof(a));//数组名单独放在sizeof中,计算的整个数组的大小,4*4=16个字节 printf("%d\n", sizeof(a + 0));//数组名a是数组首元素的地址,a+0还是首元素的地址,地址大小是4/8个字节 printf("%d\n", sizeof(*a));//*a就是数组首元素,大小是4个字节 printf("%d\n", sizeof(a + 1));//a是数组首元素地址,a+1是数组第二个元素地址,是地址,大小就是4/8个字节 printf("%d\n", sizeof(a[1]));//数组第二个元素,大小是4个字节 printf("%d\n", sizeof(&a));//&a取出整个数组的地址,它也是地址,是地址,大小就是4/8个字节 printf("%d\n", sizeof(*&a));//*&a --> a 计算的依旧是整个数组的大小,4*4=16 printf("%d\n", sizeof(&a + 1));//&a+1是跳过整个数组后的地址,是地址,大小就是4/8个字节 printf("%d\n", sizeof(&a[0]));//数组首元素的地址,大小是4/8个字节 printf("%d\n", sizeof(&a[0] + 1));//数组第二个元素的地址,大小是4/8个字节 return 0; }
注意其中 a+1和&a+1的区别:
a+1:a是数组名,数组名是数组首元素地址,a+1就是数组第二个元素的地址。
&a+1:&a取出的是整个数组的地址,所以&a+1是跳过整个数组之后的地址。
它们在内存中的关系如下图:
9.2字符数组
看下面一段代码:
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.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)); return 0; }
分析之前首先要知道数组arr中存储的数据是什么:
这段代码和上文中一维数组类似,只不过int型数据是4个字节,char型数据是1个字节:
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> int main() { //字符数组 char arr[] = { 'a','b','c','d','e','f' }; printf("%d\n", sizeof(arr));//1*6=6 printf("%d\n", sizeof(arr + 0));//arr是数组首元素地址,arr+0还是首元素地址,是地址,大小就是4/8个字节 printf("%d\n", sizeof(*arr));//数组首元素的大小是1个字节 printf("%d\n", sizeof(arr[1]));//数组第二个元素的大小是1个字节 printf("%d\n", sizeof(&arr));//&arr取出整个数组的地址,但依然是地址,大小是4/8个字节 printf("%d\n", sizeof(&arr + 1));//&arr+1是跳过整个数组后的地址,是地址,大小就是4/8个字节 printf("%d\n", sizeof(&arr[0] + 1));//数组第二个元素的地址,是地址,大小就是4/8个字节 return 0; }
接着再看一段代码,我们把上述代码用strlen来计算:
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> #include<string.h> int main() { char arr[] = { 'a','b','c','d','e','f' }; 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; }
先来明确一下strlen函数的功能:求字符串长度,统计的是在字符串\0之前的字符的个数,如果没有\0就会一直往后找。
代码分析:
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> #include<string.h> int main() { char arr[] = { 'a','b','c','d','e','f' }; printf("%d\n", strlen(arr));//因为字符数组中没有\0,所以strlen函数会一直往后找,直到找到\0,此时打印一个随机值 printf("%d\n", strlen(arr + 0));//arr+0是首元素地址,所以和第一个一样,还是随机值 //printf("%d\n", strlen(*arr));//error //printf("%d\n", strlen(arr[1]));//error printf("%d\n", strlen(&arr));//&arr是数组的地址,数组的地址和数组首元素的地址,值是一样的,所以strlen还是从数组第一个元素往后找,这里依然是随机值 printf("%d\n", strlen(&arr + 1));//随机值 printf("%d\n", strlen(&arr[0] + 1));//随机值 return 0; }
注意其中的这两句代码运行时会出现错误:
printf("%d\n", strlen(*arr)); printf("%d\n", strlen(arr[1]));
原因是strlen函数的参数要求接收的是一个地址,而*arr传给它的是数组首元素‘a’,a的ASCII值是97,所以会将97作为地址传参,此时strlen函数从97这个地址开始统计字符串长度,这就非法访问内存了,此时运行就会报错(如下图)。
通过在www.cplusplus.com中查询strlen函数会发现,strlen函数的参数类型如下:
那这时有人要有疑问了,上图中strlen函数的参数类型是const char*型的,而&arr的类型明明是数组指针 char(*)[6], 那为什么这段代码能运行呢?
printf("%d\n", strlen(&arr));
其实这无关紧要,只要把&arr的值传过去,它就会自然而然变成const char*型,而&arr和arr一个是数组地址,一个是数组首元素地址,但它们的值是一样的, 所以strlen还是从数组第一个元素往后找,这里依然是随机值。
下面我们继续看下一段代码:
#define _CRT_SECURE_NO_WARNINGS 1 #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; }
在分析这段代码前,我们先要明确字符数组中存放的内容:
代码分析:
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> #include<string.h> int main() { char arr[] = "abcdef"; 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));//数组组首元素地址,往后统计,直到\0,结果是6 //printf("%d\n", strlen(*arr));//error //printf("%d\n", strlen(arr[1]));//error printf("%d\n", strlen(&arr));//&arr是数组的地址,数组的地址和数组首元素的地址,值是一样的,所以strlen还是从数组第一个元素往后找,结果是6 printf("%d\n", strlen(&arr + 1));//跳过整个数组,同时跳过了\0,往后找是随机值 printf("%d\n", strlen(&arr[0] + 1));//从数组第二个元素往后找,结果是5 return 0; }
注意其中的这代码:
printf("%d\n", strlen(&arr + 1));//跳过整个数组,同时跳过了\0,往后找是随机值
接着看下面一段代码:
#define _CRT_SECURE_NO_WARNINGS 1 #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; }
我们前面学过,可以把字符串存放在字符指针中,字符指针变量中存放的是字符串首字符的地址。
代码分析:
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> #include<string.h> int main() { char* p = "abcdef"; printf("%d\n", sizeof(p));//p是一个指针变量,大小是4/8 printf("%d\n", sizeof(p + 1));//p+1是字符'b'的地址,大小是4/8 printf("%d\n", sizeof(*p));//p中存放的是字符串首字符'a'的地址,*p就是字符'a',大小是1 printf("%d\n", sizeof(p[0]));//p[0]是字符'a',大小是1 printf("%d\n", sizeof(&p));//&p的类型是char**,二级指针,大小是4/8 printf("%d\n", sizeof(&p + 1));//依然是地址,大小是4/8 printf("%d\n", sizeof(&p[0] + 1));//字符串第二个字符的地址,大小是4/8 printf("%d\n", strlen(p));//6 printf("%d\n", strlen(p + 1));//从第二个字符往后统计,是5 //printf("%d\n", strlen(*p));//error //printf("%d\n", strlen(p[0]));//error printf("%d\n", strlen(&p));//随机值 printf("%d\n", strlen(&p + 1));//随机值 printf("%d\n", strlen(&p[0] + 1));//从第二个字符往后统计,是5 return 0; }
注意这两句代码为什么打印出来的是随机值:
printf("%d\n", strlen(&p));//随机值 printf("%d\n", strlen(&p + 1));//随机值
先来看看他们在内存中的关系图:
由此我们可以看到,p中存放的是字符a的地址,&p就是p这4字节空间中的第一个字节的地址,而&p+1是跳过这4个字节后的地址 ,无论是从&p,还是从&p+1往后统计,我们都不清楚什么时候回遇到\0,所以打印出来的结果是个随机值。
9.3二维数组
接下来的内容有点难了,打起精神哦。
#define _CRT_SECURE_NO_WARNINGS 1 #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])); return 0; }
在这之前,我们先来回顾一些关于二维数组的知识:
代码分析:
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> int main() { //二维数组 int a[3][4] = { 0 }; printf("%d\n", sizeof(a));//4*12=48 printf("%d\n", sizeof(a[0][0]));//4 printf("%d\n", sizeof(a[0]));//a[0]代表第一行的一维数组的数组名 //数组名单独放在sizeof内部,sizeof(a[0])计算的是一维数组的大小,大小是16 printf("%d\n", sizeof(a[0] + 1)); //a[0]没有单独放在sizeof内部,也没有& //a[0]表示数组首元素的地址,也就是a[0][0]的地址 //a[0]+1就是第一行第二个元素的地址,是地址大小就是4/8 printf("%d\n", sizeof(*(a[0] + 1)));//*(a[0] + 1)是数组第一行第二个元素,大小是4 printf("%d\n", sizeof(a + 1)); //a是二维数组首元素的地址,也就是第一行的地址 //a+1就是第二行的地址,是地址大小就是4/8 printf("%d\n", sizeof(*(a + 1)));//*(a+1) --> a[1],这里计算的是第二行数组的大小,大小是16 printf("%d\n", sizeof(&a[0] + 1));//&a[0]取出第一行数组的地址,&a[0]+1就是第二行的地址,大小是4/8 printf("%d\n", sizeof(*(&a[0] + 1)));//第二行的大小,16 printf("%d\n", sizeof(*a)); //a是a是二维数组首元素的地址,也就是第一行的地址 //所以计算的是第一行大小,16 printf("%d\n", sizeof(a[3]));//16 return 0; }
注意这段代码:
printf("%d\n", sizeof(a[3]));//16
有人说,这明显越界访问了呀,为什么还能打印出来16?
因为这里的a[3]它的类型是 int [4],当我们知道这一点的时候,它就不会真的去访问内存,而是直接打印出16,因为在计算机中sizeof计算大小是根据类型来计算的。
为了验证sizeof只关注类型,我们用以下代码来证明:
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> int main() { int a = 3; short s = 4; printf("%d\n", sizeof(s = a + s)); printf("%d\n", s); return 0; }
运行结果:
造成这种结果的原因就是:sizeof只关注类型,当我们执行sizeof(s=s+a)时,我们最终的结果会赋给s,而且已经知道了s是短整型,它的大小就是2,此时计算机就不再执行后面的a+s,所以最终s的值没有改变。
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; } //程序的结果是什么?
我们来分析一下:
首先看&a+1,&a取出的是整个数组的地址,那&a+1就是跳过数组之后的地址,此时&a+1的类型是数组指针int(*)[5],通过强制类型转化为(int*),然后将其赋给指针变量ptr,ptr指向的就是跳过数组后的地址,所以*(ptr-1)就是数组最后一个元素5。而*(a+1)是数组第二个元素2。
所以最终的打印结果应该是:2 5。
笔试题2:注意此题是在x86环境下运行的。
//由于还没学习结构体,这里告知结构体的大小是20个字节 struct Test { int Num; char *pcName; short sDate; char cha[2]; short sBa[4]; }*p=(struct test*)0x100000; //假设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个字节。
先来看 p+0x1,p是一个结构体指针,我们讲过,指针的类型决定了指针+-1操作跳过的字节,所以p+0x1跳过20个字节,打印出来的16进制地址是0x100014。
(unsigned long)p现将p的值转换为一个无符号整型数据,此时+0x1,就是在16进制数字0x100000上直接加一,打印出来的应该是0x100001。
(unsigned int*)p将结构体指针p强制类型转换成(unsigned int*),此时进行+1操作跳过4个字节,所以打印结果应该是0x100004。
笔试题3:
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; }
运行结果:
%x打印16进制数。
先来看ptr1[-1],它其实就相当于*(ptr-1),我们在第一题中讲过类似的,显然它打印出来的是4。
而(int)a将a强制类型转换成 int型,此时进行加一操作就是对a的值加一,假设a中的值是0x0012ff40,那(int)a+1的值就是0x0012ff41,而又将0x0012ff41作为地址给ptr2,此时就相当于跳过一个字节,然后向后访问4个字节,此时是小端存储,所以访问到的是00 00 00 02,还原以后就是02 00 00 00,打印时省略第一个0,结果就是2000000。
笔试题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
诶?为什么是1?结果不应该是0吗?
注意这个题有一个小小的坑,它在初始化二维数组时,大括号中用的是小括号,而不是{},所以()里面的是逗号表达式,逗号表达式的值是其中最后一个表达式的值,所以只将1,3,5存放了进去。
而p[0]等价于*(p+0) --> *(a[0]+0) --> *(a[0]),a[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; }
运行结果:
首先我们可以知道的是a的类型是 int(*)[5],而p的类型是 int(*)[4],它们都是数组指针,a+1每次跳过5个整型,p+1每次跳过4个整型,p=a让它们的起始地址都是相同的,此时它们在内存中的关系如下图:
把p也看做一个二维数组会找到&p[4][2]的位置,而&p[4][2] - &a[4][2],两个指针相减得到的是他们之间的元素的个数,又因为随着下标的增长,地址是由低到高变化的,所相减得到的是-4,而-4用%p打印时,它在内存中的补码(11111111111111111111111111111110)作为地址是FFFFFFFC。
笔试题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; }
打印结果:10 5
这个题比较简单,&aa取出整个二维数组的地址,&a+1跳过二维数组后的地址,赋给ptr1,所以*(ptr1-1)是数组最后一个元素10,而*(aa+1)就相当于aa[1],是第二行数组的数组名,即就是aa[1][0]的地址,所以*(ptr2-1)就是aa[1][0]的上一个元素aa[0][5]=5。
笔试题7:
#include <stdio.h> int main() { char *a[] = {"work","at","alibaba"}; char**pa = a; pa++; printf("%s\n", *pa); return 0; }
打印结果:at
char*a[]是一个指针数组,其中存放的是三个char*型的指针,分别指向三个字符串首字符'w','a','a'的地址,而pa是二级指针变量,指向的是'w'的地址,所以pa++后,pa指向‘a’的地址,然后*pa通过%s打印出字符串“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; }
打印结果:
首先,我们来看一下,c、cp、cpp在内存中的关系:
先来看**++cpp:
cpp先++,然后经过两次解引用,因为cpp中存放是cp,所以cp++指向cp+1,而cp+1解引用找到c+2,对c+2解引用找到字符'P'的地址,通过%s打印出POINT。(如下图)
再来看*--*++cpp+3:
明确这条代码中运算符的优先级由高到低依次是:++、*、+,所以+3一定是最后进行的,先进行++cpp,注意经过上条代码后,cpp中存放的是cp+1,所以再进行++cpp,指向的是cp+2,对cp+2解引用得到c+1,对--(c+1)得到c,对c解引用得到字符'E'的地址,+3得到字符'E'的地址,通过%s往后打印出ER。
接着来看*cpp[-2]+3:
它可以写成 **(cpp-2)+3, 经过前两条代码,此时cpp中存放的内容又变成了cp+2,所以cpp-2指向的就是cp+2-2=cp,对cp解引用得到c+3,对c+3解引用得到字符'F'得地址,+3得到字符'S'的地址,通过%s往后打印出ST。
最后来看一下cpp[-1][-1]+1:
它可以写成*(*(cpp-1)-1)+1,此时cpp中的内容还是cp+2,所以cpp-1得到cp+1,对cp+1解引用得到c+2,对c+2-1得到c+1,对c+1解引用得到字符'N'的地址,+1得到字符'E'的地址,通过%s往后打印出EW。
以上就是关于一些关于指针的笔试题及详细解析,
那么今天就学到这里啦,未完待续。。。