如何使用C语言自下而上读取行?

14

我需要从下往上读取文件中列出的数字。我应该如何使用C语言实现?

文件格式如下:

4.32
5.32
1.234
0.123
9.3
6.56
8.77
例如,我想读取最后三个数字。它们必须是 float 类型。
8.77
6.56
9.3

附注:实际上我需要一种使用fseek等操作文件指针位置的解决方案。


https://dev59.com/RVnUa4cB1Zd3GeqPeuCR - Bob Thomas
@BobThomas 我在提问之前已经尝试过了。但它将变量保留在字符指针中,我无法根据我的要求进行编辑。 - Erol Guzoğlu
5个回答

27
重要的是要理解,现代操作系统都不会跟踪文件中换行符的位置。(VMS可以,我很确定一些IBM 大型机操作系统也可以,但您可能不会使用它们。)因此,无法查找行边界。也无法逆序字节读取。
因此,以逆序方式读取文件中最后三个数字的最简单方法是正向读取整个文件,并在缓冲区中保留最近看到的三个数字。当达到 EOF 时,只需反向处理该缓冲区。
更高效但复杂得多的技术是猜测文件中最后三个数字之前的接近位置;跳转到该位置,然后丢弃字符直到遇到换行符;从那一点开始使用上面段落中的技术。如果您猜错了并且缓冲区中的数字少于三个,请重新猜测。
第三种方法是使用 fseek(带有 SEEK_END)和 fread 读取文件的最后1024个字节左右,将指针设置为块的末尾,并逆向分析它。这将非常有效,但比前一个建议还有更多的头疼角落。(如果文件的最后三行合计超过1024个字节,您应该做什么?)
顺便说一下,在 C 语言中读取浮点数的正确方法是使用 fgetsstrtod。不要使用 atofscanf,因为 atof 不会告诉您有关语法错误的信息,而 scanf 在溢出时触发未定义行为。

顺带提一下,如果你有shell实用程序tac(这是GNUism),最简单的选择是编写处理标准输入中前三个数字的程序,然后作为tac < input.file | ./a.out调用它。 浏览代码让我相信,tac实现了我的“第三种方法”,加入了一些额外的巧妙。


你介意详细说明一下scanf()的未定义行为吗?或者提供一个相关链接?我尝试在SO和Google上搜索,但没有找到任何信息。 - user12205
3
我的标准回答在这里:https://dev59.com/Z4Dba4cB1Zd3GeqPIsdb#24318630(阅读所有评论!) 标准中的有问题的句子是C99 7.19.6.2p10:“如果[接收转换结果的对象]没有适当的类型,或者如果转换结果无法表示为对象,则行为未定义。” 强调是我的。 - zwol
tac?不是tail -n 3 - Kijewski
2
@kay OP确实说他们想要数字以相反的顺序,但你的想法也很好(他们总是可以自己翻转它)。 - zwol

6

显然的做法是将它们全部读取,放入一个数组中,然后获取最后三个。


7
如果文件非常大且不需要前几项,则此方法不实用。 - user12205
@JoshuaByer,实际上你是对的,但我在想解决这个问题的最佳方法。那个方法会在内存中创建不必要的空间。我是不是对的? - Erol Guzoğlu
@ErolGuzoğlu,你不必真的阅读它们,你可以数一下然后重新开始并跳到倒数第三个。 - Joshua Byer

3
“从文件中倒序读取”的概念不存在。
一种解决方案是读取所有数字,仅存储最后读取的三个数字。
float numbers[3];
char line[100]; // Make it large enough
int = 0;
char* end;
for ( ; ; ++i )
{
    i %= 3; // Make it modulo 3.
    if ( fgets(line, 100, stdin) == NULL )
    {
       // No more input.
       break;
    }

    float n = strtof(line, &end);
    if ( line == end )
    {
       // Problem converting the string to a float.
       // Deal with error
       break;
    }

    if ( errno == ERANGE )
    {
       // Problem converting the string to a float within range.
       // Deal with error
       break;
    }

    numbers[i] = n;
}

如果文件中至少有三个数字,则最后三个数字分别为 numbers[i]numbers[(i+2)%3]numbes[(i+1)%3]

3
永远不要使用 scanf 进行任何操作。 - zwol
我知道使用 scanf%s 存在的危险。您有没有理由相信使用 scanf%f 也存在危险? - R Sahu
是的:正如我在答案中所说,输入溢出会触发未定义行为。 - zwol
我的固定抱怨在这里:https://dev59.com/Z4Dba4cB1Zd3GeqPIsdb#24318630(请阅读所有评论!)标准的有问题的句子是C99 7.19.6.2p10:“如果[将接收转换结果的对象]没有适当的类型,或者如果转换的结果无法表示为对象,则行为未定义。” 强调是我的。 - zwol
1
@GrijeshChauhan,感谢提供链接。我已经更新了答案,以解决scanf的问题。 - R Sahu
显示剩余7条评论

1
首先,打开文件:
FILE* fp = fopen(..., "r");

然后,跳转到文件结尾(EOF):
fseek(fp, 0, SEEK_END);

现在,返回 X 行:
int l = X, ofs = 1;
while (l && fseek(fp, ofs++, SEEK_END) == 0) {
    if (fgetc(fp) == '\n' && ofs > 2) l--;
}

最后,从当前位置读取X个数字:
float numbers[X];
for(int p = 0; p < X; p++) fscanf(fp, "%f", &numbers[p];

0

我用以下代码解决了我的问题。我读取了文件的后半部分。

  FILE *fp = fopen("sample.txt","r");

  if( fp == NULL )
  {
    perror("Error while opening the file.\n");
    exit(EXIT_FAILURE);
  }

  int size=0;
  char ch;

  //Count lines of file
  while(( ch = fgetc(fp) ) != EOF )
  {
    if (ch=='\n') { size++; }
  }

  int i;
  float value;

  //Move the pointer to the end of the file and calculate the size of the file.
  fseek(fp, 0, SEEK_END);
  int size_of_file = ftell(fp);

  for (i=1; i<=size/2; i++)
  {
    //Set pointer to previous line for each i value.
    fseek(fp, (size_of_file-1)-i*5, SEEK_SET);
    fscanf(fp, "%f", &value);
  }

2
这种方法会读取整个文件以确定大小,然后查找并读取后一半。这是一种“两全其美”的方法,但你不会从额外的复杂性中获得任何好处。我建议你回到这里的第一种方法,因为它不仅更简单,而且更快。 - Boris the Spider
1
有一天,你可能需要从管道/套接字中读取数据。不幸的是,它们没有大小,并且无法进行寻址。 - Joker_vD
@Joker_vD,问题很明确:“我需要使用fseek操作文件指针位置的解决方案”。 - Guillaume

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接