问题描述
昨天我度过了一个不眠之夜,试图找出测试用例中的错误。我的界面看起来像这样:
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
然后,我花了几个小时尝试查找段错误,在代码的完全不相关的部分中两次释放代码,直到删除了该特定测试用例。这似乎解决了。
我的问题是:
- 我知道在关闭的文件上调用
fread
/ftell
是一个愚蠢的主意,但是真的会导致这种内存损坏吗?我环顾四周cppreference,但从未明确指定传递封闭流是未定义的行为... - 有什么方法可以在读取文件之前查明文件是否已关闭? (我看着SO,但答案似乎是:否。)
其他信息
我正在使用C ++ 17和gcc 9.3.0进行编译。我必须要处理FILE *
的原因是因为我正在从外部C API接收这些指针。
解决方法
是的,因为FILE *
可能已经分配了内存,所以它可能导致内存损坏。可能使用malloc
。
如果在程序上使用malloc
后尝试使用free
中的指针,程序会发生什么情况?
是的,一切都坏了。不要那样做。
,C和C ++语言的强大功能和效率负有重大责任:程序员必须谨慎对待生命周期或每个对象。
C ++通过智能指针和RAII使此操作变得更容易,但是C缺少这些范例,因此每个指针都是未定义行为的潜在来源。从C API收到的指针就是一个很好的例子。
您可以在每个FILE *
之后将NULL
设置为fclose
,但是如果将FILE
指针作为参数接收或以其他方式重复,则这将无法解决问题。
没有标准的API检查指针是否有效,在这种特殊情况下,FILE *
是否引用开放流。更糟糕的是,FILE
指针通常会被快速回收,因此陈旧的FILE *
可能很好地引用了一个新打开的文件,与最初收到的文件不同。
- 我知道在关闭的文件上调用
fread/ftell
是一个愚蠢的主意,但是真的会导致这种内存损坏吗?我环顾四周cppreference,但从未明确指定传递封闭流是未定义的行为...
在已关闭的fread
上尝试ftell
或FILE*
会使两个函数都返回-1,并在许多上将errno
设置为适当的值系统-但通常可以通过检查FILE*
是否有效来避免这种情况。
- 有什么方法可以在读取文件之前查明文件是否已关闭? (我看着SO,但答案似乎是:否。)
在Posix系统和Windows(可能还有其他)中,可以。如果参数不是有效的流(例如在关闭后),则Posix fileno()
和Windows _fileno()
会返回-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,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;
};
它也需要查找/告诉成员函数等,但这应该可以使指针相当安全。