【问题标题】:Segfault resulting from strdup and strtokstrdup 和 strtok 导致的段错误
【发布时间】:2016-10-24 15:52:45
【问题描述】:

我的大学教授给我布置了作业,我似乎发现了strtok 的一些奇怪行为

基本上,我们必须为我的班级解析一个 CSV 文件,其中 CSV 中的标记数量是已知的,并且最后一个元素可能有额外的 "," 字符。

一行示例:

Hello,World,This,Is,A lot, of Text

标记应该在哪里输出

1. Hello
2. World
3. This
4. Is
5. A lot, of Text

对于这项任务,我们必须使用strtok。因此,我在其他一些 SOF 帖子中发现,使用带有空字符串的strtok(或将"\n" 作为第二个参数传递)会导致读取到行尾。这非常适合我的应用程序,因为多余的逗号总是出现在最后一个元素中。

我创建了这个有效的代码:

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

#define NUM_TOKENS 5

const char *line = "Hello,World,This,Is,Text";

char **split_line(const char *line, int num_tokens)
{
    char *copy = strdup(line);

    // Make an array the correct size to hold num_tokens
    char **tokens = (char**) malloc(sizeof(char*) * num_tokens);

    int i = 0;
    for (char *token = strtok(copy, ",\n"); i < NUM_TOKENS; token = strtok(NULL, i < NUM_TOKENS - 1 ? ",\n" : "\n"))
    {
        tokens[i++] = strdup(token);
    }

    free(copy);

    return tokens;
}

int main()
{
    char **tokens = split_line(line, NUM_TOKENS);

    for (int i = 0; i < NUM_TOKENS; i++)
    {
        printf("%s\n", tokens[i]);
        free(tokens[i]);
    }
}

现在这可行,应该得到我的充分信任,但我讨厌这个不应该需要的三元:

token = strtok(NULL, i < NUM_TOKENS - 1 ? ",\n" : "\n");

我想用这个版本替换方法:

char **split_line(const char *line, int num_tokens)
{
    char *copy = strdup(line);

    // Make an array the correct size to hold num_tokens
    char **tokens = (char**) malloc(sizeof(char*) * num_tokens);

    int i = 0;
    for (char *token = strtok(copy, ",\n"); i < NUM_TOKENS - 1; token = strtok(NULL, ",\n"))
    {
        tokens[i++] = strdup(token);
    }

    tokens[i] = strdup(strtok(NULL, "\n"));

    free(copy);

    return tokens;
}

这更让我觉得好笑,因为更容易看出有一个最终案例。你也摆脱了奇怪的三元运算符。

可悲的是,这个段错误!我一辈子都想不通为什么。

编辑:添加一些输出示例:

[11:56:06] gravypod:test git:(master*) $ ./test_no_fault 
Hello
World
This
Is
Text
[11:56:10] gravypod:test git:(master*) $ ./test_seg_fault 
[1]    3718 segmentation fault (core dumped)  ./test_seg_fault
[11:56:14] gravypod:test git:(master*) $ 

【问题讨论】:

  • 啊,我现在看到了 \n 的困惑。使用带有 \n 的 strtok 是您如何逐行解析加载到内存中的整个文本文件。如果你只处理一个不包含换行符的单行,那么它就没有神奇的意义。

标签: c strtok


【解决方案1】:

在您冒险将NULL 传递给另一个函数之前,请检查来自strtok 的返回值。您的循环调用strtok 的次数比您想象的要多。

更常见的是使用这个返回值来控制你的循环,这样你就不会受到数据的支配。至于分隔符,最好保持简单,不要尝试任何花哨的东西。

char **split_line(const char *line, int num_tokens)
{
    char *copy = strdup(line);
    char **tokens = (char**) malloc(sizeof(char*) * num_tokens);
    int i = 0;
    char *token;
    char delim1[] = ",\r\n";
    char delim2[] = "\r\n";
    char *delim = delim1;                   // start with a comma in the delimiter set

    token = strtok(copy, delim);
    while(token != NULL) {                  // strtok result comtrols the loop
        tokens[i++] = strdup(token);
        if(i == NUM_TOKENS) {
            delim = delim2;                 // change the delimiters
        }
        token = strtok(NULL, delim);    
    }
    free(copy);
    return tokens;
}

