指针的使用以及运算、二级指针、造成野指针的原因以及解决方法、指针和数组相互使用

第七章,指针的学习


目录

前言

一、指针的概念

二、指针的类型

三、野指针

四、指针的运算

五、指针和数组的关系以及使用

六、指针数组

七、二级指针

总结


前言

这章主要学习的是指针方面的知识,这节只是简单了解一下指针,并不会深入的学习,如果要学习指针更深的知识点,会在后面指针详解这章去讲。我们这章主要从以下方面学习,指针的概念、指针类型、野指针、指针的简单运算、指针和数组、指针数组、以及二级指针。


一、指针的概念

指针就是一个变量,用来存放其他变量在内存中的地址。地址是内存中最小单元的编号,最小单元的地址单位为字节(byte)。指针实际就是地址。

内存中的地址的大概模型:

上面代码中,变量p就是一个指针变量,&a就是取出变量a的地址,然后将a的地址赋值给p,这样变量p存放的就是a的地址,%p就是以地址的形式打印。

下面我们来了解一下计算机是如何对内存进行编址?

计算机分为32位机器和64位机器,现在一般的电脑都是64位。这32位和64位都表示有多少根地址线,每根地址线的状态分为两种:0、1。在32位机器上地址编址的范围中最小值和最大值分别为32根地址线同时为0和同时为1,单位为字节。在32位机器上,我们就可以给 (2^32Byte == 2^32/1024KB == 2^32/1024/1024MB==2^32/1024/1024/1024GB == 4GB) 4G的空闲进行编址,那么编址的范围就是0-4GB,64位机器的编址范围跟32位机器一样的。

指针变量开辟的空间大小是多少呢?通过上面对计算机内存编址进行了解之后,我们知道32位机器的内存地址的编号就是由32个0或1组成的,64位机器的内存编号就是64个0或1组成的。指针变量就是用来存放这些地址编号,所以计算机只需要开辟对应的大小就行了,开辟大了就会造成内存浪费。结论:分为两种情况,32位机器上的指针大小位32bit(4Byte),64位机器上的指针大小为64bit(8Byte)。

二、指针的类型

上面我们对指针的概念有一定的了解之后,下面我们就来学习一下指针的类型。

有人可能想问,指针的大小不就是固定的4字节或者8字节吗?那还需要什么类型来限定指针。有这个想法的铁子们,请继续往下看,你的答案就在下面。

我们先看一下代码,指针的大小会不会随着指针的类型不同而发生变化,代码是基于64位操作平台运行。

int main()
{int a = 20;double b = 13.23;int* p = &a;double* q = &b;printf("%d\n",sizeof(p));   //8printf("%d\n", sizeof(q));  //8return 0;
}

不管是double类型的指针还是int类型的指针,他们的大小都是8字节。因为不管是什么样类型的指针,都是用来存放内存地址的编号,内存地址的编号始终都是由64位0或1组成,所以存放他们只需要8字节大小的空间。

既然指针的大小一直为8字节,那不同类型的指针有什么区别?请看下面的代码。

    int a = 10;float b = 10.12f;int* p = &a;float* q = &b;printf("&a = %p\n",&a);printf("p = %p\n",p);printf("p+1 = %p\n", p+1);printf("&b = %p\n", &b);printf("q = %p\n", q);printf("q+1 = %p\n", q + 1);

上面的代码执行的结果是否跟铁子们想的结果一样呢,如果一样的话,说明铁子已经对不同类型的指针有所了解了。如果不一样的话,我就画图给铁子们讲讲,讲完之后,就会明白为什么要去定义不同的数据类型指针。

在画图之前,先给铁子们讲一下不同数据类型指针的意义。

比如:int *p = &a;这行代码表示,定义了一个指针变量p,指针变量存放的是int数据类型变量a的地址。

double *q = &b;这行代码表示,定义了一个指针变量q,指针变量存放的是double数据类型变量b的地址。

图中的内存地址与代码运行结果不一样,因为我画图只是举例说明,而且代码每次运行的结果都会发生变化。

