Podziel ciąg z ogranicznikami w C


155

Jak napisać funkcję do dzielenia i zwracania tablicy dla ciągu z ogranicznikami w języku programowania C?

char* str = "JAN,FEB,MAR,APR,MAY,JUN,JUL,AUG,SEP,OCT,NOV,DEC";
str_split(str,',');

25
Możesz użyć strtokfunkcji z biblioteki standardowej, aby osiągnąć to samo.
Daniel Kamil Kozar


Komentarz ... kluczową kwestią dla strtok()funkcji rodziny jest zrozumienie static variablesw C. tj. Jak zachowują się pomiędzy kolejnymi wywołaniami funkcji, w których są używane. Zobacz mój kod poniżej
fnisi

Odpowiedzi:


165

Możesz użyć tej strtok()funkcji, aby podzielić ciąg (i określić separator). Zauważ, że strtok()zmodyfikuje przekazany do niego ciąg. Jeśli oryginalny ciąg jest wymagany w innym miejscu, wykonaj jego kopię i przekaż kopię do strtok().

EDYTOWAĆ:

Przykład (pamiętaj, że nie obsługuje kolejnych separatorów, na przykład „JAN ,,, FEB, MAR”):

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

char** str_split(char* a_str, const char a_delim)
{
    char** result    = 0;
    size_t count     = 0;
    char* tmp        = a_str;
    char* last_comma = 0;
    char delim[2];
    delim[0] = a_delim;
    delim[1] = 0;

    /* Count how many elements will be extracted. */
    while (*tmp)
    {
        if (a_delim == *tmp)
        {
            count++;
            last_comma = tmp;
        }
        tmp++;
    }

    /* Add space for trailing token. */
    count += last_comma < (a_str + strlen(a_str) - 1);

    /* Add space for terminating null string so caller
       knows where the list of returned strings ends. */
    count++;

    result = malloc(sizeof(char*) * count);

    if (result)
    {
        size_t idx  = 0;
        char* token = strtok(a_str, delim);

        while (token)
        {
            assert(idx < count);
            *(result + idx++) = strdup(token);
            token = strtok(0, delim);
        }
        assert(idx == count - 1);
        *(result + idx) = 0;
    }

    return result;
}

int main()
{
    char months[] = "JAN,FEB,MAR,APR,MAY,JUN,JUL,AUG,SEP,OCT,NOV,DEC";
    char** tokens;

    printf("months=[%s]\n\n", months);

    tokens = str_split(months, ',');

    if (tokens)
    {
        int i;
        for (i = 0; *(tokens + i); i++)
        {
            printf("month=[%s]\n", *(tokens + i));
            free(*(tokens + i));
        }
        printf("\n");
        free(tokens);
    }

    return 0;
}

Wynik:

$ ./main.exe
months=[JAN,FEB,MAR,APR,MAY,JUN,JUL,AUG,SEP,OCT,NOV,DEC]

month=[JAN]
month=[FEB]
month=[MAR]
month=[APR]
month=[MAY]
month=[JUN]
month=[JUL]
month=[AUG]
month=[SEP]
month=[OCT]
month=[NOV]
month=[DEC]

60
Cześć! strtokjest oznaczony jako przestarzały przez strsep(3)na stronie podręcznika .
osgx

4
Ponieważ może to być kanoniczne pytanie / odpowiedź na temat przepełnienia stosu, czy nie ma żadnych zastrzeżeń dotyczących wielowątkowości przy użyciu strtok?
Peter Mortensen

3
@osgx Według tej strony strsepjest zamiennikiem strtok, ale strtokjest preferowany ze względu na przenośność. Tak więc, jeśli nie potrzebujesz obsługi pustych pól lub dzielenia wielu ciągów jednocześnie, strtokjest lepszym wyborem.

4
@Dojo: Pamięta to; to jeden z powodów, dla których jest to problematyczne. Byłoby lepiej użyć strtok_s()(Microsoft, C11 Annex K, opcjonalne) lub strtok_r()(POSIX) niż zwykłego strtok(). Zwykłe strtok()jest złe w funkcji biblioteki. Żadna funkcja wywołująca funkcję biblioteczną nie może być używana strtok()w tym czasie i żadna funkcja wywoływana przez funkcję biblioteki nie może wywołać strtok().
Jonathan Leffler

3
Tylko uwaga, która strtok()nie jest bezpieczna wątkowo (z powodów, o których wspomniał @JonathanLeffler) i dlatego cała ta funkcja nie jest bezpieczna wątkowo. Jeśli spróbujesz użyć tego w wydeptanym środowisku, uzyskasz nieregularne i nieprzewidywalne wyniki. Wymiana w strtok()celu strtok_r()rozwiązania tego problemu.
Sean W.

70

Myślę, że strsepnadal jest to najlepsze narzędzie do tego:

while ((token = strsep(&str, ","))) my_fn(token);

To dosłownie jedna linia, która dzieli ciąg.

Dodatkowe nawiasy to element stylistyczny wskazujący, że celowo testujemy wynik przypisania, a nie operator równości ==.

Aby ten wzór zadziałał, tokeni stroba mają typ char *. Jeśli zacząłeś od literału ciągu, najpierw chciałbyś zrobić jego kopię:

// More general pattern:
const char *my_str_literal = "JAN,FEB,MAR";
char *token, *str, *tofree;

tofree = str = strdup(my_str_literal);  // We own str's memory now.
while ((token = strsep(&str, ","))) my_fn(token);
free(tofree);

Jeśli dwa separatory pojawią się razem w str, otrzymasz tokenwartość, która jest pustym ciągiem. Wartość strjest modyfikowana w taki sposób, że każdy napotkany separator jest nadpisywany bajtem zerowym - kolejny dobry powód, aby skopiować najpierw analizowany ciąg.

W komentarzu ktoś zasugerował, że strtokjest to lepsze niż to, strsepże strtokjest bardziej przenośne. Ubuntu i Mac OS X mają strsep; można zgadnąć, że inne systemy unixy również. Windows brakuje strsep, ale ma to, strbrkco umożliwia tę krótką i słodką strsepwymianę:

char *strsep(char **stringp, const char *delim) {
  if (*stringp == NULL) { return NULL; }
  char *token_start = *stringp;
  *stringp = strpbrk(token_start, delim);
  if (*stringp) {
    **stringp = '\0';
    (*stringp)++;
  }
  return token_start;
}

Tutaj jest dobrym wyjaśnieniem strsepvs strtok. Za i przeciw można ocenić subiektywnie; jednak myślę, że jest to wymowny znak, który strsepzostał zaprojektowany jako zamiennik strtok.


3
Dokładniej mówiąc o przenośności: nie jest to POSIX 7 , ale wyprowadzone z BSD i zaimplementowane w glibc .
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

Właśnie miałem zapytać ... C Pelle'a ma strdup (), ale nie ma strsep ().
rdtsc

1
dlaczego ten tofreejest wolny, a nie str?
Sdlion

1
Nie możesz zwolnić, strponieważ jego wartość można zmienić, wywołując numer strsep(). Wartość tofreekonsekwentnie wskazuje początek pamięci, którą chcesz zwolnić.
Tyler

26

String tokenizer ten kod powinien skierować Cię we właściwym kierunku.

int main(void) {
  char st[] ="Where there is will, there is a way.";
  char *ch;
  ch = strtok(st, " ");
  while (ch != NULL) {
  printf("%s\n", ch);
  ch = strtok(NULL, " ,");
  }
  getch();
  return 0;
}

13

Poniższa metoda wykona całą pracę (alokację pamięci, liczenie długości) za Ciebie. Więcej informacji i opis można znaleźć tutaj - Implementacja metody Java String.split () w celu podzielenia łańcucha C.

int split (const char *str, char c, char ***arr)
{
    int count = 1;
    int token_len = 1;
    int i = 0;
    char *p;
    char *t;

    p = str;
    while (*p != '\0')
    {
        if (*p == c)
            count++;
        p++;
    }

    *arr = (char**) malloc(sizeof(char*) * count);
    if (*arr == NULL)
        exit(1);

    p = str;
    while (*p != '\0')
    {
        if (*p == c)
        {
            (*arr)[i] = (char*) malloc( sizeof(char) * token_len );
            if ((*arr)[i] == NULL)
                exit(1);

            token_len = 0;
            i++;
        }
        p++;
        token_len++;
    }
    (*arr)[i] = (char*) malloc( sizeof(char) * token_len );
    if ((*arr)[i] == NULL)
        exit(1);

    i = 0;
    p = str;
    t = ((*arr)[i]);
    while (*p != '\0')
    {
        if (*p != c && *p != '\0')
        {
            *t = *p;
            t++;
        }
        else
        {
            *t = '\0';
            i++;
            t = ((*arr)[i]);
        }
        p++;
    }

    return count;
}

Jak tego użyć:

int main (int argc, char ** argv)
{
    int i;
    char *s = "Hello, this is a test module for the string splitting.";
    int c = 0;
    char **arr = NULL;

    c = split(s, ' ', &arr);

    printf("found %d tokens.\n", c);

    for (i = 0; i < c; i++)
        printf("string #%d: %s\n", i, arr[i]);

    return 0;
}

4
Huh Three star Programmer :)) To brzmi interesująco.
Michi

Kiedy to robię, albo dodaje zbyt dużo do ostatniego tokenu, albo przydziela mu za dużo pamięci. Oto wynik: found 10 tokens. string #0: Hello, string #1: this string #2: is string #3: a string #4: test string #5: module string #6: for string #7: the string #8: string string #9: splitting.¢
KeizerHarm

2
W tym przykładzie występują liczne wycieki pamięci. Każdy, kto to czyta, nie powinien stosować tego podejścia. Zamiast tego preferuj metody tokenizacji strtok lub strsep.
Jorma Rebane

7

Oto moje dwa centy:

int split (const char *txt, char delim, char ***tokens)
{
    int *tklen, *t, count = 1;
    char **arr, *p = (char *) txt;

    while (*p != '\0') if (*p++ == delim) count += 1;
    t = tklen = calloc (count, sizeof (int));
    for (p = (char *) txt; *p != '\0'; p++) *p == delim ? *t++ : (*t)++;
    *tokens = arr = malloc (count * sizeof (char *));
    t = tklen;
    p = *arr++ = calloc (*(t++) + 1, sizeof (char *));
    while (*txt != '\0')
    {
        if (*txt == delim)
        {
            p = *arr++ = calloc (*(t++) + 1, sizeof (char *));
            txt++;
        }
        else *p++ = *txt++;
    }
    free (tklen);
    return count;
}

Stosowanie:

char **tokens;
int count, i;
const char *str = "JAN,FEB,MAR,APR,MAY,JUN,JUL,AUG,SEP,OCT,NOV,DEC";

count = split (str, ',', &tokens);
for (i = 0; i < count; i++) printf ("%s\n", tokens[i]);

/* freeing tokens */
for (i = 0; i < count; i++) free (tokens[i]);
free (tokens);

3
oh boi, trzy wskazówki! Już boję się go używać lol to tylko ja, nie jestem zbyt dobry ze wskazówkami w c.
Hafiz Temuri

Dzięki stary, wszystkie powyższe odpowiedzi strtok nie zadziałały w moim przypadku nawet po wielu wysiłkach, a twój kod działa jak urok!
hmmftg

4

W powyższym przykładzie byłby sposób na zwrócenie tablicy ciągów zakończonych znakiem null (tak jak chcesz) w miejscu w ciągu. Nie dałoby to jednak możliwości przekazania ciągu literału, ponieważ musiałby zostać zmodyfikowany przez funkcję:

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

