strtok()在C语言中如何将字符串分割成标记?

133
请解释一下strtok()函数的工作原理。手册上说它将字符串分解为标记,但我无法从手册中理解它实际上是做什么的。
我在str*pch上添加了监视器以检查第一个while循环发生时它的工作情况,当str的内容只有"this"时,下面显示在屏幕上的输出是如何产生的?
/* 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;
}

输出:

将字符串“- This,a sample string。”拆分为标记:
This
a
sample
string

5
strtok() 在返回结果之前,会修改输入的字符串并用空字符(NUL)作为分隔符。如果您试图在连续调用strtok()之间检查整个缓冲区(即str[]),您会发现它已经被修改了。 - Michael Foukarakis
3
说实话,我从来没去核实过,但我想它会存储最后传入的指针以及离开的位置。那么如果指针为空,程序就可以继续执行;如果不为空,程序将清除位置并重新开始。 - chris
基本上,如果你传递NULL,它会保存最后一个返回结果并从下一个字符继续搜索。这显然使它不是线程安全的,一次只能有一个标记化活动。参考 - DCoder
这是一个闭包吗?我不知道在C语言中如何让函数存储状态。 - Hanfei Sun
7
@Firegun:静态变量 - DCoder
显示剩余5条评论
16个回答

260

strtok运行时函数的工作原理如下:

第一次调用strtok时,您提供要进行标记化的字符串。

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

在上面的字符串中,空格似乎是单词之间一个很好的分隔符,因此我们可以使用它:

char* p = strtok(s, " ");
现在发生的是对字符串's'进行搜索,直到找到空格字符,返回第一个标记('this'),p指向该标记(字符串)。
为了获取下一个标记并继续使用相同的字符串,需要将NULL作为第一个参数传递,因为strtok会保留对先前传递的字符串的静态指针。
p = strtok(NULL," ");

p现在指向'is'

一直进行下去,直到找不到更多空格为止,然后最后一个字符串作为最后一个标记“string”返回。

更方便的做法是改为这样写,以打印出所有标记:

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

编辑:

如果你想要存储从strtok返回的值,你需要将标记复制到另一个缓冲区中,例如strdup(p);,因为原始字符串(由strtok内部的静态指针指向)在迭代之间被修改以返回标记。


5
它确实用'\0'替换了找到的空格。但是它不会恢复空格,所以你的字符串被永久破坏了。 - user3458
44
+1 对于静态缓冲区,这是我没理解的部分。 - IEatBagels
2
从行“返回第一个标记并将p指向该标记”的语句中缺少一个非常重要的细节,即strtok需要通过在分隔符位置放置空字符来改变原始字符串(否则其他字符串函数无法知道标记的结束位置)。它还使用静态变量跟踪状态。 - vgru
1
@AndersK,你仍然没有明确提到分隔符被替换为\0,这是必要的。你只是说字符串被修改了。 - Floris
在第一次调用strtok时,指针在函数内部被存储,可能是在一个静态变量中,所以如果第一个参数是NULL,它将使用并修改该内部指针。 - undefined
显示剩余4条评论

46

strtok()函数将字符串分割成标记。即从任意一个定界符开始到下一个定界符之间的内容作为一个标记。在本例中,第一个标记是以"-"开始并以下一个空格" "结束。然后下一个标记以空格" "开始并以逗号","结束。这里的输出结果为"This"。类似地,其余的字符串从空格到空格分割成标记,并最终在"."上结束。


一个令牌的结束条件成为下一个令牌的起始令牌?同时在结束条件的位置放置了一个空字符吗? - user379888
1
@fahad- 是的,所有你所拥有的分隔符都将被替换为NUL字符,正如其他人所建议的那样。 - Sachin Shanbhag
2
@fahad - 它只用 NUL 替换分隔符字符,而不是分隔符之间的所有字符。它有点像将字符串拆分成多个标记。你得到 "This" 是因为它在两个指定的分隔符之间,而不是 "-this"。 - Sachin Shanbhag
所以,替换第二个定界符时,会放置一个空字符。 - user379888
1
@Fahad - 是的,完全正确。据我所理解,所有空格、","和"-"都被替换为NUL,因为您已将它们指定为分隔符。 - Sachin Shanbhag
显示剩余2条评论

35

strtok维护一个静态的内部引用,指向字符串中下一个可用的标记;如果您将其传递给一个空指针,则它将从该内部引用工作。

这就是为什么strtok不可重入; 一旦您将新指针传递给它,旧的内部引用就会被覆盖。


3
你所说的旧内部引用“getting clobbered”是什么意思?你是指“被覆盖”吗? - ylun.ca
1
@ylun.ca:是的,那就是我的意思。 - John Bode

12

strtok 函数不会改变参数本身 (str)。它将该指针存储在一个本地的静态变量中。然后在后续调用中可以更改该参数指向的内容,而无需将参数传回。 (同时它也可以根据需要移动保留的指针以执行其操作。)

从 POSIX strtok 页面:

该函数使用静态存储来跟踪当前字符串位置之间的调用。

有一个线程安全的变体 (strtok_r), 它不会执行此类魔法。


2
好的,C库函数早在很久以前就存在了,线程根本没有考虑到(就C标准而言,这只是从2011年开始存在),因此可重入性并不是非常重要(我猜)。那个静态局部变量使得该函数“易于使用”(某种定义下的“易于使用”)。就像ctime返回一个静态字符串-实用(没有人需要想知道谁应该释放它),但不可重入,并且如果您不非常了解它,会让您陷入困境。 - Mat
这是错误的: "strtok 不会改变参数本身 (str)。" puts(str); 打印 "- This",因为 strtok 修改了 str - MarredCheese
1
@MarredCheese:请再读一遍。它不会修改指针,而是修改指针所指向的数据(即字符串数据)。 - Mat
哦,好的,我没有意识到你的意思。同意。 - MarredCheese

10

strtok函数可以将字符串分割成一系列子字符串(称为标记)。

它通过查找分隔符来分离这些标记(或子字符串),而您需要指定这些分隔符。在您的情况下,您希望空格、逗号、句点或破折号成为分隔符。

提取这些标记的编程模型是,您将主字符串和分隔符集合交给strtok函数。然后重复调用该函数,每次strtok将返回它找到的下一个标记,直到它到达主字符串的末尾,并返回null。另一个规则是,您仅在第一次传递字符串,之后传递NULL。这是一种告诉strtok是否使用新字符串开始新的标记会话,还是从以前的标记会话中检索标记的方法。请注意,strtok会记住其标记会话的状态。因此,它不具备可重入性或线程安全性(您应该使用strtok_r代替)。另一件要知道的事情是,它实际上修改了原始字符串。它为找到的分隔符写入'\0'。

一种简洁调用strtok的方式如下:

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

结果:

this
is
the
string
I
want
to
parse

9
第一次调用它时,您需要向strtok提供要分解的字符串。然后,为了获得以下的标记,只需将NULL传递给该函数,只要它返回一个非NULL指针即可。 strtok函数记录了您在调用它时提供的字符串。(这对于多线程应用程序来说真的很危险)

5

strtok函数会修改其输入字符串。它在原字符串中插入空字符('\0'),以便将其作为标记返回。实际上,strtok并不会分配内存。如果你将字符串绘制成一系列方框,那么对它的理解会更好。


3
如果您仍然难以理解 strtok() 函数,请查看这个pythontutor 示例,它是一个很好的工具,可以可视化您的 C(或 C++、Python...)代码。

如果链接失效,请复制以下内容:

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

感谢Anders K.


3
为了理解strtok()的工作原理,首先需要知道什么是静态变量这个链接解释得非常清楚... strtok()操作的关键是保留上一次调用之间最后一个分隔符的位置(这就是为什么在连续调用中使用null指针时,strtok()会继续解析传递给它的原始字符串)...
看一下我自己的strtok()实现,名为zStrtok(),其功能与strtok()提供的略有不同。
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;
}

这里是一个使用示例

  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)

这段代码来自我在Github上维护的字符串处理库,名为zString。可以查看代码,甚至做出贡献 :) https://github.com/fnoyanisi/zString

3

这是我实现的strtok方法,虽然不是很好,但是经过2个小时的努力终于成功了。它支持多个分隔符。

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

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