W jaki sposób strtok () dzieli ciąg na tokeny w C?

114

Proszę wyjaśnij mi działanie strtok()funkcji. Instrukcja mówi, że dzieli ciąg na tokeny. Nie jestem w stanie zrozumieć na podstawie instrukcji, co to właściwie robi.

Dodałem zegarki włączone stri *pchżeby sprawdzić jak działa, kiedy pojawiła się pierwsza pętla while, zawartość strbyła tylko "ta". W jaki sposób wyniki pokazane poniżej zostały wydrukowane na ekranie?

/* strtok example */
#include <stdio.h>
#include <string.h>

int main ()
{
  char str[] ="- This, a sample string.";
  char * pch;
  printf ("Splitting string \"%s\" into tokens:\n",str);
  pch = strtok (str," ,.-");
  while (pch != NULL)
  {
    printf ("%s\n",pch);
    pch = strtok (NULL, " ,.-");
  }
  return 0;
}

Wynik:

Dzielenie ciągu „- to jest przykładowy ciąg”. na żetony:
To
za
próba
strunowy
fuddin
źródło
5
strtok()modyfikuje swój argument, kończąc tokeny z wartością NUL przed zwróceniem. Jeśli spróbujesz zbadać cały bufor (str []), zobaczysz, że jest on modyfikowany między kolejnymi wywołaniami funkcji strtok().
Michael Foukarakis
Zamiast oglądać str, oglądać str[0], str[1], str[2], ...
PMG
@pmg: Widziałem, że str [0] i str [1] .str [1] powinny mieć wartość „\ 0”, ale była tam spacja.
fuddin
3
Szczerze mówiąc, nigdy nie zadałem sobie trudu, aby to sprawdzić, ale wyobrażam sobie, że przechowuje ostatni przekazany wskaźnik, wraz z pozycją, w której się zatrzymał. Następnie może po prostu kontynuować, jeśli wskaźnik ma wartość NULL, lub wyczyścić pozycję i zacząć od nowa, jeśli nie.
chris
7
@Firegun: zmienna statyczna .
DCoder

Odpowiedzi:

38

strtok()dzieli ciąg na tokeny. tj. zaczynając od jednego z ograniczników do następnego, będzie to twój jeden token. W twoim przypadku token początkowy będzie zaczynał się od „-” i kończył się następną spacją „”. Następnie następny token będzie zaczynał się od „” i kończył na „,”. Tutaj otrzymujesz „To” jako wyjście. Podobnie reszta ciągu zostaje podzielona na tokeny z przestrzeni do przestrzeni i ostatecznie kończy ostatni token znakiem „.”

Sachin Shanbhag
źródło
warunek końcowy dla jednego tokena staje się tokenem początkowym następnego? a także czy w miejscu warunku końcowego znajduje się znak nul?
fuddin
1
@ fahad- Tak, wszystkie posiadane separatory zostaną zastąpione przez znak NUL, tak jak sugerowały to inne osoby.
Sachin Shanbhag
Jeśli wszystkie separatory zostaną zastąpione wartością Nul, dlaczego ciąg zawiera „-this”? Powinien zawierać „\ 0”
fuddin
2
@fahad - Zastępuje tylko znaki ogranicznika wartością NUL, a nie wszystkie znaki między separatorami. To rodzaj dzielenia łańcucha na wiele tokenów. Otrzymujesz „to”, ponieważ znajduje się między dwoma określonymi ogranicznikami, a nie „-to”.
Sachin Shanbhag
1
@Fahad - Tak, absolutnie. O ile rozumiem, wszystkie spacje, „,” i „-” są zastępowane przez NUL, ponieważ zostały one określone jako ograniczniki.
Sachin Shanbhag
214

funkcja środowiska wykonawczego strtok działa w ten sposób

przy pierwszym wywołaniu strtok podajesz ciąg, który chcesz tokenizować

char s[] = "this is a string";

w powyższym miejscu wydaje się być dobrym separatorem między słowami, więc użyjmy tego:

char* p = strtok(s, " ");

dzieje się teraz to, że 's' jest przeszukiwane do momentu znalezienia znaku spacji, zwracany jest pierwszy token ('this'), a p wskazuje na ten token (string)

aby uzyskać następny token i kontynuować z tym samym ciągiem, jako pierwszy argument przekazywana jest wartość NULL, ponieważ strtok utrzymuje statyczny wskaźnik do poprzedniego przekazanego ciągu:

p = strtok(NULL," ");

p wskazuje teraz na „jest”

i tak dalej, aż nie będzie można znaleźć więcej spacji, ostatni ciąg jest zwracany jako ostatni token „ciąg”.

wygodniej byłoby napisać to w ten sposób, aby wydrukować wszystkie tokeny:

for (char *p = strtok(s," "); p != NULL; p = strtok(NULL, " "))
{
  puts(p);
}

EDYTOWAĆ:

Jeśli chcesz przechowywać zwracane wartości strtok, musisz skopiować token do innego bufora, np. strdup(p);Ponieważ oryginalny ciąg (wskazywany przez statyczny wskaźnik wewnątrz strtok) jest modyfikowany między iteracjami, aby zwrócić token.

AndersK
źródło
Więc w rzeczywistości nie umieszcza znaku nul między łańcuchem? Dlaczego mój zegarek pokazuje, że w ciągu pozostaje tylko „THIS”?
fuddin
4
rzeczywiście zastępuje „” znaleziony przez „\ 0”. I nie przywraca '' później, więc twój ciąg jest zrujnowany na dobre.
33
+1 dla bufora statycznego, tego nie rozumiem
IEatBagels
1
Bardzo ważnym szczegółem, którego brakuje w wierszu „pierwszy token jest zwracany i pwskazuje na ten token” , jest konieczność strtokzmiany oryginalnego ciągu przez umieszczenie pustych znaków w miejscu ogranicznika (w przeciwnym razie inne funkcje łańcuchowe nie wiedziałyby, gdzie żeton się kończy). Śledzi również stan za pomocą zmiennej statycznej.
Groo
@Groo Myślę, że już to dodałem w Edycji, którą zrobiłem w 2017, ale masz rację.
AndersK
25

strtokutrzymuje statyczne, wewnętrzne odniesienie wskazujące na następny dostępny token w ciągu; jeśli przekażesz mu wskaźnik NULL, będzie działać na podstawie tego wewnętrznego odniesienia.

To jest powód, dla którego strtoknie powraca; jak tylko przekażesz mu nowy wskaźnik, stare wewnętrzne odniesienie zostanie zbite.

John Bode
źródło
Co masz na myśli, mówiąc o starej wewnętrznej wzmiance o „byciu wbijanym”? Czy masz na myśli „nadpisane”?
ylun.ca
1
@ ylun.ca: tak, o to mi chodzi.
John Bode
10

strtoknie zmienia samego parametru ( str). Przechowuje ten wskaźnik (w lokalnej zmiennej statycznej). Następnie może zmienić to, na co wskazuje ten parametr w kolejnych wywołaniach, bez zwracania parametru. (I może przesuwać ten wskaźnik, który zachował, niezależnie od potrzeb, aby wykonywać swoje operacje).

Ze strony POSIX strtok:

Ta funkcja wykorzystuje pamięć statyczną do śledzenia bieżącej pozycji łańcucha między wywołaniami.

Istnieje wariant bezpieczny dla wątków ( strtok_r), który nie wykonuje tego typu magii.

