【C语言】指针经典题分析

🏖️作者:@malloc不出对象
⛺专栏:《初识C语言》
👦个人简介:一名双非本科院校大二在读的科班编程菜鸟,努力编程只为赶上各位大佬的步伐🙈🙈
在这里插入图片描述


前言

本篇文章不准备给大家讲什么干货,主要是分享一些指针面试题,也是对前面指针部分的知识点做一个巩固吧,,光说不练这是最忌讳的事。

一、指针与数组经典题解析

// 一维数组
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));

// 字符数组
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));

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));

 //字符指针
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));
 
//二维数组
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]));

大家别看着有这么多题,关于上述代码其实考的就是sizeof与strlen的用法,大部分都是一样的没什么很大难度只要细心认真一点,相信对大家来说不是什么问题。

接下来我会一段一段的给大家进行讲解,这里面因为有些采用的是整型数组,而整型数组的每个元素它的字节大小为4,在32位平台下我们指针(地址)的大小也为4,为了让大家能够区分开来清楚的明白我们这里表示的是指针(地址)还是整型元素,以下代码全部在64位平台上进行,在注释时我还是写的4/8,但最终运行结果展示时是在64位平台下进行的。

下面我们先来看看sizeof和strlen的用法:

sizeof的功能是返回对象或者类型所占的字节大小,关于sizeof这个关键字用法也很简单,这里就不做过多的解释了;strlen是求字符串长度,它的原型如下图 size_t strlen(const char* str),它的返回类型为size_t无符号整型,返回值为字符串str的字符数。它的参数为char*型,所以实际上我们的传给strlen的是一个字符串首字符的地址,strlen再通过这个地址往下找,直到遇到\0停止,\0是不计算在内的。

strlen函数的原型及功能用法如下:

在这里插入图片描述

下面我们就来依次分析每一段代码:

#include<stdio.h>
int main()
{
 //一维数组
//除了以下两种情况外,数组名都是首元素地址
//1. sizeof(数组名) - 数组名表示整个数组
//2 .&数组名        - 表示数组的地址
    int a[] = { 1,2,3,4 };
    printf("%d\n", sizeof(a));                   //16,a表示数组名sizeof计算的是整个数组的大小,单位为字节,4 * 4 = 16
    printf("%d\n", sizeof(a + 0));               //4/8,a + 0表示首元素的地址,所以大小为4or8
    printf("%d\n", sizeof(*a));                  //4,a为数组名(首元素地址),*a就为数组首元素
    printf("%d\n", sizeof(a + 1));               //4/8,a + 1表示第二个元素的地址
    printf("%d\n", sizeof(a[1]));                //4,第二个元素
    printf("%d\n", sizeof(&a));                  //4/8,&a是数组的地址,数组的地址也是地址,所以为4or8
    printf("%d\n", sizeof(*&a));                 //16,&a是数组的地址,*解引用得到的是整个数组,所以为整个数组的大小16,或者这样来理解*&抵消了最后就只剩下a了,sizeof(数组名)计算的就是整个数组的大小
    printf("%d\n", sizeof(&a + 1));              //4/8,&a是数组的地址,&a+1表示跳过整个数组的地址,这也是个地址,所以为4or8
    printf("%d\n", sizeof(&a[0]));               //4/8,&a[0]是首元素的地址
    printf("%d\n", sizeof(&a[0] + 1));           //4/8,&a[0]+1是第二个元素的地址
}

结果展示:

在这里插入图片描述

继续分析下一段程序:

#include<stdio.h>
int main()
{
        //字符数组
    char arr[] = { 'a','b','c','d','e','f' };
    printf("%d\n", sizeof(arr));                 //6,sizeof(数组名),计算数组的总大小6 * 1 = 6
    printf("%d\n", sizeof(arr + 0));             //4/8,arr + 0是首元素的地址
    printf("%d\n", sizeof(*arr));                //1,*arr首元素字符'a'
    printf("%d\n", sizeof(arr[1]));              //1,第二个元素
    printf("%d\n", sizeof(&arr));                //4/8,&arr数组的地址,数组的地址也是地址
    printf("%d\n", sizeof(&arr + 1));            //4/8,&arr+1跳过一个数组的地址,也是地址
    printf("%d\n\n", sizeof(&arr[0] + 1));         //4/8,第二个元素的地址
 

    printf("%d\n", strlen(arr));                 //随机值,arr是数组名(首地址),我们从首地址开始找直到找到\0才停止,而我们这个数组后面的区域是不可预知的,所以会是一个随机值
    printf("%d\n", strlen(arr + 0));             //随机值,arr+0是首地址,同上
    printf("%d\n", strlen(*arr));                //err,*arr表示首元素,而我们的strlen找的是地址,'a'的ASCII值为97,strlen把97就当做了地址,而97不一定是这个程序的地址,这就造成了非法访问内存,程序会崩溃
    printf("%d\n", strlen(arr[1]));              //err,arr[1]表示第二个元素,同上解释
    printf("%d\n", strlen(&arr));                //随机值,&arr是数组的地址,strlen从数组的起始位置开始找,直到找到\0为止,这个数组后面的情况是不可预知的,所以会产生一个随机值
    											//这个地方还会有一个警告,&arr - 数组的地址 - 要用数组指针来接收应该是 char (*)[7]来接收,而strlen的类型是char*,它们的类型虽不兼容,但强行赋给strlen还是可行的,不影响最后的结果
    printf("%d\n", strlen(&arr + 1));            //随机值-6,&arr+1是跳过一个数组的地址,strlen从这个位置找虽然结果也是不可预知,但和从起始位置开始的差值始终为6,
    											//因为从起始位置开始一直到跳过该数组的位置都没有找到\0,往后找到\0时,这个位置要比从起始位置开始找少一个数组的长度。
    											//举个例子,你站在100的跑道上的10m处的位置上,而别人站在起始位置开始跑,那么你最后跑的距离是不是要比别人少个10m。
    printf("%d\n", strlen(&arr[0] + 1));        //随机值-1,同上解释
}

来看结果:

在这里插入图片描述

你会发现程序运行到第10个时,程序崩溃了后面的结果也不会打印出来了,这时我们为了看到后面的结果就将那两个错误的代码注释掉了,下面放图:

在这里插入图片描述

继续分析下一段代码:

//这段代码就比较简单了,跟上段代码基本是差不多的
#include<stdio.h>
int main()
{
    char arr[] = "abcdef";                    //字符串末尾会自动补\0在后面,\0是字符串结束的标志
    printf("%d\n", sizeof(arr));              //7,sizeof()计算数组的大小,\0是一个转义字符也会被计算进去
    printf("%d\n", sizeof(arr + 0));          //4/8,arr+0首元素地址
    printf("%d\n", sizeof(*arr));             //1,*arr是首元素
    printf("%d\n", sizeof(arr[1]));           //1,arr[1]第二个元素
    printf("%d\n", sizeof(&arr));             //4/8,&arr数组的地址
    printf("%d\n", sizeof(&arr + 1));         //4/8,&arr+1跳过该数组的地址
    printf("%d\n\n", sizeof(&arr[0] + 1));    //4/8,第二个元素的地址
    

    printf("%d\n", strlen(arr));              //6,arr首元素地址,strlen从这个位置开始找找到\0就停止,\0不计算在内
    printf("%d\n", strlen(arr + 0));          //6,arr+0首元素地址
    printf("%d\n", strlen(*arr));             //err,*arr是第一个元素的地址,非法访问
    printf("%d\n", strlen(arr[1]));           //err,同上
    printf("%d\n", strlen(&arr));             //6,&arr数组的起始地址,从这个位置开找答案为6
    printf("%d\n", strlen(&arr + 1));         //随机值,&arr+1跳过该数组的地址,数组后面的区域内容是不可预知的,所以是随机值
    printf("%d\n", strlen(&arr[0] + 1));      //5,从第二个元素的位置开找,答案为5
    return 0;
}

给出结果:

在这里插入图片描述

下一段程序:

#include<stdio.h>
int main()
{
       //字符指针
    char* p = "abcdef";                     //这里的p是一个指针变量,它存放的是'a'的地址,而不是整个字符串的地址              
    printf("%d\n", sizeof(p));              //4/8,'a'的地址
    printf("%d\n", sizeof(p + 1));          //4/8,'b' 的地址
    printf("%d\n", sizeof(*p));             //1,*p就是'a',一个字节
    printf("%d\n", sizeof(p[0]));           //1,这里假设int arr[10] ; a [0]== *(arr+0),我们这里类比一下就是p[0] ==*(p+0)---->'a'
    printf("%d\n", sizeof(&p));             //4/8,取出指针变量p的地址,答案就为4or8,这里的p的地址要用char**  =  &p来存
    printf("%d\n", sizeof(&p + 1));         //4/8,地址加+1,移动一个单位还是一个地址
    printf("%d\n\n", sizeof(&p[0] + 1));    //4/8,p[0]是'a',&p[0]就是'a'的地址+1就跳到了下一个元素的地址即'b'的地址
 

    printf("%d\n", strlen(p));              //6,p存的是'a'的地址,p将'a'的地址传给了strlen,往后找直找到\0
    printf("%d\n", strlen(p + 1));          //5,p+1就是'b'的地址,同上
    printf("%d\n", strlen(*p));             //err,*p-->'a','a'是一个字符,而我们的strlen要的是地址,所以strlen把'a'就当做地址传入了,但此时我们并不知道'a'处的地址是否我们有权限访问,所以你贸然的访问这个地址就造成了非法访问
    printf("%d\n", strlen(p[0]));           //err,同上
    printf("%d\n", strlen(&p));             //随机值,这里相当于用一个二级指针存放着p的地址,但此处地址后面的内容是不可预知的,,就相当于我们在盲找\0
    printf("%d\n", strlen(&p + 1));         //随机值,同上
    printf("%d\n", strlen(&p[0] + 1));      //5,&p[0]是'a'的地址,+1就是'b'的地址,从'b'这个位置开始找\0,答案为5
    return 0;
}

结果展示:

在这里插入图片描述

终于到了最后一段代码了,二维数组这里的关系要稍微复杂一点,我们要理清思路仔细分析:

#include<stdio.h>
int main()
{
    //二维数组
    //提到二维数组,我们一定要将它与一维数组联系起来,首先我们将二维数组看成一维数组,以这里的二维数组为例,我们有三个元素,每个元素分别是a[0]数组、a[1]数组、a[2]数组;
    //每个元素又是一个一维数组,所以a[0]、a[1]和a[2]就是一个数组名(类型为int*),一维数组里面的每个元素类型为int。
    int a[3][4] = { 0 };    
    printf("%d\n", sizeof(a));              //48,sizeof(数组名)计算整个二维数组的大小                    
    printf("%d\n", sizeof(a[0][0]));        //4,第一行第一列元素
    printf("%d\n", sizeof(a[0]));           //16,a[0]是a数组的第一个元素,a[0]表示数组名,sizeof(数组名)计算的是该数组的大小,该数组有四个元素每个元素的类型为int,所以大小为4*4=16
    printf("%d\n", sizeof(a[0] + 1));       //4/8,在sizeof中这里出现了(a[0]+1),不再是一个单独的数组名,它表示数组的首元素地址,这里的a[0]其实代表的是a[0][0]元素的地址,a[0][0]+1就是a[0][1]元素的地址
    printf("%d\n", sizeof(*(a[0] + 1)));    //4,*(a[0]+1)表示的是第一行第二个元素
    printf("%d\n", sizeof(a + 1));          //4/8,在sizeof中,数组名a没有单独出现表示它为首元素的地址,首元素的地址就是&a[0](第一行的地址,一维数组的地址),+1就跳到了第二个元素的地址,第二个元素的地址就是&a[1],它表示数组的地址,数组的地址也是地址,所以答案为4or8。
    printf("%d\n", sizeof(*(a + 1)));       //16,(a+1)== &a[1]-->*&a[1] == a[1],它为一个数组名,所以计算的就是该数组的大小4*4=16
    printf("%d\n", sizeof(&a[0] + 1));      //4/8,&a[0]表示第一个元素的地址,+1就跳到下一个元素的地址&a[1],a[1]为数组名&a[1]就表示数组的地址,所以答案为4or8
    printf("%d\n", sizeof(*(&a[0] + 1)));   //16,*&a[1] == a[1],它表示第二行的数组名,sizeof计算该数组的大小4*4=16;
    printf("%d\n", sizeof(*a));             //16,*a就是第一个元素a[0]-->sizeof(a[0]) = 4*4 =16
    printf("%d\n", sizeof(a[3]));           //16,这里我们可能想到数组越界的问题,但sizeof它关注的只是它的类型,而不参与其中的运算。此时a[3]是一个数组名,4 * 4 = 16
    return 0;
}

