• 【C语言】初识指针(二)


    你可以改变你的行为,但改变不了你想要什么——《浴血黑帮》


    目录

    1、指针类型

    1.1指针加减(+、-)整数

    1.2指针的解引用

    2、野指针

    2.1什么叫野指针

    2.1.1指针未初始化

    2.1.2指针越界访问

    2.1.3指针指向的空间被释放了

    2.2如何避免野指针


    前言:

    大家好,我是拳击哥。今天我给大家带来是初识指针第二期。本期主要讲解的是指针的类型,野指针概念,如何避免野指针的情况发生。


    上一期回顾:

    指针是什么?

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

    总之指针就是地址,我们可以通过&(取地址操作符)来取出一个变量在内存中的实际地址,并且可以通过指针变量修改这个变量。那么有一程序:

    1. #include
    2. int main()
    3. {
    4. short num = 10;
    5. short* p = #
    6. *p = 20;
    7. printf("%d\n", num);
    8. return 0;
    9. }

     输出结果:20

    为啥输出20呢,请容我细细道来:

    short num = 10;在内存开辟一块空间,这块空间是num的,占两个字节。怎样得到这块空间的地址呢?我们可以通过&(取地址操作符)来得到这块空间的地址也就是num的地址。

    short* p = #定义一个短整型指针变量p把num的地址赋值给p,意味着指针变量p指向了num的地址。因此我可以对指针变量p进行解引用来修改num的值。

    *p = 20; 对指针变量p进行解引用(*),然后把20赋值给*p。此时指针变量p指向的是num的地址,所以20同时也赋值给了num。


    通过上面的例子,我们知道了指针变量是用来存放地址的变量。(存放在指针中的值都被当成地址处理)那么有疑问的是指针的一个小的单元到底是多大?(1个字节)如何进行编址?在上一期我讲到了,您可以点击下方链接进去看看:

    【C语言】初识指针(一)_拳击哥带你捶键盘的博客-CSDN博客


    1、指针类型

    C语言中数据是有类型的如整型、字符型、浮点型等等,那么指针有自己的类型吗。准确来说是有的。我们来看一组代码:

    1. #include
    2. int main()
    3. {
    4. int num = 0x11223344;
    5. int* p1 = #
    6. char* p2 = #
    7. printf("%p\n", &num);
    8. printf("%p\n", p1);
    9. printf("%p\n", p2);
    10. return 0;
    11. }

    输出三个个相同的随机地址

    012FFEB4
    012FFEB4

    012FFEB4

    以上代码证明了指针变量p1和指针变量p2指向的就是num的地址。如果通过解引用p1和p2修改num的值,会发生什么呢,我们来看两组代码:

    1. //代码1
    2. #include
    3. int main()
    4. {
    5. int num = 0x11223344;
    6. int* p1 = #
    7. *p1 = 0;
    8. printf("%d\n", num);
    9. return 0;
    10. }
    11. //代码2
    12. #include
    13. int main()
    14. {
    15. int num = 0x11223344;
    16. char* p2 = #
    17. *p2 = 0;
    18. printf("%d\n", num);
    19. return 0;
    20. }

    代码1输出结果:0

    代码2输出结果:287453952

    我们可以看到对p1进行解引用修改了num四个字节的值,对p2进行解引用只能修改num第一个字节的值。说明了指针类型是有局限性的,得按照类型来划分。 注意0x11223344是十六进制数对应的十进制数是287454200。

    指针类型决定了指针进行解引用操作时,一次性访问几个字节。如果是int*的指针一次访问四个字节,如果是char*的指针一次访问一个字节 。

    有一程序:

    1. #include
    2. int main()
    3. {
    4. int num = 0x11223344;
    5. char* p1 = (char*)#
    6. int* p2 = #
    7. for (int i = 0; i < 4; i++)
    8. {
    9. printf("%d ", *p1);
    10. p1++;
    11. }
    12. printf("\n%d\n", *p2);
    13. return 0;
    14. }

    输出结果

    68 51 34 17
    287454020

    上述程序,for循环对p1解引用依次输出了四个数,分别是44、33、22、11对应的十进制数68、51、34、17。因为0x11223344在内存中是倒着存放的,所以输出的值也是倒着输出的。

     

    对p2解引用直接输出了十六进制11223344对应的十进制数287454200

    证明了char*的指针往后自增时访问一个字节,int*的指针访问四个字节。对应了各个类型字节数,因此证实了指针类型是有意义的,是根据数据类型大小来进行访问的步长


    1.1指针加减(+、-)整数

    指针的加减整数,就是决定指针往前或往后加减多少个字节。加法就是往后访问,减法就是往前访问。比如定义一个整型的变量,我想一个字节一个字节往后访问这时候可以定义字符型指针指向该变量,我想两个字节往后访问这时候可以定义短整型指针指向该变量。

    我们来看短整型往后加1:

    1. #include
    2. int main()
    3. {
    4. int num = 0x11223344;
    5. short* p= #
    6. printf("%d\n", *p);
    7. p++;
    8. printf("%d\n", *p);
    9. return 0;
    10. }

    输出结果

    13124
    4386

    分别输出的是短整型往后加1前的值和往后加1后的值 ,十六进制3344对应的十进制是13124,十六进制1122对应的十进制是4386。可以看到短整型往后加1访问的是两个字节。

    以上是加法,减法也是一样。往前访问几个字节根据指针对应的数据类型来决定。那么访问几个字节,我们认为这是指针的步长。


    1.2指针的解引用

    详细大家在上述几个例子中已经了解到了指针解引用操作的用法,还是刚才那组程序:

    1. #include
    2. int main()
    3. {
    4. int n = 0x11223344;
    5. char* p1 = (char*)&n;
    6. int* p2 = &n;
    7. *p1 = 0;
    8. printf("%d\n", n);
    9. *p2 = 0;
    10. printf("%d\n", n);
    11. return 0;
    12. }

    输出结果:

    287453952

    对指针进行解引用得到的值是根据指针的类型来决定的,如果是字符型那只能访问一个字节,整型能访问四个字节。这看我们的用法,不同的地点不同的用法。如果有程序让我们依次访问一个整型的每个字节里面存的值,这个时候就可以用字符型指针来访问了。

    总结:

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

    2、野指针

    今天听到一句惨绝人寰骂人的话:“你TM就是一个没有对象的野指针!”


    2.1什么叫野指针

    概念:野指针就是指针指向的位置是不可知(随机、无方向、无法意料)的。造成野指针的情况有三种,指针未初始、指针越界访问、指针指向的空间被释放了。

    2.1.1指针未初始化

    有一代码:

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

     出现警告

    局部变量指针未初始化指向的地址是随机值,以上代码定义的指针p就是一个野指针。如果我们直接给未初始化的指针解引用并赋值的话,这个值不知道放哪里去了。因为p指向的地址是未知的、随机的。我们应该这样写:

    1. #include
    2. int main()
    3. {
    4. int a = 10;
    5. int* p=&a;
    6. *p = 20;
    7. printf("%d\n", a);
    8. return 0;
    9. }

     输出结果:20

    此时指针p指向了a的地址,指针就有了方向不会造成指向的地址是未知、随机的。 这样的话我再对指针p指向的地址里面内容进行修改,就没有问题。


    2.1.2指针越界访问

    数组有越界访问的情况,指针也是如此,我们来看代码:

    1. #include
    2. int main()
    3. {
    4. int arry[6] = { 1,2,3,4,5,6 };
    5. int* p = arry;
    6. for(int i = 0; i <= 6; i++)
    7. {
    8. printf("%d ", *(p + i));
    9. }
    10. return 0;
    11. }

    输出结果:1 2 3 4 5 6 -858993460

    以上情况出现了指针越界的情况,arry数组只有6个元素,而指针p却访问到了下标为6的第7个元素的地址,第7个元素地址里面的值是未知的,因此造成了野指针。


    2.1.3指针指向的空间被释放了

    有一代码:

    1. #include
    2. int* test()
    3. {
    4. int a = 20;
    5. return &a;
    6. }
    7. int main()
    8. {
    9. test();
    10. int* p = test();
    11. printf("%d\n", *p);
    12. return 0;
    13. }

    输出结果:20 

    虽然输出的结果是20,但是这个程序是很危险的 ,会出现警告。因为test函数在返回主函数时,test函数的栈帧已经销毁了a是一个局部变量因此也销毁了。所以test返回的地址也是一个未知的状况,也许返回的地址让指针p恰好得到了20。但是我在前面加上一段代码,就完全打破了指针p与被销毁的test函数中a的关联。

     如果我在解引用指针p前加上一段程序:

    1. #include
    2. int* test()
    3. {
    4. int a = 20;
    5. return &a;
    6. }
    7. int main()
    8. {
    9. test();
    10. int* p = test();
    11. printf("Hellow World\n");
    12. printf("%d\n", *p);
    13. return 0;
    14. }

    输出结果

    Hellow World
    13  

    printf函数打破指针p与a的关联,如果解引用指针p能正常的输出20就说明test函数还在内存中,反之并不能那是因为test函数在返回给主函数的已经把所有的内存还给了寄存器,也就是test函数的栈帧被回收了

    但如果没有另一块栈帧挤掉当前残存在寄存器里面的test函数栈帧,就会像上方第一个代码那样输出20。 所以这就是为啥在对指针p解引用前加上一个printf语句,指针p就输出其他的值。您应该理解了吧。


    2.2如何避免野指针

    1. 指针初始化
    2. 小心指针越界
    3. 指针指向空间释放,及时置NULL
    4. 避免返回局部变量的地址
    5. 指针使用之前检查有效性

    1.指针初始化,指针p1初始化指向a的地址,这就是指针初始化了 ,如下代码:

    1. #include
    2. int main()
    3. {
    4. int a = 10;
    5. int* p1 = &a;//指针初始化
    6. return 0;
    7. }

    给指针初始化就不会造成野指针的情况,因为指针此时是有明确的指向目标的。


     2. 小心指针越界,指针访问,比如2.1.2程序修改后,我令指针p访问的下标范围在[0,6)之间。

    1. #include
    2. int main()
    3. {
    4. int arry[6] = { 1,2,3,4,5,6 };
    5. int* p = arry;
    6. for(int i = 0; i < 6; i++)
    7. {
    8. printf("%d ", *(p + i));
    9. }
    10. return 0;
    11. }

     输出结果:1 2 3 4 5 6

    指针越界跟数组越界相似,指针通过地址并对地址进行解引用来对应的值,数组则是通过下标来获取对应的值。我们只需要不让指针指向到越界的下标,就不会造成野指针。


    3.指针指向的空间被释放了,我们应该及时的把指针置为NULL。如果还要对这个指针进行操作,我们可以先对这个指针进行判断看它是否为空指针。如下方程序:

    1. #include
    2. int main()
    3. {
    4. int* p = NULL;
    5. if(p != NULL)
    6. {
    7. printf("%d\n",20);
    8. }
    9. else
    10. {
    11. printf("%d\n", 21);
    12. }
    13. return 0;
    14. }

    输出结果:21 

    上方程序就是判断指针是否为空,然后进行下一步操作。


    4.避免返回局部变量的地址,如2.1.3程序中,test函数销毁了,因此test函数里面的局部变量a也随之销毁了。因此返回的值也是无效的。

    1. #include
    2. int* test()
    3. {
    4. int a = 20;
    5. return &a;
    6. }
    7. int main()
    8. {
    9. test();
    10. int* p = test();
    11. printf("Hellow World\n");
    12. printf("%d\n", *p);
    13. return 0;
    14. }

    输出结果

    Hellow World

    13

    Hellow World打破指针p与被销毁的a的联系,因此证实了指针p此时指向的是被销毁后的局部变量a,此时p是一个野指针。


     5.使用指针前检查指针的有效性,比如我初始化指针为NULL。这个指针为空了,代表着就不可用了。如以下方程序:

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

    指针p初始化为NULL,并且指针未指向任何有意义的数据此时的指针p就不可用,指针p就是一个野指针。


    以上就是今天的博客了,下期我们讲解指针运算,指针和数组,二级指针和指针数组。感谢您的观看

     Never Give Up


    初认指针(一)

  • 相关阅读:
    docker入门加实战—docker数据卷
    Java mysql 双数据源
    Mongodb安装(mac版)
    算法题练习——NC45 实现二叉树先序,中序和后序遍历,NC119 最小的K个数
    BUUCTF misc 专题(121)[watevrCTF 2019]Evil Cuteness
    JVM进阶
    艾美捷SequENZ测序级改造型胰蛋白酶特异性和应用
    JSP的简介和生命周期
    计算机毕业设计node+vue基于微信小程序的西餐外卖系统 uniapp 小程序
    WMS之添加View
  • 原文地址:https://blog.csdn.net/weixin_64916311/article/details/127936684