openHiTLS编码安全规范

1. 规则名称:宏的名称不应与关键字相同

问题级别:严重

规则描述:

使用宏来改变语言关键字(包括用于实现语言扩展的关键字)的含义会导致代码难以理解。如果定义这种宏的同时又包含了标准头文件,则程序会产生未定义行为。

错误示例:

// 不符合:改变了int的行为,导致后面包含标准头文件时,程序出现未定义的行为
#define int OTHER_TYPE
#include <stdlib.h>

// 不符合:重定义关键字
#define while(x) for (; (x);)

修改建议:

修改宏的名称。


2. 规则名称:调用格式化输入/输出函数时,使用有效的格式字符串

问题级别:严重

规则描述:

格式化字符个数与变参个数不一致。

使用C风格的格式化输入/输出函数时,需要确保格式串是合法有效的,并且格式串与相应的实参类型是严格匹配的,否则会使程序产生非预期行为。

除C风格的格式化输入/输出函数以外,C++中类似的函数也需要确保使用有效的格式串,如C++20的std::format()函数。

对于自定义C风格的格式化函数,可以使用编译器支持的属性自动检查使用自定义格式化函数的正确性。例如:GCC支持自动检测类似printf, scanf, strftime, strfmon的自定义格式化函数,参考GCC手册的Common Function Attributes:

extern int CustomPrintf(void* obj, const char* format, ...)
      __attribute__ ((format (printf, 2, 3)));

正确示例:

如下代码中,使用%hhx确保格式串与相应的实参类型严格匹配。

unsigned char macAddr[6];
...
// macStr中的数据格式为 e2:42:a4:52:1e:33
int ret = sscanf_s(macStr, "%hhx:%hhx:%hhx:%hhx:%hhx:%hhx\n",
                  &macAddr[0], &macAddr[1],
                  &macAddr[2], &macAddr[3],
                  &macAddr[4], &macAddr[5]);
...

注:在C++中不推荐使用sscanf, sprintf等C库函数,可以替换为:std::istringstream, std::ostringstream, std::stringstream等。

错误示例:

如下代码中,格式化输入一个整数到macAddr变量中,但是macAddr为unsigned char类型,而%x对应的是unsigned int类型参数,函数执行完成后会发生写越界。

unsigned char macAddr[6];
...
// macStr中的数据格式为 e2:42:a4:52:1e:33
int ret = sscanf_s(macStr, "%x:%x:%x:%x:%x:%x\n",
                  &macAddr[0], &macAddr[1],
                  &macAddr[2], &macAddr[3],
                  &macAddr[4], &macAddr[5]);
...

修改建议:

确保format函数参数个数和实际参数个数一致。


3. 规则名称:不要声明或定义保留标识符

问题级别:严重

规则描述:

如果声明或者定义了一个保留的标识符,那么程序会产生未定义行为。

如下是C++中的一些保留标识符说明:

  • 包含双下划线的,或者以单下划线加一个大写字母开头的标识符,为实现保留
  • 在全局命名空间中以下划线开头的标识符为实现保留
  • 不以下划线开头的literal后缀为将来标准保留,用户自定义的literal后缀名应以单下划线开头
  • posix命名空间名以及std加一或多个数字的命名空间名(如std2)为将来标准保留
  • 任何包含标准库头文件的编译单元不能#define或#undef任何在标准库头文件中声明的标识符
  • 标准库头文件在全局命名空间和std命名空间中声明为外部链接的对象名为实现保留
  • 标准库头文件中声明为外部链接的全局函数名为实现保留
  • 标准C库中具有外部链接的对象名或函数名为实现保留

错误示例:

#undef __LINE__               // 不符合:__LINE__为保留宏,不允许#undef保留宏
#define _MODULE_INCLUDE_      // 不符合:下划线加一个大写字母开头  
enum { SIZE_MAX = 80 };       // 不符合:SIZE_MAX是<cstdint>中保留宏
int errno;                    // 不符合:errno是标准库中的保留标识符  
void* malloc(size_t nbytes);  // 不符合:malloc是标准库中的保留标识符  

【例外】
保留的标识符可以由编译器在标准库头文件,或标准库头文件包含的头文件中定义。

修改建议:

修改标识符名称。


4. 规则名称:宏定义不应依赖宏外部的局部变量名

问题级别:严重

规则描述:

宏定义中要使用的变量都要求作为参数传递给宏。 如果宏定义中直接使用宏外部的局部变量名,会导致宏的可重用性差,而且不利于理解。

正确示例:

#define INIT(x) ((x)->y->length)

...
int count = INIT(msg);

错误示例:

#define INIT(x) do { \
    count = (x)->y->length; \
} while (0)

...
int count;
INIT(msg);

修改建议:

将需要引用的局部变量作为参数传递进来


5. 规则名称:确保枚举常量映射到唯一值

问题级别:严重

规则描述:

C语言的枚举类型是具有一组有限值的集合,这些值的标识符被称为枚举常量。枚举常量是一个整数常量表达式,其值可表示为int。尽管C语言允许相同枚举类型中的多个枚举常量具有相同的值,但是人们普遍期望同一个枚举类型中的枚举常量具有不同的值。因此,将同一个枚举类型中的多个枚举常量定义为相同的值可能会导致一些不易被发现的错误。

正确示例:

为了防止出现错误代码示例中的问题,枚举类型声明可以采用以下形式之一:

  • 不提供显式的整数赋值,如下示例所示
typedef enum {
    ERR_SYS_ARG_ERR,
    ERR_SYS_SET_ERR,
    ERR_SYS_OCCUPIED_ERR,
    ERR_SYS_REQFREE_ERR,
    ERR_SYS_NO_REC_ERR,
    ERR_SYS_ACTIVED_ERR,
    ERR_SYS_NO_FILE_ERR,
    ERR_SYS_OPEN_ERR,
    ERR_SYS_ASSIGN_ERR,
    ERR_SYS_ALLOC_ERR,
    ERR_SYS_GET_LIST_ERR,
    ERR_SYS_IGNORE_ERR,
    ERR_SYS_REG_ERR,
    ERR_SYS_CALC_ERR,
    ERR_SYS_SNED_ERR,
    ERR_SYS_RECV_ERR,
    ERR_SYS_MODIFY_ERR,
    ERR_SYS_CHECK_ERR,

    ERR_EMG_INVALID_ERR,
    ERR_EMG_TYPE_ERR,
    ...
} SomeModuleErrCodes;
  • 只对第一个成员赋值,如下示例所示
typedef enum {
    ERR_SYS_ARG_ERR      = 0x3a020000,
    ERR_SYS_SET_ERR,
    ERR_SYS_OCCUPIED_ERR,
    ERR_SYS_REQFREE_ERR,
    ERR_SYS_NO_REC_ERR,
    ERR_SYS_ACTIVED_ERR,
    ERR_SYS_NO_FILE_ERR,
    ERR_SYS_OPEN_ERR,
    ERR_SYS_ASSIGN_ERR,
    ERR_SYS_ALLOC_ERR,
    ERR_SYS_GET_LIST_ERR,
    ERR_SYS_IGNORE_ERR,
    ERR_SYS_REG_ERR,
    ERR_SYS_CALC_ERR,
    ERR_SYS_SNED_ERR,
    ERR_SYS_RECV_ERR,
    ERR_SYS_MODIFY_ERR,
    ERR_SYS_CHECK_ERR,

    ERR_EMG_INVALID_ERR,
    ERR_EMG_TYPE_ERR,
    ...
} SomeModuleErrCodes;

在上面的两个选项中,除非第一个枚举数必须具有非零值,否则第一种做法是最简单的方法,因此也是首选方法。

错误示例:

如下代码示例中,在枚举类型SomeModuleErrCodes中定义错误码的时候,为两个枚举常量分配了显式值,造成 ERR_SYS_MODIFY_ERRERR_EMG_INVALID_ERR 被隐式声明为相同的值(0x3a020010),对于程序员来说,可能并不明显。 这种定义可能导致的错误是尝试将枚举常量用于 switch 语句的标签。由于 switch 语句中的所有标签都必须是唯一的,因此如下代码违反了此语义约束。

typedef enum {
    ERR_SYS_ARG_ERR = 0x3a020000,
    ERR_SYS_SET_ERR,
    ERR_SYS_OCCUPIED_ERR,
    ERR_SYS_REQFREE_ERR,
    ERR_SYS_NO_REC_ERR,
    ERR_SYS_ACTIVED_ERR,
    ERR_SYS_NO_FILE_ERR,
    ERR_SYS_OPEN_ERR,
    ERR_SYS_ASSIGN_ERR,
    ERR_SYS_ALLOC_ERR,
    ERR_SYS_GET_LIST_ERR,
    ERR_SYS_IGNORE_ERR,
    ERR_SYS_REG_ERR,
    ERR_SYS_CALC_ERR,
    ERR_SYS_SNED_ERR,
    ERR_SYS_RECV_ERR,
    ERR_SYS_MODIFY_ERR,    // 不符合:值与ERR_EMG_INVALID_ERR相同
    ERR_SYS_CHECK_ERR,     // 不符合:值与ERR_EMG_TYPE_ERR相同

    ERR_EMG_INVALID_ERR = 0x3a020010,
    ERR_EMG_TYPE_ERR,
    ...
} SomeModuleErrCodes;

修改建议:

修复建议

(1)纠正编码错误,或者(2)显式指定枚举值


6. 规则名称:内存中的敏感信息使用完毕后立即清0

问题级别:严重

规则描述:

内存中的敏感信息使用完毕后未清零。

内存中的口令、密钥等敏感信息使用完毕后立即清0,避免被攻击者获取或者无意间泄露给低权限用户。这里所说的内存包括但不限于:

  • 动态分配的内存
  • 静态分配的内存
  • 自动分配(堆栈)内存
  • 内存缓存
  • 磁盘缓存

正确示例:

如下代码示例中,为了防止信息泄露,应先清除包含敏感信息的动态内存(用'\0'字符填充空间),然后再释放它。

char *secret = NULL;
/*
* 假设 secret 指向敏感信息,敏感信息的内容是长度小于SIZE_MAX个字符,
* 并且以null终止的字节字符串
*/
size_t size = strlen(secret);
char *newSecret = NULL;
newSecret = (char *)malloc(size + 1);
if (newSecret == NULL) {
    ... // 错误处理
} else {
    errno_t ret = strcpy_s(newSecret, size + 1, secret);
    ... // 处理 ret

    ... // 处理 newSecret...

    (void)memset_s(newSecret, size + 1, 0, size + 1);
    free(newSecret);
    newSecret = NULL;
}

错误示例:

通常内存在释放前不需要清除内存数据,因为这样在运行时会增加额外开销,所以在这段内存被释放之后,之前的数据还是会保留在其中。如果这段内存中的数据包含敏感信息,则可能会意外泄露敏感信息。为了防止敏感信息泄露,必须先清除内存中的敏感信息,然后再释放。

在如下代码示例中,存储在所引用的动态内存中的敏感信息secret被复制到新动态分配的缓冲区newSecret,最终通过free()释放。因为释放前未清除这块内存数据,这块内存可能被重新分配到程序的另一部分,之前存储在newSecret中的敏感信息可能会无意中被泄露。

char *secret = NULL;
/*
* 假设 secret 指向敏感信息,敏感信息的内容是长度小于SIZE_MAX个字符,
* 并且以null终止的字节字符串
*/
size_t size = strlen(secret);
char *newSecret = NULL;
newSecret = (char *)malloc(size + 1);
if (newSecret == NULL) {
    ... // 错误处理
} else {
    errno_t ret = strcpy_s(newSecret, size + 1, secret);
    ... // 处理 ret
    ... // 处理 newSecret...
    free(newSecret);
    newSecret = NULL;
}
...

修改建议:

内存中的敏感信息使用完毕后立即清零,关键字包括password、psw、pwd、passwd


7. 规则名称:包含多条语句的函数式宏的实现语句必须放在 do-while(0) 中

问题级别:严重

规则描述:

宏本身没有代码块的概念。当宏在调用点展开后,宏内定义的表达式和变量融合到调用代码中,可能会出现变量名冲突和宏内语句被分割等问题。 通过 do-while(0) 显式为宏加上边界,让宏有独立的作用域,并且跟分号能更好的结合而形成单条语句,从而规避此类问题。

如下所示的宏是错误的用法(为了说明问题,示例代码稍不符规范):

// 不符合
#define FOO(x) \
    (void)printf("arg is %d\n", (x)); \
    DoSomething((x));

当像如下示例代码这样调用宏FOO,for 循环只执行了宏的第一条语句,宏的后一条语句只在循环结束后执行一次。

for (i = 1; i < MAX_TIMES; i++)
    FOO(i);

用大括号将FOO定义的语句括起来可以解决上面的问题:

#define FOO(x) { \
    (void)printf("arg is %d\n", (x)); \
    DoSomething((x)); \
}

但是,如下示例代码,会出现编译报错(else没有与之匹配的if语句):

if (condition)
    FOO(MAX_MONTH);
else
    FOO(MAX_YEAR);

更好的写法是用 do-while(0) 把宏FOO执行体括起来,如下所示:

// 符合
#define FOO(x) do { \
    (void)printf("arg is %d\n", (x)); \
    DoSomething((x)); \
} while (0)

错误示例:

// 不符合
#define FOO(x) \
    (void)printf("arg is %d\n", (x)); \
    DoSomething((x));

修改建议:

用do-while(0)封装代码块。


8. 规则名称:不要在共享目录中创建临时文件【C】

问题级别:严重

规则描述:

不能在共享目录中创建临时文件。

共享目录是指其它非特权用户可以访问的目录。程序的临时文件应当是程序自身独享的,任何将自身临时文件置于共享目录的做法,将导致其他共享用户获得该程序的额外信息,产生信息泄露。因此,不要在任何共享目录创建仅由程序自身使用的临时文件。

临时文件通常用于辅助保存不能驻留在内存中的数据或存储临时的数据,也可用作进程间通信的一种手段(通过文件系统传输数据)。例如,一个进程在共享目录中创建一个临时文件,该文件名可能使用了众所周知的名称或者一个临时的名称,然后就可以通过该文件在进程间共享信息。这种通过在共享目录中创建临时文件的方法实现进程间共享的做法很危险,因为共享目录中的这些文件很容易被攻击者劫持或操纵。这里有几种缓解策略:

  1. 使用其他低级IPC(进程间通信)机制,例如套接字或共享内存。
  2. 使用更高级别的IPC机制,例如远程过程调用。
  3. 使用仅能由程序本身访问的安全目录(多线程/进程下注意防止条件竞争)。

同时,下面列出了几项临时文件创建使用的方法,产品根据具体场景执行以下一项或者几项,同时产品也可以自定义合适的方法:

  1. 文件必须具有合适的权限,只有符合权限的用户才能访问。
  2. 创建的文件名是唯一的、或不可预测的。
  3. 仅当文件不存在时才创建打开(原子创建打开)。
  4. 使用独占访问打开,避免竞争条件。
  5. 在程序退出之前移除。

同时也需要注意到,当某个目录被开放读/写权限给多个用户或者一组用户时,该共享目录潜在的安全风险远远大于访问该目录中临时文件这个功能的本身。

在共享目录中创建临时文件很容易受到威胁。例如,用于本地挂载的文件系统的代码在与远程挂载的文件系统一起共享使用时可能会受到攻击。安全的解决方案是不要在共享目录中创建临时文件。

正确示例:

场景1:在系统共享目录中创建临时文件。
  • 修复示例:
// 临时文件名不硬编码
char *filename = GetFileName();
...
sprintf_s(path, sizeof(path), "/home/zhangsanproject/%s", filename);
// 符合:临时文件不能在共享目录中创建,应该在进程的独享目录中
FILE *fp = fopen(path, "wb+");
if (fp == NULL) {
    ... // 错误处理
}
...

错误示例:

场景1:在系统共享目录中创建临时文件。
  • 错误示例:
// 不符合:1.在系统共享目录中创建临时文件;2.临时文件名硬编码
FILE *fp = fopen("/tmp/data", "wb+");
if (fp == NULL) {
    ... // 错误处理
}
...

修改建议:

不要在共享目录中创建临时文件。


9. 规则名称:避免在代码中直接使用assert()【C】

问题级别:严重

规则描述:

断言必须使用宏定义,且只能在调试版本中生效。

使用assert()会使代码发布版本与NDEBUG宏发生直接联系。为避免发布版本定义的宏受限于NDEBUG,在代码中不应直接使用assert()。

正确示例:

场景1:使用自定义断言宏时,在发布版本中使用打印替换断言。
  • 修复示例:使用自定义断言宏时,在发布版本中不能使用打印。

    #ifdef DEBUG
    #define ASSERT(f)  assert(f)
    #else
    #define ASSERT(f)  ((void)0)    // 符合
    #endif
    
场景2:在代码中直接使用assert()。
  • 修复示例:在代码中不应直接使用assert(),应该使用自定义断言宏。

    // 例如以下宏定义
    #ifdef DEBUG
    #define ASSERT(f)  assert(f)
    #else
    #define ASSERT(f)  ((void)0)
    #endif
    
    int Foo(int *array, size_t size)
    {
        ASSERT(array != NULL);  // 符合
        ...
    }
    

错误示例:

场景1:使用自定义断言宏时,在发布版本中使用打印替换断言。
  • 错误示例:在发布版本中使用打印替换断言,生成了实际代码,是不正确的设计。

    #ifdef DEBUG
    #define ASSERT(f)  assert(f)
    #else
    #define ASSERT(f)  do { \
        if (!(f)) { \
            printf("Error in function=%s, Line=%d\n", __FUNCTION__, __LINE__); \    // 不符合
        } \
    } while (0)
    #endif
    
场景2:在代码中直接使用assert()。
  • 错误示例:

    int Foo(int *array, size_t size)
    {
        assert(array != NULL);  // 不符合
        ...
    }
    

修改建议:

断言必须使用宏定义,且只能在调试版本中生效。


10. 规则名称:删除无效或永不执行的代码

问题级别:严重

规则描述:

被注释掉的代码,无法被正常维护,因此如果代码不再使用,请直接删除,不要注释掉。

正确示例:

场景1:用//注释不用的代码段
  • 修复示例:
#define MAX_SIZE

void Foo()
{
    int x;
    ...
    // 符合:直接删除注释的代码
    x = 0;
    ...
}
场景2:用#if 0注释不用的代码段
  • 修复示例:
// 符合:直接删除注释的代码
#if defined(__arm64__) && __arm64__
typedef uint_least16_t char16_t;
typedef uint_least32_t char32_t;
#endif

错误示例:

场景1:用//注释不用的代码段
  • 错误示例:
#define MAX_SIZE

void Foo()
{
    int x;
    ...
    // 不符合:注释的代码
    // x = MAX_SIZE;
    x = 0;
    ...
}
场景2:用#if 0注释不用的代码段
  • 错误示例:
// 不符合:注释的代码
#if 0
typedef uint_least16_t char16_t;
#elif defined(__arm64__) && __arm64__
typedef uint_least16_t char16_t;
typedef uint_least32_t char32_t;
#endif

修改建议:

不用的代码段直接删除掉。若再需要时,考虑移植或重写这段代码。 这里说的“注释掉”的方式,除了/* */ 或//、还包括#if 0、#ifdef NEVER_DEFINED等,但注释中的代码示例不属于被注释掉的代码。


11. 规则名称:禁止把带副作用的表达式作为参数传递给函数式宏

问题级别:严重

规则描述:

由于宏只是文本替换,对于内部多次使用同一个宏参数的函数式宏,将带副作用的表达式作为宏参数传入会导致非预期的结果。

正确示例:

代码做如下修改:

a++; // 结果:a = 6,只自增了一次
b = SQUARE(a);

错误示例:

如下所示,宏SQUARE本身没有问题,但是使用时将带副作用的表达式a++传入导致a的值在SQUARE执行后的结果跟预期不符:

#define SQUARE(a) ((a) * (a))

int a = 5;
int b = SQUARE(a++); // 不符合: 展开后表达式中有2个 "a++",其结果可能是非预期的。

SQUARE(a++)展开后为((a++) * (a++)),变量a自增了两次,其值为7,而不是预期的6。

修改建议:

不要将副作用表达式作为宏的参数


12. 规则名称:调用格式化输入/输出函数时,使用有效的格式字符串--格式化类型不匹配

问题级别:严重

规则描述:

调用格式化函数时,Format中参数类型和实际类型一致。

使用C风格的格式化输入/输出函数时,需要确保格式串是合法有效的,并且格式串与相应的实参类型是严格匹配的,否则会使程序产生非预期行为。

除C风格的格式化输入/输出函数以外,C++中类似的函数也需要确保使用有效的格式串,如C++20的std::format()函数。

对于自定义C风格的格式化函数,可以使用编译器支持的属性自动检查使用自定义格式化函数的正确性。例如:GCC支持自动检测类似printf, scanf, strftime, strfmon的自定义格式化函数,参考GCC手册的Common Function Attributes:

extern int CustomPrintf(void* obj, const char* format, ...)
      __attribute__ ((format (printf, 2, 3)));

正确示例:

如下代码中,使用%hhx确保格式串与相应的实参类型严格匹配。

unsigned char macAddr[6];
...
// macStr中的数据格式为 e2:42:a4:52:1e:33
int ret = sscanf_s(macStr, "%hhx:%hhx:%hhx:%hhx:%hhx:%hhx\n",
                  &macAddr[0], &macAddr[1],
                  &macAddr[2], &macAddr[3],
                  &macAddr[4], &macAddr[5]);
...

注:在C++中不推荐使用sscanf, sprintf等C库函数,可以替换为:std::istringstream, std::ostringstream, std::stringstream等。

错误示例:

如下代码中,格式化输入一个整数到macAddr变量中,但是macAddr为unsigned char类型,而%x对应的是unsigned int类型参数,函数执行完成后会发生写越界。

unsigned char macAddr[6];
...
// macStr中的数据格式为 e2:42:a4:52:1e:33
int ret = sscanf_s(macStr, "%x:%x:%x:%x:%x:%x\n",
                  &macAddr[0], &macAddr[1],
                  &macAddr[2], &macAddr[3],
                  &macAddr[4], &macAddr[5]);
...

修改建议:

确保format函数参数类型和实际类型一致。


13. 规则名称:禁止宏调用参数中出现预编译指令

问题级别:严重

规则描述:

这里的宏指函数式宏,其参数不能包括预处理器指令,如#include,#define和#ifdef,这样做会导致未定义的行为。

此规则还适用于在调用标准库函数参数的场景,因为任何标准库函数可作为宏来实现。

正确示例:

#define WRITE_LOG(X)  printf("%s\n", #X)
int Foo(void)
{
#ifdef PLATFORM1
    WRITE_LOG("Notice: Something error.");
#else
    WRITE_LOG("Code: 4567");
#endif
}

错误示例:

如下代码可能会导致程序出现未定义行为。

#define WRITE_LOG(X)  printf("%s\n", #X)
int Foo(void)
{
    WRITE_LOG(
#ifdef PLATFORM1
    "Notice: Something error."
#else
    "Code: 4567"
#endif
    );
}

修改建议:

预处理指令在宏调用之外进行保护


14. 规则名称:定义宏时,要使用完备的括号

问题级别:严重

规则描述:

宏展开时只做文本替换,在编译时再求值。文本替换后,宏包含的语句跟调用点代码合并。 合并后的表达式因为操作符的优先级和结合律,可能会导致计算结果跟期望的不同。

比如:

#define C_LEN A_LEN + B_LEN     // 不符合

上述宏在展开时,A_LEN 与 B_LEN 的加法并不一定是优先计算。 正确的写法应该是:

#define C_LEN (A_LEN + B_LEN)   // 符合

带参数的宏更容易出现问题,比如:

#define SUM(a, b) a + b     // 不符合

下面这样调用该宏,执行结果跟预期不符: 100 / SUM(2, 8) 将扩展成(100 / 2) + 8,而预期结果是100 / (2 + 8)

这个问题的解决方法如下所示:

#define SUM(a, b) ((a) + (b)) // 符合

但是要避免滥用括号。如下所示,单独的数字或标识符加括号毫无意义。

#define SOME_CONST  100         // 符合: 单独的数字无需括号
#define ANOTHER_CONST   (-1)    // 符合: 负数需要使用括号
#define THE_CONST   SOME_CONST  // 符合: 单独的标识符无需括号

下列情况需要注意:

  • 宏参数参与 '#', '##' 操作时,不要加括号
  • 宏参数参与字符串拼接时,不要加括号
  • 宏参数作为独立部分,在赋值(包括+=, -=等)操作的某一边时,可以不加括号
  • 宏参数作为独立部分,在逗号表达式,函数或宏调用列表中,可以不加括号

举例如下:

// x 不要加括号
#define MAKE_STR(x) #x

// obj 不要加括号
#define HELLO_STR(obj) "Hello, " obj

// a, b 需要括号;而 value 可以不加括号
#define UPDATE_VALUE(value, a, b) (value = (a) + (b))

// a 需要括号;而 b 可以不加括号
#define FOO(a, b) Bar((a) + 1, b)

修改建议:

函数式宏参数使用时添加括号


15. 规则名称:断言必须使用宏定义,且只能在调试版本中生效【C】

问题级别:严重

规则描述:

断言必须使用宏定义,且只能在调试版本中生效。

C语言标准定义断言为用于诊断测试的宏(assert),因此,断言只能在调试版本中使用。断言被触发后,程序会立即退出,因此严禁在正式发布版本使用断言,请通过编译选项进行控制。

断言触发时虽然能提供少量提示信息,但这样的提示信息通常只对程序员有用,对用户几乎没有价值。程序总是应该优先考虑从错误中恢复,因此,断言应只在调试阶段作为诊断方式生效,其他时候,断言是一种比注释更好的文档说明。例如,断言可以用来声明使用函数的前提条件。

正确示例:

场景1:使用自定义断言宏时,在发布版本中使用打印替换断言。
  • 修复示例:使用自定义断言宏时,在发布版本中不能使用打印。
#ifdef DEBUG
#define ASSERT(f)  assert(f)
#else
#define ASSERT(f)  ((void)0)    // 符合
#endif
场景2:在代码中直接使用assert()。
  • 修复示例:在代码中不应直接使用assert(),应该使用自定义断言宏。
