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

目录

一、深刻理解指针和数组

1.一维数组

2、字符数组

(1)字符变量存放到数组中

(2)字符串存放到字符数组

(3) 字符串存放到一个指针中

3.二维数组

二、指针与数组经典笔试题

1.题1

2.题2

3.题3

4.题4

5.题5

6.题6

7.题7

8.题8

总结


一、深刻理解指针和数组

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

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

 


总结

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

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

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

评论 23
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

青色_忘川

你的鼓励是我创作的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值