С++ для начинающих. Посимвольное чтение текстового файла. Немного о EOF и eof()

Для того, чтобы посимвольно читать файл, иногда используют вместо кажущегося правильным типа char не кажущийся очевидным не опытному кодеру тип int. Связано это с проверкой EOF.
  • EOF — это числовое значение и одновременно индикация операционной системы о том, что достигнут конец файла.

Особенность EOF в том, что он не относится к символьному типу данных. Это обычное число, но особое числовое значение для операционных систем. Одна из проблем, связанных с EOF может возникнуть из того, что, как правило, это отрицательное число, а тип char по умолчанию может быть как знаковым, так и беззнаковым. И вот если char по умолчанию оказывается беззнаковый, то тут и начинается веселье: ведь -1 никогда не будет равно числу, большему, чем -1. Это значит, что сочетание EOF + char грозит задать бесконечный цикл. Для того, чтобы воспроизвести ситуацию, когда символьный тип по умолчанию беззнаковый — мы явно напишем, что тип беззнаковый. А для того, чтобы воспроизвести ошибку, забейте файл только числами и/или латинскими буквами.

Если не использовали русский текст и правильно обратились к файлу, то программа должна была зависнуть. Этот момент может быть не видно, если знаковость у (EOF) и (char по умолчанию) совпадает.

Обычно значение EOF — это -1, хотя может быть и другое значение (по стандарту всегда отрицательное). Но вероятность того, что у вас, как и у меня, это именно -1, большая. Если считать, что тип char по умолчанию знаковый, то тогда его значения — это значения диапазона [-128..127]: в этот диапазон входит -1, поэтому в переменную с типом char может быть присвоен EOF, а вот если char по умолчанию беззнаковый, то это значения диапазона [0..255], куда -1 не входит, отчего EOF не может быть сохранён в char корректно, ну и сравнения отрицательного числа с положительными числами — дело бесполезное. Другая проблема состоит в том, что в случае, если мы имеем дело с (char по умолчанию знаковым) и EOF (а поскольку это всегда отрицательное число, то EOF всегда знаковый), то код символа может совпасть со значением EOF, что иногда оказывается неожиданным сюрпризом. Например, в моём случае, код буквы 'я' совпадает со значением EOF: это число -1. Это приводит к тому, что если читается файл с русским текстом, а в тексте встречается буква 'я', то буква воспринимается как конец файла! Проверьте, так ли у вас. В зависимости от кодировки совпадать могут: 'я', 'Ъ', 'ÿ', неразрывный пробел.

Если совпадение происходит, то чтение файла обрывается на неожиданном для непосвященного человека месте: получается огрызок текста. Чтобы этого не было, для сравнения с EOF следует использовать int. Именно из-за показанных нюансов вы или ещё увидите, или уже видели, что для чтения символов используют не char, а int.

Когда используются числа, то накладывание кода символа на значение EOF не происходит. Но, возможно, вы заметили ещё одну проблему: файл читается немного неверно — на экран в в конец текста из файла добавляются какие-то лишние символы. Это довольно распространённая ошибка у новичков: читать один символ за концом файла. Смотрим на листинг #2.2. Цикл в таком виде сначала проверяет, что достигнут конец файла, потом даже если конец файла достигнут, цикл сначала выполняет свою задачу и только потом, опираясь на то, что конец файла достигнут, прекращает свою работу. Исправить это можно несколькими способами, но пока об одном: сразу после прочтения символа можно проверять, что прочитанный символ оказался не символом, а индикацией конца файла, и, если прочитанное данное — это конец файла, то принудительно завершать циклу работу:

Обычно чтение символа и проверку на конец файла объединяют в условии:

Поскольку логические операции всегда выполняются слева-направо, то сначала гарантировано прочитается символ, значение которого будет запомнено в ch, и сразу же тут же будет выполнена проверка, а это случайно не EOF у нас тут находится? Такая запись получается короче, поэтому чаще используется. Но эту запись тоже сокращают:

Здесь важно ставить скобки, иначе сыграет приоритет операций. ch = f.get() — это выражение, результат которого символ. Этот полученный символ, считанный из файла, проверяется на то, что он именно символ, а не EOF. Такая запись достаточно короткая, чтобы считататься удобной для использования, поэтому именно её вы можете встречать в разных местах.
Есть ещё и другая проблема, связанная с тем, что EOF и eof() используют на равных. Но если в случае c EOF программист точно имеет дело с указанием операционной системы, что такая-то позиция файловой переменной — это конец файла, то eof() — это не позиция конца файла, а состояние файловой переменной (состояние потока), достигшей конца файла. Использование этого состояния неопытными кодерами влечёт за собой свои проблемы. Жаль, что я о них не могу ничего рассказать, ведь я не знаю, как обыграть ситуацию, где это стреляет, расскажу о более приземлённых вещах.

