【C语言实战经验5】Bug到底怎么产生的?编码小技巧,让你的代码更稳更帅!
本帖最后由 dffzh 于 2025-5-22 19:09 编辑#申请原创# #技术资源#
@21小跑堂
有时候软件Bug的产生原因可能是由于我们在编写代码时粗心,坏习惯或者缺乏减少Bug产生的编码技巧导致的。今天,作者就分享一些简单的但非常实用的编码小技巧,希望能够为同道的朋友们提供一些编码的参考价值。话不多说,直接进正题。本期主要介绍以下编码小技巧:技巧1:变量和常量在判断语句中做相等比较时,将常量放在等号左边技巧2:变量一定要初始化,特别是局部自动变量(非static)技巧3:用好宏定义#define技巧4:重要代码位置要加注释,并且使用英文注释,代码修改后,也需同步更新注释技巧5:驱动代码开发,尽量参考官方demo,别轻易自创技巧6:尽量缩小变量和函数的作用域技巧7:加上关键字volatile修饰变量,告别偶发性Bug,让你高枕无忧技巧8:不要直接在中断程序里加printf调试信息和太多的功能代码,改用全局标志位实现技巧9:代码模块化,尽量解耦,实在无法避开,就用函数封装全局变量技巧10:使用函数API接口的方式实现和封装通用功能技巧11:函数定义要巧妙,函数命名不再是难事技巧12:尽量解决编译警告warnings,别让它们成为风险项技巧13:复杂运算分步走,别一步到位,除非你有十足把握技巧14:开发时仅导入有用的MCU固件库外设文件下面将逐一介绍。
技巧1:变量和常量在判断语句中做相等比较时,将常量放在等号左边在C语言中,注意区分等号(==)运算符和赋值(=)运算符:等号运算符的左边和右边都可以为常量;赋值运算符只有右边可以为常量;看看以下代码,编译报错:
所谓左值l-value,就是可以定位的内存位置,例如变量、数组元素或者结构体成员等。而右值r-value通常是指临时值或者不能定位的表达式结果,例如常量或者函数返回值等。比如如果你试图对一个常量赋值,编译也会报错:const int i = 1;i = 2; // 错误,因为i 是常量如果编码目的是为了判断常量和变量是否相等,代码按如下两种方式修改均可:
但希望你选择方式1,因为这样操作后,如果你在编码时漏掉一个“=”,编译报错;方式2漏掉一个“=”,编译通过,但这种赋值逻辑不是我们想要的判断逻辑。
技巧2:变量一定要初始化,特别是局部自动变量(非static)所谓变量初始化,也就是变量声明或程序运行时,设置变量的初始值(默认值、缺省值);不管是全局变量,局部变量或者是复合数据类型等,一律设置初始值。全局变量和静态变量(static)倒是还好,如果你忘记初始化,其初始值一般自动为0;但是局部自动变量(普通的局部变量)在创建时并不会自动初始化,因编译器不同,其初始值可能是内存中的随机数据;如果不显式初始化,很可能会导致不可预知的结果。我们来看下代码实测结果:
从以上实测结果就能明显看出结果;因此,你就记住:定义一个变量后,先不要做其他的,马上对其进行初始化设置,准没错!
技巧3:用好宏定义#define对于程序里有意义的或者频繁使用的数值常量,你得用宏定义方式来控制它们(当然如果你习惯用枚举方式也可以),不然随着程序迭代升级和修改,你会疯掉的;特别是刚入职场的从事软件开发的朋友们一定要养成这个习惯。打个比方吧,有一个延时时间阈值,在程序里使用了50次;如果你用宏定义方式实现,后续如果修改其值,你只要改一次即可,并且肯定不会出错;否则,你要修改50个地方,并且还有可能会漏改;很简单的逻辑,大家都懂的。看一个人的C语言功底怎么样,其实从这一点上就能分个大概。看看MCU厂商写的固件库或者RTOS源码,那真的是有数不清的宏定义!至于宏定义如何巧妙使用,可以参考作者的另一篇文章:https://bbs.21ic.com/icview-3450402-1-1.html
技巧4:重要代码位置要加注释,并且使用英文注释,代码修改后,也需同步更新注释为什么要这么操作呢?第一,我们自己写的代码,一段时间后再来解读,有些代码逻辑可能已经忘了当初为什么要这么写了?如果你有了注释,就容易回想起来了。第二,不同的代码编辑器或者IDE开发环境对中文的兼容性不一样,或者很多公司对文件是有加密操作的,可能会出现中文乱码问题,导致无法正常看注释,甚至再写入新的中文注释后也是乱码,因此强烈建议使用英文注释;另外,还可以锻炼你的英文水平,何乐而不为?
第三,代码修改后,你的注释一定要同步更新,这样你的注释才有意义;如果注释***只是第一次写代码时的样子,那毫无意义!
技巧5:驱动代码开发,尽量参考官方demo,别轻易自创芯片(MCU、ADC、DAC、flash等芯片)的驱动代码尽量参考官方demo来写,这些都是人家验证过的,基本没啥问题的;如果你有十足把握或者是预研产品,可以自创,否则即使功能上实现了,但性能上不一定优越。所以,要学会访问芯片官方网站的资料和咨询芯片技术支持等,切忌独自埋头苦干!
技巧6:尽量缩小变量和函数的作用域所谓作用域,就是你定义的变量或者函数的使用范围,是只能在此文件中使用还是可以在整个工程文件中使用。这样设计,不仅可以增加代码可读性,即使后续遇到问题,也可以缩短代码排查范围。第一,只在一个源文件中使用的变量,直接在源文件里定义为静态全局变量即可;第二,只在函数内部使用的变量,直接在函数内部定义为局部变量即可;这里其实还有一个关于变量的生命周期的概念,有兴趣的朋友可以了解一下;注意:对于元素个数成百上千的数组,考虑到占用堆栈空间多,可以定义为全局数组。第三,只在一个源文件中使用的函数,定义时直接加上static,即如果在其他文件被调用,编译会报错;
技巧7:加上关键字volatile修饰变量,告别偶发性Bug,让你高枕无忧之前有个坛友发了一个帖子咨询一个偶发性Bug,这个很经典,链接如下:https://bbs.21ic.com/icview-3444870-1-1.html其实像这种同时在中断里和main里读写同一个全局变量的代码操作,应该很多人写过吧?很多初学者可能对volatile(易变的)关键词比较陌生或者平时使用很少,其实它的主要作用就是通知编译器,被其修饰的变量值可能会在程序运行过程中,以一种编译器无法预测的方式发生改变,因此编译器在对代码进行优化时,不能对该变量进行常规的优化操作。每次访问该变量时都要直接从内存中读取真实值,而不是使用寄存器中的缓存值,以保证程序对变量的访问是实时的。那到底哪些情况下要加上volatile呢?第一、访问硬件寄存器;第二、防止编译器优化;第三、中断服务程序里的共享变量;就是上面那个帖子应该有的操作;网上有很多关于volatile的详细使用介绍,可以学习,这里就不不展开阐述了。
技巧8:不要直接在中断程序里加printf调试信息和太多的功能代码,改用全局标志位实现printf其实是一个比较复杂且运行耗时的函数,养成好习惯,即使是在自己调试阶段,也不要直接在中断程序里加打印信息。另外,特别是在中断频繁响应的应用场景,中断程序里尽量不要加太多的功能代码,尤其是那些特别耗时的浮点数运算或者数据排序什么的。即不建议使用如下代码操作:你可以改用全局标志位实现,即中断程序里置位标志位,main或者其他接口里轮询读取该标志位来判断是否执行功能代码,如下操作:这不就用到volatile关键字了嘛。
技巧9:代码模块化,尽量解耦,实在无法避开,就用函数封装全局变量不同功能模块之间,大部分情况下是不会出现代码相互耦合的,比如ADC功能模块和DAC功能模块之间,应该是没有任何逻辑互联的,这个时候就可以做成代码模块化和完全解耦,类似下面文件操作:如果两个模块之间存在耦合的逻辑关系,需要通过全局变量等方式来操作,这个时候就不要直接在逻辑程序里暴露全局变量,要通过函数方式来封装全局变量;即把如下的代码操作:改成如下的代码操作:这样操作后,就不会出现全局变量满天飞的代码了,代码质量瞬间提高了不少。
技巧10:使用函数API接口的方式实现和封装通用功能API,即Application Program Interface,应用程序接口,在这里你可以理解为函数接口;在程序开发时,我们会经常使用一些常用的功能,比如求两数之和,取两数中最大值,延时处理或者位移操作等,如果一个程序中使用某个通用功能的地方比较多,一旦有问题或者需要修改的时候,你的通用功能的实现方式就非常重要了,直接编写代码的方式让你事倍功半,函数封装的方式则让你事半功倍;
另外,你可以自定义一个源文件和头文件,专门用来封装通用API接口,可以参考如下:后续如果需要修改,你只需要修改普通函数或者宏函数的定义即可,这样代码的可维护性和可读性就大大增加了。
技巧11:函数定义要巧妙,函数命名不再是难事所谓的函数定义要巧妙,主要包括函数定义的行数限制和功能专一;行数限制:虽然一个函数定义的最佳行数没有明确标准,有推荐30行的,50行的或者200行以内的,但是考虑到代码可维护性和可读性,函数定义的行数要控制在合理范围内,过少可能会导致代码冗余,过多可能会降低维护效率。功能专一:一个函数定义尽量实现一个单一功能,不要涉及太多的逻辑功能,可以通过模块化设计将复杂功能拆解为多个小功能,每个函数处理一个小功能;大家应该对MCU固件库都不陌生,比如AT32F403A的固件库,很多函数定义就实现一个功能,并且通过函数名称基本上就能猜出该函数是实现什么功能:
因此,大家在编写自己的应用逻辑功能代码时,就可以参考这些优秀的demo程序。另外,如果函数功能太多,估计你在函数名称的命名上都不太好写吧!如果是单一功能,那就容易命名了!
技巧12:尽量解决编译警告warnings,别让它们成为风险项我们在编译程序时,往往只关注和解决errors,因为不解决它们,是没办法生成可执行文件的;但对于warnings,如果每次都会把它们解决的攻城狮和程序猿们,那可不是一般的优秀!作者以前写代码时,也和大家一样,管它呢,反正warnings不影响程序运行!直到后来进一家企业,源代码需要上服务器做coverity检查,那没办法了,只能硬着头皮解决warnings了,不然开会的时候会被“批斗”:在解决warnings的过程中,也确实发现了有些warnings可能在代码指定的运行分支下会造成程序异常,比如不同数据类型的转换和指针未初始化等;另外,消除warnings也可以提高代码的可维护性和可读性,还能省出来一些flash和RAM空间。
技巧13:复杂运算分步走,别一步到位,除非你有十足把握C语言里的运算符都有优先级和运算顺序的概念,如果一个算式里涉及了多个运算符,我们往往会通过加括号()的方式来操作;但是如果算式比较复杂,就建议不要用一条语句来一步到位,这样实现的逻辑容易与我们想要的背道而驰,在可读性和可维护性上也比较差。比如使用位操作运算符来实现交换一个字节数据里的指定位:第一种是这么实现:第二种是这么实现:作者倾向于以第二种方式实现。
技巧14:开发时仅导入有用的MCU固件库外设文件现在的MCU外设日益丰富,对应的外设文件也变多了,可能我们会一股脑地将官方demo程序里的全部外设文件导入到工程里面,每次全编译程序时都要好一会;其实大可不必如此操作,有些项目的功能简单,用不了几个外设,你把用上的外设导入到工程里面就可以了,不仅编译速度快,整个工程看上去也“清秀”许多,这才是高大上的体现!
以上作者根据自己的开发经验总结了一些编码小技巧,希望对大家有用!当然,C语言还有很多编码技巧,特别是指针的妙用,待以后分享!如果你有好的编码技巧,欢迎来贴分享!
页:
[1]