从读取前关闭的文件中读取

freading from a file that was fclosed before reading

昨天我度过了一个不眠之夜,试图找出我的测试用例中的错误。我的界面看起来像这样:

image read_image(FILE *file) {
  if (file == nullptr) {
    //throw exception 
  }
  //call ftell and fread on the file
  //but not fclose
  ...
  //return an image
}

原来我的一个测试用例测试了我的代码是否可以处理从第一次打开的文件读取(因此文件指针不是 nullptr),但在我将它传递给我的函数之前关闭,像这样:

FILE *img_file = fopen("existing_image.png", "r");
REQUIRE(img_file != nullptr); //this passes!
fclose(img_file);
auto my_image = image_read(file);

//... then somewhere down in completely
//unrelated test cases I get segfaults,
//double free errors and the like

然后我花了几个小时试图追踪段错误,在我的代码中完全不相关的部分双重释放,直到我删除了那个特定的测试用例。这似乎解决了它。

我的问题是:

  1. 我知道在关闭的文件上调用 fread/ftell 是个愚蠢的想法,但它真的会导致那种内存损坏吗?我环顾四周,例如cppreference 但从未明确指定传递关闭的流是未定义的行为...
  2. 有什么方法可以在读取文件之前查明文件是否已关闭? (我看了SO,但答案似乎是:没有。)

附加信息

我正在使用 C++17 和 gcc 9.3.0 进行编译。我必须处理 FILE * 的原因是因为我从外部 C API.

接收这些指针

是的,它会导致内存损坏,因为 FILE * 可能已经分配了内存。可能使用 malloc.

如果您在使用 free 之后尝试使用来自 malloc 的指针,您的程序会怎样?

是的,一切都坏了。不要那样做。

C 和 C++ 语言的强大和高效伴随着巨大的责任:程序员必须对生命周期或每个对象保持谨慎。

C++ 使用智能指针和 RAII 使这更容易,但 C 缺乏这些范例,因此每个指针都是未定义行为的潜在来源。从 C APIs 收到的指针就是一个很好的例子。

您可以在每个 fclose 之后将 FILE * 设置为 NULL,但是如果 FILE 指针作为参数接收或重复一些,这将无法解决问题其他方式。

没有标准 API 来检查指针是否有效,在这种特殊情况下也没有检查 FILE * 是否指向打开的流。更糟糕的是,FILE 指针通常会被快速回收,因此陈旧的 FILE * 很可能指向一个新打开的文件,与最初收到的文件不同。

  • I know calling fread/ftell on a closed file is a dumb idea but could it really cause that kind of memory corruption? I looked around on e.g. cppreference but it was never explicitly specified that passing a closed stream is undefined behavior...

在已关闭的 FILE* 上尝试 freadftell 将使两个函数 return -1 并将 errno 设置为适当的值许多 系统 - 但您通常可以通过检查 FILE* 是否有效来避免这种情况。

  • Is there any way of finding out if a file was closed before reading from it? (I looked on SO, but the answer seems: no.)

在 Posix 系统和 Windows(可能还有其他系统)中,是的。 Posix fileno() 和 Windows _fileno() returns -1 如果参数不是有效的流,比如在它被关闭之后。

因此,您可以创建一个 RAII 包装器来获取 FILE* 的所有权并检查它在构建时是否有效。如果它通过了这个测试,你的代码中的任何东西在不应该关闭它的时候关闭它的风险就会非常低。

下面是此类包装器的概要:

class File {
public:
    File(std::FILE* fp) : file(validate(fp)) {
        if(!file) throw std::runtime_error("I don't like nullptr");
    }

    template<typename T, std::size_t N>
    auto read(T(&buf)[N], std::size_t nmemb = N) {
        if(N < nmemb) throw std::runtime_error("reading out of bounds");
        return fread(buf, sizeof(T), nmemb, file.get());
    }

    template<typename T, std::size_t N>
    auto write(const T(&buf)[N], std::size_t nmemb = N) {
        if(N < nmemb) throw std::runtime_error("writing out of bounds");
        return fwrite(buf, sizeof(T), nmemb, file.get());
    }

private:
    std::FILE* validate(std::FILE* fp) {
#if defined(_POSIX_C_SOURCE)
        if(::fileno(fp) == -1) throw std::runtime_error(std::strerror(errno));
#elif defined(_WIN32)
        if(::_fileno(fp) == -1) throw std::runtime_error(std::strerror(errno));
#endif
        return fp;
    }
    struct fcloser {
        auto operator()(std::FILE* fp) const {
            return std::fclose(fp); 
        } 
    };

    std::unique_ptr<FILE, fcloser> file;
};

它也需要寻找/告诉成员函数等,但这应该可以保证您的指针相当安全。

Demo