C语言进阶——指针进阶试题讲解(万字长文详解)

简介: C语言进阶——指针进阶试题讲解(万字长文详解

🌆指针(题组四)


在本套题组中,我们将真正从指针的角度出发,通过 sizeof 和 strlen 的不断磨练来拿捏指针,因为指针指向的存储空间是连续的(类似于数组的存储方式),所以本套题组中也有很多知识点与上面重复,先来看看源码吧!

//题组四
//指针登场
 #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;
}



🌃讲解


涉及指针的题多多少少带有些难度,配上其在内存中的布局情况图会更好理解。


题1:


在本套题组中,我们得到的是一个指向字符串的字符型指针 p , 因为字符串在内存中是连续存储的,可以通过首字符地址打印出整个数组。当一级指针 p 指向字符串时,就是指向字符串中的首字符地址(此时的指针 p 有点像数组名),当将指针 p 放入 sizeof 中时,计算的是一个指针所占空间的大小(p 并不是数组名),指针大小在这里(x86环境)是4字节。



4d0fa278fb9b01e82c67b9293456206.png

题2:

第二题中,将字符串型指针 p 向后移动了一位,使其指向字符 b,此时 p+1 依旧是一个指针,sizeof 计算的仍然是指针的大小,结果为4字节。


86ce21febc3fb5c15d7fff161bddb8d.png


题3:


第三题中对字符指针 p 进行解引用,因为此时 p 指向字符 a,对其解引用后就能得到字符 a,因为a 是一个字符型数据,sizeof(*p)  计算的就是一个字符串型数据的大小, 结果为1字节。


b38a159975fdc5dfd76f9d8888441d2.png


题4:


前面说过,在数组 arr 中,*arr 与 arr[0] 效果一样,都是访问数组首元素,这也从侧面说明了指针在访问元素时有两种方式,这里的 p[0] 跟 *p 一样,也能访问到字符 a,sizeof 此时也是在计算 char 型数据的大小,即1字节。  


03350d937db77613b58839cc89ba3b8.png


题5:


p 作为一个字符型指针,对其进行取地址操作后,能得到指针 p 的地址,此时相当于一个二级指针,只要是指针类型,在传给 sizeof 计算后,结果都为4字节(x86环境下)。

f02f038f9be5536cfb37911944c1424.png



题6:


对字符指针 p 的地址+1,指向指针 p 的后一块空间,既然 &p+1 指向其他空间,sizeof 在计算时认为这是一个指针类型变量,因此结果为4字节(x86环境)。

560892f7dd22d8b8522656f69c0bb55.png



题7:

第七题中的 p[0] 表示字符 a,对其进行取地址操作,取出字符 a 的地址(相当于 p),再执行+1操作,此时 &p[0]+1 指向字符 b ,为指针类型的数据,sizeof 计算值为4字节。


7e13ccd6c393fa2dcf46ea654269252.png


关于指针的 sizeof 计算题到此就结束了,下面来看看指针关于 strlen 的运算题。


题8:


前面说过字符串在内存中也是连续存储的,同时自带一个结束标志 \0 ,将指向字符串的指针 p 传入 strlen 中计算,符合传地址的规定,同时也有结束标志,显然 strlen(p) 结果为6。

4f05000a4dab458b452d6186a28e0fb.png



题9:


字符指针 p 指向首字符 a,对 p+1 后,指向第二个字符 b,此时将 strlen(p+1) 计算时的起点不是从字符 a 开始,而且从字符 b 开始,这样一来,计算出的长度就会-1,最终值变为5。

1b5ae5c91ccfd69b336481306c17b49.png



题10:


此题对字符指针 p 进行了解引用操作,得到了首字符 a 的具体值,将 a(97) 传给 strlen 时,strlen(a) 会去访问不能访问的空间(这片空间属于操作系统),运行会报错,此题不会出结果。


977c3d69d28440728193dcaa9085e10.png


题11:

跟上一题差不多,p[0] 得到的也是首字符 a,同样会把 a 的ASCII值传给 strlen,strlen 会依据这个地址去访问空间,但是肯定会访问错误,毕竟操作系统不是谁都能访问。

