C语言中,strtok和strsep有什么区别?

35

请问有人能解释一下 strtok()strsep() 之间的区别吗?它们各自的优缺点是什么? 为什么我会选择其中一个而不是另一个。

3个回答

65

strtok()strsep()之间的一个主要区别是,strtok()是标准化的(由C标准和因此也由POSIX标准化),而strsep()没有被C或POSIX标准化;它在GNU C库中可用,并起源于BSD。因此,便携式代码更可能使用strtok()而不是strsep()

另一个区别是,对不同字符串调用strsep()函数可以交错进行,而您不能使用strtok()来做到这一点(尽管您可以使用strtok_r())。因此,在库中使用strsep()不会意外破坏其他代码,而在库函数中使用strtok()必须进行文档记录,因为同时使用strtok()的其他代码无法调用库函数。

strsep()的手册页面在kernel.org上说:

strsep()函数被引入作为strtok(3)的替代,因为后者无法处理空字段。

因此,另一个主要区别是George Gaál在他的答案中强调的;strtok()允许一个标记之间有多个分隔符,而strsep()期望标记之间只有一个分隔符,并将相邻的分隔符解释为空标记。

strsep()strtok()都会修改它们的输入字符串,也都不允许你确定哪个分隔符字符标记了标记的结尾(因为两者都会在标记结束后写入一个NUL '\0'覆盖分隔符)。

何时使用它们?

  • 当您想要空标记而不允许多个分隔符之间存在时,您将使用strsep(),并且当您不关心可移植性时。
  • 当您想要允许多个分隔符之间存在并且您不想要空标记时,您将使用strtok_r()(如果POSIX对您足够可移植)。
  • 只有在有人威胁到您的生命时,您才会使用strtok()。并且您只会使用它足以使您摆脱生命威胁的时间;然后您将再次放弃所有使用它的方法。它是有毒的;不要使用它。编写自己的strtok_r()strsep()比使用strtok()更好。

为什么strtok()是有毒的?

strtok()函数在库函数中使用时是有毒的。如果您的库函数使用了strtok(),必须清楚地记录下来。

原因如下:

  1. 如果任何调用函数正在使用strtok()并调用您使用strtok()的函数,则会中断调用函数。
  2. 如果您的函数调用任何调用strtok()的函数,则会破坏您的函数对strtok()的使用。
  3. 如果您的程序是多线程的,则最多只能有一个线程在任何给定时间使用strtok()——跨一系列strtok()调用。

这个问题的根源是调用之间保存状态,使得strtok()可以从上次离开的地方继续。没有明智的方法来解决这个问题,除了“不要使用strtok()”。

  • 如果可用,您可以使用strsep()
  • 如果可用,您可以使用POSIX的strtok_r()
  • 如果可用,您可以使用Microsoft的strtok_s()
  • 名义上,您可以使用ISO/IEC 9899:2011附录K.3.7.3.1函数strtok_s(),但其接口与strtok_r()和Microsoft的strtok_s()都不同。

BSD strsep()

char *strsep(char **stringp, const char *delim);

POSIX strtok_r()

char *strtok_r(char *restrict s, const char *restrict sep, char **restrict state);

微软 strtok_s():

char *strtok_s(char *strToken, const char *strDelimit, char **context);

附录 K strtok_s()

char *strtok_s(char * restrict s1, rsize_t * restrict s1max,
               const char * restrict s2, char ** restrict ptr);

请注意,此函数有4个参数,而不是其他两种 strtok()变体中的3个参数。

