4. 注释与文档:规则 3.1-3.2(注释使用规范)、规则 4.1-4.2(字符集与三字符组)

好,咱们今天聊聊注释和字符集。说实话,这两块内容在很多人眼里属于「软规范」——觉得只要代码能跑,注释写不写无所谓,字符集用啥都一样。但我得说,这种想法在嵌入式开发里是要吃大亏的。

我见过太多因为注释误导导致排查三天三夜的案例,也见过因为三字符组引发的编译错误,愣是让一个团队折腾了一周。嗯,咱们今天就把这些坑一个个填上。

4.1 注释使用规范(规则 3.1 - 3.2)

MISRA C 对注释的要求,说白了就两条核心原则:注释不能改变代码行为,注释不能嵌套。听起来简单?但实际踩坑的人可不少。

4.1.1 规则 3.1:注释不能嵌套

这条规则的意思是:/* ... */ 风格的注释内部不能再出现 /*。你想想看,C 语言的注释解析是从 /* 开始,到第一个 */ 结束。如果嵌套了,编译器会把内层的 */ 当作外层注释的结束符,后面的代码就全暴露出来了。

⚠ 危险示例:嵌套注释
/* 外层注释开始
    /* 内层注释 */
    这里本应是注释内容,但编译器认为注释已结束 */
int x = 0;  // 这行代码被意外注释掉了?不,它会被编译!

我曾经在一个项目里遇到过这种情况:同事为了临时屏蔽一大段代码,用 /* */ 把整段包起来,结果那段代码里本身就含有注释块。编译报错的时候,他死活找不到原因——因为报错位置和实际问题的位置差了 200 多行。最后一行一行排查,才发现是注释嵌套惹的祸。

💡 我的建议:
  • 屏蔽代码用 #if 0 ... #endif,别用注释
  • 单行注释统一用 //(C99 及以上支持)
  • 多行注释用 /* */,但内部绝对不能再出现 /*

4.1.2 规则 3.2:注释不能改变代码行为

这条规则针对的是「注释符被用作代码的一部分」的情况。最典型的例子就是三字符组(咱们下一节细讲),但还有一种更隐蔽的情况——注释符出现在字符串或字符常量里。

// 错误示例:注释符出现在字符串中
const char* str = "这是一个注释符号:*/";  // 有些编译器会误解析

// 正确做法:用转义序列
const char* str = "这是一个注释符号:*\/";  // 明确告诉编译器这不是注释结束

我个人习惯是:永远不要在字符串里直接写 /* 或 */。如果确实需要,用转义或者拼接的方式。你可能会觉得「编译器没那么傻吧?」——但 MISRA 的要求是,不依赖编译器的「聪明」,只依赖规范的确定性。

4.2 字符集与三字符组(规则 4.1 - 4.2)

这部分内容,说实在的,很多写了十年 C 的人都不一定完全清楚。但它在嵌入式领域特别重要——尤其是当你需要跨平台、跨编译器的时候。

4.2.1 规则 4.1:字符集使用限制

MISRA C 要求源代码只能使用基本字符集和执行字符集的子集。基本字符集包括:

类别 包含字符
大写字母 A - Z
小写字母 a - z
数字 0 - 9
图形字符 ! " # % & ' ( ) * + , - . / : ; < = > ? [ \ ] ^ _ { | } ~
空白字符 空格、水平制表符、垂直制表符、换页符

为什么要限制这个?因为不同编译器、不同平台对扩展字符(比如 €、中文、日文)的处理方式不一样。你在 Windows 上用 MSVC 编译好好的,换到 GCC 交叉编译工具链,可能就乱码了。

🔑 关键原则: 字符串字面量和字符常量中,只能使用基本字符集中的字符。如果需要特殊符号,用转义序列(如 \x41 表示 'A')。

我记得有一次,一个合作伙伴的代码里用了全角空格(U+3000)作为缩进——肉眼根本看不出来。结果代码在 Linux 下编译报错,报错信息指向一个完全正常的行。排查了整整半天,最后用 hexdump 才发现问题。从那以后,我要求团队所有成员在编辑器里开启「显示空白字符」功能。

4.2.2 规则 4.2:禁止使用三字符组

三字符组(Trigraph Sequences)是 C 标准为了照顾某些缺少特定字符的键盘而设计的替代方案。每个三字符组以 ?? 开头,后面跟一个字符:

三字符组 替代字符
??= #
??( [
??) ]
??< {
??> }
??/ \
??' ^
??! |
??- ~

你可能会问:「这玩意儿现在还有人用吗?」答案是:基本没有。但问题是,有些编译器默认开启三字符组支持,而有些则默认关闭。这就导致了可移植性问题。

⚠ 经典踩坑案例:

我曾经接手过一个项目,代码里有这么一行:

printf("What??!\n");

程序员的本意是输出 What??!。但编译器把 ??! 解释为三字符组 |,实际输出变成了 What|。更坑的是,这个 bug 在开发环境(GCC 默认关闭三字符组)从未出现,一部署到客户的生产环境(某老牌编译器,默认开启)就出问题。

💡 避坑指南:
  • 在编译器选项中显式禁用三字符组:GCC 用 -trigraphs 关闭,MSVC 用 /Zc:trigraphs-
  • 代码审查时,搜索 ?? 模式,确保没有意外使用三字符组
  • 如果确实需要连续两个问号,用转义或字符串拼接:"What?" "?!"

4.3 本章知识体系

下面这张图把注释和字符集的核心规则串起来了。你可以把它当作一个快速检查清单:

注释与字符集规则知识体系 注释与字符集规则知识体系 注释规范(规则 3.1 - 3.2) 字符集规范(规则 4.1 - 4.2) 规则 3.1 禁止注释嵌套 规则 3.2 注释不改变行为 规则 4.1 限制字符集使用 规则 4.2 禁止三字符组 实践建议 屏蔽代码用 #if 0 不用注释块嵌套 字符串中避免 ?? 用转义或拼接 编译器显式禁用 三字符组支持 核心原则 注释要清晰、准确、不改变行为;字符集要可移植、不依赖编译器扩展

4.4 本章小结

注释和字符集,看起来是「小问题」,但往往是最容易忽略、最难排查的。我个人的经验是:把规范落实到工具链里——用静态分析工具自动检查注释嵌套和三字符组,比靠人工审查靠谱得多。

最后送你一句话:好的注释不是解释「代码做了什么」,而是解释「代码为什么这么做」。至于字符集——保持简单,只用基本字符集,你的代码就能在任何一个编译器上安安静静地编译通过。