【问题标题】:Segmentation Fault returned using strtok使用 strtok 返回的分段错误
【发布时间】:2020-11-18 21:49:20
【问题描述】:

我正在尝试在 c 中创建一个函数来拆分字符串,就像 java 或许多其他语言中的拆分函数一样。 我做了这个

char **split(char * str, char *ch) {
  char **array = (char **)malloc((strlen(str)) * sizeof(*array));
  int i = 0;
  char *token = strtok(str, ch);
  while (token != NULL) {
    array[i++] = token;
    token = strtok(NULL, ch);
  }
  free(token);
  return array;
}

这似乎有效,但并非总是有效且不正确。 假设我们以两种不同的方式调用它: 第一个工作:

int main(){
  
  while(1){
    sleep(1);
    char h = ':';
    char a[] = "test:1234";
    char ** result = split(a,&h);
    printf("%s\n",result[0]);
    printf("%s\n",result[1]);
    free(result);
  }
}

而第二个在第二个循环中给我一个分段错误:

int main(){
  char a[] = "test:1234";
  char h = ':';
  while(1){
    sleep(1);
    char ** result = split(a,&h);
    printf("%s\n",result[0]);
    printf("%s\n",result[1]);
    free(result);
  }
}

输出:

test
1234
test
Segmentation fault (core dumped)

我认为这是由于 strtok 函数对字符串索引进行了操作,但我不明白如何修复它以及它究竟为什么会给我一个分段错误。

【问题讨论】:

  • strtok modify 将空字符放入其中的字符串,你假设字符串没有变化,所以第二轮循环 while a 是 "test\01234" 并且 split 只找到一个结果,并且result[1] 在您打印时不会以未定义的行为进行初始化。另一个未定义的行为是您没有将 nulll 终止的字符串作为分隔符
  • free(token); 应该做什么?

标签: c split segmentation-fault strtok


【解决方案1】:

一个问题是您错误地调用了strtok

strtok 需要两个字符串,即要拆分的字符串和一个分隔符字符串。

但你没有传递一个 string 分隔符 - 你传递一个指向单个字符的指针。

所以改成这样:

char h = ':';                  --->  char *h = ":";

char ** result = split(a,&h);  --->  char ** result = split(a,h);

您的代码的另一个问题是您希望它始终返回至少两个有效令牌。这是一个糟糕的假设,它将在您的第二个代码示例的第二个循环中失败。

在第一个循环中,a 将更改为字符串“test”,因为strtok':' 替换为字符串终止字符。

因此在第二个循环中只有一个标记。这意味着result[1] 没有指向有效的令牌,因此,您不能打印它所指向的内容。

解决该问题的一种方法是在函数中将所有 result 指针设置为 NULL,例如通过使用calloc 而不是malloc 喜欢:

char **array = calloc(strlen(str), sizeof(*array));

然后像这样打印:

if (result[0]) printf("%s\n",result[0]);
if (result[1]) printf("%s\n",result[1]);

或更好:

int i = 0;
while(result[i])
{
    printf("%s\n",result[i]);
    ++i;
}

把它们放在一起:

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

char **split(char * str, char *ch) {
  char **array = calloc(strlen(str), sizeof(*array));  // Use calloc to set
                                                       // all pointers to NULL
  int i = 0;
  char *token = strtok(str, ch);
  while (token != NULL) {
    array[i++] = token;
    token = strtok(NULL, ch);
  }
  return array;
}

int main(){
  char a[] = "test:1234";
  char *h = ":";
  int z = 0;
  while(z < 5){    // Just loop 5 times
    //sleep(1);
    char ** result = split(a,h);
    int i = 0;
    while(result[i])   // Print all tokens, i.e. stop when a pointer is NULL
    {
        printf("%s\n",result[i]);
        ++i;
    }    
    free(result);
    ++z;
  }
}

输出:

test
1234
test
test
test
test

顺便说一句:

这个

free(token);

一样
free(NULL);

它什么都不做,所以只需删除该行。