char** str_split( char* str, char delim, int* numSplits )
{
    char** ret;
    int retLen;
    char* c;

    if ( ( str == NULL ) ||
        ( delim == '\0' ) )
    {
        /* Either of those will cause problems */
        ret = NULL;
        retLen = -1;
    }
    else
    {
        retLen = 0;
        c = str;

        /* Pre-calculate number of elements */
        do
        {
            if ( *c == delim )
            {
                retLen++;
            }

            c++;
        } while ( *c != '\0' );

        ret = malloc( ( retLen + 1 ) * sizeof( *ret ) );
        ret[retLen] = NULL;

        c = str;
        retLen = 1;
        ret[0] = str;

        do
        {
            if ( *c == delim )
            {
                ret[retLen++] = &c[1];
                *c = '\0';
            }

            c++;
        } while ( *c != '\0' );
    }

    if ( numSplits != NULL )
    {
        *numSplits = retLen;
    }

    return ret;
}

int main( int argc, char* argv[] )
{
    const char* str = "JAN,FEB,MAR,APR,MAY,JUN,JUL,AUG,SEP,OCT,NOV,DEC";

    char* strCpy;
    char** split;
    int num;
    int i;

    strCpy = malloc( strlen( str ) * sizeof( *strCpy ) );
    strcpy( strCpy, str );

    split = str_split( strCpy, ',', &num );

    if ( split == NULL )
    {
        puts( "str_split returned NULL" );
    }
    else
    {
        printf( "%i Results: \n", num );

        for ( i = 0; i < num; i++ )
        {
            puts( split[i] );
        }
    }

    free( split );
    free( strCpy );

    return 0;
}

Prawdopodobnie jest na to lepszy sposób, ale masz pomysł.


3

Ta funkcja pobiera łańcuch char * i dzieli go przez separator. W rzędzie może znajdować się wiele separatorów. Zwróć uwagę, że funkcja modyfikuje oryginalny łańcuch. Musisz najpierw wykonać kopię oryginalnego ciągu, jeśli chcesz, aby oryginał pozostał niezmieniony. Ta funkcja nie używa żadnych wywołań funkcji cstring, więc może być trochę szybsza niż inne. Jeśli nie zależy Ci na alokacji pamięci, możesz przydzielić sub_strings na górze funkcji z rozmiarem strlen (src_str) / 2 i (tak jak wspomniana „wersja” c ++) pominąć dolną połowę funkcji. Jeśli to zrobisz, funkcja zostanie zredukowana do O (N), ale zoptymalizowany sposób pamięci pokazany poniżej to O (2N).

Funkcja:

char** str_split(char *src_str, const char deliminator, size_t &num_sub_str){
  //replace deliminator's with zeros and count how many
  //sub strings with length >= 1 exist
  num_sub_str = 0;
  char *src_str_tmp = src_str;
  bool found_delim = true;
  while(*src_str_tmp){
    if(*src_str_tmp == deliminator){
      *src_str_tmp = 0;
      found_delim = true;
    }
    else if(found_delim){ //found first character of a new string
      num_sub_str++;
      found_delim = false;
      //sub_str_vec.push_back(src_str_tmp); //for c++
    }
    src_str_tmp++;
  }
  printf("Start - found %d sub strings\n", num_sub_str);
  if(num_sub_str <= 0){
    printf("str_split() - no substrings were found\n");
    return(0);
  }

  //if you want to use a c++ vector and push onto it, the rest of this function
  //can be omitted (obviously modifying input parameters to take a vector, etc)

  char **sub_strings = (char **)malloc( (sizeof(char*) * num_sub_str) + 1);
  const char *src_str_terminator = src_str_tmp;
  src_str_tmp = src_str;
  bool found_null = true;
  size_t idx = 0;
  while(src_str_tmp < src_str_terminator){
    if(!*src_str_tmp) //found a NULL
      found_null = true;
    else if(found_null){
      sub_strings[idx++] = src_str_tmp;
      //printf("sub_string_%d: [%s]\n", idx-1, sub_strings[idx-1]);
      found_null = false;
    }
    src_str_tmp++;
  }
  sub_strings[num_sub_str] = NULL;

  return(sub_strings);
}

Jak tego użyć:

  char months[] = "JAN,FEB,MAR,APR,MAY,JUN,JUL,AUG,SEP,OCT,NOV,DEC";
  char *str = strdup(months);
  size_t num_sub_str;
  char **sub_strings = str_split(str, ',', num_sub_str);
  char *endptr;
  if(sub_strings){
    for(int i = 0; sub_strings[i]; i++)
      printf("[%s]\n", sub_strings[i]);
  }
  free(sub_strings);
  free(str);

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

/**
 *  splits str on delim and dynamically allocates an array of pointers.
 *
 *  On error -1 is returned, check errno
 *  On success size of array is returned, which may be 0 on an empty string
 *  or 1 if no delim was found.  
 *
 *  You could rewrite this to return the char ** array instead and upon NULL
 *  know it's an allocation problem but I did the triple array here.  Note that
 *  upon the hitting two delim's in a row "foo,,bar" the array would be:
 *  { "foo", NULL, "bar" } 
 * 
 *  You need to define the semantics of a trailing delim Like "foo," is that a
 *  2 count array or an array of one?  I choose the two count with the second entry
 *  set to NULL since it's valueless.
 *  Modifies str so make a copy if this is a problem
 */
int split( char * str, char delim, char ***array, int *length ) {
  char *p;
  char **res;
  int count=0;
  int k=0;

  p = str;
  // Count occurance of delim in string
  while( (p=strchr(p,delim)) != NULL ) {
    *p = 0; // Null terminate the deliminator.
    p++; // Skip past our new null
    count++;
  }

  // allocate dynamic array
  res = calloc( 1, count * sizeof(char *));
  if( !res ) return -1;

  p = str;
  for( k=0; k<count; k++ ){
    if( *p ) res[k] = p;  // Copy start of string
    p = strchr(p, 0 );    // Look for next null
    p++; // Start of next string
  }

  *array = res;
  *length = count;

  return 0;
}

char str[] = "JAN,FEB,MAR,APR,MAY,JUN,JUL,AUG,SEP,OCT,NOV,DEC,";

int main() {
  char **res;
  int k=0;
  int count =0;
  int rc;

  rc = split( str, ',', &res, &count );
  if( rc ) {
    printf("Error: %s errno: %d \n", strerror(errno), errno);
  }

  printf("count: %d\n", count );
  for( k=0; k<count; k++ ) {
    printf("str: %s\n", res[k]);
  }

  free(res );
  return 0;
}