看向下面一幅图相信大家关于将二维数组看成一维数组就能理解了:

在这里插入图片描述

总结:我们先从一维数组说起:(假设:int a[4])
1.一维数组名a是个地址,地址类型为int*
2.一维数组名取地址&a是个地址(数组指针),它取出的是数组的地址,地址类型为:int(*)[4],该指针指向一个含4个int元素的一维数组。
3.a和&a的值虽然是一样的,但含义却是天差地别。如果你写成这样a+1和&a+1,这样就能明显的看出不同了,这点在之前的指针学习中我也讲到过这个知识点。
再来看二维数组a[3][4],它可以看做一个含3个一维数组的成员,每一个成员中又有4个int元素,根据一维数组的结论我们来推断一下二维数组:
1.a[0]是数组的第一个成员,它表示为第一个数组数组名,地址类型为int*,它其实表示的是元素a[0][0]的地址
2.&a[0]也是个地址,不过它是a[0]数组的地址,地址类型为int(*)[4],它表示的是行地址(一维数组的地址)。
虽然a[0]和&a[0]的值是一样的,但它们的含义是不一样的。
3.a是个地址,它表示首元素的地址,首元素为一个一维数组,一维数组的地址的类型就为:int(*)[4],它等同于&a[0],那么a+1表示第二个元素的地址,相等于&a[1]。
4.&a是个地址,是整个二维数组的地址,地址类型为:int(*)[3][4],该指针指向一个含3*4个int元素的二维数组,&a+1表示跳过整个二维数组的地址。
最后附上在二维数组中的关系表达式:
(1) a[0] == &a[0][0] ==> a[0]+1 == &a[0][1]
(2) a == &a[0] ==> a + 1 == &a[1]

这部分题只要大家认真分析了其实真的不算很难,细心一点你也可以的orz~,最后给出这部分题的答案:

在这里插入图片描述

二、经典的八道指针笔试题

我先给出题目,大家先做一做一定要细心一点,如果有卡壳的地方可以画个图说不定就清楚了,最后我会一一进行解析。

笔试题1:

#include<stdio.h>
int main()
{
    int a[5] = { 1,2,3,4,5 };
    int* ptr = (int*)(&a + 1);

    printf("%d %d\n", *(a + 1), *(ptr - 1));

    return 0;
 
}

笔试题2:

#include<stdio.h>

struct Test
{
    int Num;
    char* pcName;
    short sData;
    char cha[2];
    short sBa[4];
}*p;
//已知该结构体在32位平台下的字节大小为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;
}

笔试题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:

#include<stdio.h>

int main()
{
    int a[3][2] = { (0,1),(2,3),(4,5) };
    int* p;
    p = a[0];
    printf("%d\n", p[0]);

    return 0;
}

笔试题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:

#include<stdio.h>

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:

#include<stdio.h>

int main()
{
    char* a[] = { "work","at","hncu" };
    char** pa = a;
    pa++;
    printf("%s\n", *pa);

    return 0;
}

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

2.1 强制类型转换

在讲下面的笔试题之前,我想给大家讲一讲强制类型转换,因为下面的很多题目也涉及到了强制类型转换这个知识点,并且也是我们经常使用到的一个知识点。

在讲这个问题之前我想给大家举一个小小的例子:有一天,你的好朋友从别人那里得知你们班上的xxx同学干了什么什么坏事,此时在你心里就树立起了一个概念:它是一个坏人,于是呢你也感觉每次见到他时就感觉他长得凶神恶煞的,不像是一个好人;又有一天,你的好朋友从别人得知你们班上的xxx同学经常干了xxx好事,于是你在心里就又树立起一个概念:它是一个好人,于是呢你每次见到他就觉得他长得眉清目秀的。这其实都是个人的心里作用,其实本质上这两名同学变化了吗?根本就没有变化,变化的是你看待他们的方式。

