如何在C语言中比较字符串的结尾?

57

我想确保我的字符串以“.foo”结尾。我正在使用C语言,这是我不太熟悉的语言。我找到了下面的最佳方法来实现它。有没有C语言专家能够验证我是否优雅而明智地完成了这个任务?

int EndsWithFoo(char *str)
{
    if(strlen(str) >= strlen(".foo"))
    {
        if(!strcmp(str + strlen(str) - strlen(".foo"), ".foo"))
        {
            return 1;
        }
    }
    return 0;
}

1
25个答案,只有4或5个没有问题。 - chqrlie
25个回答

75

每个字符串不要调用超过一次strlen。

int EndsWith(const char *str, const char *suffix)
{
    if (!str || !suffix)
        return 0;
    size_t lenstr = strlen(str);
    size_t lensuffix = strlen(suffix);
    if (lensuffix >  lenstr)
        return 0;
    return strncmp(str + lenstr - lensuffix, suffix, lensuffix) == 0;
}

int EndsWithFoo(const char *str) { return EndsWith(str, ".foo"); }

编辑:对于追求严谨的人,添加了对NULL的检查。对于超级严谨的人,可以讨论如果str和suffix都为NULL时是否应该返回非零值。


4
在这种情况下,您可以使用strcmp()而不是strncmp()(甚至是memcmp()),因为我们确切地知道此时两个字符串还剩下多少字符,尽管速度差异几乎不会被注意到。 - Adam Rosenfield
2
无论何时启用优化,对strlen的任何调用都会立即从汇编中消失,因此这可能是过早优化的情况(尽管C字符串足够复杂,以至于让人们考虑这样的问题)。 - Joey
1
@Johannes:除了编译时已知的字符串字面量之外,对于其他字符串,这怎么可能呢?当然,您可以内联strlen代码,但在某个级别上,您仍然需要找到字符串的长度。对于const字符串字面量,编译器知道它有多长,但一般情况下并非如此。您有什么想法? - Matt J
1
当 !str && !suffix 时,返回1会更有意义吧? - wilhelmtell
2
@MattJ:有点晚了……但我认为Adam的意思是编译器将删除除每个字符串的第一个调用之外的所有调用,因为它们没有被更改。当然,你是正确的,并非所有调用都可以被优化掉。 - stefanct

19
int EndsWithFoo(char *string)
{
  string = strrchr(string, '.');

  if (string != NULL)
    return strcmp(string, ".foo");

  return -1;
}

如果以".foo"结尾,将返回0。

