在C语言中使用fread()函数读取结构体

8

我需要使用fread/fwrite函数来完成任务。我写了下面的代码:

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

struct rec{
    int account;
    char name[100];
    double balance;
};

int main()
{
    struct rec rec1;
    int c;

    FILE *fptr;
    fptr = fopen("clients.txt", "r");

    if (fptr == NULL)
        printf("File could not be opened, exiting program.\n");
    else
    {
        printf("%-10s%-13s%s\n", "Account", "Name", "Balance");
        while (!feof(fptr))
        {
            //fscanf(fptr, "%d%s%lf", &rec.account, rec.name, &rec.balance);
            fread(&rec1, sizeof(rec1),1, fptr);
            printf("%d %s %f\n", rec1.account, rec1.name, rec1.balance);
        }
        fclose(fptr);
    }
    return 0;
}

clients.txt文件

100 Jones 564.90
200 Rita 54.23
300 Richard -45.00

输出结果

账户   姓名         余额
540028977 Jones 564.90
200 Rita 54.23
300 Richard -45.00╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠
╠╠ü☻§9x°é -92559631349317831000000000000000000000000000000000000000000000.000000
按任意键继续. . .

我可以使用fscanf(已被我注释掉),但我需要使用fread / fwrite。

  1. 为什么Jones的账户余额会从一个巨大的数字开始?
  2. 为什么还有垃圾字符?难道feof不能阻止这种情况吗?
  3. 使用这种方法或fscanf方法有什么缺点?

如何解决这些问题? 非常感谢您的帮助!


4
请参阅“为什么‘while(!feof(file))’总是错误的?”(原文链接:https://dev59.com/jG035IYBdhLWcg3wbPU5)。 - Sourav Ghosh
4
fread函数读取二进制数据。而你的clients.txt是一个文本文件,因此没有必要将其作为原始二进制数据进行读取。 - Jabberwocky
2
如果你需要使用fread,那么你必须将整个文件读入缓冲区,然后自己解析该缓冲区。 - Jabberwocky
2
@dasblinkenlight 不要关闭这个问题。很明显,OP的问题不在于feof()。OP的问题是他试图将文本文件读入二进制结构中。 - jforberg
2
好的一面是,现在每个人都知道你正在使用小端序的机器。 - EOF
显示剩余9条评论
1个回答

9
正如评论所说,fread读取文件中的字节而不进行任何解释。文件clients.txt由50个字符组成,第一行有16个字符,第二行有14个字符,第三行有18个字符,加上两个换行符。
(您的clients.txt在第三行后面没有换行符,很快就会看到。) 换行符是单个字节\n在UNIX或Mac OS X机器上,但在Windows机器上可能是两个字节\r\n,因此字符数为50或51。以下是十六进制ASCII字节序列:
3130 3020 4a6f 6e65 7320 3536 342e 3930     100 Jones 564.90
0a32 3030 2052 6974 6120 3534 2e32 330a     \n200 Rita 54.23\n
3330 3020 5269 6368 6172 6420 2d34 352e     300 Richard -45.
3030                                        00

您的fread语句将这些字节直接复制到您的rec1数据结构中,而不进行任何解释。该结构以int account;开始,表示将前四个字节解释为int。正如其中一条评论所指出的那样,您正在一个小端机器上运行程序(最有可能是Intel机器),因此最不重要的字节是第一个字节,最重要的字节是第四个字节。因此,您的fread指示将四个ASCII字符"100 "序列解释为四字节整数0x20303031,其十进制值为540028977。您结构体的下一个成员是char name [100];,这意味着rec1中的下一个100个数据字节将是name。但是,fread被告知读取sizeof(rec1)=112字节(4字节账户,100字节名称,8字节余额)。由于您的文件仅有50(或52)个字符,fread只能填充那么多字节的rec1。如果您没有丢弃它,fread的返回值将告诉您读取停止在请求的字节数之前。由于您遇到了EOF,feof调用在第一次通过后就退出了循环,一口气吞下了整个文件。
你的所有输出都是由第一次也是唯一一次调用 fprintf 产生的。数字 540028977 和随后的空格是由 "%d "rec1.account 参数产生的。接下来的部分只有部分确定,并且你很幸运: "%s" 格式说明符和相应的 rec1.name 参数将打印下一个字符作为 ASCII 直到找到一个 \0 字节为止。因此,输出将以文件剩余的 50-4 (或 52-4) 个字符开始 -- 包括两个换行符 -- 并且可能会无限地继续下去,因为在你的文件中没有 \0 字节(或任何文本文件中都没有),这意味着在打印文件的最后一个字符之后,你所看到的是自动变量 rec1 在程序启动时包含的任何垃圾内容。(这种意外的输出类似于 OpenSSL 中著名的 heartbleed bug。)你很幸运,垃圾内容包含了几十个字符后就有了一个 \0 字节。请注意,printf 不知道 rec1.name 只被声明为一个 100 字节的数组 -- 它只得到了指向 name 开始的指针 -- 你的责任是保证 rec1.name 包含一个终止的 \0 字节,但你从未这样做过。
我们可以知道更多。数字 -9.2559631349317831e61 (在 "%f" 格式中相当丑陋) 是 rec1.balance 的值。该 double 值的 8 个字节在 IEEE 754 机器上 (如你的 Intel 和所有现代计算机) 中的十六进制为 0xcccccccccccccccc。在与 rec1.name 相对应的 "%s" 输出中出现了六十四个奇特的 符号,而只剩下 100-46 = 54 个字符,因此你的 "%s" 输出已经超出了 rec1.name 的末尾,并包括了 rec1.balance,并且我们得知你的终端程序将非 ASCII 字符 0xcc 解释为 。有许多方法来解释大于 127 (0x7f) 的字节; 在 latin-1 中,它可能是 &Igrave;。图形字符 是古老的 MS-DOS 字符集、Windows 代码页 437 中的 0xcc (204) 字节的表示。不仅你在运行一个 Intel 机器,它是一个 Windows 机器(当然最有可能的情况一开始就是这样)。
那回答了你的前两个问题。我不确定我理解你的第三个问题。"drawbacks" 我希望是显而易见的。
至于如何修复它,使用fread读取和解释文本文件没有一个合理简单的方法。为了做到这一点,你需要复制大部分libc中的fscanf函数代码。唯一明智的方法是首先使用fwrite创建一个二进制文件;然后fread将自然地读取它。因此必须有两个程序——一个用于写入二进制clients.bin文件,另一个用于读取它。当然,这并不能解决第一个程序的数据应该从哪里来的问题。它可以通过使用fscanf读取clients.txt来完成。或者可以在fwrite程序的源代码中包含它,例如通过初始化一个类似于这样的struct rec数组:
struct rec recs[] = {{100, "Jones", 564.90},
                     {200, "Rita", 54.23},
                     {300, "Richard", -45.00}};

或者它可能来自于读取MySQL数据库,或者... 它不太可能来自于二进制文件(可以)通过fread轻松阅读。


1
哇,谢谢您花时间非常详细地解释每个部分! - user153882

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