所以讲这个例子想跟大家阐述一个结论:强制类型转换的本质是改变看待内容的方式,对内容本身是不会产生任何影响的!在C中你可以认为它改变的就是数据类型。

那么我们来看一个例子,写一个算法要你实现将一个字符串"12345"转换成十进制数12345,那么此处能使用强制类型转换(int)"12345"直接将其变为十进制数12345吗?

答案是不能的,因为它一定是要改变数据本身的,它可能是这样实现的先解引用得到字符1,再乘以10000,接着地址再偏移找到字符2,乘以1000…但这样字符串"12345"本身就发生了变化,所以它不是强制类型转换。

接下来再来看看强制类型转化的简单例子:

#include<stdio.h>

int main()
{
	int a = 97;
	printf("%d\n", a);
	printf("%c\n", (char)a);

	return 0;
}

我们进入调试观察一下:
强转前:

在这里插入图片描述

强转后:

在这里插入图片描述

我们发现强转其实就是改变了97的类型,int a = 97;此时的97为整型占32个bit位,而(char)a它此时97为char型占8个bit位,,但97还是那个97它是不会发生任何改变的,改变的是看待它的方式。

强制类型转换的目的?

如果一个运算符两边的运算数据类型不同,先要将其转换为相同的类型,强制类型转换既可以消除程序中的警告,又确保了写代码的程序员清楚此时的类型转换。它是允许丢失一定精度或做类型匹配的。

那么强制类型转换的规则是什么呢?

普通变量的强转规则

当较低类型的数据转换为较高类型时,一般只是形式上有所改变, 而不影响数据的实质内容;而较高类型的数据转换为较低类型时则可能有些数据丢失。如果一个运算符两边的运算数类型不同,先要将其转换为相同的类型,即将运算符右侧数值转换为运算符左侧类型,同侧低精度类型转换为高精度类型,然后再参加运算,转换规则如下图所示。

在这里插入图片描述

那么对于普通变量的类型转换我个人的建议是:尽量由低类型转为高类型,因为一般它只是形式上有所变化,而不影响数据实质内容;但是由低类型转为高类型的话,数据发生了截断有可能此时得到的数据变化了(这里要跟我们前面讲到的内容本身发生了变化区分开来,,此时我讲的是进行强转之后得到的数据,,例如:假设97经过强转之后发生截断得到的数据是68,,那么此时得到的是强转后的内容发生了变化,,而非97发生了变化哈~),,既然得到的数据发生了变化,,那么此时你将它强转为一个指针类型你觉得它不会发生错误吗?这里可能有些读者听的还不是特别的明白,下面我举一个例子进行说明:

#include<stdio.h>

int main()
{
	int a[] = { 0, 1 };
	int* pa = (int*)((int)a + 1);
	printf("%x\n", *pa);

	return 0;
}

这个例子其实跟接下来要进行讲解的笔试题3是几乎一样的,在x86和x64下的结果是截然不同的,x64甚至会发生崩溃,下面我们一起来看看x86时的:

在这里插入图片描述

关于这段代码我就不做过多的解释了,因为下面的笔试题3讲解的很详细,这里我的重点是想让大家明白高类型向低类型转化之后得到的数据了会发生什么问题,下面来看看x64环境下的:

在这里插入图片描述

在x64环境下为什么直接崩溃了呢?这是因为此时数组名为一个常量指针在x64环境下占8个字节,而你将它强转为一个(int)a,必定会发生截断,而截断后数据发生了丢失,你将这个截断后的地址最后再用指针变量pa保存起来,,那么通过解引用去访问它必定会出现问题的,因为你不知道此时的地址你是否有权限可以访问它,,因此程序会直接崩溃掉。

我举这个例子是想跟大家说明,在一些情况下将高类型向低类型转化此时发生截断,数据部分丢失了,那么可能会出现一些不可预料的错误…所以我的个人建议是要仔细判断强转后得到的数据会不会对程序产生影响。这里还举一个例子,你将小瓶子的水放到大瓶子里面是能够存放的吧,而一旦你将大瓶子里面的水放在小瓶子里面是不是会漏出来呢,这里就是这个原理。

指针变量的转换规则:

对于指针变量来说,其实它们都是可以互相赋值的,因为它们的内容都是一个地址,但是对于指针变量所指向的类型那就有意义了,因为指针变量所指向的类型决定了它进行解引用能够访问的空间大小以及进行±操作移动的步长。

我们来看一个例子:

#include<stdio.h>

int main()
{
	int a = 10;
	int* p = &a;
	char* pa = &a;
	printf("%p\n", &a);
	printf("%p\n", p);
	printf("%p\n\n", pa);

	p++;
	pa++;
	printf("%p\n", p);
	printf("%p\n", pa);


	return 0;
}

我们一起来看看结果:

在这里插入图片描述

我们发现指针变量的确是可以接收任意类型的指针(地址),但是指针变量的所指向的类型决定了它进行±操作移动的步长以及访问空间的大小。

并且你还可以看到此时编译器报了一个警告说char*与int*的类型不兼容:

在这里插入图片描述

此时强制类型转换的作用就出来了,我们可以显示的将它声明为同种数据类型,这样既消除了程序中的警告,又确保写代码的程序员清楚此时的类型转换。

在这里插入图片描述

关于强制类型转换就给大家讲到这里了。


接下来我来带大家分析每一段代码:

笔试题一:

#include<stdio.h>
int main()
{
    int a[5] = { 1,2,3,4,5 };
    int* ptr = (int*)(&a + 1);				//&数组名表示数组的地址,&a+1就表示跳过该数组的地址,我们数组的地址应该用数组指针来接收,但这里将它强制类型转化为int*,虽然类型不兼容,但将地址强行赋给p也是没有问题的
    printf("%d %d\n", *(a + 1), *(ptr - 1));//(a+1)-->a表示数组名(首元素地址),(a+1)就表示第二个元素的地址,*(a+1)就是第二个元素的值;
                                           //ptr指向的是跳过整个数组的地址,-1就指向了数组最后一个元素的地址,*(ptr-1)就是数组最后一个元素的值                                                 
    return 0;
 
}

我们通过图来直观的感受一下,绿色部分是数组以外的区域:

在这里插入图片描述

结果展示:

在这里插入图片描述

笔试题2:

#include<stdio.h>

struct Test
{
    int Num;
    char* pcName;
    short sData;
    char cha[2];
    short sBa[4];
}*p;
//已知该结构体在32位平台下的字节大小为20,64位平台下字节大小是32。这里涉及到了结构体内存对齐的知识,以后我会讲到的,请大家尽情期待一下🙈

int main()
{
    p = (struct Test*)0x100000;
    printf("%p\n", p + 0x1);   	  //这里的0x1十六进制化为十进制就是1,我们知道指针类型决定了指针每移动一步走多大的距离;
                                  //这里为结构体类型所以每移动一个单元,跳过20个字节,20化为十六进制为14,所以最终的结果为00100014,32位平台下十六进制数占8位,不足八位前面补0
    printf("%p\n", (unsigned long)p + 0x1);//先将p强制类型转换为unsigned long型,此时p就是一个数值了,那么它加上1就是数值上加一,所以结果为00100001
    printf("%p\n", (unsigned int*)p + 0x1);//将p强制类型转换为unsigned int*型,那么加1跳过的就是4个字节大小的距离,所以结果为0010004

    return 0;
}

结果展示:

在这里插入图片描述

笔试题3:

#include<stdio.h>

int main()
{
    int a[4] = { 1,2,3,4 };
    int* ptr1 = (int*)(&a + 1);//ptr指向的是跳过该数组的地址
    int* ptr2 = (int*)((int)a + 1);//(int)a先将a数组名(首元素地址),强制类型转化为int型再+1,最后再强制类型转换为int*型,其实这个过程就是移动了一个字节大小的距离

    printf("%x %x", ptr1[-1], *ptr2);//ptr[-1] == *(ptr-1),就是数组最后一个元素的值;*ptr2它指向的类型为int型占4个字节,最后再与十六进制的形式打印出来


    return 0;
}

在这里插入图片描述
最终结果图:

在这里插入图片描述

笔试题4:

#include<stdio.h>

