文章

MISRA C:2012 嵌入式C规范解读

术语中英表

英文中文
expression表达式
aggregate集合
compound literal复合字面表达式
persistent side effects持久性副作用

总览

MISRA C 指南定义了一个 C 语言的子集,在这个子集中,犯错的机会被消除或减少,程序可靠性得到提高。

MISRA C 发展历史:

  1. MISRA C:1998 – 第一版(汽车行业的原始指南)
  2. MISRA C:2004 – 第二版(考虑了用户反馈和跨行业应用)
  3. MISRA C:2012 – 第三版(包含对 C99 语言功能的支持,改进的强类型模型,分析关键字)
    • MISRA C:2012 (Feb 2019) – 第三版第一次修订(纳入了额外的安全准则),纳入了第 1 次修正案(AMD1)和技术更正 1(TC1) – 也称为 MISRA C:2019
    • MISRA C:2023 (Apr 2023) – 第三版第二次修订(包含对 C11 和 C18 语言功能的支持),纳入了第 2 次(AMD2)、第 3 次(AMD3)和第 4 次(AMD4)修正案,以及技术更正 2(TC2)。

本文将介绍第三版的初稿,也就是MISRA C:2012

背景

C 语言优势

C 编程语言很受欢迎,因为:

  • C 语言编译器可用于许多处理器
  • C 语言程序可被编译为高效的机器代码;
  • 它由国际标准(ISO)定义;
  • 它提供了访问目标处理器的输入/输出能力的机制,无论是直接访问还是通过语言扩展
  • 在关键系统中使用 C 语言有大量的经验
  • 它被静态分析测试工具广泛支持。

C 语言缺陷

语言定义

ISO 标准并没有完全规范 C 语言,而是故意将某些方面放到实现时由程序员(编译器)自己去定义,从而让 C 语言变得更加灵活以支持不同的处理器。

这会导致某些行为变得不可预测,这对嵌入式领域的可靠性要求是致命的。

例子:

1
if ( ishigh && (x == i++))

在上面这个例子中,该语句的执行会因编译器的不同产生不同的结果:

  • ishigh 为否时,后一个表达式不判断,i不变
  • ishigh 为否时,后一个表达式也判断,i增加 1

这就需要程序员对编译器特性非常熟悉(但还是避免不了失误),且该代码的可移植性也会很差

语言误用

编程过程中可能会出现连程序员自己也没发觉的失误:

1
2
if (a == b) /* 判断a和b是否相等 */
if (a = b) /* 将b赋值给a,并判断a是否为非0 */

这种情况一般是把 a == b 误写成 a = b 了,但对编译器来说它们都合规,无法判断错误。

不过现在的静态检查工具在编程时就能检查出这种情况,给出 warning

语言误解

C 语言有很多运算符,虽然标准规定了运算符的优先级,但比较难记忆,可能在编程中混淆了部分运算符的优先级。

C 语言不是强类型语言,其类型可以隐式转换,也就是操作产生的数据类型和操作数类型可能不同。

运行时(Run-time)错误检查

C 语言程序可以被编译成小而有效的机器代码,但其代价是运行时检查的程度非常有限。

C 语言程序一般不提供对常见问题的运行时检查,如算术异常(如除以 0)、溢出、指针的有效性或数组边界错误。

C 语言的理念是,程序员有责任明确地进行这种检查。

工具选择

C 标准和编译器选择

MISRA C:2012 基于ISO/IEC 9899:2011 [14](C99)和ISO/IEC 9899:1990 [2](C90)标准编写

根据项目需求选择 C99 或 C90 标准

写者注:原文的一些介绍已经过时,这里就不写了,比如他认为当前编译器对 C99 的支持普遍不好,实际上现在(2023 年)大多数的编译器都已经能较好的支持 C99 了,对于新项目,建议无脑选 GCC 就行

代码分析工具

写者注:本节将由写者根据项目经验列出推荐的工具

这里推荐使用开源的cppcheck搭配misra 插件,只需在安装 cppcheck 时勾选安装 addons,然后搭配 vscode 中的task功能和规则描述文件(规则描述文件可以在 github 上找找,毕竟有版权不能随便共享):

配置文件 config.ini:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[cppcheck]
# 存放临时文件的目录,需要有权限访问
temp_dir = build/cppcheck
# 头文件目录
incude_dirs = include
            src/app
# misra规则文件目录
misra_rule_file = tools/misra.txt
# 源文件目录,可递归查找
src_dirs = src
# 不在结果中输出的文件
suppress_files = 3rd_party/*
# 不启用的misra规则,逗号分隔
misra_suppress_rules = 2.3,2.4,2.5,5.9,8.9,11.3,

执行脚本 cppcheck_misra.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
#!/usr/bin/env python3
import os
import subprocess
import sys
import configparser


def run_cppcheck():
    config = configparser.ConfigParser()
    config.read("config.ini", encoding='utf-8')
    CPPCHECK_TMP_DIR = config.get("cppcheck", "temp_dir")
    CPPCHECK_OUTPUT_FILE = os.path.join(CPPCHECK_TMP_DIR, "output.txt")
    include_paths = config.get("cppcheck", "incude_dirs")
    CPPCHECK_INCLUDE_DIRS = include_paths.splitlines()
    CPPCHECK_INCLUDE_DIRS_FILE = os.path.join(CPPCHECK_TMP_DIR, "include.txt")
    CPPCHECK_SUPPRESS_FILE = config.get("cppcheck", "suppress_files")
    CPPCHECK_MISRA_RULE_FILE = config.get("cppcheck", "misra_rule_file")
    CPPCHECK_MISRA_SUPPRESS_RULES = config.get("cppcheck", "misra_suppress_rules")
    src_paths = config.get("cppcheck", "src_dirs")
    CPPCHECK_SRC_DIRS = src_paths.splitlines()

    cppcheck_version = (
        subprocess.check_output(["cppcheck", "--version"]).decode().strip()
    )
    print(f"Running cppcheck version: {cppcheck_version}")

    if os.path.exists(CPPCHECK_TMP_DIR):
        subprocess.run(["rm", "-rf", CPPCHECK_TMP_DIR])

    os.makedirs(CPPCHECK_TMP_DIR, exist_ok=True)

    with open(CPPCHECK_INCLUDE_DIRS_FILE, "w") as f:
        f.writelines("%s\n" % s for s in CPPCHECK_INCLUDE_DIRS)
    cppcheck_command = [
        "cppcheck",
        r"--template={file}:{line}:{column}: warning: CWE-{cwe} {message}:[{id}]",
        "--enable=all",
        "-j1",
        "--force",
        "--max-ctu-depth=2",
        "--platform=arm32-wchar_t2",
        "--std=c99",
        "--cppcheck-build-dir=" + CPPCHECK_TMP_DIR,
        "--std=c++03",
        "--inline-suppr",
        "--report-progress",
        "--language=c",
        "--inconclusive",
        "--includes-file=" + CPPCHECK_INCLUDE_DIRS_FILE,
        "--output-file=" + CPPCHECK_OUTPUT_FILE,
        "--suppress=*:{}".format(CPPCHECK_SUPPRESS_FILE),
        
        '--addon={{"script":"misra.py","args":["--rule-texts={}","--suppress-rules={}"]}}'.format(
        
            CPPCHECK_MISRA_RULE_FILE, CPPCHECK_MISRA_SUPPRESS_RULES
        ),
    ] + CPPCHECK_SRC_DIRS

    subprocess.run(cppcheck_command)

    if len(os.sys.argv) > 1 and os.sys.argv[1] == "-v":
        # 如果参数为 "-v",则打印完整内容
        with open(CPPCHECK_OUTPUT_FILE, "r") as output_file:
            print(output_file.read())
    else:
        # 否则只打印摘要信息
        print(f"输出在文件 {CPPCHECK_OUTPUT_FILE}")

    with open(CPPCHECK_OUTPUT_FILE, "r") as output_file:
        err_count = sum(1 for _ in output_file)

    print(f"cppcheck检测完成,错误数:{err_count}")
    if err_count > 0:
        sys.exit(1)

if __name__ == "__main__":
    run_cppcheck()

tasks.json:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
{
  "label": "cppcheck",
  "type": "shell",
  "command": "python3 tools/cppcheck_misra.py -v",
  "problemMatcher": {
    // The problem is owned by the cpp language service.
    "owner": "cppcheck",
    // The file name for reported problems is relative to the opened folder.
    "fileLocation": ["relative", "${workspaceFolder}"],
    // The actual pattern to match problems in the output.
    "pattern": {
      // The regular expression. Example to match: helloWorld.c:5:3: warning: implicit declaration of function ‘printf’ [-Wimplicit-function-declaration]
      "regexp": "^(.*):(\\d+):(\\d+):\\s+(warning|error):\\s+(.*)$",
      // The first match group matches the file name which is relative.
      "file": 1,
      // The second match group matches the line on which the problem occurred.
      "line": 2,
      // The third match group matches the column at which the problem occurred.
      "column": 3,
      // The fourth match group matches the problem's severity. Can be ignored. Then all problems are captured as errors.
      "severity": 4,
      // The fifth match group matches the message.
      "message": 5
    }
  }
}

当然如果有充足的资金可以选择购买专业的静态检查工具,List of tools for static code analysis - Wikipedia网站上列出了所有主流的静态检查工具,搜索"misra"即可找到支持 misra 的检查工具。

必备知识

开发者应具备嵌入式、高集成、高安全性编程基础,并了解使用的编译器和静态检查工具。

使用 MISRA C

MISRA C 应该从项目一开始便使用,如果是为了符合 MISRA C 而对已有项目大规模修改会造成不可预估的问题,注意评估好这么做的风险。

应在代码审核和单元测试前检查代码是否符合 MISRA C。

MISRA C 不提供任何与编程风格相关的建议。

注意编译器优化选项,平衡目标文件大小和缺陷风险。

偏离标准

允许为了某些特殊情况偏离标准,比如将 int 类型值强制转为指针来实现访问内存地址空间映射的 I/O 端口:

1
2
3
4
// 内存中的0x0002地址内数据映射了某一I/O端口数据
#define PORT (*(volatile unsigned char *)0x0002)
// 修改该位置数据就相当于修改了该I/O端口数据
PORT = 0x10u;

需要有专门的方式记录这种不得不违反 MISRA C 的地方。当然最好不要有这种违反的地方。

准则介绍

每项 MISRA C 准则(guidelines)都被分为 “规则(Rule)“或 “指示(Directive)”:

指示是指无法提供必要的完整描述来进行合规性检查的准则。为了能够执行检查,需要额外的信息,如设计文档或需求规格中可能提供的信息。静态分析工具可以帮助检查是否符合指示,但不同的工具对不符合指示的解释可能大相径庭。

规则是对要求进行完整描述的准则。检查源代码是否符合规则应该是可能的,而不需要任何其它信息。特别是,静态分析工具应该能够检查规则的合规性,但必须遵守第 6.5 节中描述的限制。

注意后续的准则、规则、指示名词

准则分类

每条准则被分为:

  • 强制(mandatory):不允许违反
  • 必要(required):只有在有明确限制、要求和预防措施的偏离情况下才能违反
  • 建议(advisory):在合理可行的范围内遵循建议

他们的重要程度相同,区别只是是否允许偏离标准

可判定性(Decidability)

规则(Rule)分为可判定的(decidable)不可判定的(undecidable):

  • 可判定的: 可由静态分析工具明确给出是否符合 MISRA C 的结论(是或否)
  • 不可判定的: 不能给出明确结论,比如有些需要在编译、链接阶段或运行时才能分析出

分析的作用域(Scope)

规则(Rule)的分析的作用域分为单一翻译单元(Single Translation Unit)系统(System)

也就是根据各种变量、函数的作用域来确定规则分析时的作用域。

1
2
3
4
5
6
7
8
extern void f(uint16_t *p);
uint16_t y;
void g(void)
{
    uint16_t x; /* x is not given a value */
    f(&x);      /* f might modify the object pointed to by its parameter */
    y = x;      /* x may or may not be unset */
}

多来源项目

项目中的代码来源于多个公司(组织):

  • 标准库来源于编译器
  • 底层驱动来源于设备厂家
  • 操作系统和上层驱动来源于特定供应厂家
  • 应用代码来自于其他厂家

特别是标准库和底层代码为了高性能会用到很多汇编以及偏离准则部分,这部分不需要符合 MISRA C 规范。

其他代码尽可能符合 MISRA C,如果推动第三方厂家配合较为困难,至少头文件(接口)要符合 MISRA C。

自动生成的代码

项目中自动生成的代码也需要遵守 MISRA C

准则格式

IdentRequirement text 
  Source ref
CategoryCategory 
AanalysisDecidability,Scope 
Applies toCxx 

引用来源

ISO C

MISRA C 引用了 C90 和 C99,注意以下的一些行为:

  • Unspecified: 未明确行为是指在 C 语言标准中没有明确规定其具体行为的情况。这意味着编译器可以根据实现的特定规则来定义其行为,但在不同的编译器或平台上可能会有不同的结果。如 x=f(&a)+g(&a),f 与 g 的执行顺序是未明确的,而且其执行顺序可能影响到 x 的最终结果。
  • Undefined: 未定义行为是指当程序违反了 C 语言标准规范,导致编译器无法确定其具体行为时发生的情况。例如,对指针进行未初始化的解引用、数组越界访问、除以零等操作都属于未定义行为。最重要的是编译器没有责任去检查这些错误,导致这些问题无法在编译阶段暴露。
  • Implementation-defined": 实现定义行为是指 C 语言标准规定了多个可能的行为,但具体的行为由编译器或平台的实现决定。这意味着在不同的编译器或平台上,同一段代码可能会有不同的行为,但这些行为都是符合标准的。应尽量避免该行为来保证代码在不同编译器上的一致性和可移植性。
  • Locale: 本地化行为,和 C 语言的本地化相关,比如字符的使用习惯、日期格式等,这里不涉及。

指示(Directives)

TODO

规则(Rule)

组别 1:标准 C 环境

Rule 1.1

(必要) 程序不得违反标准 C 语法和约束,并且不得超出具体实现的编译限制

一般不做静态检查,由编译器保证

Rule 1.2

(建议) 不应该使用语言扩展

但是嵌入式领域中一般都需要用到语言扩展,因为其对于性能的要求可能会高于可移植性,如果需要偏离标准,需要在文档中列出所使用的所有语言扩展。

Rule 1.3

(必要) 不得发生未定义或严重的未指定行为

ISO C,这些行为都由后续的准则来避免,不需要特意检查本准则。

组别 2:未使用的代码

关于 Rule 2.3-2.7 对于未使用的一些声明,审核者难以判断到底是真的没用到,还是该用到的时候误用了其他的声明。为了避免歧义,直接禁止使用未用到的声明,这样就可以直接判断所有未用到的声明都是因为误用导致的。

Rule 2.1

(必要) 项目不得包含不可达代码(unreachable code)

原因是这些代码会占用资源:

  • 它占用了目标机器内存中的空间(包括代码空间);
  • 它的存在可能导致编译器在围绕不可达代码传输控制时选择更长、更慢的跳转指令;
  • 在循环中,它可能导致整个循环无法驻留在指令缓存中。

以下是原则:

  1. 如果代码属于预编译前的,预编译后不存在,则无需适用本规则。
  2. 如果这类代码是防御性编程的一部分,目的是防御未定义行为,那根本不需要防御这种错误,因为这由Rule 1.3所要求的程序中不能存在未定义行为的准则保证。
  3. 如果是为了防御其他重要故障(如硬件故障、内存被意外覆盖),那是有必要存在的,有两种措施:

    • 偏离标准
    • 让编译器认为该分支可达从而防止部分 case 语句被优化掉,一般通过将参数转为易失性访问(volatile access)的左值(lvalue):

    观察下面代码,可以确定“x”永远不会具有“Err_2”的值,可能编译器也可以识别到,甚至可能识别到也不会具有 “Err_1”的值,f(5)返回的值将始终具有 “Success” 的值。所以 case Err_2 和 case Err_1 可能会被部分编译器优化掉。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    
    #include "stdafx.h"
    typedef enum ErrStatus {
        Success = 0,
        Err_1,
        Err_2
    } ErrStatus;
    
    ErrStatus f(int x) {
        if (x < 0) {
            return Err_1;
        }
        else
        {
            return Success;
        }
    }
    
    int main()
    {
        ErrStatus x = f(5);
        switch (x)
        {
        case Err_1:
            printf("err 1"); break;
        case Err_2:
            printf("err 2 "); break;  /* Is this dead code ? */
        default:
            printf("Success"); break;
        }
    }
    

    虽然逻辑上不可达,但为了防御可能产生的硬件错误导致的值的变更,或者 x 所在的内存地址的数据被意外覆盖,该分支不能被优化,所以通过使用*(volatile ErrStatus *)&x的方式将 x 转为 vollatile 类型的左值,强制编译器不对该值做任何假定(让编译器认为 switch 的参数是一段 volatile 内存空间而不是 x 变量,作为内存空间其就有可能为任何值,而不是与变量 x 的逻辑关系绑定。)

    1
    
    switch (*(volatile ErrStatus *)&x)
    

Rule 2.2

(必要) 不得有无效代码(dead code)

也可以叫冗余的代码,也就是删除后不会对程序产生任何影响的代码。

语言扩展不属于无效代码,因为其总是有明确意义的。

注意未使用代码不是无效代码,区别是未使用代码本身就不会被执行到,而无效代码是能被执行到的。

1
2
3
4
5
6
7
8
9
10
void g(void)
{
    // 空操作,但g()函数不是无效代码,因为它会被h()调用到
    // 删除后程序编译就无法通过
}
void h(void)
{
    g(); // 无效代码,因为其没有明确意义,删除对程序无影响
    __asm("NOP"); // 有效,虽然和g()一样是空操作,但这是语言扩展,其有明确意义(一个延迟操作)。
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
extern volatile uint16_t v;
extern char *p;
void f(void)
{
    uint16_t x;
    (void)v;     /* Compliant - 这种方式用于抑制编译器的未使用告警,是有意义的
                              ,如果删除就会产生编译器告警,不视为dead code */
    (int32_t) v; /* Non-compliant - the cast operator is dead */
    v >> 3;      /* Non-compliant - the >> operator is dead */
    x = 3;       /* Non-compliant - the = operator is dead
                * - x is not subsequently read */
    *p++;        /* Non-compliant - result of * operator is not used */
    (*p)++;      /* Compliant - *p is incremented */
}

Rule 2.3

(建议) 项目不应包含未被使用的类型(type)声明

1
2
3
4
5
int16_t unusedtype(void)
{
    typedef int16_t local_Type; /* Non-compliant */
    return 67;
}

Rule 2.4

(建议) 项目不应包含未被使用的类型标签(tag)声明

1
2
3
4
void unusedtag(void)
{
    enum state { S_init, S_run, S_sleep }; /* Non-compliant,一个匿名 enum,但 state 标签未被使用 */
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
typedef struct record_t /* Non-compliant,因为record_t这个标签后面没用到 */
{
    uint16_t key;
    uint16_t val;
} record1_t;
typedef struct
{
    uint16_t key;
    uint16_t val;
} record2_t;

// 如果想要上面的示例合规:
// 方式一:需要修改record2_t声明用到这个tag
typedef struct record_t record2_t;
// 方式二:去掉record1_t声明时的标签
typedef struct
{
    uint16_t key;
    uint16_t val;
} record1_t;

关于标签的使用方式,可见C 语言中结构体标签的使用

Rule 2.5

(建议) 项目不应包含未被使用的宏(macro)声明

1
2
3
4
5
6
7
void use_macro(void)
{
#define SIZE 4
/* Non-compliant - DATA not used */
#define DATA 3
    use_int16(SIZE);
}

Rule 2.6

(建议) 函数不应包含未被使用的执行标签(label)声明

1
2
3
4
5
6
void unused_label(void)
{
    int16_t x = 6;
label1: /* Non-compliant */
    use_int16(x);
}

Rule 2.7

(建议) 函数中不应有未使用的参数

大多数函数都会指定使用每个参数。如果函数参数未被使用,则可能是函数的实现与其规格不符。本规则强调了这种潜在的不匹配。

1
2
3
4
5
void withunusedpara(uint16_t * para1,
                    int16_t unusedpara) /* Non-compliant - unused */
{
    *para1 = 42U;
}

组别 3:注释

Rule 3.1

(必要) 字符序列“/*”和“//”不得在注释中使用

如果在 /* */风格注释中出现“/*”和“//”,可能原因是确实需要,还有可能是因为漏写了’*/‘,为了避免混淆就禁止该写法。

例外情况是允许在“//”风格注释中使用“//

错误,漏写了*/,导致函数被注释无法执行:

1
2
3
/* some comment, end comment marker accidentally omitted
Perform_Critical_Safety_Function( X );
/* this comment is non-compliant */

修正:

1
2
3
/* some comment, end comment marker accidentally omitted */
Perform_Critical_Safety_Function( X );
/* this comment is non-compliant */

错误,产生歧义,+ z这行可能被注释掉:

1
2
3
4
x = y // /*
    + z
    // */
    ;

Rule 3.2

(必要) “//”注释中不得使用换行(即“//”注释中不得使用行拼接符“\”)

错误,if(b)这行直接变成了注释:

1
2
3
4
5
6
7
8
9
extern bool_t b;
void f(void)
{
    uint16_t x = 0; // comment \
    if (b)
    {
        ++x; /* This is always executed */
    }
}

组别 4:字符集和词汇约定(Character sets and lexical conventions)

Rule 4.1

(必要) 八进制和十六进制转译序列应有明确的终止识别标识

防止混淆如’\x1f‘和’\x1’+’f‘。

1
2
3
4
5
const char *s1 = "\x41g"; /* Non-compliant */
const char *s2 = "\x41" "g"; /* Compliant - 使用分隔的方式终止 */
const char *s3 = "\x41\x67"; /* Compliant - 使用\x终止 */
int c1 = '\141t';            /* Non-compliant */
int c2 = '\141\t';           /* Compliant - 使用\t终止 */

Rule 4.2

(建议) 禁止使用三字符组(trigraphs)

trigraphs 的定义见[C99] 5.2.1.1,代码中由 3 个字符表示,由编译器在预处理阶段转义为特定单个字符:

1
2
3
??= #   ??( [   ??/ \
??) ]   ??' ˆ   ??< {
??! |   ??> }   ??- ˜

比如想用??-??-??表示一个日期的示例,但编译器会将这个字符串转义:

1
const char * datestring = "(Date should be in the form ??-??-??)";

组别 5:标识符(Identifiers)

见[C99] 6.2.1 Scopes of identifiers

标识符可以表示:

  • 一个对象
  • 一个函数
  • 一个标记(tag)或结构体、联合体或枚举的一个成员
  • 一个类型定义(typedef)名称
  • 一个标签(label)名称
  • 一个宏名称
  • 一个宏参数。

标签(label):标签就是 goto 用的标签,比如定义L1:,然后goto L1

注意宏会在编译前展开所以不讨论宏名称和宏参数

对于标识符指定的每个不同实体(entity),标识符只能在称为其范围的程序文本区域内可见(即可以使用)。实体的标识符在其作用域(scope)和命名空间(name spaces)内联合唯一

四种作用域

  • 函数(function)
  • 文件(file)
  • 块(block)
  • 函数原型(function prototype,函数原型是一个函数的声明,它声明了函数参数的类型)。