// 例如以下宏定义
#ifdef DEBUG
#define ASSERT(f)  assert(f)
#else
#define ASSERT(f)  ((void)0)
#endif

int Foo(int *array, size_t size)
{
    ASSERT(array != NULL);  // 符合
    ...
}

错误示例:

场景1:使用自定义断言宏时,在发布版本中使用打印替换断言。
  • 错误示例:在发布版本中使用打印替换断言,生成了实际代码,是不正确的设计。
#ifdef DEBUG
#define ASSERT(f)  assert(f)
#else
#define ASSERT(f)  do { \
    if (!(f)) { \
        printf("Error in function=%s, Line=%d\n", __FUNCTION__, __LINE__); \    // 不符合
    } \
} while (0)
#endif
场景2:在代码中直接使用assert()。
  • 错误示例:
int Foo(int *array, size_t size)
{
    assert(array != NULL);  // 不符合
    ...
}

修改建议:

断言必须使用宏定义,且只能在调试版本中生效。


16. 规则名称:一个断言只用于检查一个错误【C】

问题级别:严重

规则描述:

一个断言只用于检查一个错误。

为了更加准确地发现错误的位置,每一条断言只校验一个错误。

正确示例:

场景1:断言同时校验多个错误。
  • 修复示例:如果要校验多个错误,应该将每个错误分开,一个断言只用于检查一个错误。

    int Foo(int *array, size_t size)
    {
        ASSERT(array != NULL);          // 符合:一个断言只用于检查一个错误
        ASSERT(size > 0);               // 符合:一个断言只用于检查一个错误
        ASSERT(size <= ARRAY_SIZE_MAX); // 符合:一个断言只用于检查一个错误
        ...
    }
    

错误示例:

场景1:断言同时校验多个错误。
  • 错误示例:

    int Foo(int *array, size_t size)
    {
        ASSERT(array != NULL && size > 0 && size <= ARRAY_SIZE_MAX); // 不符合:在断言触发的时候,无法判断到底是哪一个错误触发了断言
        ...
    }
    

修改建议:

一个断言只用于检查一个错误。


17. 规则名称:禁止使用alloca()函数申请栈上内存

问题级别:严重

规则描述:

禁止使用alloca()函数申请栈上内存。

POSIX和C99均未定义alloca()的行为,在有些平台下不支持该函数,使用alloca会降低程序的兼容性和可移植性,该函数在栈帧里申请内存,申请的大小很可能超过栈的边界,影响后续的代码执行。

请使用malloc从堆中动态分配内存。

正确示例:

使用malloc()函数

// 使用malloc()函数
char *newPtr = (char *)malloc(NEW_SIZE);
if (newPtr == NULL) {
  ... // 错误处理
}
errno_t ret = memcpy_s(newPtr, NEW_SIZE, oldPtr, oldSize);
... // 校验ret,确保安全函数执行成功
... // 返回前,释放oldPtr

错误示例:

使用了alloca函数

void use_alloca()
{
    /* POTENTIAL FLAW: Forbid use alloca() */
    int *p = (int *)alloca(sizeof(int) * 10); 
    free(p); 
    return;
}

修改建议:

禁止调用alloca函数,请使用malloc从堆中动态分配内存。


18. 规则名称:禁止头文件循环依赖

问题级别:严重

规则描述:

头文件循环依赖,指 a.h 包含 b.h,b.h 包含 c.h,c.h 包含 a.h, 导致任何一个头文件修改,都导致所有包含了a.h/b.h/c.h的代码全部重新编译一遍。
而如果是单向依赖,如a.h包含b.h,b.h包含c.h,而c.h不包含任何头文件,则修改a.h不会导致包含了b.h/c.h的源代码重新编译。

头文件循环依赖直接体现了架构设计上的不合理,可通过优化架构去避免。

修改建议:

重构代码逻辑,避免安全隐患


19. 规则名称:禁用pthread_exit、ExitThread函数【C】

问题级别:严重

规则描述:

禁用pthread_exit、ExitThread函数。

严禁在线程内主动终止自身线程,线程函数在执行完毕后会自动、安全地退出。主动终止自身线程的操作,不仅导致代码复用性变差,同时容易导致资源泄漏错误。

pthread_exit()函数将终止调用它的线程,线程终止时不释放任何应用程序可见的进程资源,包括但不限于互斥锁和文件描述符,也不执行任何进程级别的清理操作。

windows环境下ExitThread用于使当前正在运行的线程正常且干净地停止。运行该函数可能导致主线程退出,而其他线程仍然在执行。停止线程的正确方法是发出干净的退出信号,然后允许线程自行退出,然后再允许主线程退出。

错误示例:

void test_bad_case()
{
    /* POTENTIAL FLAW: Forbid using the thread exit function */
    pthread_exit();
}

修改建议:

不要调用pthread_exit、ExitThread函数


20. 规则名称:删除无效或永不执行的代码

问题级别:严重

规则描述:

无效或永不执行的代码(如:死代码、恒真/恒假的表达式、空代码块、未被引用的代码、被注释掉的代码等)一般是由于编程错误、重构而未完全删除、逻辑考虑不当等原因导致的结果,它降低了代码的可读性并增加了维护成本,且有时会发生意外行为。

虽然在编译时,编译器有时会对此类代码进行优化,且大多数现代编译器在许多情况下可以对无效或从不执行的代码告警,但为提高可读性并确保解决逻辑错误,应该主动对其进行识别并删除。

以被注释掉的代码为例,它们无法被正常维护;当企图恢复使用这段代码时,极有可能引入易被忽略的缺陷。
正确的做法是,直接删除掉。若再需要时,考虑移植或重写这段代码。
这里说的“注释掉”的方式,除了/* *///、还包括#if 0#ifdef NEVER_DEFINED等,但注释中的代码示例不属于被注释掉的代码。

正确示例:

场景1:return之后的代码永不执行
  • 修复示例:
// 符合
int Foo() {
    return 0;
}
场景2:goto之后的代码永不执行
  • 修复示例:
// 符合
int Foo(int x, int y)
{
    y++;
    if (x < 10) {
        y++;
        return y;
    }
    return x;
}
场景3:无条件的break导致循环只执行一次
  • 修复示例:
// 符合
void Foo(const char *array, uint32_t len)
{
    for (uint32_t i = 0; i < len; i++) {
        if (array[i] > LEN_MAX) {
            array[i] = 0;
        }
    }
}

错误示例:

场景1:return之后的代码永不执行
  • 错误示例:
int Foo() {
    return 0;
    Bar(); // 不符合:该行及以下代码永不执行
    return x;
}
场景2:goto之后的代码永不执行
  • 错误示例:
int Foo(int x, int y)
{
    y++;
    goto out;
    if (x < 10) { // 不符合:该行及以下代码永不执行
        y++;
        return y;
    }
out:
    return x;
}
场景3:无条件的break导致循环只执行一次
  • 错误示例:
void Foo(const char *array, uint32_t len)
{
    for (uint32_t i = 0; i < len; i++) {
        if (array[i] > LEN_MAX) {
            array[i] = 0;
        }
        break; // 不符合:循环只执行一次就退出,循环递增i++没有执行过
    }
}

修改建议:

删除不可执行代码


21. 规则名称:禁止包含用不到的头文件

问题级别:严重

规则描述:

用不到的头文件被包含的同时引入了不必要的依赖,增加了模块或单元之间的耦合度,只要该头文件被修改,代码就要重新编译。

有些库会提供统一的API入口头文件,如一个名为X的库提供libX.h供外部使用,该头文件中又进一步包含多个子模块的头文件,这种情况下允许直接引用libX.h,即便其中某些子模块的API是使用不到的。此种情况不认为是包含了用不到的头文件,除非libX.h中的功能完全没有用到。

该规则有可配置参数,具体配置详见规则集中该规则的选项配置。

修改建议:

  • 删除头文件
  • 按需包含头文件

22. 规则名称:不用的代码段直接删除,不要注释掉

问题级别:严重

规则描述:

被注释掉的代码,无法被正常维护,因此如果代码不再使用,请直接删除,不要注释掉。

正确示例:

场景1:用//注释不用的代码段
  • 修复示例:
#define MAX_SIZE

void Foo()
{
    int x;
    ...
    // 符合:直接删除注释的代码
    x = 0;
    ...
}
场景2:用#if 0注释不用的代码段
  • 修复示例:
// 符合:直接删除注释的代码
#if defined(__arm64__) && __arm64__
typedef uint_least16_t char16_t;
typedef uint_least32_t char32_t;
#endif

错误示例:

场景1:用//注释不用的代码段
  • 错误示例:
#define MAX_SIZE

void Foo()
{
    int x;
    ...
    // 不符合:注释的代码
    // x = MAX_SIZE;
    x = 0;
    ...
}
场景2:用#if 0注释不用的代码段
  • 错误示例:
// 不符合:注释的代码
#if 0
typedef uint_least16_t char16_t;
#elif defined(__arm64__) && __arm64__
typedef uint_least16_t char16_t;
typedef uint_least32_t char32_t;
#endif

修改建议:

不用的代码段直接删除掉。若再需要时,考虑移植或重写这段代码。 这里说的“注释掉”的方式,除了/* *///、还包括#if 0#ifdef NEVER_DEFINED等,但注释中的代码示例不属于被注释掉的代码。


23. 规则名称:禁止使用realloc()函数【C】

问题级别:严重

规则描述:

禁止使用realloc()函数。

realloc()是一个非常特殊的函数,原型如下:

void *realloc(void *ptr, size_t size);

随着参数的不同,其行为也是不同:

  • 当ptr不为NULL,且size不为0时,该函数会重新调整内存大小,并将新的内存指针返回,并保证最小的size的内容不变;
  • 参数ptr为NULL,但size不为0,那么其行为等同于malloc(size);
  • 参数size为0,则realloc的行为等同于free(ptr)。

由此可见,一个简单的C函数,却被赋予了3种行为,这不是一个设计良好的函数。虽然在编程中提供了一些便利性,如果认识不足,使用不当,是却极易引发各种bug。

正确示例:

使用malloc()函数代替realloc()函数

// 使用malloc()函数代替realloc()函数
char *newPtr = (char *)malloc(NEW_SIZE);
if (newPtr == NULL) {
  ... // 错误处理
}

errno_t ret = memcpy_s(newPtr, NEW_SIZE, oldPtr, oldSize);
... // 校验ret,确保安全函数执行成功

... // 返回前,释放oldPtr

错误示例:

如下代码示例中,使用realloc不当导致内存泄漏。

代码中希望对ptr的空间进行扩充,当realloc()分配失败的时候,会返回NULL。但是参数中的ptr的内存是没有被释放的,如果直接将realloc()的返回值赋给ptr,那么ptr原来指向的内存就会丢失,造成内存泄漏

// 当realloc()分配内存失败时会返回NULL,导致内存泄漏
char *ptr = (char *)realloc(ptr, NEW_SIZE);
if (ptr == NULL) {
  .. // 错误处理
}

修改建议:

禁止调用realloc函数


24. 规则名称:头文件必须采取保护措施,防止重复包含

问题级别:严重

规则描述:

可以使用#define宏和#pragma once两种方式防止头文件重复包含,项目组可自行决策选择使用一种方式,鼓励使用方式统一。对于#pragma once,使用时注意其适用条件。

鉴于#pragma once有一定的适用条件,本条款推荐优先使用保护宏的方式。

方式一:使用保护宏

定义包含保护符时,应该遵守如下规则: 1)保护符使用唯一名字; 2)不要在受保护部分的前后放置代码或注释,文件头注释除外。

方式二:使用#pragma once 使用#pragma once可以简化保护功能的书写,避免宏撞名。采用这种方式进行保护,要注意所采用的编译器是否支持#pragma once的识别。

#pragma once  // 当前文件不会被重复包含

适用的编译器版本:

  • GCC 3.4 及之后的版本
  • CLANG 所有版本
  • Microsoft Visual C++ 4.2 及之后的版本
  • 其他:略

注意:#pragma once保护的是物理上的同一个文件不被重复包含。如果内容相同的文件被放置到不同位置(例如文件复制),#pragma once会认为是两个不同的文件。此时要注意#pragma once的行为是否是自己所需

该规则有可配置参数,具体配置详见规则集中该规则的选项配置。

正确示例:

假定VOS工程的timer模块的timer.h,其目录为VOS/include/timer/Timer.h,应按如下方式保护:

#ifndef VOS_INCLUDE_TIMER_TIMER_H
#define VOS_INCLUDE_TIMER_TIMER_H
...
#endif

也可以不用像上面添加路径,但是要保证当前工程内宏是唯一的。

#ifndef TIMER_H
#define TIMER_H
...
#endif

修改建议:

添加合适的#define保护头文件


25. 规则名称:确保除法和余数运算不会导致除零错误(被零除)【C】

问题级别:严重

规则描述:

除零错误。

整数的除法和取余运算的第二个操作数值为0会导致程序产生未定义的行为,因此使用时要确保整数的除法和余数运算不会导致除零错误(被零除,下同)。

正确示例:

如下代码示例中,添加numB是否为0的校验,防止除零错误。

int numA = ... // 来自外部数据
int numB = ... // 来自外部数据
int result = 0;
if ((numB == 0) || ((numA == INT_MIN) && (numB == -1))) {
    ... // 错误处理
}
result = numA / numB;
...

错误示例:

有符号整数类型的除法运算如果限制不当,会导致溢出。如下示例对有符号整数进行的除法运算做了防止溢出限制,确保不会导致溢出,但不能防止有符号操作数numA和numB之间的除法过程中出现除零错误。

int numA = ... // 来自外部数据
int numB = ... // 来自外部数据
int result = 0;
if ((numA == INT_MIN) && (numB == -1)) {
  ... // 错误处理
}
result = numA / numB; // 可能出现除零错误
...

修改建议:

如果除数为外部变量,则需要在使用前进行非零校验


26. 规则名称:goto语句只能向下跳转【C】

问题级别:严重

规则描述:

使用向上跳转的goto语句会导致代码可读性差、程序难以调试等问题。

因此,如果一定要使用goto语句,goto语句只能向下跳转。

正确示例:

场景1:使用向上跳转的goto语句
  • 修复示例:
void Func(void)
{
    int loopCnt = 0;

    while(loopCnt++) {
        if (loopCnt == MAX_COUNT) {
            goto LOOP2; // 符合:只能向下跳转
        }
        ...
    }
    ...

LOOP2:
    ...
}

错误示例:

场景1:使用向上跳转的goto语句
  • 错误示例:
void Func(void)
{
    int loopCnt = 0;

LOOP1:
    loopCnt++;
    if (loopCnt < MAX_COUNT) {
        goto LOOP1;     // 不符合:向上跳转
    } else {
        ...
    }
    ...

}

修改建议:

  1. 除了必须使用goto的场景(如在函数末尾进行错误处理)外,不应使用goto语句,可使用结构化的控制语句(如if、for、while等)来实现程序逻辑。
  2. 必须使用goto的场景,goto语句不要向上跳转,只能向下跳转。

27. 规则名称:禁止代码中包含公网地址

问题级别:严重

规则描述:

禁止代码中包含公网地址

对产品发布的软件(包含软件包/补丁包)中包含的公网地址(包括公网IP地址、公网URL地址/域名、邮箱地址)要求如下:

  1. 禁止包含用户界面不可见、或产品资料未描述的未公开的公网地址。

  2. 已公开的公网地址禁止写在代码或者脚本中,可以存储在配置文件或数据库中。

对于开源/第三方软件自带的公网地址必须至少满足上述第1条公开性要求。

错误示例:

变量赋值中包含公网IP

void IP_Hard_Coded()
{
   /* POTENTIAL FLAW: 禁止使用Ipv4公网IP */
   const char *data = "189.179.169.159";
   printf("%s", data);
}

修改建议:

宏定义中、变量赋值中、特定函数(*strcmp、*strncmp、*memcmp)中禁止使用硬编码公网IP


28. 规则名称:禁用abort函数【C】

问题级别:严重

规则描述:

禁用abort函数。

abort()将会导致程序立即退出,资源得不到清理。因为在使用abort()退出程序时,不能保证输入/输出流已经被清空,文件已经正确关闭,或者临时文件已经删除。

正确示例:

场景1:调用abort()函数
  • 修复示例1:当异常发生后,不调用abort(),处理错误后,通过函数返回的错误值告知上游运行异常,逐层处理异常,最后在main函数正常退出。
int32_t TestGoodCase01()
{
    int32_t age;
    scanf("%d", &age);

    // 外部输入数据不合法
    if (age < 0 || age > 150) {
        ... // 错误处理
        // 符合:用返回值逐层退出并处理错误
        return -1;
    }

    ...
    return 0;
}
  • 修复示例2:只有发生致命错误,程序无法继续执行的时候,在错误处理函数中使用abort退出程序。但必须在模型中配置错误处理函数的名称,详见下方的[模型/参数配置]。
void FatalError(int sig)
{
    ... // 错误处理
    // 必须在模型中,将FatalError函数配置成程序无法继续执行时的错误处理函数
    abort();
}

int32_t TestGoodCase02()
{
    int32_t age;
    scanf("%d", &age);
    ...
    signal(SIGSEGV, FatalError);
    ...
    return 0;
}

错误示例:

场景1:调用abort()函数
  • 错误示例:
int32_t TestBadCase01()
{
    int32_t age;
    scanf("%d", &age);

    // 外部输入数据不合法
    if (age < 0 || age > 150) {
        // 不符合:禁止在非错误处理函数中,调用abort函数
        abort();
    }

    ...
    return 0;
}

修改建议:

禁用abort函数


29. 规则名称:处理函数的返回值【C】

问题级别:一般

规则描述:

函数通常通过返回值返回数据或执行结果,调用者应该在函数调用之后,对返回数据、执行结果进行及时、有效的合法性检查和处理。

如果对函数返回值进行了无效或错误的处理,可能会导致程序执行的结果不符合预期。

正确示例:

场景1:未处理malloc函数返回值
  • 修复示例:
char *p = (char *)malloc(SOME_SIZE);
if (p == NULL) {    // 符合: 对返回值进行合法性检查
    ...
    return ...;
}
// 符合: 此处符合可省略memset_s返回值检查的例外场景,可以显式忽略掉返回值的检查
(void)memset_s(p, SOME_SIZE, 0, SOME_SIZE);
场景2:未处理fopen函数返回值
  • 修复示例:
FILE *fp = fopen(filePath, "r");
if (fp == NULL) {               // 符合: 对返回值进行合法性检查
    ...
    return ...;
}
char ch = fgetc(fp);
...
fclose(fp);

错误示例:

场景1:未处理malloc函数返回值
  • 错误示例:
char *p = (char *)malloc(SOME_SIZE);
memset_s(p, SOME_SIZE, 0, SOME_SIZE);   // 不符合: 未检查返回值 p 的合法性就直接使用。
场景2:未处理fopen函数返回值
  • 错误示例:
FILE *fp = fopen(filePath, "r"); // 不符合:未检查返回值的合法性
char ch = fgetc(fp);
...
fclose(fp);

修改建议:

  1. 对返回值进行及时、正确的处理。
  2. 如果调用者有意不处理返回值,在经过充分考虑之后,可用(void)显式忽略掉。

30. 规则名称:换行时将操作符留在行末,新行缩进一层或进行同类对齐--函数声明参数换行

问题级别:一般

规则描述:

当语句过长,或者可读性不佳时,需要在合适的地方换行。
换行时将操作符放在行末,表示“未结束,后续还有”。新行缩进一层,或者保持同类对齐。

调用函数的参数列表换行时,左圆括号总是跟函数名,右圆括号总是跟最后一个参数,并进行合理的参数对齐。

一般选择在较低优先级操作符后换行,或者根据表达式层次换行。

  1. 长表达式
// 假设下面第一行已经不满足行宽要求  
if (currentValue > MIN &&                // 符合:换行后,布尔操作符放在行末  
    currentValue < MAX) {                // 符合:与(&&)操作符的两个操作数同类对齐  
    DoSomething();
    ...
}

flashPara.endAddr = flashPara.baseAddr + // 符合:加号留在行末
                    flashPara.size;      // 符合:加法两个操作数对齐

int sum = longVaribleName1 + longVaribleName2 + longVaribleName3 +
    longVaribleName4 + longVaribleName5 + longVaribleName6;        // 符合:4空格缩进
  1. 函数参数列表
ReturnType result = FunctionName(paramName1, paramName2); // 符合:满足行宽不换行

ReturnType result = FunctionName(paramName1,
                                 paramName2,
                                 paramName3);             // 符合:保持与上方参数对齐

ReturnType result = FunctionName(paramName1, paramName2,  
    paramName3, paramName4, paramName5);                  // 符合:参数换行,4 空格缩进  

ReturnType result = VeryVeryVeryLongFunctionName(         // 符合:第1个参数直接换行  
    paramName1, paramName2, paramName3);                  // 符合:换行后,4 空格缩进  

如果函数的参数存在内在关联性,按照可理解性优先于格式排版要求,对参数进行合理分组换行。

// 符合:每行的参数代表一组相关性较强的数据结构,放在一行便于理解  
int result = DealWithStructLikeParams(left.x, left.y,     // 表示一组相关参数
                                      right.x, right.y);  // 表示另外一组相关参数 

正确示例:

场景1:函数声明时参数列表换行时未缩进对齐
  • 修复示例1:
VOS_UINT32 Download(VOS_UINT32 type, VOS_CHAR *pData,
    VOS_UINT32 dataLen, VOS_UINT32 dataNum) // 符合:相对上一行缩进4空格
  • 修复示例2:
VOS_UINT32 Download(VOS_UINT32 type, VOS_CHAR *pData,
                    VOS_UINT32 dataLen, VOS_UINT32 dataNum) // 符合:与上方参数对齐

错误示例:

场景1:函数声明时参数列表换行时未缩进对齐
  • 错误示例:
VOS_UINT32 Download(VOS_UINT32 type, VOS_CHAR *pData,
 VOS_UINT32 dataLen, VOS_UINT32 dataNum) // 不符合:换行时没有缩进对齐

修改建议:

按要求进行对齐。


31. 规则名称:使用统一的命名风格【C】

问题级别:一般

规则描述:

驼峰风格(CamelCase) 大小写字母混用,单词连在一起,不同单词间通过单词首字母大写来分开。 按连接后的首字母是否大写,又分: 大驼峰(UpperCamelCase)小驼峰(lowerCamelCase)

内核风格(unix_like) 又称蛇形风格(snake_case)。单词全小写,用下划线分割。 如:'test_result'

匈牙利风格 在“大驼峰”的基础上,加上类型或用途前缀 如:'uiSavedCount', 'bTested'

标识符命名风格的选择由产品自行决策。遵循:

  • 推荐使用驼峰风格,具体描述详见下文
  • 对于更亲和 Linux/Unix 的代码,可以使用内核风格
  • 已使用内核命名风格的代码,可以选择继续使用内核风格
  • 基于开源或外部代码进行开发或维护时,可以保持原有命名风格
  • 基于产品或版本生命周期以及人力投入考虑,可以保持存量代码的匈牙利风格不变
  • 对于仍活跃的使用了匈牙利风格的的产品或版本,建议制定合理可行的整改策略,过渡至驼峰风格
  • 不管什么样的命名风格整改策略,都应该保证同一函数或结构体、联合体内的命名风格是一致的

当前条款推荐的“驼峰风格”具体规则如下:

类别 命名风格 形式
函数,结构体类型,枚举类型,联合体类型,typedef 定义的类型 大驼峰,或带模块前缀的大驼峰 AaaBbb, XXX_AaaBbb
局部变量,函数参数,宏参数,结构体中字段,联合体中成员 小驼峰 aaaBbb
全局变量(在函数外部定义的变量) 带 'g_' 前缀的小驼峰 g_aaaBbb
宏(不包括函数式宏),枚举值,goto 标签 全大写,下划线分割 AAA_BBB
函数式宏 全大写下划线分割,或大驼峰,或带模块前缀的大驼峰 AAA_BBB, AaaBbb, XXX_AaaBbb
常量(在函数外部定义由const修饰的基本数据类型、枚举类型、字符串类型) 全大写下划线分割,或带 'g_' 前缀的小驼峰 AAA_BBB, g_aaaBbb

关于“模块前缀”

  • 仅大驼峰命名风格的符号,可选加模块前缀。
  • 模块前缀尽量简短且不超过2级(XXX_YYY_AaaBbb, XxxYyyAaaBbb)。
  • 应保持风格统一,只选用一种前缀形式(仅选用XXX_AaaBbb,或仅选用XxxAaaBbb)。

关于“函数式宏”

  • 函数式宏的命名风格优先与宏一样,采用“全大写下划线分割”。
  • 特殊场景,允许将函数式宏的命名风格与函数一样,但这种情况应该是极少的。比如:
#ifdef SOME_DEFINE
void Bar(int);
#define Foo(a) Bar(a) // 特殊场景,用大驼峰风格命名函数式宏
#else
void Foo(int);  // 函数命名风格
#endif

关于单词缩写

  • 单词缩写应当作单个单词处理,以提高可读性。比如对包含HTTP(HyperText Transfer Protocol)缩写的函数命名如下:
int GetActiveHttpClientCnt(void);

该规则有可配置参数,具体配置详见规则集中该规则的选项配置。

正确示例:

场景1:选择驼峰命名风格,但是使用时没有遵循
  • 修复示例:遵循驼峰命名风格。
struct StrType {                            // 符合:结构体类型使用“大驼峰”
    int typeMember;                         // 符合:结构体中字段使用“小驼峰”
};

void Compare(int leftVal, int rightVal);    // 符合:函数大驼峰,参数小驼峰

char * const VERSION = "V100";              // 符合:常量用全大写下划线分割
场景2:全局变量选择驼峰命名风格,但是使用时没有遵循
  • 修复示例:
int g_variable;             // 符合:全局变量,带g_前缀的小驼峰

错误示例:

场景1:选择驼峰命名风格,但是使用时没有遵循
  • 错误示例:
struct strType {                            // 不符合
    int Type_Member;                        // 不符合
};

