Может ли кто-нибудь объяснить мне, в чем разница между strtok() и strsep()? Какие у них преимущества и недостатки? И почему я должен выбрать одно вместо другого.

31
mizuki 28 Авг 2011 в 06:11

3 ответа

Лучший ответ

Из руководства библиотеки GNU C - Поиск токенов в строке:

Одно различие между strsep и strtok_r заключается в том, что если входная строка содержит более одного символа из разделителя в строке, strsep возвращает пустую строку для каждой пары символов из разделителя. Это означает, что программа обычно должна проверять, возвращает ли strsep пустую строку перед ее обработкой.

10
Cristian Ciupitu 6 Дек 2014 в 23:35

Одно из основных различий между strtok() и strsep() заключается в том, что strtok() стандартизирован (стандартом C и, следовательно, также POSIX), но strsep() не стандартизирован (C или POSIX; он доступен в библиотеке GNU C и основан на BSD). Таким образом, переносимый код с большей вероятностью будет использовать strtok(), чем strsep().

Другое отличие состоит в том, что вызовы функции strsep() для разных строк могут чередоваться, тогда как вы не можете этого сделать с strtok() (хотя вы можете с strtok_r()). Таким образом, использование strsep() в библиотеке не приводит к случайному повреждению другого кода, тогда как использование strtok() в библиотечной функции должно быть задокументировано, поскольку другой код, использующий strtok() одновременно, не может вызвать библиотеку. функция.

Страница руководства для strsep() на ядре .org говорит:

Функция strsep () была введена как замена strtok (3), поскольку последний не может обрабатывать пустые поля.

Таким образом, другое важное различие было подчеркнуто Джорджем Гаалом в своем ответе; 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(), если он доступен.
  • Вы можете использовать strtok_r() POSIX, если он доступен.
  • Вы можете использовать strtok_s() от Microsoft, если он доступен.
  • Номинально вы можете использовать функцию strtok_s() приложения K.3.7.3.1 ISO / IEC 9899: 2011, но ее интерфейс отличается как от 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);

Microsoft 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 аргумента, а не 3, как в двух других вариантах на strtok().

58
Jonathan Leffler 30 Сен 2017 в 21:17

Первое различие между 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

В выводе вы можете видеть токены "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>) между bbb и ccc. Эти две пустые строки предназначены для "--" между "bbb" и "ccc". Когда strsep() находит символ разделителя ' ' после "bbb", он заменяет символ разделителя на символ '\0' и возвращает "bbb". После этого strsep() нашел еще один символ-разделитель '-'. Затем он заменил символ-разделитель на символ '\0' и вернул пустую строку. То же самое для следующего символа-разделителя.

Смежные символы-разделители указываются, когда strsep() возвращает указатель на нулевой символ (то есть символ со значением '\0').

strsep() изменяет входную строку, а также указатель , адрес которого передан в качестве первого аргумента для 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, strtok() возвращает NULL из-за статического указателя, указывающего на 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.


Для справки:

6
H.S. 9 Апр 2020 в 04:17