int main()
{
    int a[3][2] = { (0,1),(2,3),(4,5) };//逗号表达式以最后一个值为准,所以数组里面应该为{1,3,5}
    int* p;
    p = a[0];//这里注意a[0]表示的是二维数组a的首元素,a[0]它是一个数组名
    printf("%d\n", p[0]);//a[0][0] == p[0] == *(p+0) == *(a[0]+0) == *(*(a+0)+0)其实就是第0行第0列的元素

    return 0;
}

结果展示:

在这里插入图片描述

笔试题5:

#include<stdio.h>

int main()
{
    int a[5][5];
    int(*p)[4];
    p = a;		//这里指针p的类型为int(*)[4],而a的类型为int(*)[5],它们的下标不同,但是不影响将a的首地址赋给p
    printf("%p,%d\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]);

    return 0;
}

我们知道p是一个指向数组长度为4且数组中每个元素类型为int的指针,p[4][2] == *(*(p+4)+2),*(p+4)就是跳到第四个元素,第4个数组,这里的p指向的数组长度为4而非5,所以在二维数组a中移动一个单位不是跳过一个元素,这里要注意一下;*(p+4)+2就是第4行第2列元素的地址,
*(*(p+4)+2)就是第四行第二列元素的值。
我们知道地址减地址得到的是元素的个数,这里的&p[4][2]为低地址,&a[4][2]为高地址,低地址-高地址为负数,故%d打印出来为-4,如果以%p的形式打印出来,我们知道地址为无符号数,所以-4的补码即原码,我们来写一下-4的补码:
10000000 00000000 00000000 00000100 // 原码
11111111 11111111 11111111 11111011 // 反码
11111111 11111111 11111111 11111100 // 补码
1111 1111 1111 1111 1111 1111 1111 1100,化为十六进制形式:0XFFFFFFFC

在这里插入图片描述

结果展示:

在这里插入图片描述

笔试题6:

//这道题是比较简单的,我们简单通过画一下图就能搞定
#include<stdio.h>
int main()
{
    int aa[2][5] = { 1,2,3,4,5,6,7,8,9,10 };
    int* ptr1 = (int*)(&aa + 1);//ptr1是跳过该数组的地址
    int* ptr2 = (int*)(*(aa + 1));//ptr2是二维数组aa的第二个元素(第二个一维数组的首元素)的地址
    printf("%d,%d", *(ptr1 - 1), *(ptr2 - 1));

    return 0;
}

我们通过图来观察一下:

在这里插入图片描述

结果展示:

在这里插入图片描述

笔试题7:

#include<stdio.h>
int main()
{
    char* a[] = { "work","at","hncu" };//这里的数组为一个字符指针数组,它存放的是指针(地址),它的每个元素的类型是char*型。
    char** pa = a;			//pa存放的是a的首元素地址
    pa++;					//pa++跳到下一个元素的地址
    printf("%s\n", *pa);	//%s打印的是char*型,*pa的类型就是char*

    return 0;
}

看向下图展示:

ng)

结果展示:

在这里插入图片描述

笔试题8:

//最后一题是最复杂的一道,我们一定要搞清楚cpp和cp的变化,我会通过画图来给大家分析
#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
在这里插入图片描述

我用四种不同的颜色代表了每一步的过程,第一个结果打印出来就是"POINT"了。

再来分析第二个结果:*-- * ++cpp + 3
在这里插入图片描述

第二个结果就为“ER”了,继续下一个结果的分析:*cpp[-2] + 3

在这里插入图片描述

第三个结果就为“TR”了,接着我们来分析最后一个结果:cpp[-1][-1] + 1

在这里插入图片描述

这里我们需要注意cpp和cp中元素值的变化,第一个二个运算改变了cpp的值,而第三四个只是进行了运算,实际上并没有改变cpp的值,所以我们第三四个cpp指向cp的开始位置一直没有变化,你们可以看下我的第三四个cpp的始位置一直是指向cp的第三个元素的地址的,红色箭头代表始位置。

最后展示一下最终的结果:

在这里插入图片描述

关于指针笔试题的分析就讲解到这儿了,相信细心一点对大家来说都不会很难的,今天的文章分析就到这儿了,如果有任何疑问或错处,欢迎大家评论区相互交流啊orz~🙈🙈

  • 21
    点赞
  • 36
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 16
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 16
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

malloc不出对象

你的鼓励将是我创作的最大动力

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

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

打赏作者

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

抵扣说明:

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

余额充值