2
请注意,Annex K中的strtok_s()声明如下:char *strtok_s(char * restrict s1, rsize_t * restrict s1max, const char * restrict s2, char ** restrict ptr);这与Microsoft的strtok_s()或POSIX的strtok_r()的接口不匹配。即使它被实现了,差异也很烦人——它限制了Annex K函数的实用性。另请参见Do you use the TR 24731 'safe' functions? - Jonathan Leffler
语句“调用strsep()函数时可以交替使用不同的字符串,而strtok()则不能这样做”的意思是,你可以使用strsep()同时从两个不同的字符串中分离出标记,先从string1中取一个标记,然后再从string2中取一个标记;而strtok()则要求你在处理string1之前完全分割它,然后再处理string2或者反过来。这意味着,strtok()无法进行多线程操作,但它也严重限制了单线程程序的效率。 - Jonathan Leffler
@iamoumuamua — strtok() 函数用于识别由分隔符分隔的标记。任何不是分隔符的字符都是标记的一部分。调用 strtok() 时,它会跳过任何前导分隔符,然后识别第一个非分隔符并记录其位置,然后返回该位置。它跳过一个或多个组成标记的非分隔符。当它遇到分隔符或到达字符串的末尾(空字节 '\0')时,它确保标记以空字符结尾。_ […继续…]_ - Jonathan Leffler
它记录了一个私有变量中的位置(这就是大多数与 strtok() 相关的问题所在),并返回刚刚找到的令牌的起始指针。当下一次使用 NULL 指针作为第一个参数调用它时,它从私有变量中检索到达的位置,并恢复其扫描,跳过任何分隔符,直到找到非分隔符为止。即使在处理单个字符串时,不同的 strtok() 调用之间的分隔符字符集也可以不同。 - Jonathan Leffler
strtok_r()strtok_s()strsep() 函数都避免了私有变量;用户向函数传递存储空间以获取该信息。这意味着它们是线程安全和可重入的,并且可以用于并行分析不同的字符串。 - Jonathan Leffler
显示剩余7条评论

11

来自GNU C库手册 - 在字符串中查找标记:

strsepstrtok_r之间的一个区别是,如果输入字符串中连续包含来自分隔符的多个字符,则strsep会为每对来自分隔符的字符返回一个空字符串。这意味着程序通常应该在处理它之前测试strsep是否返回一个空字符串。


你能给我一个例子吗?我有点困惑。 - mizuki
2
如果您单击链接,可以找到使用这些函数的示例 :-) 另请注意,strsep函数可能在您的C编译器中不存在。 - George Gaál

7

strtok()strsep()的第一个区别在于它们处理输入字符串中连续定界符的方式。

strtok()处理连续定界符字符的方式为:

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

int main(void) {
    const char* teststr = "aaa-bbb --ccc-ddd"; //Contiguous delimiters between bbb and ccc sub-string
    const char* delims = " -";  // delimiters - space and hyphen character
    char* token;
    char* ptr = strdup(teststr);

    if (ptr == NULL) {
        fprintf(stderr, "strdup failed");
        exit(EXIT_FAILURE);
    }

    printf ("Original String: %s\n", ptr);

    token = strtok (ptr, delims);
    while (token != NULL) {
        printf("%s\n", token);
        token = strtok (NULL, delims);
    }

    printf ("Original String: %s\n", ptr);
    free (ptr);
    return 0;
}

输出:

# ./example1_strtok
Original String: aaa-bbb --ccc-ddd
aaa
bbb
ccc
ddd
Original String: aaa

在输出中,你可以看到两个token "bbb""ccc"紧挨着出现。 strtok() 不会指示连续的分隔符字符的出现。此外,strtok() 会修改输入字符串。 strsep() 处理连续分隔符字符:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int main(void) {
    const char* teststr = "aaa-bbb --ccc-ddd"; //Contiguous delimiters between bbb and ccc sub-string
    const char* delims = " -";  // delimiters - space and hyphen character
    char* token;
    char* ptr1;
    char* ptr = strdup(teststr);

    if (ptr == NULL) {
        fprintf(stderr, "strdup failed");
        exit(EXIT_FAILURE);
    }

    ptr1 = ptr;

    printf ("Original String: %s\n", ptr);
    while ((token = strsep(&ptr1, delims)) != NULL) {
        if (*token == '\0') {
            token = "<empty>";
        }
        printf("%s\n", token);
    }

    if (ptr1 == NULL) // This is just to show that the strsep() modifies the pointer passed to it
        printf ("ptr1 is NULL\n");
    printf ("Original String: %s\n", ptr);
    free (ptr);
    return 0;
}

输出:

# ./example1_strsep
Original String: aaa-bbb --ccc-ddd
aaa
bbb
<empty>             <==============
<empty>             <==============
ccc
ddd
ptr1 is NULL
Original String: aaa

