C语言 - strtok(...)出现意外的分段错误

3
我正在使用库中的strtok(...),它的表现良好,直到终止条件出现时,它会导致分段错误和程序崩溃。该API声称,当没有更多标记可以找到时,strtok(...)将输出NULL,这意味着,我认为,您必须捕获此NULL以终止使用strtok(...)运行的任何循环。我需要做什么来捕获这个NULL以防止我的程序崩溃?我想象中,NULL被允许用作终止条件。
我为您准备了一个SSCCE,以观察此行为。我需要strtok(...)在我正在编写的大型软件中工作,但我遇到了完全相同的分段行为。命令行输出如下所示(是的,我知道您要使用<...>来包含库,但我无法使此帖子显示代码库)。我正在使用Windows 8操作系统上的gcc版本4.5.3,下面展示了两种不同的尝试在循环中捕获NULL的方式。
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>

main(){
  char* from = "12.34.56.78";
  char * ch = ".";
  char * token = strtok(from, ch);
  printf("%s\n",token);
  while(token != NULL){
    token = strtok(NULL, ch);
    printf("%s\n", token);
  }
  printf("Broke out of loop!");
  while(strcmp(token, 0) != 0){
    printf("%s\n",token);
    token = strtok(NULL, ch);
  }
}
############ 输出: ############

$ ./test
12
34
56
78
段错误 (核心已转储)

strtok发生段错误时,这并不意外。它只应与gets结合使用(也就是说,永远不要使用gets!) - William Pursell
strcmp(NULL,0) <- 炸了! - Aniket Inge
Aniket 可能是对的,但我从来没有进入第二个循环(我编辑了上面的小插曲以反映这一点——输出仍然相同)。@WilliamPursell 你通常会使用 gets(...) 吗?你是说我们永远不能使用 strtok,因为它是一个有缺陷的方法吗?它到底有什么好处?我真的很喜欢它的分词能力,否则,围绕它编写的代码将会膨胀十倍。 - 9codeMan9
@9codeMan9 我的评论有点玩笑。绝对不应该使用 getsstrtok 不是那么糟糕,但我通常发现它并不必要,因为最好通过 lex/bison/yacc 进行标记化。 - William Pursell
6个回答

4

strtok会修改其第一个参数。您正在传递来自只读内存的字符串,当strtok尝试更改它时,将导致段错误。建议更改为:

char* from = "12.34.56.78";

为了

char from[] = "12.34.56.78";

1
更糟糕的是,他正在尝试比较两个NULL值的strcmp - Aniket Inge
我刚刚尝试了你的解决方案,但是我得到了完全相同的输出。我修改了上面的示例以显示我甚至没有进入第二个循环。输出仍然相同,包括分段错误... - 9codeMan9
然而,一旦我按照@Aniket和Michael Burr的建议更改了打印语句的顺序,它就可以工作了。有趣的是,您的数组解决方案“char from []”和我的原始方法“char * from”都可以工作。 - 9codeMan9
9codeMan9 注意使用 char *。它在某些平台上可以工作,但在其他平台上可能会失败。 - William Pursell

3
你首先要检查token是否不等于NULL(当它是NULL时,它会退出while循环)。然后你正在将token(一个NULL)与常量NUMBER进行比较? 在这里:strcmp(token, 0)strcmp期望两个字符串时,你提供了一个数字。 strcmp将尝试在0地址(或NULL)获取一个字符串,从而导致分段错误。
while(strcmp(token, 0) != 0){
    token = strtok(NULL, ch);
    printf("%s\n",token);
  }

此代码应该类似于以下内容:

更改

  char * token = strtok(from, ch);
  printf("%s\n",token);
  while(token != NULL){
    token = strtok(NULL, ch);
    printf("%s\n", token);
  }

to

  char * token = strtok(from, ch);
  printf("%s\n",token);
  while(token != NULL){
    printf("%s\n", token);
    token = strtok(NULL, ch);
  }

你是对的Aniket,问题出在打印语句的顺序上,因为我试图在第一个循环结束时打印一个NULL。你会如何建议设置第二个循环?或者我只是在代码的那部分误用了strcmp(...)函数? - 9codeMan9

3

这是一个问题:

  while(token != NULL){
    token = strtok(NULL, ch);
    printf("%s\n", token);
  }

你正在检查 NULL 值,但是在调用 strtok 后没有再次检查,而是在打印之前检查。

代码还存在其他问题,但我怀疑这就是为什么它现在会崩溃的原因。


更糟糕的是,他正在尝试比较两个空值(NULL)的 strcmp - Aniket Inge
是的,事实证明 printf(...) 需要在循环开始处,因为最后我试图打印一个 NULL,这导致了主要错误。 - 9codeMan9

2
问题在于即使你在strtok()返回NULL时终止循环,你仍然尝试首先打印NULL
  while(token != NULL){
    token = strtok(NULL, ch);
    printf("%s\n", token);    // not good when token is NULL
  }

事实证明,在这个例子中除了这个机会之外,还有其他几个机会会导致段错误,正如其他答案所指出的那样。

以下是处理您的示例标记化的一种方法:

char from[] = "12.34.56.78";
char * ch = ".";
char * token = strtok(from, ch);
while (token != NULL){
    printf("%s\n", token);
    token = strtok(NULL, ch);
}

你是对的,改变printf(...)函数的顺序起作用了,并且允许代码通过第一个循环。 - 9codeMan9

2
如果代码的目的仅仅是打印由'.'分隔的元素,那么只需要更改字符声明并在打印前检查令牌值是否为NULL!
 main(){
        char from[] = "12.34.56.78.100.101";
        char * ch = ".";
        char * token = strtok(from, ch);
        //printf("%s\n",token);
        while(token != NULL){
            printf("%s\n", token);
            token = strtok(NULL, ch);
        }
   }

  ./test1
 12
 12
 34
 56
 78
 100
 101

1
你有内存访问错误和逻辑错误。我只会解决导致程序崩溃的内存访问错误。
strtok修改了它的第一个参数。由于您传递了一个字符串字面量,因此无法修改该字符串(字符串字面量是不可修改的)。
这里有一个可能的修复方法,将from定义为可修改的字符串数组:
char from[] = "12.34.56.78";

由于strtok会修改传入的字符串,所以您不能在第二个while循环中再次处理该字符串。您实际上是将一个NULL传递给strcmp函数。可能的解决方法是每次想使用strtok时将from数组复制到另一个缓冲区中。

除了strcmp(...)问题,您还看到了哪些逻辑错误?或者那是你注意到的唯一一个吗?我不认为我可以用strcmp(...)做到这一点,最终归结为将第0个内存地址(NULL)与NULL进行比较。 - 9codeMan9
正如之前的帖子所指出的那样,这个循环在token为NULL时打印它,这几乎肯定是不正确的:while(token != NULL){ token = strtok(NULL, ch); printf("%s\n", token); } - acarlow

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