请注意,您还应该检查 mallocstrdup 的返回值并正确释放内存

【讨论】:

  • 我会清理内存管理。如果这是替代方案,我将坚持使用我的三元组。我认为这个问题需要一个 for 循环模式。感谢您的帮助!
  • 感谢您的投票。 strtokfor 循环相当丑陋且不惯用,而且正如您所发现的那样,很麻烦。
  • 如果您担心此处的行数,您可以删除所有的 delim 变量,将第一个 ",\n" 或 ",\r\n" 设为此处,然后只需将您的三元组放在最后的 strtok 行中。 for 循环的优点是它保证你不会溢出标记数组:缺点是你不能很好地处理具有较少段的输入。这两种方法都可以解决,但是您的调用代码需要接受具有较少元素的返回值或返回分配的空字符串等。
  • 我想让代码显而易见。我可以使用单个分隔符字符串",\r\n",并在不再需要',' 时简单地增加字符串指针。请简单明了地编写代码。您可能有一个“幻想”,希望使某些方法工作,但最重要的问题应该是可读性和可维护性,以及调试复杂表达式结果所花费的最少时间。
  • 这根本不是我的意思。 OP 说他不会使用你的代码,因为他认为它太复杂所以他会使用他的三进制,我告诉他如何在更少的行中使用你的代码和他的三进制。
【解决方案2】:

当你到达最后一个循环时,你会得到

for (char *token = strtok(copy, ",\n"); i < NUM_TOKENS - 1; token = strtok(NULL, ",\n"))
  1. 循环体
  2. 循环递增步骤,即token = strtok(NULL, ",\n")(第二个参数错误)
  3. 循环继续检查i &lt; NUM_TOKENS - 1

即即使您现在超出范围,它仍然调用strtok。您在这里的数组索引上也有一个不一致的地方:您需要初始化 i=0 而不是 1。

你可以避免这种情况,例如

  • 使初始 strtok 成为循环外的特殊情况,例如

    int i = 0;
    tokens[i++] = strdup(strtok(copy, ",\n"));
    
  • 然后在循环内移动strtok(NULL, ",\n")

我也很惊讶你想要 \n 在那里,甚至需要调用最后一个 strtok (那不是已经指向字符串的其余部分了吗?如果你只是想砍掉一个尾随的换行符有更简单的方法)但我已经多年没有使用 strtok 了。

(顺便说一句,您也没有释放存储字符串指针的 malloced 数组。也就是说,因为此时程序结束并不重要。)

【讨论】:

  • 如果你看看我的问题,你会明白为什么我需要 \n 作为最后一个案例。看看需要打印什么。看“5”。输出案例。
  • 我明白为什么没有逗号,但我在输入字符串中看不到任何换行符。
【解决方案3】:

请记住,strtok 在找到分隔符字符串(strtok() 的第二个参数)中的 任何 个字符时会识别标记 - 它不会尝试匹配整个分隔符字符串自己。

因此,从一开始就不需要三元运算符 - 字符串将根据输入字符串中 ,\n 的出现进行标记,因此以下工作:

for (token = strtok(copy, ",\n"); i < NUM_TOKENS; token = strtok(NULL, ",\n"))
{                                                                                                         
    tokens[i++] = strdup(token);                                                                          
} 

第二个示例出现段错误,因为它在退出 for 循环时已经将输入标记到字符串的末尾。再次调用strtok() 会将token 设置为NULL,并且在NULL 指针上调用strdup() 时会产生段错误。删除对strtok 的额外调用会得到预期的结果:

for (token = strtok(copy, ",\n"); i < NUM_TOKENS - 1; token = strtok(NULL, ",\n"))
{                                                                                 
    tokens[i++] = strdup(token);                                                  
}                                                                                 
tokens[i] = strdup(token);                                                        

【讨论】:

  • 如果您查看问题的5. A lot, of Text,其中包含一个逗号,您将了解 OP 为什么要更改分隔符。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2014-12-21
  • 2020-04-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-04-08
相关资源
最近更新 更多