嵌入式中c语言指针用法详解与分析

今天给大家来讲解一下指针。

我会由浅到深,最后结合实际应用讲解,让大家学会指针的同时,知道大佬们都用指针来干嘛!

长文预警!全文大约5200多字,学指针看这篇文章就够了!

很多人跟我刚学习c语言一样,都害怕指针。

我也是后面做了一些物联网网关才知道,指针是c语言的灵魂这句话真正含义

没有指针,很多功能实现起来确实很不方便,比如做不到真正的模块化编程。

Ok,废话不多说,下面正式进入主题。

一、通过这篇文章你能掌握以下知识:

  1. 指针的相关概念

  2. 掌握指针与数组之间的关系

  3. 掌握指针指向的指针

  4. 掌握如何使用指针变量做函数参数

  5. 掌握如何使用指针函数

  6. 掌握如何使用指针数组函数

二、指针的作用

指针是C语言中一个比较重要的东西,有人说指针是C语言的灵魂这句话说的一点也没错。

正确灵活地运用它,可以有效地表达一些复杂的数据结构,比如系统的动态分配内存、消息机制、任务调度、灵活矩阵定时等等。

掌握指针可以使你的程序更加简洁、紧凑、高效。

那么在单片机领域,如果是做稍微大一点的项目,需要把每个功能做成模块化,硬件驱动层和应用层分别独立运行。

即使更换单片机型号也不用修改应用层程序,即移植性非常强,这些都离不开指针。

甚至没指针会很难实现,即使实现代码的可移植性也很差。

三、指针的概念

前面讲了指针的作用,这里再强调一点,指针是一把双刃剑。

用好了能十分灵活而且提高程序的效率,但是如果使用不当,则会出现程序”死机”等致命问题。

而这些问题往往是由于错误地使用指针而造成的,最常见的就是内存溢出错误,指针指向未知地址。

1.地址与指针

指针是一个比较抽象的概念,如果想真正了解指针,那么要先从数据是如何存储的说起,我们通过一个图来看一下数据在内存里存储的情况。

图片

在这个图中,都是以16进制显示。

红色标注的0x00000400代表地址内存地址绿色37,30代表数据而橙色标注的00 01代表地址递增量,即代表0x000004000x00000401,每个地址存储1个字节数据。

那么我们把这个图看作是数据在内存里的存储形式,0x00000400这个内存地址存储着数据37,0x00000401这个内存地址存储着数据30。

当我们在程序里定义一个字节的变量,那么在编译器编译时就会给这个变量分配一个这样的内存地址来存储。

假设我们定义以下变量

unsigned char a;

a = 0x37;

对应这个图就是,编译器在编译时会为变量a分配一个字节的内存空间并把0x37这个数据存储进去,并将变量名a改成地址0x00000400,以便CPU的访问。

通过这个地址就能找到变量a数据的存储位置,而这个地址0x00000400其实就是指针通过这个指针可以访问变量a的数据

2.指针变量

通过上面讲解我们明白了通过地址能访问内存的数据,这个地址啊就是指针。

那么指针和指针变量呢是不一样的概念,大家一定要记住了。

指针是概念、指针变量是这个概念的具体应用之一,我们先来看一下C语言里怎么定义指针变量。

指针变量定义的一般形式:

变量类型 *变量名

unsigned char *p;

通过这种语法,我们就能够定义一个指针变量p。

指针变量赋值

指针和指针变量是两个概念,指针变量跟普通变量一样,在使用前一定要定义和赋值(指向地址)

给指针变量赋的值和普通变量不同,给指针变量赋值只能赋地址,而不能赋予其他任何值,否则会引起错误。

那么怎么获取普通变量的地址呢,在C语言里可以使用”&”来获取普通变量的地址,一般用以下格式来表示:

&变量名

那么通过&变量名取得变量地址后就可以赋值给指针变量

举例:

 unsigned char a;

 unsigned char *p

 int main()

 {

       p = &a;
 }

这个代码里,我们定义了一个变量a, 定义了一个指针变量p

我们通过运算符&把变量a的内存地址赋值给变量p,所以p指向了变量a的内存存储地址

上面说了指针变量赋值的问题,那么怎么获取和改变指针变量指向那个内存地址的数据呢?我们可以通过:

*指针变量 = 数值

如:*p = 10;

这样的操作可以改变指针变量指向那个内存地址的数据。