躬身致意 你非常仁慈 :-) - user82238
6
由于零为假,非零为真,且函数名称指示布尔返回值,因此应该反转返回值。 - dreamlax
@dreamlax -> 是的,这是真的。 - user82238
1
@Squirrel strcmp会检查两个字符串的空终止符。如果一个字符串比另一个短,它将返回非零值。它不会越界读取。 - cgmb
我认为使用变量 "string" 不是一个好主意,因为它可能会与 C++ 中的 std::string 发生冲突。不确定编译器能做得多好,但我还是建议使用 const char *。因此代码应该是: int EndsWithFoo(const char *s1 ) { const char * s2 = strrchr(s1, '.'); .... 现在所有关于 s2 的操作都可以进行了。 - Tõnu Samuel
EndsWithFoo()这个名称表明如果字符串以 ".foo" 结尾,则返回值为非零:您的命名规则令人困惑。 - chqrlie

10

我现在没有编译器的访问权限,所以请问一下这个代码是否有效?

#include <stdio.h>
#include <string.h>

int EndsWithFoo(const char* s);

int
main(void)
{
  printf("%d\n", EndsWithFoo("whatever.foo"));

  return 0;
}

int EndsWithFoo(const char* s)
{
  int ret = 0;

  if (s != NULL)
  {
    size_t size = strlen(s);

    if (size >= 4 &&
        s[size-4] == '.' &&
        s[size-3] == 'f' &&
        s[size-2] == 'o' &&
        s[size-1] == 'o')
    {
      ret = 1;
    }
  }

  return ret;
}

无论如何,一定要将参数标记为const,这会告诉每个人(包括编译器)你不打算修改这个字符串。


+1 最优化。当'foo'不改变时,我更喜欢这样的版本! - dirkgently
3
小提示:如果您有互联网连接,您可以在codepad.org上使用一个C编译器。 - John Cromartie
1
墨菲定律说:“.foo” 将会在最不合时宜的时候发生变化。 - plinth
1
这并不是“最优化”的,因为你会多次读取.foo部分——一次用于strlen,一次用于比较。但是很好而且实用。+1 - aib
我非常确定,使用循环和const char *的标准版本即使不使用-O3也会优化为类似这样的代码。 - Samuel Danielson

5
下面是使用memcmp()实现与Python的str.endswith()相同返回值的通用解决方案。不检查str / suffix是否为NULL是有意义的,其他libc str函数也不检查NULL:
int ends_with(const char *str, const char *suffix) {
  size_t str_len = strlen(str);
  size_t suffix_len = strlen(suffix);

  return (str_len >= suffix_len) &&
         (!memcmp(str + str_len - suffix_len, suffix, suffix_len));
}

测试 C:

printf("%i\n", ends_with("", ""));
printf("%i\n", ends_with("", "foo"));
printf("%i\n", ends_with("foo", ""));
printf("%i\n", ends_with("foo", "foo"));
printf("%i\n", ends_with("foo", "foobar"));
printf("%i\n", ends_with("foo", "barfoo"));
printf("%i\n", ends_with("foobar", "foo"));
printf("%i\n", ends_with("barfoo", "foo"));
printf("%i\n", ends_with("foobarfoo", "foo"));

结果 C:

1
0
1
1
0
0
0
1
1

测试Python:

print("".endswith(""))
print("".endswith("foo"))
print("foo".endswith(""))
print("foo".endswith("foo"))
print("foo".endswith("foobar"))
print("foo".endswith("barfoo"))
print("foobar".endswith("foo"))
print("barfoo".endswith("foo"))
print("foobarfoo".endswith("foo"))

Python 结果:

True
False
True
True
False
False
False
True
True

3
如果您可以更改函数的签名,则尝试将其更改为:
int EndsWith(char const * str, char const * suffix, int lenstr, int lensuf);

这将导致更安全、更可重用和更高效的代码:
  1. 添加const限定符将确保您不会错误地修改输入字符串。这个函数是一个谓词,所以我假设它永远不会有副作用。
  2. 要比较的后缀作为参数传递,因此您可以保存此函数以供以后重用其他后缀。
  3. 如果您已经知道字符串的长度,则此签名将使您有机会将其传递给函数。我们称之为动态规划
我们可以这样定义函数:
int EndsWith(char const * str, char const * suffix, int lenstr, int lensuf)
{
    if( ! str && ! suffix ) return 1;
    if( ! str || ! suffix ) return 0;
    if( lenstr < 0 ) lenstr = strlen(str);
    if( lensuf < 0 ) lensuf = strlen(suffix);
    return strcmp(str + lenstr - lensuf, suffix) == 0;
}

明显的反对意见是,额外的参数会导致代码中出现更多的噪音,或者说代码表达能力降低。

如果 lenstr < lensuf,无论是作为参数值还是通过 strlen() 计算得出的,您的代码都存在未定义行为。另一个反驳的观点是 int 的正数范围比 size_t 小。 - chqrlie

2
你的解决方案只要参数是有效的以空字符结尾的字符串就可以正常工作,这是最重要的,从这个角度来看,你做得很明智。其他更复杂的解决方案并不符合这个目标。编译器会内联strlen(".foo"),并且应该能够确定两个实例的strlen(str)返回相同的值,因此生成单个调用((clang和gcc))。然而,在我看来,更加优雅的方式是计算长度一次并使用memcmp()而不是需要更多工作并且不能内联的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的编译器资源管理器上验证的那样,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

Intel的ICC编译器生成一组冗长而复杂的SIMD指令,即使在英特尔处理器上也可能难以跟踪并且效率可能更低。性能严重依赖于strlen()库函数的效率,因此基准测试应包括各种字符串长度的分布。
关于“最有效的解决方案是什么”的绝对答案并不存在,但简单并不排除高效性,简单直接的代码更易于验证。当它结合了简单性,正确性和效率时,就达到了“优雅”。
引用Brian Kernighan的话:
- 控制复杂性是计算机编程的本质。
软件工具(1976年),第319页(与P.J.Plauger合著)。 - 每个人都知道调试比首次编写程序难两倍。因此,如果您在编写程序时越聪明,那么您将如何调试它?
“编程风格的要素”,第2版,第2章。

2

已测试的代码,包括测试:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int ends_with_foo(const char *str)
{
    char *dot = strrchr(str, '.');

    if (NULL == dot) return 0;
    return strcmp(dot, ".foo") == 0;
}

int main (int argc, const char * argv[]) 
{
    char *test[] = { "something", "anotherthing.foo" };
    int i;

    for (i = 0; i < sizeof(test) / sizeof(char *); i++) {
        printf("'%s' ends %sin '.foo'\n",
               test[i],
               ends_with_foo(test[i]) ? "" : "not ");
    }
    return 0;
}

Snap!虽然我不会费心比较strcmp()的结果 - 直接返回它。 - user82238
你假设输入字符串中没有其他的“.”。 - Naveen
这没有什么区别。只有在两个字符串完全相同的情况下(例如长度必须相同),strcmp()才会返回0。如果字符串长度不同,比较将提前结束。 - user82238
@Blank Xavier:问题中的版本如果以“.foo”结尾,则返回1。只返回strcmp()的结果会在成功时返回0。 - stesch
这个解决方案适用于硬编码的“。foo”扩展名,但如果扩展名包含多个“。”,比如“。tar.gz”,该方法将失败。 - chqrlie
显示剩余2条评论

2

不需要strlen(".foo")。如果您确实希望它具有灵活性,可以使用sizeof ".foo" - 1--这是一个编译时常量。

此外,最好进行空字符串检查。


请纠正我,sizeof(“.foo”)不是5吗?但是strlen(“.foo”)是4吗?我认为strlen更容易阅读,因为我在处理字符串长度。而且编译器应该将其优化为常量...函数的其余部分如何? - JoeF
具体来说,这并不必要,因为我们已经知道“.foo”的长度。 - Chuck
“.foo”不是一个const char *吗?即使被视为数组,它也有五个字符,因为在数组形式中它确实有终止符'\0'。 - David Thornley
1
对字符串字面值使用strlen通常会被优化掉。 - dreamlax
@dirkgently:这个 NULL 是什么意思?我是在说,使用任何一个好的编译器,执行 strlen(".foo") 会被优化为一个常量 size_t,等于 4,因为字符串的长度在编译时已知。这可以通过 objdump 进行验证。 - dreamlax
显示剩余2条评论

2
这是你在这里找到的最高效(对于计算机而言)的答案。
int endsWith(const char *string,const char *tail)
{

    const char *s1;

    const char *s2;

    if (!*tail)
        return 1;
    if (!*string)
        return 0;
    for (s1 = string; *s1; ++s1);
    for (s2 = tail; *s2; ++s2);
    if (s1 - string < s2 - tail)
        return 0;
    for (--s1, --s2; *s1 == *s2 && s2 >= tail; --s1, --s2);
    if (s2 < tail)
        return 1;
    else
        return 0;
}

1
如果在s2到达tail之前将其递减到开头,那么s2 >= tail会调用未定义的行为,if (s2 < tail)也是如此。你应该写成while (*--s1 == *--s2) { if (s2 == tail) return 1; } return 0; - chqrlie
@chqrlie 在你的评论中提到,我刚刚改变了比较的顺序:s2 >= tail && *s1 == *s2; 谢谢! - Daniel De León
1
@DanielDeLeón:恐怕你的修复无效:如果匹配成功,你将使用s2 = tail - 1解引用*s2,这是未定义行为。此外,即使s2 < tail,如果s2tail之前被减少,也会产生未定义的行为。为什么不使用我的建议:while (*--s1 == *--s2) { if (s2 == tail) return 1; } return 0; - chqrlie
谢谢@chqrlie!你很谨慎,这是在使用C时最好的策略。我非常感谢你对细节的关注。现在我采用了你的建议,效果非常好! - Daniel De León
@DanielDeLeón:实际上,我的评论是指问题中的当前代码。你的修复无效,因为将s2递减到tail的开始位置会导致未定义的行为。在这种情况下,s2 >= tail毫无意义。以下是一个病态的例子:在MS/DOS或16位Windows上,使用大模型或远指针,如果tail的偏移量为0x0000,则tail - 1的偏移量将为0xFFFF并且相同的段,导致tail - 1 >= tail计算结果为true。 - chqrlie

1
抱歉我来晚了。你能否通过一些简单的指针数学操作解决问题?
char* str = "hello.foo"; //this would be string given

int x = 4; //.foo has 4 characters

int n = strlen(str)- x; //where x is equal to suffix length

char* test = &str[n]; //do some pointer math to find the last characters

if(strcmp(test, ".foo") == 0){
    //do some stuff
}// end if

字符指针通过指向其数组中的第一个字符来工作。因此,当您这样做时,您将test的第一个字符设置为'.foo'中的'.'(如果它包含在内)。这也是为什么您不需要为其分配内存,因为它只是指向已经存在的字符数组。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接