3

Poniżej moja strtok()implementacja z biblioteki zString . zstring_strtok()różni się od biblioteki standardowej strtok()sposobem, w jaki traktuje kolejne ograniczniki.

Wystarczy spojrzeć na poniższy kod, na pewno zorientujesz się, jak to działa (próbowałem użyć jak największej liczby komentarzy)

char *zstring_strtok(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;
}

Poniżej znajduje się przykładowe użycie ...

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

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

Bibliotekę można pobrać z Github https://github.com/fnoyanisi/zString


niezłe! tego właśnie szukałem.
Kostia Kim,

3

Myślę, że następujące rozwiązanie jest idealne:

  • Nie niszczy łańcucha źródłowego
  • Re-entrant - tzn. Możesz bezpiecznie zadzwonić do niego z dowolnego miejsca w jednym lub kilku wątkach
  • Przenośny
  • Prawidłowo obsługuje wiele separatorów
  • Szybko i wydajnie

Wyjaśnienie kodu:

  1. Zdefiniuj strukturę tokendo przechowywania adresu i długości tokenów
  2. Przydziel wystarczającą ilość pamięci dla nich w najgorszym przypadku, kiedy strskłada się w całości z separatorów, więc są strlen(str) + 1 tokeny, wszystkie puste ciągi
  3. Skanuj strrejestrując adres i długość każdego tokena
  4. Użyj tego, aby przydzielić tablicę wyjściową o odpowiednim rozmiarze, w tym dodatkową przestrzeń na NULLwartość wartownika
  5. Przydzielaj, kopiuj i dodawaj tokeny, korzystając z informacji o początku i długości - używaj, memcpyponieważ jest szybszy niż strcpyi znamy długości
  6. Zwolnij adres tokenu i tablicę długości
  7. Zwróć tablicę tokenów
typedef struct {
    const char *start;
    size_t len;
} token;

char **split(const char *str, char sep)
{
    char **array;
    unsigned int start = 0, stop, toks = 0, t;
    token *tokens = malloc((strlen(str) + 1) * sizeof(token));
    for (stop = 0; str[stop]; stop++) {
        if (str[stop] == sep) {
            tokens[toks].start = str + start;
            tokens[toks].len = stop - start;
            toks++;
            start = stop + 1;
        }
    }
    /* Mop up the last token */
    tokens[toks].start = str + start;
    tokens[toks].len = stop - start;
    toks++;
    array = malloc((toks + 1) * sizeof(char*));
    for (t = 0; t < toks; t++) {
        /* Calloc makes it nul-terminated */
        char *token = calloc(tokens[t].len + 1, 1);
        memcpy(token, tokens[t].start, tokens[t].len);
        array[t] = token;
    }
    /* Add a sentinel */
    array[t] = NULL; 
    free(tokens);
    return array;
}

mallocSprawdzanie notatek zostało pominięte ze względu na zwięzłość.

Ogólnie rzecz biorąc, nie zwracałbym tablicy char *wskaźników z takiej funkcji podziału, jak ta, ponieważ nakłada ona na wywołującego dużą odpowiedzialność za ich prawidłowe zwolnienie. Interfejs Wolę to, aby rozmówca przekazać funkcję zwrotną i nazywają to dla każdego powodu, jak opisałem tutaj: podzielić ciąg w C .


Dwukrotne skanowanie w poszukiwaniu separatorów jest prawdopodobnie bardziej wskazane niż przydzielanie potencjalnie dużej tablicy plików token.
chqrlie

2

Spróbuj tego użyć.

char** strsplit(char* str, const char* delim){
    char** res = NULL;
    char*  part;
    int i = 0;

    char* aux = strdup(str);

    part = strdup(strtok(aux, delim));

    while(part){
        res = (char**)realloc(res, (i + 1) * sizeof(char*));
        *(res + i) = strdup(part);

        part = strdup(strtok(NULL, delim));
        i++;
    }

    res = (char**)realloc(res, i * sizeof(char*));
    *(res + i) = NULL;

    return res;
}

2

Ta zoptymalizowana metoda tworzy (lub aktualizuje istniejącą) tablicę wskaźników w * wyniku i zwraca liczbę elementów w * count.

Użyj „max”, aby wskazać maksymalną liczbę oczekiwanych ciągów (gdy określasz istniejącą tablicę lub inny sposób), w przeciwnym razie ustaw ją na 0

Aby porównać z listą ograniczników, zdefiniuj separator jako znak * i zamień wiersz:

if (str[i]==delim) {

z dwoma następującymi wierszami:

 char *c=delim; while(*c && *c!=str[i]) c++;
 if (*c) {

Cieszyć się

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

char **split(char *str, size_t len, char delim, char ***result, unsigned long *count, unsigned long max) {
  size_t i;
  char **_result;

  // there is at least one string returned
  *count=1;

  _result= *result;

  // when the result array is specified, fill it during the first pass
  if (_result) {
    _result[0]=str;
  }

  // scan the string for delimiter, up to specified length
  for (i=0; i<len; ++i) {

    // to compare against a list of delimiters,
    // define delim as a string and replace 
    // the next line:
    //     if (str[i]==delim) {
    //
    // with the two following lines:
    //     char *c=delim; while(*c && *c!=str[i]) c++;
    //     if (*c) {
    //       
    if (str[i]==delim) {

      // replace delimiter with zero
      str[i]=0;

      // when result array is specified, fill it during the first pass
      if (_result) {
        _result[*count]=str+i+1;
      }

      // increment count for each separator found
      ++(*count);

      // if max is specified, dont go further
      if (max && *count==max)  {
        break;
      }

    }
  }

  // when result array is specified, we are done here
  if (_result) {
    return _result;
  }

  // else allocate memory for result
  // and fill the result array                                                                                    

  *result=malloc((*count)*sizeof(char*));
  if (!*result) {
    return NULL;
  }
  _result=*result;

  // add first string to result
  _result[0]=str;

  // if theres more strings
  for (i=1; i<*count; ++i) {

    // find next string
    while(*str) ++str;
    ++str;

    // add next string to result
    _result[i]=str;

  }

  return _result;
}  

Przykład użycia:

#include <stdio.h>

int main(int argc, char **argv) {
  char *str="JAN,FEB,MAR,APR,MAY,JUN,JUL,AUG,SEP,OCT,NOV,DEC";
  char **result=malloc(6*sizeof(char*));
  char **result2=0;
  unsigned long count;
  unsigned long count2;
  unsigned long i;

  split(strdup(str),strlen(str),',',&result,&count,6);
  split(strdup(str),strlen(str),',',&result2,&count2,0);

  if (result)
  for (i=0; i<count; ++i) {
    printf("%s\n",result[i]);
  }

  printf("\n");

  if (result2)
  for (i=0; i<count2; ++i) {
    printf("%s\n", result2[i]);
  }

  return 0;

}

2

Moja wersja:

int split(char* str, const char delimeter, char*** args) {
    int cnt = 1;
    char* t = str;

    while (*t == delimeter) t++;

    char* t2 = t;
    while (*(t2++))
        if (*t2 == delimeter && *(t2 + 1) != delimeter && *(t2 + 1) != 0) cnt++;

    (*args) = malloc(sizeof(char*) * cnt);

    for(int i = 0; i < cnt; i++) {
        char* ts = t;
        while (*t != delimeter && *t != 0) t++;

        int len = (t - ts + 1);
        (*args)[i] = malloc(sizeof(char) * len);
        memcpy((*args)[i], ts, sizeof(char) * (len - 1));
        (*args)[i][len - 1] = 0;

        while (*t == delimeter) t++;
    }

    return cnt;
}

2

Jest to funkcja podziału ciągów, która obsługuje ograniczniki wieloznakowe. Zwróć uwagę, że jeśli separator jest dłuższy niż dzielony ciąg, to bufferi stringLengthszostanie ustawiony na (void *) 0i numStringszostanie ustawiony na 0.

Ten algorytm został przetestowany i działa. (Zastrzeżenie: nie zostało przetestowane pod kątem ciągów innych niż ASCII i zakłada, że ​​wywołujący podał prawidłowe parametry)

void splitString(const char *original, const char *delimiter, char ** * buffer, int * numStrings, int * * stringLengths){
    const int lo = strlen(original);
    const int ld = strlen(delimiter);
    if(ld > lo){
        *buffer = (void *)0;
        *numStrings = 0;
        *stringLengths = (void *)0;
        return;
    }

    *numStrings = 1;

    for(int i = 0;i < (lo - ld);i++){
        if(strncmp(&original[i], delimiter, ld) == 0) {
            i += (ld - 1);
            (*numStrings)++;
        }
    }

    *stringLengths = (int *) malloc(sizeof(int) * *numStrings);

    int currentStringLength = 0;
    int currentStringNumber = 0;
    int delimiterTokenDecrementCounter = 0;
    for(int i = 0;i < lo;i++){
        if(delimiterTokenDecrementCounter > 0){
            delimiterTokenDecrementCounter--;
        } else if(i < (lo - ld)){
            if(strncmp(&original[i], delimiter, ld) == 0){
                (*stringLengths)[currentStringNumber] = currentStringLength;
                currentStringNumber++;
                currentStringLength = 0;
                delimiterTokenDecrementCounter = ld - 1;
            } else {
                currentStringLength++;
            }
        } else {
            currentStringLength++;
        }

        if(i == (lo - 1)){
            (*stringLengths)[currentStringNumber] = currentStringLength;
        }
    }

    *buffer = (char **) malloc(sizeof(char *) * (*numStrings));
    for(int i = 0;i < *numStrings;i++){
        (*buffer)[i] = (char *) malloc(sizeof(char) * ((*stringLengths)[i] + 1));
    }

    currentStringNumber = 0;
    currentStringLength = 0;
    delimiterTokenDecrementCounter = 0;
    for(int i = 0;i < lo;i++){
        if(delimiterTokenDecrementCounter > 0){
            delimiterTokenDecrementCounter--;
        } else if(currentStringLength >= (*stringLengths)[currentStringNumber]){
            (*buffer)[currentStringNumber][currentStringLength] = 0;
            delimiterTokenDecrementCounter = ld - 1;
            currentStringLength = 0;
            currentStringNumber++;
        } else {
            (*buffer)[currentStringNumber][currentStringLength] = (char)original[i];
            currentStringLength++;
        }
    }
    buffer[currentStringNumber][currentStringLength] = 0;
}

Przykładowy kod:

int main(){
    const char *string = "STRING-1 DELIM string-2 DELIM sTrInG-3";
    char **buffer;
    int numStrings;
    int * stringLengths;

    splitString(string, " DELIM ", &buffer, &numStrings, &stringLengths);

    for(int i = 0;i < numStrings;i++){
        printf("String: %s\n", buffer[i]);
    }
}

Biblioteki:

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

Jak mam to nazwać z main? Nie wiem, co przekazać do bufora.
Aymon Fournier

Logika alokacji jest nieprawidłowa. realloc () zwraca nowy wskaźnik, a ty odrzucasz zwróconą wartość. Brak prawidłowego sposobu na zwrócenie nowego wskaźnika pamięci - prototyp funkcji powinien zostać zmieniony tak, aby akceptował rozmiar alokacji bufferi pozostawił alokację wywołującemu, procesowi maksymalnemu rozmiarowi elementów.
Alex

@Alex Naprawiono, całkowicie przepisano i przetestowano. Uwaga: nie jestem pewien, czy to zadziała dla innych niż ASCII, czy nie.
Élektra

Na początek to nie jest kod C. I dlaczego miałbyś przekazywać wskaźniki przez rzeczywiste referencje w C ++?
Kamiccolo

@Kamiccolo Przepraszam, dlaczego to nie jest kod w C? Ponadto, dlaczego przekazywanie wskaźników przez odniesienie jest tutaj problemem?
Élektra

1

Moje podejście polega na zeskanowaniu ciągu i pozwoleniu, aby wskaźniki wskazywały na każdy znak po separatorach (i pierwszym znaku), jednocześnie przypisując wygląd separatora w ciągu do '\ 0'.
Najpierw wykonaj kopię oryginalnego ciągu (ponieważ jest on stały), a następnie uzyskaj liczbę podziałów, skanując go i przekaż go do parametru wskaźnika len . Następnie skieruj pierwszy wskaźnik wyniku na wskaźnik kopiowania ciągu, a następnie zeskanuj ciąg kopiowania: po napotkaniu separatora przypisz go do `` \ 0 '', w ten sposób poprzedni ciąg wynikowy zostanie zakończony i wskaż następny wskaźnik ciągu wynikowego na następny wskaźnik znaku.

char** split(char* a_str, const char a_delim, int* len){
    char* s = (char*)malloc(sizeof(char) * strlen(a_str));
    strcpy(s, a_str);
    char* tmp = a_str;
    int count = 0;
    while (*tmp != '\0'){
        if (*tmp == a_delim) count += 1;
        tmp += 1;
    }
    *len = count;
    char** results = (char**)malloc(count * sizeof(char*));
    results[0] = s;
    int i = 1;
    while (*s!='\0'){
        if (*s == a_delim){
            *s = '\0';
            s += 1;
            results[i++] = s;
        }
        else s += 1;
    }
    return results;
}

Ta metoda jest zła. Właśnie usunąłem ten post, ale potem zdałem sobie sprawę, że może być interesujący dla niektórych z was.
metalcrash

1

Mój kod (przetestowany):

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

int dtmsplit(char *str, const char *delim, char ***array, int *length ) {
  int i=0;
  char *token;
  char **res = (char **) malloc(0 * sizeof(char *));

  /* get the first token */
   token = strtok(str, delim);
   while( token != NULL ) 
   {
        res = (char **) realloc(res, (i + 1) * sizeof(char *));
        res[i] = token;
        i++;
      token = strtok(NULL, delim);
   }
   *array = res;
   *length = i;
  return 1;
}

int main()
{
    int i;
    int c = 0;
    char **arr = NULL;

    int count =0;

    char str[80] = "JAN,FEB,MAR,APR,MAY,JUN,JUL,AUG,SEP,OCT,NOV,DEC";
    c = dtmsplit(str, ",", &arr, &count);
    printf("Found %d tokens.\n", count);

    for (i = 0; i < count; i++)
        printf("string #%d: %s\n", i, arr[i]);

   return(0);
}

Wynik:

Found 12 tokens.
string #0: JAN
string #1: FEB
string #2: MAR
string #3: APR
string #4: MAY
string #5: JUN
string #6: JUL
string #7: AUG
string #8: SEP
string #9: OCT
string #10: NOV
string #11: DEC

1
Należy pamiętać, że funkcja strtok zmienia ciąg znaków „str”, do którego zastosowano!
SchLx

1

Explode & implode - początkowy ciąg pozostaje nienaruszony, dynamiczna alokacja pamięci

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

typedef struct
{
    uintptr_t   ptr;
    int         size;
} token_t;

int explode(char *str, int slen, const char *delimiter, token_t **tokens)
{
    int i = 0, c1 = 0, c2 = 0;

    for(i = 0; i <= slen; i++)
    {
            if(str[i] == *delimiter)
            {
                c1++;
            }
    }

    if(c1 == 0)
    {
            return -1;
    }

    *tokens = (token_t*)calloc((c1 + 1), sizeof(token_t));
    ((*tokens)[c2]).ptr = (uintptr_t)str;

    i = 0; 
    while(i <= slen)
    {
        if((str[i] == *delimiter) || (i == slen))
        {
                ((*tokens)[c2]).size = (int)((uintptr_t)&(str[i]) - (uintptr_t)(((*tokens)[c2]).ptr));
                if(i < slen)
                {
                    c2++;
                    ((*tokens)[c2]).ptr = (uintptr_t)&(str[i + 1]);
                }
        }
        i++;
    }
    return (c1 + 1);
}

char* implode(token_t *tokens, int size, const char *delimiter)
{
    int     i, len = 0;
    char    *str;

    for(i = 0; i < len; i++)
    {
        len += tokens[i].size + 1;
    }

    str = (char*)calloc(len, sizeof(char));

    len = 0;
    for(i = 0; i < size; i++)
    {
        memcpy((void*)&str[len], (void*)tokens[i].ptr, tokens[i].size);
        len += tokens[i].size;
        str[(len++)] = *delimiter;
    }

    str[len - 1] = '\0';

    return str;
}

Stosowanie:

int main(int argc, char **argv)
{
    int         i, c;
    char        *exp = "JAN,FEB,MAR,APR,MAY,JUN,JUL,AUG,SEP,OCT,NOV,DEC";
    token_t     *tokens;
    char        *imp;

    printf("%s\n", exp);

    if((c = explode(exp, strlen(exp), ",", &tokens)) > 0)
    {
        imp = implode(tokens, c, ",");
        printf("%s\n", imp);

        for(i = 0; i < c; i++)
        {
            printf("%.*s, %d\n", tokens[i].size, (char*)tokens[i].ptr, tokens[i].size);
        }
    }

    free((void*)tokens);
    free((void*)imp);
    return 0;
}

0

Jeśli chcesz skorzystać z zewnętrznej biblioteki, nie mogę bstrlibwystarczająco polecić . Wymaga trochę dodatkowej konfiguracji, ale na dłuższą metę jest łatwiejszy w użyciu.

Na przykład, podziel poniższy ciąg, najpierw tworzy się bstringz bfromcstr()wywołaniem. (A bstringto opakowanie wokół bufora znaków). Następnie podziel ciąg przecinkami, zapisując wynik w a struct bstrList, który zawiera pola qtyi tablicę entry, która jest tablicą bstrings.

bstrlibma wiele innych funkcji do działania na bstrings

Łatwe jak ciasto ...

#include "bstrlib.h"
#include <stdio.h>
int main() {
  int i;
  char *tmp = "Hello,World,sak";
  bstring bstr = bfromcstr(tmp);
  struct bstrList *blist = bsplit(bstr, ',');
  printf("num %d\n", blist->qty);
  for(i=0;i<blist->qty;i++) {
    printf("%d: %s\n", i, bstr2cstr(blist->entry[i], '_'));
  }

}

0

Jeszcze inna odpowiedź (została przeniesiona stąd ):

Spróbuj użyć funkcji strtok:

zobacz szczegóły na ten temat tutaj lub tutaj

Problem polega na tym, że musisz wordsnatychmiast przetworzyć plik. Jeśli chcesz przechowywać go w tablicy, musisz przeznaczyć correct sizena to nieznaną.

Na przykład:

char **Split(char *in_text, char *in_sep)
{
    char **ret = NULL;
    int count = 0;
    char *tmp = strdup(in_text);
    char *pos = tmp;

    // This is the pass ONE: we count 
    while ((pos = strtok(pos, in_sep)) != NULL)
    {
        count++;
        pos = NULL;
    }

    // NOTE: the function strtok changes the content of the string! So we free and duplicate it again! 
    free(tmp);
    pos = tmp = strdup(in_text);

    // We create a NULL terminated array hence the +1
    ret = calloc(count+1, sizeof(char*));
    // TODO: You have to test the `ret` for NULL here

    // This is the pass TWO: we store
    count = 0;
    while ((pos = strtok(pos, in_sep)) != NULL)
    {
        ret[count] = strdup(pos);
        count++;
        pos = NULL;
    }
    free(tmp);

    return count;
}

// Use this to free
void Free_Array(char** in_array)
{
    char *pos = in_array;

    while (pos[0] != NULL)
    {
        free(pos[0]);
        pos++;

    }

    free(in_array);

}

Uwaga : Używamy tej samej pętli i funkcji do obliczania zliczeń (przebieg pierwszy) i do tworzenia kopii (przebieg drugi), aby uniknąć problemów z alokacją.

Uwaga 2 : Możesz użyć innej implementacji strtok z powodów wymienionych w oddzielnych postach.

Możesz użyć tego jak:

int main(void)
{
  char **array = Split("Hello World!", " ");
  // Now you have the array
  // ...

  // Then free the memory
  Free_Array(array);
  array = NULL;
  return 0;
}

(Nie testowałem tego, więc daj mi znać, jeśli to nie działa!)


0

Dwie kwestie związane z tym pytaniem to zarządzanie pamięcią i bezpieczeństwo wątków. Jak widać z licznych postów, nie jest to łatwe zadanie do wykonania bezproblemowo w C. Zależało mi na rozwiązaniu, które jest:

  • Bezpieczny wątek. (strtok nie jest bezpieczny wątkowo)
  • Nie wykorzystuje malloc ani żadnej z jego pochodnych (aby uniknąć problemów z zarządzaniem pamięcią)
  • Sprawdza granice tablic w poszczególnych polach (aby uniknąć błędów segmentowych w przypadku nieznanych danych)
  • Działa z wielobajtowymi separatorami pól (utf-8)
  • ignoruje dodatkowe pola w danych wejściowych
  • udostępnia procedurę miękkiego błędu dla nieprawidłowych długości pól

Rozwiązanie, które wymyśliłem, spełnia wszystkie te kryteria. Prawdopodobnie jest to trochę więcej pracy niż konfiguracja niektórych innych zamieszczonych tutaj rozwiązań, ale myślę, że w praktyce dodatkowa praca jest tego warta, aby uniknąć typowych pułapek innych rozwiązań.

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

struct splitFieldType {
    char *field;
    int   maxLength;
};

typedef struct splitFieldType splitField;

int strsplit(splitField *fields, int expected, const char *input, const char *fieldSeparator, void (*softError)(int fieldNumber,int expected,int actual))  {
    int i;
    int fieldSeparatorLen=strlen(fieldSeparator);
    const char *tNext, *tLast=input;

    for (i=0; i<expected && (tNext=strstr(tLast, fieldSeparator))!=NULL; ++i) {
        int len=tNext-tLast;
        if (len>=fields[i].maxLength) {
            softError(i,fields[i].maxLength-1,len);
            len=fields[i].maxLength-1;
        }
        fields[i].field[len]=0;
        strncpy(fields[i].field,tLast,len);
        tLast=tNext+fieldSeparatorLen;
    }
    if (i<expected) {
        if (strlen(tLast)>fields[i].maxLength) {
            softError(i,fields[i].maxLength,strlen(tLast));
        } else {
            strcpy(fields[i].field,tLast);
        }
        return i+1;
    } else {
        return i;
    }
}


void monthSplitSoftError(int fieldNumber, int expected, int actual) {
    fprintf(stderr,"monthSplit: input field #%d is %d bytes, expected %d bytes\n",fieldNumber+1,actual,expected);
}


int main() {
  const char *fieldSeparator=",";
  const char *input="JAN,FEB,MAR,APRI,MAY,JUN,JUL,AUG,SEP,OCT,NOV,DEC,FOO,BAR";

  struct monthFieldsType {
    char field1[4];
    char field2[4];
    char field3[4];
    char field4[4];
    char field5[4];
    char field6[4];
    char field7[4];
    char field8[4];
    char field9[4];
    char field10[4];
    char field11[4];
    char field12[4];
  } monthFields;

  splitField inputFields[12] = {
    {monthFields.field1,  sizeof(monthFields.field1)},
    {monthFields.field2,  sizeof(monthFields.field2)},
    {monthFields.field3,  sizeof(monthFields.field3)},
    {monthFields.field4,  sizeof(monthFields.field4)},
    {monthFields.field5,  sizeof(monthFields.field5)},
    {monthFields.field6,  sizeof(monthFields.field6)},
    {monthFields.field7,  sizeof(monthFields.field7)},
    {monthFields.field8,  sizeof(monthFields.field8)},
    {monthFields.field9,  sizeof(monthFields.field9)},
    {monthFields.field10, sizeof(monthFields.field10)},
    {monthFields.field11, sizeof(monthFields.field11)},
    {monthFields.field12, sizeof(monthFields.field12)}
  };

  int expected=sizeof(inputFields)/sizeof(splitField);

  printf("input data: %s\n", input);
  printf("expecting %d fields\n",expected);

  int ct=strsplit(inputFields, expected, input, fieldSeparator, monthSplitSoftError);

  if (ct!=expected) {
    printf("string split %d fields, expected %d\n", ct,expected);
  }

  for (int i=0;i<expected;++i) {
    printf("field %d: %s\n",i+1,inputFields[i].field);
  }

  printf("\n");
  printf("Direct structure access, field 10: %s", monthFields.field10);
}

Poniżej znajduje się przykład kompilacji i wyników. Zauważ, że w moim przykładzie celowo przeliterowałem „KWIECIEŃ”, abyś mógł zobaczyć, jak działa błąd programowy.

$ gcc strsplitExample.c && ./a.out
input data: JAN,FEB,MAR,APRIL,MAY,JUN,JUL,AUG,SEP,OCT,NOV,DEC,FOO,BAR
expecting 12 fields
monthSplit: input field #4 is 5 bytes, expected 3 bytes
field 1: JAN
field 2: FEB
field 3: MAR
field 4: APR
field 5: MAY
field 6: JUN
field 7: JUL
field 8: AUG
field 9: SEP
field 10: OCT
field 11: NOV
field 12: DEC

Direct structure access, field 10: OCT

Cieszyć się!


0

Oto kolejna implementacja, która będzie bezpiecznie działać, aby tokenizować literał ciągu pasujący do prototypu żądanego w pytaniu, zwracając przydzielony wskaźnik do wskaźnika do znaku (np char **.). Ciąg ogranicznika może zawierać wiele znaków, a ciąg wejściowy może zawierać dowolną liczbę tokenów. Wszystkie alokacje i realokacje są obsługiwane przez POSIX malloclub reallocbez niego strdup.

Początkowa liczba przydzielonych wskaźników jest kontrolowana przez NPTRSstałą, a jedynym ograniczeniem jest to, że jest większa od zera. char **Powrócił zawiera Sentinel NULL po ostatnim podobnym do tokenu *argv[]oraz w formie użytkowej przezexecv , execvpi execve.

Podobnie jak w przypadku strtok()wielu separatorów sekwencyjnych jest traktowanych jako pojedynczy separator, "JAN,FEB,MAR,APR,MAY,,,JUN,JUL,AUG,SEP,OCT,NOV,DEC"zostanie przeanalizowany tak, jakby ','oddzielał tylko jeden "MAY,JUN".

Poniższa funkcja jest komentowana w linii, a skrót main()został dodany, dzieląc miesiące. Początkowa liczba przydzielonych wskaźników została ustawiona na, 2aby wymusić trzy realokacje podczas tokenizacji ciągu wejściowego:

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

#define NPTRS 2     /* initial number of pointers to allocate (must be > 0) */

/* split src into tokens with sentinel NULL after last token.
 * return allocated pointer-to-pointer with sentinel NULL on success,
 * or NULL on failure to allocate initial block of pointers. The number
 * of allocated pointers are doubled each time reallocation required.
 */
char **strsplit (const char *src, const char *delim)
{
    int i = 0, in = 0, nptrs = NPTRS;       /* index, in/out flag, ptr count */
    char **dest = NULL;                     /* ptr-to-ptr to allocate/fill */
    const char *p = src, *ep = p;           /* pointer and end-pointer */

    /* allocate/validate nptrs pointers for dest */
    if (!(dest = malloc (nptrs * sizeof *dest))) {
        perror ("malloc-dest");
        return NULL;
    }
    *dest = NULL;   /* set first pointer as sentinel NULL */

    for (;;) {  /* loop continually until end of src reached */
        if (!*ep || strchr (delim, *ep)) {  /* if at nul-char or delimiter char */
            size_t len = ep - p;            /* get length of token */
            if (in && len) {                /* in-word and chars in token */
                if (i == nptrs - 1) {       /* used pointer == allocated - 1? */
                    /* realloc dest to temporary pointer/validate */
                    void *tmp = realloc (dest, 2 * nptrs * sizeof *dest);
                    if (!tmp) {
                        perror ("realloc-dest");
                        break;  /* don't exit, original dest still valid */
                    }
                    dest = tmp;             /* assign reallocated block to dest */
                    nptrs *= 2;             /* increment allocated pointer count */
                }
                /* allocate/validate storage for token */
                if (!(dest[i] = malloc (len + 1))) {
                    perror ("malloc-dest[i]");
                    break;
                }
                memcpy (dest[i], p, len);   /* copy len chars to storage */
                dest[i++][len] = 0;         /* nul-terminate, advance index */
                dest[i] = NULL;             /* set next pointer NULL */
            }
            if (!*ep)                       /* if at end, break */
                break;
            in = 0;                         /* set in-word flag 0 (false) */
        }
        else {  /* normal word char */
            if (!in)                        /* if not in-word */
                p = ep;                     /* update start to end-pointer */
            in = 1;                         /* set in-word flag 1 (true) */
        }
        ep++;   /* advance to next character */
    }

    return dest;
}

int main (void) {

    char *str = "JAN,FEB,MAR,APR,MAY,,,JUN,JUL,AUG,SEP,OCT,NOV,DEC",
        **tokens;                           /* pointer to pointer to char */

    if ((tokens = strsplit (str, ","))) {   /* split string into tokens */
        for (char **p = tokens; *p; p++) {  /* loop over filled pointers */
            puts (*p);
            free (*p);      /* don't forget to free allocated strings */
        }
        free (tokens);      /* and pointers */
    }
}

Przykładowe użycie / wyjście

$ ./bin/splitinput
JAN
FEB
MAR
APR
MAY
JUN
JUL
AUG
SEP
OCT
NOV
DEC

Daj mi znać, jeśli masz dodatkowe pytania.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.