p+1实质是p指向地址加了4个字节,为什么加1就加了4个字节,这是因为指针指向的数据类型导致的,int数据类型的大小为4,所以指向int数据类型的指针加1就会向后加4个字节。q+1也是一样的操作,float数据类型的大小也为4。

结论就是:指针的数据类型决定指针+1或者-1,指针向前或者向后移动几个字节的距离。

到这里,上面的问题的答案就出来了。所以铁子们知道为什么要定义指针数据类型了吧。

还有一种方式可以证明指针数据类型的用处,那就是指针解引用操作(‘*’),前面章节已经对C语言操作符学习了一遍,如果没有看过前面的章节,可以去看看。下面我们就来看看解引用操作符是如何让指针数据类型变得有意义的。

  int n = 0x11223344;char* pc = (char*)&n;int* pi = &n;*pc = 0;*pi = 0; 

第一步,首先定义一个int类型的变量a,然后a的值为0x11223344,因为在内存中是以十六进制显示的,所以用十六进制给a赋值更容易在内存中看出来。a在内存中的地址为0x00000009FC1BF544。

第二步,定义了一个char类型的指针变量pc,当我执行*pc = 0的时候,内存中a的低8位却被改为了0,a此时在内存中的值为0x11223300。

第三步,定义一个int类型的指针变量pi,当执行*pi = 0的时候,内存中a的32位全部为0。a此时在内存中的值为0x00000000。

通过上面的代码,我们又发现了一个结论,我们在对指针变量解引用的时候,能操作的字节大小取决于指针的数据类型大小。

所以,指针变量的类型是很有必要的,因为它可以决定我们在进行加减整数的时候移动多大的距离或者在解引用的时候我们能操作多大的空间。

三、野指针

我们学会了指针简单的操作,现在还看不出来指针有多大的用处,等后面学的更深入了,就知道指针的强大之处了,但先在这里提醒铁子们后期使用指针的时候,需要注意的点。指针的唯一的坏处,就是用得不恰当就会出现野指针,下面就讲一些野指针是如何造成的以及如何避免野指针。

野指针的概念:指针指向的位置是不可知的,不知道指向什么位置,这个位置具有随机性、不正确的。

存在野指针的几个点:

1、指针未初始化。

这是因为定义一个局部变量时,如果不进行初始化,系统会默认给它赋一个随机值,我们也不知道系统分配的这个地址是否能正常访问,如果不能正常访问,然后我们对它进行解引用操作,编译器肯定会报错。

2、指针越界访问。

运行这段代码的时候,编译器会报错,因为arr数组只有5个元素,下标从0到4,而代码却去访问了下标为5的元素,下标为5的元素的地址就不是系统分配给我们的地址,我们却去强行访问它。

3、指针指向空间释放。

malloc函数就是动态在堆区开辟空间,free就是释放在堆区开辟的空间。当执行到free(pc)的时候,pc指向的那段空间就被系统回收了,被拿去给其他程序用了,然后又让q指向被释放的那块空间,这种情况肯定不允许的,也会造成野指针。

既然在用指针的时候会出现这么多野指针,那我们如何避免呢?

避免野指针的方式有以下几种:

1、定义指针时初始化

int *pi = NULL;//如果不清楚指针最开始指向的地址,就开始先让它指向一个NULL地址。