Mata
źródło
2
Cóż, funkcje biblioteki C pochodzą z dawnych czasów, kiedy wątków w ogóle nie było na obrazie (które zaczęło istnieć dopiero w 2011 roku, jeśli chodzi o standard C), więc ponowne wejście nie było tak naprawdę ważne ( Zgaduję). Ta statyczna lokalność sprawia, że ​​funkcja jest „łatwa w użyciu” (dla pewnej definicji „łatwej”). Jak ctimezwrot statycznego sznurka - praktyczny (nikt nie musi się zastanawiać, kto powinien go zwolnić), ale nie należy go ponownie wprowadzać i potykać, jeśli nie jesteś tego bardzo świadomy.
Mat
To jest złe: „ strtoknie zmienia samego parametru ( str)”. puts(str);wypisuje "- To" od czasu strtokmodyfikacji str.
MarredCheese
1
@MarredCheese: przeczytaj ponownie. Nie modyfikuje wskaźnika. Modyfikuje dane wskazywane przez wskaźnik (tj. Dane ciągów znaków)
Mat.
O ok, nie zdawałem sobie sprawy, że o to ci chodzi. Zgoda.
MarredCheese
8

Przy pierwszym wywołaniu podajesz ciąg do tokenizacji strtok. A następnie, aby uzyskać następujące tokeny, po prostu podajesz NULLtej funkcji, o ile zwraca ona NULLwskaźnik inny niż wskaźnik.

strtokFunkcja zapisuje ciąg najpierw podane podczas nazwać. (Co jest naprawdę niebezpieczne w przypadku aplikacji wielowątkowych)

Tibur
źródło
8

strtok tokenizuje łańcuch, tj. konwertuje go na serię podciągów.

Robi to, wyszukując ograniczniki oddzielające te tokeny (lub podciągi). I określasz ograniczniki. W twoim przypadku chcesz „” lub „,” lub „”. lub „-” jako separator.

Model programowania służący do wyodrębniania tych tokenów polega na ręcznym obrysowaniu głównego ciągu i zestawu ograniczników. Następnie wywołujesz go wielokrotnie i za każdym razem strtok zwróci następny znaleziony token. Dopóki nie osiągnie końca głównego ciągu, kiedy zwraca wartość null. Inną zasadą jest to, że przekazujesz ciąg tylko za pierwszym razem, a NULL przy kolejnych. Jest to sposób na poinformowanie strtok, czy rozpoczynasz nową sesję tokenizacji z nowym ciągiem, czy też pobierasz tokeny z poprzedniej sesji tokenizacji. Zauważ, że strtok pamięta swój stan dla sesji tokenizacji. Z tego powodu nie jest ponownie wprowadzany ani bezpieczny dla wątków (zamiast tego powinieneś używać strtok_r). Inną rzeczą, którą należy wiedzieć, jest to, że faktycznie modyfikuje oryginalny ciąg. Zapisuje „\ 0” dla znalezionych separatorów.

Jeden ze sposobów, aby wywołać strtok, zwięźle, jest następujący:

char str[] = "this, is the string - I want to parse";
char delim[] = " ,-";
char* token;

for (token = strtok(str, delim); token; token = strtok(NULL, delim))
{
    printf("token=%s\n", token);
}

Wynik:

this
is
the
string
I
want
to
parse
Ziffusion
źródło
5

strtok modyfikuje swój ciąg wejściowy. Umieszcza w nim znaki null ('\ 0'), aby zwróciło bity oryginalnego ciągu jako tokeny. W rzeczywistości strtok nie przydziela pamięci. Możesz to lepiej zrozumieć, jeśli narysujesz ciąg jako sekwencję pudełek.

xpmatteo
źródło
3

Aby zrozumieć, jak strtok()działa, należy najpierw wiedzieć, czym jest zmienna statyczna . Ten link wyjaśnia to całkiem dobrze ....

Kluczem do działania strtok()jest zachowanie lokalizacji ostatniego separatora między kolejnymi wywołaniami (dlatego strtok()kontynuuje analizowanie bardzo oryginalnego ciągu, który jest do niego przekazywany, gdy jest wywoływany za pomocą a null pointerw kolejnych wywołaniach).

Przyjrzyj się mojej własnej strtok()implementacji, tzw. zStrtok(), Która ma odrobinę inną funkcjonalność niż ta dostarczona przezstrtok()

