打印
[信息]

STM32 C语言“函数”深入剖析

[复制链接]
942|13
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
jcky001|  楼主 | 2021-10-12 10:56 | 只看该作者 |只看大图 回帖奖励 |倒序浏览 |阅读模式
作者:张角老师(张飞实战电子高级工程师)
大家好,我们今天对C语言函数的概念进行相关的探讨。探讨的思路还是基本按照前面几篇**的思路来进行,也就是说需要依次回答:是什么,为什么和怎么用的问题。具体到单片机C语言的函数,我们首先要明确函数是一个什么东东?C语言为什么要使用函数?这个问题主要是相对于汇编语言来说的,大家知道汇编语言就没有函数。函数是如何定义和声明的?后面的,就是函数在实际使用过程中相关的问题,比如函数和变量的定义以及声明有什么不同?函数与函数之间如何进行交互,比如参数传递以及函数如何返回计算结果?最后一部分,函数设计可能是C语言程序设计中的关键一环,我这里会总结一些函数设计相关的一些技巧,分享给大家,进一步减少程序设计过程中出错的可能性,提高编程的效率。

使用特权

评论回复
沙发
jcky001|  楼主 | 2021-10-12 10:56 | 只看该作者
我们首先来看,函数是什么呢?首先,函数有一个名字,这个名字大多数情况下,描述了它的基本功能。然后有的函数有参数,有的函数没有参数;有的函数有返回值,有的函数没有返回值。函数的参数,可以理解为函数进行运算时的加工原料;函数的返回值可以理解为函数计算结果。括号中的东西(函数体),可以理解为函数对数据的加工过程,或者理解为计算过程。

从上面的描述来看,函数应该是一个功能的封装。一般我们的C语言程序,是由许许多多封装起来的函数组成的。但是大家如果写过汇编语言的程序,可能就会有感觉,其实汇编语言并没有函数的概念,汇编语言程序就是一系列指令的罗列。那我们不禁要问,为什么C语言要引入函数的概念呢?函数概念的引入是需要解决什么问题?



使用特权

评论回复
板凳
jcky001|  楼主 | 2021-10-12 10:58 | 只看该作者
大家看一下,函数的引入是不是相当于把一个复杂程序的功能实现了拆解呀,具体拆解的颗粒度因人而异,因程序而已。这个拆解的思路,是不是完美体现了分而治之的策略。在编写程序的过程中,有些常用的功能很有可能屡次被调用,我们把这些常用的功能封装成一个一个函数。程序写到这里的时候,只需要调用这个函数就可以了,不用再重新把代码写一遍,这样是不是就可以较大程度的较少代码量呀。

实际上,我们调用的各种库函数,可以说是在编程中一些最常用的功能的集合。这些写好的、已经经过验证的、较为基本的函数模块,可以极大提升程序的开发效率。这个就相当于,建房子时,有好多基本的结构可以拿来直接使用,比如直接拿来一个柱子、一间房子、一个卫生间等等,不需要自己再使用最基本的砖块从头开始搭建。这样建房子(编程序)的效率是不是会猛增呀。

使用特权

评论回复
地板
jcky001|  楼主 | 2021-10-12 10:59 | 只看该作者
再一个,没有这个函数的封装,如果程序较大、功能较复杂,我们面对的是不是一个超长的代码呀,那读起来是不是就比较费劲。这个就有点类似古代的文言文不分段落、没有标点符号一样,读起来是不是就比较吃力。那么可以说函数这个概念的引入,可以把一些较长的程序切分成了很多个小模块,那是不是极大地增强了程序的可读性。程序的可读性强,那是不是就从侧面降低了程序的开发和维护成本呀,不用消耗那么多的智力资源就能完成任务。

前面,我们从函数功能的重用度或者说代码量的大小,以及函数对程序的可读性增强两个方面阐述了函数存在的必要性。那么C语言毕竟是人类发明的,肯定要从方便人去进行程序开发的角度来进行语言架构的设计,那么自然C语言就会引入函数的概念。从本质上讲,函数的引入就是为了降低程序开发的难度(这个是相对汇编语言而言的),提升程序开发的效率,方便人用更少的时间去做更多的事情。

