Работа с файлами С++. Проверки, для начинающих

В этой статье описываются некоторые особенности подхода к обработке файлов, когда используется С++ стиль работы с файлами.
ВНИМАНИЕ!

  • В этой теме используется работа с файлами: обработка файлов текстового формата. Чтобы осилить эту статью, необходим навык программирования в С++ простого чтения и записи текстового файла.
В целом, работа с файлами мало отличается от работы с клавиатурным вводом, только вместо клавиатуры у нас файл. В отличие от файла в клавиатуры мы, разумеется, записать информацию обычно не можем. На самом еле можем конечно перепрограммировать клавиши, но это уже другая история. Часто нужно или просто удобно работать именно с файлами. При работе с файлами хороший программист обязательно добавит некоторых проверок, чтобы и самому не тупить, и чтобы программа работала правильно, а если происходил бы сбой, то чтобы этот сбой каким-нибудь образом сообщался.
Одной из обязательных проверок является проверка открытия файла. Программа, обращаясь к файлу, может не найти файл или не смочь открыть файл из-за ограниченных прав доступа. Если файл находится в одном каталоге, а программа ищет в другом месте, то или откроется не тот файл (и представляете, нужный файл испортится после записи) или вообще не откроется, потому что описанный программе путь никуда не ведёт. Если файл не существует — то и открывать вообще нечего. Новички часто мучаются при работе с файлами по той простой причине, что часто пишут путь не тот, а потом задаются вопросом: "Почему не работает?". Чтобы найти себе спасение, сэкономить нервы и время, нужно проверять файл на успешное открытие, всегда! Такая проверка в современных компиляторах (на момент записи этой строки идёт 2018г.) возможна, например, так:

Предпочтительнее is_open(), но некоторые компиляторы такое не умеют, тогда остаётся вариант похуже: good().

  • Метод is_open() возвращает true, если файл открыт успешно, поэтому выражение !inFile.is_open() даёт в результате true, если попытка оказывается неудачной.
  • Если ваш компилятор не поддерживает is_open(), можете воспользоваться вместо него методом good(). Метод good() не проверяет возможные проблемы настолько тщательно, как это делает is_open ().
Для того, чтобы программа не выдавала сообщение об ошибке, нужно, чтобы и существовал сам файл, и путь к файлу был в программе написан правильно. На страницах моего сайта в угоду легкочитаемости показанная проверка пропускается, но это не значит, что правильно её вообще не использовать, просто я решил облегчить участь читателю, изучающему коды в моих статьях. Правильно — однозначно, использовать показанную проверку.
Кроме того, что может быть проблема с открытием файла, могут быть проблемы, возникающие при обработке файла: в файле может оказаться недопустимое значение, и кто его знает, сколько ещё возможных барьеров может возникнуть прямо во время прочтения файла. Но не только во время прочтения, даже во время записи доступ к файлу может быть прерван. В общем, нужны проверки, проверки, проверки.
Запись в файл, позволяющая предупреждать о возникшей проблемы выглядит, возможно, страшно, но страшнее получать проблемы в реальных программах, поэтому лучше конечно серьёзно подходить к проверкам открытия и чтения файлов:

===========================================================================

В идеале ещё добавить запись лога, т. е. записывать в отдельный файл инофрмацию об успешности или неудаче последней попытки записи. Я не хочу усложнять пример навешиванием логриующей части, а до функций и классов, которые могли бы помочь раскидать всё по полочкам, темы, даваемые на моём сайте, ещё до дошли. Поэтому ограничусь записью в файл в том виде, который сейчас получился.
Но даже эти проверки не дают 100% гарантии, что ваша программа не даст слабину в неподходящий момент. Есть такие тонкости, когда стандартный С++ не очень способствует решениям. Тем не менее, это основная форма записи. Т. е. файл просто проверяется на открытость. Коли файл открыт, то, значит, есть во что записывать.
Проверки чтения файла мало отличаются от проверок во время записи. Всё также: нужно проверять. А при чтении проверять надо на удачное к файлу обращение, считываемую из файла информацию. Но не всегда понятно, сколько именно из файла информации будет прочитано, поэтому добавляется проверка на достижение конца файла. Сейчас я напишу пример просто чтения. Потом соберу и чтение и запись в одном коде.


Такая вот проверка на чтение. Комменарии обо всём говорят. Выкрутасов тут вообще нет. У разных товарищей можно видеть разные вариации, но суть сводится к непосредственно проверке на открытость файла и проверки на достижение конца файла. Проверку цикла прочтения можно делать немного наоборот:


По сути это то же, что у меня, просто вывернуто наоборот. Но по вот этому примеру пояснить один момент проще:

  • Этот код находится сразу после цикла чтения информации из файла, поэтому он исследует, почему цикл был прерван. Поскольку eof() проверяет только EOF, a fail() проверяет как EOF, так и несоответствие типа, здесь вначале проверяется именно EOF. Таким образом, если выполнение дойдет до else if, то достижение конца файла (EOF) как причина выхода из цикла будет исключена, и значение true, полученное от fail(), недвусмысленно укажет на несоответствие типа.
В общем, от наиболее узкой проверки двигаемся к наиболее широкой, чтобы более точно диагностировать проблему. А саму проверку включаем сразу после цикла чтения.

  • good() сообщает о самой последней попытке чтения ввода
Т. е. перед тем как обращаться к good() надо что-то считать из файла.
Две вариации кода, с отображением предпочтительнотси (код сокращается и некоторые потенциальные проблемы предотвращаются):


В листингах a1 и a2f обозначает воплощение файла, а value переменную, в которую записывается прочитанное из файла значение. N — то возможная вместимость строки S (это знакомо тем, кто уже успел попрограммировать программы со строками). Поскольку good() надо смотреть после прочтения значения, то необходимо выполнить первое чтение до начала выполнения цикла, иначе бы на первом шаге цикла подобно использованию неинициализированной переменной могла произойти лотерея, поэтому в листинге a1 первое считывание происходит до начала выполнения цикла. Но С++ позволяет сократить код таким образом, как показано в лстинге a2. Лучше использовать вторую форму (она даже может избавить вас от некоторых мелких проблем). Объяснить новичку это немного проблемно, просто знайте, что часто применяют именно вариант из листинга a2, потому что он короче.
Подводим краткие итоги:

  • При работе с файлами нужно особое внимание уделять проверкам.
    • К файловой переменной можно приписывать точку, тогда можно выбрать подходящий для проверки метод.
    • is_open() — проверяет файл на открытость, если is_open() не поддерживается вашим компилятором, то остаётся использовать good().
    • good() — даёт знать произошла ли операция чтения успешно. Если произошла ошибка, то возвращается false, если всё хорошо — то true.

      • is_good логично смотреть после прочтения некоторого значения из файла, но С++ позволяет сокращать код, отчего можно подумать, что значение не зачитывается (хотя на самом деле в короткой показанной форме листингов a1.2 и a2.2 сначала значение считывается, а потом нам отдаётся good там всё или не good, отдаваемое мы можем подсмотреть, чем и занимемся)
    • fail() — можно использовать для проверки на несоответствие типов считываемого из файла значения и переменной-реципиента. (реципиент — это антоним слова "донор")
    • eof() — используют для проверки на достижение конца файла. (Не путайте с EOF, обозначающим символ-признак конца файла)
good()
Операция выполнена успешно

eof()
Достигнут конец ввода (например, конец файла)

fail()
Произошло нечто неожиданное (например, мы ждем цифру , а получаем букву)

bad()
Произошло нечто неожиданное и серьезное (например, ошибка чтения диска)

К сожалению, различия между состояниями fail() и bad() определены неточно и зависят от точки зрения программистов, создающих операции ввода-вывода для новых типов. Однако основная идея проста: если обнаруживается простая ошибка форматирования, поток переключается в состояние fail(), предполагая, что вы (мы) способны её исправить. Если же произошло нечто совершенно ужасное, например, неправильное чтение с диска, то поток оказывается в состоянии bad(), предполагая, что вам (нам) ничего не остаётся делать, кроме как отказаться от попыток считать данные из потока. Поток в состоянии bad() находится также и в состоянии fail().
Поток — это наша файловая переменная-объект, которая имеет отношение к файлу (в общем случае потоковые переменные далеко не только к файлам относятся). Просто их часто обозначают словом "поток". Потоки — это отдельная тема. Частный случай потоков — это файловые потоки, и я описываю здесь работу с файлами, поэтому файлами и ограничусь.

Объединим две программы в одну. Я покажу вариант очень для новичков, как можно собрать всё в main(). Это не значит, что так надо: так как раз не надо, позднее вы познокомитесь с функциями и классами, которые помогут вам (читающим меня новичкам) писать коды лучше.
В показываемом коде произойдут некоторые изменения: файлы будут открываться не сразу, а по мере необходимости. И будет добавлен счётчик счёта считанного. Пример ориентирован на работу с текстовым файлом со строками, поэтому будут считаться строки.