char *zStrtok(char *str, const char *delim) {
    static char *static_str=0;      /* var to store last address */
    int index=0, strlength=0;           /* integers for indexes */
    int found = 0;                  /* check if delim is found */

    /* delimiter cannot be NULL
    * if no more char left, return NULL as well
    */
    if (delim==0 || (str == 0 && static_str == 0))
        return 0;

    if (str == 0)
        str = static_str;

    /* get length of string */
    while(str[strlength])
        strlength++;

    /* find the first occurance of delim */
    for (index=0;index<strlength;index++)
        if (str[index]==delim[0]) {
            found=1;
            break;
        }

    /* if delim is not contained in str, return str */
    if (!found) {
        static_str = 0;
        return str;
    }

    /* check for consecutive delimiters
    *if first char is delim, return delim
    */
    if (str[0]==delim[0]) {
        static_str = (str + 1);
        return (char *)delim;
    }

    /* terminate the string
    * this assignmetn requires char[], so str has to
    * be char[] rather than *char
    */
    str[index] = '\0';

    /* save the rest of the string */
    if ((str + index + 1)!=0)
        static_str = (str + index + 1);
    else
        static_str = 0;

        return str;
}

A oto przykład użycia

  Example Usage
      char str[] = "A,B,,,C";
      printf("1 %s\n",zStrtok(s,","));
      printf("2 %s\n",zStrtok(NULL,","));
      printf("3 %s\n",zStrtok(NULL,","));
      printf("4 %s\n",zStrtok(NULL,","));
      printf("5 %s\n",zStrtok(NULL,","));
      printf("6 %s\n",zStrtok(NULL,","));

  Example Output
      1 A
      2 B
      3 ,
      4 ,
      5 C
      6 (null)

Kod pochodzi z biblioteki przetwarzającej ciągi, którą prowadzę na Github , zwanej zString. Spójrz na kod, a nawet pomóż :) https://github.com/fnoyanisi/zString

fnisi
źródło
3

W ten sposób zaimplementowałem strtok, niezbyt dobrze, ale po 2 godzinach pracy nad nim w końcu udało się. Obsługuje wiele ograniczników.

#include "stdafx.h"
#include <iostream>
using namespace std;

char* mystrtok(char str[],char filter[]) 
{
    if(filter == NULL) {
        return str;
    }
    static char *ptr = str;
    static int flag = 0;
    if(flag == 1) {
        return NULL;
    }
    char* ptrReturn = ptr;
    for(int j = 0; ptr != '\0'; j++) {
        for(int i=0 ; filter[i] != '\0' ; i++) {
            if(ptr[j] == '\0') {
                flag = 1;
                return ptrReturn;
            }
            if( ptr[j] == filter[i]) {
                ptr[j] = '\0';
                ptr+=j+1;
                return ptrReturn;
            }
        }
    }
    return NULL;
}

int _tmain(int argc, _TCHAR* argv[])
{
    char str[200] = "This,is my,string.test";
    char *ppt = mystrtok(str,", .");
    while(ppt != NULL ) {
        cout<< ppt << endl;
        ppt = mystrtok(NULL,", ."); 
    }
    return 0;
}
Dipak
źródło
1

Oto moja implementacja, która używa tablicy skrótów jako separatora, co oznacza, że ​​O (n) zamiast O (n ^ 2) (tutaj jest link do kodu) :

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

#define DICT_LEN 256

int *create_delim_dict(char *delim)
{
    int *d = (int*)malloc(sizeof(int)*DICT_LEN);
    memset((void*)d, 0, sizeof(int)*DICT_LEN);

    int i;
    for(i=0; i< strlen(delim); i++) {
        d[delim[i]] = 1;
    }
    return d;
}