通过:

a = *p;

来获取指针变量指向那个内存地址的数据。

下面我们通过一个代码实验来举例。

图片

这里我们定义了变量a和指针变量p,然后a的值初始化为10。

把a的地址赋值给指针变量p,接着我们输出a的地址是0x60ff33。

由于前面我们把a的地址赋值给了指针变量p,所以p指向的地址也是0x60ff33。

那么我们再来看一下,指针变量的在内存里的存储地址是0x60ff2c。

所以大家这里要注意了,我们定义指针变量时,即便指针变量是指向地址用的,但是编译器也会分配一块内存地址来存储指针变量

我们接着来看下变量a的输出值。

a=10, *p是获取指针指向内存地址的数据,所以也是10。

下面就是通过指针变量来改变变量a的值,因为指针变量p指向的是变量a的地址,所以改变指针变量p指向内存地址的数据就可以改变变量a的值。

那么通过这么原理,我们是不是不用指针变量,也不用a等于多少来改变a的值呢?当然可以!

我们看下面通过内存地址改变变量a的值,我们前面知道a的地址是0x60ff33,那我们可以直接写0x60ff33=12来改变变量a的值。

当然这里要注意,编译器编译时并不知道0x60ff33是什么东西,所以要把这个整形地址转换成指针类型。

最后通过*+地址语法改变这个地址里面的数据。

我们看输出结果,可以发现a的值已经成功被改成了12。

其实通过指针变量改变某个内存地址的数据就是这个原理,但是指针变量好处可以任意起名字。

也不用像这样先把变量a的地址读出来,然后通过地址去改变它的值,用起来就很方便,所以通过指针变量来替代了这种做法。

四、数组与指针

一般系统或编译器会分配连续地址的内存来存储数组里的元素,如果把数组地址赋值给指针变量,那么就可以通过指针变量来引用数组,读写数组里的元素了。我们来做个实验:

图片

从这个代码来看,定义了一个数组buff并初始化为1,2,3,4,5。

定义了2个指针变量p1和p1,分别指向buff, &buff[0]。

buff默认的是数组下标为0元素的存储地址。

所以这里buff和&buff[0]是同一个内存地址,只是写法不一样。

我们从输出结果可以看的出来,数组和指针变量的地址都是一样的,所以大家用这几种写法都是可以的。

那么我们来看下输出结果,都是1,说明操作是对的。

指针自加自减运算

指针变量除了可以用来获取内存地址的值以外,还可以用来进行加减运算。

但是这个加减呢跟普通变量加减不一样,普通变量加减的是数值,而指针变量加减的是地址,我们来通过代码来讲解下。

图片

同样这里定义了数组buff并初始化为1,2,3,4,5。

我们把指针变量p1指向数组第一个元素的地址,即0x402000。

然后我们直接看p1++的操作,p1++后我们看到p=0x402001,所以指针变量的加减等运算是指向地址的运算

其他减法乘除法也是基于地址的运算。

二维数组与指针

通过一维数组与指针的讲解,相信大家已经掌握。

那么二维数组与指针的操作也是一样的, 二维数组和一维数组一样,都是分配连续的地址来存储的数据的。

我们还是通过一个例子来实践一下:

图片

首先我们定义了一个二维数组buff和指针变量p1。

p1指向二维数组的[0][0]这个元素地址,这个就是为这个数组分配时的首地址。

然后打印二维数组里每个元素的地址和值,接着打印指针变量地址和值,这些就是指针和二维数组的用法,比较简单,这些代码大家可以去做下实验。

四、指向指针的指针

一个指针变量指向整型变量或者字符型变量,当然也可以指向指针变量,这种指针变量用于指向指针类型变量时,就称为指向指针的变量,也叫双重指针。

定义方法:

数据类型 **指针变量名;

例如:unsigned char **p;

这个含义就是定义一个指向指针的指针变量p,它指向另一个指针变量,我们通过代码来说明一下会更好理解一点。

图片

我们定义一个变量a, 定义一个指针变量p1,定义一个双重指针变量p2,然后打印这3个变量的内存地址。

编译器在编译的时候呢,也会为指针变量和双重指针变量分配一个存储空间。

虽然指针变量是指向别的内存地址的,但是变量本身还是需要一个地址空间来存储的

指针容易把人搞晕的就是,指针变量本身的存储地址和指向的地址分不清楚,这个是两个概念,大家要记住了。

