跳转至

3.1 代码编写规范

前言

对于一个成功的开源项目来说,我个人认为 代码规范在某种程度上要比代码本身更为重要。至于为什么会得到这样的结论,主要归结于当初我自己还在舞蹈机器人基地的时候就曾遇到过因代码不规范所导致的一系列问题:那时我们晓萌软件组每人负责一个机器人模块的代码编写工作,由于比赛日期逐渐迫近,我们都在尽全力完成各自的任务而没太注意代码的编写规范,可是到后期要做各模块代码集成的时候我们却发现由于彼此的代码规范不统一,导致我们需要更多的时间来理解对方所编代码的含义,并且重新修改很多软件接口以实现数据的无障碍传输。

总之,以往的经历让我明白严谨的代码规范可以显著提高代码的质量、降低后期的开发和维护成本、提升团队开发的效率并减少错误的发生。因此,我们又有什么理由不去学习并制订自己团队的代码编写规范呢?

Info

本代码规范参考自《STM32嵌入式系统开发实战指南》中的【第四章 编程规范】。

代码规范

简要介绍基于C语言的嵌入式编程规范的排版、注释、标识符命名、变量使用、代码可测性、程序效率、质量保证、代码编译、测试、程序版本与维护等内容。

ST固件库编程规范

缩写

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
---------------------
缩写         外设单元
---------------------
ADC         模数转换器
BKP         备份寄存器
CAN         控制器局域网模块
DMA         直接内存存取控制器
EXTI        外部中断事件控制器
FLASH       闪存存储器
GPIO        通用输入输出
I2C         内部集成电路
IWDG        独立看门狗
NVIC        嵌套中断向量列表控制器
PWR         电源/功耗控制
RCC         复位与时钟控制器
RTC         实时时钟
SPI         串行外设接口
SysTick     系统嘀嗒定时器
TIM         通用定时器
TIM1        高级控制定时器
USART       通用同步异步接收发射端
WWDG        窗口看门狗

命名规则

  • 系统、源程序文件和头文件命名都以 stm32f10x 作为开头。例如:stm32f10x_conf.h

  • 常量仅被应用于一个文件的,定义于该文件中。被应用于多个文件的,在对应头文件中定义。所有常量都由英文字母大写书写。

  • 寄存器作为常量处理。他们的命名都由英文字母大写书写。在大多数情况下,他们采用的缩写规范与本用户手册一致。

  • 外设函数的命名以该外设的缩写加下划线为开头。每个单词的第一个字母都由英文字母大写书写,例如:SPI_SendData。在函数名中,只允许存在一个下划线,用以分隔外设缩写和函数名的其他部分。而用以配置外设功能的函数,其名称应总是以字符串 **Config**结尾。例如:GPIO_PinRemapConfig。

    • PPP_Init: PPP_Init函数的功能是根据PPP_InitTypeDef中指定的参数初始化外设PPP。

    • PPP_DeInit: PPP_DeInit函数的功能为复位外设PPP的所有寄存器至缺省值。

    • PPP_StructInit: PPP_StructInit函数的功能是通过设置PPP_InitTypeDef结构中的各种参数来定义外设的功能。

    • PPP_Cmd: PPP_Cmd函数的功能为使能或者失能外设PPP。

    • PPP_ITConfig: PPP_ITConfig函数的功能为使能或者失能来自外设PPP某中断源。

    • PPP_DMAConfig: PPP_DMAConfig函数的功能为使能或者失能外设PPP的DMA接口。

    • PPP_GetFlagStatus: PPP_GetFlagStatus函数的功能为检查外设PPP某标志位被设置与否。

    • PPP_ClearFlag: PPP_ClearFlag函数的功能为清除外设PPP标志位。

    • PPP_GetITStatus: PPP_GetITStatus函数的功能为判断来自外设PPP的中断发生与否。

    • PPP_ClearITPendingBit: PPP_ClearITPendingBit函数的功能为清除外设PPP中断待处理标志位。

编码规则

变量

固件函数库定义了24个变量类型,它们的类型和大小是固定的。在文件stm32f10x_type.h中我们定义了这些变量:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
typedef signed long s32;
typedef signed short s16;
typedef signed char s8;
typedef signed long const sc32;
typedef signed short const sc16;
typedef signed char const sc8;
typedef volatile signed long vs32;
typedef volatile signed short vs16;
typedef volatile signed char vs8;
typedef volatile signed long const vsc32;
typedef volatile signed short const vsc16;
typedef volatile signed char const vsc8;
typedef unsigned long u32;
typedef unsigned short u16;
typedef unsigned char u8;
typedef unsigned long const uc32;
typedef unsigned short const uc16;
typedef unsigned char const uc8;
typedef volatile unsigned long vu32;
typedef volatile unsigned short vu16;
typedef volatile unsigned char vu8;
typedef volatile unsigned long const vuc32;
typedef volatile unsigned short const vuc16;
typedef volatile unsigned char const vuc8;

布尔类型

在文件stm32f10x_type.h中,布尔形变量被定义如下:

1
2
3
4
5
typedef enum
{
    FALSE = 0,
    TRUE  = !FALSE
} bool;

标志位状态类型

在文件stm32f10x_type.h中,我们定义标志位类型的2个可能值为 设置重置

1
2
3
4
5
typedef enum
{
    RESET = 0,
    SET   = !RESET
} FlagStatus;

功能状态类型

在文件stm32f10x_type.h中,我们定义功能状态类型的2个可能值为 使能失能

1
2
3
4
5
typedef enum
{
    DISABLE = 0,
    ENABLE  = !DISABLE
} FunctionalState;

错误类型

在文件stm32f10x_type.h中,我们错误状态类型类型的2个可能值为 成功错误

1
2
3
4
5
typedef enum
{
    ERROR   = 0,
    SUCCESS = !ERROR
} ErrorStatus;

外设

用户可以通过指向各个外设的指针访问各外设的控制寄存器。这些指针所指向的数据结构与各个外设的控制寄存器布局一一对应。外设控制寄存器结构文件stm32f10x_map.h包含了所有外设控制寄存器的结构,下例为SPI寄存器结构的声明,其中RESERVEDi(i为一个整数索引值)表示被保留区域:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
typedef struct
{
    vu16 CR1;
    u16 RESERVED0;
    vu16 CR2;
    u16 RESERVED1;
    vu16 SR;
    u16 RESERVED2;
    vu16 DR;
    u16 RESERVED3;
    vu16 CRCPR;
    u16 RESERVED4;
    vu16 RXCRCR;
    u16 RESERVED5;
    vu16 TXCRCR;
    u16 RESERVED6;
} SPI_TypeDef;

文件stm32f10x_map.h包含了所有外设的声明,下例为SPI外设的声明:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#ifndef EXT
#define EXT extern
#endif
...
#define PERIPH_BASE ((u32)0x40000000)
#define APB1PERIPH_BASE PERIPH_BASE
#define APB2PERIPH_BASE (PERIPH_BASE + 0x10000) ...

// SPI2 Base Address definition.
#define SPI2_BASE (APB1PERIPH_BASE + 0x3800) ...
// SPI2 peripheral declaration.
#ifndef DEBUG
...
#ifdef _SPI2
#define SPI2 ((SPI_TypeDef *) SPI2_BASE)
#endif // _SPI2
...
#else // DEBUG
...
#ifdef _SPI2
EXT SPI_TypeDef *SPI2;
#endif // _SPI2
...
#endif // DEBUG

如果用户希望使用外设SPI,那么必须在文件stm32f10x_conf.h中定义_SPI标签。通过定义标签_SPIn,用户可以访问外设SPIn的寄存器。例如,用户必须在文件stm32f10x_conf.h中定义标签_SPI2,否则是不能访问SPI2的寄存器的。在文件stm32f10x_conf.h中,用户可以按照下例定义标签_SPI和_SPIn:

1
2
3
#define _SPI
#define _SPI1
#define _SPI2

每个外设都有若干寄存器专门分配给标志位。我们按照相应的结构定义这些寄存器,标志位的命名,同样遵循上面的外设缩写规范,以 PPP_FLAG_ 开始。对于不同的外设,标志位都被定义在相应的文件stm32f10x_ppp.h中。

用户想要进入除错(DEBUG)模式的话,必须在文件stm32f10x_conf.h中定义标签DEBUG,变量DEBUG可以仿照下例进行定义:

1
#define DEBUG 1

初始化DEBUG模式与文件stm32f10x_lib.c的代码如下所示:

1
2
3
4
5
6
7
8
9
#ifdef DEBUG void debug(void)
{
...
#ifdef _SPI2
    SPI2 = (SPI_TypeDef *) SPI2_BASE;
#endif // _SPI2
...
}
#endif // DEBUG

