写点什么

【指针内功修炼】深度剖析指针笔试题(三)

作者:Albert Edison
  • 2022 年 9 月 17 日
    四川
  • 本文字数:8115 字

    阅读完需:约 27 分钟

【指针内功修炼】深度剖析指针笔试题(三)

🌟前言

关于 指针的进阶奥秘 ,我们在上一篇文章中已经接讲过了,这篇文章重点剖析在面试过程中会遇到的 指针的笔试题 

1. 指针和数组笔试题解析解析

我们再来剖析一下 指针和数组笔试题


再这之前,先回顾一下数组的重要知识点


数组名 数组首元素的地址 这里有 2 个例外: sizeof(数组名),这里的数组名是表示整个数组的,计算的是整个数组的大小,单位是字节。 &数组名,这里的数组名也表示整个数组,取出的是数组的地址; 除上面 2 种特殊情况外,所有的数组名都是数组首元素的地址

🍑 一维数组

📝 代码示例


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;}
复制代码


🌟 代码解析


sizeof(a):数组名 a 单独放在 sizeof 内部,计算的整个数组的大小,单位是字节,4*4 = 16; sizeof(a + 0)a 表示的首元素的地址,a+0 还是数组首元素的地址,是地址大小 4/8; sizeof(*a)a 表示的首元素的地址,a 就是对首元素的地址的解引用,就是首元素,大小是 4 个字节 sizeof(a + 1)a 表示的首元素的地址,a+1 是第二个元素的地址,是地址,大小就 4/8 个字节; sizeof(a[1])a[1] 是数组的第二个元素,大小是 4 个字节; sizeof(&a)&a 表示是数组的地址,数组的地址也是地址,地址大小就是 4/8 字节; sizeof(*&a):可以理解为 *& 抵消效果,*&a 相当于 asizeof(a)16&a 等于 int(*)[4];还可以这样理解:&a 是数组的地址,它的类型是 int(*)[4] 数组指针,如果解引用,访问的就是 4int 的数组,大小是 16 个字节; sizeof(&a + 1)&a 是数组的地址,&a+1 跳过整个数组后的地址,是地址就是 4/8; sizeof(&a[0])&a[0] 取出数组第一个元素的地址,是地址就是 4/8; sizeof(&a[0] + 1)&a[0]+1 就是第二个元素的地址,是地址大小就是 4/8 个字节


注意:


<font color=#FF0000 >(1)sizeof 只关注占用空间的大小,单位是字节; (2)sizeof 不关注类型; (3)sizeof 是操作符;</font>

🍑 字符数组

📝 代码示例一


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;}
复制代码


🌟 代码解析


sizeof(arr)arr 作为数组名单独放在 sizeof 内部,计算的整个数组的大小,单位是字节,6; sizeof(arr + 0)arr 就是首元素的地址,arr+0 还是首元素的地址,地址大小就是 4/8; sizeof(*arr)arr 就是首元素的地址,arr 就是首元素,是一个字符,大小是一个字节,1; sizeof(arr[1])arr[1] 就是数组的第二个元素,是一个字符,大小是 1 个字节; sizeof(&arr)&arr 取出的是数组的地址,数组的地址也是地址,地址就是 4/8 个字节; sizeof(&arr + 1)&arr 取出的是数组的地址,&arr+1,跳过了整个数组,&arr+1 还是地址,地址就是 4/8 个字节; sizeof(&arr[0] + 1)&arr[0] 是第一个元素的地址,&arr[0]+1 就是第二个元素的地址,地址就是 4/8 个字节;