下面我们通过实验来看下双重指针怎么用:

图片

这里我们定义了变量a并初始化值为10,指针变量p1,双重指针变量p2。

我们把p1指向变量a,p2指向变量p1的存储地址,这里要注意,不是p1指针指向的地址。

然后我们打印看下结果,可以看到a的地址是0x404090。

指针变量p1的存储地址通过&运算符获得即0x4040b0,p1指向a的地址,所以p1也等于0x404090。

所以指针变量分为存储地址和指向地址,这两个是不一样的概念。

而p2是双重指针,p2指向p1的存储地址0x4040b0,通过*p2获得0x4040b0这个地址里指向的地址0x404090,即p1指向的地址或变量a的地址。

再通过**p2来获取0x404090地址里的值,得到10。

这里还有一个问题需要注意,”*”这个运算符是从右到左进行运算的

所以,**p2就是*(*p2),先取指向地址,再取指向地址里面存储的值

一般在单片机程序中,尽量少使用这种指向指针的指针,防止出现Bug的时候非常难排查,目前我就在队列中使用过。

五、指针变量作为函数形参

一般我们都是以字符型、整型、数组等作为函数的形参带入。

除此以外,指针变量也可以作为形参使用,而且用的非常多,主要目的是为了改变指针指向地址的值,专业术语是通过形参改变实参的值。

我们直接写个代码来举个例子:

图片

这个代码中,我们定义一个SetValue函数,并且形参为指针变量p1。

我们调用SetValue时把&a的地址赋值给形参指针变量p1。

当我们通过*p1=5后就能把p1指向地址的值改成5,所以a的值也从1变成了5。

这个就是指针变量作为函数形参的一种作用。

实际当中使用功能当然不会这么简单。

比如说我们常用的memset库函数,他的原型就是:

Void *memset(void *s, int ch, size_t n);

这个函数的作用是给某个数组或者结构体初始化用的。

那么这个函数就使用了无指定数据类型的指针变量s,这样我们就可以很轻易的把实现某些功能的代码封装起来,使用者不用关心功能代码的实现,只需要了解函数怎么用即可。

这样的话代码很简洁紧凑,移植性也好,这是把指针作为形参的一种作用,不过这些都只是冰山一角,在后面的学习当中,你会慢慢发现指针的魅力和强大。

六、函数指针

如果在程序中定义了一个函数,那么在编译时系统就会为这个函数代码分配一段存储空间,这段存储空间的首地址称为这个函数的地址

而且函数名表示的就是这个地址

既然是地址我们就可以定义一个指针变量来存放,这个指针变量就叫作函数指针变量,简称函数指针。

在这个章节我们大家只要学会怎么定义和使用就行了,后面章节课程我们无际单片机编程会教大家函数指针的一些实际应用。

我们学东西主要还是看能运用在哪里是吧?

那么这个函数指针怎么定义呢?我们定义函数指针的格式如下:

函数返回值类型 (* 指针变量名) (函数参数列表);

图片

这样就定义了一个函数指针变量func, 该函数指针返回值为unsigned char类型,然后有2个形参,分别是unsigned char类型。

那么我们定义了这个函数指针变量以后要怎么使用呢?我们写个代码来解析一下。

图片

我们看下这个代码,首先我们定义一个函数指针func,再定义一个加法函数add,函数返回值为形参1+形参2的值。

然后我们把func指向加法函数add,因为函数名称就是函数首地址,所以我们直接func=add就可以实现func指向add了。

接着(*func)(1,2)代表执行func函数指针指向的函数,所以结果等于3。

函数指针func的返回参数和形参不一定要和函数add定义成一样,func也可以不设置返回值或者形参,但是一般不建议这样做,避免引起一些不必要的错误。

那么这里呢其实还有一个知识点要和大家说一下,我们先来写一段代码:

图片

这段代码调用函数指针的时候没有使用(*func)(1,2),这种用法也是可以的,执行的效果是一样,那么到底有什么区别呢?

其实这个是编译器实现的问题,我们不用去纠结这种对我们没有意义的东西,除非你想去做编译器。

大家只要记住函数指针是这样用的就行了。

后期应用时再把它们多练几遍,以后做产品都用上,那么基本就熟了,而且产品的程序架构也更好了。

七、函数指针数组

