让我们从一些基础知识开始。从文件中读取行(或用户输入的行)时,您通常需要使用 line-oriented 输入函数,例如 fgets 或 POSIX getline 以确保您阅读一次一整行并且输入缓冲区中没有剩余的内容取决于最后使用哪个scanf 转换说明符。使用fgets,您需要提供足够大的缓冲区来容纳整行,或者根据需要动态分配realloc,直到读取整行(getline 为您处理)。您可以通过检查最后一个字符是 '\n' 字符或缓冲区的长度是否小于最大大小(两者都留给您下面)来验证是否读取了整行。
一旦您读取了一行文本,您有两个选择,您可以使用 sscanf 将缓冲区中的数字转换为整数值(或者事先知道该行中包含的数字并提供足够数量的转换说明符,或者通过单独转换每个并使用"%n" 说明符 报告为该转换提取的字符数,并将缓冲区中的开始增加该数量下一次转换)
您的另一种选择,从错误检查和报告的角度来看,迄今为止最灵活和最可靠的选择是使用strtol 并使用endptr 参数来提供指向的指针。最后一位转换 允许您走下缓冲区直接转换值。请参阅:strtol(3) - Linux manual pagestrtol 提供了区分没有转换数字的失败,发生上溢或下溢的能力(将errno设置为适当的值),并允许您通过 endptr 参数测试转换后是否还有其他字符,以控制您的值转换循环。
与您编写的任何代码一样,验证每个必要步骤将确保您能够做出适当的响应。
让我们从您的示例输入文件开始:
输入文件示例
$ cat dat/int_file.txt
5
1 78 45 32 2
当在第一行遇到单个值时,大多数时候您只想将原始值转换为 fscanf (file, "%d", &ival);,这很好,但是 -- 使用任何 scanf 的陷阱家庭是您必须考虑转换后输入缓冲区中剩余的任何字符。虽然"%d" 转换说明符将提供所需的转换,但字符提取在最后一位停止,而'\n' 未读。只要您考虑到这一事实,就可以使用fscanf 来获取第一个值。但是,您必须在此过程中验证每一步。
让我们看一个例子的开头,打开一个文件(或者从stdin读取,如果没有给出文件名),验证文件是否打开,然后验证first_num是否被读取,例如
#include <stdio.h>
#include <stdlib.h> /* for malloc/free & EXIT_FAILURE */
#include <errno.h> /* for strtol validation */
#include <limits.h> /* for INT_MIN/INT_MAX */
#define MAXC 1024 /* don't skimp on buffer size */
int main (int argc, char **argv) {
int first_num, /* your first_num */
*arr = NULL, /* a pointer to block to fill with int values */
nval = 0; /* the number of values converted */
char buf[MAXC]; /* buffer to hold subsequent lines read */
/* open file passed as 1st argument (default: stdin if no argument) */
FILE *fp = argc > 1 ? fopen (argv[1], "r"): stdin;
if (!fp) { /* validate file open for reading */
perror ("fopen-file");
exit (EXIT_FAILURE);
}
if (fscanf (fp, "%d", &first_num) != 1) { /* read/validate int */
fputs ("error: invalid file format, integer not first.\n", stderr);
exit (EXIT_FAILURE);
}
此时,您的输入缓冲区包含:
\n
1 78 45 32 2
由于您要对文件中的剩余行进行面向行的读取,您可以简单地首次调用 fgets 以读取和丢弃 '\n',例如
if (!fgets (buf, MAXC, fp)) { /* read/discard '\n' */
fputs ("error: non-POSIX ending after 1st integer.\n", stderr);
exit (EXIT_FAILURE);
}
(注意:验证。如果文件以非 POSIX 行结尾(例如,没有 '\n'),fgets 将失败,除非您正在检查,否则您很可能通过尝试稍后从没有字符要读取的文件流中读取并随后尝试从内容不确定的缓冲区中读取来调用未定义的行为)
此时您可以为first_num 的整数个数分配存储空间,并将新块的起始地址分配给arr 以填充整数值,例如
/* allocate/validate storage for first_num integers */
if (!(arr = malloc (first_num * sizeof *arr))) {
perror ("malloc-arr");
exit (EXIT_FAILURE);
}
要读取文件中的剩余值,您可以只调用一次fgets,然后转换包含在已填充缓冲区中的整数值,但只需稍加考虑,您就可以设计一种方法:将根据需要读取尽可能多的行,直到 first_num 整数被转换或遇到 EOF。无论您是在缓冲区中接受输入还是转换值,一个稳健的方法都是相同的循环不断,直到您获得所需的数据或用完数据,例如
while (fgets (buf, MAXC, fp)) { /* read lines until conversions made */
char *p = buf, /* nptr & endptr for strtol conversion */
*endptr;
if (*p == '\n') /* skip blank lines */
continue;
while (nval < first_num) { /* loop until nval == first_num */
errno = 0; /* reset errno for each conversion */
long tmp = strtol (p, &endptr, 0); /* call strtol */
if (p == endptr && tmp == 0) { /* validate digits converted */
/* no digits converted - scan forward to next +/- or [0-9] */
do
p++;
while (*p && *p != '+' && *p != '-' &&
( *p < '0' || '9' < *p));
if (*p) /* valid start of numeric sequence? */
continue; /* go attempt next conversion */
else
break; /* go read next line */
}
else if (errno) { /* validate successful conversion */
fputs ("error: overflow/underflow in conversion.\n", stderr);
exit (EXIT_FAILURE);
}
else if (tmp < INT_MIN || INT_MAX < tmp) { /* validate int */
fputs ("error: value exceeds range of 'int'.\n", stderr);
exit (EXIT_FAILURE);
}
else { /* valid conversion - in range of int */
arr[nval++] = tmp; /* add value to array */
if (*endptr && *endptr != '\n') /* if chars remain */
p = endptr; /* update p to endptr */
else /* otherwise */
break; /* bail */
}
}
if (nval == first_num) /* are all values filled? */
break;
}
现在让我们稍微解开一下。发生的第一件事是您声明使用strtol 所需的指针,并将用fgets 填充的buf 的起始地址分配给p,然后从文件中读取一行。没有必要在空行上尝试转换,所以我们测试buf 中的第一个字符,如果它是'\n',我们得到下一行:
...
if (*p == '\n') /* skip blank lines */
continue;
...
一旦你有一个非空行,你就会开始你的转换循环,并会尝试转换,直到你拥有的值的数量等于first_num,或者你到达了行的末尾。您的循环控制很简单:
while (nval < first_num) { /* loop until nval == first_num */
...
}
在循环中,您将通过在每次转换之前重置 errno = 0; 并将转换的返回分配给临时的 long int 值来完全验证您尝试使用 strtol 进行的转换。 (例如字符串到长),例如
errno = 0; /* reset errno for each conversion */
long tmp = strtol (p, &endptr, 0); /* call strtol */
进行转换后,您需要验证三个条件才能进行良好的整数转换,
- 如果没有转换任何数字,则为
p == endptr(并且根据手册页,返回设置为零)。所以要检查是否出现这种情况,可以检查:if (p == endptr && tmp == 0);
- 如果在数字转换过程中出现错误,无论发生哪个错误,
errno 都将设置为非零值,以便您使用if (errno) 检查转换错误。您还可以进一步深入了解手册页中指定的发生情况,但出于验证目的,这里知道是否发生错误就足够了;最后
- 如果数字被转换并且没有错误,你仍然没有完成。
strtol 转换为 long 的值,该值可能与 int 兼容,也可能不兼容(例如,long 在 x86_64 上是 8-bytes,而 int 是 4-bytes。所以要确保转换后的值将适合您的整数数组,您需要检查返回的值是否在INT_MIN 和INT_MAX 内在您将值分配给arr 的元素。
(注意:上面有1.,没有数字被转换并不意味着该行没有数字,它只是意味着第一个值不是数字。你应该扫描使用指针在行中前进以查找下一个+/- 或[0-9] 以确定是否存在更多数值。这就是该代码块中while 循环的目的)
一旦你有了一个好的整数值,记住endptr 将被设置为最后一个数字转换后的下一个字符。快速检查 *endptr 是否不是 nul-terminating 字符而不是行尾将告诉您是否仍有可用于转换的章程。如果是这样,只需更新p = endptr 以便您的指针现在指向转换后的最后一个数字并重复。 (此时您也可以使用上面使用的相同 while 循环向前扫描以确定是否存在另一个数值 - 这留给您)
循环完成后,您只需检查nval == first_num 是否需要继续收集值。
总而言之,你可以做类似的事情:
#include <stdio.h>
#include <stdlib.h> /* for malloc/free & EXIT_FAILURE */
#include <errno.h> /* for strtol validation */
#include <limits.h> /* for INT_MIN/INT_MAX */
#define MAXC 1024 /* don't skimp on buffer size */
int main (int argc, char **argv) {
int first_num, /* your first_num */
*arr = NULL, /* a pointer to block to fill with int values */
nval = 0; /* the number of values converted */
char buf[MAXC]; /* buffer to hold subsequent lines read */
/* open file passed as 1st argument (default: stdin if no argument) */
FILE *fp = argc > 1 ? fopen (argv[1], "r"): stdin;
if (!fp) { /* validate file open for reading */
perror ("fopen-file");
exit (EXIT_FAILURE);
}
if (fscanf (fp, "%d", &first_num) != 1) { /* read/validate int */
fputs ("error: invalid file format, integer not first.\n", stderr);
exit (EXIT_FAILURE);
}
if (!fgets (buf, MAXC, fp)) { /* read/discard '\n' */
fputs ("error: non-POSIX ending after 1st integer.\n", stderr);
exit (EXIT_FAILURE);
}
/* allocate/validate storage for first_num integers */
if (!(arr = malloc (first_num * sizeof *arr))) {
perror ("malloc-arr");
exit (EXIT_FAILURE);
}
while (fgets (buf, MAXC, fp)) { /* read lines until conversions made */
char *p = buf, /* nptr & endptr for strtol conversion */
*endptr;
if (*p == '\n') /* skip blank lines */
continue;
while (nval < first_num) { /* loop until nval == first_num */
errno = 0; /* reset errno for each conversion */
long tmp = strtol (p, &endptr, 0); /* call strtol */
if (p == endptr && tmp == 0) { /* validate digits converted */
/* no digits converted - scan forward to next +/- or [0-9] */
do
p++;
while (*p && *p != '+' && *p != '-' &&
( *p < '0' || '9' < *p));
if (*p) /* valid start of numeric sequence? */
continue; /* go attempt next conversion */
else
break; /* go read next line */
}
else if (errno) { /* validate successful conversion */
fputs ("error: overflow/underflow in conversion.\n", stderr);
exit (EXIT_FAILURE);
}
else if (tmp < INT_MIN || INT_MAX < tmp) { /* validate int */
fputs ("error: value exceeds range of 'int'.\n", stderr);
exit (EXIT_FAILURE);
}
else { /* valid conversion - in range of int */
arr[nval++] = tmp; /* add value to array */
if (*endptr && *endptr != '\n') /* if chars remain */
p = endptr; /* update p to endptr */
else /* otherwise */
break; /* bail */
}
}
if (nval == first_num) /* are all values filled? */
break;
}
if (nval < first_num) { /* validate required integers found */
fputs ("error: EOF before all integers read.\n", stderr);
exit (EXIT_FAILURE);
}
for (int i = 0; i < nval; i++) /* loop outputting each integer */
printf ("arr[%2d] : %d\n", i, arr[i]);
free (arr); /* don't forget to free the memory you allocate */
if (fp != stdin) /* and close any file streams you have opened */
fclose (fp);
return 0;
}
(注意:退出读取和转换循环后对if (nval < first_num)的最终检查)
使用/输出示例
使用您的示例文件,您将获得以下信息:
$ ./bin/fgets_int_file dat/int_file.txt
arr[ 0] : 1
arr[ 1] : 78
arr[ 2] : 45
arr[ 3] : 32
arr[ 4] : 2
为什么要惹上额外的麻烦?
通过彻底了解转换过程并解决几行额外的麻烦,您最终会得到一个例程,该例程可以为您需要的任意数量的整数提供灵活的输入处理,而不管输入文件格式如何。让我们看一下输入文件的另一种变体:
更具挑战性的输入文件
$ cat dat/int_file2.txt
5
1 78
45
32 2 144 91 270
foo
处理从该文件中检索相同的前五个整数值需要进行哪些更改? (提示:无 - 尝试一下)
更具挑战性的输入文件
如果我们再次加大赌注呢?
$ cat dat/int_file3.txt
5
1 two buckle my shoe, 78 close the gate
45 is half of ninety
foo bar
32 is sixteen times 2 and 144 is a gross, 91 is not prime and 270 caliber
baz
从该文件中读取前 5 个整数值需要进行哪些更改? (提示:无)
但我想指定开始阅读的行
好的,让我们用另一个输入文件来配合这个例子。说:
从给定行读取的示例输入
$ cat dat/int_file4.txt
5
1,2 buckle my shoe, 7,8 close the gate
45 is half of ninety
foo bar
32 is sixteen times 2 and 144 is a gross, 91 is not prime and 270 caliber
baz
1 78 45 32 2 27 41 39 1111
a quick brown fox jumps over the lazy dog
我需要改变什么?唯一需要的更改是跳过第一行10 并在11 行开始转换循环。为此,您需要添加一个变量来保存行的值以开始读取整数(例如rdstart)和一个变量来保存行数,以便我们知道何时开始读取(例如linecnt),例如
int first_num,
*arr = NULL,
nval = 0,
rdstart = argc > 2 ? strtol(argv[2], NULL, 0) : 2,
linecnt = 1;
(注意: 开始读取整数的行被用作程序的第二个参数,或者如果没有指定行,则使用默认行 2 -- 是的,你应该对 strtol 的这种用法应用相同的完整验证,但我留给你)
还有什么需要改变的?不多。与其简单地读取和丢弃fscanf 留下的'\n',不如执行linecnt-1 次(或者只是在初始化linecnt = 1; 之后的linecnt 时间)。为此,只需将您对fgets 的第一次调用包装在一个循环中(并更改错误消息以使其有意义),例如
while (linecnt < rdstart) { /* loop until linecnt == rdstart */
if (!fgets (buf, MAXC, fp)) { /* read/discard line */
fputs ("error: less than requested no. of lines.\n", stderr);
exit (EXIT_FAILURE);
}
linecnt++; /* increment linecnt */
}
就是这样。 (请注意,它也将继续处理前 3 个输入文件,只需省略第二个参数...)
示例输出从第 11 行开始
有效吗?
$ ./bin/fgets_int_file_line dat/int_file4.txt 11
arr[ 0] : 1
arr[ 1] : 78
arr[ 2] : 45
arr[ 3] : 32
arr[ 4] : 2
查看一下,如果您还有其他问题,请告诉我。有很多方法可以做到这一点,但到目前为止,如果您了解如何使用strtol(所有strtoX 函数的工作方式都非常相似),您将在处理数字转换方面遥遥领先。