在编译程序时,宏只展开一次。由于时间旅行不是 C 语言的一部分,因此将来执行程序不可能追溯更改宏的结果。所以如果一个计算,比如计算一个字符串的长度,依赖于程序编译时不知道的信息,那么宏是完全没用的。除非字符串碰巧是文字,否则就是这种情况。而且我敢断言,在绝大多数情况下,程序编译时所需长度的字符串并不存在。
清楚地了解宏的实际作用(在编译前修改程序文本)将有助于避免分心,例如本问题中的建议。
在常量字符串文字上使用strlen 有时会很有用,以避免将来修改字符串文字时可能引入的错误。例如下面的(测试line是否以Hello开头):
/* Code smell: magic number */
if (strncmp(line, "Hello", 5) == 0) { ... }
最好写成:
/* Code smell: redundant repetition, see below */
if (strncmp(line, "Hello", strlen("Hello")) == 0) { ... }
显然,如果计算可以在编译时执行一次,那么最好这样做而不是在程序运行时重复执行。曾几何时,当编译器很原始并且几乎无法理解控制流时,担心这些事情是有道理的,尽管即使在那时,很多手动优化也过于复杂,无法获得微不足道的好处。
今天,即使是这个借口也不适用于过早的优化器。大多数现代 C 编译器都能够完美地用常量 5 替换 strlen("Hello");,因此永远不会调用库函数。实现这种优化不需要宏魔法。
如前所述,示例中的测试仍有不必要的前缀字符串重复。我们真正想写的是:
if (startsWith(line, "Hello")) { ... }
在这里将startsWith 定义为宏的诱惑将非常强烈,因为看起来简单的编译时替换就足够了。应该避免这种诱惑。现代编译器也能够“内联”函数调用;也就是将调用的主体直接插入到代码中。
所以定义:
static int startsWith(const char* line, const char* prefix) {
return strncmp(line, prefix, strlen(prefix)) == 0;
}
将与其对应的宏一样快,并且与宏不同,当使用带有副作用的第二个参数调用它时不会导致问题:
/* Bad style but people do it */
if (startsWith(line, prefixes[++i])) { doAction(i); }
一旦调用被内联,编译器就可以继续应用其他优化,例如在前缀参数是字符串文字的情况下消除对 strlen 的调用。