像字符型,整形都是可以单独定义,也可以定义成数组,同样函数指针也可以定义成数组,同样,这里我们不讲那么多理论上的概念,直接记住怎么定义,怎么使用、用在哪里就行了。

函数指针数组定义格式如下:

函数返回值类型 (* 指针变量名[数组大小]) (函数参数列表);

我们用程序表示如下:

图片

这样就定义了一个可以指向3个函数的函数指针数组。

定义了以后,我们函数指针需要赋值,赋值的意思就是让它们指向函数首地址,一般初始化的方式有两种。

图片

 

这是第一种,定义函数指针数组的时候直接初始化。

图片

这是第二种,先定义然后再初始化,这里我们主要是要记住它们这两种的写法就行了。

函数指针数组赋值以后通过以下代码来执行。

图片

我们可以看到直接写func[0]();就可以执行函数指针数值指向的函数了。

那么这种函数指针数组到底有什么用呢?

其实真正产品应用中函数指针数组是非常有用的。

我举一个例子,写控制5个LED灯亮的函数,如果用传统方式,流程是先要判断控制哪个LED,然后再控制指定GPIO口高低电平。

而函数指针只要一条语句,这就是所谓的代码的简洁、紧凑的特点,代码简洁紧凑以后自然也能节约cpu和内存的资源。

下面是演示代码:

图片

图片

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

electorn+vue3项目启动后报错unsafe-eval,如何去除提醒

electron项目报错如下: Electron Security Warning (Insecure Content-Security-Policy) This renderer process has either no Content Security Policy set or a policy with “unsafe-eval” enabled. This exposes users of this app to unnecessary security r…

【FreeRTOS基础入门】任务通知

文章目录 前言一、任务通知介绍1.1 任务通知怎么通信1.2 任务通知与其他通信方式的区别1.3 优势及限制任务通知的优势任务通知的限制 1.4 内部原理 二、任务通知的使用2.1 发出与接收通知简化版2.1 发出与接收通知专业版 总结 前言 FreeRTOS 提供了丰富而灵活的任务通知机制&a…

基于51单片机的婴儿看护监测系统[proteus仿真]

基于51单片机的婴儿看护监测系统[proteus仿真] 婴儿看护检测系统这个题目算是课程设计和毕业设计中少见的题目,本期是一个基于51单片机的婴儿看护监测系统[proteus仿真] 需要的源文件和程序的小伙伴可以关注公众号【阿目分享嵌入式】,赞赏任意文章 2&a…

openGauss学习笔记-226 openGauss性能调优-系统调优-配置LLVM-LLVM适用场景与限制

文章目录 openGauss学习笔记-226 openGauss性能调优-系统调优-配置LLVM-LLVM适用场景与限制226.1 适用场景226.2 非适用场景 openGauss学习笔记-226 openGauss性能调优-系统调优-配置LLVM-LLVM适用场景与限制 226.1 适用场景 支持LLVM的表达式 查询语句中存在以下的表达式支持…

优化设备维修流程:易点易动设备管理系统的设备维修管理策略

在现代企业中,设备是生产运营的核心要素之一。然而,设备故障和维修常常给企业带来诸多困扰和成本。为了提高设备维修的效率和质量,许多企业开始采用先进的设备管理系统。在这方面,易点易动设备管理系统以其卓越的设备维修管理策略…

2023年Q4,SSD出货量下降5%,但存储容量增长9.6%

2023年第四季度(4Q23)的SSD市场表现呈现出单位出货量下降,但存储容量增长的趋势。具体数据显示,该季度SSD总出货量下降5%,降至8820万台; 而存储容量则增长9.6%,达到85.1EB。预计2023全年SSD总容…

前端基于Verdaccio搭建私有npm仓库,上传npm插件包,及下载使用自己的npm插件包

文章目录 一、原理二、常用的仓库地址三、优势四、准备环境六、使用verdaccio搭建私有npm服务1、安装2、运行3、配置config.yaml,使局域网下能共享访问,否则只能本机访问。4、重新运行 七、npm常见操作查看当前用户信息查看源地址切换源地址删除源地址创…

【Linux】Linux调试器-gdb使用

1. 背景 程序的发布方式有两种,debug模式和release模式 Linux gcc/g出来的二进制程序,默认是release模式 要使用gdb调试,必须在源代码生成二进制程序的时候, 加上 -g 选项 2. 开始使用 gdb binFile 退出: ctrl d 或 quit 调…