7998a06af7c723cbeb05b64a8b34228.png



题12:


对指向字符串的字符指针 p 进行取地址操作,得到 p 的地址,将其传给 strlen ,strlen(&p) 会从指针 p 处开始向后比对,因为谁也不知道后面是否有结束标志,因此结果为一个随机数。


e382f4d1a4f97b954ab9a54de7bcb10.png


题13:

十三题中对 p 的地址执行了+1操作,使其向后移动4字节(因为 &p 指向一个 p,+1移动一个指针p),当 strlen(&p+1) 开始运算时,起始地址和上一题已经不一样了,最终得到的随机值也不一样。


4ececbb2ba7fb89191a52f9b76cdb60.png


题14:


目标值我们都已经很了解了,先是取出首字符 a 的地址,对它的地址+1,指向下一个字符 b,当 strlen 从此处开始往后比对时,最终长度会-1,跟题8的思路一致,结果都是5。

5ee71e2a61d4ca603c8cd7f6ab9d3dc.png



本套题组已经全部讲解完毕,其中有些题目涉及到了二级指针,如果不画图的话,是很难理清思路的,所以在处理类似题目时,一定要画图理解!


🌆二维数组(题组五)


这是最后一套题组,为二维数组,其中也会涉及到二级指针、数组指针等概念,此套题组中的二维数组为 int 型,因此只会涉及到 sizeof ,下面来看看源码吧!


//二维数组
#include<stdio.h>
int main()
{
  int arr[3][4] = { 0 };
  printf("%d\n", sizeof(arr));//?
  printf("%d\n", sizeof(arr[0][0]));//?
  printf("%d\n", sizeof(arr[0]));//?
  printf("%d\n", sizeof(arr[0] + 1));//?
  printf("%d\n", sizeof(*(arr[0] + 1)));//?
  printf("%d\n", sizeof(arr + 1));//?
  printf("%d\n", sizeof(*(arr + 1)));//?
  printf("%d\n", sizeof(&arr[0] + 1));//?
  printf("%d\n", sizeof(*(&arr[0] + 1)));//?
  printf("%d\n", sizeof(*arr));//?
  printf("%d\n", sizeof(arr[3]));//?
  return 0;
}


🌃讲解


二维数组中有行和列,对于单行的计算需要仔细思考,对于单行首地址+1的操作也要慎重。


题1:


在第一题中,往 sizeof 中放入了一个数组名 a,此时数组名代表整个数组,sizeof(a) 计算时需要计算元素数 * 类型的值,也就是 12 * 4 = 48,最终结果为48字节。


d362aeb6d73176854efa2e08e644a48.png


题2:


数组名 arr 与[0] 结合,访问第一行,arr[0] 再与 [0] 结合,访问第一行中的第一个元素,此元素为 int 型,sizeof(arr[0][0]) 计算结果为4字节。

930e50c2e44c58e8eeaa362d6ef225a.png



题3:


前面说过,数组名 arr 与 [0] 结合,访问到的是二维数组中的第一行,其中 arr[0] 为第一行元素的首地址(数组名是首元素地址,在 sizeof 中代表整个数组的大小),此时 arr[0] 可以看作这个数组(将这个二维数组看作三个数组的结合体,一个数组中放四个元素)的数组名,放在 sizeof 中表示整行数组的大小,因此 sizeof(arr[0]) 计算的就是一整行数组所占空间大小,为元素数 * 类型,即 4 * 4 = 16字节。

91a867b92bd4c101cf061e1b8dd4788.png



题4:


arr[0] 表示二维数组中首行数组的首地址,+1后会跳过一个元素,指向首行数组的第二个元素,此时 arr[0]+1 是一个地址,地址就是指针,sizeof(arr[0]+1) 计算的就是一个指针类型的大小,这里是4字节(x86)。

261c388ee54052c917664437900bf77.png



题5:


第五题实际上就是对上一题的地址进行了解引用操作,访问到首行二元素的具体值,为一个 int 型数据,sizeof(*(arr[0]+1)) 结果4字节。


678d8050207962696ef9d008bef59ac.png


题6:


数组名 arr 代表首行元素的首元素地址,对此地址+1,会跳过整行元素,此时 arr+1 是一个指向第二行元素首地址的二级指针,sizeof(arr+1) 对地址进行计算,相当于在计算一个指针类型的大小,结果为4字节(x86环境)。

df8b1ddcd72f35bd32051d82ee3bd41.png



题7:

从上图中可以看出,arr+1 是一个 int* 的数据,对其进行解引用后,得到一个指向第二行首元素的地址,也就是第二行元素的首地址,单独放在 sizeof 中,代表整个第二行元素的大小,sizeof(*(arr+1)) 计算的就是整个第二行元素所占空间的大小,为元素数 * 类型大小,即4 * 4 = 16字节。

b0fad566f24fa4a823bbf1b8d572457.png



题8:


arr[0] 为首行首元素地址,取地址后得到指向第二行首元素指针的指针(int*型),对此二级指针执行+1操作,使其跳过一个int*数据,指向 arr[0] 的下一块空间,因为 sizeof 内部表达式不会进行运算,所以没有越界。并且 sizeof 只需要知道类型就能计算出大小,显然这里是一个指针类型,大小为4字节。

713c96ae53fac5614a494650e1f8343.png



题9:


前面说过,&arr[0]+1 是一个指向第二行元素首地址的二级指针,类型为 int* ,解引用后得到第二行首元素的地址,相当于第二行元素的数组名,放在 sizeof 中代表整行元素的长度(大小),经过 sizeof 计算所占空间大小后,结果为 4 * 4 = 16字节。

50fc3bf422f99871da6936558ba9dd8.png



题10:


在二维数组中,*arr 与 arr[0] 所代表的含义一致,因此此题实际上就是在求首行元素所占空间的大小,sizeof(*arr) 就为 4 * 4 = 16字节。


baf550e0ce09333ad24130cc2b751c5.png


题11:


arr[3] 并不在二维数组中,sizeof 在计算时不会去操作那块空间,它只需要知道那片空间的类型就能计算出所占空间的大小,虽然 arr[3] 已经越出了数组,但是编译器在开辟空间时,会多往后面延申空间,此时 sizeof 会根据前面的数据判断 arr[3] 的类型,这里为指针(第三行数组名),计算结果和前面一样,都是16字节。


baed5f1224323770fd820c4bc06bc32.png


🌆小结


至此 sizeof 和 strlen 有关指针的相关题目已经全部讲解完毕了(目前全篇字数已经达到了1w2)


数组名的意义:


1.sizeof(数组名),数组名为整个数组的大小(即元素数量)。

2.&数组名,数组名表示整个数组的地址,对其操作是以整个数组为单位。

3.除以上两种情况外,数组名都是数组首元素地址。

指针的意义:


1.无论类型,指针大小在x86环境下都为4字节,在x64环境下为8字节。

2.strlen 根据指针进行计算时,如果没有结束标志,则输出为随机值。

3.&一级指针,并对其进行+1操作,会跳过整个一级指针。

4.在二维数组中,指向某行元素的一级指针,可以看某行元素的首地址。

5.二维数组的后继空间会依据前面数组的特征进行分配,比如题组五中的题11。



🌆八大指针笔试题


 我们已经走出指针新手村了,下面可以去打怪升级了。目前有八大恶魔阻挡我们前行,要想获得真正的指针之力,需要把它们全部解决掉,话不多说,让我们直接开始闯关!


🌃第一关


//笔试题一
int main()
{
  int a[5] = { 1, 2, 3, 4, 5 };
  int* ptr = (int*)(&a + 1);
  printf("%d,%d", *(a + 1), *(ptr - 1));
  return 0;
}

在第一关中,我们得到了一个大小为5的整型数组a,取出整个数组的地址,对其加1,此时 &a+1 指向数组尾元素的下一块空间,强制类型转化为 int* 并将其赋给 ptr。单独数组名+1,指向的是第二个元素,解引用后得到元素的具体值 2;对 ptr 减一后其指向第五个元素,解引用后得到其具体值 5,因此最终打印结果为 2,5。

ad2cc693f037399bf38c228efc45968.png



🌃第二关