📝 代码示例二


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(arr)arr 是首元素的地址,但是 arr 数组中没有 \0,计算的时候就不知道什么时候停止,结果是:随机值; strlen(arr + 0)arr 是首元素的地址,arr+0 还是首元素的地址,结果是:随机值; strlen(*arr)strlen 需要的是一个地址,从这个地址开始向后找字符,直到 \0,统计字符的个数。但是 *arr 是数组的首元素,也就是 'a',这时传给 strlen 的就是 'a'ascii 码值 97strlen 函数会把 97 作为起始地址,统计字符串,会形成内存访问冲突,,所以这里的写法就是错误的; strlen(arr[1]):和上一个一样,内存访问冲突; strlen(&arr)&arrarr 数组的地址,虽然类型和 strlen 的参数类型有所差异,但是传参过去后,还是从第一个字符的位置向后数字符,结果还是随机值。 strlen(&arr + 1)&arr 就是数组的地址,&arr + 1 跳过了整个数组;随机值;如下图

strlen(&arr[0] + 1)&arr[0] 拿出的就是第一个元素 a 的地址,&arr[0] + 1 表示第二个元素 b 的地址,从 b 的位置往后数,也是随机值,和上图一样; 当然我们也还是打印看一下,错误的两个代码我就直接屏蔽不打印


注意:


<font color=#FF0000 >(1)strlen 关注的字符串中 \0 的为止,计算的是 \0 之前出现了多少个字符; (2)strlen 只指针对字符串; (3)strlen 是库函数;</font>


📝 代码示例三


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;}
复制代码


🌟 代码解析


首先 arr 数组在内存中开辟了一块儿空间,连续存放的👇

sizeof(arr)arr 是数组名,单独放在 sizeof 内部,计算的是整个数组的大小,也就是 7 个字节; sizeof(arr + 0)arr 是数组名,这里没有 &,也不是单独放在 sizeof 内部的(因为有个加 0),所以 arr 就是首元素 a 的地址,arr + 0 还是首元素的地址,地址大小就是 4/8;如下图👇

sizeof(*arr)arr 表示首元素 a 的地址,对 arr 进行解引用,就是计算 a 的大小,achar 类型的,占 1 个字节;其实还可以这样理解:*arr 等价于 *(arr+0) 还等价于 arr[0]; sizeof(arr[1]):表示元素 b 的大小,1 个字节; sizeof(&arr)&arr 表示整个数组的地址,地址大小就是 4/8; sizeof(&arr + 1)&arr 就是数组的地址,&arr + 1 跳过了整个数组,不管指向哪里,取出的都是地址,是地址就是 4/8 字节👇

sizeof(&arr[0] + 1)&arr[0] 表示 a 的地址,加 1 表示 b 的地址,地址大小就是 4/8


📝 代码示例四


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;}
复制代码


🌟 代码解析


内存图👇

strlen(arr)arr 表示首元素的地址,也就是第一个元素 a 的地址,strlena 开始往后数,遇到 \0 就停止,也就是长度为 6; strlen(arr + 0)arr 表示首元素的地址,加 0 还是表示首元素的地址,所以 strlena 开始往后数,遇到 \0 就停止,也就是长度为 6; strlen(*arr)arr 表示首元素的地址,对 arr 进行解引用表示数组的首元素,也就是 a,这时传给 strlen 的就是 aascii 码值 97strlen 函数会把 97 作为起始地址,统计字符串,会形成内存访问冲突,,所以这里的写法就是错误的; strlen(arr[1]):和上一个一样,传过去的是字符 bASCII 码值 98,内存访问冲突; strlen(&arr)&arrarr 数组的地址,虽然类型和 strlen 的参数类型有所差异,但是传参过去后,还是从第一个字符的位置向后数字符,遇到 \0 就停止,也就是长度为 6; strlen(&arr + 1)&arr 就是整个数组的地址,&arr + 1 跳过了整个数组,所以是随机值;

strlen(&arr[0] + 1)&arr[0] 拿出的就是第一个元素 a 的地址,&arr[0] + 1 表示第二个元素 b 的地址,从 b 的位置往后数,遇到 \0 就停止,长度为 5


📝 代码示例五


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;}
复制代码


🌟 代码解析


如图👇

