strdup 和 strtok 导致的段错误
Segfault resulting from strdup and strtok
我的大学教授给我布置了作业,我似乎发现了 strtok
的一些奇怪行为
基本上,我们必须为我的 class 解析 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 post 上发现,将 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*) $
当你到达最后一个循环时,你会得到
for (char *token = strtok(copy, ",\n"); i < NUM_TOKENS - 1; token = strtok(NULL, ",\n"))
- 循环体
- 循环增量步骤,即
token = strtok(NULL, ",\n")
(第二个参数错误)
- 循环继续检查
i < NUM_TOKENS - 1
即它仍然调用了 strtok
,即使您现在不在范围内。你在这里的数组索引上也有一个 off-by-one:你想要初始化 i=0
而不是 1.
您可以通过例如
来避免这种情况
使初始 strtok 成为循环外的特例,例如
int i = 0;
tokens[i++] = strdup(strtok(copy, ",\n"));
然后在循环内移动 strtok(NULL, ",\n")
我也很惊讶你想要 \n
在那里,或者甚至需要调用最后一个 strtok(那不是已经指向字符串的其余部分了吗?如果你只是想切掉尾随的换行符有更简单的方法)但我已经很多年没用过 strtok 了。
(顺便说一句,您也没有释放存储字符串指针的 malloced 数组。也就是说,因为此时程序结束并不重要。)
请记住,当 strtok
在分隔符字符串(strtok()
的第二个参数)中找到 any 个字符时,它会识别一个标记 - 它不会' 尝试匹配整个分隔符字符串本身。
因此,从一开始就不需要三元运算符 - 字符串将根据输入字符串中 ,
或 \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);
在将 NULL
传递给另一个函数之前,请检查 strtok
中的 return 值。您的循环调用 strtok
的次数比您想象的要多。
更常见的是使用这个return值来控制你的循环,这样你就不会受数据的支配。至于分隔符,最好保持简单,不要尝试任何花哨的东西。
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;
}
请注意,您还应该检查 malloc
和 strdup
中的 return 值并正确释放内存
我的大学教授给我布置了作业,我似乎发现了 strtok
基本上,我们必须为我的 class 解析 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 post 上发现,将 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*) $
当你到达最后一个循环时,你会得到
for (char *token = strtok(copy, ",\n"); i < NUM_TOKENS - 1; token = strtok(NULL, ",\n"))
- 循环体
- 循环增量步骤,即
token = strtok(NULL, ",\n")
(第二个参数错误) - 循环继续检查
i < NUM_TOKENS - 1
即它仍然调用了 strtok
,即使您现在不在范围内。你在这里的数组索引上也有一个 off-by-one:你想要初始化 i=0
而不是 1.
您可以通过例如
来避免这种情况使初始 strtok 成为循环外的特例,例如
int i = 0; tokens[i++] = strdup(strtok(copy, ",\n"));
然后在循环内移动
strtok(NULL, ",\n")
我也很惊讶你想要 \n
在那里,或者甚至需要调用最后一个 strtok(那不是已经指向字符串的其余部分了吗?如果你只是想切掉尾随的换行符有更简单的方法)但我已经很多年没用过 strtok 了。
(顺便说一句,您也没有释放存储字符串指针的 malloced 数组。也就是说,因为此时程序结束并不重要。)
请记住,当 strtok
在分隔符字符串(strtok()
的第二个参数)中找到 any 个字符时,它会识别一个标记 - 它不会' 尝试匹配整个分隔符字符串本身。
因此,从一开始就不需要三元运算符 - 字符串将根据输入字符串中 ,
或 \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);
在将 NULL
传递给另一个函数之前,请检查 strtok
中的 return 值。您的循环调用 strtok
的次数比您想象的要多。
更常见的是使用这个return值来控制你的循环,这样你就不会受数据的支配。至于分隔符,最好保持简单,不要尝试任何花哨的东西。
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;
}
请注意,您还应该检查 malloc
和 strdup
中的 return 值并正确释放内存