任何 C 大师都想确保我优雅而明智地做到这一点?
只要参数是一个有效的以空字符结尾的字符串,您的解决方案就正确工作。这是最重要的,在这方面,您这样做明智。作为答案发布的更复杂的解决方案不符合此目标。
编译器将内联strlen(".foo"),并且应该能够确定strlen(str) 的两个实例返回相同的值,因此生成一个调用(clang and gcc do)。
然而,恕我直言,计算一次长度并使用 memcmp() 而不是 strcmp() 会更优雅,因为 strcmp() 需要更多的工作并且没有内联。您还应该将str 定义为const char * 以实现const 的正确性并防止在使用常量字符串或字符串文字调用您的函数时出现警告。
测试特定的".foo" 后缀是一个更普遍问题的特例:测试一个字符串是否是另一个字符串的后缀。
这是一个简单有效的解决方案:
#include <string.h>
int strEndsWith(const char *s, const char *suff) {
size_t slen = strlen(s);
size_t sufflen = strlen(suff);
return slen >= sufflen && !memcmp(s + slen - sufflen, suff, sufflen);
}
int strEndsWithFoo(const char *s) {
return strEndsWith(s, ".foo");
}
代码非常简单和通用,但现代编译器将非常有效地内联strEndsWithFoo。正如可以在GodBolt's compiler explorer 上验证的那样,clang 12.0.0 在编译时计算".foo" 的长度并将memcmp() 内联为单个cmp 指令,仅生成12 个x86_64 指令:
strEndsWithFoo: # @strEndsWithFoo
pushq %rbx
movq %rdi, %rbx
callq strlen
movq %rax, %rcx
xorl %eax, %eax
cmpq $4, %rcx
jb .LBB1_2
xorl %eax, %eax
cmpl $1869571630, -4(%rbx,%rcx) # imm = 0x6F6F662E
sete %al
.LBB1_2:
popq %rbx
retq
gcc 11.2 生成非常相似的代码,也是 12 条指令:
strEndsWithFoo:
pushq %rbx
movq %rdi, %rbx
call strlen
xorl %r8d, %r8d
cmpq $3, %rax
jbe .L7
xorl %r8d, %r8d
cmpl $1869571630, -4(%rbx,%rax)
sete %r8b
.L7:
movl %r8d, %eax
popq %rbx
ret
英特尔的 ICC 编译器会生成一组冗长而复杂的 SIMD 指令,即使在英特尔处理器上也更难以遵循并且效率可能更低。性能在很大程度上取决于strlen() 库函数的效率,因此基准测试应包括字符串长度的各种分布。
对于最有效的解决方案怎么办?没有绝对的答案,但简单性并不排除效率,简单直接的代码更容易验证。当它结合了简单、正确和高效时,就达到了优雅。
引用Brian Kernighan:
-
控制复杂性是计算机编程的本质。
软件工具 (1976), p. 319(与 P. J. Plauger)。
-
每个人都知道,调试的难度是编写程序的两倍。因此,如果您在编写它时尽可能地聪明,您将如何调试它?
“编程风格的要素”,第 2 版,第 2 章。