• 赶紧进来!!!满满干货!!详解C语言指针(初阶)


    本文对指针的初阶进行讲解,包括指针的定义,如何理解指针,指针的各种运算,野指针的定义和如何避免野指针…
    耐心看完,让你对指针不再恐惧,学会指针,你的c语言不再迷茫~~~

    在这里插入图片描述

    一.指针是什么?

    指针是什么? 指针理解的2个要点:

    1. 指针是内存中一个最小单元的编号,也就是地址
    2. 平时口语中说的指针,通常指的是指针变量,是用来存放内存地址的变量

    1.文字加图解内存空间的分布

    内存可以看作是一个非常大的空间而这个空间是由无数个内存单元组成,
    不同个数的内存单元可以构成不同类型的内存空间,
    比如常说的整形变量是由多个连续内存单元组成的一个整形空间.
    最小单元也就是内存单元大小是一个字节,而每个内存单元都依次排列在内存里,既然每个内存单元都是一样的大小, 那如何区分不同的内存单元呢?
    此时就有地址即编号,每个内存单元都对应有它自己的一个编号,用编号来确定每一个内存单元在内存里的位置,而在内存中为了区分不同编号也就是地址由则有高地址低地址之分.

    在这里插入图片描述

    上图↑是一个假想的内存空间构造图由无数个内存单元紧紧排列而成 这里假设上面的位置是高地址,下面是低地址,并且是在32位平台下,
    此时0xFFFFFFFF表示高地址依次往下排列0X00000000表示低地址,每个地址都是上都有一个内存单元.

    2.转换概念.理解指针的含义

    举个例子:每个内存单元就好比是我们的间房子,指针就是房子门口的门牌号
    这一间间相同的房子用门牌号可以区分它们,可以精确找到某个房间.

    二.指针和指针类型

    这里我们在讨论一下:指针的类型
    我们都知道,变量有不同的类型,整形,浮点型等。那指针有没有类型呢?
    准确的说:有的

    1.举例一些指针类型

    int n = 10;
    int *p = &n;
    &n让我们得到了n这个整形空间在内存中所处的地址,而这个地址我们也可以当做一个常量来看待,它需要存在一个变量中以便后续使用,此时定义了一个变量p,将&n(n的地址)保存到p中,我们知道p就是一个变量,而旁边的 * 表示p是一个指针变量
    那它的类型是怎样的呢?
    p这个指针变量存放的是整形变量在内存空间里所处的地址, 这里p就应该是整形类型的指针变量而变量有许多不同类型的变量,
    为了方便管理不同类型的地址我们存相应变量的地址时也需要相应类型的指针变量存储 此时存放整形类型的指针变量在 最前面再加上这个指针变量存放的对应地址指向的变量类型为int类型 ,
    int *p此时表示的就是定义的一个整形类型的指针变量,用来存放整形类型的内存空间地址.

    这里列举一下不同类型的指针变量
    char *pc = NULL;
    int *pi = NULL;
    short *ps = NULL;
    long pl = NULL;
    float pf = NULL;
    double pd = NULL;
    这里可以看到,指针的定义方式是: (类型)type + (星号) *
    其实:
    char
    类型的指针是为了存放 char 类型变量的地址。
    short
    类型的指针是为了存放 short 类型变量的地址。
    int
    类型的指针是为了存放 int 类型变量的地址。

    2.转换概念理解指针类型

    把指针比作门牌号,不同类型的指针就相当于不同类型的门牌号框它们所代表的房子数类型也是不同
    字符类型的空间就相当于一个房子 而整形空间就相当于将四个房子连在一起构成的一个大房子,此时也用第一个房子的门牌号表示这个大房子的位置

    三.指针的大小.

    既然有不同类型的指针变量,那这些指针变量的空间大小是否和不同类型的变量一样有着自己的大小?

    1.举例图解 得出指针大小

    在这里插入图片描述
    在这里插入图片描述

    根据上面两张图可以看到,各种类型的指针变量的内存大小跟类型无关而跟计算机平台位数有关,
    32位平台下各种类型的指针变量大小为4字节,64位平台下各种类型指针变量大小为8个字节.
    因为不管是什么类型的指针变量,终归存放的都是一个内存单元在内存里的地址

    2.转换概念理解指针大小

    把各种变量比作各种房子,不管什么大小的房子,指针还是一个门牌号,用来定位房子的位置.大小是一致的

    四.对指针的解引用

    *表示解引用操作符 对指针解引用表示访问这个指针指向的位置上的内存空间.在这篇博客中讲到了解引用操作符的用法->解引用操作符.

    1.举例加图解不同指针解引用

    int main()
    {
    int n = 0x11223344;
    char *pc = (char *)&n;
    int *pi = &n;
    *pc = 0; //重点在调试的过程中观察内存的变化。
    printf("%x\n",n);
    *pi = 0; //重点在调试的过程中观察内存的变化。
    printf("%x\n",n);
    return 0;
    }
    
    > 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    上面这段代码在编译器上运行结果会是什么?

    在这里插入图片描述

    结果以十六进制打印出来变成了11223300和0这是什么情况?
    肉眼无法看出,就一步步调试!!

    在这里插入图片描述

    这里我们得到了n里面的值和n变量在内存中的地址,因为n是四个字节的整形空间,由4个内存单元组成,而每个内存单元都有自己的编号,在设计时为了统一表示,用整形变量起始的那个内存单元地址表示这整形空间的地址,也就是这四个内存单元里最低地址的那个内存单元所在的地址表示这个整形空间的地址,

    在这里插入图片描述

    pi和pc变量里存的都是n的地址,但pc是字符型指针变量,存放的n地址被强制类型转换为了char*字符类型地址,它们的类型不同,但都存放的是n变量在内存中申请的四个字节的空间的起始内存单元的地址
    它们区别在于对pc也就是字符型指针解引用访问的是指针指向的位置上一个字节的空间,此时访问的是第一个内存单元里的数据是0x44,而pi里存的n的地址是整形类型,解引用访问的是这个指针指向的位置上上4个字节的整形空间,此时第一个内存单元大小为1个字节,会加上后面连续的三个内存单元,构成一个四个字节的整形空间被访问此时访问的就是第一个内存单元开始往后的共四个内存单元的整形内存空间里面数据为0x11223344也就是n这个整形变量里面的值
    从这里我们看出每个内存单元大小是一个字节,而两个十六进制数组成1个字节,根据内存里设置的排序方法(大小端),四个内存空间里分别放了44 33 22 11每个内存单元放了两个十六进制

    在这里插入图片描述

    进一步调试发现*pc将pc里面那个字符型指针解引用访问得到了整形空间第一个内存单元的空间,将里面赋值为了0,此时这个n整形空间里的数就变成了0x11223300 ,那个44被覆盖了,此时通过指针解引用修改了整形空间n里面的一个内存单元里的值,此时通过打印n得到了0x11223300

    在这里插入图片描述

    再往下调试发现对pi解引用得到的是访问pi里的整形指针指向的位置上的内存空间,而这个指针指向的是一个内存单元大小是一个字节,整形是4个字节,会往后进行访问直到访问到4个字节的内存空间,此时这4个字节的空间是通过对pi解引用访问到的也就是n整形变量的空间,此时里面的值为0x11223300
    将里面赋值为0,此时里面的值是每个内存单元都被赋值为了0为0x00000000
    此时打印n就得到了0 !!!

    2.指针解引用总结

    指针的类型决定了,对指针解引用的时候有多大的权限(能操作几个字节)。
    比如: char* 的指针解引用就只能访问一个字节,而 int* 的指针的解引用就能访问四个字节

    五.指针的运算.

    指针± 整数
    指针-指针
    指针的关系运算

    1.指针±整数

    整数与整数可以加减运算,指针加减整数有什么作用呢?
    先看下面一段代码

    int arr[10] = { 0,1,2,3,4,5,6,7,8,9 };//这串代码输出结果是什么?
    	printf("%d\n", *arr);
    	printf("%d\n", *(arr + 0));
    	printf("%d\n", *(arr + 1));
    	printf("%d\n", arr[1]);
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    这串代码最后输出结果为0 0 1 1,下面我们来一步步分析.

    在这里插入图片描述

    首先创建一个整形数组(不了解数组的可以看这篇博客初识数组),在之前学到整形数组里每个元素在内存中是连续存放的,也就是说这个数组里实际是10个整形空间连续排放组成的,arr为数组名,在这里表示数组首元素地址,
    首元素正是数组里第一个整形变量里面存放了0首元素地址也就是这个存放0的整形变量的第一个内存单元的地址表示这个整形的地址,对这个地址解引用就是访问这个地址指向的位置上的4个字节的空间,此时得到的就是这个地址指向的内存单元加上后面三个内存单元构成的 整形空间,里面的数字就是0
    而对指针加减整数表示指针向前向后移动,指针类型的不同,决定着指针移动的步长是多少.
    arr+0表示这个指针没有移动还是第一个元素的地址解引用访问的还是第一个整形空间里面数为0
    而arr+1,表示向后移动一个步长,而arr是整形地址,表示这个步长大小为4个字节,此时arr+1表示这个地址往后移动4个字节,而当前arr是第一个整形空间的第一个内存单元的地址,加四个字节得到的就是第二个整形空间的第一个内存单元所处的地址,而此时对其解引用,访问的就是这个地址指向的内存单元往后访问四个字节的空间,此时得到的就是第二个整形空间,输出1
    指针减整数也就是往前移动整数个步长,步长大小取决于指针的类型.
    根据这一特性我们访问数组就可以用指针加减整数,而arr[1]数组下标访问操作符也是根据指针加减整数简化而来,arr[1]等同于*(arr+1)都表示访问的是数组第二个元素.

    2.指针减指针

    指针减指针就是两个相同类型的地址相减,得到的就是两个地址间的元素个数.
    下面这串代码输出结果是什么?

    int arr[10] = { 0,1,2,3,4,5,6,7,8,9 };
    	printf("%d\n", (arr + 10) - arr);
    	printf("%d\n", arr - (arr + 4));
    
    • 1
    • 2
    • 3

    在这里插入图片描述

    结果为10和-4.
    根据上面指针加减整数知识得到arr+10为第11个整形空间的起始内存单元地址,arr为第一个内存单元地址,它们相减得到的是这两个地址间整形元素的个数,也就是10
    arr+4为数组第五个整形空间的起始地址,arr-(arr+4)相当于-((arr+4)-arr) 表示第五个整形元素起始地址到第一个整形元素起始地址之间有4个整形元素,再加上负号,结果为-4

    所以一般只有同一段连续内存里的两个同类型的指针相减才有意义

    3.指针的关系运算.

    下面这串代码实现的结果是什么?

    #define N_VALUES 5
    float values[N_VALUES];
    float *vp;
    //指针+-整数;指针的关系运算
    for (vp = &values[0]; vp < &values[N_VALUES];)
    {
    *vp++ = 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    最后结果为将values这个单精度浮点型数组里面的每个元素都赋值为0
    那下面这串代码结果呢?

    for(vp = &values[N_VALUES]; vp > &values[0];)
    {
    *--vp = 0;
    }
    
    • 1
    • 2
    • 3
    • 4

    结果也是将这个数组每个元素都赋值为0.并且这串代码还可以写成下面这种形式

    for(vp = &values[N_VALUES-1]; vp >= &values[0];vp--)
    {
    *vp = 0;
    }
    
    • 1
    • 2
    • 3
    • 4

    但是,上面这串代码实际在绝大部分的编译器上是可以顺利完成任务的
    ,然而我们还是应该避免这样写,因为标准并不保证它可行。

    标准规定:
    允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与
    指向第一个元素之前的那个内存位置的指针进行比较。

    六.野指针

    什么是野指针? 在上面的代码题中都有一个特性,就是指针变量里面都有明确的指针存在,解引用的位置都是事先确定好,而野指针就是指针变量里面的指针是未知的.
    概念: 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)

    1.野指针的成因

    ①指针未初始化

    下面这串代码结果是什么?

    
    
    #include 
    int main()
    {
    int *p;
    *p = 20;
    return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    大部分编译器现在都会报错,因为p是一个整形指针变量,里面在创建的时候没有给其初始化,则里面的地址是未知的,而一块未知的整形指针,对其解引用访问的是一个未知的整形空间,既然是未知的,你能对里面的内容进行修改嘛???即便修改了,也没有任何意义!!

    ②指针越界访问

    下面这串代码结果是什么

    #include 
    int main()
    {
    int arr[10] = {0};
    int *p = arr;
    int i = 0;
    for(i=0; i<=11; i++)
    {
    
    *(p++) = i;
    }
    return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    答案是编译器报错:内存越界访问
    arr数组里有10个元素 而经过循环最后将arr后面第11个元素进行赋值,此时属于越界访问了
    创建的数组只有10个整形空间是已知的,它后面第11个元素空间是还未分配的
    当指针指向的范围超出数组arr的范围时,p就是野指针

    ③指针指向的空间被释放

    这段代码运行结果是什么?

    #include
    int* add(a,b)
    {
    
    	int c = a + b;
    	return &c;
    }
    
    int main()
    {
    	int a = 1, b = 2;
    	int *num=add(a,b);
    	printf("%d",( * num));
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    在我编译器会正常输出3,但是这实际上是错的
    在这篇博客中写到了作用域和生命周期,根据之前的学习,因为变量的生命周期和作用域不同,在一个自定义函数里创建的变量最后生命周期会在出这个函数后结束,里面申请的空间会被返还给操作系统.
    根据下面一步步调试来看

    在这里插入图片描述

    最后回到main函数时,原add函数里面的c和变量生命周期已经结束,此时通过取到的c变量的地址再解引用访问这个c空间,而c空间此时已不属于我们,它也是一块未知的空间,即便最后的值是3也是对一块野指针!!

    2.如何规避野指针

    ①. 指针初始化

    在创建指针变量时尽可能给其初始化,如果没有明确要赋值什么先赋值上NULL表示空指针

    ②. 小心指针越界

    对数组之类的连续空间 使用指针避免出现越界访问

    ③. 指针指向空间释放,及时置NULL

    用一些动态内存开辟函数 malloc calloc开辟的空间在不用的时候使用free将空间释放掉再将指针变量赋值为NULL 这章知识在后面会讲到

    ④. 避免返回局部变量的地址

    在不同函数里的变量不要返回该变量的指针 因为函数结束变量生命周期也结束了

    ⑤. 指针使用之前检查有效性

    使用指针变量前确保这个指针变量里面的指针是有效的
    一般可以用assert断言函数 避免人的粗心造成野指针或者指向的位置为空的问题.

    在这里插入图片描述

    七.指针初阶总结

    这篇博客中讲到了指针初阶部分,指针的定义,理解,野指针,指针的运算.
    指针是c语言中的灵魂,可以说会了指针,c语言也没什么太难的地方了,
    指针用的好是神器,用不好是灾难,一定要谨防野指针问题,毕竟指针是直接访问内存单元.!!!

    写文不易,给个一键三连支持下吧~~~

    在这里插入图片描述

  • 相关阅读:
    Elasticsearch基础篇(六):es映射和常用的字段类型
    SpringCloud Alibaba 简介
    机器学习(二):线性回归
    免费开源线上社交交友婚恋系统平台 可打包小程序 支持二开 源码交付!
    高级计量经济学(part2)--小样本OLS
    8.Mobilenetv2网络代码实现
    SpringBoot 调用外部接口的三种方式
    flink的CoProcessFunction使用示例
    准备好迁移上云了?请收下这份迁移步骤清单
    “数字化重构系统,搞定 CEO 是第一步”
  • 原文地址:https://blog.csdn.net/lch1552493370/article/details/126374039