使用特权

评论回复
5
jcky001|  楼主 | 2021-10-12 11:01 | 只看该作者
再一个,我们看一下,函数与函数之间的组装配合,实现了复杂的程序功能,这个过程是不是像极了人类社会中各个组织之间的相互配合。那么我们在进行程序开发的时候,函数的功能拆分怎么样才是相对比较合理呢?我觉得有两个指标,一个是函数之间调用的深度不要太深,太深的调用会影响程序的可读性。对于一个程序来说,不要分太多层,最好尽可能扁平化;第二个,函数与函数之间,功能一定要独立起来或者说功能解耦一定要尽可能彻底,同一层的函数与函数之间的功能没有相互依赖的关系。从图形上来说,这个可能就是一个树形的结构,第一个数不要太高,太高了人爬上去比较困难;第二个分支与分支之间不能相互影响,必须是各自隔离开,这样维护起来才更方便。从这个角度上,看对一个复杂的程序功能进行有效的拆解的过程,有点像行军打仗时,对军队的管理:不同的部分有不同的功能,各个部分之间有联系但不能是依赖(要能够独立行动),每个部分不能太大否则不好管理(尾大不掉),层级不要太多否则容易政令不通。

使用特权

评论回复
6
jcky001|  楼主 | 2021-10-12 11:05 | 只看该作者
讲完了函数是什么,以及C语言为什么要使用函数,那么就到了下面一个部分,如何声明及定义函数。函数的定义和声明,有点像全局变量,或者说函数都有全局属性,它的作用域是全局的。既然是全局的,那肯定要有声明和定义,一般情况下声明是放在“xx.h”头文件里面,定义则是放在“xx.c”文件里面。当然,如何一个函数只是在局部使用,并不对外提供服务,那么可以在这个函数名字面前加上关键字“static”。另外,在这个局部特定的文件里面,这个static函数放在了调用它的函数前面,那么这个函数同样也不需要在进行声明了。编译器会自动找到这个函数,并在链接的时候,自动链接。

既然默认的情况下,函数和全局变量都有全局属性,那么它们在声明和定义上有什么区别呢?主要的区别就在这个关键字“extern”上,全局变量声明的时候,一定要加上extern关键字;但是函数声明的时候,则不需要extern关键字(或者说这个可以省略)。至于为什么有这样的区别,可能就是因为函数这种“数据类型”过于复杂,它的定义和声明之间差别极大,定义有函数体存在,对吧,声明却没有。但是对于变量来说,声明和定义是不是没有什么区别呀,比如定义“int a”,声明也用“int a”,两个是不是重复的呀,所以对于变量来说,声明的时候一定要加上“extern”。这样,其实我们对extern这个关键字,也做了一定的总结。

使用特权

评论回复
7
jcky001|  楼主 | 2021-10-12 11:07 | 只看该作者
函数的名字,本身就是一个地址,但从这一点上来看,它有点像数组,和结构体等什么的不太一样。那为什么会这样呢?函数的名字为什么要是一个地址呢?这个可能就和单片机执行程序的思路一样,从某一个地址开始开始执行,那么自然函数的名字就是一个地址更加有利于编译系统对程序进行编译。那么既然函数的名字是一个地址,我们的变量类型里面又有一个指针类型变量,那么他们两个肯定会发生关联,那也就是说一个指针变量里面存储了函数的地址值。这个指针,我们称之为函数指针,全称应该是函数类型的指针,用以区别于整数类型的指针、浮点数类型的指针等等。函数类型的指针,它的声明自然是与众不同的,声明的时候就得按照函数的形式来搞,这个是遵循int* p之类的关于指针声明的法则的。与之相关的一个,比较容易引起混淆的概念,是指针函数。其实如果想区分开来,也是蛮简单的,就是一个返回指针数据类型的函数。这里只是函数指针和指针函数放在一起容易混淆而已。