【C语言】注释

🎈个人主页:豌豆射手^ 🎉欢迎 👍点赞✍评论⭐收藏 🤗收录专栏:C语言 🤝希望本文对您有所裨益,如有不足之处,欢迎在评论区提出指正,让我们共同学习、交流进步&…

校园公寓管理系统

校园公寓管理系统是一种专为提升高校住宿管理效率而设计的综合性智能解决方案。这一系统利用现代信息技术优化和简化了传统的住宿管理流程,极大地提高了管理工作的透明度和学生的住宿体验。 一、系统组成 校园公寓管理系统一般包括住宿分配、安全监控、费用管理、维…

java面试题之SpringMVC篇

Spring MVC的工作原理 Spring MVC的工作原理如下: DispatcherServlet 接收用户的请求找到用于处理request的 handler 和 Interceptors,构造成 HandlerExecutionChain 执行链找到 handler 相对应的 HandlerAdapter执行所有注册拦截器的preHandler方法调…

树莓派E: You don t have enough free space in /var/cache/apt/archives/.

在树莓派实际使用当中,我们会发现SD卡的存储并没有得到充分的利用,是否有办法让可用空间变的更大,毫无疑问肯定是有的,不然我就不在这里废话了。 使用raspi-config扩容 一、df -h查看可用空间 首先输入"df -h"命令可…

typescrip接口 interface详解,以及ts实现多态

ts 接口 当一个对象类型被多次使用时,一般会使用接口(interface)来描述对象的类型,达到复用的目的 示例如下 当一个对象类型被多次使用时,可以看到,很明显代码有大量的冗余 let personTom: { name: string, age?: number, sayHi(name: string): void } {name: Tom,sayHi(n…

Java Web(七)__Tomcat(二)

Tomcat工作模式 Tomcat作为Servlet容器,有以下三种工作模式。 1)独立的Servlet容器,由Java虚拟机进程来运行 Tomcat作为独立的Web服务器来单独运行,Servlet容器组件作为Web服务器中的一部分而存在。这是Tomcat的默认工作模式。…

一文弄明白KeyedProcessFunction函数

引言 KeyedProcessFunction是Flink用于处理KeyedStream的数据集合,它比ProcessFunction拥有更多特性,例如状态处理和定时器功能等。接下来就一起来了解下这个函数吧 正文 了解一个函数怎么用最权威的地方就是 官方文档 以及注解,KeyedProc…

Observability:使用 OpenTelemetry 和 Elastic 监控 OpenAI API 和 GPT 模型

作者: 来自 Elastic David Hope ChatGPT 现在非常火爆,甚至席卷了整个互联网。 作为 ChatGPT 的狂热用户和 ChatGPT 应用程序的开发人员,我对这项技术的可能性感到非常兴奋。 我看到的情况是,基于 ChatGPT 的解决方案将会呈指数级…

C++实现归并排序题目

目录 例1 例2 例3 例4 例1 912. 排序数组 参考代码 class Solution { public:vector<int> tmpnums;vector<int> sortArray(vector<int>& nums) {tmpnums.resize(nums.size());mergeSort(nums, 0, nums.size() - 1);return nums;}void mergeSort(vector…

如何使用rocketmq实现分布式事务?

什么是rocketmq事务消息 事务消息是 Apache RocketMQ 提供的一种高级消息类型&#xff0c;支持在分布式场景下保障消息生产和本地事务的最终一致性。 RocketMQ的分布式事务又称为“半消息事务”。 事务消息处理流程 RocketMQ是靠半消息机制实现分布式事务 事务消息&#x…

OpenAI 的 GPTs 提示词泄露攻击与防护实战:防御卷(一)

前面的OpenAI DevDay活动上&#xff0c;GPTs技术的亮相引起了广泛关注。随着GPTs的创建权限开放给Plus用户&#xff0c;社区里迅速涌现了各种有趣的GPT应用&#xff0c;这些都是利用了Prompt提示词的灵活性。这不仅展示了技术的创新潜力&#xff0c;也让人们开始思考如何获取他…

C++学习Day09之系统标准异常

目录 一、程序及输出1.1 系统标准异常示例1.2 标准异常表格 二、分析与总结 一、程序及输出 1.1 系统标准异常示例 #include<iostream> using namespace std; #include <stdexcept> // std 标准 except 异常class Person { public:Person(int age){if (age <…