作用域和标识符:

  • label 名称是唯一一种具有函数作用域的标识符。它可以在所有函数中出现的任何地方使用(可以从一个函数 goto 到另一个函数),并通过其语法外观(后面跟一个 : 和一个语句)隐式声明。

  • 其他标识符的作用域由其声明(声明符(declarator)或类型规范符(type specifier))的位置决定:

    • 如果出现在程序块或形参列表之外,则标识符具有文件作用域,该作用域在翻译单元结束时终止。
    • 如果出现在程序块或形参列表之内,标识符具有代码块作用域,在相关代码块结束时终止。
    • 如果出现在函数原型(不是函数定义的一部分)的形参声明列表中,则标识符具有函数原型作用域,该作用域在函数声明符结束时终止。
    • 如果标识符指定了同一命名空间中的两个不同实体,其作用域可能会重叠(作用域不能相同,但可以重叠)。如果是这样,一个实体的作用域(内层作用域)将是另一个实体的作用域(外层作用域)的严格子集。在内部作用域中,标识符指定在内部作用域中声明的实体;在外部作用域中声明的实体在内部作用域中隐藏(不可见)。

命名空间

如果在一个翻译单元的任何一点上可以看到不止一个特定标识符(同一作用域)的声明,句法上下文就会对指代不同实体的使用进行消歧。因此,各类标识符都有独立的命名空间,如下所示:

  • 标签(label)名称(通过标签声明和使用的语法来消除歧义);
  • 结构、联合和枚举的标记(tag)(通过 struct、union 或 enum 关键字的来消除歧义);
  • 结构或联合的成员;每个结构或联合的成员都有单独的命名空间(通过 .-> 操作符访问成员时所用表达式的类型来消除歧义);
  • 所有其他标识符,称为普通标识符(在普通声明符中声明或作为枚举常量声明)。

也就是说在同一个作用域内还是允许有两个不同的实体有同一个标识符的,只要它们的命名空间类别不同,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <stdio.h>

struct fun
{
    int a;
    int b;
};

// 在同一个文件作用域下有两个实体的标识符相同,
// 但fun tag属于“标记”命名空间,fun函数属于“普通”命名空间,
// 它们之间没有歧义,允许使用同一标识符
void fun()
{
    printf("hahaha\n");
}

// 此时如果增加这个fun外部链接变量和fun函数作用域相同,
// 则因为和fun函数同属“普通”命名空间,将会存在歧义。
// gcc报错:error: ‘fun’ redeclared as different kind of symbol
int fun = 10;

int main()
{
    return 0;
}

标识符的链接性

不同作用域中声明的标识符或在同一作用域中声明多次的标识符,可以通过一个称为链接的过程来指代同一个对象或函数(这样标识符在同一作用域同一命名空间内就可以重复,因为表示同一个实体)。链接性有三种:

  • 外部链接(external):

    对于在可见标识符先前声明的作用域中用存储类(storage-class)规范符 extern 声明的标识符:

    • 如果先前声明指定了内部或外部链接,则标识符在后一次声明中的链接性与先前声明中指定的链接性相同;
    • 如果先前的声明不可见,或先前的声明没有指定链接性,则标识符具有外部链接。

    在构成整个程序的一组翻译单元和库中,每个具有外部链接的特定标识符的声明都表示同一个的对象或函数;

  • 内部链接(internal)

    在文件作用域内使用存储类(storage-class)规范符 static 修饰的对象或函数声明具有内部链接。

    在一个翻译单元内,每个具有内部链接的标识符声明都表示同一个的对象或函数;

  • 无链接(none)

    下列标识符为无链接:

    • 声明为对象或函数以外的标识符;
    • 声明为函数形参的标识符;
    • 块作用域内声明的没有 extern 的对象的标识符(包括无修饰或 static 修饰的)

    无链接的标识符的每次声明都表示一个独立的实体。

如果函数标识符的声明中没有存储类说明符,则其链接方式与使用存储类说明符 extern 声明的方式完全相同(取决于前面声明时的链接性);如果对象的标识符的声明具有文件作用域且没有存储类指定符,则其链接方式为外部链接

如果在一个翻译单元中,同一个标识符同时出现在内部和外部链接中,则行为未定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <stdio.h>
// -------------------------
static int fun;
int fun; // error: non-static declaration of ‘fun’ follows static declaration
// -------------------------
int fun;
static int fun; // error: static declaration of ‘fun’ follows non-static declaration
// -------------------------
extern int fun;
static int fun; // error: static declaration of ‘fun’ follows non-static declaration
// -------------------------
static int fun;
extern int fun; // 内部链接
// -------------------------
extern int fun;
extern int fun; // 外部链接
// -------------------------
extern int fun;
int fun; // 外部链接
// -------------------------
int fun;
extern int fun; // 外部链接

int main()
{
    return 0;
}

Rule 5.1

(必要) 外部链接标识符不得重名

在 C99 中规定外部链接标识符的有效识别长度为 31 个字符(是否大小写敏感取决于编译器),也就是前 31 个字符需要唯一,才能区分两个外部链接标识符表示不同的项。

当前 31 个字符相同时(即使后面字符不同),行为未定义。

长标识符可能会影响代码的可读性。

注:在 C99 中,如果一个扩展源字符出现在外部标识符中,而该字符没有相应的通用字符,则《标准》未规定它占用多少字符。

扩展源字符(Extended Source Character)是指在 C99 中引入的一种字符表示方式,它允许在标识符和字符串中使用一些特殊字符,包括一些非 ASCII 字符,如特殊符号、非拉丁字母、汉字等(GCC 好像不支持),如int 数字123 = 42;通用字符(Universal Character)是一种在 C99 中引入的字符表示方式,用于表示 Unicode 字符。通用字符以\u\U 为前缀,后面跟着 Unicode 编码点,用来表示任意 Unicode 字符,如 int \u6570\u5b57123 = 42;

1
2
3
4
5
6
/*      1234567890123456789012345678901********* Characters */
int32_t engine_exhaust_gas_temperature_raw;
int32_t engine_exhaust_gas_temperature_scaled; /* Non-compliant,两个变量名的前31个字符相同 */
/*      1234567890123456789012345678901********* Characters */
int32_t engine_exhaust_gas_temp_raw;
int32_t engine_exhaust_gas_temp_scaled; /* Compliant */

大小写不敏感时以下示例也不合规:

1
2
3
4
5
/* file1.c */
int32_t abc = 0;

/* file2.c */
int32_t ABC = 0;

Rule 5.2

(必要) 同作用域(scope)和命名空间内的标识符不得重名

根据《标准》同作用域和命名空间内的不同实体不能有多个声明的标识符重名,同一实体可以有多个声明具有重名标识符(但可能有未定义行为),本规则同时禁止这两种情况,规避所有未定义行为。

C99 中非外部链接(包括内部链接和无链接)标识符的有效识别字符数量为 63 个,C90 为 31 个。

为了和 C90 兼容,建议在使用中有效识别字符规定降为 31 个,后面都以 31 个字符上限为例。

1
2
3
4
5
6
7
8
9
10
11
12
13
/*             1234567890123456789012345678901********* Characters */
extern int32_t engine_exhaust_gas_temperature_raw;
static int32_t engine_exhaust_gas_temperature_scaled; /* Non-compliant,和 engine_exhaust_gas_temperature_raw
                                                       * 位于都位于“普通”命名空间和“文件”作用域,前31个字符相同违规 */
void f(void)
{
    /*      1234567890123456789012345678901********* Characters */
    int32_t engine_exhaust_gas_temperature_local; /* Compliant,本“块”作用域内其唯一,但其实覆盖了其他外面的标识符,
                                                   * 符合5.2但不符合5.3 */
}
/*             1234567890123456789012345678901********* Characters */
static int32_t engine_exhaust_gas_temp_raw;
static int32_t engine_exhaust_gas_temp_scaled; /* Compliant */

Rule 5.3

(必要) 内部声明的标识符不得隐藏外部声明的标识符

根据《标准》,如果在内层作用域中声明了标识符,但该标识符与外层作用域中已存在的标识符不一致,那么最内层的声明将 “隐藏“外层的声明。这可能会引起开发人员的混淆。

注意:在一个命名空间中声明的标识符不会隐藏在另一个命名空间中声明的标识符(因为命名空间之间并没有层次联系)(但违反 Rule 5.9)。所以隐藏只发生在有层次包含关系的作用域中。编写本规则的原因不同于 Rule 5.1 和 Rule 5.2 的未定义行为,只是为了防止这种隐式的覆盖导致的混淆。

外作用域和内作用域的定义如下:

  • 具有文件作用域的标识符可视为具有最外层的作用域;
  • 具有块作用域的标识符具有更内层的作用域;
  • 连续嵌套的块引入了更多的内层作用域
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void fn1(void)
{
    int16_t i; /* Declare an object "i" */
    {
        int16_t i; /* Non-compliant - hides previous "i" ,第三种情况连续嵌套块*/
        i = 3;     /* Could be confusing as to which "i" this refers */
    }
}
struct astruct
{
    int16_t m;
};
extern void g(struct astruct *p);
int16_t xyz = 0;             /* Declare an object "xyz" */
void fn2(struct astruct xyz) /* Non-compliant - outer "xyz" is
                              * now hidden by parameter name */
{
    g(&xyz);
}
uint16_t speed;
void fn3(void)
{
    typedef float32_t speed; /* Non-compliant - type hides object */
}

Rule 5.4

(必要) 宏名称不得重名

如果两个宏名称仅在非标识字符上(类似 Rule 5.2 在 C99 中也是 63 个字符)存在差异,则行为未定义。

下面也是以 31 个标识字符为例:

1
2
3
4
5
6
/*      1234567890123456789012345678901********* Characters */
#define engine_exhaust_gas_temperature_raw egt_r
#define engine_exhaust_gas_temperature_scaled egt_s /* Non-compliant */
/*      1234567890123456789012345678901********* Characters */
#define engine_exhaust_gas_temp_raw egt_r
#define engine_exhaust_gas_temp_scaled egt_s /* Compliant */

Rule 5.5

(必要) 宏名称与其他标识符不得重名

《标准》未讨论预处理前标识符情况,本规则将对其做限制。

该规则要求预处理前存在的宏名称必须与预处理后存在的标识符不同(因为预编译后宏本来就不存在了)。该规则的原因并非之前 5.1、5.2 提到的重名未定义问题,主要是为了避免产生混淆

下面的不合规,本规则中宏名称指的是不包含括号的部分,也就是宏名称是Sum而不是Sum(x, y),所以它们重名了:

1
2
#define Sum(x, y) ( ( x ) + ( y ) )
int16_t Sum;

以下不合规,因为它们重名(按照 31 个字符的标准)了:

1
2
3
/*             1234567890123456789012345678901********* Characters */
#define        low_pressure_turbine_temperature_1 lp_tb_temp_1
static int32_t low_pressure_turbine_temperature_2;

Rule 5.6

(必要) typedef 名称应是唯一标识符

typedef 名称在所有命名空间和翻译单元中都必须是唯一的。只有当 typedef 是在头文件中进行的,且该头文件被包含在多个源文件中时,本规则才允许重复声明同一 typedef 名。

包括和另一个 typedef 名称相同,以及和其他的标识符名称相同,目的是防止重复的命名产生混淆。

例外

与本 typedef 相关的 struct、union 或 enum 的 tag 名称可以和本 typedef 名称相同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
void func(void)
{
    {
        typedef unsigned char u8_t;
    }
    {
        typedef unsigned char u8_t; /* Non-compliant - reuse */
    }
}
typedef float mass;
void func1(void)
{
    float32_t mass = 0.0f; /* Non-compliant - reuse */
}
typedef struct list
{
    struct list *next;
    uint16_t element;
} list; /* Compliant - 例外 */
typedef struct
{
    struct chain
    {
        struct chain *list;
        uint16_t element;
    } s1;
    uint16_t length;
} chain; /* Non-compliant - tag "chain" not
          * associated with typedef */

Rule 5.7

(必要) 标签(tag)名称应是唯一标识符

同 Rule 5.6

例外

tag 名可以和与之相关的 typedef 名称相同(见 Rule 5.6 例外)

1
2
3
4
5
6
7
8
9
struct stag
{
    uint16_t a;
    uint16_t b;
};
struct stag a1 = {0, 0}; /* Compliant - compatible with above */
union stag a2 = {0, 0};  /* Non-compliant - declares different type
                          * from struct stag.
                          * Constraint violation in C99 */
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
struct deer
{
    uint16_t a;
    uint16_t b;
};
void foo(void)
{
    struct deer
    {
        uint16_t a;
    }; /* Non-compliant - tag "deer" reused */
}
typedef struct coord
{
    uint16_t x;
    uint16_t y;
} coord; /* Compliant by Exception */
struct elk
{
    uint16_t x;
};
struct elk /* Non-compliant - declaration of different type
            * Constraint violation in C99 */
{
    uint32_t x;
};

Rule 5.8

(必要) 外部链接(external linkage)对象和函数的标识符应是唯一的

《标准》规定外部链接的对象和函数标识符允许在其他命名空间(标签、标记、成员)中或其他作用域(块、函数原型、函数)中使用作为其他实体的标识符,本规则将禁止这种情况。

同 Rule 5.6,作为外部链接实体使用的标识符不得在任何命名空间或翻译单元中用于任何其他目的,即使该标识符表示的对象没有任何联系

以这种方式确保标识符名称的唯一性有助于避免混淆。无链接的对象的标识符不必是唯一的,因为这种混淆的风险很小。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* file1.c */
int32_t count; /* "count" has external linkage,外部链接变量 */
void foo(void) /* "foo" has external linkage,外部链接函数 */
{
    int16_t index; /* "index" has no linkage */
}

/* file2.c */
static void foo(void) /* Non-compliant - "foo" is not unique
                       * (it is already defined with external
                       * linkage in file1.c),将外部链接函数foo
                       * 标识符用于了其他目的 */
{
    int16_t count; /* Non-compliant - "count" has no linkage
                    * but clashes with an identifier with
                    * external linkage,块作用域中也不能使用 */
    int32_t index; /* Compliant - "index" has no linkage */
}

Rule 5.9

(建议) 内部链接(internal linkage)对象和函数的标识符应是唯一的

其实类似于 Rule 5.8。两种情况:

  • 两个文件中的内部链接对象或函数重名
  • 同个文件中内部链接对象或函数名称被用于其他目的,如函数内声明的变量。

本条规则属于建议,因为不同于 Rule 5.8,实际上两个文件之间的内部链接对象或函数并没有实际关系,发生混淆的情况并不会多。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/* file1.c */
static int32_t count; /* "count" has internal linkage */
static void foo(void) /* "foo" has internal linkage */
{
    int16_t count; /* Non-compliant - "count" has no linkage
                    * but clashes with an identifier with
                    * internal linkage */
    int16_t index; /* "index" has no linkage */
}
void bar1(void)
{
    static int16_t count; /* Non-compliant - "count" has no linkage
                           * but clashes with an identifier with
                           * internal linkage */
    int16_t index;        /* Compliant - "index" is not unique but
                           * has no linkage */
    foo();
}
/* End of file1.c */

/* file2.c */
static int8_t count;  /* Non-compliant - "count" has internal
                       * linkage but clashes with other
                       * identifiers of the same name */
static void foo(void) /* Non-compliant - "foo" has internal
                       * linkage but clashes with a function of
                       * the same name */
{
    int32_t index;  /* Compliant - both "index" and "nbytes" */
    int16_t nbytes; /* are not unique but have no linkage */
}
void bar2(void)
{
    static uint8_t nbytes; /* Compliant - "nbytes" is not unique but
                            * has no linkage and the storage class is
                            * irrelevant */
}
/* End of file2.c */

组别 6:类型

Rule 6.1

(必要) 位域(Bit-fields)仅允许使用适当的类型来声明(位域成员类型限制)

允许的位域类型是

  • C90: 无符号 int 或有符号 int;
  • C99: 以下类型之一:
    • 无符号 int 或有符号 int;
    • 实现允许的另一种显式有符号或显式无符号整数类型;
    • _Bool。

注意:允许使用 typedef 来指定适当的类型

int 是由实现定义的,因为 int 类型的位可以是有符号或无符号的,所以不能直接使用 int 作为位域类型。

在 C90 中,不允许在位元组中使用 enum、short、char 或任何其他类型,因为其行为是未定义的;在 C99 中,实现可以定义其他允许在位元组声明中使用的整数类型。

1
2
3
4
5
6
7
8
9
10
11
12
typedef unsigned int UINT_32;

struct s
{
    unsigned int b1:2; /* Compliant */
    int          b2:2; /* Non-compliant - plain int not permitted */
    UINT_32      b3:2; /* Compliant     - typedef designating unsigned int */
    signed long  b4:2; /* Non-compliant - even if long and int are the same size */
                        /* C90: always non-compliant                              */
                        /* C99: non-compliant if "signed long" is not a permitted
                                by implementation                                 */
};

Rule 6.2

(必要) 单比特(single-bit)位域成员不可声明为有符号类型

有符号类型的首位用于表示符号,单 bit 情况下无法表示有符号类型(符号+值至少两 bit)

注意:本规则不适用于未命名的位字段,因为无法访问它们的值。

组别 7:字面值(Literals)和常量

Rule 7.1

(必要) 禁止使用八进制常数

C 语言中表示八进制就是在数字前加个0,比如052表示十进制的42。但编程时可能误认为是十进制的,产生混淆。

注意:此规则不适用于八进制转义序列,因为使用前导 \ 字符意味着混淆的范围较小

例外:单个数字 0 在定义上是八进制的 0,当然理解成十进制的 0 也没什么关系,所以允许这种情况。

1
2
3
4
5
extern uint16_t code[10];
code[1] = 109; /* Compliant - decimal 109 */
code[2] = 100; /* Compliant - decimal 100 */
code[3] = 052; /* Non-Compliant - decimal 42 */
code[4] = 071; /* Non-Compliant - decimal 57 */

Rule 7.2

(必要) 后缀“u”或“U”应使用于所有无符号的整数常量

注意:常量不是常变量(const 修饰的叫常变量)

本规则适用于:

  • 出现在 #if#elif 预处理指令控制表达式中的整数常量;
  • 预处理后存在的任何其他整数常量

注意:预处理期间,整数常量的类型的确定方式与预处理后相同,只是:

  • 所有有符号整数类型的行为就像它们是 long(C90)或 intmax_t(C99);
  • 所有无符号整数类型的行为就像它们是 unsigned(C90)或 uintmax_t(C99)。

整数常量的类型可能会引起混淆,因为它取决于一系列复杂的因素,包括:

  • 常量的大小;
  • 整数类型的实现大小;
  • 是否存在任何后缀;
  • 表示值的数基(即十进制、八进制或十六进制)

例如,整数常量 40000 在 32 位环境中属于带符号 int 类型,但在 16 位环境中属于 signed long 类型。数值 0x8000 在 16 位环境中属于 unsigned int 类型,但在 32 位环境中属于 signed int 类型。

在 2-bit int 和 64-bit long 环境中:

1
2
3
4
5
6
7
8
9
void R_7_2(void)
{
    use_int32(2147483647);   /* int constant */
    use_int32(0x7FFFFFFF);   /* int constant */
    use_int64(2147483648);   /* long constant */
    use_uint32(2147483648U); /* unsigned int constant */
    use_uint32(0x80000000);  /* unsigned int constant -  Non-compliant */
    use_uint32(0x80000000U); /* unsigned int constant */
}

Rule 7.3

(必要) 小写字符“l”不得作为常量的后缀使用(仅可使用“L”)

避免与“l”(字母)与“1”(数字)产生歧义。

1
2
3
4
5
6
7
8
9
10
const int64_t a = 0L;
const int64_t b = 0l;        /* Non-compliant */
const uint64_t c = 0Lu;
const uint64_t d = 0lU;      /* Non-compliant */
const uint128_t e = 0ULL;
const uint128_t f = 0Ull;     /* Non-compliant */
const int128_t g = 0LL;
const int128_t h = 0ll;      /* Non-compliant */
const float128_t m = 1.2L;
const float128_t n = 2.4l;   /* Non-compliant */

Rule 7.4

(必要) 除非对象的类型为“指向 const char 的指针”,否则不得将字符串常量赋值给该对象

常量存放在不可修改的内存区域,如果被可修改的 char 指针指向,如果对其修改,就会产生错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
extern void f1(char *s1);

extern void f2(const char *s2);

static void g2(void)
{
    f1("string"); /* Non-compliant,形参为非const,实参是字符串常量 */
    f2("string"); /* Compliant     */
}

static char *name1(void)
{
    return ("MISRA"); /* Non-compliant,返回参数类型非const */
}

static const char *name2(void)
{
    return ("MISRA"); /* Compliant*/
}

void R_7_4(void)
{
    char *s = "string"; /* Non-compliant */

    const volatile char *p = "string"; /* Compliant     */

    "0123456789"[0] = '*'; /* Non-compliant,未定义行为 */

    g2();
    (void)name1();
    (void)name2();

    use_const_char_ptr(s);
    use_const_volatile_char_ptr(p);
}

组别 8:声明与定义

函数定义(function-definition):

  • declaration-specifiers declarator declaration-list(opt) compound-statement
    • declaration-list:
      • declaration
      • declaration-list declaration

声明说明符(declaration-specifiers,各个声明符的顺序没有明确要求):

  • storage-class-specifier declaration-specifiers(opt)
  • type-specifier declaration-specifiers(opt)
  • type-qualifier declaration-specifiers(opt)
  • function-specifier declaration-specifiers(opt)

存储类说明符(storage-class-specifier,最多一个):

  • typedef
  • extern
  • static
  • auto
  • register

类型说明符(Type specifiers,C99 有且仅有一个,C90 允许隐式为 int):

  • void
  • char
  • short
  • int
  • long
  • float
  • double
  • signed
  • unsigned
  • _Bool
  • _Complex
  • _Imaginary
  • struct-or-union-specifier
  • enum-specifier
  • typedef-name

函数说明符(function-specifier):

  • inline(C99 新增,可以有多个,效果和一个相同)

类型限定符(type-qualifier):

  • const
  • restrict(不能用于函数定义)
  • volatile

Rule 8.1

(必要) 类型须明确声明

C90 标准允许在某些情况下省略类型,在这种情况下,int 类型是隐式指定的。可能使用隐式 int 的情况举例如下:

  • 对象声明;
  • 参数声明;
  • 成员声明;
  • 类型定义声明;
  • 函数返回类型。

但省略会引起混淆。在 C99 中将该情况删除了,使用 GCC 指定 C99 标准时将会报 warning。

c99functiontype

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
extern x;            /* Non-compliant - implicit int type */
extern int16_t x_ok; /* Compliant - explicit type */

extern f(void);            /* Non-compliant - implicit int return type */
extern int16_t f_ok(void); /* Compliant */

extern void g(char c, const k);              /* Non-compliant - implicit int for parameter k */
extern void g_ok(char c2, const int16_t k2); /* Compliant */

typedef (*pfi)(void);            /* Non-compliant - implicit int return type */
typedef int16_t (*pfi_ok)(void); /* Compliant */

typedef void (*pfv)(const x);       /* Non-compliant  - implicit int for parameter x */
typedef void (*pfv_ok)(int16_t xx); /* Compliant */