C语言嵌入式编程规范

代码排版

1、程序块要采用缩进风格编写,缩进的空格数为4个。

2、相对独立的程序块之间、变量说明之后必须加空行。

示例:以下例子不符合规范。

1
2
3
4
5
6
if (!flag)
{
    ... // Program code.
}
variable_a = data_buffer[index].a;
variable_b = data_buffer[index].b;

应如下书写:

1
2
3
4
5
6
7
if (!flag)
{
    ... // Program code.
}

variable_a = data_buffer[index].a;
variable_b = data_buffer[index].b;

3、较长的语句(大于80字符)要分成多行书写,长表达式要在低优先级操作符处划分新行,操作符放在旧行之尾,划分出的新行要进行适当的缩进,使排版整齐,语句可读。

示例:

1
2
3
4
5
6
7
8
9
data_package.length = DATA_PACKAGE_HEAD_LENGTH + DATA_PACKAGE_TAIL_LENGTH +
    DATA_LENGTH_PER_BYTE * sizeof(data_byte_sum);

action_task_table[frame_id * ACTION_TASK_CHECK_NUMBER + index].occupied =
    task_table[index].occupied;

report_or_not_flag = ((task_index < ACTION_TASK_MAX_NUMBER) &&
    (judgeTaskIndexIsValid(task_index)) &&
    (action_task_table[task_index].result_data != 0));

4、循环、判断等语句中若有较长的表达式或语句,则要进行适应的划分,长表达式要在低优先级操作符处划分新行,操作符放在旧行之尾。

示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
if ((variable_a <= DATA_BUFFER_MAX_NUMBER) &&
    (variable_b != getFunctionValue(parameter_a)))
{
    ... // Program code.
}

for (i = 0, j = 0; (i < data_buffer[index].length) &&
    (j < data_buffer[index].length); i++, j++)
{
    ... // Program code.
}

for (i = 0, j = 0;
    (i < first_word_length) && (j < second_word_length);
    i++, j++)
{
    ... // Program code.
}

5、若函数或过程中的参数较长,则要进行适当的划分。

示例:

1
2
3
void testFunction((BYTE *)parameter_a + sizeof(parameter_c),
                  (BYTE *)parameter_b + sizeof(parameter_c),
                  data_buffer[index]);

6、不允许把多个短语句写在一行中,即一行只写一条语句。

示例:以下例子不符合规范。

1
rect.length = 0; rect.width = 0;

应如下书写:

1
2
rect.length = 0;
rect.width  = 0;

7、iffordowhilecaseswitchdefault等语句自占一行,且 iffordowhile等语句的执行语句部分无论多少都要加括号{}

示例:以下例子不符合规范。

1
if (a == NULL) return;

应如下书写:

1
2
3
4
if (a == NULL)
{
    return;
}

8、对齐只能使用空格键,不能使用TAB键。

说明:以免用不同的编辑器阅读程序时,因TAB键所设置的空格数目不同而造成程序布局不整齐。

9、函数或过程的开始、结构的定义及循环、判断等语句中的代码都要采用缩进风格,case 语句下的情况处理语句也要遵从语句缩进要求。

10、程序块的分界符(如C/C++语言的大括号{})应各独占一行并且位于同一列,同时与引用它们的语句左对齐。在函数体的开始、类的定义、结构的定义、枚举的定义以及iffordowhileswitchcase语句中的程序都要采用如上的缩进方式。

示例:以下例子不符合规范。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
for (...) {
    ... // Program code.
}

if (...)
 {
   ... // Program code.
 }

void testFunction(void)
 {
   ... // Program code.
 }

应如下书写:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
for (...)
{
    ... // Program code.
}

if (...)
{
    ... // Program code.
}

void testFunction(void)
{
    ... // Program code.
}

11、在两个以上的关键字、变量、常量进行对等操作时,它们之间的操作符之前、之后或者前后要加空格。进行非对等操作时,如果是关系密切的立即操作符(如->),后不应加空格。

说明:采用这种松散方式编写代码的目的是使代码更加清晰。由于留空格所产生的清晰性是相对的,所以在已经非常清晰的语句中没有必要再留空格,如果语句已足够清晰,则括号内侧(即左括号后面和右括号前面)不需要加空格,多重括号间不必加空格,因为在C/C++语言中括号已经是最清晰的标志了。

在长语句中,如果需要加的空格非常多,那么应该保持整体清晰,而在局部不加空格。给操作符留空格时不要连续留两个以上空格。

示例:

(1)逗号、分号只在后面加空格。

1
int a, b, c;

(2)比较操作符,赋值操作符(=+=),算术操作符(+%),逻辑操作符(&&&),位域操作符(<<^)等双目操作符的前后加空格。

1
2
3
4
5
if (a >= b && b != 0)
{
    c = a + b;
    d = c ^ 2;
}

(3)!~++--&(地址运算符)等单目操作符前后不加空格。

1
2
3
4
*p = 1;
a = !b;
p = &a;
i++;

(4)->.前后不加空格。

1
p->id = 1;

(5)ifforwhileswitch等与后面的括号间应加空格,使if等关键字更为突出和明显。

1
if (a >= b && c > d)

12、一行程序以小于80字符为宜,不要写得过长。

代码注释

1、一般情况下,源程序应尽量简洁明了。

说明:注释的原则是有助于对程序的阅读理解,注释应准确、易懂、简洁,此外为与各种编译环境的兼容,注释语言 必须 采用英文注释(为了便于大家理解,本规范中的一部分示例源码采用中文注释)。

2、说明性文件(如.h文件、.icf文件、.txt文件等)头部应进行注释,注释必须列出:版权说明、版本号、生成日期、作者、内容、功能、与其他文件的关系、修改日志等。头文件的注释中还应有函数功能简要说明。

示例:下面这段头文件的头注释比较标准。当然,并不局限于此格式,但上述信息建议要包含在内。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/****************************************************************************
Copyright (C), 2017-2018, Team MicroDynamics <microdynamics@126.com>

// 文件名。
File name:

// 作者、版本及完成日期。
Author:    Version:    Date:

// 用于详细说明此程序文件完成的主要功能,与其他模块或函数的接口,输出值、取值范围、
// 含义及参数间的控制、顺序、独立或依赖等关系。
Description:

// 其他内容的说明。
Others:

// 主要函数列表,每条记录应包括函数名及功能简要说明。
Function List:
1. ....

// 修改历史记录列表,每条修改记录应包括修改日期、修改者及修改内容简述。
History:
1. Date:
   Author:
   Modification:
2. ...
****************************************************************************/

3、源文件头部应进行注释,列出:版权说明、版本号、生成日期、作者、模块目的/功能、主要函数及其功能、修改日志等。

示例:下面这段源文件的头注释比较标准,当然,并不局限于此格式,但上述信息建议要包含在内。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/****************************************************************************
Copyright (C), 2017-2018, Team MicroDynamics <microdynamics@126.com>

// 文件名。
FileName:

// 作者、版本及完成日期。
Author:    Version:    Date:

// 模块描述
Description:

// 版本信息
Version:

// 主要函数及其功能
Function List:
1. ...

// 历史修改记录
History:
<author>    <time>    <version>    <desc>
...
****************************************************************************/

说明:Description 一项用于描述本文件的内容、功能、内部各部分之间的关系及本文件与其他文件关系等。History 是修改历史记录列表,每条修改记录应包括修改日期、修改者及修改内容简述。

以Team MicroDynamics目前使用的源码头注释为例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
/*****************************************************************************
THIS PROGRAM IS FREE SOFTWARE. YOU CAN REDISTRIBUTE IT AND/OR MODIFY IT
UNDER THE TERMS OF THE GNU GPLV3 AS PUBLISHED BY THE FREE SOFTWARE FOUNDATION.

Copyright (C), 2017-2018, Team MicroDynamics <microdynamics@126.com>

Filename:    stm32f10x_driver_sys.c
Author:      maksyuki
Version:     0.1.0.20161231_release
Create date: 2016.8.3
Description: Define the bitband operation
Others:      none
Function List:
             none
History:
1. <author>    <date>         <desc>
   maksyuki  2016.11.29     modify the module
*****************************************************************************/

4、函数头部应进行注释,列出:函数的目的/功能、输入参数、输出参数、返回值、调用关系(函数、表)等。

