当我认为应该在 linux 上时 c fputc 不会返回错误,在 Windows 上按预期工作

Aqo*_*Aqo 2 c linux gcc

我正在尝试使用 stdio.h 中的 c 库函数 fputc

我假设它应该根据https://linux.die.net/man/3/fputc上的规范工作

具体来说,感兴趣的部分是:

  • fputc() 将转换为无符号字符的字符 c 写入流。
  • fputc()、putc() 和 putchar() 将作为无符号字符写入的字符在错误时返回为 int 或 EOF。

根据这些信息,我假设如果 fputc 成功将字符写入提供的流,我应该收到一个等于写入字符的返回值,如果写入流失败,我应该得到 EOF 的值。

这是我的代码:

// COMPILE
//    gcc -Wall -Wextra -Werror -O3 -s ./fputc.c -o ./fputc-test
// RUN
//    ./fputc-test

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

void print_error_exit();

int main() {
    FILE * fp;
    
    // ensure file exists
    if((fp = fopen("file", "w")) == NULL) print_error_exit();
    
    // close stream
    if(fclose(fp) == EOF) print_error_exit();
    
    // open file in read-only mode
    if((fp = fopen("file", "r")) == NULL) print_error_exit();
    
    // close stream
    if(fclose(fp) == EOF) print_error_exit();
    
    printf("EOF is: %d\n", EOF);
    // use fputc on a read-only closed stream (should return error, right?)
    printf("fputc returned: %d\n", fputc(97, fp)); // 97 is ascii for 'a'
    // expected:
    //    prints EOF (probably -1)
    // actual:
    //    prints 97 on linux with both gcc and clang (multiple versions tested)
    //    prints -1 on windows with mingw-gcc
    
    return EXIT_SUCCESS;
}

void print_error_exit() {
    fprintf(stderr, "%s\n", strerror(errno));
    exit(EXIT_FAILURE);
}
Run Code Online (Sandbox Code Playgroud)

我已经使用 gcc 8.1.0、gcc 8.3.0、gcc 9.3.0 和 clang 7.0.1 在 Ubuntu 20、Debian 9、Windows 10 上测试了代码。在 Windows 上,我使用了 mingw。

我发现的唯一趋势是 fputc 在 Windows 上返回我期望的结果,而不是在 linux 上返回我期望的结果。

以下哪一项是正确的?

  • 我的代码中有一个错误(如果有,请解释原因并发布修复代码)
  • 我没有正确理解规范(如果是这样,请更好地解释)
  • 为 linux 编译时,gcc 和 clang 中都有一个错误(在哪里报告?)
  • linux(某些发行版或所有发行版)存在一个错误(在哪里报告?)

请帮助我理解这一点。当我尝试在关闭的流上使用 fputc 时,为什么 fputc 不返回错误代码 (EOF),更不用说打开仅用于读取的流了?

Kam*_*Cuk 8

您的代码表现出未定义的行为。

J.2 未定义行为

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

在关闭相关文件后使用指向 FILE 对象的指针的值 (7.21.3)。

关闭文件后,使用该FILE对象无效,并且指针本身具有不确定的值。


Nat*_*dge 6

C17 7.21.3(4):

关联文件关闭后,指向 FILE 对象的指针的值是不确定的。

所以在你的fputc调用中,fp有一个不确定的值。该标准只定义了fputcwhenfp指向输出流的行为,我们不能说是这种情况,所以行为是未定义的。

文本有点误导,因为在典型系统上,指针的值在传递给 时不会改变fclose;毕竟,C 按值传递参数,所以即使它想FILE *fp; ...; fclose(fp);改变你的fp变量的值也不能。它仍然像往常一样指向相同的地址。但是位于该地址的数据肯定会变得不确定,并且不再需要系统将其解释为流。


以下是 Linux 幕后发生的事情。不用说,这些都是实现细节,您不应在任何程序中依赖它们。

你可以在这里看到 Linux 的fputc实际作用。该fp参数指向一个FILE对象,该对象包含一个指向缓冲区的指针和指示剩余空间的数字。如果缓冲区中有空间,则在那里写入字符;这没有办法失败。只有当缓冲区已满并且需要通过操作系统写出数据时,才有可能fputc返回错误。

现在,当您fclose处理文件时,缓冲区和FILE对象会被简单地释放,使用free()或 类似的机制。如果在此期间没有其他人分配任何内存,则这些对象的内容可能仍在内存中并且在您调用fputc. 在这样做之前,这些对象不会以任何方式被标记为无效,因为除非他们正在访问已释放的内存,否则没有人会看到该标志,并且任何正确的程序都不应该这样做。因此,当您调用 时fputc,指向的内存内容fp仍然看起来与有效FILE对象完全一样,但缓冲区未满,因此fp在那里写入字符并返回成功 - 因为毕竟,将字符写入缓冲区不会失败。但实际上你现在已经写入释放的内存,可能会导致各种麻烦。

所以它类似于 malloc 的“释放后使用”错误:系统相信你在释放资源后不会使用它,但如果你这样做了,它也不承诺会抓住你。

您测试的其他系统很可能FILE在解除分配之前设置的对象中具有某种“无效”标志,并且他们fputc可能会测试此标志。所以如果FILE对象没有被其他东西覆盖,标志的值可能仍然存在于内存中,因此fputc失败。但它仍然是错误的,因为它必须读取释放的内存才能看到标志。如果您在这两者之间做更多的工作,包括分配更多内存并写入其中,您可能还会在这些系统上看到更多不可预测的错误行为,可能包括虚假的成功回报。