sizeof(p)p 是一个指针变量,sizeof(p) 计算的就是指针变量的大小,4/8 个字节; sizeof(p + 1)p 是指针变量,是存放地址的,p+1 也是地址,地址大小就是 4/8 字节; sizeof(*p)pchar* 的指针,解引用访问 1 个字节,sizeof(*p)1 个字节; sizeof(p[0])p[0] 等价于 *(p+0) 还等价于 *p,也就是访问 1 个字节; sizeof(&p)&p 也是地址,是地址就是 4/8 字节,&p 是二级指针; sizeof(&p + 1)&p 是地址, 加 1 后还是地址,是地址就是 4 / 8 字节; sizeof(&p[0] + 1)p[0] 就是 a&p[0] 就是 a 的地址,&p[0]+1 就是 b 的地址,是地址就是 4/8 字节;


📝 代码示例六


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;}
复制代码


🌟 代码解析


如图👇

strlen(p)p 中存放的是 a 的地址,strlen(p) 就是从 a 的位置向后求字符串的长度,长度是 6; strlen(p + 1)p+1b 的地址,从 b 的位置开始求字符串长度是 5; strlen(*p)p 表示首元素的地址,对 p 进行解引用表示数组的首元素,也就是 a,这时传给 strlen 的就是 aascii 码值 97strlen 函数会把 97 作为起始地址,统计字符串,会形成内存访问冲突,,所以这里的写法就是错误的;传内存访问冲突; strlen(p[0]):和上一个一样,传过去的是字符 aASCII 码值 97,内存访问冲突;p[0] 等价于 *(p+0) 还等价于 *p; strlen(&p)&p 取出的是指针变量 p 的地址,它也是存储在内存中的,所以访问的是随机值;

strlen(&p + 1)&p 取出的是指针变量 p 的地址,加 1 表示跳过整个 p,所以访问的是随机值;

strlen(&p[0] + 1)p[0] 等价于 *(p+0) 等价于 *p 等价于 'a'&p[0] 就是首字符的地址,&p[0]+1 就是第二个字符 b 的地址,从第 2 个字符的位置向后数字符串,长度是 5

🍑 二维数组

📝 代码示例一


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;}
复制代码


🌟 代码解析


如图👇

sizeof(a):数组名单独放在 sizeof 内部,计算的是整个数组的大小,344=48; sizeof(a[0][0]):表示第 0 行,第 0 个元素,大小是 4; sizeof(a[0])a[0] 表示第一行的数组名,a[0] 作为数组名单独放在 sizeof 内部,计算的是第一行的大小,也就是 16; sizeof(a[0] + 1)a[0] 表示第一行的数组名,没有 &,没有单独放在 sizeof 内部,所以 a[0] 表示的就是首元素的地址,即 a[0][0] 的地址,a[0] + 1 就是第一行第二个元素的地址,也就是 4/8 字节; sizeof(*(a[0] + 1))a[0] + 1 就是第一行第二个元素的地址,解引用,就是第一行第二个元素,大小为 4sizeof(a + 1)a 表示二维数组的数组名,没有 &,没有单独放在 sizeof 内部,所以 a 表示首元素地址,也就是第一行 a[0] 的地址,加 a + 1 第二行的地址,是类型为 int(*)[4] 的数组指针,是地址也就是 4/8 字节; sizeof(*(a + 1))a + 1 是第二行的地址,*(a + 1) 相当于第二行的数组名,*(a + 1) 等价于 a[1],计算的就是第二行的大小,16 字节; sizeof(&a[0] + 1)a[0] 是第一行的地址,&a[0] 就是第一行的地址,&a[0] + 1 就是第二行的地址,是地址也就是 4/8 字节; sizeof(*(&a[0] + 1))*(&a[0] + 1) 相当于第二行,也就是 a[1],大小是 16 字节; sizeof(*a)a 是二维数组的数组名,没有 &,没有单独放在 sizeof 内部,a 表示首元素的地址,*a 就是二维数组的首元素,也就是第一行,大小为 16 字节;*a 等价于 *(a+0) 还等价于 a[0]; sizeof(a[3]):感觉 a[3] 越界了,但是没关系,a[3] 表示第四行的数组名,它的类型是个一维数组,也就是 int[4],所以大小为 16

