在C语言中使用分隔符拆分字符串

196

我该如何编写一个函数来在 C 编程语言中分割字符串并返回数组?

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

32
你可以使用标准库中的 strtok 函数来实现相同的功能。 - Daniel Kamil Kozar
http://stackoverflow.com/questions/8461170/how-to-remove-spaces-that-are-in-a-string-sentence/8464902#8464902 - BLUEPIXY
一个注释...strtok()家族函数的关键点是理解C语言中的静态变量,即它们在连续使用它们的函数调用之间的行为。请参见下面的代码。 - fnisi
5
strtok 不是解决此问题的方案,有多个原因:它修改了原始字符串,它具有隐藏的静态状态使其不可重入,它将分隔符序列处理为单个分隔符,这对于,似乎是不正确的,结果将不会在,X,,Y, 的开头、中间或结尾拆分空字符串。不要使用 strtok - chqrlie
25个回答

198

您可以使用strtok()函数来拆分字符串(并指定要使用的分隔符)。需要注意的是,strtok()会修改传递给它的字符串。如果原始字符串在其他地方被使用,则需要复制一份并将其传递给strtok()

编辑:

示例(请注意,它不能处理连续的分隔符,“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;
}

输出:

$ ./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]

2
嗨。我认为该函数硬编码了“,”作为分隔符:char* token = strtok(a_str, ","); - SteveP
4
使用strtok进行多线程操作时,由于这可能是Stack Overflow上的标准问题/答案,是否存在一些注意事项? - Peter Mortensen
3
根据那个页面,strsepstrtok 的替代品,但为了可移植性,建议使用 strtok。因此,除非您需要支持空字段或同时拆分多个字符串,否则选择 strtok 更好。 - user1236508
5
它会记住上一次切分的位置,这是它存在问题的原因之一。最好使用strtok_s()(Microsoft,C11 Annex K,可选)或strtok_r()(POSIX),而不是普通的strtok()。在库函数中使用普通的strtok()是有害的。调用库函数的任何函数都不能同时使用strtok(),并且由库函数调用的任何函数都不能调用strtok() - Jonathan Leffler
3
请注意,strtok()在多线程环境下不安全(因为@JonathanLeffler提到的原因),因此整个函数也不安全。如果您尝试在多线程环境中使用它,则会得到不可预测和错误的结果。用strtok_r()替换strtok()可以解决这个问题。 - Sean W
显示剩余10条评论

92

我认为 strsep 仍然是最适合这个任务的工具:

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

这实际上是将一个字符串分成一行的代码。

额外的括号是一种风格要素,用于表示我们有意测试赋值结果而不是等式操作符==

为了使该模式起作用,tokenstr都必须是char *类型。如果你从一个字符串字面量开始,那么你需要先复制它:

// 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);
如果在str中出现两个定界符相连,token的值将为空字符串。每次遇到定界符时,str的值都会被修改为零字节-这是复制要解析的字符串的另一个好理由。
有人在评论中建议使用strtok而不是strsep,因为strtok更具可移植性。Ubuntu和Mac OS X都有strsep;可以猜测其他类Unix系统也有。Windows没有strsep,但它有strbrk,它可以实现以下简单的strsep替换:
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;
}

这里有一个对strsepstrtok进行比较的很好的解释。优缺点可能会根据主观判断而异,但是我认为一个值得注意的迹象是strsep被设计作为strtok的替代品。


5
更准确地说,关于可移植性:它不是 POSIX 7 标准,而是源自 BSD,并在 glibc 上实现(参见 Finding Tokens in a String)。 - Ciro Santilli OurBigBook.com
我正想问一下... Pelle的C有strdup(),但没有strsep()。 - rdtsc
2
为什么 tofree 被释放而不是 str - Sdlion
2
你不能释放 str,因为它的值可能会被调用 strsep() 的函数更改。tofree 的值始终指向要释放的内存的开头。 - Tyler

34

字符串分割器,这段代码应该能让你朝正确的方向前进。

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

以下是我的建议:

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;
}

用法:

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);

5
哇,三分球!我已经害怕使用了,哈哈哈。只是因为我在C语言中不太擅长使用指针。 - Hafiz Temuri
谢谢,兄弟。以上所有的strtok答案在我的情况下都没有起作用,即使我付出了很多努力,而你的代码像魔法一样工作! - hmmftg

13
下面的方法将为您完成所有工作(内存分配,计算长度)。更多信息和描述可以在此处找到 - Java String.split() 方法实现用于分割 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;
}

如何使用它:
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
哇,三星程序员:))这听起来很有趣。 - Michi
当我这样做时,它要么在最后一个标记上添加太多内容,要么分配了太多内存。这是输出: 找到10个标记。 字符串#0:你好, 字符串#1:这个 字符串#2:是 字符串#3:一个 字符串#4:测试 字符串#5:模块 字符串#6:用于 字符串#7:字符串 字符串#8:拆分。¢ - KeizerHarm
8
这个例子存在多个内存泄漏问题。如果您在阅读这篇文章,请不要使用这种方法。相比之下,建议使用strtok或者strsep标记化方法。 - Jorma Rebane

4
在上面的示例中,有一种方法可以返回一个以空字符结尾的字符串数组(就像您想要的那样)放置在字符串中。但这不可能让函数传递一个文字字符串,因为它必须被修改:
#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;
}

也许有更简洁的方法,但你可以理解这个想法。


4
我认为以下解决方案是理想的:
  • 不破坏源字符串
  • 可重入 - 即您可以安全地在一个或多个线程中的任何位置调用它
  • 可移植
  • 正确处理多个分隔符
  • 快速高效
代码解释如下:
  1. 定义一个结构体token来存储令牌的地址和长度
  2. 在最坏的情况下为这些分配足够的内存,即当str完全由分隔符组成时,有strlen(str) + 1个令牌,所有令牌都是空字符串
  3. 扫描str记录每个令牌的地址和长度
  4. 使用此信息分配具有正确大小的输出数组,其中包括额外的空间以容纳NULL标记值
  5. 使用开始和长度信息分配、复制并添加令牌 - 使用memcpy,因为它比strcpy更快,并且我们知道长度
  6. 释放令牌地址和长度数组
  7. 返回令牌数组
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;
}
注意:出于简洁起见,本文中省略了malloc检查。
一般来说,在像这样的拆分函数中,我不会返回一个char *指针数组,因为这使得调用者需要承担很多内存释放的责任。我更喜欢的接口是允许调用者传递回调函数,并为每个标记调用此函数,正如我在这里所描述的:在C中拆分字符串

扫描分隔符两次可能比分配一个潜在的大型token数组更明智。 - chqrlie

4
#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;
}

工作得很好,但是需要在循环中添加“+1”来计数,以获取分隔符后面的部分。 - Subin

3

这种优化的方法会在*result中创建(或更新现有的)指针数组,并返回*count中元素的数量。

使用“max”来表示您期望的字符串的最大数量(当您指定现有数组或任何其他原因时),否则将其设置为0。

要与分隔符列表进行比较,请将delim定义为char *并替换该行:

if (str[i]==delim) {

使用以下两行代码:

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

祝您愉快

#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;
}  

使用示例:

#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;

}

3

My version:

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;
}

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