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

news2025/1/29 8:35:43

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

目录

    • 前言
    • 一、指针与数组经典题解析
    • 二、经典的八道指针笔试题
      • 2.1 强制类型转换


前言

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

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

// 一维数组
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~🙈🙈

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/145127.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

创新的概念、设计和生产鞋类和鞋类软件丨Jevero及Botcha 3D功能简介

Jevero功能简介 重新定义鞋类发展 Jevero是图案工程师、鞋类开发人员和设计师的优秀支持。从设计到生产都在一个工具中完成。 产品功能及优势 01、更快的开发&#xff0c;缩短上市时间 Jevero使您的图案工程师、鞋类开发人员、工业设计师之间能够进行协作。利用Rhino平台产…

两数相加 java语言

leetcode地址&#xff1a;两数相加描述&#xff1a;给你两个 非空 的链表&#xff0c;表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的&#xff0c;并且每个节点只能存储 一位 数字。请你将两个数相加&#xff0c;并以相同形式返回一个表示和的链表。你可以假设除…

DevData Talks | 张乐、茹炳晟、应阔浩、任晶磊:研发效能实践的2022年复盘和展望

跌宕起伏的 2022 年已经成为过去时。在这一年&#xff0c;我们既看到外部环境变幻莫测&#xff0c;也看到研发效能行业沉下心来稳步发展&#xff0c;从宏大的概念和价值&#xff0c;转向具体的问题&#xff0c;和务实、可行动的解决方案。 在新一年的开端上回望&#xff0c;20…

靶机测试CyNix笔记

靶机测试CyNix笔记 靶机描述 Level: Intermediate-HardUser flag: user.txtRoot flag: root.txtDescription: It’s a Boot2Root machine. The machine is VirtualBox compatible but can be used in VMWare as well (not tested but it should work). The DHCP will assign …

webpack中模块加载器Loader、插件plugins、optimization属性

目录 模块加载器&#xff08;Loader&#xff09; 导入css文件 加载图片 方法一 方法二 转换es6&#xff08;向下兼容es5&#xff09; html代码组件导入导出 导入less文件 自定义loader&#xff08;Markdown文件加载器&#xff09; markdown-loader.js文件 webpack.c…

【Linux】程序的翻译过程(图示详解)

因为淋过雨&#xff0c;所以懂的为别人撑伞&#xff1b;因为迷茫过&#xff0c;所以懂得为别人指路。 我们都知道写好代码后&#xff0c;编译器会帮助我们把代码生成可执行程序&#xff0c;细加了解又会知道程序的生成又分为四步&#xff1a;预处理、编译、汇编、链接。那么这四…

STM32MP157驱动开发——Linux IIO驱动(上)

STM32MP157驱动开发——Linux IIO驱动&#xff08;上 &#xff09;0.前言一、IIO 子系统简介1.iio_dev 结构体2.iio_dev 申请与释放3.iio_dev 注册与注销4.iio_info5.iio_chan_spec二、驱动开发1. ICM20608 的 IIO 驱动框架搭建2.IIO 设备申请与初始化3.基于以上驱动框架开发 I…

[JavaEE初阶] 线程安全问题的原因和解决方案

努力努力,月薪过亿!!! 格局打开~~~ 文章目录前言1. 线程安全问题的概念2. 线程安全问题的原因3. 线程安全问题解决--加锁3. synchronized4. 死锁4.1 产生死锁的情况4.3 产生死锁的必要条件4.4 避免死锁的方法前言 线程安全这里可能会出道面试题,在日常工作中也是很重要的内容.…

MathType公式对齐不正确

MathType公式对齐不正确1.软件环境⚙️2.问题描述&#x1f50d;3.解决方法&#x1f421;4.1.通过标尺对齐4.2.通过输入具体的制表符位置对齐1.软件环境⚙️ Windows10 教育版64位 Word 2021 MathType 7 2.问题描述&#x1f50d; 在使用Word写论文的时候&#xff0c;总是避免不…

JavaScript 模块:理解模块系统

前言 现代JavaScript开发毋庸置疑会遇到代码量大和广泛使用第三方库的问题。解决这个问题的方案通常需要把代码拆分成很多部分&#xff0c;然后再通过某种方式将它们连接起来。 在ECMAScript 6模块规范出现之前&#xff0c;虽然浏览器原生不支持模块的行为&#xff0c; 但也迫…