🍑 总结

数组名的意义:


<font color=#FF0000 >(1)sizeof(数组名),这里的数组名表示整个数组,计算的是整个数组的大小。 (2)&数组名,这里的数组名表示整个数组,取出的是整个数组的地址。 (3)除此之外所有的数组名都表示首元素的地址。</font>

2. 指针笔试题

🍑 笔试题一

📝 代码示例


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 - 1)&a + 1 的类型为 int(*)[5],然后赋给 ptrptr 是一个整型指针,ptr - 1 向前挪一个整型,指向 5,解引用,相当于把 5 拿出来;

*(a + 1)a 表示首元素的地址,加 1 向后访问一个整型,指向 2 的地址,解引用就是 2


🌟 运行结果


🍑 笔试题二

📝 代码示例


//由于还没学习结构体,这里告知结构体的大小是20个字节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;}
复制代码


🌟 代码解析


p + 0x1p 的值为 0x100000,又因为强制类型转换成了 struct Test*,并且结构体 Test 类型的变量大小是 20 个字节,所以 p+1 也就是 结构体指针 + 1,那么就要跳过一个结构体的大小,也就是跳过 20 字节,20 用十六进制表示就是 0x100014,所以答案也就是 0x100014; (unsigned long)p + 0x1p 强制类型转换成 unsigned long,说明此时 p 就是一个数字,整型数字加 1,就是加上一个 1;也就是 0x100000 + 1,等于 0x100001; (unsigned int*)p + 0x1p 强制类型转换成 unsigned int*,也就是个整型指针,整型指针加 1,就是加 4 个字节,所以就是 0x100000 + 4 等于 0x100004


🌟 运行结果


🍑 笔试题三

📝 代码示例


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;}
复制代码


🌟 代码解析


ptr1[-1]&a 取出的是整个数组的地址,&a + 1 跳过整个数组,然后把这个地址强制类型转换成 int*,并赋值给 ptr1ptr1[-1] 等价于 *(ptr1+(-1)) 还等价于 *(ptr1-1),所以此时就指向元素 4

*ptr2a 是数组名表示首元素的地址,那这个地址强制类型转换成整型,也就是一个指针,加 1,就只是指针加 1,作为地址,只是向后跳了一个字节,那么此时我们就要把内存换分成以字节为单位的顺序,也就是下面这样👇

*ptr2 解引用,就是要访问 4 个字节,也就是访问 0x00000002 一个整型的内容

我是以小端放进去的,要以小端拿出来,所以 *ptr2 还原出来的数字就是:0x02000000


🌟 运行结果


🍑 笔试题四

📝 代码示例


#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;}
复制代码


🌟 代码解析


(0, 1), (2, 3), (4, 5) 是一个 逗号表达式,其实数组里面的内容就是

a[0] 表示第一行第一个元素的地址,所以指针 p 指向就是 1; p[0] 等价于 *(p+0) 也就是等价于 *p,答案就是 1


🌟 运行结果


🍑 笔试题五

📝 代码示例


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 是一个数组指针, p 能够指向的数组是 4 个元素,p+1 往后跳 4 个整型,也就是 16 个字节; p[4][2] 等价于 *(*(p+4)+2)a 是二维数组的数组名,代表首元素的地址; 所以 p = a 表示 p 也指向首元素的地址,那么 p+1 往后跳 4 个整型;

所以就是两个地址相减,小地址减去大地址,得到的就是中间元素的个数,但是这里是负数,也就是 -4


🌟 运行结果