2、防止指针越界访问

 int arr[5] = { 0 };int* p = arr;for (int i=0;i<5;i++)  {*(p + i) = i;  //访问数组时不要超过数组下标的最大值}

3、指针指向空间释放及时置NULL

 char a = 'A';char* q = &a;char* pc = (char *)malloc(sizeof(char));free(pc);pc = NULL;  //释放之后要及时置NULL,防止后面的指针指向pc

4、避免返回局部变量地址。

int* add(int a ,int b)
{int ret = a+b;return &ret; //error
}int main()
{int *p = add();*p = 20;
}

为什么不能返回局部变量地址,因为局部变量的生命周期就是在add函数内,add函数执行完之后,系统就会释放变量ret的地址,然而把释放掉的地址返回到main函数中,然后指针变量p又去接收被系统释放的地址,最后对释放的地址进行解引用操作,这种操作肯定是不允许的。所以我们在使用函数的时候,不要返回局部变量的地址。

5、指针使用之前检查有效性。

 int* p = NULL;int a = 50;p = &a;if (p != NULL)  //在使用指针之前先判断这个指针变量是否为NULL{*p = 100;}

四、指针的运算

指针的运算顾名思义就是对指针变量进行运算,不是所以运算指针变量都支持的,指针变量只支持下面几种运算。

1、指针+-整数

只要掌握了前面的指针类型的作用之后,这里原理就一清二楚了。

  //指针+-整数float arr[10] = { 0 };float* fp = arr;for (int i=0;i<10;i++){*(fp++) = i + 0.12;}for (int i = 0; i < 10; i++){printf("%f\n",arr[i]);}

2、指针-指针

char arr[] = "hello world";
char* s = arr;
while (*s != '\0')
{s++;
}
printf("%d\n",s-arr);

 

 指针-指针返回的是一个数字,并不是地址。比如0x00001122 - 0x00001113 = 15。如果是高地址减低地址则为一个正整数,低地址减高地址则为一个负整数。这种情况通常用于数组中求数组中元素的个数。但多数是用我们前面章节所讲的sizeof(arr)/sizeof(arr[0])来求数组元素个数。

3、指针的关系运算

  //指针的关系运算int arr[5] = { 1,2,3,4,5 };int* p = NULL;
//这个for循环是正确的写法
for (p=&arr[5];p>&arr[0];)
{*--p = 0;
}//这种for循环的写法能正常运行for (p = &arr[4]; p >= &arr[0]; p--){*p = 0;}for (int i = 0;i < 5;i++){printf("%d\n",arr[i]);}

代码中能正常运行的那个for循环,只是在大部分编译器上可以运行成功,但是C语言的标准并不支持这种写法。

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

建议:如果要进行指针关系运算,铁子们还是用第一种for循环的方式写。因为这种写法是C语言标准规定的,移植性更好。

五、指针和数组的关系以及使用

在之前的章节,我们学习了数组的使用,现在就来看看指针与数组的联系以及如何配合使用。

我们先来复习一下数组的使用:

int main()
{//数组int arr[5] = { 1,2,3,4,5 };//访问数组的方式一for (int i = 0;i < 5;i++){printf("%d ",arr[i]);}printf("\n");//访问数组的方式二for (int i = 0;i < 5;i++){printf("%d ",*(arr+i));}return 0;
}

访问数组的第一种方式其实就是通过数组下标访问操作符进行访问的。而第二种方式使用的是指针的方式去访问,这里为什么指针也能访问数组呢?等我们学完数组与指针的关系后,铁子们就知道为什么能这样写了。

我们先来看一下数组名表示的是什么。

    int arr[5] = { 1,2,3,4,5 };printf("%p\n",arr); //数组名printf("%p\n",&arr[0]); //第1个元素的地址

从上面代码的执行结果来看,arr和&arr[0]是同一个地址,这里就可以知道其实数组名就是数组首元素地址,但是有两个例外的情况不是表示首元素地址,1、当数组名放在sizeof()操作符中时arr就不表示首元素地址,而表示的是整个数组,sizeof(arr)求的是整个数组的大小。2、取数组名地址的时候,arr也不表示首元素地址,&arr求的整个数组的地址。如果要详细了解数组的知识,就去这篇博客学习数组的定义以及使用的详细教程(一维数组、二维数组等等)-CSDN博客。

既然数组名表示首元素地址,那我们是不是就可以用一个指针变量去存储首元素地址呢。

现在指针指向数组首元素地址,并且指针的类型是int,我们前面也学习了指针+-整数,那么我们是不是就可以通过指针+-整数去访问数组了呢?

 int arr[5] = { 1,2,3,4,5 };int* p = arr;for (int i = 0;i < 5;i++){printf("&arr[%d] = %p ----> p+%d = %p\n", i,&arr[i],i,p+i);}

从上面代码的结果来看,指针p+i的指向的地址就是数组下标为i的地址。那我们再对指针指向的地址解引用不就是访问到数组的元素了吗。

所以讲到这里,铁子们现在知道最开始代码中的第二种访问方式的原因了吧。

我们为什么要用指针变量去访问数组元素呢,不能直接用数组名访问数组吗?下面通过代码来看看有些地方指针可以使用,但是数组名就不行。

代码中的两中方式,明明都是指向首元素地址,为什么p++就可以,而arr++就报错呢,这个问题的答案详细讲解在这里C语言的探索之数组名自增自减-CSDN博客。铁子们可以去了解一下。

六、指针数组

我们学完了指针和数组,现在怎么又来个指针数组这个玩意,哈哈哈哈,是不是这些名词绕来绕去,后面还会学习数组指针等等。只要了解深入之后,这些再怎么饶都是那些东西。现在我们就先来学习一下指针数组。

那指针数组到底是指针还是数组呢?如果不清楚,没关系。那字符数组、整型数组这些是字符还是数组,这些铁子们肯定知道是数组吧,我们所有的字符数组并不是表示字符而是表示数组,只不过数组里面的元素类型为字符类型。其实指针数组与字符数组的理解一样,首先指针数组是一个数组,然后数组里面存放的元素类型为指针类型。我们了解这个概念之后,就来具体实现一下。

char arr[5];  //字符数组
char* arr2[5]; //指针数组

这样铁子们不是很理解里面的原理,那就通过画图更容易理解。

从画图中可以看出来,char arr[5]中的每个元素的类型为char,而char* arr2[5]中的每个元素为char*。

我们下面就简单学习一下定义和使用,后面的章节会出详细教程。

 //字符数组和指针数组的定义char arr[5] = { 'a','b','c','d','e' };char* arr2[5] = {"hello", "world", "nihao", "luck", "strive"};//字符数组for (int i = 0;i < 5;i++){printf("%c ",arr[i]);}printf("\n");//指针数组的使用for (int i = 0;i < 5;i++){printf("%s ",arr2[i]);}

为什么指针数组打印的时候要用%s的形式,因为指针数组存放的是常量字符串的首元素地址,所以想要打印字符串则需要用%s。

七、二级指针

二级指针又是什么呢,其实本质还是一个指针,只不过比我们上面讲的指针高级一点点,上面所讲的指针是一级指针,口头说的时候把一级给去掉了。

二级指针的作用呢?我们定义一级指针的时候,是不是也要在内存中开辟一个8字节大小的空间来存储变量的地址编号,既然指针变量也会在内存中开辟空间,是不是也有地址编号,那这个地址编号用什么来存储呢,那就是用我们此时此刻学习的二级指针变量来存储一级指针变量的地址,那二级指针变量的地址编号就要用三级指针来存储。其实在C语言中最多就使用二级指针就够了,很少情况会用到三级指针。

int a = 30;
int* p = &a;  
int** pp = &p;  //等价于 *pp = &a 等价于 p = &aprintf("a = %d\n",a);
printf("*p = %d\n", *p);
printf("**pp = %d\n", **pp);

这里我们只要了解解引用操作符的使用就知道代码的结果为什么是这个。

*p:表示访问指针p指向内存地址的内容。

*pp:表示访问指针pp指向内存地址的内容,这个内容就是指针变量p的地址。

**pp:表示访问指针变量p指向内存地址的内容。

可能光这样看代码,不是很好理解他们之间的关系,那下面就给我亲爱的铁子们画图理解一下。

如果我们要通过pp二级指针去更改变量a的内容,改如何操作呢?

所以二级指针还是蛮简单的,只要知道二级指针是用来存放一级指针的地址,以及他们之间的关系,这样就不会觉得二级指针很难了。


总结

本章主要学习了指针相关的知识,本章重点就是理解指针的类型,因为指针的类型决定了访问权限以及移动的距离,其次就是指针和数组之间相互使用的方法。我们要了解在写指针的时候,如何避免出现野指针。本章只略提了一下指针数组,后面的章节中会详细讲解。

谢谢大家的支持~

铁子们一起加油努力~

如果阅读中发现问题,请我亲爱的铁子们一定要指出来,因为你的意见对我很重要,也是我继续写下去的动力。感谢铁子们。

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

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

相关文章

uniapp H5项目 获取接口的二进制流转化成图片url(base64)

如果你使用的是uniapp, 并且你从接口获取下来的数据长这样&#xff1a; 想要把取到的数据展示成图片&#xff0c;那么你可以这样做&#xff1a; // 这是我们的项目封装的请求方法const res await this.$api.getKaptcha({originResponse: true, // 这样写是为了在request那边特…

路由器热备份

HSRP HSRP&#xff08;Hot Standby Routing Protocol&#xff09;热备份路由选择协议 HSRP是思科私有的协议&#xff0c;HSRP起到一个双网关热备份的一个目的&#xff0c;不考虑线路问题针对设备而言&#xff0c;一个设备挂了还有另外一台设备&#xff0c;所以双网关也叫双机…

stl_set

文章目录 set1.关联式容器2.键值对3. set3.1 set介绍3.2 set的使用3.2.1 pair3.2.2 find3.2.3 lower_bound 3.3 multiset3.3.1 multiset的介绍3.3.2 multiset的使用3.3.3 find3.3.4 equal_range3.3.5 erase set 1.关联式容器 在初阶阶段&#xff0c;我们已经接触过STL中的部分…

嵌入式物联网实战开发笔记-乐鑫ESP32芯片功能对比以及功能选型【doc.yotill.com】

乐鑫ESP32入门到精通项目开发参考百例下载&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/1ATvRnAZvxkev-PJfd3EAPg?pwd4e33 提取码&#xff1a;4e33 2.1 初识 ESP32 ESP32-S3 是一款低功耗的 MCU 系统级芯片 (SoC)&#xff0c;支持 2.4 GHz Wi-Fi 和低功耗蓝牙 (…

强固型国产化工业电脑,在电子看板行业应用,机器视觉在汽车产线行业应用

电子看板行业应用 智能电子看板的核心是通过实现工厂的全面可视化、自动化管理&#xff0c;最终达到提高效率、降低成本及提高产品质量的目标。电子看板硬件主要有两部分组成&#xff1a;微型工业计算机&#xff0c;显示终端&#xff08;平板电视、LCD&#xff09; 方案需求 …

在Java中使用XxlCrawler时防止被反爬的几种方式

目录 前言 一、常见的反爬措施 1、User-Agent识别 2、Referer识别 3、频率限制 4、IP限制 二、XxlCrawer的应对之道 1、User-Agent应对 2、频率限制 3、IP限制 三、XxlCrawler执行解析 1、XxlCrawler对象 2、启动对象 3、信息爬取线程 总结 前言 众所周知&#x…

【c++】vector的使用

&#x1f525;个人主页&#xff1a;Quitecoder &#x1f525;专栏&#xff1a;c笔记仓 朋友们大家好&#xff0c;我们本篇来到一个新的容器&#xff0c;vector的讲解和使用 目录 1.vector简单介绍2.vector的使用2.1构造函数2.2遍历vector2.3对容量操作2.4vector的增删查改 1.v…

Java测试编程题

题目1 1.创建5个线程对象 线程名设置为&#xff08;Thread01&#xff0c;Thread02&#xff0c;Thread03&#xff0c;Thread04&#xff0c;Thread05&#xff09;使用 代码实现5个线程有序的循环打印&#xff0c;效果如下&#xff1a; Thread01正在打印1 Thread02正在打印2 Threa…

Day60 单调栈 part03

Day60 单调栈 part03 最后一天啦&#xff01;完结撒花~ 84.柱状图中最大的矩形 我的思路&#xff1a; 感觉和接雨水差不多&#xff0c;只需要多考虑一些情况 双指针 lheight 和 rheight 分别是用来存储每个柱子的左边界和右边界的数组。 解答&#xff1a; class Solutio…

齐次变换矩阵、欧拉角

齐次变换矩阵 因为老是忘记齐次变换矩阵的含义以及方向&#xff0c;每次推导公式都很费劲&#xff0c;写下这篇文章用于快速回顾齐次变换矩阵。 表示的是&#xff1a;坐标系A到坐标系B的齐次变换矩阵&#xff0c;也是坐标系B在坐标系A下的位姿。 对于这个矩阵&#xff0c;有三…

Matlab软件使用教学

1. Matlab简介 Matlab&#xff08;Matrix Laboratory的缩写&#xff09;是一种由MathWorks公司开发的数值计算和可视化编程环境。它广泛应用于工程、科学研究、数学和教育等领域&#xff0c;因其强大的计算能力和丰富的工具箱而受到青睐。 2. 安装与启动 安装&#xff1a;从M…

贪心算法(一)

什么是贪心算法&#xff1f;&#xff1f;&#xff1f; 贪心算法是指通过每一次都选择最优解情况&#xff0c;然后通过局部最优从而达到全局最优&#xff0c;简单理解为目光短浅&#xff0c;走一步看一步。 需要注意的是&#xff0c;贪心算法是一种思想&#xff0c;而非直接的…

车载以太网解决方案

车载以太网对现代汽车行业具有非常高的价值&#xff0c;随着汽车技术的不断发展&#xff0c;车载电子组件和传感器的数量与复杂度都在持续增加。为了满足这些复杂系统的需求&#xff0c;车载以太网作为一种高速数据交换介质&#xff0c;发挥着至关重要的作用。 汇迪能提供的车载…

数据的质量控制软件----fastQC

一、前言 FastQC的基本介绍: FastQC是一款基于Java的软件&#xff0c;它可以快速地对测序数据进行质量评估&#xff0c;其官网为&#xff1a;Babraham Bioinformatics - FastQC A Quality Control tool for High Throughput Sequence Data 高通量测序数据的高级质控工具输入…

7. DAX 时间函数-- DATE 日期--TOTALMTD、TOTALQTD、TOTALYTD

函数名目的语法返回值TOTALMTD计算当前上下文中该月份至今的表达式的值 。TOTALMTD ( <表达式>, <日期列>, [<筛选器>] )标量 表示表达式的标量值&#xff0c;在“日期”中给定日期&#xff0c;计算当前月份至今的日期 。TOTALQTD计算当前上下文中该季度至今…

Github 2024-04-20 开源项目日报 Top10

根据Github Trendings的统计,今日(2024-04-20统计)共有10个项目上榜。根据开发语言中项目的数量,汇总情况如下: 开发语言项目数量非开发语言项目2Python项目2Swift项目2HTML项目1CSS项目1Go项目1C项目1C++项目1Rust项目1编程面试大学:成为软件工程师的全面学习计划 创建周期…

【最新可用】Claude国内镜像,可上传图片,可用Claude3全系模型,包括Pro版本的Opus),亲测比GPT好用