void R_8_1(void)
{
    const y;            /* Non-compliant - implicit int type */
    const int16_t y_ok; /* Compliant     - explicit type */

    struct
    {
        int16_t x1; /* Compliant */
        const y1;   /* Non-compliant - implicit int for member y */
    } s =
        {1, 2};

    pfi F1 = &get_int32;
    pfi_ok F2 = &get_int16;

    pfv F11 = &use_int32;
    pfv_ok F22 = &use_int16;

    F11(F1() + s.y1);
    F22(F2() + s.x1);
}

Rule 8.2

(必要) 函数类型应为带有命名形参的原型形式

C 语言的早期版本通常被称为 K&R C [30],它没有提供根据相应参数检查参数个数或参数类型的机制。对象或函数的类型在 K&R C 中无需声明,因为对象的默认类型和函数的默认返回类型都是 int。

C90 标准引入了函数原型,这是一种声明参数类型的函数声明器。这样就可以根据参数类型检查参数类型。它还允许对参数个数进行检查,除非函数原型规定参数个数是可变的。出于与现有代码向后兼容的考虑,C90 标准没有要求使用函数原型。出于同样的原因,它继续允许省略类型,在这种情况下,类型默认为 int

C99 标准从语言中删除了默认的 int 类型,但继续允许 K&R 风格的函数类型,即在声明中不提供参数类型信息,而在定义中提供参数类型信息则是可选的。

参数数量、参数类型以及函数的预期返回类型和实际返回类型之间的不匹配有可能导致未定义的行为。本规则以及规则 8.1 和规则 8.4 的目的是通过要求明确说明参数类型和函数返回类型来避免这种未定义的行为。规则 17.3 确保在函数调用时可以获得这些信息,从而要求编译器对检测到的任何不匹配进行诊断。

该规则还要求为声明中的所有参数指定名称。参数名称可以提供有关函数接口的有用信息,如果声明和定义不匹配,则可能表明存在编程错误。

注意:空参数列表在原型中无效。如果函数类型没有参数,其原型形式将使用关键字 void。所以不允许空参数情况,函数原型至少加上 void。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* Compliant */
extern int16_t func1(int16_t n);
/* Non-compliant - parameter name not specified */
extern void func2(int16_t);
/* Non-compliant - not in prototype form */
static int16_t func3();
/* Compliant - prototype specifies 0 parameters */
static int16_t func4(void);
/* Compliant */
int16_t func1(int16_t n)
{
    return n;
}
/* Non-compliant - old style identifier and declaration list */
static int16_t func3(vec, n)
int16_t *vec;
int16_t n;
{
    return vec[n - 1];
}
1
2
3
4
5
6
7
8
/* Non-compliant - no prototype */
int16_t (*pf1)();
/* Compliant - prototype specifies 0 parameters */
int16_t (*pf1)(void);
/* Non-compliant - parameter name not specified */
typedef int16_t (*pf2_t)(int16_t);
/* Compliant */
typedef int16_t (*pf3_t)(int16_t n);

Rule 8.3

(必要) 对象或函数(包括形参)的所有声明均应使用相同的名称和类型限定符

这个规定不包括存储类说明符(Storage-class specifier)。

见 [C99] 6.7.1 Storage-class specifiers

storage-class-specifier:

  • typedef
  • extern
  • static
  • auto
  • register

在同一对象或函数的声明中一致地使用类型和限定符,可以加强类型化。在函数原型中指定参数名,可以检查函数定义与其声明的接口是否一致

例外:同一基本类型的兼容版本可以互换使用。例如,int、signed 和 signed int 都是等价的,它们被视为同名。

1
2
3
4
5
6
7
8
9
10
11
extern void f(signed int);
void f(int); /* Compliant - Exception ,但违反8.2,没有参数名*/
extern void g(int *const);
void g(int *); /* Non-compliant - type qualifiers */

extern int16_t func(int16_t num, int16_t den);
/* Non-compliant - parameter names do not match */
int16_t func(int16_t den, int16_t num)
{
    return num / den;
}
1
2
3
4
5
6
7
8
9
typedef uint16_t width_t;
typedef uint16_t height_t;
typedef uint32_t area_t;
extern area_t area(width_t w, height_t h);
// 不合规,虽然height和width_t对应的类型相同,但类型名称不一样
area_t area(width_t w, width_t h)
{
    return (area_t)w * h;
}

本规则不要求函数指针声明使用与函数声明相同的名称。因此,下面的示例符合要求:

1
2
3
4
5
6
extern void f1(int16_t x);
extern void f2(int16_t y);
void f(bool_t b)
{
    void (*fp1)(int16_t z) = b ? f1 : f2;
}

Rule 8.4

(必要) 外部链接(external linkage)的对象和函数,应有显式的合规的声明

如果对象或函数的声明在定义该对象或函数时是可见的,编译器必须检查声明和定义是否兼容。在有函数原型的情况下,根据 Rule 8.2 的要求,检查范围扩展到函数参数的数量和类型。

建议使用的外部链接对象和函数声明方法是在头文件中声明,然后将头文件包含在所有需要它们的代码中,包括定义它们的代码(见 Rule 8.5)。

下面代码独立,没有表示声明的头文件:

1
2
3
4
5
6
extern int16_t count;
int16_t count = 0;             /* Compliant */
extern uint16_t speed = 6000u; /* Non-compliant - no declaration
                                * prior to this definition,声明不合规 */
uint8_t pressure = 101u;       /* Non-compliant - no declaration
                                * prior to this definition,没有声明 */
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
extern void func1(void);
extern void func2(int16_t x, int16_t y);
extern void func3(int16_t x, int16_t y);
void func1(void)
{
    /* Compliant */
}
void func2(int16_t x, int16_t y)
{
    /* Compliant */
}
void func3(int16_t x, uint16_t y)
{
    /* Non-compliant - parameter types different,违反规则8.3 */
}
void func4(void)
{
    /* Non-compliant - no declaration of func4 before this definition */
}
static void func5(void)
{
    /* Compliant - rule does not apply to objects/functions with internal
     * linkage */
}

Rule 8.5

(必要) 外部链接对象或函数应在且只在一个文件中声明一次

通常情况下,只需在头文件中作出一项声明,该声明将包含在定义或使用标识符的任何翻译单元中。这样可以确保:

  • 声明和定义的一致性;
  • 不同翻译单元中的声明的一致性。

注意:一个项目中可能有多个头文件,但每个外部对象或函数只能在一个头文件中声明。(只声明一次原则)

1
2
3
4
5
6
7
/* featureX.h */
extern int16_t a; /* Declare a */

/* file.c */
#include "featureX.h"

int16_t a = 0; /* Define a */

Rule 8.6

(必要) 外部链接标识符应在且只在一处定义

如果使用的标识符存在多个定义(在不同文件中)或根本不存在定义,则行为未定义。此规则不允许不同文件中存在多个定义,即使定义完全相同。如果声明不同,或者将标识符初始化为不同的值,则这是未定义的行为。

1
2
3
4
5
/* file1.c */
int16_t i = 10;

/* file2.c */
int16_t i = 20; /* Non-compliant - two definitions of i */

因为两个 j 在同一文件中,也就是同一翻译单元中,下一个 j 的定义会覆盖上一个临时定义,这个文件翻译完后实际只有一个定义,合规:

1
2
3
/* file3.c */
int16_t j;     /* Tentative definition,临时定义 */
int16_t j = 1; /* Compliant - external definition */

两个 k 在不同的翻译单元中,file4 翻译完后有了 k 的定义(没初始值可能会初始化为 0),而 file5 翻译完后也有一个定义,产生未定义行为,不合规:

1
2
3
4
5
/* file4.c */
int16_t k; /* Tentative definition - becomes external */

/* file5.c */
int16_t k = 0; /* External definition */

Rule 8.7

(建议) 仅在本翻译单元中调用的对象和函数,应定义成内部链接

通过将对象的链接属性设置为内部链接(根据标准就是加 static 修饰)或无链接,可以降低其被误访问的可能性。同样地,通过将函数的链接属性设置为内部链接,可以减少其被误调用的机会。

遵循这条规则还可以避免在其他翻译单元或库中发生标识符与相同标识符的混淆。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/* R_08_07.h*/
extern int32_t extern_required;
extern int32_t explicit_extern; /* Non-compliant - doesn't need external linkage */
extern void explicit_ext(void); /* Non-compliant - doesn't need external linkage */
extern void R_8_7_2(void);

/* main.c */
#include "R_08_07.h"

int32_t implicit_extern = 8;  /* Non-compliant,仅在本翻译单元(文件)内使用,also breaks R.8.4  */
int32_t explicit_extern = 10; /* Non-compliant  */

int32_t extern_required;

void explicit_ext(void) /* Non-compliant,仅在本翻译单元内使用  */
{
    use_int32(implicit_extern);
    use_int32(explicit_extern);
}

void R_8_7(void)
{
    R_8_7_2();
    explicit_ext();
}

Rule 8.8

(必要) “static”修饰符应用在所有内部链接对象和函数的声明中

由于定义本身也是声明,所以这个规则同样适用于定义。

标准规定,如果一个对象或函数被声明为 extern 存储类说明符,但之前已经存在另一个可见的声明,则链接性(内部或外部)应遵循先前声明中指定的方式(见标识符)。这可能会让人感到混淆,因为人们可能会期望当前 extern 存储类说明符创建外部链接性,而不是先前的那个链接性。因此,应始终将 static 存储类说明符应用于具有内部链接性的对象和函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static int32_t x = 0;   /* definition: internal linkage */
extern int32_t x;       /* Non-compliant,先前已存在x的声明,
                         * 导致这个x的链接性就是内部的,
                         * 而不是我们平常认为的用extern修饰的是外部链接 */
static int32_t f(void); /* declaration: internal linkage */
int32_t f(void)         /* Non-compliant */
{
    return 1;
}
static int32_t g(void); /* declaration: internal linkage */
extern int32_t g(void)  /* Non-compliant */
{
    return 1;
}

Rule 8.9

(建议) 若一个对象的标识符仅在一个函数中出现,则应将它定义在块范围内

在块作用域内定义一个对象可以减少意外访问该对象的可能性,并清楚地表明它不应在其他地方被访问的意图。

在函数内部,对象是在最外层块还是最内层块中定义,很大程度上取决于个人风格。

承认有一些情况下可能无法遵守这个规则。例如,在块作用域中声明的具有静态存储期限的对象无法直接从块外部访问。这使得在不使用对该对象进行间接访问的情况下,无法设置和检查单元测试用例的结果。在这种情况下,一些项目可能选择不应用此规则。

1
2
3
4
5
6
7
8
void func(void)
{
    int32_t i; /* i仅作为循环用临时变量,其他函数用不到,
                * 所以声明为块内局部变量合规 */
    for (i = 0; i < N; ++i)
    {
    }
}

在下面这个合规示例中,函数”count”会追踪它被调用的次数,并返回这个数字。其他函数不需要知道”count”的具体实现细节,所以计数器使用块级作用域进行定义:

1
2
3
4
5
6
uint32_t count(void)
{
    static uint32_t call_count = 0;
    ++call_count;
    return call_count;
}

Rule 8.10

(必要) 内联函数应使用静态(static)存储类(Storage-class)声明

如果一个内联函数声明具有外部链接但在同一翻译单元中没有定义,那么它的行为是未定义的:

1
2
3
4
5
6
7
8
9
10
11
12
13
/* file1.c */
inline int32_t max(int32_t val1, int32_t val2)
{
    return (val1 > val2) ? val1 : val2;
}

/* file2.c */
extern inline int32_t max(int32_t val1, int32_t val2);
void R_8_10(void)
{
    int32_t xmax = max(3, 5);/* 未定义行为 */
    use_int32(xmax);
}

对于一个声明具有外部链接的内联函数的调用,可能会调用函数的外部定义(使用正常函数调用的方式),也可能使用内联定义(使用内联方式在编译时直接加入函数)。尽管这不应该影响被调用函数的行为,但它可能会影响执行时序(使用内联定义时会比函数调用的方式更快),从而对实时程序产生影响。

注意:通过将内联函数的定义放在头文件中,可以使其在多个翻译单元中可用。

Rule 8.11

(建议) 声明具有外部链接的数组时,应明确指定其大小

只适用于非定义声明

尽管可以声明一个具有不完整类型的数组并访问其元素,但在可以明确确定数组大小时,最好这样做。为每个声明提供大小信息可以确保它们在一致性方面进行检查。这也可以使静态分析器在不需要分析多个翻译单元的情况下执行一些数组边界分析

1
2
extern int32_t array1[10]; /* Compliant */
extern int32_t array2[];   /* Non-compliant */

Rule 8.12

(必要) 在枚举列表中,隐式指定的枚举常量的值应唯一

标准定义:

  • 一个隐式指定的枚举常量的值比其前面的枚举常量大 1。
  • 如果第一个枚举常量是隐式指定的,则其值为 0。
  • 一个显式指定的枚举常量具有与关联常量表达式相同的值。

如果隐式指定和显式指定的常量在枚举列表中混合使用,则可能会出现值的重复。这种重复可能是无意的,并可能导致意外行为。此规则要求对枚举常量的任何重复都要明确表达,从而使意图明确。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* Non-compliant - yellow replicates implicit green */
enum colour
{
    red = 3,
    blue,
    green,
    yellow = 5
};
/* Compliant */
enum colour
{
    red = 3,
    blue,
    green = 5, // 明确 green 和 yellow 值相同
    yellow = 5
};

Rule 8.13

(建议) 指针应尽可能指向 const 限定类型

指针应该指向一个 const 限定类型,除非以下情况之一:

  • 用于修改对象
  • 或被复制到另一个指向非 const 限定类型的指针,通过以下方式实现:
    • 赋值
    • 内存移动或复制函数

为了简便起见,此规则以指针及其指向的类型来表述。然而,该规则同样适用于数组及其所包含的元素类型。数组的元素应具有 const 限定的类型,除非以下情况之一:

  • 数组的任何元素被修改过,或
  • 它被复制到一个通过上文描述方式指向非 const 限定类型的指针。

该规则通过确保指针不会意外地用于修改对象来促进最佳实践。从概念上讲,它等同于默认认为:

  • 所有数组的元素具有 const 限定类型,以及
  • 所有指针指向 const 限定类型

然后只在需要符合语言标准的限制时才去除 const 限定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
static uint16_t f13(uint16_t *p) /* Non-compliant */
{
    return *p;
}

static uint16_t g13(const uint16_t *p) /* Compliant */
{
    return *p;
}

static char last_char(char *const s) /* Non-compliant,s内元素没有被修改,优先使用const */
{
    return s[strlen(s) - 1u];
}

static char last_char_ok(const char *const s) /* Compliant */
{
    return s[strlen(s) - 1u];
}

static int16_t first(int16_t a5[5]) /* Non-compliant,同上 */
{
    return a5[0];
}

static int16_t first_ok(const int16_t a5[5]) /* Compliant */
{
    return a5[0];
}

Rule 8.14

(必要) 不得使用类型限定符“restrict”

当小心使用时,限制(restrict)类型限定符可以提高编译器生成的代码的效率。它还可以改进静态分析。然而,要使用限制类型限定符,程序员必须确保两个或多个指针操作的内存区域不重叠。如果限制使用不正确,则存在编译器生成不按预期运行的代码的重大风险

1
2
3
4
5
6
#include <string.h>
void f(void)
{
    /* memcpy has restrict-qualified parameters,但misra规范不检测标准库,所以合规 */
    memcpy(p, q, n);
}
1
2
3
4
// 不能用restrict,不合规
void user_copy(void *restrict p, void *restrict q, size_t n)
{
}

组别 9:初始化

对象的存储期限(Storage durations of objects),[C99] 6.2.4:

一个对象有一个决定其生命周期的存储期限。有三种存储期限:

  • 静态(static)
  • 自动(automatic)
  • 分配(allocated)

对象的生命周期是程序执行过程中保证为其保留存储空间的时间段。当一个对象存在,其地址恒定,并在整个生命周期内保留其最后存储的值。如果在生命周期外引用对象,其行为是未定义的。当指针指向的对象的生命周期结束时,指针的值将变得不确定

当一个对象的标识符是用外部链接或内部链接声明、或者是用存储类说明符 static 声明时,其具有静态存储时间。它的生命周期是程序的整个执行过程,其存储值只在程序启动前初始化一次。

当一个对象的标识符在声明时无链接,也没有存储类说明符 static,则它的存储时间是自动的:

  • 这种对象(除了可变长数组类型)的生命周期从进入与之关联的程序块开始,直到以任何方式结束该程序块的执行为止(进入一个封闭的程序块或调用一个函数会暂停但不会结束当前程序块的执行)。如果程序块是递归输入的,那么每次都会创建一个新的对象实例。对象的初始值是不确定的。如果为对象指定了初始化,则在执行代码块过程中每次到达声明处时都会执行初始化;否则,每次到达声明处时,对象的值都会变得不确定。
  • 对于具有可变长度数组类型的这种对象,其生命周期从对象的声明开始,直到程序的执行离开声明的范围为止。如果该范围是递归进入的,则每次都会创建一个新的对象实例。对象的初始值是不确定的。

Rule 9.1

(强制) 具有自动存储持续时间的对象(自动变量)的值在设置前不得读取

cppcheck 无法检查本错误,GCC 开启-Wall可以检查

注意:就本规则而言,数组元素或结构成员应视为一个离散对象

根据 ANSI C 标准:

  • 除非明确初始化,否则静态存储时间的对象(static)会自动初始化为零。
  • 具有自动存储时间的对象(auto 自动变量)不会自动初始化,因此可能具有不确定的值。

注意:有时自动对象的显式初始化可能会被忽略。当使用 goto 或 switch 语句 “绕过”对象的带显式初始化的声明跳转到标签时,就会出现这种情况:

1
2
3
4
5
6
7
8
9
#include <stdio.h>
int main(void)
{
    goto L1;
    int x = 10;
L1:
    x = x + 1u;
    printf("x = %d\n", x);
}

但是 x 会被正常声明,此时 GCC 会报警告,不会报错:

1
2
3
format.c:7:11: warning: ‘x’ is used uninitialized in this function [-Wuninitialized]
     x = x + 1u;
         ~~^~~~

x 是具有自动存储期限的对象,其生命周期从进入 main 函数块开始,到退出 main 函数块结束,也就是从块开始的地方 x 就已经被分配内存了,int x = 10;只不过是个初始化,为其进行赋值,所以即使 goto 跳过了这句,仅会导致 x 未被赋予初值 10,但声明是正常的。所以 GCC 仅会报警告 x 未初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
static void f(bool_t b, uint16_t *p)
{
    if (b)
    {
        *p = 3U;
    }
}

static void g(void)
{
    uint16_t u; /* Non-compliant declaration,u未被显式赋值 */

    f(false, &u);

    if (u == 3U) /* Non-compliant use - "u" has not been assigned a value. */
    {
        use_uint16(u); /*  */
    }
}

static void jmp_over_init(void)
{
    goto L1; /* violates R.15.1 */
    uint16_t x = 10u;
L1:
    // 此处的x声明虽然被跳过,但x还是被正常声明了,可编译通过
    x = x + 1u; /* Non-compliant - x has not been been assigned a value */
    use_uint16(x);
}

void R_9_1(void)
{
    bool_t b = get_bool();
    uint16_t val = 3u;

    f(b, &val);
    use_uint16(val);

    g();
    jmp_over_init();
}

Rule 9.2

(必要) 集合(aggregate)(n 维数组或结构体)或联合体(union)的初始化应括在花括号“{}”中

这条规则适用于对象和子对象的初始化器。

形式为 { 0 } 的初始化器可用于初始化任意结构。

注意:此规则本身并不要求对对象或子对象进行显式初始化

原因:

使用大括号表示子对象的初始化可以提高代码的清晰度,并迫使程序员考虑复杂数据结构(如多维数组或结构数组)中元素的初始化。

例外:

  • 数组可以用字符串字面(String Literal)形式初始化,如char str[6] = "Hello"
  • 自动结构体或联合体可以使用与结构体或联合体类型兼容的表达式进行初始化。
  • 可以使用指定的初始化器初始化子对象的一部分。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
int16_t y1[3][2] = {1, 2, 0, 0, 5, 6};       /* Non-compliant,符合C标准但未表现出期望的数据结构(二维数组) */
int16_t y2[3][2] = { {1, 2}, {0}, {5, 6} };    /* Compliant,{0}可以用于初始化一个对象,无论其结构如何     */
int16_t y3[3][2] = { {1, 2}, {0, 0}, {5, 6} }; /* Compliant     */

int16_t z1[2][2] = { {0}, [1][1] = 1 }; /* Compliant,例外2      */
int16_t z2[2][2] = { {0},
                    [1][1] = 1,
                    [1][0] = 0 };         /* Compliant,例外2      */
int16_t z3[2][2] = { {0}, [1][0] = 0, 1 }; /* Non-compliant,不符合二维数组的定义方式  */
int16_t z4[2][2] = {[0][1] = 0, {0, 1}}; /* Compliant      */

float32_t a1[3][2] = {0};             /* Compliant      */
float32_t a2[3][2] = { {0}, {0}, {0} }; /* Compliant      */
float32_t a3[3][2] = { {0.0f, 0.0f},
                      {0.0f, 0.0f},
                      {0.0f, 0.0f} }; /* Compliant     */

union /* breaks R.19.2 */
{
    int16_t i16;
    float32_t f32;
} u = {0}; /* Compliant     */

struct
{
    uint16_t len;
    char buf[8];
} s[3] = {
    {5u, {'a', 'b', 'c', 'd', 'e', '\0', '\0', '\0'}},
    {2u, {0}},
    {.len = 0u} /* Compliant - buf initialized implicitly */
};              /* Compliant - s[ ] fully initialized     */

Rule 9.3

(必要) 数组不得部分初始化

如果一个 array 对象或子对象的任何元素被显式初始化,则整个对象或子对象都应被显式初始化。

为数组中的每个元素提供一个明确的初始化,可以清楚地表明每个元素都被考虑到了

  • 可以使用 { 0 } 形式的初始化器显式地初始化数组对象或子对象的所有元素。
  • 可以使用仅指定部分明确的初始化项的初始化器来初始化数组,例如执行稀疏初始化
  • 使用字符串字面(String Literal)初始化的数组不需要为每个元素都指定初始化符。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int32_t x[3] = {0, 1, 2}; /* Compliant   */
int32_t y[3] = {0, 1};    /* Non-compliant: y[ 2 ] is implicitly initialised  */

float32_t t[4] = {[1] = 1.0f, 2.0f};         /* Non-compliant: t[ 0 ] and t[ 3 ] are implicitly initialised */
float32_t z[50] = {[1] = 1.0f, [25] = 2.0f}; /* 例外2:指定且仅指定特定的两个初始化项,没有其他隐式的指定  */

float32_t arr[3][2] =
    {
        {0.0f, 0.0f},
        {PI / 4.0f, -PI / 4.0f},
        {0} /* { 0 } initialises all elements of array subobject arr[ 3 ]  */
};

char_t ac_5[5] = {'\0'}; /* Non-compliant */

uint16_t au_3_2[3][2] = {0U}; /* Non-compliant */

uint16_t au_3[3] = {0U}; /* Non-compliant */

float32_t af_3_2[3][2] = {0.0F}; /* Non-compliant */

float32_t arr2[3][2] = {0}; /* Compliant by exception 1 */

char h[10] = "Hello"; /* Compliant by exception 3 */

Rule 9.4

(必要) 数组的元素不得被初始化超过一次

这条规则适用于对象和子对象的初始化器。

C99 允许通过指定适用的数组索引或结构体成员名称,以任何顺序初始化对象的元素(没有初始化值的元素默认为未初始化对象)。重复初始化带来的副作用是未明确的。

