aithinker_dev_sdk 中间件 | 安信可嵌入式代码规范:
文档版本
修订内容
编制
部门
V1.2.2.0
拟定安信可嵌入式软件代码规范,首版初稿
陈子锋
产品技术中心软件部
V1.2.2.1
更新安信可嵌入式软件代码规范,正式发布
陈子锋
产品技术中心软件部
V1.2.2.2
增加使用 typedef 关键字定义枚举类型的规则和示例,增加全局变量和静态变量命名规范,共计 113条规约
陈子锋
产品技术中心软件部
原则:编程时必须坚持的指导思想 | 规则:编程时强制必须遵守的约定 | 建议:编程时必须加以考虑的约定
原则1.1 头文件中适合放置接口的声明,不适合放置实现
-内部使用的函数(相当于类的私有方法)声明不应放在头文件中
-内部使用的宏、枚举、结构定义不应放入头文件中
-变量定义不应放在头文件中,应放在.c文件中
变量的声明尽量不要放在头文件中,亦即尽量不要使用全局变量作为接口。变量是模块或单元的内部实现细节,不应通过在头文件中声明的方式直接暴露给外部,应通过函数接口的方式进行对外暴露。即使必须使用全局变量,也只应当在.c中定义全局变量,在.h中仅声明变量为全局的
规则1.1 每一个.c文件应有一个同名.h文件,用于声明需要对外公开的接口
规则1.2 .c/.h文件禁止包含用不到的头文件,头文件过多的包含关系会导致编译时间的上升
规则1.3 头文件总是编写内部#include保护符(#define 保护,源文件不需要)
C 语言头文件为了避免多次重复包含,需要定义一个符号。这个符号的定义形式请采用如下的风格:
#ifndef __FILE_H__
#define __FILE_H__
#endif
头文件的版权声明部分以及头文件的整体注释部分(如阐述此头文件的开发背景、使用注意事项等)放在保护符(#ifndef __XX_H__)前面
规则1.4 禁止在头文件中定义变量
在头文件中定义变量,将会由于头文件被其他.c文件包含而导致变量重复定义(经验不足的工程师需要特别关注)
规则1.5 只能通过包含头文件的方式使用其他.c提供的接口,禁止在.c中通过extern的方式使用外部函数接口、变量
若a.c使用了b.c定义的foo()函数,则应当在b.h中声明extern int foo(int input);并在a.c中通过#include 来使用foo,禁止通过在a.c中直接写extern int foo(int input);来使用foo,后面这种写法容易在foo改变时可能导致声明和定义不一致
那么,头文件中声明是否需要加extern修饰:
对于函数来说,默认为extern,不需要加添加。但是,对于变量来说,需要加extern才能保证访问的是同一个全局变量,需要增加extern修饰
规则1.6 禁止在extern "C"中包含头文件
在extern "C"中包含头文件,会导致extern "C"嵌套,Visual Studio对extern "C"嵌套层次有限制,嵌套层次太多会编译错误
在extern "C"中包含头文件,可能会导致被包含头文件的原有意图遭到破坏
错误示例:
#ifndef A_H__
#define A_H__
#ifdef __cplusplus
void foo(int);
#define a(value) foo(value)
#else
void a(int)
#endif
#endif /* A_H__ */
#ifndef B_H__
#define B_H__
#ifdef __cplusplus
extern "C" {
#endif
#include "a.h"
void b();
#ifdef __cplusplus
}
#endif
#endif /* B_H__ */
使用C++预处理器展开b.h,将会得到
extern "C" {
void foo(int);
void b();
}
按照a.h作者的本意,函数foo是一个C++自由函数,其链接规范为"C++"
但在b.h中,由于#include "a.h"被放到了extern "C" { }的内部,函数foo的链接规范被不正确地更改了
示例:
错误的使用方式:
extern "C"
{
#include "xxx.h"
...
}
正确的使用方式:
#include "xxx.h"
extern "C"
{
...
}
规则1.7 在每个源文件文件头上,应该包括相应的版权信息,Change Log 记录:
/** @brief RTOS application interface.
*
* @file aiio_os_port.h
* @copyright Copyright (C) 2020-2023, Shenzhen Anxinke Technology Co., Ltd
* @note RTOS application interface.
* @par Change Logs:
* <table>
* <tr><th>Date <th>Version <th>Author <th>Notes
* <tr><td>2023/01/30 <td>1.0.0 <td>chenzf <td>Define file
* <tr><td>2023/02/01 <td>1.0.1 <td>chenzf <td>Define file
* </table>
*
*/
关于Author:
标志更改人,更改人名称规范:统一使用公司分配的邮箱前缀,例如邮箱[email protected] ,标志名称为chenzf
关于Copyright (C) 2020-2023:其中2020指文件的创建年份,2023指文件的最后修改年份
建议1.1 头文件排列方式,应根据功能块进行排序排序
/* RTOS */
#include ""
#include ""
/* WIFI */
#include ""
#include ""
这样进行头文件包含会比较有逻辑性,同类功能的头文件尽可能保持紧凑;非同类功能建议用一行空行进行分隔
原则2.1 一个函数仅完成一件功能
如果一个函数实现多个功能给开发、使用、维护都带来很大的困难,将没有关联或者关联很弱的语句放到同一函数中,会导致函数职责不明确,难以理解,难以测试和改动
案例:
realloc。在标准C语言中,realloc是一个典型的不良设计。这个函数基本功能是重新分配内存,但它承担了太多的其他任务:如果传入的指针参数为NULL就分配内存,如果传入的大小参数为0就释放内存,如果可行则就地重新分配,如果不行则移到其他地方分配。如果没有足够可用的内存用来完成重新分配(扩大原来的内存块或者分配新的内存块),则返回NULL,而原来的内存块保持不变。这个函数不易扩展,容易导致问题
例如下面代码容易导致内存泄漏:
char *buffer = (char *)malloc(XXX_SIZE);
...
buffer = (char *)realloc(buffer, NEW_SIZE);
如果没有足够可用的内存用来完成重新分配,函数返回为NULL,导致buffer原来指向的内存被丢失。
原则2.2 重复代码应该尽可能提炼成函数,重复代码提炼成函数可以带来维护成本的降低
重复代码是不良代码最典型的特征之一。在“代码能用就不改”的指导原则之下,大量的烟囱式设计及其实现充斥着各产品代码之中。新需求增加带来的代码拷贝和修改,随着时间的迁移,产品中堆砌着许多类似或者重复的代码
重复代码示例:
UC ccb_aoc_process( )
{
...
struct AOC_E1_E7 aoc_e1_e7;
aoc_e1_e7.aoc = 0;
aoc_e1_e7.e[0] = 0;
... //aoc_e1_e7.e[i]从到赋值,下同
aoc_e1_e7.e[6] = 0;
aoc_e1_e7.tariff_rate = 0;
...
if (xxx)
{
if (xxx)
{
aoc_e1_e7.e[0] = 0;
...
aoc_e1_e7.e[6] = 0;
aoc_e1_e7.tariff_rate = 0;
}
...
}
else if (xxx)
{
if (xxx)
{
aoc_e1_e7.e[0] = 0;
...
aoc_e1_e7.e[6] = 0;
aoc_e1_e7.tariff_rate = 0;
}
ccb_caller_e1 = aoc_e1_e7.e[0];
...
ccb_caller_e7 = aoc_e1_e7.e[6];
ccb_caller_tariff_rate = aoc_e1_e7.tariff_rate;
...
}
...
if (xxx)
{
if (xxx)
{
if (xxx)
{
aoc_e1_e7.e[0] = 0;
...
aoc_e1_e7.e[6] = 0;
aoc_e1_e7.tariff_rate = 0;
}
...
}
else if (xxx)
{
if (xxx)
{
aoc_e1_e7.e[0] = 0;
...
aoc_e1_e7.e[6] = 0;
aoc_e1_e7.tariff_rate = 0;
}
ccb_caller_e1 = aoc_e1_e7.e[0];
...
ccb_caller_e7 = aoc_e1_e7.e[6];
ccb_caller_tariff_rate = aoc_e1_e7.tariff_rate;
...
}
return 1;
}
else
{
return 0;
}
}
这类写法造成的后果是代码篇幅很大,对重复代码的修改、维护工作量都很大。这类代码是不允许提交的,在评审环节需要拦截
规则2.1 避免函数过长,新增函数不超过80行(非空非注释行)
本规则仅对新增函数做要求,对已有函数修改时,建议不增加代码行
过长的函数往往意味着函数功能不单一,过于复杂(参见原则2.1:一个函数只完成一个功能)
函数的有效代码行数,即NBNC(非空非注释行)应当在[1,80]区间
例外:
某些实现算法的函数,由于算法的聚合性与功能的全面性,可能会超过80行,这类特殊情况评审时可根据实际放行
延伸阅读材料:
业界普遍认为一个函数的代码行不要超过一个屏幕,避免来回翻页影响阅读;一般的代码度量工具建议都对此进行检查
例如Logiscope的函数度量:"Number of Statement" (函数中的可执行语句数)建议不超过20行,QA C建议一个函数中的所有行数(包括注释和空白行)不超过50行
规则2.2 避免函数的代码块嵌套过深,新增函数的代码块嵌套不超过4层
本规则仅对新增函数做要求,对已有的代码建议不增加嵌套层次
函数的代码块嵌套深度指的是函数中的代码控制块(例如:if、for、while、switch等)之间互相包含的深度。每级嵌套都会增加阅读代码时的脑力消耗,因为需要在脑子里维护一个“栈”(比如,进入条件语句、进入循环……)。应该做进一步的功能分解,从而避免使代码的阅读者一次记住太多的上下文。优秀代码参考值:[1, 4]
规则2.3 可重入函数应避免使用共享变量;若需要使用,则应通过互斥手段(关中断、信号量)对其加以保护
可重入函数是指可能被多个任务并发调用的函数。在多任务操作系统中,函数具有可重入性是多个任务可以共用此函数的必要条件。共享变量指的全局变量和static变量
编写C语言的可重入函数时,不应使用static局部变量,否则必须经过特殊处理,才能使函数具有可重入性
示例:函数square_exam返回g_exam平方值。那么如下函数不具有可重入性
int g_exam;
unsigned int example(int para)
{
unsigned int temp;
g_exam = para; // (**)
temp = square_exam();
return temp;
}
此函数若被多个线程调用的话,其结果可能是未知的,因为当(**)语句刚执行完后,另外一个使用本函数的线程可能正好被激活,那么当新激活的线程执行到此函数时,将使g_exam赋于另一个不同的para值,所以当控制重新回到“temp =square_exam ( )”后,计算出的temp很可能不是预想中的结果。此函数应如下改进:
int g_exam;
unsigned int example( int para )
{
unsigned int temp;
[申请信号量操作]
g_exam = para;
temp = square_exam( );
[释放信号量操作]
return temp;
}
若申请不到“信号量”,说明另外的进程正处于给g_exam赋值并计算其平方过程中(即正在使用此信号),本进程必须等待其释放信号后,才可继续执行。其它线程必须等待本线程释放信号量后才能再使用本信号
规则2.4 对参数的合法性检查,规定由接口函数负责而不是调用者,缺省由调用者负责
对于模块间接口函数的参数的合法性检查这一问题,往往有两个极端现象,即:要么是调用者和被调用者对参数均不作合法性检查,结果就遗漏了合法性检查这一必要的处理过程,造成问题隐患;要么就是调用者和被调用者均对参数进行合法性检查,这种情况虽不会造成问题,但产生了冗余代码,降低了效率
示例:下面红色部分的代码在每一个函数中都写了一次,导致代码有较多的冗余。如果函数的参数比较多,而且判断的条件比较复杂(比如:一个整形数字需要判断范围等),那么冗余的代码会大面积充斥着业务代码。
void PidMsgProc(MsgBlock *Msg)
{
MsgProcItem *func = NULL;
if (Msg == NULL)
{
return;
}
...
GetMsgProcFun(Msg, &func);
func(Msg);
return;
}
int GetMsgProcFun(MsgBlock *Msg, MsgProcItem **func)
{
if (Msg == NULL)
{
return 1;
}
...
*func = VOS_NULL_PTR;
for (Index = 0; Index < NELEM(g_MsgProcTable); Index++)
{
if ((g_MsgProcTable[Index].FlowType == Msg->FlowType)
&& (g_MsgProcTable[Index].Status == Msg->Status)
&& (g_MsgProcTable[Index].MsgType == Msg->MsgType))
{
*func = &(g_MsgProcTable[Index]);
return 0;
}
}
return 1;
}
int ServiceProcess(int CbNo, MsgBlock *Msg)
{
if ( Msg == NULL)
{
return 1;
}
...
// 业务处理代码
...
return 0;
}
编写接口函数应遵循要求:在不产生冗余代码的前提下做错误检查,至少在可能造成严重后果的地方做错误检查,例如指针NULL、数据溢出等情况判断
规则2.5 对函数的错误返回码要全面处理
一个函数(标准库中的函数/第三方库函数/用户定义的函数)能够提供一些指示错误发生的方法。这可以通过使用错误标记、特殊的返回数据或者其他手段,不管什么时候函数提供了这样的机制,调用程序应该在函数返回时立刻检查错误指示
示例:下面的代码导致宕机
FILE *fp = fopen( "./writeAlarmLastTime.log","r");
if (fp == NULL)
{
return;
}
char buff[128] = "";
fscanf(fp,“%s”, buff); /* 读取最新的告警时间;由于文件writeAlarmLastTime.log为空,导致buff为空 */
fclose(fp);
long fileTime = getAlarmTime(buff); /* 解析获取最新的告警时间;getAlarmTime函数未检查buff指针,导致宕机 */
正确写法:
FILE *fp = fopen( "./writeAlarmLastTime.log","r");
if (fp == NULL)
{
return;
}
char buff[128] = "";
if (fscanf(fp,“%s”,buff) == EOF) //检查函数fscanf的返回值,确保读到数据
{
fclose(fp);
return;
}
fclose(fp);
long fileTime = getAlarmTime(buff); //解析获取最新的告警时间;
规则2.6 设计高扇入,合理扇出(小于7)的函数。
扇出是指一个函数直接调用(控制)其它函数的数目,而扇入是指有多少上级函数调用它
扇出过大,表明函数过分复杂,需要控制和协调过多的下级函数;而扇出过小,例如:总是1,表明函数的调用层次可能过多,这样不利于程序阅读和函数结构的分析,并且程序运行时会对系统资源如堆栈空间等造成压力。通常函数比较合理的扇出(调度函数除外)通常是3~5。
扇出太大,一般是由于缺乏中间层次,可适当增加中间层次的函数。扇出太小,可把下级函数进一步分解多个函数,或合并到上级函数中。当然分解或合并函数时,不能改变要实现的功能,也不能违背函数间的独立性。
扇入越大,表明使用此函数的上级函数越多,这样的函数使用效率高,但不能违背函数间的独立性而单纯地追求高扇入。公共模块中的函数及底层函数应该有较高的扇入
较良好的软件结构通常是顶层函数的扇出较高,中层函数的扇出较少,而底层函数则扇入到公共模块中
延伸阅读材料:
扇入(Fan-in)和扇出(Fan-out)是Henry和Kafura在1981年引入,用来说明模块间的耦合(coupling),后面人们扩展到函数/方法、模块/类、包等。
The Fan-in (Informational fan-in) metric measures the fan-in of a module. The fan-in of a module A is the number of modules that pass control into module A.
The Fan-out metric measures the number of the number of modules that are called by a given module.
规则2.7 废弃代码(没有被调用的函数和变量)要及时清除。
程序中的废弃代码不仅占用额外的空间,而且还常常影响程序的功能与性能,很可能给程序的测试、维护等造成不必要的麻烦
规则2.8 函数命名
adt目录:
函数名称请使用小写英文的形式,单词之间使用 "_" 连接。提供给上层应用使用的 API接口,必须在相应的头文件中声明;如果函数入口参数是空,必须使用 void 作为入口参数,例如:
aithinker_led_t aiio_func(void); //备注:adtpater_chip/chip适配层函数声明,头文件声明时必须加上CHIP_API符号标识
函数命名规则:(aiio)_(组件名称)_(功能用途) //举例:aiio_uart_send(),前缀需要添加 aiio_
application目录:
函数命名规则:(动词)_(名词) //前缀不需要添加 aiio_
建议2.1 函数不变参数使用const
不变的值更易于理解/跟踪和分析,把const作为默认选项,在编译时会对其进行检查,使代码更牢固/更安全
示例:
C99标准 7.21.4.4 中strncmp 的例子,不变参数声明为const
int strncmp(const char *s1, const char *s2, register size_t n)
{
register unsigned char u1, u2;
while (n-- > 0)
{
u1 = (unsigned char) *s1++;
u2 = (unsigned char) *s2++;
if (u1 != u2)
{
return u1 - u2;
}
if (u1 == '\0')
{
return 0;
}
}
return 0;
}
建议2.2 函数应避免使用全局变量、静态局部变量和I/O操作,不可避免的地方应集中使用
说明:带有内部“存储器”的函数的功能可能是不可预测的,因为它的输出可能取决于内部存储器(如某标记)的状态。这样的函数既不易于理解又不利于测试和维护。在C语言中,函数的static局部变量是函数的内部存储器,有可能使函数的功能不可预测,然而,当某函数的返回值为指针类型时,则必须是static的局部变量的地址作为返回值,若为auto类,则返回为错针。
示例:如下函数,其返回值(即功能)是不可预测的。
unsigned int integer_sum( unsigned int base )
{
unsigned int index;
static unsigned int sum = 0; // 注意,是static类型的。
// 若改为auto类型,则函数即变为可预测。
for (index = 1; index <= base; index++)
{
sum += index;
}
return sum;
}
建议2.3 检查函数所有非参数输入的有效性,如数据文件、公共变量等
说明:函数的输入主要有两种:一种是参数输入;另一种是全局变量、数据文件的输入,即非参数输入。函数在使用输入参数之前,应进行有效性检查。
示例:下面的代码导致宕机
hr = root_node->get_first_child(&log_item); // list.xml 为空,导致读出log_item为空
...
hr = log_item->get_next_sibling(&media_next_node); // log_item为空,导致宕机
正确写法:确保读出的内容非空。
hr = root_node->get_first_child(&log_item);
...
if (log_item == NULL) // 确保读出的内容非空
{
return retValue;
}
hr = log_item->get_next_sibling(&media_next_node);
建议2.4 函数的参数个数不超过5个。
函数的参数过多,会使得该函数易于受外部(其他部分的代码)变化的影响,从而影响维护工作。函数的参数过多同时也会增大测试的工作量
函数的参数个数不要超过5个,如果超过了建议拆分为不同函数或合并参数
建议2.5 除打印类函数外,不要使用可变长参函数。
可变长参函数的处理过程比较复杂容易引入错误,而且性能也比较低,使用过多的可变长参函数将导致函数的维护难度大大增加
建议2.6 在源文件范围内声明和定义的所有函数,除非外部可见,否则应该增加static关键字
如果一个函数只是在同一文件中的其他地方调用,那么就用static声明。使用static确保只是在声明它的文件中是可见的,并且避免了和其他文件或库中的相同标识符发生混淆的可能性
原则3.1 标识符的命名要清晰、明了,有明确含义,同时使用完整的单词或大家基本可以理解的缩写,避免使人产生误解
尽可能给出描述性名称,不要节约空间,让别人很快理解你的代码更重要
示例:好的命名:
int error_number;
int number_of_completed_connection;
不好的命名:使用模糊的缩写或随意的字符:
int n;
int nerr;
int n_comp_conns;
标识符的命名规则历来是一个敏感话题,典型的命名风格如unix风格、windows风格等等,从来无法达成共识。实际上,各种风格都有其优势也有其劣势,而且往往和个人的审美观有关。我们对标识符定义主要是为了让团队的代码看起来尽可能统一,有利于代码的后续阅读和修改
原则3.2 除了常见的通用缩写以外,不使用单词缩写,不得使用汉语拼音
较短的单词可通过去掉“元音”形成缩写,较长的单词可取单词的头几个字母形成缩写,一些单词有大家公认的缩写,常用单词的缩写必须统一。协议中的单词的缩写与协议保持一致。对于某个系统使用的专用缩写应该在注视或者某处做统一说明。
示例:
一些常见可以缩写的例子:
argument 可缩写为 arg
buffer 可缩写为 buff
clock 可缩写为 clk
command 可缩写为 cmd
compare 可缩写为 cmp
configuration 可缩写为 cfg
device 可缩写为 dev
error 可缩写为 err
hexadecimal 可缩写为 hex
increment 可缩写为 inc、
initialize 可缩写为 init
maximum 可缩写为 max
message 可缩写为 msg
minimum 可缩写为 min
parameter 可缩写为 para
previous 可缩写为 prev
register 可缩写为 reg
semaphore 可缩写为 sem
statistic 可缩写为 stat
synchronize 可缩写为 sync
temp 可缩写为 tmp
规则3.5 对于数值或者字符串等等常量的定义,建议采用全大写字母,单词之间加下划线‘_’的方式命名(枚举同样建议使用此方式定义)。
示例:
#define PI_ROUNDED 3.14
规则3.6 除了头文件或编译开关等特殊标识定义,宏定义不能使用下划线‘_’开头和结尾
一般来说,’_’开头、结尾的宏都是一些内部的定义,ISO/IEC 9899(俗称C99)中有如下的描述(6.10.8 Predefined macro names):
None of these macro names(这里上面是一些内部定义的宏的描述), nor the identifier defined, shall be the subject of a #define or a #undef preprocessing directive. Any other predefined macro names shall begin with a leading underscore followed by an uppercase letter or a second underscore.
建议3.1 用正确的反义词组命名具有互斥意义的变量或相反动作的函数等。
示例:
add/remove
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
copy/paste
up/down
建议3.2 尽量避免名字中出现数字编号,除非逻辑上的确需要编号
示例:
如下命名,使人产生疑惑
#define EXAMPLE_0_TEST_
#define EXAMPLE_1_TEST_
应改为有意义的单词命名
#define EXAMPLE_UNIT_TEST_
#define EXAMPLE_ASSERT_TEST_
建议3.4 平台/驱动等适配代码的标识符命名风格保持和平台/驱动一致。
涉及到外购芯片以及配套的驱动,这部分的代码变动(包括为产品做适配的新增代码),应该保持原有的风格
建议3.5 重构/修改部分代码时,应保持和原有代码的命名风格一致
根据源代码现有的风格继续编写代码,有利于保持总体一致
建议3.6 文件命名统一采用小写字符,如果是适配层目录文件,前面统一由aiio_开头
因为不同系统对文件名大小写处理会不同(如MS的DOS、Windows系统不区分大小写,但是Linux系统则区分),所以代码文件命名建议统一采用全小写字母命名
原则4.1 一个变量只有一个功能,不能把一个变量用作多种用途
一个变量只用来表示一个特定功能,不能把一个变量作多种用途,即同一变量取值不同时,其代表的意义也不同
示例:
具有两种功能的反例
WORD DelRelTimeQue(void)
{
WORD Locate;
Locate = 3;
Locate = DeleteFromQue(Locate); /* Locate具有两种功能:位置和函数DeleteFromQue的返回值 */
return Locate;
}
正确做法:使用两个变量
WORD DelRelTimeQue(void)
{
WORD Ret;
WORD Locate;
Locate = 3;
Ret = DeleteFromQue(Locate);
return Ret;
}
原则4.2 结构功能单一,不要设计面面俱到的数据结构
相关的一组信息才是构成一个结构体的基础,结构的定义应该可以明确的描述一个对象,而不是一组相关性不强的数据的集合
设计结构时应力争使结构代表一种现实事务的抽象,而不是同时代表多种。结构中的各元素应代表同一事务的不同侧面,而不应把描述没有关系或关系很弱的不同事务的元素放到同一结构中
示例:
如下结构不太清晰、合理。
typedef struct STUDENT_STRU
{
unsigned char name[32]; /* student's name */
unsigned char age; /* student's age */
unsigned char sex; /* student's sex, as follows */
/* 0 - FEMALE; 1 - MALE */
unsigned char teacher_name[32]; /* the student teacher's name */
unsigned char teacher_sex; /* his teacher sex */
} STUDENT;
若改为如下,会更合理些。
typedef struct TEACHER_STRU
{
unsigned char name[32]; /* teacher name */
unsigned char sex; /* teacher sex, as follows */
/* 0 - FEMALE; 1 - MALE */
unsigned int teacher_ind; /* teacher index */
} TEACHER;
typedef struct STUDENT_STRU
{
unsigned char name[32]; /* student's name */
unsigned char age; /* student's age */
unsigned char sex; /* student's sex, as follows */
/* 0 - FEMALE; 1 - MALE */
unsigned int teacher_ind; /* his teacher index */
} STUDENT;
原则4.3 不用或者少用全局变量
但是单个文件内部可以使用static的全局变量,可以将其理解为类的私有成员变量
规则4.1 防止局部变量与全局变量同名
尽管局部变量和全局变量的作用域不同而不会发生语法错误,但容易使人误解
规则4.2 通讯过程中使用的结构,必须注意字节序
通讯报文中,字节序是一个重要的问题,我司设备使用的cpu类型复杂多样,大小端、32位/64位的处理器也都有,如果结构会在报文交互过程中使用,必须考虑字节序问题。
由于位域在不同字节序下,表现看起来差别更大,所以更需要注意。
对于这种跨平台的交互,数据成员发送前,都应该进行主机序到网络序的转换;接收时,也必须进行网络序到主机序的转换
规则4.3 严禁使用未经初始化的变量作为右值
在首次使用前初始化变量,初始化的地方离使用的地方越近越好,可以有效避免未初始化错误
规则4.4 全局变量和静态变量命名规则
adt目录:
全局变量:aiio_(功能)_g,例子:uint8_t wifi_connected_flag_g;
静态变量:aiio_(功能)_s,例子:static uint8_t timer_count_s;
同理,如果定义的变量非全局变量或静态变量,不能以_g和_s后缀结尾,避免造成混淆
建议4.1 构造仅有一个模块或函数可以修改、创建,而其余有关模块或函数只访问的全局变量,防止多个不同模块或函数都可以修改、创建同一全局变量的现象
建议4.2 使用面向接口编程思想,通过API访问数据:如果本模块的数据需要对外部模块开放,应提供接口函数来设置、获取,同时注意全局数据的访问互斥
避免直接暴露内部数据给外部模型使用,是防止模块间耦合最简单有效的方法
建议4.3 明确全局变量的初始化顺序,避免跨模块的初始化依赖
系统启动阶段,使用全局变量前,要考虑到该全局变量在什么时候初始化,使用全局变量和初始化全局变量,两者之间的时序关系,谁先谁后,一定要分析清楚,不然后果往往是低级而又灾难性的
建议4.4 尽量减少没有必要的数据类型默认转换与强制转换
当进行数据类型强制转换时,其数据的意义、转换后的取值等都有可能发生变化,而这些细节若考虑不周,就很有可能留下隐患
示例:如下赋值,多数编译器不产生告警,但值的含义还是稍有变化
char ch;
unsigned short int exam;
ch = -1;
exam = ch; // 编译器不产生告警,此时exam为0xFFFF
规则5.1 用宏定义表达式时,要使用完备的括号
说明:因为宏只是简单的代码替换,不会像函数一样先将参数计算后,再传递
示例:
如下定义的宏都存在一定的风险
#define RECTANGLE_AREA(a, b) a * b
#define RECTANGLE_AREA(a, b) (a * b)
#define RECTANGLE_AREA(a, b) (a) * (b)
正确的定义应为:
#define RECTANGLE_AREA(a, b) ((a) * (b))
如果定义:#define RECTANGLE_AREA(a, b) a * b 或 #define RECTANGLE_AREA(a, b) (a * b)
则c/RECTANGLE_AREA(a, b) 将扩展成c/a * b , c与b本应该是除法运算,结果变成了乘法运算,造成错误
如果定义:#define RECTANGLE_AREA(a, b) (a) * (b)
则RECTANGLE_AREA(c + d, e + f)将扩展成:(c + d * e + f), d与e 先运算,造成错误
规则5.2 将宏所定义的多条表达式放在大括号中
更好的方法是多条语句写成do while(0)的方式
示例:
看下面的语句,只有宏的第一条表达式被执行
#define FOO(x) \
printf("arg is %d\n", x); \
do_something_useful(x);
为了说明问题,下面for语句的书写稍不符规范
for (blah = 1; blah < 10; blah++)
FOO(blah)
用大括号定义的方式可以解决上面的问题:
#define FOO(x) { \
printf("arg is %s\n", x); \
do_something_useful(x); \
}
但是如果有人这样调用:
if (condition == 1)
FOO(10);
else
FOO(20);
那么这个宏还是不能正常使用,所以必须这样定义才能避免各种问题:
#define FOO(x) do { \
printf("arg is %s\n", x); \
do_something_useful(x); \
} while(0)
用do-while(0)方式定义宏,完全不用担心使用者如何使用宏,也不用给使用者加什么约束
规则5.3 使用宏时,不允许参数发生变化
示例:如下用法可能导致错误
#define SQUARE(a) ((a) * (a))
int a = 5;
int b;
b = SQUARE(a++); // 结果:a = 7,即执行了两次增
正确的用法是:
b = SQUARE(a);
a++; // 结果:a = 6,即只执行了一次增。
同时也建议即使函数调用,也不要在参数中做变量变化操作,因为可能引用的接口函数,在某个版本升级后,变成了一个兼容老版本所做的一个宏,结果可能不可预知
规则5.4 不允许直接使用魔鬼数字
使用魔鬼数字的弊端:代码难以理解;如果一个有含义的数字多处使用,一旦需要修改这个数值,代价惨重
使用明确的物理状态或物理意义的名称能增加信息,并能提供单一的维护点
解决途径:
对于局部使用的唯一含义的魔鬼数字,可以在代码周围增加说明注释,也可以定义局部const变量,变量命名自注释
对于广泛使用的数字,必须定义const全局变量/宏;同样变量/宏命名应是自注释的
0作为一个特殊的数字,作为一般默认值使用没有歧义时,不用特别定义
建议5.1 除非必要,应尽可能使用函数代替宏
宏对比函数,有一些明显的缺点:
宏缺乏类型检查,不如函数调用检查严格
宏展开可能会产生意想不到的副作用,如#define SQUARE(a) (a) (a)这样的定义,如果是SQUARE(i++),就会导致i被加两次;如果是函数调用double square(double a) {return a a;}则不会有此副作用
以宏形式写的代码难以调试难以打断点,不利于定位问题
宏如果调用的很多,会造成代码空间的浪费,不如函数空间效率高
示例:下面的代码无法得到想要的结果:
#define MAX_MACRO(a, b) ((a) > (b) ? (a) : (b))
int MAX_FUNC(int a, int b) {
return ((a) > (b) ? (a) : (b));
}
int testFunc()
{
unsigned int a = 1;
int b = -1;
printf("MACRO: max of a and b is: %d\n", MAX_MACRO(++a, b));
printf("FUNC : max of a and b is: %d\n", MAX_FUNC(a, b));
return 0;
}
上面宏代码调用中,结果是(a < b),所以a只加了一次,所以最终的输出结果是:
MACRO: max of a and b is: -1
FUNC : max of a and b is: 2
建议5.2 常量建议使用const定义代替宏
说明: “尽量用编译器而不用预处理”,因为#define经常被认为好象不是语言本身的一部分。看下面的语句:
#define ASPECT_RATIO 1.653
编译器会永远也看不到ASPECT_RATIO这个符号名,因为在源码进入编译器之前,它会被预处理程序去掉,于是ASPECT_RATIO不会加入到符号列表中。如果涉及到这个常量的代码在编译时报错,就会很令人费解,因为报错信息指的是1.653,而不是ASPECT_RATIO。如果ASPECT_RATIO不是在你自己写的头文件中定义的,你就会奇怪1.653是从哪里来的,甚至会花时间跟踪下去。这个问题也会出现在符号调试器中,因为同样地,你所写的符号名不会出现在符号列表中。
解决这个问题的方案很简单:不用预处理宏,定义一个常量:
const double ASPECT_RATIO = 1.653;
这种方法很有效,但有两个特殊情况要注意。首先,定义指针常量时会有点不同。因为常量定义一般是放在头文件中(许多源文件会包含它),除了指针所指的类型要定义成const外,重要的是指针也经常要定义成const。例如,要在头文件中定义一个基于char*的字符串常量,你要写两次const:
const char * const authorName = "Scott Meyers";
建议5.3 宏定义中尽量不使用return、goto、continue、break等改变程序流程的语句。
说明:如果在宏定义中使用这些改变流程的语句,很容易引起资源泄漏问题,使用者很难自己察觉。
示例:在某头文件中定义宏CHECK_AND_RETURN:
#define CHECK_AND_RETURN(cond, ret) {if (cond == NULL_PTR) {return ret;}}
然后在某函数中使用(只说明问题,代码并不完整):
pMem1 = VOS_MemAlloc(...);
CHECK_AND_RETURN(pMem1 , ERR_CODE_XXX)
pMem2 = VOS_MemAlloc(...);
CHECK_AND_RETURN(pMem2 , ERR_CODE_XXX) /*此时如果pMem2==NULL_PTR,则pMem1未释放函数就返回了,造成内存泄漏。*/
所以说,类似于CHECK_AND_RETURN这些宏,虽然能使代码简洁,但是隐患很大,使用须谨慎。
规则6.1 结构体命名规则
结构体名称请使用小写英文名的形式,单词与单词之间采用 "_" 连接,例如:
adt目录:aiio_前缀开头
struct aiio_list_node
{
struct aiio_list_node *next;
struct aiio_list_node *prev;
};
application目录:结构体定义前缀无要求,以功能命名即可
注意:
当结构体仅用名称声明时,它的名称后不能包含_t后缀,下面是错误示例:
struct aiio_list_node_t
{
struct aiio_list_node_t *next;
struct aiio_list_node_t *prev;
};/* 错误示例,结构体仅作声明,后缀不能包含_t,_t是预留给typedef类型定义使用的 */
规则7.1 结构体等的类型定义请以结构体名称加上 "_t" 的形式作为名称,例如:
/* 结构体定义 */
typedef struct aiio_list_node aiio_list_t;
/* 枚举的定义 */
typedef enum aiio_sock
{
AIIO_SOCK_STG_CONNECTED,
AIIO_SOCK_STG_DISCONNECTED,
} aiio_sock_t;
可以看到,只要是通过tpyedef的类型定义,都是以_t结尾;所以,平时编写非typedef类型定义时,尽可能不要使用_t结尾
规则8.1 日志打印不能直接使用printf或者原始SDK的打印函数,应该使用适配层的日志打印接口
原则9.1 代码质量保证优先原则
正确性,指程序要实现设计要求的功能。
简洁性,指程序易于理解并且易于实现。
可维护性,指程序被修改的能力,包括纠错、改进、新需求或功能规格变化的适应能力。
可靠性,指程序在给定时间间隔和环境条件下,按设计要求成功运行程序的概率。
代码可测试性,指软件发现故障并隔离、定位故障的能力,以及在一定的时间和成本前提下,进行测试设计、测试执行的能力。
代码性能高效,指是尽可能少地占用系统资源,包括内存和执行时间。
可移植性,指为了在原来设计的特定环境之外运行,对系统进行修改的能力。
个人表达方式/个人方便性,指个人编程习惯。
原则9.2 要时刻注意易混淆的操作符,包括易混淆和的易用错操作符
1、易混淆的操作符
C语言中有些操作符很容易混淆,编码时要非常小心。
赋值操作符“=” 逻辑操作符“==” 关系操作符“<” 位操作符"<<" 关系操作符“>” 位操作符“>>” 逻辑操作符“||” 位操作符"|" 逻辑操作符“&&” 位操作符"&" 逻辑操作符"!" 位操作符“~”
2、易用错的操作符
(1) 除操作符"/"
当除操作符“/”的运算量是整型量时,运算结果也是整型。
如:1/2=0
(2)求余操作符"%"
求余操作符"%"的运算量只能是整型。
如:5%2=1,而5.0%2是错误的。
(3)自加、自减操作符“++”、“--”
示例1
k = 5;
x = k++;
执行后,x = 5,k = 6
示例2
k = 5;
x = ++k;
执行后,x = 6,k = 6
示例3
k = 5;
x = k--;
执行后,x = 5,k = 4
示例4
k = 5;
x = --k;
执行后,x = 4,k = 4
原则9.3 必须了解编译系统的内存分配方式,特别是编译系统对不同类型的变量的内存分配规则,如局部变量在何处分配、静态变量在何处分配等
原则9.4 不仅关注接口,同样要关注实现
这个原则看似和“面向接口”编程思想相悖,但是实现往往会影响接口,函数所能实现的功能,除了和调用者传递的参数相关,往往还受制于其他隐含约束,如:物理内存的限制,网络状况,具体看“抽象漏洞原则”
规则9.1 禁止内存操作越界
内存操作主要是指对数组、指针、内存地址等的操作。内存操作越界是软件系统主要错误之一,后果往往非常严重,所以当我们进行这些操作时一定要仔细小心
示例:
使用itoa()将整型数转换为字符串时:
char TempShold[10] ;
itoa(ProcFrecy, TempShold, 10); /* 数据库刷新间隔设为值1073741823时,系统监控后台coredump,监控前台抛异常。*/
TempShold是以‘\0’结尾的字符数组,只能存储9个字符,而ProcFrecy的最大值可达到10位,导致符数组TempShold越界。
正确写法:一个int(32位)在-2147483647~2147483648之间,将数组TempShold设置成12位。
char TempShold[12] ;
itoa(ProcFrecy, TempShold, 10);
坚持下列措施可以避免内存越界:(延伸阅读材料,《公司常见软件编程低级错误:内存越界.ppt》)
数组的大小要考虑最大情况,避免数组分配空间不够。
避免使用危险函数sprintf/vsprintf/strcpy/strcat/gets操作字符串,使用相对安全的函数snprintf/strncpy/strncat/fgets代替。
使用memcpy/memset时一定要确保长度不要越界
字符串考虑最后的'\0',确保所有字符串是以'\0'结束
指针加减操作时,考虑指针类型长度
数组下标进行检查
使用时sizeof或者strlen计算结构/字符串长度,避免手工计算
sizeof与strlen使用示例:
char str[20]="0123456789";
char *p1=str;
int num[20]={0,1,2,3,4,5,6,7,8,9};
int *p2=num;
cout<<"strlen(str):"<<strlen(str)<<endl; //传递给strlen就退化为指针:结果是10
cout<<"strlen(p1):"<<strlen(p1)<<endl; //手动退化和自动退化一样:结果是10
cout<<"strlen(num):"<<strlen(num)<<endl; //错误:strlen的参数只能是char*,且必须是以'\0'结尾
cout<<"strlen(p2):"<<strlen(p2)<<endl; //错误:strlen的参数只能是char*,且必须是以'\0'结尾
cout<<"sizeof(str):"<<sizeof(str)<<endl; //数组做sizeof的参数不退化:结果是20
cout<<"sizeof(p1):"<<sizeof(p1)<<endl; //结果是4
cout<<"sizeof(*p1):"<<sizeof(*p1)<<endl; //相当于sizeof('0'),结果是1
cout<<"sizeof(num):"<<sizeof(num)<<endl; //注意一个int类型是4个字节:一共20*4=80字节
cout<<"sizeof(p2):"<<sizeof(p2)<<endl; //相当于sizeof(0),结果是4
规则9.2 禁止内存泄漏。
说明:内存和资源(包括定时器/文件句柄/Socket/队列/信号量/GUI等各种资源)泄漏是常见的错误。示例:异常出口处没有释放内存
MsgDBDEV = (PDBDevMsg)GetBuff( sizeof( DBDevMsg ), __LINE__);
if (MsgDBDEV == NULL)
{
return;
}
MsgDBAppToLogic = (LPDBSelfMsg)GetBuff( sizeof(DBSelfMsg), __LINE__ );
if ( MsgDBAppToLogic == NULL )
{
return; //MsgDB_DEV指向的内存丢失
}
坚持下列措施可以避免内存泄漏:(延伸阅读材料,《公司常见软件编程低级错误:内存泄漏.ppt》)
异常出口处检查内存、定时器/文件句柄/Socket/队列/信号量/GUI等资源是否全部释放
删除结构指针时,必须从底层向上层顺序删除
使用指针数组时,确保在释放数组时,数组中的每个元素指针是否已经提前被释放了
避免重复分配内存
小心使用有return、break语句的宏,确保前面资源已经释放
检查队列中每个成员是否释放
规则9.3 禁止引用已经释放的内存空间
说明:在实际编程过程中,稍不留心就会出现在一个模块中释放了某个内存块,而另一模块在随后的某个时刻又使用了它。要防止这种情况发生
示例:一个函数返回的局部自动存储对象的地址,导致引用已经释放的内存空间
int* foobar (void)
{
int local_auto = 100;
return &local_auto;
}
坚持下列措施可以避免引用已经释放的内存空间:(延伸阅读材料,《公司常见软件编程低级错误:野指针.ppt》)
内存释放后,把指针置为NULL;使用内存指针前进行非空判断。
耦合度较强的模块互相调用时,一定要仔细考虑其调用关系,防止已经删除的对象被再次使用。
避免操作已发送消息的内存。
自动存储对象的地址不应赋值给其他的在第一个对象已经停止存在后仍然保持的对象(具有更大作用域的对象或者静态对象或者从一个函数返回的对象)
规则9.4 编程时,要防止差1错误
说明:此类错误一般是由于把“<=”误写成“<”或“>=”误写成“>”等造成的,由此引起的后果,很多情况下是很严重的,所以编程时,一定要在这些地方小心。当编完程序后,应对这些操作符进行彻底检查。使用变量时要注意其边界值的情况。
示例:如C语言中字符型变量,有效值范围为-128到127。故以下表达式的计算存在一定风险
char ch = 127;
int sum = 200;
ch += 1; // 127为ch的边界值,再加将使ch上溢到-128,而不是128
sum += ch; // 故sum的结果不是328,而是72
规则9.5 所有的if ... else if结构应该由else子句结束;switch语句必须有default分支
建议9.1 函数中分配的内存,在函数退出之前要释放
有很多函数申请内存,保存在数据结构中,要在申请处加上注释,说明在何处释放
建议9.2 if语句尽量加上else分支,对没有else分支的语句要小心对待
建议9.3 不要滥用goto语句
goto语句会破坏程序的结构性,所以除非确实需要,最好不使用goto语句
可以利用goto语句方面退出多重循环;同一个函数体内部存在大量相同的逻辑但又不方便封装成函数的情况下,譬如反复执行文件操作,对文件操作失败以后的处理部分代码(譬如关闭文件句柄,释放动态申请的内存等等),一般会放在该函数体的最后部分,再需要的地方就goto到那里,这样代码反而变得清晰简洁。实际也可以封装成函数或者封装成宏,但是这么做会让代码变得没那么直接明了。
示例:
int foo(void)
{
char* p1 = NULL;
char* p2 = NULL;
char* p3 = NULL;
int result = -1;
p1 = (char *)malloc(0x100);
if (p1 == NULL)
{
goto Exit0;
}
strcpy(p1, "this is p1");
p2 = (char *)malloc(0x100);
if (p2 == NULL)
{
goto Exit0;
}
strcpy(p2, "this is p2");
p3 = (char *)malloc(0x100);
if (p3 == NULL)
{
goto Exit0;
}
strcpy(p3, "this is p3");
result = 0;
Exit0:
free(p1); // C标准规定可以free空指针
free(p2);
free(p3);
return result;
}
建议9.4 时刻注意表达式是否会上溢、下溢
示例:如下程序将造成变量下溢
unsigned char size ;
…
while (size-- >= 0) // 将出现下溢
{
... // program code
}
当size等于0时,再减不会小于0,而是0xFF,故程序是一个死循环。应如下修改
char size; // 从unsigned char 改为char
...
while (size-- >= 0)
{
... // program code
}
原则10.1 在保证软件系统的正确性、简洁、可维护性、可靠性及可测性的前提下,提高代码效率。
本章节后面所有的规则和建议,都应在不影响前述可读性等质量属性的前提下实施。
说明:不能一味地追求代码效率,而对软件的正确、简洁、可维护性、可靠性及可测性造成影响。 产品代码中经常有如下代码:
int foo()
{
if (异常条件)
{
异常处理;
return ERR_CODE_1;
}
if (异常条件)
{
异常处理;
return ERR_CODE_2;
}
正常处理;
return SUCCESS;
}
这样的代码看起来很清晰,而且也避免了大量的if else嵌套。但是从性能的角度来看,应该把执行概率较大的分支放在前面处理,由于正常情况下的执行概率更大,若首先考虑性能,应如下书写:
int foo()
{
if (满足条件)
{
正常处理;
return SUCCESS;
}
else if (概率比较大的异常条件)
{
异常处理;
return ERR_CODE_1;
}
else
{
异常处理;
return ERR_CODE_2;
}
}
除非证明foo函数是性能瓶颈,否则按照本规则,应优先选用前面一种写法
以性能为名,使设计或代码更加复杂,从而导致可读性更差,但是并没有经过验证的性能要求(比如实际的度量数据和目标的比较结果)作为正当理由,本质上对程序没有真正的好处。无法度量的优化行为其实根本不能使程序运行得更快
建议10.1 将不变条件的计算移到循环体外
将循环中与循环无关,不是每次循环都要做的操作,移到循环外部执行
示例一:
for (int i = 0; i < 10; i++ )
{
sum += i;
back_sum = sum;
}
对于此for循环来说语句“back_Sum = sum;”没必要每次都执行,只需要执行一次即可,因此可以改为:
for (int i = 0; i < 10; i++ )
{
sum += i;
}
back_sum = sum;
示例二:
for (_UL i = 0; i < func_calc_max(); i++)
{
// process;
}
函数func_calc_max()没必要每次都执行,只需要执行一次即可,因此可以改为:
_UL max = func_calc_max();
for (_UL i = 0; i < max; i++)
{
// process;
}
建议10.2 对于多维大数组,避免来回跳跃式访问数组成员
示例:
多维数组在内存中是从最后一维开始逐维展开连续存储的。下面这个对二维数组访问是以SIZE_B为步长跳跃访问,到尾部后再从头(第二个成员)开始,依此类推。局部性比较差,当步长较大时,可能造成cache不命中,反复从内存加载数据到cache。应该把i和j交换
for (int i = 0; i < SIZE_B; i++)
{
for (int j = 0; j < SIZE_A; j++)
{
sum += x[j][i];
}
}
上面这段代码,在 SIZE_B 数值较大时,效率可能会比下面的代码低:
for (int i = 0; i < SIZE_B; i++)
{
for (int j = 0; j < SIZE_A; j++)
{
sum += x[i][j];
}
}
建议10.3 创建资源库,以减少分配对象的开销
例如,使用线程池机制,避免线程频繁创建、销毁的系统调用;使用内存池,对于频繁申请、释放的小块内存,一次性申请一个大块的内存,当系统申请内存时,从内存池获取小块内存,使用完毕再释放到内存池中,避免内存申请释放的频繁系统调用
建议10.4 将多次被调用的 “小函数” 改为inline函数或者宏实现
如果编译器支持inline,可以采用inline函数。否则可以采用宏
在做这种优化的时候一定要注意下面inline函数的优点:其一编译时不用展开,代码SIZE小。其二可以加断点,易于定位问题,例如对于引用计数加减的时候。其三函数编译时,编译器会做语法检查。三思而后行。
原则11.1 优秀的代码可以自我解释,不通过注释即可轻易读懂
优秀的代码不写注释也可轻易读懂,注释无法把糟糕的代码变好,需要很多注释来解释的代码往往存在坏味道,需要重构
示例:
注释不能消除代码的坏味道:
/* 判断m是否为素数 */
/* 返回值:: 是素数,: 不是素数 */
int p(int m)
{
int k = sqrt(m);
for (int i = 2; i <= k; i++)
if (m % i == 0)
break; /* 发现整除,表示m不为素数,结束遍历 */
/* 遍历中没有发现整除的情况,返回 */
if (i > k)
return 1;
/* 遍历中没有发现整除的情况,返回 */
else
return 0;
}
重构代码后,不需要注释也很清晰:
int IsPrimeNumber(int num)
{
int sqrt_of_num = sqrt (num);
for (int i = 2; i <= sqrt_of_num; i++)
{
if (num % i == 0)
{
return FALSE;
}
}
return TRUE;
}
原则11.2 注释的内容要清楚、明了,含义准确,防止注释二义性
说明:有歧义的注释反而会导致维护者更难看懂代码,正如带两块表反而不知道准确时间
示例:注释与代码相矛盾,注释内容也不清楚,前后矛盾。
/* 上报网管时要求故障ID与恢复ID相一致*/
/* 因此在此由告警级别获知是不是恢复ID */
/* 若是恢复ID则设置为ClearId,否则设置为AlarmId */
if (CLEAR_ALARM_LEVEL != RcData.level)
{
SetAlarmID(RcData.AlarmId);
}
else
{
SetAlarmID(RcData.ClearId);
}
正确做法:修改注释描述如下:
/* 网管达成协议:上报故障ID与恢复ID由告警级别确定,若是清除级别,ID设置为ClearId,否则设为AlarmId。 */
原则11.3 在代码的功能、意图层次上进行注释,即注释解释代码难以直接表达的意图,而不是重复描述代码。
注释的目的是解释代码的目的、功能和采用的方法,提供代码以外的信息,帮助读者理解代码,防止没必要的重复注释信息。
对于实现代码中巧妙的、晦涩的、有趣的、重要的地方加以注释。
注释不是为了名词解释(what),而是说明用途(why)。
示例:如下注释纯属多余。
++i; /* increment i */
if (receive_flag) /* if receive_flag is TRUE */
如下这种无价值的注释不应出现(空洞的笑话,无关紧要的注释)。
/* 时间有限,现在是:04,根本来不及想为什么,也没人能帮我说清楚*/
而如下的注释则给出了有用的信息:
/* 由于xx编号网上问题,在xx情况下,芯片可能存在写错误,此芯片进行写操作后,必须进行回读校验,如果回读不正确,需要再重复写-回读操作,最多重复三次,这样可以解决绝大多数网上应用时的写错误问题 */
int time = 0;
do
{
write_reg(some_addr, value);
time++;
} while ((read_reg(some_addr) != value) && (time < 3));
对于实现代码中巧妙的、晦涩的、有趣的、重要的地方加以注释,出彩的或复杂的代码块前要加注释,如:
/* Divide result by two, taking into account that x contains the carry from the add. */
for (int i = 0; i < result->size(); i++)
{
x = (x << 8) + (*result)[i];
(*result)[i] = x >> 1;
x &= 1;
}
规则11.1 修改代码时,维护代码周边的所有注释,以保证注释与代码的一致性。不再有用的注释要删除
不要将无用的代码留在注释中,随时可以从源代码配置库中找回代码;即使只是想暂时排除代码,也要留个标注,不然可能会忘记处理它
规则11.2 注释语言
请使用英文做为注释,使用中文注释将意味着在编写代码时需要来回不停的切换中英文输入法从而打断编写代码的思路。并且使用英文注释也能够比较好的与中国以外的技术者进行交流;源代码的注释不应该过多,更多的说明应该是代码做了什么,仅当个别关键点才需要一些相应提示性的注释以解释一段复杂的算法它是如何工作的。对语句的注释只能写在它的上方或右方,其他位置都是非法的
规则11.3 文件头部应进行注释(包括头文件和源文件)
/** @brief RTOS application interface.
*
* @file aiio_os_port.h
* @copyright Copyright (C) 2020-2023, Shenzhen Anxinke Technology Co., Ltd
* @note RTOS application interface.
* @par Change Logs:
* <table>
* <tr><th>Date <th>Version <th>Author <th>Notes
* <tr><td>2023/01/30 <td>1.0.0 <td>chenzf <td>Define file
* <tr><td>2023/02/01 <td>1.0.1 <td>chenzf <td>Define file
* </table>
*
*/
关于Author:
标志更改人,更改人名称规范:统一使用公司分配的邮箱前缀,例如邮箱[email protected] ,标志名称为chenzf
规则11.4 文件函数变量注释规范
/**
* @brief 简要说明结构体目的
*/
typedef struct DOXYFILE
{
long DEV1, /*!< 简要说明文字 */
long DEV2, /*!< 简要说明文字 */
long DEV3, /*!< 简要说明文字 */
}DOXYFILE_DEMO;
long KEIL_VAR01; /*!< 简要说明变量作用 */
long KEIL_VAR02; /*!< 简要说明变量作用 */
/** 这里写这个函数的概述和作用
* @note 注解:功能以及注意事项(首字母大写,英文句号结尾)
* @see 类似于请参考XXXX函数之类的(可以链接)
*/
void DOXYFILE_DEMO_01(void);
/**
* @brief 这里写这个函数的概述和作用(首字母大写,英文句号结尾)
* @param[in] T1 什么作用
* @param[in] T2 什么作用
* @param[in] *OUT1 什么作用
* @return 函数执行结果(首字母大写,英文句号结尾)
* @retval NB_NOTIFY_SUCCESS 上报成功
* @retval NB_NOTIFY_FAIL 上报失败
* @retval NB_IOT_REGIST_FAILED 注册失败返回
* @retval Others 其他错误
* @note 功能以及注意事项(首字母大写,英文句号结尾)
* @see 类似于请参考XXXX函数之类的(可以链接)
*/
long DOXYFILE_DEMO_02(int T1, int T2, int *OUT1);
注释示例:
/**
* @brief Log dedicated serial port structure.
*/
typedef struct {
uint8_t uart_num; /*!< UART ID */
uint8_t uart_tx_pin; /*!< UART TX */
uint8_t uart_rx_pin; /*!< UART RX */
uint32_t baud_rate; /*!< UART BAUD_RATE */
} aiio_uart_config_t;
/** @brief Log uart initialization function.
*
* @param[in] uart Configure serial port printing, including serial port ID, pin, and baud rate.
* @return Return the operation status. When the return value is AIIO_OK, is successful.
* @retval AIIO_OK Init successful.
* @retval AIIO_ERROR Init error.
* @note This function needs to be adapted according to different platforms.
* @see
*/
CHIP_API int32_t aiio_uart_log_init(aiio_uart_config_t uart);
/** @brief Log uart send date function.
*
* @param[in] *fmt Format string.
* @param[in] ... Parameter scale.
* @return Return the operation status. When the return value is AIIO_OK, the send is successful.
* @retval AIIO_OK Init successful.
* @retval AIIO_ERROR Init error.
* @note This function needs to be adapted according to different platforms.
*
* @see
*/
CHIP_API int32_t aiio_uart_log_send(const char *fmt, ...);
规则11.5 全局变量要有较详细的注释,包括对其功能、取值范围以及存取时注意事项等的说明
规则11.6 注释应放在其代码上方相邻位置或右方,不可放在下面。如放于上方则需与其上面的代码用空行隔开,且与下方代码缩进相同。
示例:
/* active statistic task number */
#define MAX_ACT_TASK_NUMBER 1000
#define MAX_ACT_TASK_NUMBER 1000 /* active statistic task number */
规则11.7 避免在注释中使用缩写,除非是业界通用或子系统内标准化的缩写
规则11.8 避免在一行代码或表达式的中间插入注释
规则12.1 程序块采用缩进风格编写,每级缩进为4个空格
说明:当前各种编辑器/IDE都支持TAB键自动转空格输入,需要打开相关功能并设置相关功能
编辑器/IDE如果有显示TAB的功能也应该打开,方便及时纠正输入错误
IDE向导生成的代码可以不用修改
宏定义、编译开关、条件预处理语句可以顶格(或使用自定义的排版方案,但产品/模块内必须保持一致)
规则12.2 相对独立的程序块之间、变量说明之后必须加空行
示例:
如下例子不符合规范。
if (!valid_ni(ni))
{
// program code
...
}
repssn_ind = ssn_data[index].repssn_index;
repssn_ni = ssn_data[index].ni;
应如下书写
if (!valid_ni(ni))
{
// program code
...
}
repssn_ind = ssn_data[index].repssn_index;
repssn_ni = ssn_data[index].ni;
规则12.4 多个短语句(包括赋值语句)不允许写在同一行内,即一行只写一条语句
示例:
int a = 5; int b= 10; //不好的排版
较好的排版
int a = 5;
int b= 10;
规则12.5 if、for、do、while、case、switch、default等语句独占一行
执行语句必须用缩进风格写,属于if、for、do、while、case、switch、default等下一个缩进级别;
一般写if、for、do、while等语句都会有成对出现的‘{}’,对此有如下建议可以参考:
if、for、do、while等语句后的执行语句建议增加成对的‘{}’;
如果if/else配套语句中有一个分支有‘{}’,那么令一个分支即使一行代码也建议增加‘{}’;
添加‘{’的位置可以在if等语句后,也可以独立占下一行;独立占下一行时,可以和if在一个缩进级别,也可以在下一个缩进级别;但是如果if语句很长,或者已经有换行,建议‘{’使用独占一行的写法
规则12.6 在两个以上的关键字、变量、常量进行对等操作时,它们之间的操作符之前、之后或者前后要加空格;进行非对等操作时,如果是关系密切的立即操作符(如->),后不应加空格
采用这种松散方式编写代码的目的是使代码更加清晰
在已经非常清晰的语句中没有必要再留空格,如括号内侧(即左括号后面和右括号前面)不需要加空格,多重括号间不必加空格,因为在C语言中括号已经是最清晰的标志了
在长语句中,如果需要加的空格非常多,那么应该保持整体清晰,而在局部不加空格。给操作符留空格时不要连续留两个以上空格
示例:
(1) 逗号、分号只在后面加空格
int a, b, c;
(2) 比较操作符, 赋值操作符"="、 "+=",算术操作符"+"、"%",逻辑操作符"&&"、"&",位域操作符"<<"、"^"等双目操作符的前后加空格。
if (current_time >= MAX_TIME_VALUE)
a = b + c;
a *= 2;
a = b ^ 2;
(3) "!"、"~"、"++"、"--"、"&"(地址操作符)等单目操作符前后不加空格。
*p = 'a'; // 内容操作"*"与内容之间
flag = !is_empty; // 非操作"!"与内容之间
p = &mem; // 地址操作"&" 与内容之间
i++; // "++","--"与内容之间
(4) "->"、"."前后不加空格。
p->id = pid; // "->"指针前后不加空格
(5) if、for、while、switch等与后面的括号间应加空格,使if等关键字更为突出、明显。
if (a >= b && c > d)
规则13.1 表达式的值在标准所允许的任何运算次序下都应该是相同的
除了少数操作符(函数调用操作符 ( )、&&、| |、? : 和 , (逗号)) 之外,子表达式所依据的运算次序是未指定的并会随时更改。注意,运算次序的问题不能使用括号来解决,因为这不是优先级的问题。
将复合表达式分开写成若干个简单表达式,明确表达式的运算次序,就可以有效消除非预期副作用。
1、自增或自减操作符
示例:
x = b[i] + i++;
b[i] 的运算是先于还是后于 i++ 的运算,表达式会产生不同的结果,把自增运算做为单独的语句,可以避免这个问题。
x = b[i] + i;
i++;
2﹑函数参数
说明:函数参数通常从右到左压栈,但函数参数的计算次序不一定与压栈次序相同。
示例:
x = func( i++, i);
应该修改代码明确先计算第一个参数:
i++;
x = func(i, i);
3、函数指针
说明:函数参数和函数自身地址的计算次序未定义。
示例:
p->task_start_fn(p++);
求函数地址p与计算p++无关,结果是任意值。必须单独计算p++:
p->task_start_fn(p);
p++;
4﹑函数调用
示例:
int g_var = 0;
int fun1()
{
g_var += 10;
return g_var;
}
int fun2()
{
g_var += 100;
return g_var;
}
int x = fun1() + fun2();
编译器可能先计算fun1(),也可能先计算fun2(),由于x的结果依赖于函数fun1()/fun2()的计算次序(fun1()/fun2()被调用时修改和使用了同一个全局变量),则上面的代码存在问题。
应该修改代码明确fun1、fun2的计算次序:
int x = fun1();
x = x + fun2();
5、嵌套赋值语句
说明:表达式中嵌套的赋值可以产生附加的副作用。不给这种能导致对运算次序的依赖提供任何机会的最好做法是,不要在表达式中嵌套赋值语句。
示例:
x = y = y = z / 3;
x = y = y++;
6、volatile访问
说明:限定符volatile表示可能被其它途径更改的变量,例如硬件自动更新的寄存器。编译器不会优化对volatile变量的读取。
示例:下面的写法可能无法实现作者预期的功能:
/* volume变量被定义为volatile类型*/
UINT16 x = ( volume << 3 ) | volume; /* 在计算了其中一个子表达式的时候,volume的值可能已经被其它程序或硬件改变,导致另外一个
建议13.1 函数调用不要作为另一个函数的参数使用,否则对于代码的调试、阅读都不利
如下代码不合理,仅用于说明当函数作为参数时,由于参数压栈次数不是代码可以控制的,可能造成未知的输出:
int g_var;
int fun1()
{
g_var += 10;
return g_var;
}
int fun2()
{
g_var += 100;
return g_var;
}
int main(int argc, char *argv[], char *envp[])
{
g_var = 1;
printf("func1: %d, func2: %d\n", fun1(), fun2());
g_var = 1;
printf("func2: %d, func1: %d\n", fun2(), fun1());
}
上面的代码,使用断点调试起来也比较麻烦,阅读起来也不舒服,所以不要为了节约代码行,而写这种代码
建议13.2 赋值语句不要写在if等语句中,或者作为函数的参数使用
因为if语句中,会根据条件依次判断,如果前一个条件已经可以判定整个条件,则后续条件语句不会再运行,所以可能导致期望的部分赋值没有得到运行
示例:
int main(int argc, char *argv[], char *envp[])
{
int a = 0;
int b;
if ((a == 0) || ((b = fun1()) > 10))
{
printf("a: %d\n", a);
}
printf("b: %d\n", b);
}
作用函数参数来使用,参数的压栈顺序不同可能导致结果未知
看如下代码,能否一眼看出输出结果会是什么吗?好理解吗?
int g_var;
int main(int argc, char *argv[], char *envp[])
{
g_var = 1;
printf("set 1st: %d, add 2nd: %d\n", g_var = 10, g_var++);
g_var = 1;
printf("add 1st: %d, set 2nd: %d\n", g_var++, g_var = 10);
}
建议13.3 用括号明确表达式的操作顺序,避免过分依赖默认优先级
使用括号强调所使用的操作符,防止因默认的优先级与设计思想不符而导致程序出错;同时使得代码更为清晰可读,然而过多的括号会分散代码使其降低了可读性。下面是如何使用括号的建议。
一元操作符,不需要使用括号
x = ~a; / 一元操作符,不需要括号/ x = -a; / 一元操作符,不需要括号/
二元以上操作符,如果涉及多种操作符,则应该使用括号
x = a + b + c; / 操作符相同,不需要括号/ x = f ( a + b, c ) / 操作符相同,不需要括号/ if (a && b && c) / 操作符相同,不需要括号/ x = (a 3) + c + d; / 操作符不同,需要括号/ x = ( a == b ) ? a : ( a –b ); / 操作符不同,需要括号*/
即使所有操作符都是相同的,如果涉及类型转换或者量级提升,也应该使用括号控制计算的次序
以下代码将3个浮点数相加:
/* 除了逗号(,),逻辑与(&&),逻辑或(||)之外,C标准没有规定同级操作符是从左还是从右开始计算,以上表达式存在种计算次序:f4 = (f1 + f2) + f3 或f4 = f1 + (f2 + f3),浮点数计算过程中可能四舍五入,量级提升,计算次序的不同会导致f4的结果不同,以上表达式在不同编译器上的计算结果可能不一样,建议增加括号明确计算顺序*/
f4 = f1 + f2 + f3;
建议13.4 赋值操作符不能使用在产生布尔值的表达式上
如果布尔值表达式需要赋值操作,那么赋值操作必须在操作数之外分别进行。这可以帮助避免 = 和 == 的混淆,帮助我们静态地检查错误
示例:
x = y;
if (x != 0)
{
foo ();
}
不能写成:
if (( x = y ) != 0)
{
foo ();
}
或者更坏的
if (x = y)
{
foo ();
}
规则14.1 使用编译器的最高告警级别,理解所有的告警,通过修改代码而不是降低告警级别来消除所有告警
编译器是你的朋友,如果它发出某个告警,这经常说明你的代码中存在潜在的问题;
某些语句经编译/静态检查产生告警,但如果你认为它是正确的,那么应通过某种手段去掉告警信息
规则14.2 本地构建工具(如PC-Lint)的配置应该和持续集成的一致,避免经过本地构建的代码在持续集成上构建失败
建议14.3 要小心地使用编辑器提供的块拷贝功能编程
原则15.1 模块划分清晰,接口明确,耦合性小,有明确输入和输出,否则单元测试实施困难
说明:单元测试实施依赖于:
模块间的接口定义清楚、完整、稳定;
模块功能的有明确的验收条件(包括:预置条件、输入和预期结果);
模块内部的关键状态和关键数据可以查询,可以修改;
模块原子功能的入口唯一;
模块原子功能的出口唯一;
依赖集中处理:和模块相关的全局变量尽量的少,或者采用某种封装形式
规则15.1 在同一项目组或产品组内,要有一套统一的为集成测试与系统联调准备的调测开关及相应打印函数,并且要有详细的说明
本规则是针对项目组或产品组的。代码至始至终只有一份代码,不存在开发版本和测试版本的说法。测试与最终发行的版本是通过编译开关的不同来实现的。并且编译开关要规范统一。统一使用编译开关来实现测试版本与发行版本的区别,一般不允许再定义其它新的编译开关
规则15.2 在同一项目组或产品组内,调测打印的日志要有统一的规定
统一的调测日志记录便于集成测试,具体包括:
统一的日志分类以及日志级别;
通过命令行、网管等方式可以配置和改变日志输出的内容和格式;
在关键分支要记录日志,日志建议不要记录在原子函数中,否则难以定位;
调试日志记录的内容需要包括文件名/模块名、代码行号、函数名、被调用函数名、错误码、错误发生的环境等
规则15.3 使用断言记录内部假设
说明:断言是对某种内部模块的假设条件进行检查,如果假设不成立,说明存在编程、设计错误。断言可以对在系统中隐藏很深,用其它手段极难发现的问题进行定位,从而缩短软件问题定位时间,提高系统的可测性
规则15.4 不能用断言来检查运行时错误
说明:断言是用来处理内部编程或设计是否符合假设;不能处理对于可能会发生的且必须处理的情况要写防错程序,而不是断言。如某模块收到其它模块或链路上的消息后,要对消息的合理性进行检查,此过程为正常的错误检查,不能用断言来实现
断言的使用是有条件的。断言只能用于程序内部逻辑的条件判断,而不能用于对外部输入数据的判断,因为在网上实际运行时,是完全有可能出现外部输入非法数据的情况
原则16.1 对用户输入进行检查
说明:不能假定用户输入都是合法的,因为难以保证不存在恶意用户,即使是合法用户也可能由于误用误操作而产生非法输入。用户输入通常需要经过检验以保证安全,特别是以下场景:
用户输入作为循环条件
用户输入作为数组下标
用户输入作为内存分配的尺寸参数
用户输入作为格式化字符串
用户输入作为业务数据(如作为命令执行参数、拼装sql语句、以特定格式持久化)
这些情况下如果不对用户数据做合法性验证,很可能导致DOS、内存越界、格式化字符串漏洞、命令注入、SQL注入、缓冲区溢出、数据破坏等问题。
可采取以下措施对用户输入检查:
用户输入作为数值的,做数值范围检查
用户输入是字符串的,检查字符串长度
用户输入作为格式化字符串的,检查关键字“%”
用户输入作为业务数据,对关键字进行检查、转义
规则16.1 确保所有字符串是以NULL结束
说明:C语言中’\0’作为字符串的结束符,即NULL结束符。标准字符串处理函数(如strcpy()、strlen())依赖NULL结束符来确定字符串的长度。没有正确使用NULL结束字符串会导致缓冲区溢出和其它未定义的行为。
为了避免缓冲区溢出,常常会用相对安全的限制字符数量的字符串操作函数代替一些危险函数。如:
用strncpy()代替strcpy()
用strncat()代替strcat()
用snprintf()代替sprintf()
用fgets()代替gets()
这些函数会截断超出指定限制的字符串,但是要注意它们并不能保证目标字符串总是以NULL结尾。如果源字符串的前n个字符中不存在NULL字符,目标字符串就不是以NULL结尾。
示例:
char a[16];
strncpy(a, "0123456789abcdef", sizeof(a));
上述代码存在安全风险:在调用strncpy()后,字符数组a中的字符串是没有NULL结束符的,也没有空间存放NULL结束符。
正确写法:截断字符串,保证字符串以NULL结束。
char a[16];
strncpy(a, "0123456789abcdef", sizeof(a) - 1 );
a[sizeof(a) - 1] = '\0';
规则16.2 不要将边界不明确的字符串写到固定长度的数组中
说明:边界不明确的字符串(如来自gets()、getenv()、scanf()的字符串),长度可能大于目标数组长度,直接拷贝到固定长度的数组中容易导致缓冲区溢出。
示例:
char buff[256];
char *editor = getenv("EDITOR");
if (editor != NULL)
{
strcpy(buff, editor);
}
上述代码读取环境变量"EDITOR"的值,如果成功则拷贝到缓冲区buff中。而从环境变量获取到的字符串长度是不确定的,把它们拷贝到固定长度的数组中很可能导致缓冲区溢出。
正确写法:计算字符串的实际长度,使用malloc分配指定长度的内存
char *buff;
char *editor = getenv("EDITOR");
if (editor != NULL)
{
buff = malloc(strlen(editor) + 1);
if (buff != NULL)
{
strcpy(buff, editor);
}
}
规则16.3 避免整数溢出
说明:当一个整数被增加超过其最大值时会发生整数上溢,被减小小于其最小值时会发生整数下溢。带符号和无符号的数都有可能发生溢出
示例1:有符号和无符号整数的上溢和下溢
int i;
unsigned int j;
i = INT_MAX; // 2,147,483,647
i++;
printf("i = %d\n", i); // i=-2,147,483,648
j = UINT_MAX; // 4,294,967,295;
j++;
printf("j = %u\n", j); // j = 0
i = INT_MIN; // -2,147,483,648;
i--;
printf("i = %d\n", i); // i = 2,147,483,647
j = 0;
j--;
printf("j = %u\n", j); // j = 4,294,967,295
示例2:整数下溢导致报文长度异常
/* 报文长度减去FSM头的长度 */
unsigned int length;
length -= FSM_HDRLEN;
处理过短报文时,length的长度可能小于FSM_HDRLEN,减法的结果小于。由于length是无符号数,结果返回了一个很大的数。
正确写法:增加长度检查
if (length < FSM_HDRLEN)
{
return VOS_ERROR;
}
length -= FSM_HDRLEN;
规则16.4 避免符号错误
有时从带符号整型转换到无符号整型会发生符号错误,符号错误并不丢失数据,但数据失去了原来的含义
带符号整型转换到无符号整型,最高位(high-order bit)会丧失其作为符号位的功能。如果该带符号整数的值非负,那么转换后值不变;如果该带符号整数的值为负,那么转换后的结果通常是一个非常大的正数。
示例:符号错误绕过长度检查
#define BUF_SIZE 10
int main(int argc,char* argv[])
{
int length;
char buf[BUF_SIZE];
if (argc != 3)
{
return -1;
}
length = atoi(argv[1]); //如果atoi返回的长度为负数
if (length < BUF_SIZE) // len为负数,长度检查无效
{
memcpy(buf, argv[2], length); /* 带符号的len被转换为size_t类型的无符号整数,负值被解释为一个极大的正整数。memcpy()调用时引发buf缓冲区溢出 */
printf("Data copied\n");
}
else
{
printf("Too many data\n");
}
}
正确写法1:将len声明为无符号整型
#define BUF_SIZE 10
int main(int argc, char* argv[])
{
unsigned int length;
char buf[BUF_SIZE];
if (argc != 3)
{
return -1;
}
length = atoi(argv[1]);
if (length < BUF_SIZE)
{
memcpy(buf, argv[2], length);
printf("Data copied\n");
}
else
{
printf("Too much data\n");
}
return 0;
}
正确写法2:增加对len的更有效的范围校验
#define BUF_SIZE 10
int main(int argc, char* argv[])
{
int length;
char buf[BUF_SIZE];
if (argc != 3)
{
return -1;
}
length = atoi(argv[1]);
if ((length > 0) && (length < BUF_SIZE))
{
memcpy(buf, argv[2], length);
printf("Data copied\n");
}
else
{
printf("Too much data\n");
}
return 0;
}
规则16.5:避免截断错误
将一个较大整型转换为较小整型,并且该数的原值超出较小类型的表示范围,就会发生截断错误,原值的低位被保留而高位被丢弃。截断错误会引起数据丢失
使用截断后的变量进行内存操作,很可能会引发问题。
示例:
int main(int argc, char* argv[])
{
unsigned short total = strlen(argv[1]) + strlen(argv[2]) + 1;
char* buffer = (char*)malloc(total);
strcpy(buffer, argv[1]);
strcat(buffer, argv[2]);
free(buffer);
return 0;
}
示例代码中total被定义为unsigned short,相对于strlen()的返回值类型size_t(通常为unsigned long)太小。如果攻击者提供的两个入参长度分别为65500和36,unsigned long的65500+36+1会被取模截断,total的最终值是(65500+36+1)%65536 = 1。malloc()只为buff分配了1字节空间,为strcpy()和strcat()的调用创造了缓冲区溢出的条件。
正确写法:将涉及到计算的变量声明为统一的类型,并检查计算结果
int main(int argc, char* argv[])
{
size_t total = strlen(argv[1]) + strlen(argv[2]) + 1;
if ((total <= strlen(argv[1])) || (total <= strlen(argv[2])))
{
/* handle error */
return -1;
}
char* buffer = (char*)malloc(total);
strcpy(buffer, argv[1]);
strcat(buffer, argv[2]);
free(buffer);
return 0;
}
规则16.6:确保格式字符和参数匹配
使用格式化字符串应该小心,确保格式字符和参数之间的匹配,保留数量和数据类型。格式字符和参数之间的不匹配会导致未定义的行为。大多数情况下,不正确的格式化字符串会导致程序异常终止
示例:
char *error_msg = "Resource not available to user.";
int error_type = 3;
/* 格式字符和参数的类型不匹配*/
printf("Error (type %s): %d\n", error_type, error_msg);
/* 格式字符和参数的数量不匹配*/
printf("Error: %s\n");
格式化字符串在编码时会大量使用,容易copy-paste省事,这就容易出现不匹配的错误
规则16.7 避免将用户输入作为格式化字符串的一部分或者全部
说明:调用格式化I/O函数时,不要直接或者间接将用户输入作为格式化字符串的一部分或者全部。攻击者对一个格式化字符串拥有部分或完全控制,存在以下风险:进程崩溃、查看栈的内容、改写内存、甚至执行任意代码。
示例1:
char input[1000];
if (fgets(input, sizeof(input) - 1, stdin) == NULL)
{
/* handle error */
}
input[sizeof(input)-1] = '\0';
printf(input);
上述代码input直接来自用户输入,并作为格式化字符串直接传递给printf()。当用户输入的是“%s%s%s%s%s%s%s%s%s%s%s%s”,就可能触发无效指针或未映射的地址读取。格式字符%s显示栈上相应参数所指定的地址的内存。这里input被当成格式化字符串,而没有提供参数,因此printf()读取栈中任意内存位置,指导格式字符耗尽或者遇到一个无效指针或未映射地址为止。
正确做法:给printf()传两个参数,第一个参数为”%s”,目的是将格式化字符串确定下来;第二个参数为用户输入input。
char input[1000];
if (fgets(input, sizeof(input)-1, stdin) == NULL)
{
/* handle error */
}
input[sizeof(input)-1] = '\0';
printf(“%s”, input);
示例2:
void check_password(char *user, char *password)
{
if (strcmp(password(user), password) != 0)
{
char *msg = malloc(strlen(user) + 100);
if (!msg)
{
/* handle error condition */
}
sprintf(msg, "%s login incorrect", user);
fprintf(STDERR, msg);
syslog(LOG_INFO, msg);
free(msg);
}
/* ... */
}
上述代码检查给定用户名及其口令是否匹配,当不匹配时显示一条错误信息,并将错误信息写入日志中。同样的,如果user为” %s%s%s%s%s%s%s%s%s%s%s%s”,经过格式化函数sprintf()的拼装后,msg指向的字符串为” %s%s%s%s%s%s%s%s%s%s%s%s login incorrect”,在fprintf()调用中,msg将作为fprintf()的格式化字符串,可能引发如同示例1一样的问题。而且,syslog()函数也一样存在格式化字符串的问题。
正确做法:格式化字符串由代码确定,未经检查过滤的用户输入只能作为参数。
void check_password(char *user, char *password)
{
if (strcmp(password(user), password) != 0)
{
char *msg = malloc(strlen(user) + 100);
if (!msg)
{
/* handle error condition */
}
sprintf(msg, "%s password incorrect", user);
fprintf(stderr, "%s", user);
syslog(LOG_INFO, "%s", msg);
free(msg);
}
}
规则16.8 避免使用strlen()计算二进制数据的长度
strlen()函数用于计算字符串的长度,它返回字符串中第一个NULL结束符之前的字符的数量。因此用strlen()处理文件I/O函数读取的内容时要小心,因为这些内容可能是二进制也可能是文本
示例:
char buf[BUF_SIZE + 1];
if (fgets(buf, sizeof(buf), fp) == NULL)
{
/* handle error */
}
buf[strlen(buf) - 1] = '\0';
上述代码试图从一个输入行中删除行尾的换行符(\n)。如果buf的第一个字符是NULL,strlen(buf)返回0,这时对buf进行数组下标为[-1]的访问操作将会越界。
正确做法:在不能确定从文件读取到的数据的类型时,不要使用依赖NULL结束符的字符串操作函数。
char buf[BUF_SIZE + 1];
char *p;
if (fgets(buf, sizeof(buf), fp))
{
p = strchr(buf, '\n');
if (p)
{
*p = '\0';
}
}
else
{
/* handle error condition */
}
规则16.9 使用int类型变量来接受字符I/O函数的返回值
字符I/O函数fgetc()、getc()和getchar()都从一个流读取一个字符,并把它以int值的形式返回。如果这个流到达了文件尾或者发生读取错误,函数返回EOF。fputc()、putc()、putchar()和ungetc()也返回一个字符或EOF
如果这些I/O函数的返回值需要与EOF进行比较,不要将返回值转换为char类型。因为char是有符号8位的值,int是32位的值。如果getchar()返回的字符的ASCII值为0xFF,转换为char类型后将被解释为EOF。因为这个值被有符号扩展为0xFFFFFFFF(EOF的值)执行比较
示例:
char buf[BUF_SIZE];
char ch;
int i = 0;
while ( (ch = getchar()) != '\n' && ch != EOF )
{
if ( i < BUF_SIZE - 1 )
{
buf[i++] = ch;
}
}
buf[i] = '\0'; /* terminate NTBS */
正确做法:使用int类型的变量接受getchar()的返回值。
char buf[BUF_SIZE];
int ch;
int i = 0;
while (((ch = getchar()) != '\n') && ch != EOF)
{
if (i < BUF_SIZE - 1)
{
buf[i++] = ch;
}
}
buf[i] = '\0'; /* terminate NTBS */
对于sizeof(int) == sizeof(char)的平台,用int接收返回值也可能无法与EOF区分,这时要用feof()和ferror()检测文件尾和文件错误
规则16.10 防止命令注入
C99函数system()通过调用一个系统定义的命令解析器(如UNIX的shell,Windows的CMD.exe)来执行一个指定的程序/命令。类似的还有POSIX的函数popen()
如果system()的参数由用户的输入组成,恶意用户可以通过构造恶意输入,改变system()调用的行为
示例:
system(sprintf("any_exe %s", input));
如果恶意用户输入参数:
happy; useradd attacker
最终shell将字符串“any_exe happy; useradd attacker”解释为两条独立的命令:
正确做法:使用POSIX函数execve()代替system()。
void secuExec (char *input)
{
pid_t pid;
char *const args[] = {"", input, NULL};
char *const envs[] = {NULL};
pid = fork();
if (pid == -1)
{
puts("fork error");
}
else if (pid == 0)
{
if (execve("/usr/bin/any_exe", args, envs) == -1)
{
puts("Error executing any_exe");
}
}
return;
}
Windows环境可能对execve()的支持不是很完善,建议使用Win32 API CreateProcess()代替system()