示例:下面这段函数的注释比较标准,当然,并不局限于此格式,但上述信息建议要包含在内。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
/****************************************************************************
Function:       // 函数名称。
Description:    // 函数功能、性能等的描述。
Calls:          // 被本函数调用的函数清单。
Called By:      // 调用本函数的函数清单。
Table Accessed: // 被访问的表(此项仅对于牵扯到数据库操作的程序)。
Table Updated:  // 被修改的表(此项仅对于牵扯到数据库操作的程序)。
Input:          // 输入参数说明,包括每个参数的作。
                // 用、取值说明及参数间关系。
Output:         // 对输出参数的说明。
Return:         // 函数返回值的说明。
Others:         // 其他说明。
****************************************************************************/

5、注释应与编写代码同步,修改代码同时修改相应的注释,以保证注释与代码的一致性。

6、注释的内容要清楚、明了,含义准确,避免产生歧义。

7、避免在注释中使用缩写,特别是非常用缩写,命名的标识符除外。

说明:在使用缩写时或之前,应对缩写进行必要的说明。

8、注释应与其描述的代码相近,对代码的注释应放在其上方或右方(对单条语句的注释)相邻位置,不可放在下面,如放于上方则需与其上面的代码用空行隔开。

示例:以下例子不符合规范。

1
2
3
// Get replicate sub system index and net indicator.
repssn_ind = ssn_data[index].repssn_index;
repssn_ni  = ssn_data[index].ni;
1
2
3
repssn_ind = ssn_data[index].repssn_index;
repssn_ni  = ssn_data[index].ni;
// Get replicate sub system index and net indicator.

应如下书写:

1
2
3
// Get replicate sub system index and net indicator.
repssn_ind = ssn_data[index].repssn_index;
repssn_ni  = ssn_data[index].ni;

9、对于所有具有物理含义的变量、常量,如果其命名不是充分自注释的,在声明时都必须加以注释,说明其物理含义。变量、常量、宏的注释应放在其上方相邻位置或右方。

示例:

1
2
// Active statistic task number.
#define MAX_ACT_TASK_NUMBER 1000

10、数据结构声明(包括数组、结构、枚举等),如果其命名不是充分自注释的,必须加以注释。对数据结构的注释应放在其上方相邻位置,不可放在下面。对结构中的每个域的注释放在此域的右方。

示例:可按如下形式说明枚举/数据/联合结构。

1
2
3
4
5
6
7
8
// USART Ring Data buffer struct.
typedef struct USART_RingBuffer
{
    u8  *buffer;   // Data buffer pointer.
    u16  mask;     // Data mask.
    vu16 index_rd; // Data read index.
    vu16 index_wt; // Data write index.
} USART_RingBuffer;

11、全局变量要有较详细的注释,包括对其功能、取值范围、哪些函数或过程存取它以及存取时注意事项等的说明。

示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
/****************************************************************************
// 全局变量功能。
Variable function:

// 全局变量可能的取值即说明。
Avalable value:

// 函数调用关系。
Call relationship:
...
****************************************************************************/

12、注释与所描述内容进行同样的缩排。

示例:以下例子不符合规范,排版不整齐,阅读稍感不方便。

1
2
3
4
5
6
7
8
void testFunction(void)
{
  // Code one comments.
    program code one;

      // Code two comments.
     program code two;
}

应改为如下布局:

1
2
3
4
5
6
7
8
void testFunction(void)
{
    // Code one comments.
    program code one;

    // Code two comments.
    program code two;
}

13、将注释与其上面的代码用空行隔开。

示例:以下例子不符合规范,代码显得过于紧凑。

1
2
3
4
// Code one comments.
program code one;
// Code two comments.
program code two;

应如下书写:

1
2
3
4
5
// Code one comments.
program code one;

// Code two comments.
program code two;

14、对变量的定义和分支语句(条件分支、循环语句等)必须编写注释。

说明:这些语句往往是程序实现某一特定功能的关键,对于维护人员来说,良好的注释帮助更好的理解程序,有时甚至优于看设计文档。

15、对于switch语句下的case语句,如果因为特殊情况需要处理完一个case后进入下一个case处理,必须在该case语句处理完、下一个case语句前加上明确的注释。

说明:这样有助于防止无故遗漏break语句情况的出现。

示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
case CMD_UP:
{
    ProcessUp();
    break;      
}
case CMD_DOWN:
{
    ProcessDown();
    if (...)
    {
        ...
        break;          
    }
    else
    {
        // Now jump into case CMD_A.
        ProcessUp();
    }
}
case CMD_A:
{
    ProcessA();
    break;
}
case CMD_B:
{
    ProcessB();
    break;
}
case CMD_C:
{
    ProcessC();
    break;
}
case CMD_D:
{
    ProcessD();
    break;
}
...

16、避免在一行代码或表达式的中间插入注释。

说明:除非必要,不应在代码或表达中间插入注释,否则容易使代码可理解性变差。

17、通过对函数或过程、变量、结构等正确的命名以及合理地组织代码的结构,使代码成为自注释的。

说明:清晰准确的函数、变量等的命名,可增加代码可读性,并减少不必要的注释。

18、在代码的功能、意图层次上进行注释,提供有用、额外的信息。

说明:注释的目的是解释代码的目的、功能和采用的方法,提供代码以外的信息,帮助读者理解代码,防止没必要的重复注释信息。

示例:如下注释意义不大。

1
2
// If receive_flag is TRUE.
if (receive_flag)

应如下书写:

1
2
// If receive a package from serial.
if (receive_flag)

19、在程序块的结束行右方加注释标记,以表明某程序块的结束。

说明:当代码段较长,特别是多重嵌套时,这样做可以使代码更清晰,更便于阅读。

示例:参见以下例子。

1
2
3
4
5
6
7
8
9
if (...)
{
    while (index < MAX_INDEX)
    {
        // Program code.
        ...
    } // End of while (index < MAX_INDEX).

} // End of if (...).