为了允许使用稀疏数组和结构,只初始化应用程序所需的数组和结构是可以接受的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
static uint16_t glob_arr[3] = {0u};
static uint16_t *glob_p = glob_arr;

static void f4(void)
{
    uint16_t a[2] = {[0] = *glob_p++, [0] = 1u}; /* Non-compliant, also breaks R.13.3, R.13.4 */
    use_uint16(a[0]);
    use_uint16_ptr(glob_p);
}

void R_9_4(void)
{
    /* Required behaviour using positional initialisation                                                                 */
    int32_t a1[5] = {-5, -4, -3, -2, -1}; /* Compliant: a1 is  -5, -4, -3, -2, -1        */

    /* Similar behaviour using designated initialisers                                                                    */
    int32_t a2[5] = {[0] = -5, [1] = -4, [2] = -3, [3] = -2, [4] = -1}; /* Compliant: a2 is  -5, -4, -3, -2, -1        */

    /* Repeated designated initialiser element values overwrite earlier ones                                              */
    int32_t a3[5] = {[0] = -5, [1] = -4, [2] = -3, [2] = -2, [4] = -1}; /* Non-compliant: a3 is -5, -4, -2, 0, -1      */

    struct mystruct
    {
        int32_t a;
        int32_t b;
        int32_t c;
        int32_t d;
    };

    /* Required behaviour using positional initialisation                                                                 */
    struct mystruct s1 = {100, -1, 42, 999}; /* Compliant: s1 is 100, -1, 42, 999           */

    /* Similar behaviour using designated initialisers                                                                    */
    struct mystruct s2 = {.a = 100, .b = -1, .c = 42, .d = 999}; /* Compliant: s2 is 100, -1, 42, 999           */

    /* Repeated designated initialiser element values overwrite earlier ones                                              */
    struct mystruct s3 = {.a = 100, .b = -1, .a = 42, .d = 999}; /* Non-compliant: s3 is 42, -1, 0, 999         */
}

Rule 9.5

(必要) 在使用指定初始化方式初始化数组对象的情况下,应明确指定数组的大小

为了明确意图,应明确声明数组的大小。如果在程序开发过程中改变了初始化元素的索引,这将提供一定的保护,因为在数组边界之外初始化元素是违反约束的(C99 第 6.7.8 节)。

1
2
int32_t a1[] = {[0] = 1};   /* Non-compliant - probably unintentional to have single element */
int32_t a2[10] = {[0] = 1}; /* Compliant */

组别 10: 基本类型模型

该组规则的作用

  • 更有效的类型检查
  • 控制隐式和显示类型转换
  • 提高代码可移植
  • 解决在 ISO C 中发现的一些类型转换异常

基本类型

表中第一行表示大类,第二行是具体基本类型

Booleancharactersignedunsignedenum<i>floating
_Boolcharsigned char
signed short
signed int
signed long
signed long long
unsigned char
unsigned short
unsigned int
unsigned long
unsigned long long
named enumfloat
double
long double
  • 只有非匿名的 enum 是enum<i>基本类型,匿名的 enum 的元素视为有符号(signed)基本类型。
  • enum<i>中的 i 表示 enum 的元素所属的类型。例外是 C90 中没有标准 Boolean 基本类型,用来表示布尔类型的enum{False=0,True=1}本质上属于 Boolean 基本类型而不是整数类型。
  • C99 引入了 extended signed integer type 扩展类型,注意它们和基本类型间的优先级(rank)

复合运算符和表达式

适用于 10.6,10.7,10.8

根据附录 C 介绍,ISO C 允许进行大量隐式类型转换,因此可以认为它的类型安全性较差。这些类型转换可能会损害安全性,因为它们的实现定义方面可能会引起开发人员的困惑。

通过限制可能应用于非简单表达式(也就是复合表达式)的隐式和显式转换,可以避免。这些问题包括附录 C 中提到的一些问题:

  • 整数表达式的求值类型的混淆,因为这取决于任何整型提升(integer promotion)后的操作数的类型(算术运算时会将小于 int 宽度的整型自动提升到 int 宽度。见[C99] 6.3.1.8 Usual arithmetic conversions 和c 语言数据类型提升)。算术运算的结果类型取决于 int 的实现大小;
  • 程序员中普遍存在的误解:进行计算的类型会受到结果所分配或转换的类型的影响(如f32a = 10u / 3u;f32a = (float)(10u / 3u);,可能会误认为先将 10u 和 3u 转为 float 后再计算,但实际是10u/3u计算得到整型后再转换,丢失了精度)。这种错误的期望可能导致意外的结果:

    1
    2
    3
    4
    
    float f32a;
    f32a = 10u / 3u;                // 3.000000
    f32a = (float)(10u / 3u);       // 3.000000
    f32a = (float)10u / (float)3u;  // 3.333333
    

