strtok()问题:如果令牌由分隔符分隔,为什么最后一个令牌位于分隔符和空字符'\0'之间?

7
在下面的程序中,strtok()在大部分情况下都能按预期工作,但我无法理解一个发现背后的原因。我已经阅读了关于 strtok() 的资料:

要确定令牌的开始和结束位置,该函数首先从起始位置扫描第一个不包含在定界符中的字符(成为令牌的开始)。然后从该令牌的开始位置开始扫描第一个包含在定界符中的字符,这成为令牌的结束位置。

来源: http://www.cplusplus.com/reference/cstring/strtok/

而且我们知道,strtok() 在每个标记的末尾放置一个 \0。但在以下程序中,最后一个定界符是一个点(.),在该点和引号(")之间有Toad。现在点是我的程序中的定界符,但在Toad之后没有定界符,甚至没有空格(在我的程序中是一个定界符)。请解释以下这个前提所引起的混淆:
为什么strtok()认为Toad是一个标记,即使它不在两个定界符之间?当遇到空字符(\0)时,我在strtok()中读到:

一旦在调用 strtok 中找到 str 的终止空字符,所有随后使用 null 指针作为第一个参数调用此函数的调用都返回 null 指针。

来源: http://www.cplusplus.com/reference/cstring/strtok/

这里没有说一旦遇到空字符就会返回指向标记开头的指针(我们甚至没有标记,因为我们没有找到标记的结尾,因为在从标记开头开始扫描时(即从Toad的 'T' 开始扫描),我们只找到了一个空字符,而没有找到定界符)。那么为什么strtok()认为参数字符串的最后一个定界符和引号之间的部分是标记呢?请解释一下。

代码:

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

int main ()
{
  char str[] =" Falcon,eagle-hawk..;buzzard,gull..pigeon sparrow,hen;owl.Toad";
  char * pch=strtok(str," ;,.-");

    while (pch != NULL)
  {
    printf ("%s\n",pch);
    pch = strtok (NULL, " ;,.-");
  }

  return 0;
}

输出:

猎鹰
老鹰

秃鹫
海鸥
鸽子
麻雀
母鸡
猫头鹰
蟾蜍


不确定我理解你的问题;你期望的输出是什么?你希望“Toad”不会被打印出来吗?按照这个逻辑,如果你删除输入字符串中的前导空格,“Falcon”也不应该被打印。我认为这会导致一些不直观的行为。 - Praetorian
如果您在“Falcon”之前删除了空格,“strtok()”仍将认为“Falcon”是第一个标记。 - Jonathan Leffler
@JonathanLeffler 我故意这样做的。就像我说的,除了最后一个标记明显不在两个分隔符之间之外,所有内容都符合strtok()的预期。 - Rüppell's Vulture
@JonathanLeffler 很抱歉我在发布这个问题后不得不出门。 - Rüppell's Vulture
@Praetorian 为什么我不应该期望 Falcon 被打印出来?我已经从源代码中提到了 the function first scans from the starting location for the first character not contained in delimiters,也就是说,在标记令牌的开头时,我们不需要分隔符(在我的程序中,空格是分隔符),但是为了标记令牌的结尾,我们显然需要一个分隔符,并且字符串末尾的 NULL 不在分隔符列表中。 - Rüppell's Vulture
@JonathanLeffler 我很惊讶我在这个问题中甚至无法向您表达我的观点。 - Rüppell's Vulture
5个回答

9
标准规范中的strtok (7.24.5.8)非常清楚。 特别是第4段(由我强调)直接与问题相关,如果我理解正确:第一次调用顺序在当前分隔符字符串指针s2中搜索指向的字符串s1中不包含的第一个字符。 如果未找到此类字符,则s1中没有标记,并且strtok函数返回空指针。 如果找到这样的字符,则它是第一个标记的开始。 strtok函数然后从那里开始搜索包含在当前分隔符字符串中的字符。 如果未找到此类字符,则当前令牌延伸到指向s1的字符串的末尾,并且对标记的后续搜索将返回空指针。 如果找到这样的字符,则将其覆盖为null字符,该字符终止当前标记。 strtok函数保存指向以下字符的指针,下次搜索标记时将从该字符开始。
char *where = strtok(string_or_NULL, delimiters);

如果有的话,返回的令牌(指针)从起始位置(包括)找到的第一个非分隔符字符开始延伸,直到下一个分隔符字符(不包括),如果存在,或者字符串的结尾,如果没有更多的分隔符字符。

链接的描述没有明确提到令牌延伸到字符串末尾的情况,与标准不同,因此在这方面是不完整的。


2
如果在s1指向的字符串中没有找到这样的字符,则当前标记将延伸到该字符串的末尾,并且随后对标记的搜索将返回空指针。谢谢,这正是我想知道的,直接从标准中获取。 - Rüppell's Vulture
1
一箭命中精准无误!! - Rüppell's Vulture

4

查看 POSIX 中 strtok() 的描述,其描述如下:

char *strtok(char *restrict s1, const char *restrict s2);

strtok()函数将由s1指向的字符串按照由s2指向的字符串中的分隔符进行分割,得到一系列标记。第一次调用该函数时,s1作为第一个参数,后续调用使用空指针作为第一个参数。每次调用时,分隔符字符串可以不同。

第一次调用该函数在s1指向的字符串中搜索第一个不包含在s2指向的当前分隔符字符串中的字符。如果没有找到这样的字符,则s1指向的字符串中没有标记,strtok()返回空指针。如果找到了这样的字符,则它是第一个标记的起始位置。

然后,strtok()函数从此处开始搜索一个包含在当前分隔符字符串中的字符。如果没有找到这样的字符,则当前标记延伸到s1指向的字符串的末尾,并且后续的标记搜索将返回空指针。如果找到了这样的字符,则它被一个NUL字符覆盖,表示当前标记的结束。strtok()函数保存指向下一个字符的指针,从该指针开始搜索下一个标记。

请注意第三段的第二句话:

如果没有找到这样的字节,则当前令牌将延伸到由 s1 指向的字符串的末尾,并且随后对令牌的搜索将返回空指针。

这清楚地说明,在问题的示例中,Toad 确实是一个标记。一种思考方式是,分隔符列表始终包括分隔符字符串末尾的 NUL '\0'
诊断出来后,请注意 strtok() 不是一个好的函数使用 —— 它不是线程安全或可重入的。在 Windows 上,您可以使用 strtok_s() 代替;在 Unix 上,通常可以使用 strtok_r()。这些是更好的函数,因为它们不会在内部存储搜索恢复的指针。
由于 strtok() 不是可重入的,您不能在使用 strtok() 的函数内部调用使用 strtok() 的函数,同时它正在使用 strtok()。此外,任何使用 strtok() 的库函数必须明确标识出来,因为它不能从正在使用 strtok() 的函数中调用。因此,使用 strtok() 会让生活变得困难。 strtok()函数族(以及相关的strsep()函数)的另一个问题是它们会覆盖分隔符。在分词器对字符串进行分词后,你无法找到分隔符是什么。这在某些应用程序中很重要(例如解析shell命令行;分隔符是管道还是分号或者&等有所区别)。因此,尽管SO上关于使用strtok()的shell解析器的问题很多,但shell解析器通常不使用strtok()

一般来说,你应该避免使用普通的strtok(),并且需要根据你的目的决定是否适用于strtok_r()strtok_s()


Daniel Fischer只比你快了几秒钟!! - Rüppell's Vulture
是的 - 我不应该去吃午饭...我看到他的答案在我写我的时候到了,但是只有在我提交后才看到。 - Jonathan Leffler

2
因为cplusplus.com没有告诉你全部的故事。Cppreference.com 有一个更好的描述。
Cplusplus.com还没有提到 strtok 不是线程安全的,并且仅记录了C++编程语言中的 strtok 函数,而cppreference.com则提到了线程安全问题,并记录了 CC ++ 编程语言中的 strtok 函数。

0

strtok函数将一个字符串按照给定的分隔符分割成一系列标记。 分隔符只是用来分离标记,不一定在两侧都终止。


0

您可能是读错了描述吗?

一旦在调用 strtok 中发现 str 的终止空字符,所有 后续 使用空指针作为第一个参数调用此函数的调用都将返回空指针。

鉴于“subsequent”的意思,我理解这里是指在发现\0之后对strtok的每个调用,而不仅仅是当前调用本身。因此,该定义与行为一致(也符合从strtok预期的行为)。


从源描述中可以明显看出,它表明在没有定界符的情况下无法结束标记。在这个上下文中,后续调用或当前调用并不重要。以下是关于标记结尾的说明:“然后从标记的起始位置开始扫描,直到找到包含在定界符中的第一个字符,该字符成为标记的结尾。” - Rüppell's Vulture
@Rüppell'sVulture 我同意在初始字符串为“.Toad”时,该描述并不完全正确。然而,目前看来,这个问题显然只是源代码文档不够清晰明了,strtok 本身没有问题。 - Matt Phillips
@Rüppell'sVulture :) 我不确定你来自哪里,但在美国,我们说“Uncle!”此时,是的,你是正确的! cplusplus.com 的文档是不充分的。但尽管它很受欢迎,但我不知道任何一种意义上它是 C 语言的规范或代表。因此,也许可以给他们发送电子邮件... - Matt Phillips
@Rüppell'sVulture 对,确实“链接的描述没有明确提到令牌延伸到字符串末尾的情况,与标准相反,因此在这方面不完整。”来自 DF 的说法正是我所说的。看起来在这个问题上有一个相当明确的共识。 - Matt Phillips
@MattPhllips 以后我会小心那个网站的。实际上,那个网站的布局非常吸引人,看起来很专业。而且它给人一种“只谈生意,不闲聊”的感觉。 - Rüppell's Vulture
显示剩余3条评论

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