讲完了函数的声明和定义,我们来看一下第三个问题,函数与函数之间怎么怎么进行交互的。这个交互包括两部分,一个调用方给被调用方数据传递,另一个是被调用方返回值给调用方。调用方给被调用方传递数据,其实大家理解起来也比较简单,就是通过函数的参数进行的。函数的参数需要什么类型的数据,调用方要按照约定传过去。被调用方给调用方传递数据,方式就多了。第一种方式,可以通过返回值的方式,把计算结果返回给调用方;第二种方式,则比较隐晦,不是通过计算结果来实现计算结果返回。具体实现的方式,调用方给被调用方传递参数的时候,传递的是地址;被调用方,通过修改这个地址指向的值来进行计算结果的返回。这个地方其实就是值传递和地址传递的区别,值传递的时候,没有办法实现计算结果这样返回。

使用特权

评论回复
8
jcky001|  楼主 | 2021-10-12 11:08 | 只看该作者
最后这一部分,我们来讲一讲函数设计的一般技巧,这样可以让我们在程序设计的时候,更少犯错,进一步提高程序编写的效率。

第一个,原则上尽量少使用全局变量。每个源文件负责本身文件的全局变量,同时提供一组对外函数,方便其他函数使用该对函数来访问这个变量,比如“SetValue”、“GetValue”等等,不要直接读写全局变量。尤其是在多线程编程的时候,必须要使用这种方式,并且要对读/写操作加锁。

第二,和这个类似,函数也尽量少使用static类型的变量,这种变量有**功能。有**功能的函数,其行为可能是不可预测的,因为它的行为可能取决于某种“**状态”,这种函数既不利于理解也不利于维护。

第三,我们要避免函数有太多参数,尽量把函数参数控制在4个或者4个以内。过多的函数参数,可能会导致函数的使用成本太高,比如容易把参数的顺序搞错。

第四,函数体的规模尽可能小,比如控制在80行以内或者说一页屏幕要能够看完。这个规则也可以说我们进行功能拆解的时候,要遵守的。当然,特殊的函数除外呀。

第五,我们要在函数体的入口如,对参数的有效性进行检查,尤其是指针参数。

第六,函数的返回值,一定不能是临时变量的地址。这个地址是在栈空间里面的,函数执行结束后,这个地址就是无效的了。那么自然这个地址,不能用作函数的返回值。

第七,如果是地址传递,则尽量在指针前面使用“const”关键词修饰,防止指针被函数内存的计算误修改。当然特殊需求除外。

关于C语言函数相关的知识,我就和大家先探讨到这里。




参考资料
(复制链接在浏览器打开)

①C语言中为什么要引入函数的概念
http://blog.sina.com.cn/s/blog_6fd2803b0100y9fl.html
②C语言函数
https://www.cnblogs.com/wucongzhou/p/12498949.html
③函数指针与指针函
https://www.cnblogs.com/nevel/p/6370264.html
④定义与声明、头文件和extern总结
https://www.cnblogs.com/liushui-sky/p/7693537.html
⑤C语言深度解剖,陈正冲,北京航空航天大学出版社

使用特权

评论回复
9
wakayi| | 2021-11-3 14:37 | 只看该作者
c语言博大精深啊

使用特权

评论回复
10
wowu| | 2021-11-3 14:40 | 只看该作者
一旦复杂了我就看不明白了

使用特权

评论回复
11
xiaoqizi| | 2021-11-3 14:44 | 只看该作者
讲解的非常详细啊

使用特权

评论回复
12
tpgf| | 2021-11-3 14:46 | 只看该作者
很多技巧都很实用

使用特权

评论回复
13
木木guainv| | 2021-11-3 14:50 | 只看该作者
当初学的时候就不是很明白

使用特权

评论回复
14
heimaojingzhang| | 2021-11-3 14:53 | 只看该作者
遇到问题了就知道了解的太少了

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

1504

主题

4513

帖子

6

粉丝