void compare(int left_val, int right_val);  // 不符合

char * const version = "V100";              // 不符合
场景2:全局变量选择驼峰命名风格,但是使用时没有遵循
  • 错误示例:
int Global_variable = 7;    // 不符合

修改建议:

修改标识符名称。


32. 规则名称:整型表达式比较或赋值为一种更大类型之前必须用这种更大类型对它进行求值【C】

问题级别:一般

规则描述:

类型转换时导致的整型溢出。

由于整数在运算过程中可能出现有符号整数溢出、无符号整数回绕等问题,当运算结果赋值给比它更大的类型,或者与比它更大的类型进行比较时,可能会导致实际结果与预期结果不符。

如果将涉及某个操作的整数表达式与较大的整数大小进行比较或分配给较大的整数,则该整数表达式应该通过显式转换其中一个操作数来以较大类型的大小进行计算。

类似的,当组合表达式的运算结果赋值给比它更大类型,或者与比它更大类型进行运算时,应显式转换其中一个操作数为较大的类型。本规范中组合表达式指的是由如下操作符组成的表达式:

  1. 乘除取模(*、 /、 %)
  2. 加减(二元+、 二元-)
  3. 位运算(&、 |、 ^)
  4. 移位(<<、 >>)
  5. 三元表达式(?:)如果第二个操作数和第三个操作数都是组合表达式

在int为32位,long long为64位并以二进制补码表示整数的系统中,请观察以下二个代码及其输出,以便了解本规则所解决的问题:

int main(int argc, char *argv[])
{
    unsigned int a = 0x10000000;
    unsigned long long b = a * 0xab;
    printf("b = %llX\n", b);
    return 0;
}

输出:

b = B0000000
int main(int argc, char *argv[])
{
    unsigned int a = 0x10000000;
    unsigned long long b = (unsigned long long )a * 0xab;
    printf("b = %llX\n", b);
    return 0;
}

输出:

b = AB0000000

正确示例:

场景1:算术运算(加法)表达式的值可能在结果被赋值给较大的数据类型之前溢出
  • 修复示例:
int OverflowBeforeIntegerWidenCombinationGoodCase01()
{
    uint64_t a = 0xffffffffU;
    uint64_t b = 1;
    uint64_t c = 2;
    /* POTENTIAL FLAW GOOD: 最佳做法是修改变量类型,保持表达式中的操作数类型一致 */
    uint64_t x = a + b + c;
    return 0;
}

int OverflowBeforeIntegerWidenCombinationGoodCase02()
{
    uint32_t a = 0xffffffffU;
    uint32_t b = 1;
    uint64_t c = 2;
    /* POTENTIAL FLAW GOOD: 将这个整数表达式中的一个操作数显式转换为较大的整数类型 */
    uint64_t y = (uint64_t)a + (uint64_t)b + c;
    return 0;
}
场景2:算术运算(乘法)表达式的值可能在结果被赋值给较大的数据类型之前溢出
  • 修复示例:
int OverflowBeforeIntegerWidenGoodCase01(void)
{
    uint64_t a = 0x10000000;
    /* POTENTIAL FLAW GOOD: 最佳做法是修改变量类型,保持表达式中的操作数类型一致 */
    uint64_t b = a * 0xab;
    printf("b = %llX\n", b);
    return 0;
}

int OverflowBeforeIntegerWidenGoodCase02(void)
{
    uint32_t a = 0x10000000;
    /* POTENTIAL FLAW GOOD: uint32_t类型变量a乘法之前已经强转为uint64_t类型 */
    uint64_t b = (uint64_t)a * 0xab;
    printf("b = %llX\n", b);
    return 0;
}
场景3:算术运算(乘法)表达式的值可能在结果与较大的数据类型比较之前溢出
  • 修复示例:
void OverflowBeforeIntegerWidenGoodCase03()
{
    uint64_t x = 2147483648;
    uint64_t y = 2;
    uint64_t z = 0;
    /* POTENTIAL FLAW GOOD: 最佳做法是修改变量类型,保持表达式中的操作数类型一致 */
    if ((x * y) < z) {
        x = 0;
        y = 0;
    }
}

void OverflowBeforeIntegerWidenGoodCase03()
{
    uint32_t x = 2147483648;
    uint32_t y = 2;
    uint64_t z = 0;
    /* POTENTIAL FLAW GOOD: 将这个整数表达式中的一个操作数显式转换为较大的整数类型 */
    if (((uint64_t)x * (uint64_t)y) < z) {
        x = 0;
        y = 0;
    }
}

错误示例:

场景1:算术运算(加法)表达式的值可能在结果被赋值给较大的数据类型之前溢出
  • 错误示例:
int OverflowBeforeIntegerWidenCombinationBadCase01()
{
    uint32_t a = 0xffffffffU;
    uint32_t b = 1;
    uint64_t c = 2;
    /* POTENTIAL FLAW: 表达式 a + b + c 等同于表达式 (a + b) + c ,计算组合表达式 (a + b) 时先发生整数回绕 */
    uint64_t x = a + b + c;
    return 0;
}
场景2:算术运算(乘法)表达式的值可能在结果被赋值给较大的数据类型之前溢出
  • 错误示例:
int OverflowBeforeIntegerWidenBadCase07()
{
    uint32_t a = 0x10000000;
    /* POTENTIAL FLAW: uint32_t类型乘法赋值和uint64_t类型 */
    uint64_t b = a * 0xab;
    printf("b = %llX\n", b);
    return 0;
}
场景3:算术运算(乘法)表达式的值可能在结果与较大的数据类型比较之前溢出
  • 错误示例:
// @scene 两个int型变量做乘法之后与longlong类型变量做<比较
void OverflowBeforeIntegerWidenBadCase03()
{
    uint32_t  x = 2147483648;
    uint32_t  y = 2;
    uint64_t z = 0;
    /* POTENTIAL FLAW: x * y 超过了目标平台的 unsigned 类型允许的最大值(2^32    1,即 4,294,967,295) */
    if ((x * y) < z) {
        x = 0;
        y = 0;
    }
}

修改建议:

当组合表达式的运算结果赋值给比它更大类型,或者与比它更大类型进行运算时,应显式转换其中一个操作数为较大的类型。


33. 规则名称:用空格突出关键字和重要信息--函数名

问题级别:一般

规则描述:

空格应该突出关键字和重要信息。总体建议如下:

  • 行末不应加空格
  • if, switch, case, do, while, for 等关键字之后加空格;#include之后加空格
  • 小括号内部的两侧,不加空格;外部两侧与关键字或重要信息之间加空格
  • 无论大括号内部两侧有无空格,左右要保持一致;外部两侧与关键字或重要信息之间加空格
  • 使用大括号进行初始化时,左侧大括号跟随类型或变量名时不加空格
  • 一元操作符(& * + ‐ ~ !)之后不应加空格
  • 二元操作符(= + ‐ < > * / % | & ^ <= >= == !=)两侧都应加空格
  • 三元操作符(? :)符号两侧都应加空格
  • 前置和后置的自增、自减操作符(++ --)和变量之间不应加空格
  • 尾置返回类型语法中的->前后加空格
  • 结构体成员操作符(. ->)前后不应加空格
  • 结构体中表示位域的冒号,两侧都应加空格
  • 函数参数列表的小括号与函数名之间不应加空格
  • 类型强制转换的小括号与被转换对象之间不应加空格
  • 数组的中括号与数组名之间不应加空格
  • 对于模板和类型转换(<>)和类型之间不加空格
  • 域操作符(::)前后不加空格
  • 类成员指针(::* .* ->*)前后或中间不加空格
  • 类继承、构造函数初始化、范围for语句中的冒号(:)前后加空格
  • 逗号、分号、冒号(上述规则场景除外)前面不加空格,后面加空格

初始化语句

std::string str{"Hello, World"};            // 符合:左大括号和变量名之间不加空格
std::vector<int> v{1, 2, 3};                // 符合:大括号内部两侧都没有空格
std::array<std::string, 2> arr{ "a", "b" }; // 符合:大括号内部两侧都有空格
int buf[BUF_SIZE] = {0};                    // 符合:大括号内部两侧都没有空格
int i = 0;                                  // 符合:等号前后应该有空格,分号前面不要留空格
Foo({1, 2, 3}, 0);                          // 符合:作为实参,和普通实参的空格要求一致
int arr[] = { 10, 20};                      // 不符合:大括号内部两侧空格不一致

函数定义和函数调用

// 符合:大括号换行,换行前没有空格
void Foo(int b)
{
    ...
}

int result = Foo(arg1,arg2);    // 不符合: 逗号后面应该有空格        
int result = Foo( arg1, arg2 ); // 不符合: 小括号内部两侧不应该有空格    

指针和取地址

x = *p;            // 符合:*操作符和指针p之间不加空格  
p = &x;            // 符合:&操作符和变量x之间不加空格  
x = r.y;           // 符合:通过.访问成员变量时不加空格  
x = r->y;          // 符合:通过->访问成员变量时不加空格  

操作符

x = 0;             // 符合:赋值操作的=前后都要加空格  
x = -5;            // 符合:负数的符号之前要加空格  
++x;               // 符合:前置和后置的++/--和变量之间不要加空格  
x--;

if (x && !y)       // 符合:布尔操作符前后要加上空格,!操作符和变量之间不要空格  
v = w * x + y / z; // 符合:二元操作符前后要加空格  
v = w * (x + z);   // 符合:括号内的表达式前后不需要加空格  

循环和选择语句

if (condition) {   // 符合:if关键字和括号之间加空格,括号内表达式前后不加空格  
    ...
} else {           // 符合:else关键字和大括号之间加空格  
    ...
}

// 符合:while关键字和括号之间加空格,括号内表达式前后不加空格  
while (condition) {}
do {} while (condition);

// 符合:for关键字和括号之间加空格,分号之后加空格  
for (int i = 0; i < someRange; ++i) {
    ...
}

switch (var) {     // 符合:switch 关键字后面有1空格  
    case CASE1:    // 符合:case语句条件和冒号之间不加空格  
        ...
        break;
    ...
    default:
        ...
        break;
}

模板和转换

// 符合:尖括号(`<` 和 `>`) 不与空格紧邻, `<` 前没有空格, `>` 和 `(` 之间也没有
std::vector<std::string> x;
y = static_cast<char*>(x);

域操作符

std::cout;                         // 符合:命名空间访问,不要留空格

int SomeClass::GetValue() const {} // 符合:对于成员函数定义,不要留空格

冒号(添加空格的场景)

// 符合:类的派生需要留有空格
class Derived : public Base {
    ...
};

// 符合:构造函数初始化列表需要留有空格
SomeClass::SomeClass(int var) : someVar(var)
{
    DoSomething();
}

// 符合:位域表示也留有空格
struct SomeType {
    char a : 4;
    char b : 5;
    char c : 4;
};

// 符合:范围for语句中的冒号两边留有空格
for (auto& data : dataList) {
    ...
}

冒号(不添加空格的场景)

// 对于public:, private:这种类访问权限的冒号不添加空格
class SomeClass {
public:
    SomeClass(int var);

private:
    int someVar;
};

// 对于switch-case的case和default后面的冒号不添加空格
switch (var) {
    case CASE1:
        DoSomething1();
        break;
    case CASE2:
        DoSomething2();
        break;
    default:
        break;
}

正确示例:

场景1:函数名后面多空格
  • 修复示例:移除函数名后的空格
int main(int argc, char **argv);          // 符合

错误示例:

场景1:函数名后面多空格
  • 错误示例:
int main (int argc, char **argv);          // 不符合:main 和 ( 之间有空格

修改建议:

按要求增加/减少空格。


34. 规则名称:函数式宏要简短

问题级别:一般

规则描述:

函数式宏本身的一大问题是比函数更难以调试和定位,特别是宏过长,调试和定位的难度更大。

而且宏扩展会导致目标代码膨胀。建议函数式宏不要超过10行(非空非注释)。

正确示例:

// 符合: 将函数式宏改为函数
void BubbleSort(int arr[], int n)
{
    int i, j;
    for (i = 0; i < n - 1; i++) {
        for (j = 0; j < n - i - 1; j++) {
            if (arr[j] > arr[j + 1]) {
                int temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
    }
}

错误示例:

// 不符合: 函数式宏超过10行
#define BUBBLESORT(arr, n)                \
    int i, j;                             \
    for (i = 0; i < n - 1; i++) {         \
        for (j = 0; j < n - i - 1; j++) { \
            if (arr[j] > arr[j + 1]) {    \
                int temp = arr[j];        \
                arr[j] = arr[j + 1];      \
                arr[j + 1] = temp;        \
            }                             \
        }                                 \
    }

修改建议:

减少函数式宏的复杂度或者改成函数。


35. 规则名称:注释符与注释内容间留有1个空格

问题级别:一般

规则描述:

注释符与注释内容之间留有1个空格。 注释符包括: /* */和 //。 注释符的对齐方式保持与下面示例相同:

使用/* */

/* 单行注释 */
/*
 * 多行注释
 * 第二行
 */

使用//

// 单行注释  
// 多行注释
// 第二行

在多行注释中为了换行对齐,可以在注释符与注释内容之间存在多个空格:

// 这是一段注释符与注释内容之间存在多个空格的例子:
// XX 功能说明
// 提示:提示信息,超出一行行宽时需要换行
//       这是第二行,前面使用空格对齐

【例外】
若项目组选用如 doxygen 来基于注释生成代码文档,则应由项目组制定统一的各类广义的注释符及注释风格。

修改建议:

按要求增加或减少空格。


36. 规则名称:禁用rand函数产生用于安全用途的伪随机数【C】

问题级别:一般

规则描述:

禁用rand函数产生用于安全用途的伪随机数

C语言标准库rand()函数生成的是伪随机数,所以不能保证其产生的随机数序列质量。根据C11标准,rand()函数产生的随机数范围是 [0, RAND_MAX(0x7FFF)] ,因为范围相对较短,所以这些数字可以被预测。

所以禁止使用rand()函数产生的随机数用于安全用途,必须使用安全的随机数产生方式,如:Linux操作系统的/dev/random设备接口,Windows操作系统的CryptGenRandom接口。

典型的安全用途场景包括(但不限于)以下几种:

  • 会话标识SessionID的生成;
  • 挑战算法中的随机数生成;
  • 验证码的随机数生成;
  • 用于密码算法用途(例如用于生成IV、盐值、密钥等)的随机数生成。

正确示例:

在类Unix平台上,可以使用/dev/random文件得到随机数。需要注意的是,设备刚启动时,由于硬件输入的熵可能不足,读取该接口可能产生阻塞问题。

错误示例:

程序员期望生成一个唯一的不可被猜测的HTTP会话ID,但该ID是通过调用rand()函数产生的数字随机数,它的ID是可猜测的,并且随机性有限。

修改建议:

  1. 加解密场景必须使用安全随机数(非加解密场景可按误报处理);
  2. IPSI组件的CRYPT_random,须确保开启了NIST SP 800-90A标准的DRBG后才可使用。

37. 规则名称:非纯ASCII码源文件使用UTF-8编码

问题级别:一般

规则描述:

对于含有非ASCII字符的源文件,使用UTF-8编码。

修改建议:

使用UTF-8编码。


38. 规则名称:使用统一的大括号换行风格

问题级别:一般

规则描述:

选择并统一使用一种大括号换行风格,避免多种风格并存。

K&R风格
换行时,函数(不包括lambda表达式)左大括号另起一行放行首,并独占一行;其他左大括号跟随语句放在行末。
右大括号独占一行,除非后面跟着同一语句的剩余部分,如 do 语句中的 while,或者 if 语句的 else/else if,或者逗号、分号、小括号。

struct SomeType { // 跟随语句放行末,前置1空格  
    ...
};                // 右大括号后面紧跟分号  

typedef struct {  // 跟随语句放行末,前置1空格  
    ...
} SomeType;       // 右大括号后面加空格

int Foo(int a)  
{                 // 函数左大括号独占一行,放行首  
    if (a > 0) {  
        ...
    } else {      // 右大括号、"else"、以及后续的左大括号均在同一行  
        ...
    }             // 右大括号独占一行  
    ...
}

对于空函数体或者只有一条语句的函数体,可以将大括号放在同一行:

class SomeClass {
public:
    SomeClass() : value(0), pool(0) {}            // 空函数体
    void SetPoll(int pool) { this->pool = pool; } // 函数体只有一条语句

private:
    int value;
    int pool;
};

lambda表达式捕获列表中的逗号后留有1个空格,引用捕获时,&紧靠变量名;参数列表与函数参数列表风格一致;尾置返回类型的前后留1空格,->与类型名之间留1空格。
只有一条语句时可将大括号放在同一行;多条语句时使用K&R风格换行。

int x = 0;
int y = 0;
auto f = [x, &y](int m, int n) { y =  m * (x + n); }; // 符合:大括号内只有一条语句

// 换行时进行合理对齐
auto f = [x, &y](int m, int n) -> int {
    y =  m * (x + n);
    return y;
};

Foo([x](int datum) { return x < datum; }); // 符合:可将lambda表达式嵌入函数参数中使用

Allman风格 换行时,左大括另起并独占一行,保持与上一行相同缩进; 右大括号独占一行,除非后面跟着 do 语句中的 while,或者逗号、分号。

struct SomeType
{               // 另起并独占一行
    ...
};              // 右大括号后面紧跟分号  

int Foo(int a)  
{
    if (a > 0)
    {
        ...
    }
    else        // 前后的左右大括号均独占一行,所以 'else' 也只能独占一行
    {
        ...
    }
    ...
}

初始化语句

原则上初始化列表表达式中的大括号换行风格应当与整个项目的风格保持一致。同时,为了代码简洁,多个并列的初始化列表表达式可以写在同一行。

// 符合:满足行宽要求时不换行  
int arr[4] = {1, 2, 3, 4};
std::string name{"nobody"};
std::vector<int> numbers{1, 2, 3, 4};
// 符合:行宽较长换行时,第一个左大括号与变量放在同一行 
const int rank[] = {
    16, 16, 16, 16, 32, 32, 32, 32,
    64, 64, 64, 64, 32, 32, 32, 32
};

对于复杂结构数据的初始化,不局限某种特定的格式,以尽量清晰、紧凑为原则,保持风格一致。可参考如下格式:

int a[][4] = {
    {1, 2, 3, 4}, {2, 3, 4, 1},
    {3, 4, 1, 2}, {4, 1, 2, 3}
};
SomeType table[][4] = {
    {
        {1, 2, 3, 4}, {2, 3, 4, 1},
        {3, 4, 1, 2}, {4, 1, 2, 3}
    }, 
    {
        {5, 6, 7, 8}, {6, 7, 8, 5},
        {7, 8, 5, 6}, {8, 5, 6, 7}
    }
};

或者其他紧凑的格式:

SomeType table[][4] = {
    {
        {1, 2, 3, 4}, {2, 3, 4, 1},
        {3, 4, 1, 2}, {4, 1, 2, 3}
    }, {
        {5, 6, 7, 8}, {6, 7, 8, 5},
        {7, 8, 5, 6}, {8, 5, 6, 7}
    }
};

对于初始化语句中的大括号,遵循:

  • 左大括号放行末时,对应的右大括号需另起一行
  • 左大括号被内容跟随时,对应的右大括号也应跟随内容

该规则有可配置参数,具体配置详见规则集中该规则的选项配置。

修改建议:

使用相应的大括号换行风格。


39. 规则名称:用空格突出关键字和重要信息--二元表达式

问题级别:一般

规则描述:

空格应该突出关键字和重要信息。总体建议如下:

  • 行末不应加空格
  • if, switch, case, do, while, for 等关键字之后加空格;#include之后加空格
  • 小括号内部的两侧,不加空格;外部两侧与关键字或重要信息之间加空格
  • 无论大括号内部两侧有无空格,左右要保持一致;外部两侧与关键字或重要信息之间加空格
  • 使用大括号进行初始化时,左侧大括号跟随类型或变量名时不加空格
  • 一元操作符(& * + ‐ ~ !)之后不应加空格
  • 二元操作符(= + ‐ < > * / % | & ^ <= >= == !=)两侧都应加空格
  • 三元操作符(? :)符号两侧都应加空格
  • 前置和后置的自增、自减操作符(++ --)和变量之间不应加空格
  • 尾置返回类型语法中的->前后加空格
  • 结构体成员操作符(. ->)前后不应加空格
  • 结构体中表示位域的冒号,两侧都应加空格
  • 函数参数列表的小括号与函数名之间不应加空格
  • 类型强制转换的小括号与被转换对象之间不应加空格
  • 数组的中括号与数组名之间不应加空格
  • 对于模板和类型转换(<>)和类型之间不加空格
  • 域操作符(::)前后不加空格
  • 类成员指针(::* .* ->*)前后或中间不加空格
  • 类继承、构造函数初始化、范围for语句中的冒号(:)前后加空格
  • 逗号、分号、冒号(上述规则场景除外)前面不加空格,后面加空格

初始化语句

std::string str{"Hello, World"};            // 符合:左大括号和变量名之间不加空格
std::vector<int> v{1, 2, 3};                // 符合:大括号内部两侧都没有空格
std::array<std::string, 2> arr{ "a", "b" }; // 符合:大括号内部两侧都有空格
int buf[BUF_SIZE] = {0};                    // 符合:大括号内部两侧都没有空格
int i = 0;                                  // 符合:等号前后应该有空格,分号前面不要留空格
Foo({1, 2, 3}, 0);                          // 符合:作为实参,和普通实参的空格要求一致
int arr[] = { 10, 20};                      // 不符合:大括号内部两侧空格不一致

函数定义和函数调用

// 符合:大括号换行,换行前没有空格
void Foo(int b)
{
    ...
}

int result = Foo(arg1,arg2);    // 不符合: 逗号后面应该有空格        
int result = Foo( arg1, arg2 ); // 不符合: 小括号内部两侧不应该有空格    

指针和取地址

x = *p;            // 符合:*操作符和指针p之间不加空格  
p = &x;            // 符合:&操作符和变量x之间不加空格  
x = r.y;           // 符合:通过.访问成员变量时不加空格  
x = r->y;          // 符合:通过->访问成员变量时不加空格  

操作符

x = 0;             // 符合:赋值操作的=前后都要加空格  
x = -5;            // 符合:负数的符号之前要加空格  
++x;               // 符合:前置和后置的++/--和变量之间不要加空格  
x--;

if (x && !y)       // 符合:布尔操作符前后要加上空格,!操作符和变量之间不要空格  
v = w * x + y / z; // 符合:二元操作符前后要加空格  
v = w * (x + z);   // 符合:括号内的表达式前后不需要加空格  

循环和选择语句

if (condition) {   // 符合:if关键字和括号之间加空格,括号内表达式前后不加空格  
    ...
} else {           // 符合:else关键字和大括号之间加空格  
    ...
}

// 符合:while关键字和括号之间加空格,括号内表达式前后不加空格  
while (condition) {}
do {} while (condition);

// 符合:for关键字和括号之间加空格,分号之后加空格  
for (int i = 0; i < someRange; ++i) {
    ...
}

switch (var) {     // 符合:switch 关键字后面有1空格  
    case CASE1:    // 符合:case语句条件和冒号之间不加空格  
        ...
        break;
    ...
    default:
        ...
        break;
}

模板和转换

// 符合:尖括号(`<` 和 `>`) 不与空格紧邻, `<` 前没有空格, `>` 和 `(` 之间也没有
std::vector<std::string> x;
y = static_cast<char*>(x);

域操作符

std::cout;                         // 符合:命名空间访问,不要留空格

int SomeClass::GetValue() const {} // 符合:对于成员函数定义,不要留空格

冒号(添加空格的场景)

// 符合:类的派生需要留有空格
class Derived : public Base {
    ...
};

// 符合:构造函数初始化列表需要留有空格
SomeClass::SomeClass(int var) : someVar(var)
{
    DoSomething();
}

// 符合:位域表示也留有空格
struct SomeType {
    char a : 4;
    char b : 5;
    char c : 4;
};

// 符合:范围for语句中的冒号两边留有空格
for (auto& data : dataList) {
    ...
}

冒号(不添加空格的场景)

// 对于public:, private:这种类访问权限的冒号不添加空格
class SomeClass {
public:
    SomeClass(int var);

private:
    int someVar;
};

// 对于switch-case的case和default后面的冒号不添加空格
switch (var) {
    case CASE1:
        DoSomething1();
        break;
    case CASE2:
        DoSomething2();
        break;
    default:
        break;
}

正确示例:

场景1:二元操作符至少一边没加空格
  • 修复示例:二元操作符两边加上空格
c = a + b;      // 符合

错误示例:

场景1:二元操作符至少一边没加空格
  • 错误示例:
c = a+b;        // 不符合:'+' 左右没有空格
c = a +b;       // 不符合:'+' 右边没有空格
c = a+ b;       // 不符合:'+' 左边没有空格

修改建议:

按要求增加/减少空格。


40. 规则名称:使用统一的命名风格

问题级别:一般

规则描述:

标识符命名风格原则

  • 具体的风格由项目组自行选择,并保持风格统一
  • 推荐使用驼峰命名风格,不推荐使用匈牙利命名风格
  • 对于更亲和标准库的代码,可以使用标准库命名风格
  • 基于开源或外部代码进行开发或维护时,可以保持原有命名风格

常见的命名风格

  • 驼峰命名风格(CamelCase) 大小写字母混用,单词连在一起,不同单词间通过单词首字母大写来分开。 根据连接后的首字母是否大写,分为两种风格:大驼峰(UpperCamelCase)和小驼峰(lowerCamelCase)。

  • 内核命名风格(unix_like) 又称蛇形风格(snake_case),标准库使用该风格。单词全小写,用下划线分割。 如:'test_result'。

  • 匈牙利命名风格 在“大驼峰”的基础上,加上类型或用途前缀。 如:'uiSavedCount', 'bTested'。

本条款推荐的 驼峰命名风格 具体规则如下:

类别 命名风格 形式
命名空间,类类型,结构体类型,联合体类型,枚举类型,typedef定义的类型,类型别名 大驼峰 AaaBbb
函数(包括全局函数,作用域内函数,成员函数) 大驼峰 AaaBbb
全局变量(包括全局、文件、namespace域下的变量以及相应作用域下的静态变量) 带 'g_' 前缀的小驼峰 g_aaaBbb
类成员变量 在三种小驼峰命名风格中,选择并统一使用一种风格 aaaBbb, m_aaaBbb, aaaBbb_
局部变量,函数参数,宏参数,结构体和联合体中的成员变量 小驼峰 aaaBbb
枚举值,常量 全大写,下划线分割,或者选择与变量相同的命名风格,并统一使用一种风格 变量命名风格,AAA_BBB
宏,goto 标签 全大写,下划线分割 AAA_BBB

注:用户自定义的literal后缀名应以单下划线开头,命名不限于驼峰风格。

关于常量 本条款中的常量指的是以constexprconst修饰的,其值在程序生命周期内固定不变的对象,如:全局、文件以及namespace域中的对象,或者类的静态成员等。枚举值也被视为常量,而函数局部constexprconst修饰的标识符和类的普通const成员变量,等同变量看待。

项目组可以选择某类常量(如:布尔、整数、浮点数、字符等基本类型常量,枚举,指向字符串字面量的常量指针,以及C风格字符串常量等)的命名方式采用全大写,下划线分割的命名风格,也可以选择与变量命名相同的风格,并保持风格一致。

当常量采用全大写,下划线分割的命名风格时,需要注意避免和宏的名字冲突。因为在编译代码时,如果宏的名字与常量名字相同,会将常量替换为宏的内容。

常量命名示例 仅列举了使用全大写,下划线分割的命名风格。

const int NAME_MAX_LEN = 10;            // 基本数据类型
const char* const VERSION = "V100";     // 指向字符串字面量的常量指针
const char C_STR[] = "c string";        // C字符串常量
enum class Color { RED, GREEN, BLUE };  // 枚举类型是大驼峰,枚举常量使用全大写下划线分割
const Color TABLE_COLOR = Color::GREEN;

namespace Utils {
const uint32_t DEFAULT_FILE_SIZE = 200; // 全局常量
}

class Foo {
public:
    static const uint32_t DEFAULT_VALUE = 0x55aa; // 静态成员常量
};

命名空间、类型、函数的命名示例 使用大驼峰命名风格。

namespace FileUtils { ...

class UrlTable { ...

struct UrlTableProperties { ...

union Packet { ...

enum class UrlTableErrors { ...

typedef std::map<std::string, UrlTable*> UrlTableMap;

using UrlPropertiesMap = std::map<std::string, UrlTableProperties*>;

函数命名一般采用动词或者动宾结构。

class List {
public:
    void AddElement(const Element& element);
    Element GetElement(const uint32_t index) const;
    bool IsEmpty() const;
};

namespace Utils {
void DeleteUser();
}

全局变量命名示例 全局变量使用小驼峰添加g_前缀。

int g_activeConnectCount = 0; // 全局变量
SomeClass g_someObject{};     // 全局对象
static int g_blockCount = 0;  // 文件作用域中的全局变量

namespace Utils {
int g_fileCount = 0;          // 命名空间中的全局变量
}

类成员变量命名示例 有三种小驼峰命名风格可选:小驼峰命名风格、带m_ 前缀或下划线后缀的小驼峰命名风格。例如:aaaBbb, m_aaaBbb, aaaBbb_。 各项目组可根据自身情况,从三种风格中选择一种,并在项目组内保持统一风格。

  1. 小驼峰命名风格
class Foo {
    ...

private:
    std::string fileName;
    static int objCount;
    ...
};
  1. 带 'm_' 前缀的小驼峰命名风格
class Foo {
    ...

private:
    std::string m_fileName;
    static int m_objCount;
    ...
};
  1. 带下划线后缀的小驼峰命名风格
class Foo {
    ...

private:
    std::string fileName_;
    static int objCount_
    ...
};

局部变量、函数参数、宏参数、结构体和联合体中的成员变量命名示例 使用小驼峰命名风格。


void Fun(const std::string& fileName) // 符合:函数参数使用小驼峰命名风格
{
    std::string tableName;            // 符合:小驼峰命名风格
    std::string tablename;            // 不符合:name首字母应大写
    std::string path;                 // 符合:只有一个单词时,小驼峰为全小写

    static size_t packetCount = 0;    // 符合:函数局部静态变量,使用小驼峰命名风格
    const size_t bufferSize = 100;    // 符合:函数局部常量,使用小驼峰命名风格
    ...
}

struct UrlTableProperties {
    size_t rowCount;                  // 符合:小驼峰命名风格
    ...
};
  • 关于单词缩写 单词缩写应当作单个单词处理,以提升可读性。 例如对包含HTTP(HyperText Transfer Protocol)缩写的函数命名如下:
int GetActiveHttpClientCount();

该规则有可配置参数,具体配置详见规则集中该规则的选项配置。

正确示例:

场景1:全局变量命名没有g_前缀
  • 修复示例1:
int g_variable;                 // 符合:全局变量,带g_前缀的小驼峰

SomeClass g_someObject{};       // 符合:全局对象,带g_前缀的小驼峰

static int g_blockCount = 0;    // 符合:文件作用域中的全局变量,带g_前缀的小驼峰

namespace Utils {
int g_fileCount = 0;            // 符合:命名空间中的全局变量,带g_前缀的小驼峰
}
场景2:选择驼峰命名风格,但是使用时没有遵循
  • 修复示例1:遵循驼峰命名风格。
namespace FileUtils {                       // 符合:命名空间使用“大驼峰”
...
}

class SomeClass {                           // 符合:类类型使用“大驼峰”
    void SetData();                         // 符合:成员函数使用“大驼峰”
    ...
    std::string m_msg;                      // 符合:类成员变量使用“小驼峰”
};

struct StrType {                            // 符合:结构体类型使用“大驼峰”
    int typeMember;                         // 符合:结构体中字段使用“小驼峰”
};

void Compare(int leftVal, int rightVal);    // 符合:函数大驼峰,参数小驼峰

char * const VERSION = "V100";              // 符合:常量用全大写下划线分割

错误示例:

场景1:全局变量命名没有g_前缀
  • 错误示例:
int Global_variable = 7;    // 不符合

SomeClass G_someObject{};   // 不符合

static int blockCount = 0;  // 不符合

namespace Utils {
int file_Count = 0;         // 不符合
}
场景2:选择驼峰命名风格,但是使用时没有遵循
  • 错误示例:
namespace FILEUTILS {                       // 不符合
...
}

class Some_Class {                          // 不符合
    void set_data();                        // 不符合
    ...
    std::string Msg;                        // 不符合
};

struct strType {                            // 不符合
    int Type_Member;                        // 不符合
};

void compare(int left_val, int right_val);  // 不符合

char * const version = "V100";              // 不符合

修改建议:

根据提示信息验证代码风格。


41. 规则名称:禁止用断言检测程序在运行期间可能导致的错误,可能发生的错误要用错误处理代码来处理

问题级别:一般

规则描述:

断言不能用于校验程序在运行期间可能导致的错误。

断言主要用于调试期间,在发布版本中应将其关闭。因此,断言应该用于防止不正确的程序员假设,而不能用在发布版本上检查程序运行过程中发生的错误。

断言永远不应用于验证是否存在运行时(与逻辑相对)错误,包括但不限于:

  • 无效的用户输入(例如:命令行参数和环境变量)
  • 文件错误(例如:打开、读取或写入文件时出错)
  • 网络错误(例如:网络协议错误)
  • 内存不足的情况(例如:malloc()类似的故障)
  • 系统资源耗尽(例如:文件描述符、进程、线程)
  • 系统调用错误(例如:执行文件、锁定或解锁互斥锁时出错)
  • 无效的权限(例如:文件、内存、用户)

例如,防止缓冲区溢出的代码不能使用断言实现,因为该代码必须编译到发布版本的可执行文件中。如果服务器程序在网运行时由恶意用户触发断言失败,会导致拒绝服务攻击。在这种情况下,更适合使用软故障模式,例如写入日志文件和拒绝请求。

正确示例:

下面代码演示了如何重构上面的错误代码

FILE *fp = fopen(path, "r");
if (fp == NULL) {
    ... // 错误处理
}
char *str = (char *)malloc(MAX_LINE);
if (str == NULL) {
    ... // 错误处理
}
ReadLine(fp, str);
char *p = strstr(str, "age=");
if (p == NULL) {
    ... // 错误处理
}
char *end = NULL;
long age = strtol(p + 4, &end, 10);
if (age <= 0) {
    ... // 错误处理
}

错误示例:

以下代码的所有ASSERT的用法都是错误的。例如,错误的使用ASSERT宏来验证内存分配是否成功,因为内存的可用性取决于系统的整体状态,并且在程序运行的任何时候都可能耗尽,所以必须以具有韧性的方式来妥善处理并将程序从内存耗尽中恢复。因此,使用ASSERT宏来验证内存分配是否成功将是不合适的,因为这样做可能导致进程突然终止,从而开启了拒绝服务攻击的可能性。

FILE *fp = fopen(path, "r");
ASSERT(fp != NULL);  // 不符合:文件有可能打开失败
char *str = (char *)malloc(MAX_LINE);
ASSERT(str != NULL); // 不符合:内存有可能分配失败
ReadLine(fp, str);
char *p = strstr(str, "age="");
ASSERT(p != NULL);  // 不符合:文件中不一定存在该字符串
char *end = NULL;
long age = strtol(p + 4, &end, 10);
ASSERT(age > 0);   // 不符合:文件内容不一定符合预期

修改建议:

不要在发布版本上使用assert检查程序运行过程中发生的错误。


42. 规则名称:按照合理的顺序包含头文件

问题级别:一般

规则描述:

使用一致的头文件包含顺序可提升可读性, 避免隐藏依赖,建议按照稳定度排序:cpp对应的头文件, C/C++标准库, 系统库的.h, 其他库的.h, 本项目内其他的.h。

该规则有可配置参数,具体配置详见规则集中该规则的选项配置。

正确示例:

#include "Foo/Foo.h"

#include <cstdlib>
#include <string>

#include <linux/list.h>
#include <linux/time.h>

#include "platform/Base.h"
#include "platform/Framework.h"

#include "project/public/Log.h"

修改建议:

调整头文件包含顺序


43. 规则名称:函数式宏定义中慎用 return、goto、continue、break 等改变程序流程的语句

问题级别:一般

规则描述:

宏中使用 return、goto、continue、break等改变流程的语句,虽然能简化代码,但同时也隐藏了真实流程,不易于理解,存在过度封装,容易导致资源泄漏等问题。

错误示例:

如下是宏封装return容易引发内存泄漏的场景:

#define CHECK_PTR(ptr, ret) do { \
    if ((ptr) == NULL) { \
        return (ret); \
    } \
} while (0)

...

mem1 = MemAlloc(STR_SIZE_MAX);
CHECK_PTR(mem1, ERR_CODE_XXX);

mem2 = MemAlloc(STR_SIZE_MAX);
CHECK_PTR(mem2, ERR_CODE_XXX); // 内存泄漏问题

如果 mem2 申请内存失败了,CHECK_PTR 会直接返回,而没有释放 mem1。 除此之外,CHECK_PTR 宏命名也不好,宏名只反映了检查动作,没有指明结果。只有看了宏实现才知道指针为空时返回失败。

修改建议:

重构代码逻辑,避免安全隐患。


44. 规则名称:除main函数以外的其他函数,禁止使用exit、ExitProcess函数退出【C】

问题级别:一般

规则描述:

除main函数以外的其他函数,禁止使用exit、ExitProcess函数退出。

除了main函数以外,禁止任何地方调用exit、ExitProcess函数退出进程。直接退出进程可能会导致代码的复用性降低,某些资源(尤其是不属于进程的系统资源)在程序退出前难以得到有效地清理。

处理程序退出时必须通过不断返回上一层函数来终止运行。对于所有退出处理程序来说,能够正确执行它们的清理操作是很重要的,在安全性上可能很关键。

正确示例:

场景1:在非main函数中,调用exit或ExitProcess函数。
  • 修复示例1:出现异常时,返回错误码或抛出异常,直到返回到main函数。

    bool DoNotUseExitProcessGood(const char *filePath)
    {
        FILE *fp = fopen(filePath, "rt");
        if (fp == NULL) {
            // POTENTIAL FLAW GOOD: 在非main函数中,用返回值逐层处理错误并退出
            return false;
        }
        return true;
    }
    

错误示例:

场景1:在非main函数中,调用exit或ExitProcess函数。
  • 错误示例:

    void DoNotUseExitProcessBad(const char *filePath)
    {
        FILE *fp = fopen(filePath, "rt");
        if (fp == NULL) {
            // POTENTIAL FLAW: 在非main函数中,禁止用exit直接退出程序
            exit(0);
        }
    }
    

修改建议:

除main函数以外的其他函数,禁止使用exit、ExitProcess函数退出。


45. 规则名称:用空格突出关键字和重要信息--类型转换

问题级别:一般

规则描述:

空格应该突出关键字和重要信息。总体建议如下:

  • 行末不应加空格
  • if, switch, case, do, while, for 等关键字之后加空格;#include之后加空格
  • 小括号内部的两侧,不加空格;外部两侧与关键字或重要信息之间加空格
  • 无论大括号内部两侧有无空格,左右要保持一致;外部两侧与关键字或重要信息之间加空格
  • 使用大括号进行初始化时,左侧大括号跟随类型或变量名时不加空格
  • 一元操作符(& * + ‐ ~ !)之后不应加空格
  • 二元操作符(= + ‐ < > * / % | & ^ <= >= == !=)两侧都应加空格
  • 三元操作符(? :)符号两侧都应加空格
  • 前置和后置的自增、自减操作符(++ --)和变量之间不应加空格
  • 尾置返回类型语法中的->前后加空格
  • 结构体成员操作符(. ->)前后不应加空格
  • 结构体中表示位域的冒号,两侧都应加空格
  • 函数参数列表的小括号与函数名之间不应加空格
  • 类型强制转换的小括号与被转换对象之间不应加空格
  • 数组的中括号与数组名之间不应加空格
  • 对于模板和类型转换(<>)和类型之间不加空格
  • 域操作符(::)前后不加空格
  • 类成员指针(::* .* ->*)前后或中间不加空格
  • 类继承、构造函数初始化、范围for语句中的冒号(:)前后加空格
  • 逗号、分号、冒号(上述规则场景除外)前面不加空格,后面加空格

初始化语句

std::string str{"Hello, World"};            // 符合:左大括号和变量名之间不加空格
std::vector<int> v{1, 2, 3};                // 符合:大括号内部两侧都没有空格
std::array<std::string, 2> arr{ "a", "b" }; // 符合:大括号内部两侧都有空格
int buf[BUF_SIZE] = {0};                    // 符合:大括号内部两侧都没有空格
int i = 0;                                  // 符合:等号前后应该有空格,分号前面不要留空格
Foo({1, 2, 3}, 0);                          // 符合:作为实参,和普通实参的空格要求一致
int arr[] = { 10, 20};                      // 不符合:大括号内部两侧空格不一致

函数定义和函数调用

// 符合:大括号换行,换行前没有空格
void Foo(int b)
{
    ...
}

int result = Foo(arg1,arg2);    // 不符合: 逗号后面应该有空格        
int result = Foo( arg1, arg2 ); // 不符合: 小括号内部两侧不应该有空格    

指针和取地址

x = *p;            // 符合:*操作符和指针p之间不加空格  
p = &x;            // 符合:&操作符和变量x之间不加空格  
x = r.y;           // 符合:通过.访问成员变量时不加空格  
x = r->y;          // 符合:通过->访问成员变量时不加空格  

操作符

x = 0;             // 符合:赋值操作的=前后都要加空格  
x = -5;            // 符合:负数的符号之前要加空格  
++x;               // 符合:前置和后置的++/--和变量之间不要加空格  
x--;

if (x && !y)       // 符合:布尔操作符前后要加上空格,!操作符和变量之间不要空格  
v = w * x + y / z; // 符合:二元操作符前后要加空格  
v = w * (x + z);   // 符合:括号内的表达式前后不需要加空格  

循环和选择语句

if (condition) {   // 符合:if关键字和括号之间加空格,括号内表达式前后不加空格  
    ...
} else {           // 符合:else关键字和大括号之间加空格  
    ...
}

// 符合:while关键字和括号之间加空格,括号内表达式前后不加空格  
while (condition) {}
do {} while (condition);

// 符合:for关键字和括号之间加空格,分号之后加空格  
for (int i = 0; i < someRange; ++i) {
    ...
}

switch (var) {     // 符合:switch 关键字后面有1空格  
    case CASE1:    // 符合:case语句条件和冒号之间不加空格  
        ...
        break;
    ...
    default:
        ...
        break;
}

模板和转换

// 符合:尖括号(`<` 和 `>`) 不与空格紧邻, `<` 前没有空格, `>` 和 `(` 之间也没有
std::vector<std::string> x;
y = static_cast<char*>(x);

域操作符

std::cout;                         // 符合:命名空间访问,不要留空格

int SomeClass::GetValue() const {} // 符合:对于成员函数定义,不要留空格

冒号(添加空格的场景)

// 符合:类的派生需要留有空格
class Derived : public Base {
    ...
};

// 符合:构造函数初始化列表需要留有空格
SomeClass::SomeClass(int var) : someVar(var)
{
    DoSomething();
}

// 符合:位域表示也留有空格
struct SomeType {
    char a : 4;
    char b : 5;
    char c : 4;
};

// 符合:范围for语句中的冒号两边留有空格
for (auto& data : dataList) {
    ...
}

冒号(不添加空格的场景)

// 对于public:, private:这种类访问权限的冒号不添加空格
class SomeClass {
public:
    SomeClass(int var);

private:
    int someVar;
};

// 对于switch-case的case和default后面的冒号不添加空格
switch (var) {
    case CASE1:
        DoSomething1();
        break;
    case CASE2:
        DoSomething2();
        break;
    default:
        break;
}

正确示例:

场景1:类型转换(<>)和类型之间存在空格
  • 修复示例1:
char* ch = reinterpret_cast<char*>(s);      // 符合
场景2:被强制转换的变量与类型之间存在空格
  • 修复示例1:
double a = 1.5;
int i = static_cast<int>(a); // 符合:使用由C++提供的类型转换操作,且没有多余空格

错误示例:

场景1:类型转换(<>)和类型之间存在空格
  • 错误示例:
char* ch = reinterpret_cast< char* >(s);    // 不符合:<> 中有多余空格
场景2:被强制转换的变量与类型之间存在空格
  • 错误示例:
double a = 1.5;
int i = (double) a; // 不符合:'(double)'和a之间存在空格

修改建议:

按要求增加/减少空格。


46. 规则名称:使用空格进行缩进,每次缩进4个空格

问题级别:一般

规则描述:

建议使用空格进行缩进,每次缩进为 4 个空格。避免使用制表符('\t')进行缩进。
当前几乎所有的集成开发环境(IDE)和代码编辑器都支持配置将Tab键自动扩展为 4 空格输入。

修改建议:

按要求进行缩进。


47. 规则名称:使用合适的类型表示字符【C】

问题级别:一般

规则描述:

字符类型和整型的语义是不同的,因此使用时要注意区分整数类型和字符类型。

表示字符时应使用char,表示整数时不应使用char

unsigned char不能表示字符,也不能表示字符串。

正确示例:

场景1:整型运算用了字符类型
  • 修复示例:
void Foo(char c, int y)
{
    int x;
    int n;
    n = c - '0'; // 符合:字符间运算,取偏移量
    x = y + n;   // 符合:此处是整型运算
}

错误示例:

场景1:整型运算用了字符类型
  • 错误示例:
void Foo(char c, int y)
{
    int x;
    x = c + y; // 不符合:此处是整型运算,但是用了字符类型
}

修改建议:

正确使用字符类型。


48. 规则名称:设计函数时,优先使用返回值而不是输出参数【C】

问题级别:一般

规则描述:

设计函数时,优先使用返回值而不是输出参数。

正确示例:

场景1:使用输出参数
  • 修复示例:使用返回值。
int GetValue(void) // 符合
{
    return 1;
}

错误示例:

场景1:使用输出参数
  • 错误示例:
void GetValue(int *x) // 不符合
{
    *x = 1;
}

修改建议:

使用返回值。


49. 规则名称:禁止在断言内改变运行环境【C】

问题级别:一般

规则描述:

禁止在断言内改变运行环境

在程序正式发布阶段,断言不会被编译进去,为了确保调试版和正式版的功能一致性,严禁在断言中使用任何赋值、修改变量、资源操作、内存申请等操作。

例如,以下的断言方式是错误的:

ASSERT(p1 = p2);     // p1被修改
ASSERT(i++ > 1000);   // i被修改
ASSERT(close(fd) == 0); // fd被关闭

正确示例:

场景1:在断言内改变运行环境。
  • 修复示例:在断言中不能有任何赋值、修改变量、资源操作、内存申请等操作。
p1 = p2;
ASSERT(p1);             // 符合

ASSERT(i > 1000);       // 符合
i++;

int ret = close(fd);
if (ret != 0) {         // 符合:可能发生的错误且必须处理的情况要用错误处理代码来处理
    ...
}

val <<= offset;
if (val == 0) {         // 符合:可能发生的错误且必须处理的情况要用错误处理代码来处理
    ...
}

ASSERT(ptr != NULL);    // 符合
ptr--;

ptr = (char *)malloc(SIZE);
if (ptr == NULL) {      // 符合:可能发生的错误且必须处理的情况要用错误处理代码来处理
    ...
}

错误示例:

场景1:在断言内改变运行环境。
  • 错误示例:
ASSERT(p1 = p2);        // 不符合:p1被修改
ASSERT(i++ > 1000);     // 不符合:i被修改
ASSERT(close(fd) == 0); // 不符合:fd被关闭
ASSERT(val <<= offset); // 不符合:val被赋值
ASSERT(ptr--);          // 不符合:ptr递减
ASSERT(ptr = (char *)malloc(SIZE)); // 不符合:ptr被赋值

修改建议:

调用ASSERT,入参中不能使用赋值(例如 =、+=、-= 或<<=)、递增和递减(例如++或—)、修改堆(例如 ASSERT(new xxx))。


50. 规则名称:声明一个带有外部链接的数组时,必须显式指定它的大小【C】

问题级别:一般

规则描述:

声明具有外部链接的数组时,明确指定其大小会使代码更加安全。明确声明外部链接数组的大小可以方便在编写代码时,校验数组索引是否越界;而且还有利于静态分析工具做数组边界检查。

此规则仍然允许通过初始化列表隐式指定大小的方式来定义一个数组,但在将其声明为一个带有外部链接的数组时,必须显式指定它的大小。

在声明同时也要遵从规则:G.INC.07 禁止通过声明的方式引用外部函数接口、变量

该规则有可配置参数,具体配置详见规则集中该规则的选项配置。

正确示例:

场景1:没有显式指定数组大小
  • 修复示例:
// in foo.h
extern int g_array[MAX_LEN];   // 符合:显式指定了数组大小
  • 修复示例:
// in foo.h
const char g_array[];
const size_t g_arrayLen; // 符合:显式指定了数组大小,且和数组声明之间无间隔

错误示例:

场景1:没有显式指定数组大小
  • 错误示例:
// in foo.h
extern int g_array[];     // 不符合:没有显式指定数组大小

修改建议:

数组声明时,同时显式指定其大小,如:

  1. 固定长度数组
extern int g_array[MAX_LEN]; 
  1. 需要通过计算的数组长度,注意数组和数组长度参数的声明放一起,中间不要有间隔。
const char g_array[];
const size_t g_arrayLen;

51. 规则名称:头文件的扩展名只使用.h,不使用非习惯用法的扩展名,如.inc【C】

问题级别:一般

规则描述:

头文件的扩展名只使用.h,不使用非习惯用法的扩展名,如.inc。

该规则有可配置参数,具体配置详见规则集中该规则的选项配置。

正确示例:

场景1:头文件扩展名为.inc
  • 修复示例:
// sample.h 开始
// 符合

// sample.h 结束

错误示例:

场景1:头文件扩展名为.inc
  • 错误示例:头文件扩展名为.inc
// sample.inc 开始
// 不符合

// sample.inc 结束

修改建议:

修改头文件扩展名为.h。


52. 规则名称:循环必须安全退出

问题级别:建议

规则描述:

循环必须安全退出。

在应用程序中,一个重复提供服务的逻辑循环应当设计退出机制,并且将资源正确释放后安全退出。退出条件的设计,除了让程序逻辑更加完整,也能通过实现优雅退出的代码,显式释放服务循环中分配的资源,避免资源泄漏。

正确示例:

重新设计函数,通过提供服务退出条件,并在资源释放函数ReleaseServiceResource内释放服务循环前申请的资源。

bool ParseMsg(unsigned char *msg, size_t msgLen)
{
    ...
    if (msg->type == EXIT_MESSAGE_TYPE) {
        return false;
    } else {
        return true;
    }
}

void DoService(void)
{
    ...
    size_t size = 0;
    unsigned char *pMsg = NULL;
    bool doRunServiceFlag = true; // 服务退出条件
    CreateServiceResource(); // 分配服务资源
    while (doRunServiceFlag) {
        pMsg = ReceiveMsg(&size);
        if (pMsg != NULL) {
            doRunServiceFlag = ParseMsg(pMsg, size);
            FreeMsg(pMsg);
        }
        size = 0;
    }
    ReleaseServiceResource(); // 释放服务资源
}

错误示例:

以下代码,在一个大循环内,ReceiveMsg函数内申请资源,接收外部数据。ParseMsg函数内处理数据,但没有退出条件,会导致循环前申请的资源无法释放,该程序没有安全退出。

...
void DoService(void)
{
    ...
    size_t size = 0;
    unsigned char *pMsg = NULL;
    CreateServiceResource(); // 分配服务资源
    while (true) {
        pMsg = ReceiveMsg(&size);
        if (pMsg != NULL) {
            ParseMsg(pMsg, size);
            FreeMsg(pMsg);
        }
        size = 0;
    }
}

修改建议:

确保循环能够正常退出,不出现死循环的情况。