20、注释格式尽量统一,除文件头注释外应全部使用行注释(//...)。

21、注释应考虑程序易读及外观排版的因素,使用的语言必须为英文。

标识符命名

1、标识符的命名要清晰、明了,有明确含义,同时使用完整的单词或大家基本可以理解的缩写,避免使人产生误解。

说明:较短的单词可通过去掉 元音 形成缩写,而较长的单词可取单词的头几个字母形成缩写。

示例:以下单词的缩写能够被大家基本认可。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
---------------------------------------------------------------------
单词         缩写       单词             缩写  单词           缩写
---------------------------------------------------------------------
addition     add        float           flt   previous      pre或prev
answer       ans        frequency       freq  payload type  pt
array        arr        header          hdr   pointer       ptr
average      avg        index           idx   return code   rc
buffer       buf或buff  image           img   record        rcd
capture      cap或capt  increment       inc   receive       recv
check        chk        initalize       init  result        res
count        cnt        iteration       itr   return        ret
column       col        length          len   source        src
control      ctrl       memory          mem   stack         stk
decode       dec        middle          mid   statistic     stat
define       def        make            mk    string        str
delete       del        message         msg   subtraction   sub
destination  dst或dest  multiplication  mul   table         tab
display      disp       number          num   temporary     tmp或temp
division     div        operand         opnd  total         tot
encode       enc        optimization    opt   time stamp    ts
environment  env        operator        optr  value         val
error        err        packet          pkt
flag         flg        positon         pos

2、命名中若使用特殊约定或缩写,则要有注释说明。

说明:应该在源文件的开始之处,对文件中所使用的缩写或约定,特别是特殊的缩写,进行必要的注释说明。

3、自己特有的命名风格,要自始至终保持一致,不可来回变化。

说明:个人的命名风格,在符合所在项目组或产品组的命名规则的前提下,才可使用(即命名规则中没有规定到的地方才可有个人命名风格)。

4、对于变量命名,禁止取单个字符(如i、j、k...),建议除了要有具体含义外,还能表明其变量类型、数据类型等,但i、j、k作局部循环变量是允许的。

说明:变量,尤其是局部变量,如果用单个字符表示,很容易敲错(如i写成j),而编译时又检查不出来,有可能为了这个小小的错误而花费大量的查错时间。

根据变量的类型,需要在变量原有名字之前添加相应的前缀,以下是变量前缀的类型表:

1
2
3
4
5
6
---------------------
变量前缀  解释
---------------------
g        全局变量(Global)
c        常量(Const)
s        静态变量(Static)

5、命名规范必须与所使用的系统风格保持一致,并在同一项目中统一。

6、除非必要,不要用数字或较奇怪的字符来定义标识符。

7、在同一软件开发项目内,应在编写代码前规划好接口部分标识符(变量、结构、函数及常量)的命名方式,以防止编译、链接时产生冲突。

说明:对接口部分的标识符应该有更严格限制,防止冲突。如可规定接口部分的变量与常量之前加上 模块 标识等。

8、用正确的反义词组命名具有互斥意义的变量或相反动作的函数等。

示例:下面是一些在软件中常用的反义词组。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
----------------------
正义词      反义词
----------------------
begin      end
create     destroy
insert     delete
first      last
get        release
increment  decrement
put        get
add        delete
lock       unlock
open       close
min        max
old        new
start      stop
next       previous
source     target
show       hide
send       receive
source     destination
cut        paste
up         down

示例:

1
2
int min_sum;
int max_sum;

9、除了编译开关/头文件等特殊应用,应避免使用以下划线开始和结尾的宏定义。

代码可读性

1、注意运算符的优先级,并用括号明确表达式的操作顺序,避免使用默认优先级。

说明:防止阅读程序时产生误解,防止因默认的优先级与设计思想不符而导致程序出错。

示例:下列语句中的表达式

1
2
3
word = (high << 8) | low
if ((a | b) && (a & c))
if ((a | b) < (c & d))

如果书写为:

1
2
3
high << 8 | low     1
a | b && a & c      2
a | b < c & d       3

由于

1
2
3
high << 8 | low = ( high << 8) | low
a | b && a & c  = (a | b) && (a & c)
a | b < c & d = a | (b < c) & d

所以(1)和(2)不会出错,但语句不易理解,而语句(3)却造成了条件判断的错误。

2、避免使用不易理解的数字,用有意义的标识来替代。涉及物理状态或者含有物理意义的常量,不应直接使用数字,必须用有意义的枚举或宏来代替。

示例:以下程序可读性差。

1
2
3
4
5
if (TaskTable[index].state == 0)
{
    TaskTable[index].state = 1;
    ...  // Program code.
}

应改为如下形式:

1
2
3
4
5
6
7
8
#define TASK_STATE_IDLE 0
#define TASK_STATE_BUSY 1

if (TaskTable[index].state == TASK_STATE_IDLE)
{
    TaskTable[index].state = TASK_STATE_BUSY;
    ...  // Program code.
}

3、源程序中关系较为紧密的代码应尽可能相邻。

说明:便于程序阅读和查找。

示例:以下代码布局不太合理。

1
2
3
rect.length  = 10;
receive_flag = true;
rect.width   = 5;

若按如下形式书写,可能更清晰一些:

1
2
3
rect.length  = 10;
rect.width   = 5;
receive_flag = true;

4、不要使用难懂的技巧性很高的语句,除非很有必要时。

说明:高技巧语句不等于高效率的程序,实际上程序的效率关键在于算法。

示例:以下表达式,考虑不周就可能出问题,也较难理解。

1
2
*stat_poi++ += 1;
*++stat_poi += 1;

应分别改为如下:

1
2
3
4
5
6
7
// 此二语句功能相当于“*stat_poi++ += 1;”。
*stat_poi += 1;
stat_poi++;

// 此二语句功能相当于“*++stat_poi += 1;”。
++stat_poi;
*stat_poi += 1;

代码变量与结构

1、尽可能少地使用公共变量。

说明:公共变量是增大模块间耦合的原因之一,故应减少没必要的公共变量以降低模块间的耦合度。

2、仔细定义并明确公共变量的含义、作用、取值范围及公共变量间的关系。

说明:在对变量声明的同时,应对其含义、作用及取值范围进行注释说明,同时若有必要还应说明与其他变量的关系。

3、明确公共变量与操作此公共变量的函数或过程的关系,如访问、修改及创建等。

说明:明确过程操作变量的关系后,将有利于程序的进一步优化、单元测试、系统联调以及代码维护等。这种关系的说明可在注释或文档中描述。

示例:在源文件中,可按如下注释形式说明。

1
2
3
4
5
------------------------------------------------------------------------------
RELATION      initRectValues inputRectValues printRectValues calculateRectArea
------------------------------------------------------------------------------
g_rect_length Create         Modify          Access          Access       
g_rect_width  Create         Modify          Access          Access/Modify

注:RELATION为操作关系。initRectValues()inputRectValues()printRectValues()calculateRectArea()为四个不同的函数。g_rect_lengthg_rect_width为两个全局变量。Create 表示创建,Modify 表示修改,Access 表示访问。其中,函数inputRectValues()printRectValues()都可修改变量g_rect_width,故此变量将引起函数间较大的耦合,并可能增加代码测试、维护的难度。

4、当向公共变量传递数据时,要十分小心,防止赋予不合理的值或越界等现象发生。

说明:对公共变量赋值时,若有必要应进行合法性检查,以提高代码的可靠性、稳定性。

5、防止局部变量与公共变量同名。

说明:若使用了较好的命名规则,那么此问题可自动消除。

6、严禁使用未经初始化的变量作为右值。

说明:特别是在C/C++中引用未经赋值的指针,经常会引起系统崩溃。

7、构造仅有一个模块或函数可以修改、创建,而其余有关模块或函数只访问的公共变量,防止多个不同模块或函数都可以修改、创建同一公共变量的现象。

说明:降低公共变量耦合度。

8、使用严格形式定义的、可移植的数据类型,尽量不要使用与具体硬件或软件环境关系密切的变量。

说明:使用标准的数据类型,有利于程序的移植。

9、结构的功能要单一,是针对一种事务的抽象。

说明:设计结构时应力争使结构代表一种现实事务的抽象,而不是同时代表多种。结构中的各元素应代表同一事务的不同侧面,而不应把描述没有关系或关系很弱的不同事务的元素放到同一结构中。

示例:以下结构不太清晰、合理。

1
2
3
4
5
6
7
8
typedef struct StudentInformation
{
    unsigned char student_name[8]; // Student's name.
    unsigned char student_age;     // Student's age.
    unsigned char student_sex;     // Student's sex.
    unsigned char teacher_name[8]; // Student teacher's name.
    unisgned char teacher_sex;     // Student teacher's sex.
} StudentInformation;

若改为如下,可能更合理些:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
typedef struct TeacherInfomation
{
    unsigned char teacher_name[8]; // Student teacher's name.
    unisgned char teacher_sex;     // Student teacher's sex.
} TeacherInfomation;

typedef struct StudentInformation
{
    unsigned char student_name[8]; // Student's name.
    unsigned char student_age;     // Student's age.
    unsigned char student_sex;     // Student's sex.
    unsigned int  teacher_index;   // Student's teacher index.
} StudentInformation;

10、不要设计面面俱到、非常灵活的数据结构。

说明:面面俱到、灵活的数据结构反而容易引起误解和操作困难。

11、不同结构间的关系不要过于复杂。

说明:若两个结构间关系较复杂、密切,那么应合为一个结构。

示例:以下两个结构体的构造不合理。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
typedef struct PersonInformationA
{
    unsigned char sex;
    unsigned char name[8];
    unsigned char city[15];
    unsigned char addr[40];
} PersonInformationA;

typedef struct PersonInformationB
{
    unsigned char age;
    unsigned char tel;
    unsigned char name[8];
} PersonInformationB;

由于两个结构都是描述同一事物的,那么不如合成一个结构:

1
2
3
4
5
6
7
8
9
typedef struct PersonInformation
{
    unsigned char age;
    unsigned char sex;
    unsigned char tel;
    unsigned char name[8];
    unsigned char city[15];
    unsigned char addr[40];
} PersonInformation;

12、结构体中元素的个数应适中。若结构体中元素个数过多可考虑依据某种原则把元素组成不同的子结构体,以减少原结构体中元素的个数。

说明:增加结构体的可理解性、可操作性和可维护性。

示例:可将上面的PersonInformation进行如下的划分。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
typedef struct PersonInformationBase
{
    unsigned char age;
    unsigned char sex;
    unsigned char name[8];
} PersonInformationBase;

typedef struct PersonInformationAddress
{
    unsigned char tel;
    unsigned char addr[40];
    unsigned char city[15];
} PersonInformationAddress;

typedef struct PersonInformation
{
    PersonInformationBase    person_base;
    PersonInformationAddress person_addr;
} PersonInformation;

13、仔细设计结构体中元素的布局与排列顺序,使结构体容易理解、节省占用空间,并减少误用情况的发生。

说明:合理排列结构体中元素顺序,可节省空间并提高可读性。

示例:以下结构体中的位域排列会占用不少空间,且可读性也稍差。

1
2
3
4
5
6
typedef struct ExampleStruct
{
    unsigned int valid: 1;
    PersonInformation person;
    unsigned int flag:  1;
} ExampleStruct;

若改成如下形式,不仅可节省1字节空间,可读性也变好了。

1
2
3
4
5
6
typedef struct ExampleStruct
{
    unsigned int valid: 1;
    unsigned int flag:  1;
    PersonInformation person;
} ExampleStruct;

14、结构体的设计要尽量考虑向前兼容和以后的版本升级,并为某些未来可能的应用保留余地(如预留一些空间等)。

说明:软件向前兼容的特性,是软件产品是否成功的重要标志之一。如果要想使产品具有较好的前向兼容,那么在产品设计之初就应为以后版本升级保留一定余地,并且在产品升级时必须考虑前一版本的各种特性。

15、留心具体语言及编译器处理不同数据类型的原则及有关细节。

说明:如在C语言中,static局部变量将在内存的“数据区”中生成,而非static局部变量将在“堆栈”中生成。这些细节对程序质量的保证非常重要。

16、编程时,要注意数据类型的强制转换。

说明:当进行数据类型强制转换时,其数据的意义、转换后的取值等都有可能发生变化,而这些细节若考虑不周,就很有可能留下隐患。

17、对编译系统默认的数据类型转换,也要有充分的认识。

示例:以下赋值多数编译器不产生告警,但值的含义还是稍有变化。

1
2
3
4
5
char           variable_a;
unsigned short variable_b;

variable_a = -1;
variable_b = variable_a; // 编译器不产生告警,此时variable_b为0xFFFF。

18、尽量减少没有必要的数据类型默认转换与强制转换。

19、合理地设计数据并使用自定义数据类型,避免数据间进行不必要的类型转换。

20、对自定义数据类型进行恰当命名,使它成为自描述性的,以提高代码可读性。注意其命名方式在同一产品中的统一。

说明:使用自定义类型,可以弥补编程语言提供类型少、信息量不足的缺点,并能使程序清晰、简洁。

示例:可参考如下方式声明自定义数据类型。下面的声明可使数据类型的使用简洁、明了。

1
2
3
typedef unsigned char  BYTE;
typedef unsigned short WORD;
typedef unsigned int   DWORD;

21、当声明用于分布式环境或不同CPU间通信环境的数据结构时,必须考虑机器的字节顺序、使用的位域及字节对齐等问题。

说明:比如Intel CPU与68360 CPU在处理位域及整数时,其在内存存放的**顺序**正好相反。

代码函数与过程

1、对所调用函数的错误返回码要仔细、全面地处理。

2、明确函数功能,精确(而不是近似)地实现函数设计。

3、编写可重入函数时,应注意局部变量的使用(如编写C/C++语言的可重入函数时,应使用 auto即缺省态局部变量或寄存器变量)。

说明:编写C/C++语言的可重入函数时,不应使用static局部变量,否则必须经过特殊处理,才能使函数具有可重入性。

4、编写可重入函数时,若使用全局变量,则应通过关中断、信号量(即 P、V 操作)等手段对其加以保护。

说明:若对所使用的全局变量不加以保护,则此函数就不具有可重入性,即当多个进程调用此函数时,很有可能使有关全局变量变为不可知状态。

示例:假设g_variable是int型全局变量,函数testSemaphore()返回g_variable平方值。那么如下函数不具有可重入性。

1
2
3
4
5
6
7
8
9
unsigned int testSemaphore(int variable)
{
    unsigned int temp;

    g_variable = variable;         // (1)
    temp = calculateSqureValue();  // (2)

    return temp;
}

此函数若被多个进程调用的话,其结果可能是未知的,因为当语句(1)刚执行完后,另外一个使用本函数的进程可能正好被激活,那么当新激活的进程执行到此函数时,将使g_variable赋与另一个不同的variable值,所以当控制重新回到语句(2)后,计算出的temp很可能不是预想中的结果。此函数应如下改进:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
unsigned int testSemaphore(int variable)
{
    unsigned int temp;

    [申请信号量操作]

    g_variable = variable;
    temp = testSemaphore();

    [释放信号量操作]

    return temp;
}

若申请不到“信号量”,说明另外的进程正处于给g_variable赋值并计算其平方过程中(即正在使用此信号),本进程必须等待其释放信号后,才可继续执行。若申请到信号,则可继续执行,但其他进程必须等待本进程释放信号量后,才能再用本信号。

5、在同一项目组中应明确规定对接口函数参数的合法性检查应由函数的调用者负责还是由接口函数本身负责,缺省是由函数调用者负责。

说明:对于模块间接口函数参数的合法性检查这一问题,往往有两个极端现象,即:要么是调用者和被调用者对参数均不作合法性检查,结果就遗漏了合法性检查这一必要的处理过程,造成问题隐患。要么就是调用者和被调用者均对参数进行合法性检查,这种情况虽不会造成问题,但产生了冗余代码,降低了效率。

6、防止将函数的参数作为工作变量。

说明:将函数的参数作为工作变量,有可能错误地改变参数内容,所以很危险。对必须改变的参数,最好先用局部变量代之,最后再将该局部变量的内容赋给该参数。

示例:以下函数的实现不太好。

1
2
3
4
5
6
7
8
9
void calculateDataSum(int num, int *data, int *sum)
{
    *sum = 0;

    for (int i = 0; i < num; i++)
    {
        *sum += data[i]; // sum成了工作变量,不太好。
    }
}

若改为如下,则更好些:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
void calculateDataSum(int num, int *data, int *sum)
{
    int sum_temp = 0;

    for (int i = 0; i < num; i++)
    {
        sum_temp += data[i];
    }

    *sum = sum_temp;
}

7、函数的规模尽量限制在200行以内。

说明:不包括注释和空格行。

8、一个函数仅完成一件功能。

9、为简单功能编写函数。

说明:虽然为仅用一两行就可完成的功能去编函数好象没有必要,但用函数可使功能明确化,增加程序可读性,亦可方便维护、测试。

示例:以下语句的功能就不是很明显。

1
value = (a > b) ? a : b;

改为如下就很清晰了:

1
2
3
4
5
6
int getMaxValue(int a, int b)
{
    return ((a > b) ? a : b);
}

value = getMaxValue(a, b);

或改为如下的形式:

1
2
3
#define GET_MAX_VALUE(a, b) (((a) > (b)) ? (a) : (b))

value = GET_MAX_VALUE(a, b);

10、不要设计多用途、面面俱到的函数。

说明:多功能集于一身的函数,很可能使函数的理解、测试、维护等变得困难。

11、函数的功能应该是可以预测的,也就是只要输入数据相同就应产生同样的输出。

说明:带有内部“存储器”的函数功能可能是不可预测的,因为它的输出可能取决于内部存储器(如某标记)的状态。这样的函数既不易于理解又不利于测试和维护。在C/C++语言中,函数的static局部变量是函数的内部存储器,有可能使函数的功能不可预测,然而,当某函数的返回值为指针类型时,则必须将static局部变量的地址作为返回值,若为auto类,则返回为错误指针。

示例:以下函数,其返回值(即功能)是不可预测的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
unsigned int integer_sum(unsigned int a)
{
     // sum是static类型的,若改为auto类型,则函数即变为可预测。
    static unsigned int sum = 0;

    for (unsigned int i = 1; i <= a; i++)
    {
        sum += i;
    }

    return sum;
}

12、尽量不要编写依赖于其他函数内部实现的函数。

说明:此条为函数独立性的基本要求。由于目前大部分高级语言都是结构化的,所以通过具体语言的语法要求与编译器功能,基本就可以防止这种情况发生。但在汇编语言中,由于其灵活性,很可能使函数出现这种情况。

13、避免设计多参数函数,不使用的参数从接口中去掉。

说明:目的是减少函数间接口的复杂度。

14、非调度函数应减少或防止控制参数,尽量只使用数据参数。

说明:本建议的目的是防止函数间的控制耦合。调度函数是指根据输入的消息类型或控制命令,来启动相应的功能实体(即函数或过程),而本身并不完成具体功能。控制参数是指改变函数功能行为的参数,即函数要根据此参数来决定具体怎样工作。非调度函数的控制参数增加了函数间的控制耦合,很可能使函数间的耦合度增大,并使函数的功能不唯一。

示例:以下函数的构造不太合理。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
int addOrSubDataValue(int a, int b, unsigned char flag)
{
    if (flag == INTEGER_ADD)
    {
        return (a + b);
    }
    else if (flag == INTEGER_SUB)
    {
        return (a  b);
    }
    else
    {
        return;
    }
}

不如分为如下两个函数清晰:

1
2
3
4
5
6
7
8
9
int addDataValue(int a, int b)
{
    return (a + b);
}

int subDataValue(int a, int b)
{
    return (a  b);
}

15、检查函数所有参数输入的有效性。

16、检查函数所有非参数输入的有效性,如数据文件、公共变量等。

说明:函数的输入主要有两种:一种是参数输入;另一种是全局变量、数据文件的输入,即非参数输入。函数在使用输入之前,应进行必要的检查。

17、函数名应准确描述函数的功能。

18、使用动宾词组为执行某操作的函数命名。如果是OOP方法,可以只有动词(名词是对象本身)。

示例:参照以下方式命名函数。

1
2
3
void printRecordFile(unsigned int file_name);
void inputRecoreFile(void);
unsigned char getCurrentData(void);

19、避免使用无意义或含义不清的动词为函数命名。

说明:避免用含义不清的动词如**process**、**handle**等为函数命名,因为这些动词并没有说明要具体做什么。

20、函数的返回值要清楚、明了,让使用者不容易忽视错误情况。

说明:函数的每种出错返回值的意义要清晰、明了、准确,防止使用者误用、理解错误或忽视错误返回码。

21、除非必要,最好不要把与函数返回值类型不同的变量,以编译系统默认的转换方式或强制的转换方式作为返回值返回。

22、让函数在调用点显得易懂、容易理解。

23、在调用函数填写参数时,应尽量减少没有必要的默认数据类型转换或强制数据类型转换。

说明:因为数据类型转换或多或少存在危险。

24、避免函数中不必要语句,防止程序中的垃圾代码。

说明:程序中的垃圾代码不仅占用额外的空间,而且还常常影响程序的功能与性能,很可能给程序的测试、维护等造成不必要的麻烦。

25、防止把没有关联的语句放到一个函数中。

说明:防止函数或过程内出现随机内聚。随机内聚是指将没有关联或关联很弱的语句放到同一个函数或过程中。随机内聚给函数或过程的维护、测试及以后的升级等造成了不便,同时也使函数或过程的功能不明确。使用随机内聚函数,常常容易出现在一种应用场合需要改进此函数,而另一种应用场合又不允许这种改进,从而陷入困境。

在编程时,经常遇到在不同函数中使用相同的代码,许多开发人员都愿把这些代码提出来,并构成一个新函数。若这些代码关联较大并且是完成一个功能的,那么这种构造是合理的,否则这种构造将产生随机内聚函数。

示例:以下函数就是一种随机内聚。

1
2
3
4
5
6
7
8
void initVariableValues(void)
{
    rect.length = 0;
    rect.width  = 0;

    point.x = 10;
    point.y = 10;
}

矩形的长、宽与点的坐标基本没有任何关系,故以上函数是随机内聚。应如下分为两个函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
void initRectValues(void)
{
    rect.length = 0;
    rect.width  = 0;
}

void initPointValues(void)
{
    point.x = 10;
    point.y = 10;
}

26、如果多段代码重复做同一件事情,那么在函数的划分上可能存在问题。

说明:若此段代码各语句之间有实质性关联并且是完成同一件功能的,那么可考虑把此段代码构造成一个新的函数。

27、功能不明确、较小的函数,特别是仅有一个上级函数调用它时,应考虑把它合并到上级函数中,而不必单独存在。

说明:模块中函数划分的过多,一般会使函数间的接口变得复杂。所以功能过小的函数,特别是扇入很低的或功能不明确的函数,不值得单独存在。

28、设计高扇入、合理扇出(小于7)的函数。

说明:扇出是指一个函数直接调用(控制)其他函数的数目,而扇入是指有多少上级函数调用它。

扇出过大,表明函数过分复杂,需要控制和协调过多的下级函数。而扇出过小,如总是1,表明函数的调用层次可能过多,这样不利程序阅读和函数结构的分析,并且程序运行时会对系统资源如堆栈空间等造成压力。函数较合理的扇出(调度函数除外)通常是 3~5。扇出太大,一般是由于缺乏中间层次,可适当增加中间层次的函数。扇出太小,可把下级函数进一步分解成多个函数,或合并到上级函数中。当然分解或合并函数时,不能改变要实现的功能,也不能违背函数间的独立性。

扇入越大,表明使用此函数的上级函数越多,这样的函数使用效率高,但不能违背函数间的独立性而单纯地追求高扇入。公共模块中的函数及底层函数应该有较高的扇入。

较好的软件结构通常是顶层函数的扇出较高,中层函数的扇出较少,而底层函数则扇入到公共模块中。

29、减少函数本身或函数间的递归调用。

说明:递归调用特别是函数间的递归调用(如 A->B->C->A),影响程序的可理解性:递归调用一般都占用较多的系统资源(如栈空间);递归调用对程序的测试有一定影响。因此,除非为某些算法或功能的实现方便,否则应减少没必要的递归调用。

30、仔细分析模块的功能及性能需求,并进一步细分,同时若有必要画出有关数据流图,据此来进行模块的函数划分与组织。

说明:函数的划分与组织是模块实现过程中很关键的一步,如何划分出合理的函数结构,关系到模块的最终效率和可维护性、可测性等。根据模块的功能图和数据流图映射出函数结构是常用方法之一。

31、改进模块中函数的结构,降低函数间的耦合度,并提高函数的独立性以及代码可读性、效率和可维护性。优化函数结构时,要遵守以下原则:

  • 不能影响模块功能的实现。
  • 仔细考查模块或函数出错处理及模块的性能要求并进行完善。
  • 通过分解或合并函数来改进软件结构。
  • 考查函数的规模,过大的要进行分解。
  • 降低函数间接口的复杂度。
  • 不同层次的函数调用要有较合理的扇入、扇出。
  • 函数功能应可预测。
  • 提高函数内聚(单一功能的函数内聚最高)。

说明:对初步划分后的函数结构应进行改进、优化,使之更为合理。

32、在多任务操作系统的环境下编程,要注意函数可重入性的构造。

说明:可重入性是指函数可以被多个任务进程调用。在多任务操作系统中,函数是否具有可重入性是非常重要的,因为这是多个进程可以共用此函数的必要条件。另外,编译器是否提供可重入函数库,与它所服务的操作系统有关,只有操作系统是多任务时,编译器才有可能提供可重入函数库。

33、对于提供了返回值的函数,在引用时最好使用其返回值。

34、当一个函数(过程)中对较长变量(一般是结构体成员)有较多引用时,可以用一个意义相当的宏代替。

说明:这样可以增加编程效率和程序的可读性。

示例:在某过程中较多引用receive_buffer[socket].data_ptr,则可以通过以下宏定义来代替:

1
# define PTR_SOCKET_DATA receive_buffer[socket].data_ptr

代码可测性

1、在同一项目组内,要有一套统一的为集成测试与系统联调准备的调测开关及相应输出函数,并且要有详细的说明。

2、在同一项目组内,调测输出的信息串的格式要有统一的形式。信息串中至少要有所在模块名(或源文件名)及行号。

说明:统一的调测信息格式便于集成测试。

3、编程的同时要为单元测试选择恰当的测试点,并仔细构造测试代码、测试用例,同时给出明确的注释说明。测试代码部分应作为(模块中的)一个子模块,以方便测试代码在模块中的安装与拆卸(通过调测开关)。

说明:为单元测试而准备。

4、在进行集成测试/系统联调之前,要构造好测试环境、测试项目及测试用例,同时仔细分析并优化测试用例,以提高测试效率。

说明:好的测试用例应尽可能模拟出程序所遇到的边界值、各种复杂环境及一些极端情况等。

5、使用断言来发现软件问题,提高代码可测性。

说明:断言是对某种假设条件进行检查(可理解为若条件成立则无动作,否则应报告),它可以快速发现并定位软件问题,同时对系统错误进行自动报警。断言可以对在系统中隐藏很深,用其他手段极难发现的问题进行定位,从而缩短软件问题定位时间,提高系统的可测性。在实际应用时,可根据具体情况灵活地设计断言。

6、用断言来检查程序正常运行时不应发生但在调测时有可能发生的非法情况。

7、不能用断言来检查最终软件肯定会出现且必须处理的错误情况。

说明:断言是用来处理不应该发生的错误情况的,对于可能会发生的且必须处理的情况要写防错程序,而不是断言。如某模块收到其他模块或链路上的消息后,要对消息的合理性进行检查,此过程为正常的错误检查,不能用断言来实现。

8、对较复杂的断言加上明确的注释。

说明:为复杂的断言加注释,可澄清断言含义并减少不必要的误用。

9、用断言确认函数的参数。

示例:假设某函数参数中有一个指针,那么使用指针前可对它检查。

1
2
3
4
5
6
void testFunction(unsigned char *str)
{
    TEST_ASSERT(str != NULL);

    ... // Program code.
}

10、用断言保证没有定义的特性或功能不被使用。

11、用断言对程序开发环境的假设进行检查。

说明:针对程序运行时所需的软硬件环境及配置要求,不能用断言来检查,而必须有一段专门代码处理。断言仅用于对程序开发环境中的假设及所配置的某版本软硬件是否具有某种功能的假设进行检查。如某网卡是否在系统运行环境中配置了,应由程序中的正式代码来检查;而此网卡是否具有某设想的功能,则可由断言来检查。

对编译器提供的功能及特性假设可用断言检查,原因是最终的运行代码或机器码与编译器已没有任何直接关系,及软件运行过程中不会也不应该对编译器的功能提出任何需求。

示例:用断言检查编译器的int型数据占用的内存空间是否为2。

1
TEST_ASSERT(sizeof(int) == 2);

12、正式软件中应把断言及其他调测代码删除。

说明:加快软件运行速度。

13、在软件系统中设置与取消有关测试手段,不能对软件实现的功能等产生影响。

说明:即有测试代码的软件和关闭测试代码的软件在功能行为上应一致。

14、用调测开关来切换软件的Debug版和Release版,而不要同时存在Debug和Release版本的不同源文件,以减少维护的难度。

15、软件的Debug和Release版本应该统一进行维护,并时刻保证两个版本在功能实现上的一致性。

16、在编写代码之前,应预先设计好程序调试与测试的方法和手段,并设计好各种调测开关机相应测试代码如输出函数等。

说明:程序的调试与测试是软件生存周期中很重要的一个阶段,如何对软件进行较全面、高效率的测试,并尽可能地找出软件中的错误就成为一个很关键的问题。因此在编写源代码之前,除了要有一套比较完善的测试计划外,还应设计出一系列代码测试手段,为单元测试、集成测试及系统联调提供方便。

17、调测开关应分为不同级别和类型。

说明:调测开关的设置及分类应从以下几方面考虑:针对模块或系统某部分代码的调测;针对模块或系统某功能的调测;出于某种其他目的,如对性能、容量等的测试。这样做便于软件功能的调测,并利于模块的单元测试和系统联调等。

18、编写防错程序,然后在处理错误之后用断言宣布错误的发生。

代码执行效率

1、编程时要经常注意代码的效率。

说明:代码效率分为全局效率、局部效率、时间效率和空间效率。全局效率是站在整个系统角度上的效率;局部效率是站在模块或函数角度上的效率;时间效率是指程序处理输入任务所需的时间长短;空间效率是指程序所需内存空间,如机器代码空间大小、数据空间大小、栈空间大小等。

2、在保证软件系统的正确性、稳定性、可读性和可测性的前提下,提高代码效率。

说明:不能因一味地追求代码效率而对软件的正确性、稳定性、可读性和可测性造成影响。

3、局部效率应为全局效率服务,不能应为提高局部效率而对全局效率造成影响。

4、通过对系统数据结构的划分与组织的改进,以及对程序算法的优化来提高空间效率。

说明:这种方式是解决软件空间效率的根本办法。

示例:以下记录学生学习成绩的结构体不合理。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
typedef unsigned char  BYTE;
typedef unsigned short WORD;

typedef struct StudentScoreInformation
{
    BYTE  name[8];
    BYTE  age;
    BYTE  sex;
    BYTE  class;
    BYTE  subject;
    float score;
} StudentScoreInformation;

因为每个学生都会有很多科目的成绩,如果使用以上的结构将占用较大的空间。应按照以下的方式进行改进(分为两个结构体),总的存储空间将变小,操作也变得更方便。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
typedef struct StudentInformation
{
    BYTE name[8];
    BYTE age;
    BYTE sex;
    BYTE class;
} StudentInformation;

typedef struct StudentScoreInformation
{
    WORD  student_index;
    BYTE  subject;
    flaot score;
} StudentScoreInformation;

5、循环体内工作量最小化。

说明:应仔细考虑循环体内的语句是否可以放在循环体之外,使循环体内工作量最小,从而提高程序的执行效率。

示例:以下代码效率较低。

1
2
3
4
5
for (int i = 0; i < MAX_NUMBER; i++)
{
    sum += i;
    sum_backup = sum;
}

语句sum_backup = sum;完全可以放到for语句之后,如下所示:

1
2
3
4
5
6
for (int i = 0; i < MAX_NUMBER; i++)
{
    sum += i;
}

sum_backup = sum;

6、仔细分析有关算法、并进行优化。

7、仔细考查、分析系统及模块处理输入(如事务、消息等)的方式,并加以改进。

8、对模块中函数的划分及组织方式进行分析、优化,改进模块中函数的组织结构,提高程序执行效率。

说明:软件系统的效率主要与算法、处理任务方式、系统功能及函数结构有很大关系,仅在代码上下功夫一般不能解决根本问题。

9、编程时,要随时留意代码效率;优化代码时,要考虑周全。

10、不应花过多的时间提高调用频率不高的函数的代码效率。

说明:对代码优化可提高效率,但若考虑不周很有可能引起严重后果。

11、要仔细地构造或直接用汇编编写调用频繁或性能要求极高的函数。

说明:只有对编译系统产生机器码的方式以及硬件系统较为熟悉时,才可使用汇编嵌入方式。嵌入汇编可提高时间及空间效率,但也存在一定风险。

12、在保证程序质量的前提下,通过压缩代码量、去掉不必要代码以及减少不必要的局部和全局变量来提高空间效率。

说明:这种方式对提高空间效率可起到一定作用,但往往不能解决根本问题。

13、减少CPU切入循环层的次数。

示例:以下代码效率较低。

1
2
3
4
5
6
7
for (int i = 0; i < 100; i++)
{
    for (int j = 0; j < 5; j++)
    {
        sum += a[i][j];
    }
}

可以改为如下方式,以提高效率:

1
2
3
4
5
6
7
for (int j = 0; j < 5; j++)
{
    for (int i = 0; i < 100; i++)
    {
        sum += a[i][j];
    }
}

14、尽量减少循环嵌套层次。

15、避免在循环体内包含判断语句,应将循环语句至于判断语句的代码块中。

说明:目的是减少判断次数。循环体中的判断语句是否可以移到循环体外,要视程序的具体情况而言,一般情况,与循环变量无关的判断语句可以移到循环体外,而有关的则不可以。

示例:以下代码的效率稍低。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
for (int i = 0; i < MAX_NUMBER; i++)
{
    if (data_type == RECT_AREA)
    {
        rect_sum_area += rect[i].area;
    }
    else
    {
        rect_sum_length += rect[i].length;
        rect_sum_width  += rect[i].width;
    }
}

因为判断语句与循环变量无关,可进行以下改进,以减少判断次数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
if (data_type == RECT_AREA)
{
    for (int i = 0; i < MAX_NUMBER; i++)
    {
        rect_sum_area += rect[i].area;
    }
}
else
{
    for (int i = 0; i < MAX_NUMBER; i++)
    {
        rect_sum_length += rect[i].length;
        rect_sum_width  += rect[i].width;
    }
}

16、尽量用乘法或其他方法代替除法,特别是浮点运算中的除法。

说明:浮点运算除法要占用较多CPU资源。

示例:以下表达式可能占用较多CPU资源。

1
2
3
#define PI 3.1415926

radius = circle_length / (2 * PI);

应把浮点除法改为浮点乘法,如下所示:

1
2
3
#define PI_RECIPROCAL (1 / 3.1415926)

radius = circle_length * PI_RECIPROCAL / 2;

17、不要一味追求紧凑的代码。

说明:因为紧凑的代码并不代表高效的机器码。

代码质量

1、在软件设计过程中构建软件质量。

2、代码质量保证优先原则。

  • 正确性,之程序要实现设计要求的功能。
  • 稳定性、安全性,指程序稳定、可靠、安全。
  • 可测试性,指程序要具有良好的可测试性。
  • 规范/可读性,指程序书写风格、命名规则等要符合规范。
  • 全局效率,指软件系统的整体效率。
  • 局部效率,指某个模块、子模块、函数的本身效率。
  • 个人表达方式,指个人编程习惯。

3、只引用属于自己的存储空间。

4、防止引用已经释放的内存空间。

5、在过程/函数中分配的内存,要在过程/函数退出之前释放。

6、过程/函数中申请的文件句柄,要在过程/函数退出之前关闭。

说明:分配的内存不释放以及文件句柄不关闭是比较常见的错误,而且稍不注意就有可能发生。这类错误往往会引起很严重的后果,且难以定位。

7、防止内存操作越界。

说明:内存操作主要是指对数组、指针、内存地址等的操作。内存操作越界是软件系统主要错误之一,后果往往非常严重,所以当进行这些操作时一定要仔细小心。

8、认真处理程序所能遇到的各种出错情况。

9、系统运行之初,要初始化有关变量及运行环境,防止引用未经初始化的变量。

10、系统运行之初,要对加载到系统中的数据进行一致性检查。

说明:使用不一致的数据,容易式系统进入混乱和不可知状态。

11、严禁随意修改其他模块或系统的有关设置。

说明:编程时,不要随意修改不属于自己模块的有关设置,如常量、数组的大小等。

12、不能随意改变与其他模块的接口。

13、充分了解系统的接口之后,在使用系统提供的功能。

14、编程时,要防止“差1”错误。

说明:此类错误一般是由于把<=误写成<>=误写成>等造成的,由此引起的后果,很多情况下是很严重的,所以编程时一定要在这些地缝小心。当编写完程序后,应第一这些操作符进行彻底检查。

15、要时刻主要易混淆的操作符。当编完程序后,应从头至尾检查一遍这些操作符,以防止拼写错误。

说明:形式相近的操作符最容易引起误用,如C/C++中的===|||&&&等,若拼写错误,编译器不一定能够检查的出来。

示例:

1
if (variable_a == 10)

被写成:

1
if (variable_a = 10)

16、尽可能为所有的if语句加上else分支,而对于没有else分支的语句要小心对待。switch语句则必须要有default分支。

17、在类Unix系统中,多线程中的子线程退出必须采用主动退出方式,即子线程应return出口。

18、不要滥用goto语句。

说明:goto语句会破坏程序的结构性,所以除非用于让程序统一跳转到异常处理代码块进行异常处理之外,不允许使用goto语句。

19、不使用与硬件或操作系统关系很大的语法。建议使用标准的语法,以提高代码在不同操作系统之间的可移植性和可重用性。

20、除非为了满足特殊需求,否则避免使用嵌入式汇编。

说明:若在程序中嵌入汇编语句,一般都会对代码的可移植性造成较大的影响。

21、精心地构造、划分子模块,并按“接口”部分以及“内核”部分合理地组织子模块,以提高“内核”部分的可移植性和可重用性。

说明:对于不同软件中的某个功能相同的模块,若能做到其内核部分完全或基本一致,那么无论对软件的测试、维护,还是以后的升级都会有很大帮助。

22、精心构造算法,并对其性能、效率进行测试。

23、对较为关键的算法,最好使用其他算法来进行验证。

24、时刻注意表达式是否存在上溢和下溢问题。

示例:以下代码将造成变量的下溢。

1
2
3
4
5
6
unsigned char size;

while (size-- >= 0)
{
    ... // Program code.
}

size等于0的时候,再减1不会小于0,而是0XFF,故程序是一个死循环。应做如下的修改:

1
2
3
4
5
6
char size;

while (size-- >= 0)
{
    ... // Program code.
}

25、使用变量时要注意其边界值的情况。

示例:在C语言中,字符型变量的有效值范围为 -128~127 。因此以下表达式的计算存在一定的风险。

1
2
3
4
5
6
7
char a = 127;
int  b = 200;

// 加1使得变量a的值上溢到-128,而不是128。
a += 1;
// 所以b的结果是72,而不是328。
b += a;

26、留心程序机器码大小(如指令空间大小、数据空间大小、堆栈空间大小等)是否超出系统有关的限制。

27、为用户提供良好的接口界面,使用户能较充分地了解系统内部运行状态及有关系统出错情况。

28、系统应具备一定的容错能力,能对一些错误事件(如用户误操作等)进行自动补救。

29、对一些具有危险性的操作代码(如写硬盘、删数据等)要仔细考虑,防止对数据、硬件等安全构成危害,以提高系统的安全性。

30、使用第三方提供的软件开发工具包或控件时,要注意以下几点。

  • 充分了解应用接口、使用环境及使用时的注意事项。
  • 不能过分信赖其正确性。
  • 除非有必要,否则不要使用不熟悉的第三方工具包或控件。

说明:使用工具包与控件,可加快程序开发速度,节省时间,但是用之前一定要对它有较为充分的了解,同时第三方工具包与控件也有可能存在Bug等问题。

31、如果资源文件是对语言敏感的,应让该资源文件与源文件脱离,具体方法包括:使用单独的资源文件、DLL文件或其他单独的描述文件(如数据库格式)等。

代码编辑、编译与审查

1、打开编译器的所有警告开关对程序进行编译。

2、统一软件代码中的编译开关选项。

3、通过代码走读及审查方式对代码进行检查。

说明:代码走读主要是对程序的编程风格如注释、命名以及容易出错的内容进行检查,可由开发人员自己或其他开发人员进行。代码审查主要是对程序实现的功能及程序的稳定性、安全性、可靠性等进行检查。

4、编写的代码要注意及时保存,并定期备份,防止由于硬盘损坏等不可控因素造成代码丢失。

5、同一项目组内,应尽量使用相同的代码编辑器或IDE,而其相关的配置选项也要尽可能保持一致。

说明:同一项目组应采用相同的代码编辑器或IDE,如Atom、Sublime Text、Qt等,并设计使用一套缩进宏和注释宏等,将代码缩进和注释等问题交给编辑器来处理。

6、要小心使用编辑器提供的块拷贝功能编程。

说明:当某段代码与另一段代码的处理功能相似时,可用块拷贝功能完成这段代码的编写工作。由于程序功能相近,故所使用的变量、采用的表达式等在功能及命名上可能很接近,所以使用块拷贝时要注意:除了修改相应的程序外,一定要把使用的每个变量仔细查看一遍,以改成正确的。

7、合理地设计软件系统目录,方便开发人员使用。

说明:方便、合理的软件系统目录可提高工作效率。目录构造的原则是方便有关源程序的存储、查询、编译、链接等工作。

8、使用代码检查工具对源程序进行检查。

代码测试与维护

1、单元测试要求至少达到语句覆盖。

2、单元测试开始要跟踪每一条语句,并观察数据流及变量的变化。

3、清理、整理或优化后的代码要经过审查及测试。

4、代码版本升级要经过严格的测试。

5、使用Git对代码版本进行维护。

6、有关正式版本软件的任何修改都应有详细的文档记录。

7、发现错误要立即进行修复,并将其记录到开发日志当中去。

8、仔细设计并分析测试用例,使测试用例覆盖尽可能多的情况,以提高效率。

9、尽可能模拟出程序的各种出错情况,并充分测试出错处理代码。

10、仔细测试代码处理数据、变量的边界情况。

11、保留测试信息,以便分析、总结经验和进行更充分的测试。

12、对自动消失的错误进行分析,搞清楚错误是如何发生和消失的。

13、测试时应设法使很少发生的事件经常发生。

14、明确模块或函数处理哪些事件,并使它们经常发生。

15、坚持在编码阶段就对代码进行彻底的单元测试,不要等以后在测试工作中发现问题。

16、去除代码运行的随机性,让函数运行的结果可预测,并使出现的错误可再现。

代码宏定义

1、用宏定义表达式时,要是用完整的括号。

示例:以下定义的宏都存在一定的风险。

1
2
3
#define RECTANGLE_AREA(a, b) a * b
#define RECTANGLE_AREA(a, b) (a * b)
#define RECTANGLE_AREA(a, b) (a) * (b)

正确的定义应为:

1
#define RECTANGLE_AREA(a, b) ((a) * (b))

2、将宏定义的多条表达式放在大括号中。

3、使用宏定义时,不允许参数发生变化。

示例:以下用法可能会导致错误。

1
2
3
4
5
6
7
#define SQUARE(value) ((value) * (value))

int a = 5;
int b = 0;

// a的值为7,即执行了两次自加操作。
b = SQUARE(a++);

正确的用法是:

1
2
3
4
5
6
7
8
#define SQUARE(value) ((value) * (value))

int a = 5;
int b = 0;

b = SQUARE(a);
// a的值为6,即执行了一次自加操作。
a++;

工程规范

建立工程

配置工程

总结

通过以上的介绍,我相信大家都对我们MicroDynamics团队制订的STM32嵌入式代码编写规范有了一个较为清楚的认识。古人曾经说过:不以规矩,不能成方圆,很难想象如果我们不用严格的代码规范来约束自己,我们编写出来的代码质量能有多高呢?因此,我希望每个编写嵌入式代码的同学都能够在开始之前先沉下心把代码规范仔细地学习一遍,就算你以后不再搞嵌入式,拥有编写规范代码的能力也会成为你将来最为宝贵的一份财富!