为啥在 putw 在 C 中扩展文件后使用 fread?

Posted

技术标签:

【中文标题】为啥在 putw 在 C 中扩展文件后使用 fread?【英文标题】:Why using fread after putw expands the file in C?为什么在 putw 在 C 中扩展文件后使用 fread? 【发布时间】:2021-11-28 09:30:15 【问题描述】:

我试图使用fread() 从文件中读取一些数据,但我意识到我的文件不断增长。但是由于我只是从文件中读取,所以这种行为对我来说是不合理的。所以我写了这段代码,发现如果我使用putw() 将数据写入文件,然后尝试从该文件中读取(在关闭和重新打开文件之前),fread 扩展文件以便能够从中读取.

操作系统:Windows 8.1 编译器:MinGW gcc

代码:

typedef struct 
    int a;
    int b;
 A;

int main() 
    FILE* f = fopen("file", "wb");
    A a;
    a.a = 2;
    a.b = 3;
    putw(1, f);
    fwrite(&a, sizeof(A), 1, f);
    fclose(f); // To make sure that wb mode and fwrite are not responsible
    f = fopen("file", "rb+");
    printf("initial position: %ld\n", ftell(f));
    putw(1, f);
    printf("position after putw: %ld\n", ftell(f));
    printf("fread result: %d\n", fread(&a, sizeof(A), 1, f));
    printf("position after 1st fread: %ld\n", ftell(f));
    printf("fread result: %d\n", fread(&a, sizeof(A), 1, f));
    printf("position after 2nd fread: %ld\n", ftell(f));
    fclose(f);
    remove("file");
    return 0;

结果:

initial position: 0
position after putw: 4
fread result: 1
position after 1st fread: 12
fread result: 1
position after 2nd fread: 20

【问题讨论】:

不是我得到的输出。系统信息是什么(例如操作系统、编译器)?写入和读取之前和之后的文件位置是什么(在适当的地方添加printf("...: %ld\n", ftell(f));,并显示输出)? @outis 操作系统:Windows 8.1 - 编译器:gcc(mingw) - 和位置:[开始:0 - putw 后:4 - 第一次 fread 12 - 第二次 fread 后:20] 标准 C 要求您调用 fflush 或在写入和读取 FILE* 之间显式重新定位。有些实现可能不在乎,但 UB 就是 UB。 @NavidNaseri:请编辑对原始问题的说明,而不是作为 cmets 发布。 【参考方案1】:

问题

代码中有几个问题可能导致undefined behavior:

    混合面向宽和面向字节的函数, 在写入宽向流的字符之后使用带有位置的内容(导致潜在的帧错误),以及 在输出函数之后调用输入函数,而没有干预 fflush

问题 2 难以简洁地表达;下面引用的 C 标准部分应该更清楚。

与方向相关的函数行为在 C17(草案)§§ 7.21.2 4,5 中定义:

4 每个流都有一个方向。在流与外部文件关联之后,但在对其执行任何操作之前,流是没有方向的。一旦一个宽字符输入/输出函数被应用到一个没有方向的流上,这个流就变成了一个宽方向的流。类似地,一旦一个字节输入/输出函数被应用到一个没有方向的流上,这个流就变成了一个面向字节的流。只有调用 freopen 函数或 fwide[*] 函数才能改变流的方向。 (成功调用 freopen 会移除任何方向。)

5 字节输入/输出函数不应应用于面向宽的流,宽字符输入/输出函数不应应用于面向字节的流。其余的流操作不影响流的方向,也不受其影响,但以下附加限制除外: […]

——对于面向宽的流,在成功调用文件定位函数后,在文件结束之前离开文件位置指示符,宽字符输出函数可以覆盖部分多字节字符;任何超出写入字节的文件内容都是不确定的。

§ 7.19.5.3 6 (fopen) 涵盖了在不冲洗的情况下混合输出和输入:

6 当文件以更新模式打开时('+' 作为上述 mode 参数值列表中的第二个或第三个字符),输入和输出可以在关联的流上执行。但是,如果没有对 fflush 函数或文件定位函数(fseekfsetpos、或倒带),[…]

这些也列在未定义行为的大列表中,附件 J.2:

在以下情况下行为未定义:

[…]

- 字节输入/输出函数应用于面向宽的流,或宽字符输入/输出函数应用于面向字节的流 (7.21.2)。

— 使用文件的任何部分,而不是写入到面向宽的流 (7.21.2) 中的最新宽字符。

[…]

——更新流上的输出操作之后是输入操作,没有对 fflush 函数或文件定位函数的干预调用,[…] (7.19.5.3)。

解决方案

有两种方法:

在宽字符和面向字节的函数之间使用freopen,或者 在读写之间只使用面向字节的函数(例如fwrite)和fflush(或fseek,根据标准)。

注意fwide只能设置无向流的方向,无法解决问题;一旦设置了流的方向,就只能使用freopen 清除它。

freopen解决方案

freopen 自己解决了 3 个问题中的 2 个:

    它清除了宽函数和字节函数之间的方向,因此它们不会混合。 freopen 本身会在文件的尾部留下任何垃圾字符,尽管在给定的示例中它不应该成为问题。如果这是一个问题,则流必须首先是 truncated(尽管这不适用于示例)。 freopen 调用fflush,所以输出不直接跟在输入后面。
    const char* fName = "file";
    f = fopen(fName, "rb+");
    putw(1, f);
    // truncate here, if applicable
    if (freopen(NULL, "rb+", f)) 
        int nA;
        fread(&nA, sizeof(nA), 1, f);
        printf("fread result: %d\n", fread(&a, sizeof(A), 1, f));
        printf("position after 1st fread: %ld\n", ftell(f));
        printf("fread result: %d\n", fread(&a, sizeof(A), 1, f));
        printf("position after 2nd fread: %ld\n", ftell(f));
    

面向字节的 I/O 解决方案

putw 替换为fwrite 并添加对fflush 的调用可解决所有三个问题:

    不再使用宽方向函数,因此没有方向混合。 没有使用宽向功能,您不会遇到第 7.21.2 5 节中提到的帧错误问题。 fflush 明确解决 § 7.19.5.3 6.
    const char* fName = "file";
    f = fopen(fName, "rb+");
    int nA = 1;
    fwrite(&nA, sizeof(nA), 1, f);
    fflush(f);
    printf("fread result: %d\n", fread(&a, sizeof(A), 1, f));
    printf("position after 1st fread: %ld\n", ftell(f));
    printf("fread result: %d\n", fread(&a, sizeof(A), 1, f));
    printf("position after 2nd fread: %ld\n", ftell(f));

PS

在玩具问题的上下文中,调用putw 后跟fread 并没有多大意义,因为它会在生产中完成(尽管这并不重要,因为它的目的是说明一个问题)。因此,上述解决方案可能无法解决将putwfread 混合在一起的生产代码方面。

示例代码中只显示了最少的错误处理。

【讨论】:

以上是关于为啥在 putw 在 C 中扩展文件后使用 fread?的主要内容,如果未能解决你的问题,请参考以下文章

为啥我无法打开/读取从 Python 调用的 C 扩展名中的 txt 文件?

为啥在使用扩展语法复制对象后 getter/setter 不再工作?

检查C代码时cppcheck不扩展宏?

python源程序文件的扩展名为啥

为啥我不能在 cx_Freeze 中创建线程池?

使用awk命令循环查找并修改后输出。