如何在C语言中检查一个字符串是否以另一个字符串开头?

113

标准C库中是否有类似于 startsWith(str_a, str_b) 的函数?

它应该接受指向两个以 nullbytes 结尾的字符串的指针,并告诉我第一个字符串是否完全出现在第二个字符串的开头。

示例:

"abc", "abcdef" -> true
"abcdef", "abc" -> false
"abd", "abdcef" -> true
"abc", "abc"    -> true

3
我认为你的第三个例子应该有一个正确的结果。 - Michael Burr
可能是重复的问题 https://dev59.com/CWUp5IYBdhLWcg3wGEoR#15515276 - vacing
10个回答

223

没有标准函数来实现这个,但是您可以定义

bool prefix(const char *pre, const char *str)
{
    return strncmp(pre, str, strlen(pre)) == 0;
}

我们不必担心strpre短,因为根据C标准(7.21.4.4/2):

strncmp函数仅比较从指向s1的数组到指向s2的数组中的最多n个字符(跟在空字符后面的字符不会被比较)。


17
为什么回答是否定的?显然,回答是肯定的,它叫做“strncmp”。 - Jasper
14
答案为否应该是显而易见的。一个使用 strncmpstrlen 的算法并不被称为“叫做 strncmp”。 - Jim Balter
1
它不是一个直接返回布尔值的 startsWith() 函数。 - Sridhar Sarnobat

92

显然,这里没有标准的C函数可以完成这个任务。所以:

bool startsWith(const char *pre, const char *str)
{
    size_t lenpre = strlen(pre),
           lenstr = strlen(str);
    return lenstr < lenpre ? false : memcmp(pre, str, lenpre) == 0;
}
请注意,上面的方法非常清晰明了,但如果你正在紧密循环或处理非常大的字符串,则它并不提供最佳性能,因为它会在一开始扫描两个字符串的全部长度(使用`strlen`函数)。像wj32Christoph的解决方案可能会提供更好的性能(尽管这条评论关于向量化超出了我的C知识范畴)。还要注意Fred Foo的解决方案,它避免了对`str`进行`strlen`操作(他是正确的,如果您使用`strncmp`而不是`memcmp`,则这是不必要的)。只有在(非常)大的字符串或紧密的循环中重复使用时才会产生影响,但当它有影响时,就非常重要。

8
我应该提到,通常字符串会作为第一个参数,前缀作为第二个参数。但我保留原样是因为你的问题似乎是这样表述的... 参数顺序完全由您决定,但实际上我真的应该按照另一种方式处理 - 大多数字符串函数将完整字符串作为第一个参数,子字符串作为第二个参数。 - T.J. Crowder
2
这是一个优雅的解决方案,但它确实存在一些性能问题。优化后的实现从每个字符串中最多查看min(strlen(pre), strlen(str))个字符,并且永远不会超出第一个不匹配的位置。如果字符串很长,但早期不匹配很常见,那么它将非常轻量级。但由于此实现在一开始就需要完整长度的两个字符串,即使字符串在第一个字符上不同,它也会强制执行最坏情况的性能。是否重要取决于具体情况,但这是一个潜在的问题。 - Tom Karzes
2
@TomKarzes 在这里你可以用 memcmp 替换 strncmp,它会更快。因为两个字符串都已知至少有 lenpre 个字节,所以没有 UB。strncmp 检查两个字符串的每个字节是否为 NUL,但是 strlen 调用已经保证了没有任何 NUL。(但是当 prestr 长度大于实际的公共初始序列时,仍然会有你提到的性能损失。) - Jim Balter
1
@JimBalter - 非常好的观点!由于在此之前使用memcmp不会侵犯其他答案,因此我已经在答案中进行了更改。 - T.J. Crowder
1
P.P.S. 如果函数是内联的,并且一个或多个参数的长度已知,例如常量,则这种方法会更加有效。考虑针对同一字符串检查几个不同的前缀--一个strlen用于目标字符串,加上每个前缀的strlen(除非是常量)和memcmp(如果前缀比目标字符串长,则甚至不需要)。 - Jim Balter
显示剩余2条评论

41

我可能会选择使用strncmp(),但只是为了好玩,这里有一个基本的实现:

_Bool starts_with(const char *restrict string, const char *restrict prefix)
{
    while(*prefix)
    {
        if(*prefix++ != *string++)
            return 0;
    }

    return 1;
}

8
我最喜欢这个 - 无需扫描任一字符串的长度。 - Michael Burr
1
我也可能会选择strlen+strncmp,但尽管它确实有效,但所有关于它模糊定义的争议让我望而却步。所以我会使用这个,谢谢。 - Sam Watkins
6
除非你的编译器非常擅长向量化处理,否则这很可能比strncmp慢,因为glibc的编写者肯定是这样的 :-) - Ciro Santilli OurBigBook.com
3
如果前缀不匹配,这个版本应该比strlen+strncmp版本更快,特别是如果在前几个字符中已经有差异的情况下。 - dpi
如果字符串是常量,好的编译器已经知道它的长度,所以这可能会再次变慢... - Antti Haapala -- Слава Україні
1
那种优化只有在函数被内联时才会应用。 - Jim Balter

5

