#c #fopen #fread #fclose
#c #открыть #загрузка #fclose
Вопрос:
Вчера я провел бессонную ночь, пытаясь отследить ошибку в моем тестовом примере. Мой интерфейс выглядит примерно так:
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
Затем я потратил часы, пытаясь отследить ошибки segfaults, двойные освобождения в совершенно не связанных частях моего кода, пока не удалил этот конкретный тестовый пример. Казалось, это решило проблему.
Мои вопросы:
- Я знаю, что вызов
fread
/ftell
для закрытого файла — глупая идея, но может ли это действительно вызвать такое повреждение памяти? Я посмотрел, например, на cppreference, но никогда явно не указывалось, что передача закрытого потока является неопределенным поведением… - Есть ли какой-либо способ узнать, был ли файл закрыт перед чтением из него? (Я посмотрел на SO, но ответ кажется: нет.)
Дополнительная информация
Я использую C 17 и gcc 9.3.0 для компиляции. Причина, по которой мне вообще приходится иметь дело FILE *
, заключается в том, что я получаю эти указатели от внешнего C API.
Комментарии:
1. Использование любой
f*
функции (ftell
,fread
, и т.д.) Для указателя на ФАЙЛ, который был закрыт ранее, или любого другого недопустимого указателя на ФАЙЛ не определено.2. Для предотвращения подобных проблем обычно (для некоторых значений «обычный») устанавливается указатель на
NULL
… iefclose(pointer); pointer = NULL;
(та же идея, что и вfree(pointer); pointer = NULL;
)3.
it was never explicitly specified that passing a closed stream is undefined behavior
«Неопределенный» означает… не определен, поэтому, если он не определен, лучше всего сделать вывод, что это означает, что он не определен. В любом случае в приложении j2 это простоThe value of a pointer to a FILE object is used after the associated file is closed
4. Могу ли я снять деньги с банковского счета, который я закрыл?
5. » Я получаю эти указатели от внешнего C API » — вы также получаете право собственности на
FILE*
? Если вы это сделаете, сохраните его в интеллектуальном указателе, напримерstruct fcloser { auto operator()(std::FILE* fp) const { return std::fclose(fp); } };
, и затемstd::unique_ptr<std::FILE, fcloser> file;
Ответ №1:
Да, это может привести к повреждению памяти, поскольку a FILE *
мог выделить память. Вероятно, используется malloc
.
Что произойдет с вашей программой, если вы попытаетесь использовать указатель malloc
после того, как вы использовали free
его?
Да, все ломается. Не делайте этого.
Ответ №2:
Мощь и эффективность языков C и C сопряжены с большой ответственностью: программист должен быть осторожен в отношении жизненного цикла или каждого объекта.
C упрощает это с помощью интеллектуальных указателей и RAII, но в C отсутствуют эти парадигмы, поэтому каждый указатель является потенциальным источником неопределенного поведения. Хорошим примером являются указатели, полученные из API-интерфейсов C.
Вы могли бы установить значение FILE *
to NULL
после каждого fclose
, но это не решит проблему, если FILE
указатель был получен в качестве аргумента или продублирован каким-либо другим способом.
Не существует стандартного API для проверки правильности указателя, а также в данном конкретном случае FILE *
, ссылается ли a на открытый поток. Что еще хуже, FILE
указатели обычно быстро перерабатываются, поэтому устаревший FILE *
файл вполне может ссылаться на недавно открытый файл, отличный от того, для которого он был первоначально получен.
Комментарии:
1. Да, это именно моя проблема. Если я уже получил указатель на закрытый ФАЙЛ с самого начала, кажется, я облажался…
Ответ №3:
- Я знаю, что вызов
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(amp;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(amp;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;
};
Для этого также потребуется поиск / сообщение функций-членов и т. Д., Но это должно обеспечить разумную безопасность вашего указателя.