Claude对话、上传图片的超详细教程来啦&#xff01; 近期&#xff0c;Claude 3 Opus的发布引发了网络上的广泛关注与热议&#xff0c;有观点认为其性能已经凌驾于GPT-4之上。虽然网络上已经出现了大量基于这两款先进AI技术的实际应用案例&#xff0c;但仍有许多人对在国内如何…

Java最新图形化界面开发技术——JavaFx教程(含UI控件用法介绍、属性绑定、事件监听、FXML)

文章目录 一、JavaFx介绍1、JavaFx简介2、可用性3、主要特征4、UI控件 二、JavaFx概述1、JavaFx结构图2、JavaFx组件&#xff08;1&#xff09;舞台&#xff08;2&#xff09;场景① 场景图② 节点 &#xff08;3&#xff09;控件&#xff08;4&#xff09;布局&#xff08;5&a…

Unity射击游戏开发教程:(2)实例化和销毁游戏对象

现在我们有了“飞船”,我们可以在屏幕上移动它,现在我们需要发射一些激光!与宇宙飞船一样,我们将让事情变得简单并使用 Unity 自己的基本形状。舱体的效果很好,所以我们来创建一个。 我们保存了有关位置、旋转和缩放的信息。我们想要缩小这个对象,假设每个轴上缩小到 0.2…

【声呐仿真】学习记录1-配置dave、uuv_simulator

【声呐仿真】学习记录1-配置dave、uuv_simulator 1.介绍2.配置3.一些场景 1.介绍 家|DAVE项目 — Home | Project DAVE 2.配置 参考官方教程安装|DAVE项目 — Installation | Project DAVE mkdir -p ~/uuv_ws/src cd ~/uuv_ws/src git clone https://github.com/Field-Robot…