5
这似乎是一种有些不合理的做法 - 即使从非常短的初始段落中就应该清楚strb是否为前缀,您仍然需要遍历整个stra。 - StasM
2
过早的优化是万恶之源。我认为,如果不是时间关键代码或长字符串,这是最好的解决方案。 - Frank Buss
4
这句话是著名的计算机科学家说过的名言,建议您可以在Google上搜索一下。但这句话经常被错误引用(例如此处),您可以参考http://www.joshbarczak.com/blog/?p=580。 - Jim Balter
我个人支持弗兰克。Unix哲学:清晰比聪明更重要。 - Sridhar Sarnobat

4

我不是写优雅代码的专家,但...


int prefix(const char *pre, const char *str)
{
    char cp;
    char cs;

    if (!*pre)
        return 1;

    while ((cp = *pre++) && (cs = *str++))
    {
        if (cp != cs)
            return 0;
    }

    if (!cs)
        return 0;

    return 1;
}

2
我发现在Linux内核中有如下函数定义。如果strprefix开头,则返回true,否则返回false
/**
* strstarts - does @str start with @prefix?
* @str: string to examine
* @prefix: prefix to look for.
*/
bool strstarts(const char *str, const char *prefix)
{
     return strncmp(str, prefix, strlen(prefix)) == 0;
}

除了参数的顺序之外,这与Fred Foo的答案有什么不同? - chqrlie
1
明显的区别是我提供了一个我没有编写的代码的参考。该代码于2009年被添加到Linux内核中1,比Fred Foo的答案早两年发布。因此,你应该质疑Fred Foo的答案,而不是我的。 - Farzam
这个解决方案相当明显,Linus 也不是第一个编写它的人。请注意,Christoph 的解决方案更简单、更有效,而被接受的解决方案则很笨拙。 - chqrlie

1
优化后(版本2-已更正):
uint32 startsWith( const void* prefix_, const void* str_ ) {
    uint8 _cp, _cs;
    const uint8* _pr = (uint8*) prefix_;
    const uint8* _str = (uint8*) str_;
    while ( ( _cs = *_str++ ) & ( _cp = *_pr++ ) ) {
        if ( _cp != _cs ) return 0;
    }
    return !_cp;
}

3
投反对票:startsWith("\2", "\1")返回1,startsWith("\1", "\1")也返回1。 - thejh
这个决定不会使用clang中的优化,因为不使用内置函数。 - socketpair
汇编指令在这里并没有帮助,特别是如果目标字符串比前缀长得多。 - Jim Balter

0

或者两种方法的结合:

_Bool starts_with(const char *restrict string, const char *restrict prefix)
{
    char * const restrict prefix_end = prefix + 13;
    while (1)
    {
        if ( 0 == *prefix  )
            return 1;   
        if ( *prefix++ != *string++)
            return 0;
        if ( prefix_end <= prefix  )
            return 0 == strncmp(prefix, string, strlen(prefix));
    }  
}

编辑:下面的代码不起作用,因为如果strncmp返回0,则不知道是到达了终止0还是长度(块大小)。

另一个想法是逐块进行比较。如果块不相等,则将该块与原始函数进行比较:

_Bool starts_with_big(const char *restrict string, const char *restrict prefix)
{
    size_t block_size = 64;
    while (1)
    {
        if ( 0 != strncmp( string, prefix, block_size ) )
          return starts_with( string, prefix);
        string += block_size;
        prefix += block_size;
        if ( block_size < 4096 )
          block_size *= 2;
    }
}

常量13644096以及block_size的指数仅仅是猜测。它必须根据所使用的输入数据和硬件进行选择。


1
这些是好主意。但请注意,如果前缀长度小于12个字节(包括NUL在内的13个字节),则第一个主意在技术上是未定义行为,因为语言标准没有定义计算字符串之外地址的结果,除了紧随其后的字节。 - Jim Balter
@JimBalter:你能加上一个参考吗?如果指针被解引用并且在终止0之后,那么解引用的指针值是未定义的。但为什么地址本身应该是未定义的呢?这只是一个计算。 - shpc
然而,存在一个普遍的错误:block_size 的增加必须在指针增加之后。现已修复。 - shpc

0

我使用这个宏:

#define STARTS_WITH(string_to_check, prefix) (strncmp(string_to_check, prefix, ((sizeof(prefix) / sizeof(prefix[0])) - 1)) ? 0:((sizeof(prefix) / sizeof(prefix[0])) - 1))

如果字符串以该前缀开头,则返回前缀长度。此长度在编译时(使用sizeof)计算,因此没有运行时开销。


1
(我非常确定)优化器无论如何都会评估字面值字符串的strlen,因此对于sizeof而言使用价值很小。另外,在宏中隐藏sizeof(仅在相应对象在作用域内时才正常工作)是引入可怕错误的非常简单的方法。 - stefanct

0

因为我运行了被接受的版本,但是遇到了一个非常长的字符串问题,所以我不得不添加以下逻辑:

bool longEnough(const char *str, int min_length) {
    int length = 0;
    while (str[length] && length < min_length)
        length++;
    if (length == min_length)
        return true;
    return false;
}

bool startsWith(const char *pre, const char *str) {
    size_t lenpre = strlen(pre);
    return longEnough(str, lenpre) ? strncmp(str, pre, lenpre) == 0 : false;
}

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