//笔试题二
struct Test
{
  int Num;
  char* pcName;
  short sDate;
  char cha[2];
  short sBa[4];
}*p;
int main()
{
  printf("%p\n", p + 0x1);
  printf("%p\n", (unsigned long)p + 0x1);
  printf("%p\n", (unsigned int*)p + 0x1);
  return 0;
}

在这关中我们遇到了结构体指针,首先我们有一个结构体,大小为 4*1+4*1+2*1+2*1+2*4 =20字节,并创建了一个结构体指针 p (默认为0)指向此结构体,%p 是按十六进制型式打印地址,0x1 相当于1,对指针 p+1 会跳过整个结构体,即加20字节;将结构体指针强制类型转换为 unsigned long 后,相当于一个普通整型数据,+1就是单纯的加1字节;p 强制类型转换为 unsigned int* 后,对其+1表示增加4字节,因为此时是整型指针,步长为4字节。 又因为十六进制中,20表现为14,因此最终打印结果为 00000014、00000001、00000004。

b4209802186c836b38011112a2a1757.png



🌃第三关


//笔试题三
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;
}

第三关与第一关有些许相似,同样的,&a+1 指向尾元素下一块空间,强制类型转换后赋给指针 ptr1,因为 a 为数组名,是首元素地址,是一个十六进制数,强制类型转换为整型后+1,只会取到十六进制中的前两位数,因为是小端存储,所以 int(a) 为1,int(a)+1 为2,2 在内存中存储为 02000000,再将其强制类型转换为 int* ,此时 ptr2 中存储的值就是 02000000。输出时,%x 表示按十六进制打印,与 %p 不同的是 %x 打印时会省略前面的 0,综上所述,最终打印结果为 4,2000000。


fd7caf14a174e4d23bc796c43639452.png


🌃第四关


//笔试题四
#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) 实际上为1,这样一来二维数组 a 中的存储情况就变成了 { 1, 3, 5 }, 将首行数组名赋给整型指针 p,p[0] 相当于 *p ,也就是 *(a[0]) -> *(*(a+0)) -> a[0][0],这关就变成了打印元素 a[0][0] ,也就是元素 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;
}

在这关中,int(*p)[4] 是一个数组指针,可以用来存放二维数组,其中 [4] 表示列,因为这里的二维数组 a[5][5] 中有五列数据,强行放入 p 中会导致位置错位,比如 p[1][0] 相当于 a[0][4] ,因此两个相减会计算出中间的元素数。


6ee8689eccf007c01272799113c185e.png


🌃第六关


//笔试题六
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;
}

&数组名,取出的是整个二维数组的地址,+1的移动步长是整个二维数组,此时 ptr1 指向二维数组尾元素的下一块空间;aa 表示第一行数组的数组名,+1的移动步长是第一行数组,因为 aa+1 是int* 型数据,解引用后才能正确指向第二行数组首地址,将其赋给 ptr2。ptr1 - 1 指向尾元素,解引用后得到 10;ptr2 - 1 指向第一行中的尾元素,解引用后得到 5,结果为 10,5。

0860765cfd0d21c0147010ecf453cac.png



🌃第七关

//笔试题七
#include <stdio.h>
int main()
{
  char* a[] = { "work","at","alibaba" };
  char** pa = a;
  pa++;
  printf("%s\n", *pa);
  return 0;
}

这关是来自阿里巴巴的面试题,work at alibaba,首先 char* a[ ] 是一个指针数组,是一个存放指针的数组,数组名 a 指向首元素地址,也就是 "work" 中 w 的地址,因为字符串的首地址可以看作指针。创建一个二级指针 pa 指向 a,对 pa++ ,使其跳过一个 a,即由原来的指向 "work" 变为指向 "at" ,对 pa 解引用后打印,就是打印字符串 "at" 。

7ab460a3c267d918aad9e3351a56062.png



🌃第八关

//笔试题八
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;
}

 作为我们的大Boss,这关的难度可以说是空前绝后,没有两把刷子还理解不了这题。首先题目给了一级指针数组 c,在其中存放了四个字符串,然后有一个二级指针数组 cp ,其中存放了一级指针数组中的各字符串,最后是一个三级指针 cpp,指向二级指针数组 cp ,好了,准备工作已经做完了,下面可以开始做题了。