char *my_strtok(char *str, char *delim)
{

    static char *last, *to_free;
    int *deli_dict = create_delim_dict(delim);

    if(!deli_dict) {
        /*this check if we allocate and fail the second time with entering this function */
        if(to_free) {
            free(to_free);
        }
        return NULL;
    }

    if(str) {
        last = (char*)malloc(strlen(str)+1);
        if(!last) {
            free(deli_dict);
            return NULL;
        }
        to_free = last;
        strcpy(last, str);
    }

    while(deli_dict[*last] && *last != '\0') {
        last++;
    }
    str = last;
    if(*last == '\0') {
        free(deli_dict);
        free(to_free);
        deli_dict = NULL;
        to_free = NULL;
        return NULL;
    }
    while (*last != '\0' && !deli_dict[*last]) {
        last++;
    }

    *last = '\0';
    last++;

    free(deli_dict);
    return str;
}

int main()
{
    char * str = "- This, a sample string.";
    char *del = " ,.-";
    char *s = my_strtok(str, del);
    while(s) {
        printf("%s\n", s);
        s = my_strtok(NULL, del);
    }
    return 0;
}
Kohn1001
źródło
1

strtok () przechowuje wskaźnik w zmiennej statycznej, w której ostatnio przerwałeś, więc w drugim wywołaniu, kiedy przekażemy wartość null, strtok () pobiera wskaźnik ze zmiennej statycznej.

Jeśli podasz tę samą nazwę ciągu, zacznie się ona ponownie od początku.

Ponadto strtok () jest destrukcyjna, tj. Wprowadza zmiany w oryginalnym ciągu znaków. więc upewnij się, że zawsze masz kopię oryginalnego.

Jeszcze jednym problemem związanym z używaniem strtok () jest to, że gdy przechowuje adres w zmiennych statycznych, w programowaniu wielowątkowym wywoływanie strtok () więcej niż raz spowoduje błąd. W tym celu użyj strtok_r ().

Vaibhav
źródło
0

Dla tych, którzy nadal mają trudności ze zrozumieniem tej strtok()funkcji, spójrz na ten przykład w języku Python , jest to świetne narzędzie do wizualizacji kodu w C (lub C ++, Python ...).

W przypadku zerwania linku, wklej:

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

int main()
{
    char s[] = "Hello, my name is? Matthew! Hey.";
    char* p;
    for (char *p = strtok(s," ,?!."); p != NULL; p = strtok(NULL, " ,?!.")) {
      puts(p);
    }
    return 0;
}

Kredyty trafiają do Andersa K.


źródło
0

możesz zeskanować tablicę znaków w poszukiwaniu tokena, jeśli go znalazłeś, po prostu wypisz nową linię, w przeciwnym razie wypisz znak.

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

int main()
{
    char *s;
    s = malloc(1024 * sizeof(char));
    scanf("%[^\n]", s);
    s = realloc(s, strlen(s) + 1);
    int len = strlen(s);
    char delim =' ';
    for(int i = 0; i < len; i++) {
        if(s[i] == delim) {
            printf("\n");
        }
        else {
            printf("%c", s[i]);
        }
    }
    free(s);
    return 0;
}
Fahad Alotaibi
źródło
0

To jest fragment kodu, który pomoże lepiej zrozumieć ten temat.

Drukowanie tokenów

Zadanie: Biorąc pod uwagę zdanie, wypisz każde słowo zdania w nowej linii.

char *s;
s = malloc(1024 * sizeof(char));
scanf("%[^\n]", s);
s = realloc(s, strlen(s) + 1);
//logic to print the tokens of the sentence.
for (char *p = strtok(s," "); p != NULL; p = strtok(NULL, " "))
{
    printf("%s\n",p);
}

Wejście: How is that

Wynik:

How
is
that

Objaśnienie: Więc tutaj używana jest funkcja "strtok ()" i jest ona iterowana przy użyciu pętli for, aby wydrukować tokeny w oddzielnych wierszach.

Funkcja przyjmie parametry jako „ciąg” i „punkt przerwania” i przerwie łańcuch w tych punktach przerwania i utworzy tokeny. Teraz te tokeny są przechowywane w „p” i są dalej używane do drukowania.

tr_abhishek
źródło
Myślę, że wyjaśnienie na przykładzie jest znacznie lepsze niż odwoływanie się do jakiegoś dokumentu.
tr_abhishek