注释是编译器会丢弃的文本。它纯粹是写给以后读代码的人看的,而这个人通常就是你自己。C 语言提供了两种形式,弄清楚什么时候该用哪一种,大约只需要两分钟。
两种形式
运行一下:输出只有一行。两条注释在编译器解析程序之前就被删掉了:它们在运行时没有任何开销,也不会给可执行文件增加任何内容。
// 一直延续到这一物理行的末尾。同一行中它后面不能再写任何代码,所以下面这行的效果和看上去不一样:
int x = 5; // set x to five int y = 6; /* y is never declared */
/* ... */ 在第一个 */ 处结束,无论它在哪里。它可以在行中间开始和结束,偶尔会派上用场:
int total = price /* before tax */ + shipping;
为什么会有两种风格
/* */ 是 1972 年最初的 C 语言就有的。// 来自 C++,直到 C99 才正式加入 C 语言。这段历史解释了你在阅读旧代码时会注意到的一个现象:为了能移植到 C89 而编写的库,连单行注释都用 /* */,因为 // 在它们仍要支持的旧工具链上会编译失败。
如今,你可能用到的每个编译器都同时支持这两种。普通的说明用 //,真正需要跨行的注释用 /* */。如果你的目标是非常老旧的嵌入式编译器,在依赖 // 之前先确认一下。
注释不能嵌套
这是唯一真正的陷阱:
/* Disable this section for now
int a = compute();
/* the classic helper - keep an eye on it */
int b = a * 2;
*/
块注释在第一个 */ 处结束,也就是第 3 行的那个。于是第 4 行和第 5 行又变回了有效代码,而第 6 行末尾的 */ 就成了语法错误。编译器的错误信息会指向最后一行,对真正的原因完全没有帮助。
解决办法是改用预处理器,它能正确处理嵌套:
#if 0
int a = compute();
/* the classic helper - keep an eye on it */
int b = a * 2;
#endif
#if 0 永远不成立,所以预处理器会在编译器看到之前,删除直到 #endif 为止的所有内容。里面有注释、引号或其他 #if 块都不受影响,清理代码时也很容易搜索到。
调试时注释掉代码
临时去掉一行代码,是注释在日常中最常见的用法。程序出现异常时,一次禁用一条语句,就能找出是哪一条在起作用。
取消 printf 的注释,再运行一次,就能看到循环是怎样一步步算出答案的。用打印语句追踪并不优雅,但在 C 语言中它又快又总是管用:调试器能告诉你更多,而 printf 能立刻告诉你一些东西。
有两个习惯可以避免这变成一团糟。提交之前删掉被注释掉的代码;版本控制会记住旧版本,你不需要自己记。如果你有意保留一行被禁用的代码,就在旁边写个说明,解释原因。
文档注释
函数上方的块注释,是用来说明它做什么、参数是什么意思,以及有什么出人意料之处的地方。
Doxygen 之类的工具会读取这样的结构化注释,并生成参考文档。Doxygen 自己的风格使用 /** ... */,配合 @param 和 @return 标签:
/**
* Converts Celsius to Fahrenheit.
* @param c temperature in Celsius
* @return the same temperature in Fahrenheit
*/
double celsius_to_fahrenheit(double c);
在你自己的代码中,哪种都可以。重要的是注释要放在别人会阅读的声明旁边(通常是头文件里),而不是埋在实现代码中。
哪些内容值得注释
在真实代码库中经得起考验的规则是:注释“为什么”,而不是“做了什么”。
i++; // increment i <- says nothing the code did not
/* Skip the BOM: files exported by the old system start with
three bytes that are not part of the data. */
offset += 3;
第二条注释包含了代码中任何地方都没有的信息。第一条则是噪音,迟早会和它所描述的那行代码自相矛盾,因为代码改了,注释往往不会跟着更新。
在 C 语言中,特别值得写注释的内容有:
- 这块内存归谁所有。 如果函数返回一个调用者必须
free的指针,就要写明。C 语言没有办法在类型中表达这一点。 - 单位和范围。
int timeout;含义不明:是秒还是毫秒? - 不那么显而易见的正确性。 为什么循环在
n - 1处停止,为什么这个类型转换是安全的,为什么缓冲区是 256 字节。 - 有意为之的古怪写法。 看起来像 bug 但其实不是的代码,如果不加说明,就会招来后来者的“修复”。
这条注释是值得的:它下面那行看起来多余,其实不然。
字符串里的注释不是注释
最后一个细节。在字符串字面量或字符常量中,注释符号没有任何特殊含义:
两行都会完整打印出来。编译器会先识别字符串,再去查找注释,所以引号里的 // 只是两个普通字符。(第一行的 %% 是用 printf 打印一个百分号的方法,单独的 % 会开始一个格式说明符。)
常见问题
C 语言怎么写注释?
有两种写法。// this is a comment 一直延续到行尾。/* this is a comment */ 可以跨越任意多行,在 */ 处结束。两者都会在编译前被删除,所以永远不会影响程序。
C 语言支持 // 注释吗?
支持,从 C99 开始。它借鉴自 C++,如今所有编译器都支持。只有非常古老的 C89 编译器会拒绝它,这就是为什么很老的代码连单行注释都用 /* */。
C 语言的注释可以嵌套吗?
不可以。/* outer /* inner */ still outer */ 在第一个 */ 处就结束了,剩下的 still outer */ 成了错误的代码。要禁用一段本身已包含 /* */ 注释的代码,请改用 #if 0 ... #endif,它可以正确嵌套。
C 语言怎么注释掉一段代码?
如果里面没有块注释,就用 /* */ 包起来,或者在每行前面加 //。对于大段代码,更稳妥的做法是在前面写 #if 0、后面写 #endif:预处理器会删除两者之间的所有内容,里面有注释和引号也不受影响。