35abb5f1ed525d02b8fb97bacf3ea0a.png



83af169b255bbd2674573aedb6cb1af.png




fe70d0e7f1a7d9ea2ec8634369e278c.png


776c1c57099fd99923fdaa5b032cc38.png

 好了,Boss已经打完了,现在你已经学会指针了,可以去各种题目中大杀四方了,指针是把双刃剑,使用需要谨慎,要提防野指针的出现(关于指针的用法)。


🌇总结


 历时十余个小时,字数累计15k+,这篇关于指针进阶试题的讲解文章终于算是划上了一个句号。回顾全文,我们从最简单的整型一维数组开始,到三级指针结束,中间穿插了 sizeof、strlen、指针指向与解引用等知识点,难度也是逐级递进,坚持做到每道题都有配图和讲解(希望这样能让大家得到更好的理解),正因为如此,使得本文的篇幅偏长,如果有不对的地方,欢迎随时指出,我愿洗耳恭听。


如果你觉得本文写的还不错的话,期待留下一个小小的赞👍,你的支持是我分享的最大动力!


如果本文有不足或错误的地方,随时欢迎指出,我会在第一时间改正。



目录
相关文章
|
28天前
|
存储 C语言
【C语言篇】深入理解指针3(附转移表源码)
【C语言篇】深入理解指针3(附转移表源码)
31 1
|
8天前
|
C语言
【c语言】指针就该这么学(1)
本文详细介绍了C语言中的指针概念及其基本操作。首先通过生活中的例子解释了指针的概念,即内存地址。接着,文章逐步讲解了指针变量的定义、取地址操作符`&`、解引用操作符`*`、指针变量的大小以及不同类型的指针变量的意义。此外,还介绍了`const`修饰符在指针中的应用,指针的运算(包括指针加减整数、指针相减和指针的大小比较),以及野指针的概念和如何规避野指针。最后,通过具体的代码示例帮助读者更好地理解和掌握指针的使用方法。
30 0
|
6天前
|
C语言
【c语言】指针就该这么学(3)
本文介绍了C语言中的函数指针、typedef关键字及函数指针数组的概念与应用。首先讲解了函数指针的创建与使用,接着通过typedef简化复杂类型定义,最后探讨了函数指针数组及其在转移表中的应用,通过实例展示了如何利用这些特性实现更简洁高效的代码。
11 2
|
7天前
|
C语言
如何避免 C 语言中的野指针问题?
在C语言中,野指针是指向未知内存地址的指针,可能引发程序崩溃或数据损坏。避免野指针的方法包括:初始化指针为NULL、使用完毕后将指针置为NULL、检查指针是否为空以及合理管理动态分配的内存。
|
7天前
|
C语言
C语言:哪些情况下会出现野指针
C语言中,野指针是指指向未知地址的指针,通常由以下情况产生:1) 指针被声明但未初始化;2) 指针指向的内存已被释放或重新分配;3) 指针指向局部变量,而该变量已超出作用域。使用野指针可能导致程序崩溃或不可预测的行为。
|
13天前
|
存储 C语言
C语言32位或64位平台下指针的大小
在32位平台上,C语言中指针的大小通常为4字节;而在64位平台上,指针的大小通常为8字节。这反映了不同平台对内存地址空间的不同处理方式。
|
13天前
|
存储 算法 C语言
C语言:什么是指针数组,它有什么用
指针数组是C语言中一种特殊的数据结构,每个元素都是一个指针。它用于存储多个内存地址,方便对多个变量或数组进行操作,常用于字符串处理、动态内存分配等场景。
|
13天前
|
存储 C语言
C语言指针与指针变量的区别指针
指针是C语言中的重要概念,用于存储内存地址。指针变量是一种特殊的变量,用于存放其他变量的内存地址,通过指针可以间接访问和修改该变量的值。指针与指针变量的主要区别在于:指针是一个泛指的概念,而指针变量是具体的实现形式。
|
14天前
|
C语言
C语言指针(3)
C语言指针(3)
10 1
|
14天前
|
C语言
C语言指针(2)
C语言指针(2)
11 1