【讨论】:

  • 相当于用这样的常量 char 调用: char ** result = split(a,":");或者这可能会导致像 char 指针一样的不稳定问题?
  • @Virgula ":" 是一个有 2 个字符的字符串,':' 然后是 '\0',你的代码中 &amp;h 不是这种情况
  • @Virgula 是的,您可以将其称为char ** result = split(a,":"); 甚至char ** result = split(a,":,;-"); 作为strtok 的分隔符传递的字符指针必须是字符串,即它必须以零结尾.
  • 我认为@bruno 是对的,这里缺少很多东西。一开始在我的电脑上工作,但现在再次出现分段错误。让我们也看看这里:godbolt.org/z/Kcc6an
  • @Virgula 这是您的第一个建议之间的区别是您在所有循环中处理“test:1234”,而第二个建议是在您处理“测试”时(无论空字符等后的内容如何)在里面)
【解决方案2】:

strtok 使用起来有点棘手,因为它处理内存的方式不同 从习惯 - 它修改作为参数传递的字符串,返回指向子字符串的指针,当 strtok(NULL,..) 将新指针返回到缓冲区时,如果缓冲区超出范围,则指针变为无效或者如果另一个线程正在调用 strtok,则指针变得无效,因此最好在继续之前将返回的令牌复制到另一个缓冲区。

这可以通过分配一个内存块然后复制进去来实现 那里的返回值

char **split(char * str, char *ch) {
  char **array = (char **)malloc((strlen(str)) * sizeof(*array));
  int i = 0;
  char *token = strtok(str, ch);
  while (token != NULL) {
    char* dupToken = malloc(strlen(token)+1);
    strcpy(dupToken, token);
    array[i++] = dupToken;
    token = strtok(NULL, ch);
  }
  // free(token); // this here is wrong
  return array;
}

现在您的代码的另一个问题是调用者无法知道 返回的数组中有多少令牌,所以我建议另一种方法

一旦你点击了最后一个标记,在返回之前将下一个指针设置为 NULL 数组

char **split(char * str, char *ch) {
  char **array = (char **)malloc((strlen(str)) * sizeof(*array));
  int i = 0;
  char *token = strtok(str, ch);
  while (token != NULL) {
    char* dupToken = malloc(strlen(token)+1);
    strcpy(dupToken, token);
    array[i++] = dupToken;
    token = strtok(NULL, ch);
  }
  array[i] = NULL;    
  return array;
}

这样,当您通过令牌时,您只需检查指针

for (int i = 0; array[i] != NULL; ++i)
{ 
...
}

编辑:那么最好在你的数组中添加另一个条目,这样你就可以处理最大数量的令牌 + 1

char **array = (char **)malloc((strlen(str) + 1) * sizeof(*array));

编辑:更改了我对返回的指针所发生情况的相当草率的描述,只要传递给 strtok 的原始缓冲区有效,它就有效。

【讨论】:

  • 有趣。引用“...再次调用该指针变得无效”您对此有参考吗?
  • @4386427 在 strtok 中有一个静态指针,每次调用 strtok(NULL, ... ) 时都会移动它,这就是返回的内容。 strtok 在每次后续调用中使用 strtok(NULL...) 修改第一次调用时传递给它的字符串
  • @AndersK 但它不会使先前返回的指针失效。它们都是指向原始字符串中某个位置的指针(最终的 NULL 除外)。 根本不需要复制。 strtok 的目的是将原始字符串缓冲区转换为多个字符串,并一一返回指向这些新字符串的指针。发布的代码可以完成这项工作,但说需要复制是一种误导。不是。
  • @AndersK 请参阅ideone.com/71a5xM 它显示了返回的指针如何全部指向原始缓冲区并始终指向正确的令牌。 strtok 返回的任何内容都不会失效。
  • @4386427 你说得对,我的描述有点草率,我已经更正了。
猜你喜欢
  • 2023-03-08
  • 2012-02-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-03-24
相关资源
最近更新 更多