至此,我们已经将新人入职需要掌握的编程规范介绍完毕了,是否感觉和其他版本的编程规范不太一样呢。出现这种现象,最主要的原因是因为我们的编程规范仅仅是工具,便于代码审核才是目的。
同时,其他版本的编程规范就没有价值了呢?我认为所有的编程规范都是很多一线工程师的经验总结,都是值得去尊重的,虽然不能不加区别的拿来乱用,但很多内容是非常值得借鉴参考的。
还记得本章一开始我整理的那一段警世危言吗?
“一名优秀工程师的成长需要时间
但仅仅靠时间堆砌,却难以培养出一名优秀的工程师
让人无奈的是,国内工程师的成长环境恶劣
导致所谓的十年工作经验,仅仅是十年工作经历而已
或悲催的转行,或无奈的继续,不小心陷入中年危机
然而上有老,下有小……
”
要跨过这一道槛,需要完成从个人到团队的转变,更需要尽早构建自己的知识体系。
构建知识体系,是一个很抽象的概念,有点像正确的废话,但让人无从下手。为了让这一步操作可执行,我们团队经过多年的探索,继续祭出了自己的绝招:形式化。
四条半规范仅仅是最基础的要求,它会帮助新人尽快的融入团队,但并非符合四条半规范的代码就是优秀的代码。同样,审核代码时,符合四条半规范的代码仅仅表面让人看起来舒服,但逻辑混乱依然会让审核员抓狂。在硬件电路审核时,我们习惯对照检测表(checklist)进行,软件领域是否也可以构建自己checklist呢?更进一步,我们是否可以将别人的优秀经验拿来吸收呢?是否可以将平时经常犯的错误整理出来呢?是否可以将这些checklist归类整理体系化呢?
强化checklist,成为我们团队构建知识体系的形式化策略。
◇◇◇
为了让大家理解我们形式化的checklist,我举一些例子。
1. const限定词
编程中,程序员容易犯的一个错误是通过指针或数组传递的数据被函数内部给修改了,如下述代码所示,错误比较隐晦:
int sum(int *ar, int n)
{
int i;
int total = 0;
for (i = 0; i < n; i++)
total += ar[i]++;
return total;
}
为了减少这类隐患,我们约定:如果传递为指针或数组变量,且不允许程序内部修改其指向值时,必须增加const声明,如上述程序应修改为int sum(const int *ar, int n)。
在嵌入式系统中,const数据类型位于flash空间,而非const数据位于ram空间,一般情况下,芯片中flash空间比ram空间更大更便宜,因此,我们约定:所有的常量型参数都应该增加const声明,如:
const int days[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
在使用const时,有一种用法为:int * const p;这表示p这个指针不能修改他指向其他位置,但依旧可以修改p指向的整数。这种用法适用情况很少,而且语法也会给新人带来困惑,为了便于代码审核,我们约定:不要使用int * const p类程序。
增加const约定后,不带const限定符的指针一般表示返回值,结合函数注释风格,该部分参数应该位于output段,如下示意:
/*
* Description: 读取文件内容
* Input:
* LPCTSTR lpszName: 文件名称
* DWORD dwOffset: 偏移位置
* DWORD dwLen: 读取长度,0表示读取到文件尾部
* Output:
* BYTE* pBuf: 读取文件数据存储地址
* Return: 成功返回TRUE,否则返回失败
*/
BOOL hwReadFileData(LPCTSTR lpszName, DWORD dwOffset, BYTE *pBuf, DWORD dwLen);
2. 配置常量选项
在嵌入式系统中,经常采取预先分配内存的策略,如系统允许的最大任务数,如最大允许的tcp连接数等。我们约定:这类配置选项,尽量在相关模块中提供默认值,且允许在系统配置文件中修改。如下示意:
系统配置文件syscfg.h中:
…
#define xxxx 20000
…
特定实现文件:
#include “syscfg.h” /* 必须包含系统配置文件 */
#ifndef xxxx
#define xxxx 10000
#endif
3. 限定边界判断
审核代码时,碰到边界判断最让人头疼,是应该用>呢,还是用>=。这类程序不仅难判断,而且容易错误,也不容易测试。
c语言中的变量是从0开始计数的,因此使用>=和<进行判断是最舒服的姿态,如下示意:
for (i = 0; I < dwCount; i++) {
ar[i] = …;
…
}
for (I = dwCount – 1; i >= 0; i--) {
ar[i] = …;
…
}
因此,我们约定:边界判断仅允许使用>=和<,如果用到>和<=,需要增加注释说明,以提醒审核员。
4. 化除为乘
在arm cortex-m4体系中,VMUL.F32指令(浮点乘法指令)执行时间是1个时钟周期,而VDIV.F32指令(浮点除法指令)执行时间是14个时钟周期。因此,强实时代码模块,应该尽量使用乘法,且增加注释说明以提升可读性。
如:
*p++ = s_fIaSum / 5.0f;
可以优化为:
*p++ = s_fIaSum * 0.2f; /* 除法优化为乘法,用*0.2f代替/5.0f操作 */
整数除法类同于浮点数除法,某些算法中可以用1/x作为算法因子,或者调整计算顺序,减少除法计算次数。
注意,该约定仅适用于对执行时间有严格要求的强实时模块,常规模块需综合权衡可读性和执行效率。
5. For each function parameter the type given in the declaration and definition shall be identical, and the return types shall also be identical.
这是MISRA-C规范的8.3条目,要求函数的原型声明和定义完全一致,包括函数参数和返回值,注意也要求了const等类型的限定词。
◇◇◇
纵观我上面举的这些例子,是否感觉比较乱,有C语言基础语法的加强,有编程技巧,有关于高性能程序的,甚至有直接从MISRA-C直接挪移过来的。
checklist特容易杂序混乱,而且会随着条数的增加而进一步加剧。杂乱的checklist不是知识体系,甚至会因为学习记忆困难,连checklist最基本的功能都做不到。因此需要对checkList整理归类。
我们团队经过了多年迭代后,整个checkList分为如下几类:
- 整体概述部分,侧重介绍checklist如何使用、迭代等。
- 四条半代码审核机制,也即本章内容。
- 重学c语言。新人在接触真实的程序代码一段时间后,一般都会重学一次c语言,是所谓查缺补漏。我们习惯将c语言中的一些基本语法约定放置在该部分。上述例子1“const限定词”就属于该部分。
- 更好的c。这部分侧重总结各种好的编程技巧。上述例子2“配置常量选项”和例子3“限定边界判断”就属于该部分。
- 嵌入式领域。嵌入式编程有很多特殊性,该部分侧重于汇集嵌入式领域需要避免的各种坑,或者各种程序技巧。上述例子4“化除为乘”就属于该部分。
- misra-c规范。在嵌入式行业中最值得参考的编程规范就是misra-c规范了,我们会逐条分析哪些适合我们,哪些不适合。上述例子5就属于该部分。
- bug汇集。在真实产品中,经常会碰到各种诡异的问题,我们不妨整理出来,以醒后人。
- 设计层面。程序编写过程中,经常会冒出各种各样的优秀设计灵感。实践证明,这类源于真实产品的设计技巧,在提升大家设计能力方面,远超各种经典书籍。
- 杂项。留给无处安置的灵魂一个未知。
一份长长的checklist文档,是随着日常工作一点一滴积累起来的。时间长了,甚至会长成大家内心的一盏灯塔,用于照亮大家的成长之路。磨合的时间长了,团队之间代码审核机制会更加顺畅。
嵌入式领域产品种类繁多,不同的应用领域方向,不同的团队特征,只能靠大家去构建属于自己的checklist,但只要坚持下去,相信有一天,你会发现自己已经走出去很远。
——————————————
我是小马儿,一个渴望良知与灵魂的嵌入式软件工程师,欢迎您的陪伴与同行,如感兴趣可加个人微信号nzn_xiaomaer交流,需备注“异维”二字。