本文档将以下运算符定义为复合运算符:

  • 乘法(*/%
  • 加法(二元 +,二元 -)(不包括一元的+-)
  • 位运算(&|^
  • 移位(<<>>)(注意移位操作和字节序无关)
  • 条件运算(?:)如果第二个或第三个操作数是一个复合表达式

Rule 10.1

(必要) 操作数不得为不适当的基本类型

  1. float 不能用于仅限整型作为操作数的操作,如取下标[ ]、移位>>操作
  2. 所有仅限 Boolean 类型作为操作数的操作必须使用 Boolean,如非!、与&&、条件运算符?:的第一个操作数
  3. Boolean 不能被视为数值(numeric value),比如 False 不能被视为 0 作为 int 类型操作数
  4. 字符基本类型不能视为数值,同 3
  5. 枚举基本类型不能用于算术运算,因为其具体类型由实现定义。(匿名的枚举类型属于有符号基本类型,可以用于算术运算)
  6. 移位和位运算只能在无符号基本类型的操作数上执行。
  7. 移位运算符的右侧操作数必须为无符号基本类型
  8. 不能为无符号基本类型添加一元运算符负号-,让其成为有符号的,因为这有可能超出其原来的范围限制,比如最大的一个 u32_t 加负号就超出 s32_t 的限制了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
enum enuma { a1, a2, a3 } ena, enb;/* Essentially enum<enuma> */
enum { K1 = 1, K2 = 2 };/* Essentially signed */

f32a & 2U;     /* Rationale 1 - constraint violation */
f32a << 2;     /* Rationale 1 - constraint violation */

cha && bla;    /* Rationale 2 - char type used as a Boolean value */
ena ? a1 : a2; /* Rationale 2 - enum type used as a Boolean value */
s8a && bla;    /* Rationale 2 - signed type used as a Boolean value */
u8a ? a1 : a2; /* Rationale 2 - unsigned type used as a Boolean value */
f32a && bla;   /* Rationale 2 - floating type used as a Boolean value */

bla * blb;     /* Rationale 3 - Boolean used as a numeric value */
bla > blb;     /* Rationale 3 - Boolean used as a numeric value */

cha & chb;     /* Rationale 4 - char type used as a numeric value */
cha << 1;      /* Rationale 4 - char type used as a numeric value */

ena--;         /* Rationale 5 - enum type used in arithmetic operation */
ena * a1;      /* Rationale 5 - enum type used in arithmetic operation */
ena += a1;     /* Rationale 5 - enum type used in arithmetic operation */

s8a & 2;       /* Rationale 6 - bitwise operation on signed type */
50 << 3U;      /* Rationale 6 - shift operation on signed type */

u8a << s8a;    /* Rationale 7 - shift magnitude uses signed type */
u8a << -1;     /* Rationale 7 - shift magnitude uses signed type */

-u8a;          /* Rationale 8 - unary minus on unsigned type */

Rule 10.2

(必要) 字符基本类型的表达式不得在加减运算中不当使用

使用加减运算来处理字符基本类型一般有几个用途:

  • 两个字符基本类型相减用于获取字符对应的序号(使用’0’-‘9’共 10 个字符),比如表示星期中的某一天,用字符’1’到’7’表示,通过字符相减操作'7'-'0'来将字符值转为一个序数(ordinal)值 7,表示一周的第 7 天。
  • 一个字符基本类型和一个基本无符号类型相加可以用来将序数值转为字符值,如'0'+9输出'9'
  • 从一个字符基本类型中减去一个基本无符号类型可用于将一个字符从小写转换为大写,如'd'-32(32 可以通过'a'-'A'得到)输出’D’。
  • 减去一个基本类型等效于加上这个基本类型的负数,来适用上面第二个用途。

以下合规:

1
2
3
4
'0' + u8a /* 用途2 */
s8a + '0' /* 用途2 */
cha - '0' /* 用途1 */
'0' - s8a /* 用途4 */

以下不合规:

1
2
3
s16a - 'a' /* 用途不明 */
'0' + f32a /* 加浮点数不符合上述用途 */
cha + ':' /* 两个都是字符基本类型,不符合用途2 */

争议项:

1
cha - ena /* 字符基本类型减去enum类型(enum可能就是基本无符号类型),而且有可能违反了10.1,不过cppcheck不能判断 */

Rule 10.3

(必要) 表达式的值不得赋值给同类别的较窄基本类型或不同类别的基本类型的对象

表达式包含了简单表达式和复合表达式

本规则涵盖以下操作:

  • 术语表中定义的赋值(=);
  • 将 switch 语句的 case 标签中的常量表达式(constant expression)转换为控制表达式(control expression)的提升类型:

    1
    2
    3
    4
    5
    
    switch (控制表达式) {
      case 常量表达式: 语句序列
      case 常量表达式: 语句序列
      default: 语句序列
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    
    switch (dayOfWeek) { // dayOfWeek属于控制表达式
        case 0: // 0属于常量表达式
            // SUNDAY
            break;
        case 1:
            // MONDAY
            break;
        case 2:
            // TUESDAY
            break;
        // ...
        }
    

C 语言允许程序员有相当大的自由度,并允许自动执行不同算术类型之间的赋值。不过,使用这些隐式转换可能会导致意外结果,有可能造成数值、符号或精度的损失。有关 C 类型系统的更多详情,请参阅附录 C。

使用 MISRA 基本类型模型强制执行的更强类型,可降低出现这些问题的可能性

例外:

  1. 如果非负整数常量表达式(0,1,2 之类的)在可以用无符号整数类型表示(比如在 uint8_t 允许的 0-255 之内),则可将其赋值给该无符号整数类型变量。
  2. 初始化器 { 0 } 可用来初始化集合或联合类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
u8a = 2;      /* Compliant By 例外1 */
u8a = 2 * 24; /* Compliant By 例外1 */

uint8_t u8f = 1.0f; /* Non-compliant - unsigned and floating */
bool_t bla = 0;     /* Non-compliant - boolean and signed,不符合例外1,因为bla不是无符号整型 */
cha = 7;            /* Non-compliant - character and signed */
u8a = 'a';          /* Non-compliant - unsigned and character */
u8b = 1 - 2;        /* Non-compliant - unsigned and signed,不符合例外1,因为1-2不是非负的 */
u8c += 'a';         /* Non-compliant - u8c = u8c + 'a' assigns character to unsigned */

s8a = K2;    /*  Non-compliant - Constant value does not fit */
u16a = u32a; /*  Non-compliant - uint32_t to uint16_t */

s8a = -123L; /*  Non-compliant - signed long to int8_t */

u8a = 6L; /* Non-compliant - signed long to uint8_t,不符合例外1 */
          /* Standard Type has rank greater than int,
           * so exception does not apply */

/* integer constant expression from + with value 5U and UTLR of unsigned char */
u8a = (uint16_t)2U + (uint16_t)3U; /* Compliant,例外1? */

/* integer constant expression from + with value 100000U and UTLR of unsigned int */
u16a = (uint16_t)50000U + (uint16_t)50000U; /*  Non-compliant,不符合例外1,超过了u16的最大值 */

/* Top-level cast returns C standard type of unsigned short */
u8a = (uint16_t)(2U + 3U); /*  Non-compliant,经过转换后不再是常量表达式,不符合例外1 */

Rule 10.4

(必要) 执行常规算术转换的运算符的两个操作数应有相同类别的基本类型

本规则适用于常规算术转换中描述的运算符(见 C90 第 6.2.1.5 节,C99 第 6.3.1.8 节),另外:

  • 包括除了移位、逻辑 &&、逻辑 || 和逗号运算符外所有二进制运算符
  • 包括三元运算符的第二和第三操作数。
  • 不包含递增和递减运算符。

原因类似 Rule 10.3

例外:

允许使用 Rule 10.2 规定的例外字符操作形式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
enum enuma
{
    A1,
    A2,
    A3
} ena;
enum enumb
{
    B1,
    B2,
    B3
} enb;

if (ena > A1) /*  Compliant  - same essential type  */
{
    ; /*no action */
}
u16b = u8a + u16b; /* Compliant - same essential type */
use_uint16(u16b);

cha += u8a; /*  Compliant by exception */
use_char(cha);

s8a += u8a; /* Non-compliant - Signed and unsigned,
                               also breaks R.10.3*/
use_int8(s8a);

u8b = u8b + 2; /* Non-compliant - unsigned and signed,
                                  returns standard type  */
use_uint8(u8b);

if (enb > A1) /* Non-compliant - Enum<enuma> to enum<enumb>    */
{
    ; /* no action */
}
if (ena == enb) /* Non-compliant - Enum<enumb> to enum<enuma>      */
{
    ; /* no action */
}

u8a += cha; /* Compliant by exception, but breaks R.10.3 */
use_uint8(u8a);

Rule 10.5

(建议) 表达式的值不应被(强制)转换为不适当的基本类型

下表列出了应避免的类型转换:

 from/Booleancharacterenumsignedunsignedfloating
to/Boolean AvoidAvoidAvoidAvoidAvoid
characterAvoid    Avoid
enumAvoidAvoidAvoid*AvoidAvoidAvoid
signedAvoid     
unsignedAvoid     
floatingAvoidAvoid    

*注意:只有相同的 enum 间可以转换(虽然这种转换没什么意义),其他情况都应避免转换。

不允许从 void 转换为任何其他类型,因为这会导致未定义的行为。Rule 1.3 涵盖了这一点。

原因:

可以出于合规的功能原因引入显式转换,例如:

  • 为了改变后续算术运算所使用的类型;
  • 故意截断数值;
  • 为清晰起见,使类型转换显式化

然而,有些显式转换被认为是不合适的:

  • 在 C99 中,转换或赋值给_Bool(C99 自带的布尔型)的结果总是 0 或 1。在转换为另一种定义为Boolean 基本类型的类型时(如 C90 不自带布尔型,需要#typedef int _Bool)不一定是这样;
  • 转换为 enum 基本类型可能会产生一个不在该类型的枚举常量集合内的值;
  • 从 Boolean 基本类型转换为任何其他类型都不太有意义;
  • 在浮点和字符类型之间转换没有意义,因为两种表示之间没有精确的映射

例外:

一个有符号的值为 0 或 1 的整数常量表达式,可以被转换为 Boolean 基本类型(非 C99 定义的)。这样就可以实现非 C99 布尔运算模型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typedef _Bool bool_t;
typedef enum enum_tag { ENUM_0, ENUM_1, ENUM_2 } enum_t;

char cha;
enum_t ena = ENUM_1;
enum_t enc;
int32_t si;

bool_t bna = (bool_t) false; /* Compliant - C99 'false' is essentially Boolean */
si = (int32_t)3U;            /* Compliant */
bna = (bool_t)0;             /* Compliant  - by exception */
bna = (bool_t)3U;            /* Non-compliant  */
bna = (bool_t)ENUM_0;        /* Non-compliant - ENUM_0 has essentially enum type */

si = (int32_t)ena; /* Compliant */
enc = (enum_t)3;   /* Non-compliant,不允许将有符号整型转为enum */
cha = (char)enc;   /* Compliant */

Rule 10.6

(必要) 复合表达式的值不得赋值给具有较宽基本类型的对象

涵盖条件同 Rule 10.3

由于不同实现的结果可能不同,因此不允许向更宽的类型转换:

1
u32a = u16a + u16b;

在 16 位机器上,加法将以 16 位执行,如果产生超过 16 位范围的进位,则会丢弃。然而,在 32 位机器上,加法将以 32 位进行,并且会保留在 16 位机器上会丢失的高位。

由于结果的显式截断总是导致同样的信息丢失,因此将结果转换为具有相同类别的较窄类型是可以接受的,但这不符合 Rule 10.3,所以和 Rule 10.3 结合起来就是复合表达式只能赋值给相同宽度、相同类别的基本类型的对象,也就是同一个种基本类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
u16c = u16a + u16b; /* Same essential type */

u32a = (uint32_t)u16a + u16b; /* Cast causes addition in uint32_t */

u32a = u16a + u16b; /* Non-compliant - Implicit conversion on assignment  */
use_uint32(u32a);
use_uint32(u16a + u16b); /* Non-compliant - Implicit conversion of fn argument */

u32a = ++u8a; /* Compliant,不是复合表达式,不适用 - but breaks R.13.3 */

u8a = ~u8a;  /* Compliant  */
u16a = ~u8a; /* Non-compliant - ~ is a composite operator */

u16a = ~(uint16_t)u8a; /* Compliant  */

u16a = (uint8_t)(~u8a); /* Compliant,强制转换复合表达式的值后视为一个简单表达式,不适用  */

u64a = +(u32a * u32a); /* Non-compliant - unary + operator */
                       /* on composite expression          */

u64a = +u32a; /* Compliant - unary + operator    */
              /* on non-composite expression,+u32a不是一个复合表达式,不适用本规则 */

Rule 10.7

(必要) 如果将复合表达式用作执行常规算术转换的运算符的一个操作数,则另一个操作数不得具有更宽的基本类型

理由在上面的复合运算符和表达式中有说明。

限制复合表达式上的隐式转换意味着表达式中的算术运算序列必须在同一种基本类型中进行。这样可以减少开发者的可能混淆。

1
2
3
4
5
6
7
u32a = u32a * u16a + u16b;             /* No composite conversion,u16b比复合表达式窄, but breaks R.12.1 */
u32a = (u32a * u16a) + u16b;           /* No composite conversion */
u32a = u32a * ((uint32_t)u16a + u16b); /* Cast means no conversion */
u32a += (u32b + u16b);                 /* No composite conversion */

u32a = u32a * (u16a + u16b); /* Non-compliant - Implicit conversion of ( u16a + u16b ) */
u32a += (u16a + u16b);       /* Non-compliant - Implicit conversion of ( u16a + u16b ) */

Rule 10.8

(必要) 复合表达式的值不得转换为其他类别的基本类型或本类别中更宽的基本类型

理由在上面的复合运算符和表达式中有说明。

由于不同实现的结果可能不同,因此不允许向更宽的类型转换:

1
(uint32_t)(u16a + u16b);

在 16 位机器上,加法将以 16 位执行,如果产生超过 16 位范围的进位,则会丢弃。然而,在 32 位机器上,加法将以 32 位进行,并且会保留在 16 位机器上会丢失的高位。

由于结果的显式截断总是导致同样的信息丢失,因此将结果转换为具有相同类别的较窄基本类型是可以接受的。

1
2
3
4
5
6
7
8
9
10
11
12
u16a = (uint16_t)(u32a + u32b); /* Compliant,转为同一类别更窄的基本类型 */

u16a = (uint16_t)(s32a + s32b); /* Non-compliant - different essential
                                 * type category,其他类别的基本类型,有符号类别转无符号类别 */

u16a = (uint16_t)s32a; /* Compliant - s32a is not composite,非复合表达式,不适用 */

u32a = (uint32_t)(u16a + u16b); /* Non-compliant - cast to wider
                                 * essential type,同意类别更宽 */

u32a = (uint32_t)(uint16_t)(u16a + u16b); /* Compliant - uint32_t cast is
                                           * not on a composite expression,要转为uint32_t的不是复合表达式*/

组别 11:指针类型转换

指针类型可分类如下:

  • 指向对象的指针;
  • 指向函数的指针;
  • 指向不完整的指针(当一个指针指向一个尚未完整定义的数据类型时,如仅声明了一个结构体、仅声明了一个未定义大小的数组);
  • 指向 void 的指针;
  • 空指针常量(NULL),即值 0,可选择转换为 void *

《标准》允许的涉及指针的转换只有以下几种:

  • 从指针类型转换为 void;
  • 从指针类型转换为算术类型;
  • 从算术类型转换为指针类型;
  • 从一种指针类型转换为另一种指针类型

尽管语言限制允许指针与整数类型以外的任何算术类型之间的转换,但这种转换是未定义的。

以下允许的指针转换不需要显式转换:

  • 从指针类型转换为 _Bool(仅适用于 C99);
  • 从空指针常量转换为指针类型;
  • 从指针类型转换为兼容的指针类型,条件是目标类型具有源类型的所有类型限定符(const 之类的);
  • 指向对象或不完整类型的指针与 void * 或其限定版本之间的转换,条件是目标类型具有源类型的所有类型限定符。

在 C99 中,任何不属于指针转换子集的隐式转换都违反了约束(C99 第 6.5.4 和 6.5.16.1 节)。

在 C90 中,任何不属于指针转换子集的隐式转换都会导致未定义的行为(C90 第 6.3.4 和 6.3.16.1 节)。

指针类型和整数类型之间的转换由实现定义。

Rule 11.1

(必要) 不得在指向函数的指针和任何其他类型的指针之间进行转换

将指向函数的指针和以下任何一种情况互相转换,都会导致未定义的行为:

  • 指向对象的指针;
  • 指向不完整对象的指针;
  • void *

如果通过指针调用函数,而该指针的类型与被调用函数的类型不兼容,则该函数的行为是未定义的。《标准》允许将指向函数的指针转换为指向不同类型函数的指针。《标准》也允许将整数转换为指向函数的指针。但是,为了避免使用不兼容的指针类型调用函数时产生未定义的行为,本规则禁止这两种转换。

例外

  1. 空指针常量可转换为指向函数的指针;
  2. 指向函数的指针可转换为 void;
  3. 函数类型可隐含转换为指向该函数类型的指针。

    包括 C90 第 6.2.2.1 节和 C99 第 6.3.2.1 节所述的隐式转换。通常发生在以下情况:

    • 直接调用函数,即使用函数标识符表示要调用的函数;
    • 将函数赋值给函数指针
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
typedef void (*fp16)(int16_t n);
typedef void (*fp32)(int32_t n);

static fp16 get_fp16(void)
{
    return &use_int16;
}

extern void f1(int16_t n);

void R_11_1(void)
{
    fp16 fp1 = NULL; /* Compliant - exception 1 */
    fp32 fp2 = (fp32)fp1; /* Non-compliant - function pointer to
                                           different function pointer */

    if (fp2 != NULL) /* Compliant - exception 1,将空指针常量NULL转为函数指针类型与fp2对比, also breaks R.14.3  */
    {
    }

    fp16 fp3 = (fp16)0x8000; /* Non-compliant  - integer to function pointer */
    fp16 fp4 = (fp16)1.0e6F; /* Non-compliant  -   float to function pointer */

    typedef fp16 (*pfp16)(void);
    pfp16 pfp1 = &get_fp16;

    (void)(*pfp1()); /* Compliant - exception 2,调用pfp1并将返回的函数指针转为void */

    f1(1);         /* Compliant - exception 3 - implicit conversion
                    * of f1 into pointer to function */
    fp16 fp5 = f1; /* Compliant - exception 3 */
}

Rule 11.2

(必要) 不得在指向不完整类型的指针和其他任何类型间进行转换

将指针转换为不完整类型或从不完整类型转换,可能导致指针未正确对齐,从而产生未定义的行为。

将不完整类型的指针转 ​​ 换为浮点类型或从浮点类型转换总是会导致未定义的行为。

指向不完整类型的指针有时用于隐藏对象的表示。如果将指向不完整类型的指针转换成指向对象的指针,就会破坏这种封装。

例外

  1. 空指针常量可转换为指向不完整类型的指针。
  2. 指向不完整类型的指针可转换为 void。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct s; /* Incomplete type                        */
struct t; /* A different incomplete type            */
extern void use_structs_ptr(struct s *ps);
extern void use_structt_ptr(struct t *pt);

extern struct s *f2(void);

void R_11_2(void)
{
    struct s *sp;
    struct t *tp;
    int16_t *ip;

    sp = (struct s *)1234; /* Non-compliant                          */
    ip = (int16_t *)sp;    /* Non-compliant                          */
    tp = (struct t *)sp;   /* Non-compliant - casting pointer to a
                              different incomplete type              */

    sp = NULL; /* Compliant - exception 1                */

    (void)f2(); /* Compliant - exception 2               */
}

Rule 11.3

(必要) 不得在指向不同对象类型的指针之间执行强制转换

将指向对象的指针转换为指向不同对象的指针,可能导致指针不能正确对齐,从而产生未定义的行为。即使已知转换产生的指针是正确对齐的,但如果使用该指针访问对象,其行为也可能是未定义的。例如,如果一个类型为 int 的对象被当作 short 访问,即使 int 和 short 具有相同的表示和对齐要求,行为也是未定义的。详见 C90 第 6.3 节、C99 第 6.5 节第 7 段

允许将对象类型指针转换为对象类型 char——有符号 char 或无符号 char 的指针。该标准保证,指向这些类型的指针可用于访问对象的各个字节

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
uint8_t *p1 = get_uint8_ptr();
uint32_t *p2;

p2 = (uint32_t *)p1; /* Non-compliant  - possible incompatible alignment */

extern uint32_t read_value(void);
extern void print(uint32_t n);
void f(void)
{
    uint32_t u = read_value();
    uint16_t *hi_p = (uint16_t *)&u; /* Non-compliant even though
                                      * probably correctly aligned */
    *hi_p = 0;                       /* Attempt to clear high 16-bits on big-endian machine */
    print(u);                        /* Line above may appear not to have been performed */
}

const short *p;
const volatile short *q;
q = (const volatile short *)p; /* Compliant,本规则并没有规定不能为指针加限定符,除了const限定符外 */

int *const *pcpi;
const int *const *pcpci;
pcpci = (const int *const *)pcpi; /* Non-compliant,TODO:指针类型不同,一个使用了const修饰 */

Rule 11.4

(建议) 不得在指向对象的指针和整数类型之间进行转换

  • 指针类型不应转换为整型:

    将指向对象的指针转换为整数,可能会产生一个无法用所选整数类型表示的值,从而导致未定义行为。

  • 整型不应转换为指针类型:

    将整型变量转换为对象指针可能导致指针未正确对齐,进而导致未定义行为。

注意:C99 中的数据类型 intptr_t 和 uintptr_t(在<stdint.h>中声明)分别是有符号和无符号整数类型,能够表示指针值。尽管如此,根据此规则,对象指针与这些类型之间的转换是不被允许的,因为它们的使用并不能避免与不对齐指针相关的未定义行为。

应尽可能避免在指针和整数类型之间进行转换,但在寻址内存映射寄存器或其他硬件特性时,可能有必要这样做。如果要在整数和指针之间进行转换,应注意确保产生的指针不会导致规则 11.3 中讨论的未定义行为。

例外

空指针常量(0 也算整数类型)可转换为对象指针

1
2
3
4
5
6
7
8
9
10
uint8_t *PORTA = (uint8_t *)0x0002; /* Non-compliant */
uint16_t *p;
int32_t addr = (in t32_t)&p;  /* Non-compliant */
uint8_t *q = (uint8_t *)addr; /* Non-compliant */
bool_t b = (bool_t)p;         /* Non-compliant */
enum etag
{
    A,
    B
} e = (enum etag)p; /* Non-compliant */

Rule 11.5

(建议) 不得将指向 void 的指针转换为指向对象的指针

将指向 void 的指针转换为指向对象的指针,可能会导致指针未正确对齐,从而产生未定义的行为。在可能的情况下应避免使用这种方法,但在使用内存分配功能时,这种方法可能是必要的。如果使用将对象指针转换为 void 指针的方法,应注意确保产生的指针不会导致规则 11.3 中讨论的未定义行为。

例外

指向 void 的空指针常量可转换为指向对象的指针

1
2
3
4
5
6
7
8
9
10
uint32_t *p32 = get_uint32_ptr();
void *p;
uint16_t *p16;

p = p32; /* Compliant - pointer to uint32_t -> pointer to void */
p16 = p; /* Non-compliant */

p = (void *)p16; /* Compliant     */

p32 = (uint32_t *)p; /* Non-compliant */

Rule 11.6

(必要) 不得在指向 void 的指针和算术类型之间执行强制转换

将整数转换为指向 void 的指针可能导致指针未正确对齐,从而产生未定义的行为。

将指向 void 的指针转换为整数,可能会产生一个无法用所选整数类型表示的值,从而导致未定义的行为。

任何非整数算术类型和指向 void 的指针之间的转换都是未定义的。

例外

值为 0 的整数常量表达式可被转换为指向 void 的指针。

1
2
3
4
5
6
7
8
9
void *p;
uint32_t u;

p = (void *)0x1234u; /* Non-compliant - implementation-defined */
use_void_ptr(p);

p = (void *)1024.0f; /* Non-compliant - undefined              */

u = (uint32_t)p; /* Non-compliant - implementation-defined */

Rule 11.7

(必要) 不得在指向对象的指针和非整数算术类型之间执行强制转换

就本规则而言,非整数运算类型指以下类型之一:

  • 布尔基本类型;
  • 字符基本类型;
  • 枚举基本类型;
  • 浮点基本类型;

组别 10: 基本类型模型

  • 将指向对象的指针和浮点基本类型相互转换会产生未定义的行为。
  • 其余类型的指针转换为对象指针,可能导致指针未正确对齐,从而产生未定义的行为。将对象指针转换为其余类型的指针,可能会产生一个无法用所选整数类型表示的值,从而导致未定义的行为。
1
2
3
4
5
6
7
8
int16_t *p = get_int16_ptr();
float32_t f;

if (p != NULL)
{
    f = (float32_t)p; /* Non-compliant,未定义行为 */
    p = (int16_t *)f; /* Non-compliant,未定义行为 */
}

和 Rule 11.4 结合看,将对象指针和整型指针单独划为另一条建议型规则

Rule 11.8

(必要) 强制转换不得从指针指向的类型中删除任何 const 或 volatile 限定符

任何试图通过转换来重新移动与寻址类型相关的限定符的行为,都是对类型限定原则的违反。

注意:这里的限定符指的是const int *这种修饰的是指针指向的内容,而不是int * const修饰的是指针

如果从寻址对象中移除限定符,可能会出现以下问题:

  • 删除 const 限定符可能会规避对象的只读状态,导致对象被修改;
  • 删除 const 限定符可能会导致在访问对象时出现异常;
  • 删除 volatile 限定符可能会导致对对象的访问被优化。

注意:删除 C99 restrict 类型限定符是无害的,但 Rule 8.14 规定不得使用 restrict 限定符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const uint16_t cx = 3U;
volatile uint16_t vx = 3U;

uint16_t x = 3U;
uint16_t *const cpi = &x;     /* const pointer               */
uint16_t *const *pcpi = &cpi; /* pointer to const pointer    */
uint16_t **ppi;
const uint16_t *pci = &cx;    /* pointer to const            */
volatile uint16_t *pvi = &vx; /* pointer to volatile         */
uint16_t *pi;

pi = cpi; /* Compliant - no conversion
                         no cast required */

pi = (uint16_t *)pci; /* Non-compliant               */

pi = (uint16_t *)pvi; /* Non-compliant               */

ppi = (uint16_t **)pcpi; /* Non-compliant               */

cppcheck 无法识别 volatile 限定符

Rule 11.9

(必要) 宏“NULL”是整数型空指针常量的唯一允许形式

如果数值为 0 的整数常量出现在以下任何一种情况下,则应通过扩展宏 NULL 得出:

  • 作为指针的赋值;
  • 作为另一个操作数为指针的 ==!=的操作数;
  • 作为第三个操作数为指针的 ?:的第二个操作数;
  • 作为第二个操作数为指针的 ?:的第三个操作数。

忽略空格和任何括号,任何这样的整数常量表达式都应该表示 NULL 的完整扩展。

注意:允许使用 (void *)0 形式的空指针常量,无论它是否从 NULL 扩展而来

使用 NULL 而不是 0,可以清楚地表明想要使用一个空的指针而不是整数 0。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#define MY_NULL_1 0
#define MY_NULL_2 (void *)0
#define MY_NULL_3 NULL

extern void f9(uint8_t *p);

int32_t *p1 = 0;         /* Non-compliant */
int32_t *p2 = (void *)0; /* Compliant     */
int32_t *p3 = MY_NULL_3; /* Compliant     */

if (p1 == MY_NULL_1) /* Non-compliant - also breaks R.14.3 */
{
}
if (p2 == MY_NULL_2) /* Compliant - but breaks R.14.3 */
{
}

f9(NULL); /* Compliant for any conforming definition of
           * NULL, such as:
           *       0
           *       (void *)0
           *       (((0)))
           *       (((1 - 1)))
           */

组别 12:表达式

Rule 12.1

(建议) 表达式中运算符的优先级应明确

DescriptionOperator or OperandPrecedence
Primaryidentifier, constant, string literal, ()(括号)16 (high)
Postfix(后缀)[] () (function call)
. -> ++ (post-increment)
-- (post-decrement)
(){} (C99: compound literal)
15
Unary(一元)++ (pre-increment)
-- (pre-decrement)
& * + - ~ !
sizeof defined (preprocessor)
14
Cast(强制类型转换)()13
Multiplicative* / %12
Additive+ -11
Bitwise shift<< >>10
Relational< > <= >=9
Equality== !=8
Bitwise AND&7
Bitwise XOR^6
Bitwise OR|5
Logical AND&&4
Logical OR||3
Conditional?:2
Assignment= *= /= %= += -= <<= >>= &= ^= |=1
Comma(逗号),0 (low)

例如,a << b + c的解析树,优先级越低的越靠近根部:

1
2
3
4
5
    <<
   / \
  a   +
     / \
    b   c

建议如下:

  • 操作符 sizeof 的操作数应该用圆括号括起来;
  • 优先级在 2 到 12 之间的表达式,应该在满足以下两个条件的任何操作数周围加上括号:

    • 优先级小于 13,且
    • 优先级大于表达式的优先级

    例子:

    a == b ? a : a - b这个表达式的优先级取决于?:为 2,其中有操作数a==baa-b,其中a==b优先级为 8,符合小于 13 且大于整个表达式的优先级 2,加括号;其中a由于优先级为 16 不符合,不加括号;其中a-b优先级为 11 且大于整个表达式优先级 2,加括号。最终为(a == b) ? a : (a - b)

原因

C 语言有相对较多的运算符,而它们的相对优先级并不直观。这可能导致经验不足的程序员犯错。使用括号明确运算符的优先级,就不会出现程序员的预期不正确的情况。它还能让代码的审核者或维护者清楚地了解原始程序员的意图。

我们认识到,过度使用括号会使代码杂乱无章,降低代码的可读性。本规则的目的是在因括号过多或过少而导致代码难以理解之间达成折衷。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
/* Operands are primary_expressions or top-level operator level 15 */
arr[i]->n = 0U;    /* Compliant no need to write ( arr[ i ] )->n */
*p++;              /* Compliant no need to write *(p++),
                    * ++后导运算符优先级为15,*取值操作符为14,
                    *  but breaks R.2.2 */
sz = sizeof x + y; /* Non-compliant - write sizeof (x)+y or sizeof (x+y) */

/* Same precedence - all are compliant */
x = a + b;
x = a + b + c;
x = (a + b) + c;
x = a + (b + c);
x = a + b - c + d;
x = (a + b) - (c + d);

/* Different precedence */
x = f(a + b, c); /* Compliant - no need to write f ((a + b), c),逗号操作符优先级为1 */

/* Operands of conditional operator (precedence 2) are:
 * "==":precedence 8 needs parentheses
 * "a": precedence 16 does not need parentheses
 * "-":precedence 11 needs parentheses
 */
x = a == b ? a : a - b; /* Non-compliant */
x = (a == b) ? a : (a - b); /* Compliant */

/* Operands of << operator (precedence 10) are:
 * "a": precedence 16 does not need parentheses
 * "(...)": precedence 16 already parenthesised
 */
x = a << (b + c); /* Compliant */

/* Operands of && operator (precedence 4) are:
 * "bj bk bl": precedence 16 does not need parentheses
 * "&&": precedence 4 does not need parentheses
 */
if (bj && bk && bl) /* Compliant */
{
}


/* Operands of && operator (precedence 4) are:
 * defined(XX) precedence 14 does not need parentheses
 * (E) precedence 16 already parenthesised
 */
#if defined(XX) && ((XX + YY) > ZZ) /* Compliant */
use_bool(bj);
#endif

/* Compliant
 * Operands of && operator (precedence 4) are:
 *   !defined(XZ) precedence 14 does not need parentheses
 *    defined(YZ) precedence 14 does not need parentheses
 * Operand of ! operator (precedence 14) is:
 *    defined(XZ) precedence 14 does not need parentheses
 */
#if !defined(XZ) && defined(YZ) /* Compliant */
use_bool(bj);
#endif

x = a, b; /* Compliant - parsed as (x = a) , b - violates R.2.2 and R.12.3 */

Rule 12.2

(必要) 移位运算符的右操作数应在范围:[0,左操作数基本类型的位宽度减 1]

如果右侧操作数为负数,或者大于或等于左侧操作数的宽度,则行为未定义。

举例来说,如果左移或右移的左操作数是一个 16 位整数,那么必须确保它只被 0 至 15 范围内的数字移位。

有关移位操作符操作数的基本类型和基本类型限制的说明,请参见第 8.10 节。

有多种方法可以确保这一规则得到遵守:

  • 最简单的方法是将右边的操作数设为常量(可以静态检查其值)。
  • 使用无符号整数类型可以确保操作数为非负(保证下限 0),因此只需检查上限(运行时动态检查或审查)。
  • 否则,上限和下限都需要检查。
1
2
3
4
5
6
7
8
9
10
11
u8a = u8a << 7; /* Compliant */

u8a = u8a << 8; /* Non-compliant,只能0-7 */

u16a = (uint16_t)u8a << 9; /* Compliant */

u8a = 1u << 10u; /* Non-compliant */

u16a = (uint16_t)1u << 10u; /* Compliant     */

u64a = 1UL << 10u; /* Compliant     */

Rule 12.3

(建议) 不得使用逗号(,)运算符

使用逗号操作符通常不利于代码的可读性,通常可以通过其他方法达到同样的效果。

1
2
3
4
5
6
/* also violates R.14.2 */
for (i = 0, p = &a[0]; /* Non-compliant */
     i < N;
     ++i, ++p) /* Non-compliant */
{
}

Rule 12.4

(建议) 常量表达式的求值不应导致无符号整数的回绕

无符号整数表达式严格来说不会溢出,而是回绕。

平时在编程时会用取余操作来模拟特定的回绕,如 i=(i+1)%3 表示每到 3 回绕一次。但在常量表达式中并不需要这种回绕。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// 与 case 标签关联的表达式必须是常量表达式。
// 如果在 case 表达式求值期间发生无符号环绕,则很可能是无意的。
// 在具有 16 位 int 类型的计算机上会导致以下示例中的回绕:
#define BASE 65024u
switch (x)
{
case BASE + 0u:
     f();
     break;
case BASE + 1u:
     g();
     break;
case BASE + 512u: /* Non-compliant - wraps to 0 */
     h();
     break;
}

// #if 或 #elif 预处理器指令的控制表达式必须是常量表达式。
#if 1u + (0u - 10u) /* Non-compliant as ( 0u - 10u ) wraps */

// 在本例中,表达式 DELAY + WIDTH 的值为 70 000,
// 但在使用 16 位 int 类型的机器上,这个值会被绕成 4 464。
#define DELAY 10000u
#define WIDTH 60000u
void fixed_pulse(void)
{
     uint16_t off_time16 = DELAY + WIDTH; /* Non-compliant */
}

// c是一个对象不是常量表达式,因此不符合常量表达式的约束条件:
const uint16_t c = 0xffffu;
void f(void)
{
     uint16_t y = c + 1u; /* Compliant */
}

// 在下面的示例中,子表达式 ( 0u - 1u ) 导致无符号整数回绕。
// 在 x 的初始化过程中,子表达式不会被求值,因此表达式符合要求。
// 然而,在初始化 y 时,子表达式可能会被求值,因此表达式不符合要求。
bool_t b;
void g(void)
{
     uint16_t x = (0u == 0u) ? 0u : (0u - 1u); /* Compliant,
                                                * 0u==0u恒成立,0u-1u是不会被执行的 */
     uint16_t y = b ? 0u : (0u - 1u);          /* Non-compliant */
}

组别 13:副作用

Rule 13.1

(必要) 数组(lists)的初始化器不得包含持久性副作用(persistent side effects)

C90 规定具有集合(aggregate)类型的自动对象的初始化器只能包含常量表达式

然而,C99 允许自动集合(aggregate)类型的初始化器包含在运行时求值的表达式。此外,它还允许使用复合字面表达式(compound literal),这些复合字面表达式就像匿名初始化对象一样。在对初始化器列表中的表达式进行评估时,副作用发生的顺序是未指定的,因此,如果这些副作用是持久性的,初始化的行为将是不可预测的。

volatile 限定的对象的补充说明

对 volatile 限定的对象的访问会带来持久性副作用(persistent side effects),也就是对运行环境带来后续影响。

在 C 语言中,volatile 类型限定符用于表示其值可独立于程序执行而改变的对象(例如输入寄存器)。如果访问 volatile 限定类型的对象,可能会改变其值。C 编译器不会对 volatile 的读取进行优化。总之,就 C 程序而言,读取 volatile 会产生副作用(改变 volatile 的值)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
volatile uint16_t v1;
void f(void)
{
     /* Non-compliant -
      * 对volatile类型的变量的访问可能带来对系统的后续影响 */
     uint16_t a[2] = {v1, 0};
}
void g(uint16_t x, uint16_t y)
{
     /* Compliant - no side effects
      * x和y都是局部变量,作用域在本块内,不受其他代码逻辑影响 */
     uint16_t a[2] = {x + y, x - y};
}
uint16_t x = 0u;
extern void p(uint16_t a[2]);
void h(void)
{
     /* Non-compliant - two side effects
      * 很明显执行后会修改x的值,产生影响 */
     p((uint16_t[2]){x++, x++});
}

Rule 13.2

(必要) 表达式的值及其持久性副作用(persistent side effects)在该表达式所有允许的求值顺序下都应相同

C 语言中的序列点(sequence points)和副作用(side effects)

有些表达式会有除了得到表达式值外的其他功能,称为该表达式的副作用(Side Effects)。比如fun1(i++);,除了函数调用,i 也加了 1。

序列点(序点,Sequence Point)是一个执行程序中的分割点,在这个点之前语句产生的所有副作用都将生效,而之后语句的副作用则还没有发生。

一般认为在两个序列点之间,语句执行顺序是任意的,所以建议一个对象所保存的值最多只能被修改一次

例子:x = 2;y = (x++, x+1);,假设逗号,不是序列点,则表达式(x++,x+1)的执行顺序可以任意,则 y 的值可能为 x+1(x++在之后执行)也就是 3;也可能为 x++后的 x+1,也就是 4,因为没有序列点的情况下x++x+1执行顺序可以随意,所以标准规定逗号,是一个序列点,则在逗号前,副作用x++必须生效,也就是 x++必须先执行,这样 y 的值明确为 4。

例子:x = 2;y = (x++) * (x++);,这里只有分号是序列点,但两个序列点之间其实有两个副作用,x 被修改了两次,最后 y 的值会是无法预估的,4 和 6 都是符合标准的。程序员需要对此负责。

在任何两个相邻的序列点之间或在任何完整的表达式中:

  1. 不得对任何对象进行一次以上的修改;
  2. 不得对任何对象同时进行修改和读取,除非对该对象值的读取有助于计算要存储到该对象中的值;
  3. 不得进行一次以上具有 volatile 限定类型的修改访问;
  4. 不得进行一次以上具有 volatile 限定类型的读取访问

注:对象可以通过指针或调用函数间接访问,也可以通过表达式直接访问。

注:本解释有意比规则标题更严格。因此,本规则不允许诸如:x = x = 0; 这样的表达式(尽管只要 x 不是易失性的,其值和持久性副作用就与求值顺序无关)。

C90 和 C99 标准的附件 C 对序列点进行了总结。C90 中的序列点是 C99 中序列点的子集。完整表达式的定义见 C90 标准第 6.6 节和 C99 标准第 6.8 节。

该标准为编译器求值表达式提供了相当大的灵活性。大多数运算符都可以按任意顺序求值。主要的例外情况有:

  • 逻辑 AND && 运算符是序列点,只有当第一个操作数求值为非零时,才对第二个操作数求值;
  • 逻辑 OR || 运算符是序列点,只有当第一个操作数求值为零时,才对第二个操作数求值;
  • 条件运算符 ?: 运算符中?是序列点,总是先对第一个操作数求值,然后再对第二个或第三个操作数求值;
  • 逗号运算符,是序列点,先对第一个操作数求值,然后再对第二个操作数求值。(注意不是分隔函数参数的逗号)

注意:括号的存在可能会改变运算符的应用顺序。但是,这并不影响最底层操作数的运算顺序,这些操作数可以按任何顺序运算。遵循规则 13.3 和规则 13.4 的建议,可以避免许多与表达式求值相关的不可预测行为的常见情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
typedef struct struct_fn_t
{
    void (*fn)(struct struct_fn_t *s_fn_ptr);
} struct_fn;

typedef struct_fn *struct_fn_ptr;
extern struct_fn *get_struct_fn_ptr(void);
extern void f(uint16_t xx, uint16_t yy);
extern volatile uint16_t v1, v2;

static struct_fn_ptr g2(struct_fn **hn_ptr)
{
    (*hn_ptr)++;
    return *hn_ptr;
}

#define COPY_ELEMENT(index) (b[(index)] = c[(index)]) /* violates D.4.9 */

void R_13_2(void)
{
    uint16_t i = get_uint16();
    uint16_t b[10];
    uint16_t c[10] = {0u};

    COPY_ELEMENT(i++); /* Non-compliant - i is modified twice and also read  */
                       /* Also breaks R.13.3                                 */

    uint16_t t;
    t = v1 + v2; /* Non-compliant - read order of v1 and v2 unspecified */

    volatile uint8_t PORT = get_uint8();
    PORT = PORT & 0x80u; /* Compliant - PORT is read and modified     */

    i = 0;
    f(i++, i); /* Non-compliant - 注意这里的逗号不是序列点,到函数参数结束才
                * 是序列点,不符合规则2对同一个变量不能同时修改和访问,
                * 此时i++和读取i执行的顺序任意,结果无法预估。
                * order of evaluation unspecified     */
               /* Also breaks R.13.3                                  */

    struct_fn *p = get_struct_fn_ptr();
    p->fn(g2(&p)); /* Non-compliant,p被同时访问和修改,到底是g2先执行修改了p对象的fn,还是p->fn先执行。  */
}

Rule 13.3

(建议) 包含自增(++)或自减(–)运算符的完整表达式,除由自增或自减运算符引起的副作用外,不应有其他潜在的副作用

本规则中,函数调用含有副作用;所有子表达式都视为被执行,即使《标准》中并不要求,比如,(1u == 1u) ? 0u : u8b++;中的u8b++也视为会被执行并产生副作用

不建议将增量运算符和减量运算符与其他运算符结合使用,因为:

  • 会严重影响代码的可读性;
  • 在语句中引入额外的副作用,有可能产生未定义的行为(见 Rule 13.2)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
u8a = u8b++; /* Non-compliant */
u8a = ++u8b + u8c--; /* Non-compliant */

// 下面合规
++u8b;
u8a = u8b + u8c;
u8c--;
x++;
a[i]++;
b.x++;
c->x++;
++(*p);
*p++;
(*p)++;

// 下面不合规因为函数调用也有副作用
if ((f() + --u8a) == 0u)
{
}
g(u8b++);

// 下面不合规因为本规则将逻辑上不会被执行到的子表达式也视为会产生副作用
u8a = (1u == 1u) ? 0u : u8b++;
if (u8a++ == ((1u == 1u) ? 0u : f()))
{
}

cppcheck 无法识别函数带来的副作用

Rule 13.4

(建议) 不得使用赋值运算符的结果

即使包含赋值运算符的表达式未被求值,如(1u == 1u) ? 0u : a[x] = a[x=y];,后面的表达式逻辑上不可达,该规则也适用。

不建议将简单或复合赋值运算符与其他算术运算符结合使用,原因如下:

  • 会大大影响代码的可读性;
  • 会在语句中引入额外的副作用,使避免 Rule 13.2 所涉及的未定义行为变得更加困难
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
x = y; /* Compliant */

a[x] = a[x = y]; /* Non-compliant - also breaks R.13.2 */

bool_t bool_var = get_bool();

if (bool_var = false) /* Non-compliant - also breaks R.14.3 */
{
}

if ((0u == 0u) || (bool_var = true)) /* Non-compliant, also breaks R.13.5, R.14.3 */
{
}

if ((x = f4()) != 0) /* Non-compliant */
{
}

a[b += c] = a[b]; /* Non-compliant - also breaks R.13.2  */

x = b = c = 1; /* Non-compliant */

Rule 13.5

(必要) 逻辑与(&&)和逻辑或(||)的右操作数不得含有持久性副作用

&&||运算符右侧操作数的评估以左侧操作数的值为条件。如果右侧操作数包含副作用,那么这些副作用可能会发生,也可能不会发生,这可能与程序员的预期相反。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
static uint16_t f5(uint16_t y)
{
    uint16_t temp;
    /* This side effect is not persistent as seen by the caller */
    temp = y & 0x8080U;

    return temp;
}

static uint16_t h5(uint16_t y)
{ /* function has persistent side_effect as seen by the caller */
    static uint16_t temph = 0;

    temph = y + temph;

    return temph;
}

void g(void)
{
    /* Compliant - f ( ) has no persistent side effects */
    if (ishigh && (a == f(x)))
    {
    }
    /* Non-compliant - h ( ) has a persistent side effect */
    if (ishigh && (a == h(x)))
    {
    }
}
volatile uint16_t v;
uint16_t x;
/* Non-compliant - access to volatile v is persistent */
if ((x == 0u) || (v == 1u))
{
}
/* Non-compliant if fp points to a function with persistent side effects */
(fp != NULL) && (*fp)(0);

Rule 13.6

(强制) sizeof 运算符的操作数不得包含任何可能产生副作用的表达式

根据《标准》,sizeof 操作符中出现的任何表达式通常不会被求值。但此规则还是要求任何此类表达式的求值均不应包含副作用,无论它是否实际上被求值。

对于此规则,函数调用被视为副作用。

sizeof 操作符的操作数可以是表达式,也可以是指定类型。如果操作数包含一个表达式,可能出现的编程错误是预期该表达式将被求值,而实际上在大多数情况下它是不会被求值的:

  • C90 标准规定,操作数中出现的表达式在运行时不被求值。
  • 在 C99 中,操作数中出现的表达式通常不会在运行时被求值。但是,如果操作数包含一个可变长的数组类型(Variable Length Array,VLA),那么必要时将对数组大小表达式进行求值。如果不求值数组大小表达式也能确定结果,则不指定是否求值。

例外

允许使用 sizeof(V) 形式的表达式(V 不是变长数组),其中 V 是具有 volatile 限定类型的左值(无视 volatile 读取的副作用)。

1
2
3
4
5
6
7
8
9
10
11
12
s = sizeof(int32_t[n]);                /* Compliant, but breaks R.18.8 */
s = sizeof(int32_t[n++]);              /* Non-compliant, also breaks R.18.8  */
s = sizeof(void (*[n])(int32_t a[v])); /* Non-compliant, also breaks R.18.8  */

volatile int32_t i;
int32_t j;
size_t s;

s = sizeof(j);       /* Compliant              */
s = sizeof(j++);     /* Non-compliant          */
s = sizeof(i);       /* Compliant - exception  */
s = sizeof(int32_t); /* Compliant              */

组别 14:控制语句表达式

本节的某些规则使用了循环计数器(loop counter)这一术语。循环计数器定义为满足以下条件的对象、数组元素、(结构体或联合体)成员:

  1. 它具有标量(scalar)类型;
  2. 它的值在给定循环实例的每次迭代中单调变化;
  3. 它参与了退出循环的决策。

注意:第二个条件意味着循环计数器的值必须在循环的每次迭代中发生变化,而且在给定的循环实例中必须始终朝同一方向变化。但是,在不同的实例中,计数器的变化方向可能不同,例如,有时向后读取数组元素,有时向前读取数组元素。

根据这个定义,一个循环不一定只有一个循环计数器:一个循环可以没有循环计数器,也可以有多个循环计数器。关于 for 循环中循环计数器的更多限制,请参见 Rule 14.2

Rule 14.1

(必要) 循环计数器的基本类型不能为浮点型

使用循环计数器时,四舍五入误差的累积可能导致预期迭代次数与实际迭代次数不匹配。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
for (float32_t f1 = 0.0f; f1 < 1.0f; f1 += 0.001f) /* Non-compliant */
{
    ++sum;
}

float32_t f;
for (uint32_t counter = 0u; counter < 1000u; ++counter) /* compliant */
{
    f = (float32_t)counter * 0.001f;
}

f = 0.0f;
while (f < 1.0f) /* Non-compliant */
{
    f += 0.001f;
}

uint32_t u32a;
f = get_float32();
do
{
    u32a = get_uint32();
    /* f does not change in the loop so cannot be a loop counter */
} while (((float32_t)u32a - f) > 10.0f); /* compliant */

Rule 14.2

(必要) for 循环应为良好格式

for 语句的三个子句:

  • 第一个子句
    • 应为空,或
    • 应为循环计数器赋值,或
    • 应定义并初始化循环计数器(C99)。
  • 第二个子句
    • 应是一个没有持久性副作用的表达式
    • 应使用循环计数器和可选的循环控制标志
    • 不得使用在 for 循环体中被修改的任何其他对象
  • 第三个子句
    • 应是一个表达式,其唯一的持久性副作用是修改循环计数器的值
    • 不得使用在 for 循环体中被修改的任何对象

for 循环中只能有一个循环计数器,该计数器不得在 for 循环体中修改。

循环控制符被定义为一个单一标识符,表示在第二子句中使用的布尔类型对象。

for 循环体的行为包括在该语句中调用的任何函数的行为,也就是在循环体中调用函数修改循环计数器也是不允许的。

原因

for 语句提供了通用循环工具。使用受限形式的循环使代码更易于审查和分析

例外

这三个子句都可以是空的,例如 for ( ; ; ) ,以便允许无限循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static void set_val(int32_t *ind)
{
    *ind = 0;
}

bool_t flag = false;
bool_t C = get_bool();

for (int16_t i = 0; (i < 5) && !flag; i++)
{
    if (C)
    {
        flag = true; /* Compliant - allows early termination of loop */
    }

    i = i + 3; /* Non-compliant - altering the loop counter,不允许在循环体内修改循环计数器 */
}

int32_t index;
for (set_val(&index); index < 10; index++) /* compliant - index assigned a value within set_val */
{
    use_int32(index);
}

Rule 14.3

(必要) 控制表达式不得是值不变的

本规则适用于:

  • 控制 if、while、for、do…while 和 switch 语句的表达式;
  • ?: 操作符的第一个操作数。

如果控制表达式具有不变值,则可能存在编程错误。由于存在不变表达式而无法执行的代码可能会被编译器删除。例如,这可能会从可执行代码中删除防御代码

例外

  1. 用于创建无限循环的不变式被允许。
  2. 允许 do … while 循环使用 Boolean 基本类型的控制表达式,且其值为 0。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
const uint8_t numcyl = 4u;
const volatile uint8_t numcyl_cal = 4u;

if (numcyl == 4u) /* Non-compliant - always 4 */
{
}

if (numcyl_cal == 4u) /* Compliant - volatile may change */
{
}

s8a = (u16a < 0u) ? 0 : 1; /* Non-compliant - u16a always >= 0 */

if (u16a <= 0xffffu) /* Non-compliant - always true */
{
}

if (2 > 3) /* Non-compliant - always false */
{
}
if ((s8a < 10) && (s8a > 20)) /* Non-compliant - always false */
{
}

if ((s8a < 10) || (s8a > 5)) /* Non-compliant - always true */
{
}
while (s8a > 10)
{
    if (s8a > 5) /* Non-compliant - s8a always > 5 as not volatile */
    {
    }
    break;
}
do
{
} while (0u == 1u); /* Compliant by exception */

for (s8a = 0; s8a < 130; ++s8a) /* Non-compliant - always true, also breaks R.2.1 */
{
}

while (true) /* Compliant by exception, but breaks R.2.1 */
{
}

Rule 14.4

(必要) if 语句和循环语句的控制表达式的基本类型应为 Boolean 基本类型

for 类型中控制表达式可为空;严格的类型要求 if 语句或迭代语句的控制表达式必须具有 Boolean 基本类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
while (p) /* Non-compliant - p is a pointer */
{
    p = get_int32_ptr();
}

while (q != NULL) /* Compliant */
{
    q = get_int32_ptr();
}

bool_t flag = get_bool();
while (flag) /* Compliant */
{
    flag = get_bool();
}

if (i) /* Non Compliant */
{
}

if (i != 0) /* Compliant */
{
}

while (true) /* Compliant, but breaks R.2.1  */
{
}

组别 15:控制流

Rule 15.1

(建议) 不应使用 goto 语句

无限制地使用 goto 会导致程序结构混乱,极难理解。

在某些情况下,完全禁用 goto 需要引入”flags”以确保正确的控制流,而这些”flags”本身可能比所取代的 goto 更不透明。所以部分情况下可以不遵守本规则,可以在遵守规则 15.2 和规则 15.3 的情况下有限制地使用 goto。

1
2
3
4
5
6
void R_15_1(void)
{
    goto lab1; /* Non-compliant */
lab1:
    use_int32(3);
}

Rule 15.2

(必要) goto 语句仅允许跳到在同一函数中在此之后声明的标签

无拘无束地使用 goto 语句可能导致程序变得不可组织且难以理解。限制 goto 语句的使用,禁止向上跳跃,确保迭代仅通过语言中提供的迭代语句实现,有助于减少代码复杂性。

1
2
3
4
5
6
7
8
9
10
11
12
13
void R_15_2(void)
{
    int32_t j = 0;
L1:
    ++j;
    if (10 == j)
    {
        goto L2; /* Compliant     */
    }
    goto L1; /* Non-compliant */
L2:
    ++j;
}

Rule 15.3

(必要) goto 语句引用的标签必须在 goto 语句所在代码块或包含该代码块的父级代码块中声明

就本规则而言,不包含复合语句的 switch 子句也将被视为块。

无限制地使用 “goto” 会导致程序结构混乱,极难理解。

防止同级的程序块之间或超过一层的嵌套程序块中的跳转,有助于最大限度地降低代码的复杂性。

跳到父级代码块被允许,是因为这类似于 break 是跳出本块的行为。

注意:如果试图从具有可变长修改类型(variably modified types,C99 新增类型)的标识符的作用域之外跳转到这样的作用域,就会导致违反约束。(TODO:解释)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
static void f1(int32_t a)
{
    if (a <= 0)
    {
        goto L2; /* Non-compliant,不能跳到同级块 */
    }

    goto L1; /* Compliant     */

    if (a == 0) /* breaks R.2.1  */
    {
        goto L1; /* Compliant,可以跳到父块     */
    }

    goto L2; /* Non-compliant,不能跳到子块*/

L1:
    if (a > 0) /* breaks R.14.3 */
    {
    L2:;
    }
}

void R_15_3(void)
{

    switch (x)
    {
    case 0:
        if (x == y)
        {
            goto L1; /* Non-compliant,不允许跳转到同级块 */
        }
        break;
    case 1:
        y = x;
    L1:
        ++x;
        break;
    default:
        /* no action */
        break;
    }

    f1(x + y);
}

Rule 15.4

(建议) 最多只能有一个用于终止循环语句的 break 或 goto 语句

限制循环出口的数量可以最大限度地减少可视化代码的复杂性。当需要提前终止循环时,使用一个 break 或 goto 语句可以创建一个第二退出路径(第一退出路径就是整个循环结束)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#define LIMIT 100u

/* Note: All uses of goto also break R.15.1 */

void R_15_4(void)
{
    uint32_t x;
    uint32_t y;
    uint32_t z;

    for (x = 0; x < LIMIT; ++x)
    {
        if (ExitNow(x))
        {
            break; /* compliant - single exit from outer loop */
        }

        for (y = 0; y < x; ++y)
        {
            if (ExitNow(LIMIT - y))
            {
                break; /* compliant - single exit from inner loop
                        * 这个break仅用来退出本for循环而不是上级for,
                        * 所有和上面那个break不冲突 */
            }
        }
    }

    for (x = 0; x < LIMIT; ++x)
    {
        if (BreakNow(x))
        {
            break;
        }
        else if (GotoNow(x))
        {
            goto EXIT; /* Non-compliant - break and goto in loop */
        }
        else
        {
            KeepGoing(x);
        }
    }

EXIT:;

    while (x != 0u)
    {

        if (x == 1u)
        {
            break;
        }

        while (y != 0u)
        {
            if (y == 1u)
            {
                // 这个goto直接退了两层while,和上面的break冲突
                goto L1; /* Non-compliant (outer loop) Compliant (inner loop)   */
                         /* goto causes early exit of both inner and outer loop */
            }
        }
    }
L1:
    z = x + y;
}

Rule 15.5

(建议) 应仅在函数的末尾有单个函数出口

一个函数的 return 语句不应超过一个。当使用 return 语句时,它应该是构成函数主体的复合语句中的最后一条语句

作为模块化方法要求的一部分,IEC 61508 和 ISO 26262 规定了单点退出。

提前返回可能会导致无意中遗漏函数终止代码。

如果函数的退出点与产生持续副作用的语句穿插在一起,就不容易确定在执行函数时会出现哪些副作用。

在之前的项目中就遇到过,提前 return 退出导致临界变量没有解锁,造成死锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static bool_t f(uint16_t n, const char *p)
{
    if (n > MAX)
    {
        return false; /* Non-compliant */
    }

    if (p == NULL)
    {
        return false; /* Non-compliant */
    }

    return true;
}

Rule 15.6

(必要) 循环语句和选择语句的主体应为复合语句

  • 循环语句(while、do … while 或 for)
  • 选择语句(if、else、switch)

复合语句表示用大括号{}包裹的块

开发人员有可能误以为一连串语句通过缩进构成了迭代语句或选择语句的主体。在控制表达式后意外加入分号是一种特别的危险,会导致控制语句无效。使用复合语句可以明确定义哪些语句真正构成主体。

此外,缩进可能会导致开发人员将 else 语句与错误的 if 语句联系起来。

紧随 else 之后的 if 语句不必包含在复合语句中,也就是else if()不用在 else 后加{括号(elseif是两个关键字,else if是个常用代码结构,而非一个关键字)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
while (data_available)
    data_available = process_data(); /* Non-compliant,未用{}包裹 */

if (flag_1)
    if (flag_2)     /* Non-compliant */
        action_1(); /* Non-compliant */
    else
        action_2(); /* Non-compliant */

if (flag_1)
{
    action_1();
}
else if (flag_2) /* Compliant by exception */
{
    action_2();
}
else
{
    ; /* no action */
}

while (flag); /* Non-compliant,这种情况是误加了一个分号 */
{
    flag = fn();
}

while (!data_available)
{
    data_available = process_data();
}

Rule 15.7

(必要) 所有的 if(){}else if{} 构造都应以 else 语句结束

每当 if 语句后有一个或多个 else if 结构序列时,必须提供最后的 else 语句。else 语句应至少包含一个副作用或一个注释。

用 else 语句终止 if…else 构造序列是一种防御性编程,是对 switch 语句中 default 子句要求的补充(见 Rule 16.5)。

else 语句必须有附带副作用或注释,以确保对所需行为作出正面说明,从而有助于代码审查过程。

注意:简单的 if 语句不需要最后的 else 语句。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
if (flag_1)
{
    action_f1();
}
else if (flag_2)
{
    action_f2();
}
/* Non-compliant */

if (flag_1)
{
    action_f1();
}
else if (flag_2)
{
    action_f2();
}
else
{
    ; /* No action required - ; is optional */
}

组别 16:switch 语句

Rule 16.1

(必要) switch 语句应格式正确

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
switch-statement:
  switch ( switch-expression ) { case-label-clause-list final-default-clause-list }
  switch ( switch-expression ) { initial-default-clause-list case-label-clause-list }

case-label-clause-list:
  case-clause-list
  case-label-clause-list case-clause-list

case-clause-list:
  case-label switch-clause
  case-label case-clause-list

case-label:
  case constant-expression:

final-default-clause-list:
  default: switch-clause
  case-label final-default-clause-list

initial-default-clause-list:
  default: switch-clause
  default: case-clause-list

switch-clause:
  statement-list(可选) break;
  C90: { declaration-listopt statement-list(可选) break; }
  C99: { block-item-list(可选) break; }

C 语言中 switch 语句的语法并不特别严格,可以允许复杂的、非结构化的行为。本规则和其他规则为 switch 语句规定了简单一致的结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
switch (zwitch)
{
    uint8_t decl; /* Non-compliant */
    case 0U:
    {
        zwitched += 1U;
        decl = zwitched;
        use_uint8(decl);
        break;
    }
    case 1U:
    {
        uint8_t local = zwitched;
        use_uint8(local);
        break;
    }
    case 2U:
        use_uint8(zwitched);
        break;
    default:
    {
        break;
    }
}

Rule 16.2

(必要) switch 标签只能出现在构成 switch 语句主体的复合语句的最外层

《标准》允许将 switch 的标签(即 case 标签或 default 标签)放置在 switch 语句主体中包含的任何语句之前,这可能会导致代码结构混乱。为了防止这种情况,switch 标签只能出现在形成 switch 语句主体的复合语句的最外层。

1
2
3
4
5
6
7
8
9
10
11
12
13
switch (x)
{
    case 1:
    if (flag)
    {
    case 2: /* Non-compliant */
        x = 1;
    }
    break;
    default:
    /* no action */
    break;
}

Rule 16.3

(必要) 每一个 switch 子句(switch-clause)都应以无条件 break 语句终止

如果开发人员没有用 break 语句结束一个 switch 子句,那么控制权就会 “落入”下面的 switch 子句,如果没有 switch 子句,控制权就会脱离 switch 子句,进入 switch 语句后面的状态。

虽然控制权 “落入”下面的 switch 子句有时是有意为之,但大多数情况下是漏写 break 了。在 switch 语句末尾出现的未终止的 switch 子句可能会落入后来添加的任何新的 switch 子句中。

为确保能检测到此类错误,每个 switch 子句的最后一条语句都应是 break 语句,如果 switch 子句是复合语句,则复合语句的最后一条语句应是 break 语句。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
switch (x)
{
    case 0:
        break; /* Compliant - unconditional break */
    case 1:    /* Compliant - empty fall through allows a group */
    case 2:
        break; /* Compliant */
    case 4:
        a = b; /* Non-compliant - break omitted */
    case 5:
        if (a == b)
        {
            ++a;
            break; /* Non-compliant - conditional break */
        }
    default:; /* Non-compliant - default must also have a break */
}

Rule 16.4

(必要) 每个 switch 语句都应具有 default 标签

default 标签之后 break 语句之前的包含以下任一内容:

  • 语句
  • 注释

对 default 标签的要求是防御性编程。default 标签后面的任何语句都是为了采取某种适当的行动。如果 default 标签后面没有任何语句,则可以用注释来解释为什么没有采取任何具体操作。

1
2
3
4
5
6
7
8
9
10
switch (x)
{
    case 0:
        ++x;
        break;
    case 1:
    case 2:
        break;
        /* Non-compliant - default label is required */
}

Rule 16.5

(必要) Default 标签应作为 switch 语句的第一个或最后一个 switch 标签

这条规则使得在 switch 语句中找到 defalut 标签变得容易。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
switch (x)
{
    default: /* Compliant - default is the first label */
    case 0:
        ++x;
        break;
    case 1:
    case 2:
        break;
}

switch (x)
{
    case 0:
        ++x;
        break;
    default: /* Non-compliant - default is mixed with the case labels */
        x = 0;
        break;
    case 1:
    case 2:
        break;
}

Rule 16.6

(必要) 每个 switch 语句应至少有两个 switch 子句

只有一条路径的 switch 语句是无意义的,可能表明编程错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
switch (x)
{
    default: /* Non-compliant */
        x = x + 1;
        break;
}

switch (y)
{
    case 1:
    default: /* Non-compliant */
        y = y + 1;
        break;
}

Rule 16.7

(必要) switch 语句的控制表达式(switch-expression)的基本类型不得是布尔型

标准要求 switch 语句的控制表达式具有整数类型。如果用于实现布尔值的类型是整型,则可以用这种布尔表达式来控制 switch 语句。

对于布尔型,使用 if-else 结构会更合适。

1
2
3
4
5
6
7
8
9
switch (x == 0) /* Non-compliant */
{
    case false:
        y = x;
        break;
    default:
        y = z;
        break;
}

组别 17:函数

Rule 17.1

(必要) 不得使用<stdarg.h>的功能

不得使用 va_list、va_arg、va_start、va_end 以及 va_copy(C99)

《标准》列出了许多与 <stdarg.h> 功能相关的未定义行为实例,包括

  • 在使用了 va_start 的函数结束之前没有使用 va_end;
  • 在同一 va_list 上的不同函数中使用了 va_arg;
  • 参数类型与 va_arg 指定的类型不兼容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <stdarg.h>

static void h(va_list ap) /* Non-compliant */
{
    float64_t y;
    y = va_arg(ap, float64_t); /* Non-compliant */
    use_float64(y);
}

static void f(uint16_t n, ...)
{
    uint32_t x;

    va_list ap; /* Non-compliant */

    va_start(ap, n);          /* Non-compliant */
    x = va_arg(ap, uint32_t); /* Non-compliant */

    h(ap);

    /* undefined - ap is indeterminate because va_arg used in h ( ) */
    x = va_arg(ap, uint32_t); /* Non-compliant */

    /* undefined - returns without using va_end ( ) */
}

void R_17_1(void)
{
    /* undefined - uint32_t:double type mismatch when f uses va_arg( ) */
    f(1, 2.0, 3.0);
}

Rule 17.2

(必要) 函数不得直接或间接调用自身(不得使用递归函数)

递归有可能导致栈空间溢出,从而引发严重的故障。除非递归非常严格地控制,否则在执行之前无法确定最坏情况下的栈空间使用量。

1
2
3
4
5
6
7
8
9
10
11
12
13
static uint16_t fn_a(uint16_t parama)
{
    uint16_t ret_val;
    if (parama > 0U)
    {
        ret_val = parama * fn_a(parama - 1U); /* Non-compliant */
    }
    else
    {
        ret_val = parama;
    }
    return ret_val;
}

Rule 17.3

(强制) 禁止隐式声明函数

只要函数调用是在有原型的情况下进行的,约束就能确保实参(arguments)个数与形参(parameters)个数相匹配,并且每个实参都能分配给相应的形参。

如果函数是隐式声明的,C90 编译器将假定函数的返回类型为 int。由于隐式函数声明不提供原型,因此编译器将不知道函数形参的数量及其类型。不恰当的类型转换可能导致传递参数和赋值返回值时出现错误,以及其他未定义的行为。

隐式声明函数的使用在早期的 C 语言标准中是合法的,但在现代的 C 语言标准中已经不再推荐使用,并且在严格的编译器中可能会产生警告或错误。

1
2
3
4
5
6
7
8
9
10
11
// extern int add(int,int); // 未声明

void R_17_3(void)
{
    float64_t sq1 = add(1, 2.0); /* Non-compliant  */
}

int add(int a, int b)
{
    return a + b;
}

Rule 17.4

(强制) 具有非 void 返回类型的函数的所有退出路径都应为具有带有表达式的显式 return 语句

return 语句的表达式提供了函数返回的值。如果一个非 void 函数未返回值,但调用函数时使用了返回值,则行为未定义。可以通过确保在非 void 函数中满足以下条件来避免这种情况:

  • 每个 return 语句都有一个表达式,并且
  • 不能在没有遇到返回语句的情况下到达函数的末尾。

注意:C99 规定非 void 函数中的每个 return 语句必须返回一个值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static int32_t absolute(int32_t v)
{
    if (v < 0)
    {
        return v;
    }
    /* Non-compliant - control can reach this point without
     * returning a value, also breaks R.15.5
     */
}

static uint16_t lookup(const uint16_t table[5], uint16_t v)
{
    if ((v < V_MIN) || (v > V_MAX))
    {
        /* Non-compliant - no value returned. Constraint in C99
         * 靠编译器检查 */
        return;
    }
    return table[v]; /* Also breaks R.15.5 */
}

Rule 17.5

(建议) 与数组型函数形参对应的函数入参应具有适当数量的元素

如果形参被声明为具有特定大小的数组,则每次函数调用中,实参数组对象的元素数量要和形参相同。

与使用指针相比,为函数形参使用数组声明符能更清楚地说明函数接口。函数所期望的最小元素数被明确指出,而指针则无法做到这一点。

如果函数形参数组声明符没有指定大小,则假定该函数可以处理任意大小的数组。在这种情况下,预计数组的大小将通过其他方式传递,例如作为另一个参数传递,或以一个哨兵值(sentinel value)结束数组。

建议使用数组绑定,因为它允许在函数体中实施越界检查,并对参数传递进行额外检查。在 C 语言中,将大小不正确的数组传递给指定大小的参数是合法的,但这可能会导致意想不到的行为。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
static void fn1(const int32_t array1[4])
{
    use_int32(array1[3]);
}

/* Intent is that function handles arrays of any size               */
static void fn2(const int32_t array2[])
{
    use_int32(array2[0]);
}

static void fn(const int32_t *ptr)
{
    int32_t *ptr = get_int32_ptr();
    int32_t arr3[3] = {1, 2, 3};
    int32_t arr4[4] = {0, 1, 2, 3};

    fn1(arr4); /* Compliant - size of array matches the prototype */

    fn1(arr3); /* Non-compliant - size of array does not match prototype */

    if (ptr != NULL)
    {
        fn1(ptr); /* Compliant only if ptr points to at least 4 elements
                   * 传指针而不是数组对象也可以 */
    }

    fn2(arr4); /* Compliant,fn2允许任何长度的数组 */

    if (ptr != NULL)
    {
        fn2(ptr); /* Compliant */
    }
}

Rule 17.6

(强制) 数组形参的声明不得在[]之间包含 static 关键字

C99 语言标准为程序员提供了一种机制,可以告知编译器某个数组参数包含指定数量的最小元素。一些编译器可以利用这一信息,为某些类型的处理器生成更高效的代码。

如果程序员所做的保证没有兑现,元素数量少于规定的最小值,则行为将被取消。

典型嵌入式应用中使用的处理器可能并不提供该优化功能。程序无法达到保证的最小元素数的风险大于任何潜在的性能提升。

1
2
3
4
static uint16_t total(uint16_t n,
                      const uint16_t a[static 20] /* Non-compliant */)
{
}

Rule 17.7

(必要) 非 void 返回类型的函数的返回值应该被使用

调用函数时有可能不使用返回值,这可能是一个错误。如果不想明确使用函数的返回值,则应将其转换为 void 类型。这样可以在不违反 Rule 2.2 的情况下使用返回值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static uint16_t func(uint16_t para1)
{
    return para1;
}

static void discarded(uint16_t para2)
{
    uint16_t x;

    func(para2); /* Non Compliant - value discarded  */

    (void)func(para2); /* Compliant  */

    x = func(para2); /* Compliant  */

    use_uint16(x);
}

Rule 17.8

(建议) 不应更改函数形参

函数形参的行为方式与具有自动存储期限的对象相同。虽然 C 语言允许修改参数,但这种使用方式可能会造成混乱,并与程序员的期望相冲突。将参数复制到自动对象中并修改该副本,可能会减少混乱。对于现代编译器来说,这通常不会造成任何存储或执行时间上的损失。

不熟悉 C 语言但习惯于其他语言的程序员可能会修改参数,认为修改的效果会在调用函数中体现出来。

1
2
3
4
5
6
7
8
9
10
int16_t glob = 0;
void proc(int16_t para)
{
    para = glob; /* Non-compliant */
}
void f(char *p, char *q)
{
    p = q;   /* Non-compliant */
    *p = *q; /* Compliant */
}

组别 18:指针和数组

Rule 18.1

(必要) 指针操作数的算术运算应仅用于寻址与该指针操作数相同数组的元素

也就是指针运算仅用于找数组内元素。比如指针运算中 +1表示增加指针的值,使其指向下一个相邻的内存位置,也就是数组内的后一个元素。

创建指针指向数组末尾的后一个元素的是《标准》明确定义的,本规则允许这样做。但解引用这类指针会导致未定义的行为,本规则禁止这样做。

这条规则适用于所有形式的数组索引:

1
2
3
4
5
6
7
8
9
10
11
integer_expression + pointer_expression
pointer_expression + integer_expression
pointer_expression - integer_expression
pointer_expression += integer_expression
pointer_expression -= integer_expression
++pointer_expression
pointer_expression++
--pointer_expression
pointer_expression--
pointer_expression [ integer_expression ]
integer_expression [ pointer_expression ]

注:子数组也是数组。

注:就指针运算而言,《标准》将不属于数组成员的对象视为具有单个元素的数组(C90 第 6.3.6 节,C99 第 6.5.6 节)。

虽然有些编译器可以在编译时确定数组边界已被超出,但一般不会在运行时检查数组下标是否无效。使用无效的数组下标会导致程序出现错误的行为。

运行时派生的数组(比如用 malloc 分配的数组)下标值最令人担忧,因为它们不容易通过静态分析或人工审核进行检查。在可能和可行的情况下,应提供防御性编程代码,以便对照有效值检查此类下标值,并在必要时采取适当措施。

如果从上述表达式中得到的结果不是指向 pointer_expression 所指向数组元素的指针(也就是不属于该数组),或者不是超出数组末尾一个元素的指针,则是未定义的行为。更多信息请参见 C90 第 6.3.6 节和 C99 第 6.5.6 节。

多维数组是 “数组的数组”。本规则不允许指针运算导致指针寻址到不同的子数组(同一级别的不同数组)。不得在 “内部” 边界上使用数组下标,比如int a[10];a[10]=0;,因为这种行为是未定义的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
int32_t f1(int32_t *const a1, int32_t a2[10])
{
    int32_t *p = &a1[3]; /* Compliant/non-compliant depending on
                          * the value of a1 */
    return *(a2 + 9);    /* Compliant,找第10个元素 */
}
void f2(void)
{
    int32_t data = 0;
    int32_t b = 0;
    int32_t c[10] = {0};
    int32_t d[5][2] = {0}; /* 5-element array of 2-element arrays
                            * of int32_t */
    int32_t *p1 = &c[0];   /* Compliant */
    int32_t *p2 = &c[10];  /* Compliant - points to one beyond,
                            * 允许指针指向末尾的后第一个元素 */
    int32_t *p3 = &c[11];  /* Non-compliant - undefined, points to
                            * two beyond,指向了末尾的后第二个元素,就不行了 */
    data = *p2;            /* Non-compliant - undefined, dereference
                            * one beyond,解引用这类超出末尾的指针就不行 */
    data = f1(&b, c);
    data = f1(c, c);
    p1++;                   /* Compliant,允许++,指向后一个元素 */
    c[-1] = 0;              /* Non-compliant - undefined, array
                             * bounds exceeded */
    data = c[10];           /* Non-compliant - undefined, dereference
                             * of address one beyond */
    data = *(&data + 0);    /* Compliant - C treats data as an
                             * array of size 1,data虽然是int类型的,
                             * 但视为单元素数组 */
    d[3][1] = 0;            /* Compliant */
    data = *(*(d + 3) + 1); /* Compliant */
    data = d[2][3];         /* Non-compliant - undefined, internal
                             * boundary exceeded */
    p1 = d[1];              /* Compliant */
    data = p1[1];           /* Compliant - p1 addresses an array
                             * of size 2 */
}

struct
{
    uint16_t x;
    uint16_t y;
    uint16_t z;
    uint16_t a[10];
} s;
uint16_t *p;

void f3(void)
{
    p = &s.x;
    ++p;         /* Compliant - p points one beyond s.x,
                  * 但是这样移动后并不表示p就指向y了,
                  * 而是表示s.x作为数组时的后一个元素,当然只是概念上的,
                  * 实际并不存在 */
    p[0] = 1;    /* Non-compliant - undefined, dereference of address one
                  * beyond s.x which is not necessarily
                  * the same as s.y */
    p[1] = 2;    /* Non-compliant - undefined */
    p = &s.a[0]; /* Compliant - p points into s.a */
    p = p + 8;   /* Compliant - p still points into s.a */
    p = p + 3;   /* Non-compliant - undefined, p points more than one
                  * beyond s.a */
}

Rule 18.2

(必要) 指针之间的减法应仅用于寻址同一数组元素的指针

允许的表达式:

1
pointer_expression_1 - pointer_expression_2

如果 pointer_expression_1 和 pointer_expression_2 不指向同一数组的元素或是数组末尾之外的元素,则是未定义的行为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int32_t a1[10];
int32_t a2[10];
int32_t *p1 = &a1[1];
int32_t *p2 = &a2[10];
int32_t *ptr = get_int32_ptr();
ptrdiff_t diff;

diff = p1 - a1; /* Compliant           */

diff = p2 - a2; /* Compliant           */

diff = p1 - p2; /* Non-compliant,两个不同的数组   */

diff = ptr - p1; /* Non-compliant,ptr不指向a1数组   */

Rule 18.3

(必要) 关系运算符>,>=,<和<=不得应用于指针类型的对象,除非它们指向同一对象

如果两个指针指向的不是同一个对象,试图在指针之间进行比较将产生未定义的行为。

注意:可以寻址数组末尾元素的下一个元素,但不允许访问该元素(Rule 18.1 已经提到过了)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
void f1(void)
{
    int32_t a1[10];
    int32_t a2[10];
    int32_t *p1 = a1;
    if (p1 < a1) /* Compliant */
    {
    }
    if (p1 < a2) /* Non-compliant */
    {
    }
}
struct limits
{
    int32_t lwb;
    int32_t upb;
};
void f2(void)
{
    struct limits limits_1 = {2, 5};
    struct limits limits_2 = {10, 5};
    if (&limits_1.lwb <= &limits_1.upb) /* Compliant */
    {
    }
    if (&limits_1.lwb > &limits_2.upb) /* Non-Compliant */
    {
    }
}

cppcheck 无法检查是否是指向同一个数组的,建议不考虑这种例外情况,直接禁止全部指针比较。

Rule 18.4

(建议) +,-,+=和-=运算符不得应用于指针类型的表达式

使用数组下标语法 ptr[expr] 进行数组索引是指针运算的首选形式,因为它通常比指针操作更清晰,也更不容易出错。任何显式计算的指针值都有可能访问非预期或无效的内存地址,数组索引也可能出现这种情况,这时使用下标语法可以减轻人工审查的任务。

C 语言中的指针运算可能会让新手感到困惑。表达式 ptr+1 可能会被误解为在 ptr 中保存的地址上加 1。事实上,新的内存地址取决于指针目标的字节大小。如果不正确地使用 sizeof,这种误解可能会导致意想不到的行为。

不过,如果谨慎使用,使用 ++ 进行指针操作在某些情况下会更自然;例如,在内存测试过程中顺序访问位置时,将内存空间视为一组连续的位置会更方便,而且地址边界可以在编译时确定。

例外

Rule 18.1,Rule 18.2 规定的运算除外

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
static void fn1(void)
{
    uint8_t a[10];
    uint8_t *ptr;
    uint8_t index = 0U;

    index = index + 1U; /* Compliant - rule only applies to pointers */

    a[index] = 0U; /* Compliant */
    ptr = &a[5];   /* Compliant */
    use_uint8_ptr(ptr);

    ptr = a;
    ptr++;           /* Compliant - increment operator not + */
    *(ptr + 5) = 0U; /* Non-compliant,不能对指针做+运算 */
    ptr[5] = 0U;     /* Compliant */

    use_uint8_ptr(ptr);
}

static void fn2(void)
{
    uint8_t array_2_2[2][2] = { {1U, 2U}, {4U, 5U} };
    uint8_t i;
    uint8_t j;
    uint8_t sum = 0U;

    for (i = 0U; i < 2U; i++)
    {
        uint8_t *row = array_2_2[i];

        for (j = 0U; j < 2U; j++)
        {
            sum += row[j]; /* Compliant  */
        }
    }

    use_uint8(sum);
}

static void fn3(uint8_t *p1, uint8_t p2[])
{
    /* Note: Altering p1 and p2 also breaks R.17.8 and may violate R.18.1 depending on values */
    p1++;        /* Compliant     */
    p1 = p1 + 5; /* Non-compliant */
    p1[5] = 0U;  /* Compliant     */

    p2++;        /* Compliant     */
    p2--;        /* Compliant     */
    ++p2;        /* Compliant     */
    --p2;        /* Compliant     */
    p2 = p2 + 3; /* Non-compliant */
    p2[3] = 0U;  /* Compliant     */
}

Rule 18.5

(建议) 声明中最多包含两层指针嵌套

使用两层以上的指针嵌套会严重影响理解代码行为的能力,因此应避免使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
typedef int8_t * INTPTR;

static void function(int8_t **arrPar[]) /* Non-compliant */
{
    int8_t **obj2;             /* Compliant     */
    int8_t ***obj3;            /* Non-compliant */
    INTPTR *obj4;              /* Compliant     */
    INTPTR *const *const obj5; /* Non-compliant */
    int8_t **arr[10];          /* Compliant     */
    int8_t **(*parr)[10];      /* Compliant     */
    int8_t *(**pparr)[10];     /* Compliant     */

    if (arrPar[0] != NULL)
    {
        if (*(arrPar[0]) != NULL)
        {
            **(arrPar[0]) = get_int8();
        }
    }
}

static int8_t **(*pfunc1)(void);   /* Compliant     */
static int8_t **(**pfunc2)(void);  /* Compliant     */
static int8_t **(***pfunc3)(void); /* Non-compliant */
static int8_t ***(**pfunc4)(void); /* Non-compliant */

struct s
{
    int8_t *s1;   /* Compliant     */
    int8_t **s2;  /* Compliant     */
    int8_t ***s3; /* Non-compliant */
};

Rule 18.6

(必要) 具有自动存储功能的对象的地址不得复制给在它的生命周期结束后仍会存在的另一个对象

对象的地址可以通过以下方式复制:

  • 赋值(Assignment)
  • 内存移动或复制功能

当一个对象的生命周期结束时,该对象的地址就会变得不确定。使用不确定的地址会导致不确定的行为。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
static int8_t *func(void)
{
    int8_t local_auto;
    return &local_auto; /* Non-compliant - &local_auto is indeterminate
                         * when func returns */
}

uint16_t *sp;

static void g(uint16_t *p)
{
    sp = p; /* Non-compliant - 调用者f函数中u参数的地址被赋值给
             * 具有更长生命周期的外部链接变量sp */
}

static void f(uint16_t u)
{
    g(&u);
}

static void h(void)
{
    static uint16_t *q;
    uint16_t x = 0u;
    q = &x; /* Non-compliant - &x stored in object with
             * greater lifetime */
}

Rule 18.7

(必要) 不得声明灵活数组成员

灵活的数组成员最有可能与动态内存分配结合使用,而动态内存分配是 Dir 4.12 和 Rule 21.3 所禁止的。

可变数组成员的存在会让 sizeof 操作符的行为与程序员预想情况不同。将包含可变数组成员的结构体赋值给另一个相同类型的结构体时,可能不会按照预期的方式进行,因为它只会拷贝结构体中可变数组成员之前的成员,而不包括该可变数组成员。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct sdata
{
    uint16_t len;
    // data成员将不计入对sdata进行sizeof时的长度计算
    uint32_t data[]; /* Non-compliant */
};

static struct sdata *copy(const struct sdata *s1)
{
    struct sdata *s2;

    int sdata_len = sizeof(struct sdata); // sdata_len为2,也就是只记录了len成员的长度

    s2 = (struct sdata *)malloc(sizeof(struct sdata) + ((s1->len) * sizeof(uint32_t)));
    /* breaks R.21.3 */

    if (s2 != NULL)
    {
        *s2 = *s1; /* only copies s1->len,data成员不会被拷贝 */
    }
    return s2;
}

Rule 18.8

(必要) 不得使用可变长数组类型

当程序块或函数原型中声明的数组大小不是一个整数常量表达式时,就需要使用变长数组类型。它们通常是作为存储在栈中的可变大小对象来实现的。因此,使用变长数组无法静态确定必须为栈预留的内存量。

如果可变长度数组的大小为负数或零,则其行为是未定义的。

如果在使用变长数组时,要求它与另一种数组类型(可能本身就是变长的)兼容,那么这两种数组类型的大小必须相同。此外,所有大小都应为正整数。如果不满足这些要求,则行为未定义。

如果在 sizeof 操作符的操作数中使用了变长数组类型,在某些情况下,数组大小表达式是否求值是不确定的(见 Rule 13.6)。

可变长度数组类型的每个实例在其生命周期开始时都已确定其大小。这可能会导致一些令人困惑的行为(提前确定好大小就失去了可变长数组的意义,用固定数组就行),例如:

1
2
3
4
5
6
7
8
void f(void)
{
    uint16_t n = 5;
    typedef uint16_t Vector[n]; /* An array type with 5 elements */
    n = 7;
    Vector a1;      /* An array type with 5 elements */
    uint16_t a2[n]; /* An array type with 7 elements */
}
1
uint16_t vla[n]; /* Non-compliant - 如果n为0或负值就是未定义行为 */

组别 19:重叠存储

Rule 19.1

(强制) 不得将对象赋值或复制给与之重叠的对象

这里指的是内存重叠,比如 union 内的对象就会重叠,数组中的某一段和另一段也有可能重叠

当创建的两个对象在内存中有些重叠,其中一个对象被分配或复制到另一个对象时,该行为是未定义的

例外

以下行为是允许的,因为这些行为是明确定义的:

  1. 在两个完全重叠且类型兼容的对象之间赋值(忽略其类型限定符)
  2. 使用标准库函数 memmove 在部分或完全重叠的对象之间复制
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static void fn(void)
{
    union /* breaks R.19.2 */
    {
        int16_t i;
        int32_t j;
    } a = {0};
    a.j = a.i; /* Non Compliant,j和i重叠 */
}

static int16_t arr[20] = {0};

static void f(void)
{
    (void)memcpy(&arr[5], &arr[4], 2u * sizeof(arr[0])); /* Non-compliant,将4和5拷贝到5和6,有重叠 */
}

static void g(void)
{
    int16_t *p = &arr[0];
    int16_t *q = &arr[0];
    *p = *q; /* Compliant - exception 1 */
}

Rule 19.2

(必要) 不得使用 union 关键字

一个 union 成员可以被写入,然后相同的成员可以以一种定义明确的方式被读回。

但是,如果写入一个联合成员,然后读回一个不同的联合成员,其行为取决于成员的相对大小:

  • 如果读取的成员比写入的成员大,那么值就是未指定的;
  • 否则,值就是实现定义的。

《标准》规定,如果 union 中有类型为无符号字符数组的成员,则可通过该成员访问整个 union 的每个字节。但是,由于可以访问未指定值的字节,因此不应使用。

如果不遵守本规则,需要确定的行为类型有:

  • 填充(Padding)–在联合体末尾插入多少填充;
  • 对齐(Alignment)–联合体中任何结构的成员如何对齐;
  • 端位(Endianness)–一个字中高位的字节是存储在最低(大端)还是最高(小端)的内存地址;
  • 位序(Bit-order)–字节中的位如何编号,位如何分配到位域。
1
2
3
4
5
6
7
8
9
10
11
static uint32_t zext(uint16_t s)
{
    union /* Non-compliant */
    {
        uint32_t ul;
        uint16_t us;
    } tmp;

    tmp.us = s;
    return tmp.ul; /* unspecified value */
}

组别 20:预处理指令

Rule 20.1

(建议) #include 指令之前仅允许出现预处理指令或注释

该规则应在预处理之前应用于文件内容。

为了提高代码的可读性,特定代码中的所有 #include 指令应集中在代码顶部附近。

此外,在声明或定义中使用 #include 包含标准头文件,或在相关的标准头文件包含之前使用标准库的一部分会导致未定义行为。

1
2
3
4
5
6
7
static int32_t i = 0;

#include "mc3_header.h" /* Non-compliant */

static int16_t

#include "R_20_01.h" /* Non-compliant */

Rule 20.2

(必要) 头文件名中不得出现“'”、“"”、“\”字符以及“/*”或“//”字符序列

如果出现以下情况,则该行为未定义:

  • 在标头名称预处理标记中的 <> 分隔符之间使用了'"\字符,或是 /*// 字符序列。
  • 在标头名称预处理标记中的"分隔符之间使用了'\字符,或是 /*// 字符序列。

注意:使用 \ 字符会导致未定义的行为,许多实现将接受 / 字符代替它。

1
#include "fi'le.h" /* Non-compliant */

Rule 20.3

(必要) #include 指令后须跟随<filename>"filename"序列

此规则适用于宏程序替换后。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include "filename.h" /* Compliant */
#include <filename.h> /* Compliant */

#include another.h    /* Non-compliant */

#define HEADER "filename.h"
#include HEADER /* Compliant */

#define FILENAME file2.h
#include FILENAME /* Non-compliant */

#define BASE "base"
#define EXT ".ext"
#include BASE EXT          /* Non-compliant - strings are concatenated
                            * after preprocessing */
#include "./include/cpu.h" /* Compliant - filename may include a path */

#include"mc3_header.h"             /* Compliant - space not required */
#include/* a comment */"R_20_03.h" /* Compliant - comment permitted  */

Rule 20.4

(必要) 宏不得与关键字同名

此规则适用于所有关键字,包括那些实现语言扩展的关键字

使用宏来改变关键字的含义可能会引起混淆。如果在宏定义为与关键字同名时包含了标准头文件,那么行为将是未定义的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#define int some_other_type /* Non-compliant - redefined int */
#include <stdlib.h> // 宏与int重名时包含标准头文件是未定义行为

#define while(E) for (; (E);) /* Non-compliant - redefined while */
#define unless(E) if (!(E))   /* Compliant,unless不是关键字 */
#define seq(S1, S2) \
    do              \
    {               \
        S1;         \
        S2;         \
    } while (false) /* Compliant */
#define compound(S) \
    {               \
        S;          \
    } /* Compliant */

#define inline                        /* Non-compliant C99, Compliant C90 */

Rule 20.5

(建议) 不应使用#undef

使用 #undef 可能会使人不清楚在翻译单元中的某一点上存在哪些宏。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#define QUALIFIER volatile

#undef QUALIFIER /* Non-compliant */

#ifdef QUALIFIER
static void f(QUALIFIER int32_t p)
#else
static void f5(int32_t p)
#endif
{
    while (p != 0)
    {
        p = get_int32(); /* Breaks R.17.8 */
    }
}

Rule 20.6

(必要) 看起来像预处理指令的符号不得出现在宏参数内

参数中包含的标记序列被期望作为预处理指令,但却会导致未定义的行为

1
2
3
4
5
6
7
8
9
10
11
12
#define M(A) printf(#A)
#include <stdio.h>
void main(void)
{
    M(
#ifdef SW /* Non-compliant,我们期望该#ifdef指令生效 */
        "Message 1"
#else  /* Non-compliant */
        "Message 2"
#endif /* Non-compliant */
    );
}

打印结果可能为:#ifdef SW "Message 1" #else "Message 2" #endifMessage 1Message 2(取决于 SW 是否定义),或是其他情况。

Rule 20.7

(必要) 宏参数展开产生的表达式应放在括号内

如果任何宏参数的展开产生一个标记或标记序列(形成一个表达式),那么在完全展开的宏中,该表达式应

  • 本身就是一个括号表达式;或
  • 用括号括起来。

注意:这并不一定要求所有宏形参都用括号;在宏实参中提供括号也是可以接受的。

如果不使用括号,那么在发生宏替换时,运算符优先级可能达不到预期效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#define M1(x, y) (x * y) //Non-compliant
r = M1(1 + 2, 3 + 4); // 展开后是r = ((1 + 2 * 3 + 4)),而不是预期的 r = ((1 + 2) * (3 + 4))

#define M1(x, y) ((x) * (y)) /* Compliant,宏的形参带括号 */

r = M1((1 + 2), (3 + 4)); /* Compliant,或使用带括号的实参 */

// 下面的示例符合要求,因为 x 的第一次展开是作为 ## 操作符的操作数,
// 而 ## 操作符并不产生表达式。x 的第二次展开是一个表达式,并按要求加了括号。
#define M3(x) a##x = (x)
int16_t M3(0); // 展开后为 int16_t a0 = (0);

// 下面的示例符合要求,因为将参数 M 作为成员名展开不会产生表达式。
// 对参数 S 的扩展会产生一个表达式,该表达式具有结构或联合类型,需要使用括号
#define GET_MEMBER(S, M) (S).M
v = GET_MEMBER(s1, minval); // (s1).minval;

// 下面这个符合规则的示例表明,并不总是有必要将参数的每个实例都用括号括起来,
// 尽管这通常是符合这一规则的最简单方法。
#define F(X) G(X)
#define G(Y) ((Y) + 1)
int16_t x = F(2)

// 完全展开的宏是 ((2) + 1)。
// 回溯宏扩展,值 2 来自宏 G 中参数 Y 的扩展,而参数 Y 又来自宏 F 中的参数 X。

Rule 20.8

(必要) #if 或#elif 预处理指令的控制表达式的计算结果应为 0 或 1

强类型要求条件包含预处理指令的控制表达式具有 Boolean 类型的值(整型的 0 和 1 名义上也是 Boolean 类型)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#define FALSE 0

#if FALSE /* Compliant     */
#endif

#if 10 /* Non-compliant */
#endif

#if !defined(X) /* Compliant    */
#endif

#define A 10
#define B 5

#if A > B /* Compliant    */
#endif

Rule 20.9

(必要) #if 或#elif 预处理指令的控制表达式中使用的所有标识符应在其评估前被 #define 或类似的方式定义

除了使用 #define 处理器指令外,标识符还可以通过其他实现方式有效地进行 “#define”。例如,有些实现支持:

  • 使用编译器命令行选项,如 -D 命令行参数允许在翻译前定义标识符;
  • 使用环境变量实现相同效果;
  • 编译器提供的预定义标识符

如果试图在预处理器指令中使用宏标识符,而该标识符尚未定义,则预处理器将假定其值为零。这可能不符合开发人员的期望

1
2
3
4
5
6
7
8
9
10
11
12
13
#if M == 0 /* Non-compliant */
           /* Does 'M' expand to zero or is it undefined? */
#endif

#if defined(M) /* Compliant - M is not evaluated,确保M已被定义 */
#if M == 0     /* Compliant - M is known to be defined        */
               /* 'M' must expand to zero.                    */
#endif
#endif

/* Compliant - B is only evaluated in (B == 0) if it is defined */
#if defined(B) && (B == 0)
#endif

Rule 20.10

(建议) 不应使用“#”和“##”预处理运算符

当多个 #、多个 ## 或是 # 和 ## 预处理器运算符混合使用时,相关的评估顺序是未指定的。因此,在某些情况下,无法预测宏扩展的结果。

使用 ## 操作符可能导致代码晦涩难懂。

注意:Rule 1.3 包括以下两种情况下出现的未定义行为:

  • 操作符 ## 的结果不是有效的字符串字面量;或
  • 操作符 ## 的结果不是有效的预处理标记。
1
2
#define A(x) #x          /* Non-compliant */
#define B(x, y) x##y = 0 /* Non-compliant */

Rule 20.11

(必要) 紧跟在“#”运算符之后的宏参数后面不得紧随“##”运算符

与多个 #、多个 ## 或 # 和 ## 混合预处理器运算符相关的运算顺序未作规定。Rule 20.10 不鼓励使用 # 和 ##。特别是 # 操作符的结果是一个字符串字面量,将其粘贴到任何其他预处理标记符上都不太可能得到一个有效的标记符。

1
2
3
#define A(x) #x       /* Compliant */
#define B(x, y) x##y  /* Compliant */
#define C(x, y) #x##y /* Non-compliant */

Rule 20.12

(必要) 用作“#”或“##”运算符的操作数的宏参数,不得是本身需要进一步宏替换的操作数

用作 # 或 ## 运算符操作数的宏参数不会展开,如果程序员希望这种情况下宏参数需要展开,就产生了误解。

1
2
3
4
5
6
7
8
9
10
11
12
13
#define AA 0xffff
#define BB(x) (x) + wow##x /* Non-compliant   */

static void f12(void)
{
    int32_t wowAA = 0;

    /* Expands as wowAA = ( 0xffff ) + wowAA; */
    // 结果就是wow##x替换时没有将AA视为另一个宏而展开,而是作为了字符串直接进行了拼接
    wowAA = BB(AA);
}

#define SCALE(X) ((X) * X##_scale) /* Compliant */

Rule 20.13

(必要) 以“#”作为第一个字符的一行代码应为有效的预处理指令

# 和预处理标记之间允许有空格

预处理器指令(如#if,#ifdef,#ifndef)可用于有条件地排除源代码,直到遇到相应的 #else、#elif 或 #endif 指令。被排除的源代码中包含的畸形或无效的预处理指令可能无法被编译器检测到,从而可能导致被排除或被启用的代码比预期的要多。

要求所有预处理器指令在语法上有效,即使它们出现在排除的代码块中,也能确保这种情况不会发生

1
2
3
4
5
6
7
8
9
10
11
12
13
/*
#start is not a token in a comment
在注释中的句子用#开头符合规则
*/

#define AAA 2

#ifndef AAA
x = 1;
#else1            /* Non-compliant,错误的#else1无法被编译器检测,
                   * 导致#ifndef到#endif之间的整段代码都被忽略了 */
x = AAA;
#endif

Rule 20.14

(必要) 所有#else,#elif 和#endif 预处理程序指令都应和与其相关的#if,#ifdef 或#ifndef 指令位于同一文件中

通过使用分布在多个文件中的条件编译指令来包含或排除代码块时,可能会产生混淆。要求 #if 指令在同一区段内终止,可减少代码的视觉复杂性,并降低维护过程中出错的几率。

注意:#if 指令可以在包含的文件中使用,但必须在同一文件中终止。

1
2
3
4
5
6
7
8
9
10
11
12
/* R_20_14_2.h */
#endif

/* main.c */
#define A

#ifdef A /* Compliant  */
#include "R_20_14_1.h"
#endif

#if 1 /* Non-compliant */
#include "R_20_14_2.h" // 不能依赖于该包含文件中的#endif终止本文件中的#if

组别 21:标准库

Rule 21.1

(必要) 不得将#define 和#undef 用于保留的标识符或保留的宏名称

本规则适用于以下情况:

  • 以下划线开头的标识符或宏名;
  • 《标准》第 7 节 “库” 所述文件范围中的标识符;
  • 《标准》第 7 节 “库” 所述在标准头中定义的宏名。

本规则还禁止在已定义的标识符上使用 #define 或 #undef,因为这会导致明确的未定义行为。

本规则不包括适用的 C 语言标准中题为 “未来库方向(Future Library Directions)” 一节中描述的标识符或宏名。

《标准》规定,当定义的宏与以下内容同名时,只要未包含这些内容对应的头文件,依然被视为正确定义:

  • 标准头文件中已定义的宏;或者
  • 在标准头文件中声明了具有文件作用域的标识符。

此规则不允许这样的定义,因为这并不好把握,后面可能会添加这个头文件,这样就引起了混淆。

注意:宏 NDEBUG 没有在标准头中定义,因此可以被 #define。

1
2
3
4
5
6
7
8
9
#undef __LINE__      /* Non-compliant, also breaks R.20.5 */
#define _GUARD_H 1   /* Non-compliant                     */
#undef _BUILTIN_sqrt /* Non-compliant, also breaks R.20.5 */

#define defined        /* Non-compliant - reserved identifier  */
#define errno my_errno /* Non-compliant - library identifier   */

/* Compliant - isneg位于Future Library Directions中,不适用本规则 */
#define isneg(x) ((x) < 0) /* breaks D.4.9 */

Rule 21.2

(必要) 不得声明保留的标识符或宏名称

如果保留标识符被重复使用,程序可能会出现未定义的行为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/** math.h **/
extern double sqrt(double x); // sqrt函数
#define sqrt(x) (_BUILTIN_sqrt(x)) // 为了高效,math还会提供一个同名宏实现内联函数

/** mainc.c **/
#include <stddef.h>
// _BUILTIN_sqrt标识符与math库中的宏重复了,
// 虽然math库中的宏没有明确对外公开,但sqrt展开后会有这个宏,
// 这就导致了未定义行为
static float64_t _BUILTIN_sqrt(float64_t x) /* Non-compliant, also breaks R.20.1  */
{
    return x * x;
}

#include <math.h>
/* break R.20.1 */

// memcpy已经包含在<string.h>中
extern void *memcpy(void *restrict s1,
                    const void *restrict s2, /* also breaks R.8.14   */
                    size_t n);               /* Non-compliant        */

Rule 21.3

(必要) 不得使用<stdlib.h>中的内存分配和释放函数

不得使用 calloc、malloc、realloc 和 free 标识,也不得扩展具有这些名称的宏

使用标准库提供的动态内存分配和取消分配例程可能导致未定义的行为,例如

  • 随后释放未动态分配的内存;
  • 以任何方式使用已释放内存的指针;
  • 先访问已分配的内存,然后再将值存储到内存中。

注意:本规则是 Dir 4.12 的一个具体实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdlib.h>

void R_21_3(void)
{
    char_t *p;

    p = (char_t *)malloc(11U); /* Non-compliant: use of malloc  */

    if (p != NULL)
    {
        (void)realloc(p, 20U); /* Non-compliant: use of realloc */
    }

    free(p); /* Non-compliant: use of free    */

    p = (char_t *)calloc(10, sizeof(char_t)); /* Non-compliant: use of calloc */

    free(p); /* Non-compliant: use of free    */
}

Rule 21.4

(必要) 不得使用标准头文件<setjmp.h>

setjmp 和 longjmp 允许绕过正常的函数调用机制。使用它们可能会导致未定义和未指定的行为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <setjmp.h> /* Non-compliant */

static jmp_buf myenv;

static void jmpfunc(int8_t p)
{
    if (p == 10)
    {
        longjmp(myenv, 9); /* Non-compliant, also breaks R.15.5 */
    }

    use_int8(p);
}

void R_21_4(void)
{
    int16_t istat = 0;

    if (setjmp(myenv) != 0) /* Non-compliant */
    {
        jmpfunc(10);
    }

    use_int16(istat);
}

Rule 21.5

(必要) 不得使用标准头文件<signal.h>

信号处理包含实现定义和未定义的行为

Rule 21.6

(必要) 不得使用标准库输入/输出函数

本规则适用于由 <stdio.h> 提供的函数,以及在 C99 中,在 C99 标准第 7.24.2 和 7.24.3 节中指定由 <wchar.h> 提供的宽字符等价函数。

不得使用这些标识符,也不得扩展具有这些名称的宏。

流和 I/O 有未指定、未定义和实现定义的相关行为

Rule 21.7

(必要) 不得使用<stdlib.h>中的 atof、atoi、atol 和 atoll 函数

当字符串无法转换时,这些函数具有未定义的相关行为

1
2
3
4
5
6
7
8
9
10
#include <stdlib.h>

void R_21_7(void)
{
    float64_t a_to_float_result;

    a_to_float_result = atof("123.5"); /* Non_compliant, also breaks R.22.8, R.22.9 */

    use_float64(a_to_float_result);
}

Rule 21.8

(必要) 不得使用<stdlib.h>中的 abort, exit, getenv 和 system 函数

这些函数具有与其相关的未定义和实现定义的行为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <stdlib.h>

void R_21_8(void)
{
    int32_t status = 0;
    char_t *env;

    env = getenv("path"); /* Non-compliant  */

    if (env != NULL)
    {
        status = system(env);
        /* Non-compliant */
        use_char_ptr(env);
    }

    if (status == 0)
    {
        abort(); /* Non-compliant  */
    }

    if (status == 1)
    {
        exit(status); /* Non-compliant */
    }
}

Rule 21.9

(必要) 不得使用<stdlib.h>中的 bsearch 和 qsort 函数

如果比较函数在比较元素时的行为不一致,或者修改了任何元素,则其行为是未定义的。

注意:当比较函数返回 0 时,会出现未指定的行为,可以通过确保比较函数永远不返回 0 来避免。当两个元素在所有方面都相等时,比较函数可以返回一个值,表示它们在排序前数组中的相对顺序。

qsort 的实现很可能是递归的,因此会对栈资源产生未知的需求。在嵌入式系统中,这一点值得关注,因为栈的大小可能是固定的,通常很小

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <stdlib.h>

#define COUNT 10U

extern int32_t compare(const void *e1, const void *e2);

void R_21_9(void)
{
    int32_t array[COUNT] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    const int32_t key = 5;

    int32_t *result = bsearch(&key,
                              &array[0],
                              (size_t)COUNT,
                              sizeof(key),
                              compare); /* Non-compliant - also breaks R.11.5 */
    if (result != NULL)
    {
        use_int32(*result);
    }

    qsort(&array[0], (size_t)COUNT, sizeof(key), compare); /* Non-compliant */

    use_int32(array[0]);
}

Rule 21.10

(必要) 不得使用标准库时间和日期功能

不得使用 <time.h> 指定提供的任何功能。在 C99 中,不应使用标识符 wcsftime,也不应扩展该名称的宏

时间和日期函数具有未指定、未定义和实现定义的相关行为

1
2
3
4
5
6
7
8
9
10
11
12
#include <time.h> /* Non-compliant */

void R_21_10(void)
{
    float64_t time_dif;
    time_t time_1;
    time_t time_2;

    time_1 = clock();                    /* Non-compliant */
    time_2 = clock();                    /* Non-compliant */
    time_dif = difftime(time_1, time_2); /* Non-compliant */
}

Rule 21.11

(必要) 不得使用标准头文件<tgmath.h>

使用 <tgmath.h> 的功能可能会导致未定义的行为

1
2
3
4
5
6
7
8
9
10
11
#include <tgmath.h> /* Non-compliant */

void R_21_11(void)
{
    float32_t f1;
    float32_t f2 = get_float32();

    f1 = sqrt(f2); /* Non-compliant - generic sqrt used      */

    f1 = sqrtf(f2); /* Compliant - float version of sqrt used  */
}

Rule 21.12

(建议) 不得使用<fenv.h>的异常处理功能

不应使用和扩展标识符 feclearexcept、fegetexceptflag、feraiseexcept、fesetexceptflag 和 fetestexcept,也不应扩展任何具有这些名称之一的宏。

不应使用宏 FE_INEXACT、FE_DIVBYZERO、FE_UNDERFLOW、FE_OVERFLOW、FE_INVALID 和 FE_ALL_EXCEPT 以及任何实现定义的浮点异常宏。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <fenv.h>

static void f(float32_t x, float32_t y)
{
    float32_t z;

    (void)feclearexcept(FE_DIVBYZERO); /* Non-compliant */

    z = x / y;

    if (fetestexcept(FE_DIVBYZERO) != 0) /* Non-compliant */
    {
    }
    else
    {
#pragma STDC FENV_ACCESS on
        z = x * y;
    }

    if (z > x)
    {
#pragma STDC FENV_ACCESS off
        if (fetestexcept(FE_OVERFLOW) != 0) /* Non-compliant */
        {
        }
    }
}

组别 22:资源

Rule 22.1

(必要) 通过标准库功能动态获取的所有资源均应明确释放

分配资源的标准库函数有 malloc、calloc、realloc 和 fopen。

如果没有明确释放资源,就有可能因资源耗尽而发生故障。尽快释放资源可降低资源耗尽的可能性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <stdlib.h>
#include <stdio.h>

static int32_t R_22_1_heap(void)
{
    void *b = malloc(40);

    use_void_ptr(b);

    /* Non-compliant - dynamic memory not released */
    return 1;
}

static int32_t R_22_1_file1(void)
{
    int32_t value;

    FILE *fp1 = fopen("tmp", "r");

    (void)fscanf(fp1, "%d", &value);

    /* Non-compliant - file not closed */
    return value;
}

static int32_t R_22_1_file2(void)
{
    FILE *fp;
    fp = fopen("tmp-1", "w");

    (void)fprintf(fp, "*");
    /* File "tmp-1" should be closed here, but stream 'leaks'. */

    fp = fopen("tmp-2", "w"); /*Non-compliant */

    (void)fprintf(fp, "!");

    (void)fclose(fp);

    return (0);
}

Rule 22.2

(强制) 只有通过标准库函数分配的内存块才能释放

标准库中分配内存的函数有 malloc、calloc 和 realloc。

当内存块的地址被传递给 free 时,该内存块被释放;当内存块的地址被传递给 realloc 时,该内存块可能被释放。内存块一旦被释放,就不再被视为已分配的内存块,因此不能再被释放。

释放未分配内存或多次释放同一分配内存会导致未定义行为。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <stdlib.h>

static void fn(void)
{
    int32_t a;

    free(&a); /* Non-compliant - a does not point to allocated storage    */
}

static void g(void)
{
    char *p = (char *)malloc(512);
    char *q = p;

    if (p != NULL)
    {
        *p = 'A';
        use_char(*p);
        use_char(*q);
    }

    free(p);
    free(q); /* Non-compliant: allocated block freed a second time       */

    p = (char *)realloc(p, 1024); /* Non-compliant: allocated block may be freed a third time */

    free(p);
}

Rule 22.3

(必要) 不得在不同的数据流上同时打开同一文件以进行读写访问

《标准》没有规定通过不同数据流写入和读取文件时的行为。

注意:可以多次打开一个文件进行只读访问

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>

static void fn3(void)
{
    int32_t value1;
    int32_t value2;

    FILE *fw = fopen("tmp3", "r+");
    FILE *fr = fopen("tmp3", "r"); /* Non_compliant */

    (void)fscanf(fw, "%d", &value1);
    (void)fscanf(fw, "%d", &value2);

    use_int32(value1 + value2);

    (void)fclose(fw);
    (void)fclose(fr);
}

Rule 22.4

(强制) 禁止尝试对以只读方式打开的流执行写操作

《标准》没有说明试图写入只读数据流时的行为。因此,写入只读数据流被认为是不安全的。

1
2
3
4
5
6
7
#include <stdio.h>
void fn(void)
{
    FILE *fp = fopen("tmp", "r");
    (void)fprintf(fp, "What happens now?"); /* Non-compliant */
    (void)fclose(fp);
}

Rule 22.5

(强制) 禁止解引用指向 FILE 对象的指针

包括直接解引用和间接解引用(memcpy or memcmp)

《标准》(C90 第 7.9.3(6)节,C99 第 7.19.3(6)节)规定,用于控制流的 FILE 对象的地址可能也是有意义的,该对象的副本(至少地址和原对象不同了)可能不会产生相同的行为(作为流的行为)。本规则可确保不会出现这种复制。

禁止直接操作 FILE 对象,因为这可能与其作为流指定符的用途不符。

1
2
3
4
5
6
7
8
FILE *pf1 = tmpfile();
FILE *pf2;
FILE f3;

pf2 = pf1; /* Compliant */
f3 = *pf2; /* Non-compliant */

(void)fclose(pf2);

Rule 22.6

(强制) 关联的流关闭后,禁止再使用指向 FILE 的指针值

《标准》规定,在对数据流进行关闭操作后,FILE 指针的值是不确定的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>

static void fn6(void)
{
    FILE *fp;
    fp = fopen("tmp", "w");
    if (fp == NULL) /* Compliant */
    {
        error_action();
    }
    else
    {
        void *p;
        (void)fclose(fp);
        (void)fprintf(fp, "?"); /* Non-compliant */
        p = fp; /* Non-compliant */
        use_void_ptr(p);
    }
}
本文由作者按照 CC BY 4.0 进行授权

© Kai. 保留部分权利。

浙ICP备20006745号-2,本站由 Jekyll 生成,采用 Chirpy 主题。