Проблема в #3.1 заключается в том, что написавший такой код человек просто забывает файловую переменную двигать по файлу. Ведь в #3.1 файловая переменная совсем не смещается по файловым позициям, так как же тогда она может достигнуть конца файла, чтобы своё достижение активировать? Чтобы это исправить, файловую переменную необходимо смещать по файлу, а делать это можно чтением файла:

Здесь, как и в показанных раннее кодах, можно увидеть вывод на экран мусорных значений. Это значит, что нужно либо сразу после прочтения сделать проверку, что достигнут конец файла, либо что файловая переменная (файловый поток) получил себе состояние достижения конца файла.

Здесь таится несколько опасностей. Но я пока напишу только одну из них. Мне не хватает глубины знаний, чтобы расписать все. Проблема может возникнуть тогда, когда некто неправильно объединит чтение данных из файла и проверку на достижение потоком конца файла или на конец файла в условии цикла:

Ошибка такого объединения может сыграть тогда, когда в файле есть нуль-символы. В обычных текстовых файлах таких, как правило, нет, но что если файл символьный? Суть ошибки заключается в том, что написавший такой код не разделяет типы возвращаемых значений, из-за чего написал так, что в выражении (ch = f.get()) результатом будет не булево значение, а символ. Это выражение будет false только тогда, когда ch окажется нуль-символом. Тут с большой вероятностью был расчёт на то, что нужно что-то делать, пок встречаются символы, а не что-то другое. Простыми словами: в выражении работает не принцип "пока есть символ и поток не достиг конца файла", а принцип "До тех пор, пока прочитанный символ не нуль-символ и поток не достиг конца файла". Здесь всё, что нужно было, это использовать правильную форму get(): гет с параметром:

В случае использования формы гет с параметром, цикл ориентируется именно чтение, а не на значения прочитанных данных. Если бы поток был не файловым, а клавиатурным (cin), то можно было бы сказать, что в случае с присваиванием цикл ориентируется на значение нажатой клавиши, а в случае использования формы гет с параметром цикл ориентируется на само нажатие клавиш.
Вообще использование eof() в условии цикла не приветствуется в профессиональных кругах. Как минимум, это потому что когда читают файлы, обычно делают ориентир не на то, что наконец-то достигнут конец файла, а на читаемые данные: если порция данных не смогла быть прочитана, то завершают цикл. Ну а зачем ждать, пока конец файла будет достигнут, если файл уже не может быть прочитан? Если же файл исправен, то как только поток попробует прочитать что-то после конца файла, порция данных будет опознана как нечитаемая. В зависимости от ситуации для проверки порции читаемых данных используют good(), bad(), fail(). Чтобы понять, когда какие нужно использовать — это нужно понять, как использовать таблицу, которая показывает, когда какие состояния активируются. Моя задача сейчас не научить таблице, а только дать понять, что могут быть, так сказать, мягкие ошибки, а могут быть фатальные, и именно они влияют на выбор конкретной проверки. Например, если нет никаких ошибок, то включено состояние good(), на это состояние и можно делать проверку, если нужно исключить любые ошибки:

Это хоть и работает корректно, но тут имеется невидимая проблема: проверка на good неявно присутствует в форме гета с параметром, из-за чего дополнительная проверка состояния потока лишняя. Если бы чтение происходило в теле цикла, то в условии цикла имело бы смысл оставить проверку на good(), а так из условия нужно её убрать, поскольку она как повторная просто не имеет смысла.

Подводим итоги:
  • Чтобы исключить работу с символом за концом файла, нужно сразу с прочтением порции данных делать проверку состояния потока или факта достижения конца файла.
  • Если нет прямой необходимости проверять достижение конца файла, следет проверять порцию прочитанных данных.
  • Если проверять прочитанную порцию данных, то в зависимости от определяющих обстоятельств можно делать проверку на good(), fail(), bad(), eof(). Без острой необходимости выбрать именно одно конкретное обычно останавливаются на good() или fail(). Иногда такие проверки могут быть включены в какую-то используемую функцию.
  • EOF и eof() — это разные сущности. Если EOF — это непосредственно конец файла, то eof() — это состояние потоковой переменной. Если использовать eof() как признак конца файла, то это где-то может спровоцировать возникновение проблемы.
  • Использовать EOF следует только в связке с int, использовать EOF в связке c char категорически нельзя.
  • Для чтения файла — в условии цикла следует использовать гет с параметром, а в теле цикла можно использовать гет как с парамером, так и без параметра.

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

Ваш адрес email не будет опубликован.

Поиск

 
     

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

https://www.litres.ru/vandad-nahavandipur/ios-priemy-programmirovaniya/?lfrom=15589587
Яндекс.Метрика