将字符串数组传递给 fscanf 的函数
Passing an array of strings into a function for fscanf
当我 运行 此代码出现分段错误时,我不知道为什么会这样,我试图从文本文件的每一行中读取姓名和现金金额,然后将其放入成一个数组。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void ReadFile(int *cash, char **name)
{
int r, line = 0;
FILE *fp;
fp = fopen("coins.txt", "r");
if (fp == NULL)
{
printf ("Error opening the file\n\n'");
exit(EXIT_FAILURE);
} else {
while (line <= 14)
{
r = fscanf(fp, "%s %d\n", name[line], &cash[line]);
if (r != 2)
{
printf ("Error, line %d in wrong format!\n\n", line);
}
//printf("Name: %s, Cash: $%d\n",name, *cash);
line++;
}
fclose(fp);
}
}
int main()
{
int cash[14];
char *name[14] = { "" };
ReadFile(cash,name);
//printf("%s\n", name[0]);
}
gdb 非常适合查找此类问题。
segfault$ gcc -ggdb segfault.c -o segfault
segfault$ gdb segfault
GNU gdb (Ubuntu 8.1.1-0ubuntu1) 8.1.1
[...]
Reading symbols from segfault...done.
(gdb) break segfault.c:20
Breakpoint 1 at 0x896: file segfault.c, line 20.
(gdb) run
Starting program: /tmp/segfault/segfault
Breakpoint 1, ReadFile (cash=0x7fffffffdd80, name=0x7fffffffddc0) at segfault.c:20
20 r = fscanf(fp, "%s %d\n", name[line], &cash[line]);
(gdb) print line
= 0
(gdb) print &cash[line]
= (int *) 0x7fffffffdd80
(gdb) print name[line]
= 0x0
在 main()
中使用 char *name[14]
,您正在创建一个 char 指针数组,但您没有设置指针。因此,它们可能指向任何地方,最有可能指向无效位置(当您尝试 read/write 该位置时会导致段错误)。
在我的系统上,我们可以看到我们很幸运,未初始化的 name[0]
指向 0x0
(NULL
)。这并不能保证,我们可能会遇到比这里的段错误更有趣的错误。想象一下,例如,如果 name[0]
改为指向内存中存储 line
的位置。你会覆盖那个内存,并从你的循环中得到非常奇怪的行为。
您必须初始化 name
数组中的每个指针(在 main()
中),以便它们都指向足够大小的有效区域。
继续评论,直接错误是由于 char *name[14]
声明了一个包含 14 个指向 char 的指针的数组,这些指针 未初始化 .这意味着每个指针都指向任何地方(例如,指针所持有的地址,因为它的值指向某个不确定的内存地址)。在你可以存储任何东西之前,你必须确保你有一个有效的内存位置来保存你正在处理的任何值。对于字符串,例如 name[x]
,这意味着您需要 length + 1
个字符可用(+1
为 nul-terminating 字符提供存储, '[=21=]'
, 相当于普通的 0
)
您的 ReadFile()
功能严重不足,您的阅读能力很脆弱。任何时候读取数据行时,都应该使用 面向行的 输入函数,例如 fgets()
(或 POSIX getline()
)。这样你每次都会读取(消耗)一行输入,任何导致 匹配失败 的输入文件格式差异只会影响那一行,而不会破坏文件的读取从那时起。
永远不要硬编码文件名 或在代码中使用MagicNumbers。你不应该仅仅为了从不同的输入文件中读取而重新编译你的程序。您可以提供默认使用的文件名,否则将文件名作为程序的参数读取(这就是 int argc, char **argv
的用途),或者提示用户并将文件名作为输入。为了避免代码中的硬编码文件名和幻数:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define COINS 14 /* if you need a constant, #define one (or more) */
#define MAXC 1024
和
int main (int argc, char **argv)
{
...
FILE *fp = fopen (argc > 1 ? argv[1] : "coins.txt", "r");
...
此外,您通常希望在调用函数中打开并验证文件是否已打开以供读取,并将 FILE*
指针作为函数的参数传递给打开的流。如果文件打开失败,则无需调用函数开头。
将所有部分放在一起并使用 MAXC
个字符的缓冲区来保存在转换(并验证)转换之前从文件中读取的每一行。 (很好地验证了 fscanf()
中的 return)。
要解决您的问题,您应该将 name
值读入临时数组,然后分配存储空间,只需将临时数组中字符串的 strlen()
和 malloc (len + 1)
个字符用于 name[i]
(验证每个分配),然后从临时数组复制到 name[i]
。 (当你完成调用者的值后,你将需要释放内存(main()
这里)
您还需要 line
和 index
的单独计数器。由于您报告了发生任何故障的行(再次干得好!),因此您需要在每次迭代时增加 line
计数器,但您只想增加 name
和 [= 的索引40=] 表示成功转换,例如
size_t ReadFile (FILE *fp, int *cash, char **name)
{
char buf[MAXC]; /* temporary array to hold line */
size_t index = 0, line = 0; /* separate counters for index and line */
while (line < COINS && fgets (buf, MAXC, fp)) /* protect array, read line */
{
char coinname[MAXC]; /* temporary array for name */
int r, value; /* declare variables in scope required */
size_t len; /* length of name */
r = sscanf (buf, "%1023s %d", coinname, &value); /* convert values */
if (r != 2) { /* validate conversion */
fprintf (stderr, "Error, line %zu in wrong format!\n\n", line);
line++; /* don't forget to increment line */
continue; /* before continuing to read next line */
}
len = strlen (coinname); /* get length of name */
name[index] = malloc (len + 1); /* allocate len + 1 chars */
if (name[index] == NULL) { /* validate allocation */
perror ("malloc-name[index]");
return index;
}
memcpy (name[index], coinname, len + 1); /* copy to name[index] */
cash[index] = value; /* assign to cash[index] */
index++; /* increment index */
line++; /* increment line */
}
return index; /* return index */
}
(注意: 将 return 类型从 void
更改为 size_t
以启用 return从文件中读取的名称和值的数量。始终为任何可以成功或失败的函数提供有意义的 return 类型,例如,除了简单的打印或自由函数之外的任何东西)
您还有另一个存储 name
的选项。您可以将 name
声明为二维字符数组,而不是使用 malloc()
(或 calloc()
或 realloc()
)分配存储空间,每行的存储空间足以容纳最长的名称(+1) 个字符。这取决于名称之间的长度变化,可能需要比完全分配以容纳每个 name
多得多的存储空间。它通过消除分配的需要确实简化了一些事情。由你决定。
现在您可以将函数中的 return 分配给 main()
中的变量,这样您就可以知道有多少对 name
和 cash
被成功读取,例如
int main (int argc, char **argv)
{
int cash[COINS];
char *name[COINS] = {NULL};
size_t ncoins = 0;
/* use filename provided as 1st argument (coins.txt by default) */
FILE *fp = fopen (argc > 1 ? argv[1] : "coins.txt", "r");
if (!fp) { /* validate file open for reading */
perror ("file open failed");
return 1;
}
ncoins = ReadFile (fp, cash, name);
fclose (fp);
for (size_t i = 0; i < ncoins; i++) {
printf ("%-12s %3d\n", name[i], cash[i]);
free (name[i]); /* free mem when done */
}
}
示例输入文件
$ cat dat/name_coins.txt
penny 20
nickle 3
dime 8
quarter 15
half-dollar 5
dollar 4
示例Use/Output
$ ./bin/name_coins dat/name_coins.txt
penny 20
nickle 3
dime 8
quarter 15
half-dollar 5
dollar 4
内存Use/Error检查
在您编写的任何动态分配内存的代码中,您对分配的任何内存块负有 2 责任:(1) 始终保留指向内存块的起始地址 因此,(2) 当不再需要它时可以释放。
您必须使用内存错误检查程序来确保您不会尝试访问内存或写入 beyond/outside 您分配的块的边界,尝试读取或基于未初始化的条件跳转值,最后,确认您释放了所有已分配的内存。
对于Linux valgrind
是正常的选择。每个平台都有类似的内存检查器。它们都很简单易用,只需运行你的程序就可以了。
$ valgrind ./bin/name_coins dat/name_coins.txt
==5870== Memcheck, a memory error detector
==5870== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==5870== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==5870== Command: ./bin/name_coins dat/name_coins.txt
==5870==
penny 20
nickle 3
dime 8
quarter 15
half-dollar 5
dollar 4
==5870==
==5870== HEAP SUMMARY:
==5870== in use at exit: 0 bytes in 0 blocks
==5870== total heap usage: 9 allocs, 9 frees, 5,717 bytes allocated
==5870==
==5870== All heap blocks were freed -- no leaks are possible
==5870==
==5870== For counts of detected and suppressed errors, rerun with: -v
==5870== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
始终确认您已释放所有分配的内存并且没有内存错误。
检查一下,如果您还有其他问题,请告诉我。
此外,您通常需要打开并验证报告是否在调用函数内部打开以供研究,并绕过 FILE* 指针指向打开循环作为函数的争议 page。如果文档打开失败,就不用先做特征名了。
当我 运行 此代码出现分段错误时,我不知道为什么会这样,我试图从文本文件的每一行中读取姓名和现金金额,然后将其放入成一个数组。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void ReadFile(int *cash, char **name)
{
int r, line = 0;
FILE *fp;
fp = fopen("coins.txt", "r");
if (fp == NULL)
{
printf ("Error opening the file\n\n'");
exit(EXIT_FAILURE);
} else {
while (line <= 14)
{
r = fscanf(fp, "%s %d\n", name[line], &cash[line]);
if (r != 2)
{
printf ("Error, line %d in wrong format!\n\n", line);
}
//printf("Name: %s, Cash: $%d\n",name, *cash);
line++;
}
fclose(fp);
}
}
int main()
{
int cash[14];
char *name[14] = { "" };
ReadFile(cash,name);
//printf("%s\n", name[0]);
}
gdb 非常适合查找此类问题。
segfault$ gcc -ggdb segfault.c -o segfault
segfault$ gdb segfault
GNU gdb (Ubuntu 8.1.1-0ubuntu1) 8.1.1
[...]
Reading symbols from segfault...done.
(gdb) break segfault.c:20
Breakpoint 1 at 0x896: file segfault.c, line 20.
(gdb) run
Starting program: /tmp/segfault/segfault
Breakpoint 1, ReadFile (cash=0x7fffffffdd80, name=0x7fffffffddc0) at segfault.c:20
20 r = fscanf(fp, "%s %d\n", name[line], &cash[line]);
(gdb) print line
= 0
(gdb) print &cash[line]
= (int *) 0x7fffffffdd80
(gdb) print name[line]
= 0x0
在 main()
中使用 char *name[14]
,您正在创建一个 char 指针数组,但您没有设置指针。因此,它们可能指向任何地方,最有可能指向无效位置(当您尝试 read/write 该位置时会导致段错误)。
在我的系统上,我们可以看到我们很幸运,未初始化的 name[0]
指向 0x0
(NULL
)。这并不能保证,我们可能会遇到比这里的段错误更有趣的错误。想象一下,例如,如果 name[0]
改为指向内存中存储 line
的位置。你会覆盖那个内存,并从你的循环中得到非常奇怪的行为。
您必须初始化 name
数组中的每个指针(在 main()
中),以便它们都指向足够大小的有效区域。
继续评论,直接错误是由于 char *name[14]
声明了一个包含 14 个指向 char 的指针的数组,这些指针 未初始化 .这意味着每个指针都指向任何地方(例如,指针所持有的地址,因为它的值指向某个不确定的内存地址)。在你可以存储任何东西之前,你必须确保你有一个有效的内存位置来保存你正在处理的任何值。对于字符串,例如 name[x]
,这意味着您需要 length + 1
个字符可用(+1
为 nul-terminating 字符提供存储, '[=21=]'
, 相当于普通的 0
)
您的 ReadFile()
功能严重不足,您的阅读能力很脆弱。任何时候读取数据行时,都应该使用 面向行的 输入函数,例如 fgets()
(或 POSIX getline()
)。这样你每次都会读取(消耗)一行输入,任何导致 匹配失败 的输入文件格式差异只会影响那一行,而不会破坏文件的读取从那时起。
永远不要硬编码文件名 或在代码中使用MagicNumbers。你不应该仅仅为了从不同的输入文件中读取而重新编译你的程序。您可以提供默认使用的文件名,否则将文件名作为程序的参数读取(这就是 int argc, char **argv
的用途),或者提示用户并将文件名作为输入。为了避免代码中的硬编码文件名和幻数:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define COINS 14 /* if you need a constant, #define one (or more) */
#define MAXC 1024
和
int main (int argc, char **argv)
{
...
FILE *fp = fopen (argc > 1 ? argv[1] : "coins.txt", "r");
...
此外,您通常希望在调用函数中打开并验证文件是否已打开以供读取,并将 FILE*
指针作为函数的参数传递给打开的流。如果文件打开失败,则无需调用函数开头。
将所有部分放在一起并使用 MAXC
个字符的缓冲区来保存在转换(并验证)转换之前从文件中读取的每一行。 (很好地验证了 fscanf()
中的 return)。
要解决您的问题,您应该将 name
值读入临时数组,然后分配存储空间,只需将临时数组中字符串的 strlen()
和 malloc (len + 1)
个字符用于 name[i]
(验证每个分配),然后从临时数组复制到 name[i]
。 (当你完成调用者的值后,你将需要释放内存(main()
这里)
您还需要 line
和 index
的单独计数器。由于您报告了发生任何故障的行(再次干得好!),因此您需要在每次迭代时增加 line
计数器,但您只想增加 name
和 [= 的索引40=] 表示成功转换,例如
size_t ReadFile (FILE *fp, int *cash, char **name)
{
char buf[MAXC]; /* temporary array to hold line */
size_t index = 0, line = 0; /* separate counters for index and line */
while (line < COINS && fgets (buf, MAXC, fp)) /* protect array, read line */
{
char coinname[MAXC]; /* temporary array for name */
int r, value; /* declare variables in scope required */
size_t len; /* length of name */
r = sscanf (buf, "%1023s %d", coinname, &value); /* convert values */
if (r != 2) { /* validate conversion */
fprintf (stderr, "Error, line %zu in wrong format!\n\n", line);
line++; /* don't forget to increment line */
continue; /* before continuing to read next line */
}
len = strlen (coinname); /* get length of name */
name[index] = malloc (len + 1); /* allocate len + 1 chars */
if (name[index] == NULL) { /* validate allocation */
perror ("malloc-name[index]");
return index;
}
memcpy (name[index], coinname, len + 1); /* copy to name[index] */
cash[index] = value; /* assign to cash[index] */
index++; /* increment index */
line++; /* increment line */
}
return index; /* return index */
}
(注意: 将 return 类型从 void
更改为 size_t
以启用 return从文件中读取的名称和值的数量。始终为任何可以成功或失败的函数提供有意义的 return 类型,例如,除了简单的打印或自由函数之外的任何东西)
您还有另一个存储 name
的选项。您可以将 name
声明为二维字符数组,而不是使用 malloc()
(或 calloc()
或 realloc()
)分配存储空间,每行的存储空间足以容纳最长的名称(+1) 个字符。这取决于名称之间的长度变化,可能需要比完全分配以容纳每个 name
多得多的存储空间。它通过消除分配的需要确实简化了一些事情。由你决定。
现在您可以将函数中的 return 分配给 main()
中的变量,这样您就可以知道有多少对 name
和 cash
被成功读取,例如
int main (int argc, char **argv)
{
int cash[COINS];
char *name[COINS] = {NULL};
size_t ncoins = 0;
/* use filename provided as 1st argument (coins.txt by default) */
FILE *fp = fopen (argc > 1 ? argv[1] : "coins.txt", "r");
if (!fp) { /* validate file open for reading */
perror ("file open failed");
return 1;
}
ncoins = ReadFile (fp, cash, name);
fclose (fp);
for (size_t i = 0; i < ncoins; i++) {
printf ("%-12s %3d\n", name[i], cash[i]);
free (name[i]); /* free mem when done */
}
}
示例输入文件
$ cat dat/name_coins.txt
penny 20
nickle 3
dime 8
quarter 15
half-dollar 5
dollar 4
示例Use/Output
$ ./bin/name_coins dat/name_coins.txt
penny 20
nickle 3
dime 8
quarter 15
half-dollar 5
dollar 4
内存Use/Error检查
在您编写的任何动态分配内存的代码中,您对分配的任何内存块负有 2 责任:(1) 始终保留指向内存块的起始地址 因此,(2) 当不再需要它时可以释放。
您必须使用内存错误检查程序来确保您不会尝试访问内存或写入 beyond/outside 您分配的块的边界,尝试读取或基于未初始化的条件跳转值,最后,确认您释放了所有已分配的内存。
对于Linux valgrind
是正常的选择。每个平台都有类似的内存检查器。它们都很简单易用,只需运行你的程序就可以了。
$ valgrind ./bin/name_coins dat/name_coins.txt
==5870== Memcheck, a memory error detector
==5870== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==5870== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==5870== Command: ./bin/name_coins dat/name_coins.txt
==5870==
penny 20
nickle 3
dime 8
quarter 15
half-dollar 5
dollar 4
==5870==
==5870== HEAP SUMMARY:
==5870== in use at exit: 0 bytes in 0 blocks
==5870== total heap usage: 9 allocs, 9 frees, 5,717 bytes allocated
==5870==
==5870== All heap blocks were freed -- no leaks are possible
==5870==
==5870== For counts of detected and suppressed errors, rerun with: -v
==5870== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
始终确认您已释放所有分配的内存并且没有内存错误。
检查一下,如果您还有其他问题,请告诉我。
此外,您通常需要打开并验证报告是否在调用函数内部打开以供研究,并绕过 FILE* 指针指向打开循环作为函数的争议 page。如果文档打开失败,就不用先做特征名了。