ssh连接ubuntu报错

记录问题&#xff1a;1我在本机windows用ssh rootubuntu连接失败 显示端口21啥的2 打开Ubuntu系统&#xff0c;输入ps -e|grep ssh&#xff0c;发现只有agent&#xff0c;没有server3 安装ssh server&#xff0c;输入sudo apt-get install openssh-server&#xff0c;发现报错信…

仅需一个注解,实现 SpringBoot 项目中的隐私数据脱敏!

这两天在整改等保测出的问题&#xff0c;里面有一个“用户信息泄露”的风险项&#xff08;就是后台系统里用户的一些隐私数据直接明文显示了&#xff09;&#xff0c;其实指的就是要做数据脱敏。数据脱敏&#xff1a;把系统里的一些敏感数据进行加密处理后再返回&#xff0c;达…

一键自动化 | Salesforce发布Automation Anywhere自动化组合!

2022年12月1日&#xff0c;Salesforce推出了一个新的Automation Everywhere Bundle&#xff0c;以加速端到端的工作流编排&#xff08;Workflow Orchestration&#xff09;、跨系统自动化&#xff0c;以及在任何地方嵌入数据和AI驱动的工作流。 该捆绑包完全集成到Salesforce F…

acwing第84场周赛(4788,4789,4890)题解

4788. 最大数量 某商场在一天中一共来了 nn 个客人。 每个客人进入商场的具体时刻&#xff08;精确到分钟&#xff09;已知。 请你计算并输出在同一时刻&#xff08;精确到分钟&#xff09;进入商场的最大客人数量。 输入格式 第一行包含整数 nn。 接下来 nn 行&#xff…

二叉搜索树比起二叉树又有什么不一样呢?

二叉搜索树比起二叉树又有什么不一样呢&#xff1f;&#x1f3d0;什么是二叉搜索树&#x1f3d0;二叉搜索树的实现&#x1f3c0;节点类:&#x1f3c0;构造函数&#x1f3c0;析构函数&#x1f3c0;插入insert⚽非递归版本⚽递归版本&#x1f3c0;查找find⚽非递归版本⚽递归版本…

spring boot 八:SpringBoot响应返回xml数据

spring boot 八&#xff1a;SpringBoot响应返回xml数据 1 前言 根据DispatcherServlet源码分析&#xff0c;研究SpringBoot的Controller返回xml数据的一些方法&#xff0c;包含单独配置和全局配置返回xml数据两种方式。 依赖的SpringBoot版本&#xff1a; <parent>&l…

u盘有病毒怎么办?修复U盘,3个方法解决

U盘和外部的驱动器相比&#xff0c;它的体积更小&#xff0c;携带更加方便&#xff0c;可以轻松地与他人分享文件。虽然U盘使用很方便&#xff0c;但是有时会出现中病毒的情况。u盘有病毒怎么办&#xff1f;如果您也受到此问题的影响&#xff0c;我们可以提供一种有效的方法来修…

物联网架构实例—Ubuntu 安装Redis

1.准备更新apt-get源sudo apt-get update2.安装执行Redis 安装命令sudo apt-get install redis-server3.检查安装状态sudo /etc/init.d/redis-server status查看Redis运行进程ps -aux|grep redis4.将Redis添加到服务器启动项修改/etc/rc.localvim /etc/rc.local将下面的命令加到…

阿里云办公安全产品专家高传贵:零信任,让全球办公安全更简单

2022 年 8 月 30 日&#xff0c;阿里云用户组&#xff08;AUG&#xff09;第 9 期活动在北京举办。活动现场&#xff0c;阿里云办公安全产品专家高传贵&#xff0c;向参会企业代表分享了零信任&#xff0c;让全球办公安全更简单。本文根据演讲内容整理而成。 大家下午好。我今天…

内部类导致的内存泄漏

前两天刷文章偶然翻到一篇因使用非静态内部类时导致内存泄漏的问题,出于好奇自己也动手一试 什么叫内存泄漏 内存泄漏&#xff08;Memory Leak&#xff09;是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放&#xff0c;造成系统内存的浪费&#xff0c;导致程序…