这里解释一下 -4 的十六进制👇

🍑 笔试题六

📝 代码示例


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,取出的是整个二维数组的地址,&aa + 1 跳过整个二维数组,然后强制类型转换成 整型指针, 再赋给 ptr1ptr1 - 1 就是向后挪一个字节,然后解引用,指向元素 10; aa 表示第一行的地址,aa+1 表示跳过第一行,来到了第二行,再解引用,相当于拿到了第二行的数组名,再解引用,相当于拿到了第二行首元素的地址,也就是 6 的地址,然后赋给 ptr2ptr2 - 1 就是向后挪一个字节,然后解引用,指向元素 5


🌟 运行结果


🍑 笔试题七

📝 代码示例


int main(){  char* a[] = { "work","at","alibaba" };  char** pa = a;  pa++;  printf("%s\n", *pa);  return 0;}
复制代码


🌟 代码解析


内存图👇

pachar** 的类型,pa++ 其实就是 pa+1,相当于跳过 1char* 的元素👇

*papa 解引用,向后访问 1 个元素,这个元素里面放的是 a 的地址,打印 at

🍑 笔试题八

📝 代码示例


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;}
复制代码


🌟 代码解析


内存图👇

**++cppcpp 本来指向 c+3 的地址,当 ++cpp 后,跳过 1char** 的元素,指向 c+2 的地址,*++cpp 第一颗星解引用的时候,拿到的是 c+2 指向的元素的地址,然后再解引用 **++cpp,拿到了 char* 里面的值,也就是 P 的地址,这里是 %s 打印,也就是通过 P 的地址向后打印,也就是打印 PONIT; *-- * ++cpp + 3:先计算 ++cpp,上面已经计算过一次 ++cpp 了,所以此时 cpp 跳过 1char* 的元素,指向 c+1* ++cpp 并对其进行解引用,拿到的是 c+1 里面的内容,-- * ++cpp 然后在对其进行减 1,也就是 c+11,变成 c 了,此时这里面指向的元素也发生变化了,也就是指向了数组 c 的首元素的地址,指向 ENTERchar* 了,然后再解引用,就拿到了 char* 所指向的元素 E 的地址,然后再加 3,跳过 N、T,指向 E,再以 %s 打印,输出 ER;👇

*cpp[-2] + 3cpp[-2] 其实就是 *(cpp-2),所以这个表达式就等价于 **(cpp-2)+3,当 cpp-2 时,又回到了指向 c+3 的地址 ,第一个解引用,找到了里面的元素 c+3,第二个解引用,拿到了 c+3 指向的元素,也就是 F 的地址,然后再加 3,就指向 S,再以 %s 打印,输出 ST;👇

cpp[-1][-1] + 1cpp[-1][-1] 其实就是 *(*(cpp-1)-1),所以这个表达式就等价于 *(*(cpp-1)-1)+1,首先 cpp 还是指向 c+1 的位置,cpp-1,指向 c+2 的地址,然后解引用,拿到这个位置的元素,也就是 c+2,然后再减 1,变成了 c+1c 是数组首元素的地址,c+1 表示数组 c 第二个元素的地址,第二次解引用,拿到了 N 的地址,从 N 的地址加 1 指向 E,再以 %s 打印,输出 EW;👇


🌟 运行结果


发布于: 2022 年 09 月 17 日阅读数: 3
用户头像

Albert Edison

关注

目前在某大厂担任后端开发,欢迎交流🤝 2022.03.08 加入

🏅️平台:InfoQ 签约作者、阿里云 专家博主、CSDN 优质创作者 🛫领域:专注于C语言、数据结构与算法、C++、Linux、MySQL、云原生的研究 ✨成就:2021年CSDN博客新星Top9,算法领域优质创作者,全网累计粉丝4W+

评论

发布
暂无评论
【指针内功修炼】深度剖析指针笔试题(三)_C语言_Albert Edison_InfoQ写作社区