MISRA C:2012 详解 · 专栏01:语言基础(第 1~5 章)
MISRA C:2012 详解 · 专栏01语言基础第 1~5 章覆盖第 1~5 章共23 条规则标准环境、未使用代码、注释、字符集、标识符。这一篇讲的是 C 语言「语法层」的规矩——大多属于「地基」问题违反后轻则编译器告警重则链接期/运行期出诡异 bug。第 1 章 · 标准 C 环境A Standard C Environment这一章只干一件事让代码活在「标准 C」的世界里别碰编译器私货和未定义行为。Rule 1.1 不得违反标准 C 语法和约束且不得超出翻译限制等级必需Required 可判定Decidable cppcheck 工具可检编译器必报规则程序必须符合标准 C 的语法和约束并且不超出实现规定的翻译限制如标识符长度、嵌套层数、case 数量等。为什么约束违规在标准里属于「必须诊断」的范畴但某些编译器只警告不报错或者干脆扩展放行——这正是隐患的温床。❌ 违规intarr[3]{1,2,3,4};/* ✗ 初始化项超过数组大小违反 C 约束 */int(*func)(void)NULL;func3.14;/* ✗ 类型不兼容的赋值违反约束 */✅ 合规intarr[4]{1,2,3,4};/* ✓ 初始化项与数组大小一致 */Rule 1.2 不应使用语言扩展等级建议Advisory 可判定Decidable cppcheck 工具可检规则不要使用编译器私有的语言扩展如 GNU 的typeof、嵌套函数、__attribute__。为什么扩展一旦用了代码就被某个编译器「绑架」了换平台、换编译器、换架构都会翻车。❌ 违规typeof(x)yx;/* ✗ GNU 扩展typeof */intouter(void){intinner(void){return1;}/* ✗ GNU 扩展嵌套函数 */returninner();}✅ 合规intyx;/* ✓ 用标准 C 语法 */staticintinner(void){return1;}intouter(void){returninner();}/* ✓ 平铺成普通函数 */Rule 1.3 不得出现未定义行为或关键的未指定行为等级必需Required 可判定Undecidable cppcheck 需人工审查规则代码不得触发未定义行为UB或「关键的」未指定行为。为什么UB 是 C 语言最大的坑——一旦触发编译器可以做任何事包括「看起来正常」。这类问题往往只在特定优化等级、特定平台爆发。❌ 违规inta[5];a[5]0;/* ✗ 数组越界未定义行为 */intbINT_MAX1;/* ✗ 有符号整数溢出未定义行为 */inti0;ii1;/* ✗ 同一表达式内读写且无顺序点未定义行为 */✅ 合规inta[5];if(index0index5){a[index]0;/* ✓ 先做边界检查 */}longb(long)INT_MAX1L;/* ✓ 用更宽的类型避免溢出 */第 2 章 · 未使用代码Unused Code这一章的潜台词是代码不是「越全越好」而是「越少越稳」。每一行死代码都是未来的阅读负担和维护陷阱。Rule 2.1 项目不得包含不可达代码等级必需Required 可判定Decidable cppcheck 工具可检规则不能存在永远执行不到的代码unreachable code。为什么不可达代码要么是笔误漏了break/写错条件要么是历史遗留都会误导后人。❌ 违规voidf(intflag){if(flag0){return;}else{return;}/* ✗ 下面这行永远执行不到 */process_data();}✅ 合规voidf(intflag){if(flag0){return;}process_data();/* ✓ 只有一条返回路径代码可达 */}Rule 2.2 不得存在死代码等级必需Required 可判定Decidable cppcheck 工具可检规则不能存在「执行了但对程序无任何影响」的死代码dead code例如从不被调用的函数、赋值后从不读取的变量。为什么死代码会让覆盖率「假达标」也容易藏着本应删除的逻辑。❌ 违规staticintlegacy_calc(void)/* ✗ 死代码整个工程无人调用 */{return42;}intmain(void){intunused10;/* ✗ 赋值后从不读取 */unused20;return0;}✅ 合规intmain(void){return0;/* ✓ 移除死代码 */}Rule 2.3 项目不应包含未使用的类型声明等级建议Advisory 可判定Decidable cppcheck 工具可检规则不要保留从未被使用的typedef类型声明。❌ 违规typedefintlegacy_speed_t;/* ✗ 无人使用 */typedefunsignedintuint32_t_impl;✅ 合规/* ✓ 删除未使用的 typedef */Rule 2.4 项目不应包含未使用的标签tag声明等级建议Advisory 可判定Decidable cppcheck 工具可检规则不要保留从未被使用的struct/union/enum标签声明。❌ 违规structlegacy_config/* ✗ tag 从未被使用 */{intmagic;intversion;};✅ 合规/* ✓ 删除未使用的 struct 声明 */Rule 2.5 项目不应包含未使用的宏声明等级建议Advisory 可判定Decidable cppcheck 工具可检规则不要保留从未被使用的宏。❌ 违规#defineLEGACY_TIMEOUT_MS500/* ✗ 从未被引用 */#defineMAX_CHANNELS8✅ 合规#defineMAX_CHANNELS8/* ✓ 只保留真正用到的 */Rule 2.6 函数不应包含未使用的标签label声明等级建议Advisory 可判定Decidable cppcheck 工具可检规则函数内不要保留从未被goto引用的标签。❌ 违规voidf(void){intx0;cleanup:/* ✗ 没有任何 goto 指向它 */xx1;}✅ 合规voidf(void){intx0;xx1;/* ✓ 移除无用标签 */}Rule 2.7 函数不应有未使用的形参等级建议Advisory 可判定Decidable cppcheck 工具可检规则函数形参如果没用到应当移除或通过「未使用」机制显式标注。为什么未使用形参往往是接口设计过度的信号也可能是「本该处理却没处理」的漏网之鱼。❌ 违规voidhandle_event(intevent_id,intpayload)/* ✗ payload 未使用 */{process_event(event_id);}✅ 合规voidhandle_event(intevent_id)/* ✓ 移除无用形参 */{process_event(event_id);}特殊情况回调函数被函数指针约束、必须保留某个签名时可用(void)param;显式标记「此参数有意不使用」这是被接受的偏离写法。第 3 章 · 注释Comments注释是写给「三个月后的自己」看的。但注释本身也可能引发词法歧义——这一章就是防这个的。Rule 3.1 注释内不得使用/*或//字符序列等级必需Required 可判定Decidable cppcheck 工具可检规则注释内容里不能再出现/*或//字符序列嵌套或误触发注释定界。为什么C 的注释不支持嵌套块注释里出现/*或行注释符会让人误读注释边界甚至导致代码被意外「注释掉」。❌ 违规/* 这里的写法 /* 有问题 */会让人困惑*/// 单行注释里不要写 // 这种样子✅ 合规/* 这里说明块注释不要嵌套用自然语言描述即可 */// 单行注释里避免重复使用斜杠符号Rule 3.2 行注释中不得使用换行拼接line-splicing等级必需Required 可判定Decidable cppcheck 工具可检规则//行注释里不能用「行尾反斜杠」把下一行拼接进来。为什么反斜杠换行会让//注释「吞掉」下一行真正的代码属于经典事故现场。❌ 违规// 这是一行注释结尾有个反斜杠 \ int x 5; /* ✗ 这行被拼接进注释代码消失了 */✅ 合规// 这是一行注释正常结束intx5;/* ✓ 代码可见 */第 4 章 · 字符集与词法约定Character Sets and Lexical Conventions这一章很短但每条都针对「长得像、其实不是」的词法陷阱。Rule 4.1 八进制和十六进制转义序列必须被终止等级必需Required 可判定Decidable cppcheck 工具可检规则\x、\0这类转义序列必须「终止」即后面不能紧跟着更多的八进制/十六进制数字。为什么\x41B会被解析成一个字符值 0x41B而不是\x41B。转义序列「贪吃」是新手高频踩坑点。❌ 违规constchar*s\x41B;/* ✗ 被解析成单个字符 0x41B */✅ 合规constchar*s\x41B;/* ✓ 用字符串拼接明确分隔 */Rule 4.2 不应使用三元组trigraph等级建议Advisory 可判定Decidable cppcheck 工具可检规则不要使用??、??(这类三元组字符序列。为什么三元组是 C 为了兼容「没有某些字符的键盘」而发明的替换机制但现代代码里出现??!几乎都是无意的且会静默改变语义。❌ 违规constchar*sWhat??!;/* ✗ ??! 会被替换成 |语义变了 */✅ 合规constchar*sWhat?!;/* ✓ 显式拆开避免三元组 */第 5 章 · 标识符Identifiers命名是程序员最频繁的决策也是最容易「撞车」的地方。这一章的核心就俩字唯一。Rule 5.1 外部标识符必须相互区别等级必需Required 可判定Decidable cppcheck 工具可检规则具有外部链接的标识符必须相互区别在 C99 的前 31 个有效字符内、以及兼容 C90 的前 6 个字符大小写不敏感范围内均不冲突。为什么某些链接器/老标准只关心标识符前几个字符或忽略大小写两个「看起来不同」的名字可能被当成同一个。❌ 违规externintSensorValue;/* ✗ 前 6 字符 Sensor 大小写不敏感 */externintsensorvalue;/* ✗ 与上面冲突C90 链接器不区分大小写 */✅ 合规externintsensor_value;/* ✓ 前 6 字符 sensor vs tempe 已区别 */externinttemperature_value;Rule 5.2 同一作用域和命名空间内声明的标识符必须相互区别等级必需Required 可判定Decidable cppcheck 工具可检规则同一作用域、同一命名空间内标识符在有效字符范围内必须唯一超出有效长度的差异不可靠。为什么C 标准只保证前 N 个字符有意义超出部分不同仍可能被编译器视为「同名」。❌ 违规staticinttotal_samples_recorded;/* ✗ 前 31 字符相同 */staticinttotal_samples_recording;/* ✗ 仅第 32 字符不同可能被截断 */✅ 合规staticinttotal_samples_recorded;/* ✓ 差异落在有效字符范围内 */staticinttotal_errors_recorded;Rule 5.3 内层作用域的标识符不得遮蔽外层作用域的标识符等级必需Required 可判定Decidable cppcheck 工具可检规则内层块声明的名字不能「遮蔽」hide外层同名标识符。为什么遮蔽会让读代码的人包括你自己误以为操作的是外层变量实际却是内层的——静默的语义陷阱。❌ 违规int16_tvalue10;voidf(void){int16_tvalue20;/* ✗ 遮蔽了外层的 value */value5;/* 改的是内层外层纹丝不动 */}✅ 合规int16_tvalue10;voidf(void){int16_tlocal_value20;/* ✓ 换一个不冲突的名字 */local_value5;}Rule 5.4 宏标识符必须相互区别等级必需Required 可判定Decidable cppcheck 工具可检规则宏名之间不能重复定义同名宏定义不同内容。为什么宏重定义会静默覆盖之前的定义谁先谁后全靠#include顺序极易埋雷。❌ 违规#defineMAX_SIZE1024#defineMAX_SIZE2048/* ✗ 宏重定义 */✅ 合规#defineMAX_SIZE1024#defineMIN_SIZE256/* ✓ 名字唯一 */Rule 5.5 标识符必须与宏名相区别等级必需Required 可判定Decidable cppcheck 工具可检规则变量、函数、typedef、tag 等标识符不能与宏同名反之亦然。为什么宏是文本替换函数/变量与宏同名会被意外替换产生「编译通过、逻辑全错」的诡异结果。❌ 违规#definemin(a,b)((a)(b)?(a):(b))intmin(inta,intb)/* ✗ 函数名被宏替换声明被破坏 */{return(ab)?a:b;}✅ 合规#defineMIN(a,b)((a)(b)?(a):(b))/* ✓ 宏用大写区分 */intmin(inta,intb)/* ✓ 函数名与宏不冲突 */{return(ab)?a:b;}Rule 5.6 typedef 名必须是唯一标识符等级必需Required 可判定Decidable cppcheck 工具可检规则typedef名字不能与其他 typedef 名或标识符重复。❌ 违规typedefint16_tspeed_t;typedefint32_tspeed_t;/* ✗ typedef 重名 */✅ 合规typedefint16_tspeed_t;typedefint32_tdistance_t;/* ✓ 各自唯一 */Rule 5.7 标签tag名必须是唯一标识符等级必需Required 可判定Decidable cppcheck 工具可检规则struct/union/enum的标签名必须唯一。❌ 违规structpoint{int16_tx;int16_ty;};structpoint{int16_tz;};/* ✗ tag 名重复 */✅ 合规structpoint2d{int16_tx;int16_ty;};structpoint3d{int16_tx;int16_ty;int16_tz;};/* ✓ tag 唯一 */Rule 5.8 具有外部链接的对象或函数的标识符必须唯一等级必需Required 可判定Decidable cppcheck 工具可检规则非static的全局函数/对象在整个项目里名字必须唯一跨文件也不能重名。为什么外部符号重名要么链接报错要么更糟链接器静默选了其中一个行为随链接顺序而变。❌ 违规/* file_a.c */voidinit(void){/* ... */}/* ✗ 外部函数 *//* file_b.c */voidinit(void){/* ... */}/* ✗ 与 file_a.c 重名 */✅ 合规/* file_a.c */voidinit_sensor(void){/* ... */}/* ✓ 命名带上模块前缀 *//* file_b.c */voidinit_motor(void){/* ... */}Rule 5.9 具有内部链接的对象或函数的标识符应唯一等级建议Advisory 可判定Decidable cppcheck 工具可检规则static的内部符号也建议在整个项目里保持唯一即使 C 允许不同文件用同名static。为什么static重名虽合法但会让调试、检索、代码阅读都产生歧义——两个static int counter到底改的是哪个❌ 违规/* file_a.c */staticintcounter;/* ✗ 与其他文件的 static counter 同名 *//* file_b.c */staticintcounter;/* ✗ 同名易混淆 */✅ 合规/* file_a.c */staticintsensor_counter;/* ✓ 用前缀区分 *//* file_b.c */staticintmotor_counter;本篇小结第 1~5 章是 MISRA 的「地基规则」写标准 C、删死代码、注释干净、命名唯一。它们大多属于 Decidablecppcheck 能自动帮你盯住建议在 CI 里第一批启用。一句话总结这一篇别用私货别留尸体别起重名。地基打稳了后面类型系统、指针那些「高层建筑」才不会塌。下一篇专栏02类型系统第 6~10 章开始进入 MISRA 最「烧脑」的部分——基本类型模型。