【C语言航路】第十站:指针(三)深刻理解指针运算

简介: 【C语言航路】第十站:指针(三)深刻理解指针运算



一、深刻理解指针和数组

对于指针和数组,我们必须要要知道的几个核心原则是:

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


总结

本节讲解了指针和数组的经典笔试面试题,一定要记住那几个原则,以不变应万变

如果对你有帮助,不要忘记点赞加收藏哦!!!

想获得更多优质内容,一定要关注我哦!!!

相关文章
|
7天前
|
C语言
指针进阶(C语言终)
指针进阶(C语言终)
|
3天前
|
搜索推荐 程序员 C语言
指针赋值与引用传递:C语言的基础知识与实践技巧
指针赋值与引用传递:C语言的基础知识与实践技巧
|
7天前
|
C语言
指针进阶(回调函数)(C语言)
指针进阶(回调函数)(C语言)
|
7天前
|
存储 C语言 C++
指针进阶(函数指针)(C语言)
指针进阶(函数指针)(C语言)
|
9天前
|
存储 C++
有关【指针运算】的经典笔试题
有关【指针运算】的经典笔试题
14 4
|
7天前
|
编译器 C语言
指针进阶(数组指针 )(C语言)
指针进阶(数组指针 )(C语言)
|
1天前
|
搜索推荐 程序员 C语言
指针赋值与引用传递:C语言的基础知识与实践技巧
指针赋值与引用传递:C语言的基础知识与实践技巧
|
5天前
|
Java 程序员 Linux
探索C语言宝库:从基础到进阶的干货知识(类型变量+条件循环+函数模块+指针+内存+文件)
探索C语言宝库:从基础到进阶的干货知识(类型变量+条件循环+函数模块+指针+内存+文件)
13 0
|
6天前
|
C语言
C语言中的函数指针、指针函数与函数回调
C语言中的函数指针、指针函数与函数回调
7 0