return语句
在前些日子,return提供了函数的返回值,并且结束当前函数返回到调用它的地方,实际上,即使函数没有返回值,也可以使用return语句,比如在检查到一个错误时提前结束当前函数打印错误并返回:
当x不大于0的话就打印错误提示,并提前返回。好像感觉用处不是很大,可能是我目前知识层没接触到它的用法吧。
return 的好处就是,可以返回布尔值,这样的话,某个函数就可以作为 if 的判断语句
return 甚至也可以直接进行运算,如: return !(x % 2); 这个运算,如果x时偶数将返回0,如果是奇数将返回1,结合if_else语句,就有很多拓展写法了
对于return的写法千万要注意,很有可能使用return写出永远不会被执行的代码:
其中的最后一行printf永远不被执行。
如果当x = 0 函数就会默默结束,什么也不返回。
那么如果我使用了这个函数的返回值怎么办,C语言对上述情况是未定义的,所以使用return应该考虑好所有可能的结果
当然,上面这两种写法都是不会报错的,实际上,编译器报错反而比不报错的错误要好的多,不确定因素对未来是十分恐怖的,就像是蝴蝶效应,也许你在某个晚上疯狂调试没有错误的代码,就是因为这一只小小的蝴蝶。
增量式开发
这个项目我有时间抽空做一个详细的整理,如果觉得很繁琐,请见谅,我的水平十分有限,如果能得到您的简洁和指教,我是十分开心的。
增量式开发是一种思路,将问题不断拆解拆解,从一点一点小地方去实现,然后整合,增加,达到最终的目的。
我以一个智能风扇底座为例子:以前曾临时帮同学编写过一个小项目,叫做智能风扇底座,实际上就是结合蓝牙和温湿度传感器以及人体传感器的一个很简单的项目,当时时间很紧,只有3天时间就要校赛,主控芯片选用的是简单的arduino uno R3,主要实现如下几个功能:
1、手机蓝牙串口实时操控底座的旋转以及通断电;
2、蓝牙串口实时检测当前温湿度,通过串口app反馈;
3、手机蓝牙app发送指令,让智能风扇底座分别进入定时断电,定点巡航,智能朝向人体吹风,传统摆动模式等
我刚拿到手上,我一看,好家伙,这么多功能,我已经在怀疑3天够不够用了,加上我还是从来没有用过arduino,当时就慌的一批,首先第一步干什么?找资料啊,看他们买的各种模块的淘宝链接,先去淘宝看有没有资料,然后百度这么用,还好以前用使用过mps430,发现arduino和mps430还是挺像的,接着就去看了下如何将这些模块结合arduino,不搜不知道,发现全部都有开源,突然信心就来了。
PS: 我有个学长说过,玩嵌入式资料都不会找,你玩个鬼!
好,扯远了,接着来看增量式思路:
一个一个来,先设置蓝牙串口,就是一个rx一个tx,在主循环函数直接写了串口配置,然后插线做测试,好,这时候发现蓝牙串口没问题,手机也能收到,接着要测试手机发送单片机是否能收到,接着编写一个如果收到蓝牙的某个信息,就改变led的状态,因为代码量很少,基本上就像是在做测试,所以很快就实现了,好了,这样意味着手机可以和mcu通过蓝牙串口通信了,接着不是要进行下一个模块的测试,首先把测试好的蓝牙 “封装” 起来,编写一个蓝牙初始化函数,将蓝牙配置写进去,单独放置(其实arduino给了一个setup函数,就是用来干这个的),编写一个检测蓝牙串口信息的函数,同时通过硬串口将收到信息打印出来,好了,现在测试检测蓝牙串口并输出到硬串口的函数是否正常,打开串口调试助手测试,哎呀,有问题了,输出不对,停止,开始就这个地方debug,对于这种没有实时调试的ide,要debug就要依靠串口输出,在每一行代码下都打印出某个表示,然后输出相关的变量,当发现变量有问题的时候,就是那个位置有问题,接着把它屏蔽,看看还有没有错,没有,就可以肯定是这里出了问题,然后先检查数据类型,哦~原来是 int 和 char 搞错了,更改,在测试,好没问题了,接着往下写;
蓝牙串口可以通信了,接着看底座旋转和通断电,通断电简单,是个继电器,就是高低电平控制通断了,好,先写一个初始化引脚的函数,接着先直接去主函数让这个引脚输出高电平,过几秒输出低电平,接线测试,ok没问题,但是我们以后使用不可能每次都写这个引脚高电平,低电平吧,多了我都不知道这是干嘛的,还是要 “ 封装 ” ,写一个专门的函数控制高低电平吧,这个函数要有输入量,0就是低,1就是高,开始写,写好了去主函数测试一下,没问题好,放一边,下一步看底座旋转的问题,底座旋转是步进电机,这好办啊,先使能几个引脚,然后给脉冲就对了,直接主函数里写好,做测试,能动那就没问题,接着又要 “ 封装 ” ,步进电机有步数,还有正反方向转,所以要两个输入量,比如,1是正转,0是反转,200是旋转步数,重新写一个步进电机控制函数,主函数里做这个函数的测试,有问题,好,使用串口,一部一部打印调试纠错,写好了步进电机了,放一边,接着看:
温湿度传感器,接好对于引脚,引入温湿度传感器的库,看一下库如何使用,单独做测试,ok放一边,接着写人体传感器,也是一步一步来,所有的模块都实现了,接着就是难的地方了,改整合的继续整合,比如传统摆动,就是左转一些,右边转一些,写一个传统摆动的函数,调用上一步写的步进电机函数,接着是传感器和步进电机的结合,确定传感器位置和步进电机多少步数对于角度,单独写函数,接着要把温湿度传感器传送到手机app上,调用最开始写的通信函数,等等等等,实现了更具体的模块,结束了吗?当然是没有;
最后将这些所有功能都能实现的函数进行逻辑排序,先进行什么,后进行什么,什么放在中断里,什么放在主函数里,谁和谁有关联,出现错误如何查找,这是层层递进的关系,你会发现这就是增量式编程,它出错率很低,因为你是一个一个写的,都是对的,并且你的思路很清晰,每一个函数是干什么的,怎么使用,即使你放在那边几个月,再次打开你还是能用最少的注释和最少的时间了解这些函数的功能,并且可以在这个基础上继续开发,然后看看你写的代码吧,哇~上千行了,放眼望去感觉自己是不是很了不起,哈哈哈,这就是增量式编程的好处。
千万要记得一点,我们不要编写功能重复的函数,维护重复的函数是要死人的,你永远不知道什么时候有什么人来打乱你的逻辑,要尽可能复用(reuse)函数,封装就是为了让你重复使用,解决问题就是这样,把大问题分为中问题,中问题分为小问题,小问题分为不是问题,接着就没有问题,所有的问题都是可以解决的!
永远相信你自己可以解决所有问题 ! ! ! ! ! !
如果你不相信自己没关系,相信我吧,相信我相信你就行了。
递归(Recursive)
这是一个我十分模糊的概念,如果定义一个概念需要用到这个概念本身,那么我们称它的定义是递归的,感觉这句话十分矛盾,我仿佛想起了被高数支配的那些日子,咳咳,数学上有很多概念是用它自己来定义的,n的阶乘等于n乘以n-1的阶乘,这不是套娃吗??这样下去是永远没办法结束的,所以必须要先定义一个最关键的条件:0的阶乘 = 1;这个关键基础条件称为(Base Case)
我们来尝试使用代码来解决这个问题:
对于这个函数我是这么理解的,带值进去,0的值是1,1的值是n*n-1的值,也就是1*1=1,2的值等于n*n-1的值,也就是2*1的值,1的值在前面又是1*1=0,所以2的值是2*1*1=2,同理,4的值是4*3的值,又等于4*3*2的值,等于4*3*2*1的值,等于4*3*2*1*1,结束,输出24。
随着函数调用的层层深入,占用的存储空间逐渐增长,然后随着调用的结束层层返回,存储空间又逐步被释放,并且每次访问参数和局部变量是只能访问这一段存储单元,不能访问其他的存储单元,具有这样的性质的数据结构我们称之为堆栈或栈(stack),每一个函数调用的参数和局部变量的存储空间称为一个栈帧(Stack Frame),操作系统为程序的运行预留了一块栈空间,函数调用时就在这个栈空间里分配栈帧,函数返回时就释放栈帧。
上面这句话我仿佛理解了,又仿佛没理解。
接着,再写一个类似上面这样的递归函数的时候,如何证明它是正确的呢?每次一步一步递推是绝对不可能的,首先我们相信某个函数能正确运行,然后调用它去完成另一项工作,应该也是正确的,这种相信称之为 Leap of Faith ,首先相信一些结论,然后用它们去证明另一些结论,这还真像高数,不是吗?
我觉的这本书写的已经很好,要我用自己的话可能我还没办法描述这么好,原文是:
在写factorial(n)的代码时写到这个地方:
int recurse = factorial(n-1);
int result = n * recurse;
这时,如果我们相信factorial(n-1)是正确的,也就是相信传给它n-1它就能返回(n-1)!,那 么recurse就是(n-1)!,那么result就是n*(n-1)!,也就是n!,这正是我们要返回 的factorial(n)的结果。当然这有点奇怪:我们还没写完factorial这个函数,凭什么要相 信factorial(n-1)是正确的?可Leap of Faith本身就是Leap(跳跃)的,不是吗?如果你相信 你正在写的递归函数是正确的,并调用它,然后在此基础上写完这个递归函数,那么它就会是 正确的,从而值得你相信它正确。 这么说好像有点儿玄,我们从数学上严格证明一下factorial函数的正确性。刚才说 了,factorial(n)的正确性依赖于factorial(n-1)的正确性,只要后者正确,在后者的结果上乘 个n返回这一步显然也没有疑问,那么我们的函数实现就是正确的。因此要证明factorial(n)的正确性就是要证明factorial(n-1)的正确性,同理,要证明factorial(n-1)的正确性就是要证 明factorial(n-2)的正确性,依此类推下去,最后是:要证明factorial(1)的正确性就是要证 明factorial(0)的正确性。而factorial(0)的正确性不依赖于别的函数调用,它就是程序中的一 个小的分支return 1;,这个1是我们根据阶乘的定义写的,肯定是正确的,因此factorial(1)的 实现是正确的,因此factorial(2)也正确,依此类推,最后factorial(n)也是正确的。其实这就 是在中学时学的数学归纳法(Mathematical Induction),用数学归纳法来证明只需要证明两 点:Base Case正确,递推关系正确。
我想到这里的话应该是非常清楚明了了,接着举出,如果写递归函数忘记写 Base Case ,即使关系正确,整个函数就会无限调用下去,直到操作系统崩溃,这称之为无穷递归。
循环和递归式等价的,用循环能做的事情,用递归都能做,顺便一提,编译器在我们写程序的时候也用了大量递归。
这里我依稀记得有点类似的一个东西,曾经我在知乎上看到这样一个问题:C语言本身是用什么语言写的?
我当时看到的第一眼就有点懵,C语言是用什么写的呢?我看到了回答,C语言的编译器使用C语言写的!!
啊?wtf?鸡生蛋蛋生鸡问题?
是不是很有趣,但是我意识到,这里是迭代,不是递归,那么迭代和递归又有什么区别呢?在查找这些问题的时候,我有发现了其他的,在这里把它们列出来:
循环(loop),指的是在满足条件的情况下,重复执行同一段代码。
迭代(iterate),指的是按照某种顺序逐个访问列表中的每一项。
遍历(traversal),指的是按照一定的规则访问树形结构中的每个节点,而且每个节点都只访问一次。
递归(recursion),指的是一个函数不断调用自身的行为。
剩下的,呃,实际上我越看越有点看不懂了,以后有时间对它们透彻的理解了在这里重新整理。