Как было отмечено, этот показанный код дан только в учебных целях. Но по нему хорошо можно видеть, что код достаточно распух. Когда код становится таким неудобным, это повод выносить разделять внутренние подзадачи на отдельные функции: запись организовать в одной функции, чтение в другой, проверку на ошибки в третьей. Вы же должны обратить внимание на метод open(), появившейся в этом коде. Этот метод позволяет нам обращаться к разным файлам в ходе работы программы: открыть один файл, открыть другой. Если бы я оставил всё как показано в первых листингах, где сразу определяется, какой именно файл будет открыт, то получил бы усечение файла до нулевого размера. Вы можете попробовать написать:



Тогда, что бы в файле изначально записано не было, оно просто перезапишется. В общем, иногда имеет смысл отложить открытие файла до нужного момента. Ещё обратите внимание, что я использую две переменные-объекта: f1 и f2 — это потому что одна в показанном коде записывает файл, а другая читает файл.
На этом, наверное, закончу, если что, сильно не бейте, а будет что подсказать или чем помочь — то можете поспособствовать улучшению этого текста. Мне остаётся надеяться, что эта страница помогла узнать что-то кому-то нужное и продвинуться в своём обучении хоть ненамного.
Использованные материалы:

Язык программирования C++. Лекции и упражнения. 6-е изд (Стивен Прата)
Программирование. Принципы и практика с использованием C++ (Бьярне Страуструп)

12 комментариев на «“Работа с файлами С++. Проверки, для начинающих”»

  1. Игорь:

    подскажите, что делает (return -1) и тогда соответственно (return 0)?

    • В моём случае ничего полезного.
      return 0, используемое в функции main(), только-лишь сообщает операционной системе, что программа успешно завершилась.
      return -1 могло бы использоваться для работы с внешними программами. Т. е. какая-нибудь внешняя программа могла бы использовать написанную мной, по -1 можно было бы легко выявить, что моя эта программа задачу свою не выполнила и, например, сообщить об этом пользователю. -1 необязательное значение, а взятое с потолка. Т. е. любое можно использовать, достаточно будет знать, что оно должно обозначить (в моём случае неоткрытие файла).

  2. Аноним:

    что то не пойму как организовать в проге проверку методом fail(). он пишется после цикла считывания с файла или само считывание пишется в-  цикл() {if ( f2.fail() ){ f2.get() = переменная;}} ?

  3. Igor:

    что то не пойму как организовать в проге проверку методом fail(). он пишется после цикла считывания с файла или само считывание пишется в-  цикл() {if ( f2.fail() ){ f2.get() = переменная;}} ?

    • У меня в примере сначала проверяется, что файл успешно открыт. Потом проверяется fail, т. е. успешность прочитанности значения. В случае неуспеха выбирается поведение диагностики ошибки. В моём случае ошибке присваивается номер и запоминается в переменную ERROR.
      fail переводится как неудача. Если компилятор прочитал что смог и всё поломалось, то или достигнут конец файла, или не соответствует тип для записи в переменную значения. Поскольку дальнейшее чтение не имеет смысла, нужно зафиксировать проблему удобным способом.

  4. Igor:

    admin, я больше интересовался для выхода из цикла «фэйлом», если прочитанный символ в переменную из файла оказался не того типа как у переменной; если это имеет смысл вообще

  5. Igor:

    я написал программу в которой хотел, чтоб если на пути считывания f2.get()-ом в переменную типа чар попадала цифра, то считывание прекращалось и выводило на каком символе произошел конец. но у меня несколько раз, то ничего не выводило, то выводило все подряд до EOF

    • char — целочисленный тип.
      цифра — целое число
      Читаем в целочисленный тип целое число и ждём, что сломается? Не дождётесь.

  6. Igor:

    а разве char это не символьный тип?

    • Я его называю целочисленным типом с символьным представлением. Т. е. для компьютера он ничем не отличается от числа, а для нас как символ.
      То, что мы его видим как символ, — это только умение cout его так выводить.
      Программисты С, например, сами указывают какой тип ожидается на выходе (у них нету cout).
      Всё, что можно делать с целыми числами, — всё можно делать с типом char.
      Называем мы этот тип символьным скорее по традиции и для ясности, но не потому что так есть на самом деле.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Поиск

 
     

Случайная книга в электронном формате

https://www.litres.ru/uriy-schupak/win32-api-razrabotka-prilozheniy-dlya-windows/?lfrom=15589587
Яндекс.Метрика