在输出中,您可以看到两个空字符串(通过<empty>表示)在bbbccc之间。这两个空字符串是为了"--"而存在的。当strsep()"bbb"后找到定界符字符' '时,它将分隔符字符替换为'\0'字符并返回"bbb"。此后,strsep()发现另一个定界符字符'-'。然后它会将分隔符字符替换为'\0'字符并返回空字符串。下一个定界符字符也是如此。
strsep()返回指向空字符的指针(即值为'\0'的字符)时,表示连续的定界符字符。 strsep()修改输入字符串以及作为第一个参数传递的指针的指针。
第二个区别是,strtok()依赖于静态变量来跟踪字符串中当前解析位置。此实现需要在开始第二个字符串之前完全解析一个字符串。但是,strsep()不需要这样做。
在另一个strtok()未完成时调用strtok()
#include <stdio.h>
#include <string.h>

void another_function_callng_strtok(void)
{
    char str[] ="ttt -vvvv";
    char* delims = " -";
    char* token;

    printf ("Original String: %s\n", str);
    token = strtok (str, delims);
    while (token != NULL) {
        printf ("%s\n", token);
        token = strtok (NULL, delims);
    }
    printf ("another_function_callng_strtok: I am done.\n");
}

void function_callng_strtok ()
{
    char str[] ="aaa --bbb-ccc";
    char* delims = " -";
    char* token;

    printf ("Original String: %s\n", str);
    token = strtok (str, delims);
    while (token != NULL)
    {
        printf ("%s\n",token);
        another_function_callng_strtok();
        token = strtok (NULL, delims);
    }
}

int main(void) {
    function_callng_strtok();
    return 0;
}

输出:

# ./example2_strtok
Original String: aaa --bbb-ccc
aaa
Original String: ttt -vvvv
ttt
vvvv
another_function_callng_strtok: I am done.

函数 function_callng_strtok() 仅打印标记 "aaa",并且不打印输入字符串的其余标记,因为它调用了 another_function_callng_strtok(),后者又调用了 strtok() 并在提取所有标记后将 strtok() 的静态指针设置为 NULL。控制权回到 function_callng_strtok()while 循环,由于静态指针指向 NULL,因此 strtok() 返回 NULL,使循环条件为 false,从而退出循环。
在另一个 strsep() 未完成时调用 strsep()
#include <stdio.h>
#include <string.h>

void another_function_callng_strsep(void)
{
    char str[] ="ttt -vvvv";
    const char* delims = " -";
    char* token;
    char* ptr = str;

    printf ("Original String: %s\n", str);
    while ((token = strsep(&ptr, delims)) != NULL) {
        if (*token == '\0') {
            token = "<empty>";
        }
        printf("%s\n", token);
    }
    printf ("another_function_callng_strsep: I am done.\n");
}

void function_callng_strsep ()
{
    char str[] ="aaa --bbb-ccc";
    const char* delims = " -";
    char* token;
    char* ptr = str;

    printf ("Original String: %s\n", str);
    while ((token = strsep(&ptr, delims)) != NULL) {
        if (*token == '\0') {
            token = "<empty>";
        }
        printf("%s\n", token);
        another_function_callng_strsep();
    }
}

int main(void) {
    function_callng_strsep();
    return 0;
}

输出:

# ./example2_strsep
Original String: aaa --bbb-ccc
aaa
Original String: ttt -vvvv
ttt
<empty>
vvvv
another_function_callng_strsep: I am done.
<empty>
Original String: ttt -vvvv
ttt
<empty>
vvvv
another_function_callng_strsep: I am done.
<empty>
Original String: ttt -vvvv
ttt
<empty>
vvvv
another_function_callng_strsep: I am done.
bbb
Original String: ttt -vvvv
ttt
<empty>
vvvv
another_function_callng_strsep: I am done.
ccc
Original String: ttt -vvvv
ttt
<empty>
vvvv
another_function_callng_strsep: I am done.

在这里,您可以看到,在完全解析一个字符串之前调用strsep()没有任何区别。

因此,strtok()strsep()的缺点是两者都修改输入字符串,但是strsep()strtok()具有上述优点。

来自strsep

strsep()函数旨在替换strtok()函数。虽然应该出于可移植性原因首选strtok()函数(它符合ISO / IEC 9899:1990(“ISO C90”)),但它无法处理空字段,即检测由两个相邻分隔符字符限定的字段,或仅用于单个字符串。 strsep()函数最初出现在4.4BSD中。


参考:


你好。参考链接似乎无效。你能否更改为https://www.gnu.org/software/libc/manual/html_node/Finding-Tokens-in-a-String.html或其他有效链接? - jian

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