Обработка ошибок при работе с файлами с

Обновлено: 04.07.2024

Сопоставление кодов ошибок с исключениями

Например, при вызове метода в операционной системе Windows код ошибки ERROR_FILE_NOT_FOUND (или 0x02) преобразуется в исключение FileNotFoundException, а код ошибки ERROR_PATH_NOT_FOUND (или 0x03) — в DirectoryNotFoundException.

К сожалению, точные условия возникновения определенных кодов ошибок в операционной системе часто не документируются или документируются в недостаточном объеме. Это означает, что возможны непредвиденные исключения. Например, при работе с каталогом логично ожидать, что передача недопустимого пути в конструктор DirectoryInfo приведет к созданию исключения DirectoryNotFoundException. Но в этой ситуации может создаваться и FileNotFoundException.

Обработка исключений при операциях ввода-вывода

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

Обработка IOException

IOException является базовым классом для исключений в пространстве имен System.IO и создается для любого кода ошибки, который не имеет сопоставления с определенным типом исключения. Это означает, что оно может появиться в любой операции ввода-вывода.

Так как IOException является базовым классом для других типов исключений в пространстве имен System.IO, его нужно обрабатывать в блоке catch после обработки других исключений, связанных с вводом-выводом.

Обратите внимание, что в коде обработки исключений IOException всегда нужно обрабатывать последним. Иначе блоки catch для производных классов не проверяются, ведь это исключение является базовым классом для всех остальных.

В случае с IOException дополнительные сведения об ошибке можно получить из свойства IOException.HResult. Чтобы преобразовать значение HResult в код ошибки Win32, отбросьте верхние 16 бит из 32-разрядного значения. В приведенной ниже таблице перечислены коды ошибок, которые могут быть заключены в IOException.

HResult Константа Описание
ERROR_SHARING_VIOLATION 32 Отсутствует имя файла, или файл или каталог уже используется.
ERROR_FILE_EXISTS 80 Файл уже существует.
ERROR_INVALID_PARAMETER 87 Методу передан недопустимый аргумент.
ERROR_ALREADY_EXISTS 183 Файл или каталог уже существует.

Для обработки этих исключений можно применить предложение When в инструкции catch, как показано в приведенном ниже примере.

При написании программ возникновение ошибок почти неизбежно. Ошибки в языке C++ делятся на две категории: синтаксические и семантические.

Синтаксические ошибки

Синтаксическая ошибка возникает при нарушении правил грамматики языка C++. Например:

если 7 не равно 8, то пишем "not equal";

Хотя этот стейтмент нам (людям) понятен, компьютер не сможет его корректно обработать. В соответствии с правилами грамматики языка C++, корректно будет:

Семантические ошибки

Возможно, программист хотел, чтобы вывелось 0 1 2 3 , но на самом деле выведется 0 1 2 3 4 .

Семантические ошибки не улавливаются компилятором и могут иметь разное влияние: некоторые могут вообще не отображаться, что приведет к неверным результатам, к повреждению данных или вообще к сбою программы. Поэтому о семантических ошибках беспокоиться уже придется.

Они могут возникать несколькими способами. Одной из наиболее распространенных семантических ошибок является логическая ошибка. Логическая ошибка возникает, когда программист неправильно программирует логику выполнения кода. Например, вышеприведенный фрагмент кода имеет логическую ошибку. Вот еще один пример:

Что произойдет, если x будет равен 4 ? Условие выполнится как true , а программа выведет x is greater than 4 . Логические ошибки иногда бывает довольно-таки трудно обнаружить.

Другой распространенной семантической ошибкой является ложное предположение. Ложное предположение возникает, когда программист предполагает, что что-то будет истинным или ложным, а оказывается наоборот. Например:

Заметили потенциальную проблему здесь? Предполагается, что пользователь введет значение между 0 и длиной строки Hello, world! . Если же пользователь введет отрицательное число или число, которое больше длины указанной строки, то index окажется за пределами диапазона массива. В этом случае, поскольку мы просто выводим значение по индексу, результатом будет вывод мусора (при условии, что пользователь введет число вне диапазона). Но в других случаях ложное предположение может привести и к изменениям значений переменных, и к сбою в программе.

Определение ложных предположений

Оказывается, мы можем найти почти все предположения, которые необходимо проверить в одном из следующих трех мест:

При вызове функции, когда caller может передать некорректные или семантически бессмысленные аргументы.

При возврате значения функцией, когда возвращаемое значение может быть индикатором выполнения (произошла ли ошибка или нет).

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

Поэтому, придерживаясь безопасного программирования, нужно следовать следующим 3-м правилам:

В верхней части каждой функции убедитесь, что все параметры имеют соответствующие значения.

Проверяйте данные ввода на соответствие ожидаемому типу данных и его диапазону.

Рассмотрим примеры проблем:

Проблема №1: При вызове функции caller может передать некорректные или семантически бессмысленные аргументы:

Р абота с текстовым файлом похожа работу с консолью: с помощью функций форматированного ввода мы сохраняем данные в файл, с помощью функций форматированного вывода считываем данные из файла. Есть множество нюансов, которые мы позже рассмотрим. Основные операции, которые необходимо проделать, это

  • 1. Открыть файл, для того, чтобы к нему можно было обращаться. Соответственно, открывать можно для чтения, записи, чтения и записи, переписывания или записи в конец файла и т.п. Когда вы открываете файл, может также произойти куча ошибок – файла может не существовать, это может быть файл не того типа, у вас может не быть прав на работу с файлом и т.д. Всё это необходимо учитывать.
  • 2. Непосредственно работа с файлом - запись и чтение. Здесь также нужно помнить, что мы работаем не с памятью с произвольным доступом, а с буферизированным потоком, что добавляет свою специфику.
  • 3. Закрыть файл. Так как файл является внешним по отношению к программе ресурсом, то если его не закрыть, то он продолжит висеть в памяти, возможно, даже после закрытия программы (например, нельзя будет удалить открытый файл или внести изменения и т.п.). Кроме того, иногда необходимо не закрывать, а "переоткрывать" файл для того, чтобы, например, изменить режим доступа.

Кроме того, существует ряд задач, когда нам не нужно обращаться к содержимому файла: переименование, перемещение, копирование и т.д. К сожалению, в стандарте си нет описания функций для этих нужд. Они, безусловно, имеются для каждой из реализаций компилятора. Считывание содержимого каталога (папки, директории) – это тоже обращение к файлу, потому что папка сама по себе является файлом с метаинформацией.

Иногда необходимо выполнять некоторые вспомогательные операции: переместиться в нужное место файла, запомнить текущее положение, определить длину файла и т.д.

Для работы с файлом необходим объект FILE. Этот объект хранит идентификатор файлового потока и информацию, которая нужна, чтобы им управлять, включая указатель на его буфер, индикатор позиции в файле и индикаторы состояния.

Объект FILE сам по себе является структурой, но к его полям не должно быть доступа. Переносимая программа должна работать с файлом как с абстрактным объектом, позволяющим получить доступ до файлового потока.

Создание и выделение памяти под объект типа FILE осуществляется с помощью функции fopen или tmpfile (есть и другие, но мы остановимся только на этих).

Функция fopen открывает файл. Она получает два аргумента – строку с адресом файла и строку с режимом доступа к файлу. Имя файла может быть как абсолютным, так и относительным. fopen возвращает указатель на объект FILE, с помощью которого далее можно осуществлять доступ к файлу.

Например, откроем файл и запишем в него Hello World

Функция fopen сама выделяет память под объект, очистка проводится функцией fclose. Закрывать файл обязательно, самостоятельно он не закроется.

Функция fopen может открывать файл в текстовом или бинарном режиме. По умолчанию используется текстовый. Режим доступа может быть следующим

Параметры доступа к файлу.
Тип Описание
r Чтение. Файл должен существовать.
w Запись нового файла. Если файл с таким именем уже существует, то его содержимое будет потеряно.
a Запись в конец файла. Операции позиционирования (fseek, fsetpos, frewind) игнорируются. Файл создаётся, если не существовал.
r+ Чтение и обновление. Можно как читать, так и писать. Файл должен существовать.
w+ Запись и обновление. Создаётся новый файл. Если файл с таким именем уже существует, то его содержимое будет потеряно. Можно как писать, так и читать.
a+ Запись в конец и обновление. Операции позиционирования работают только для чтения, для записи игнорируются. Если файл не существовал, то будет создан новый.

Если необходимо открыть файл в бинарном режиме, то в конец строки добавляется буква b, например “rb”, “wb”, “ab”, или, для смешанного режима “ab+”, “wb+”, “ab+”. Вместо b можно добавлять букву t, тогда файл будет открываться в текстовом режиме. Это зависит от реализации. В новом стандарте си (2011) буква x означает, что функция fopen должна завершиться с ошибкой, если файл уже существует. Дополним нашу старую программу: заново откроем файл и считаем, что мы туда записали.

Вместо функции fgets можно было использовать fscanf, но нужно помнить, что она может считать строку только до первого пробела.
fscanf(file, "%127s", buffer);

Также, вместо того, чтобы открывать и закрывать файл можно воспользоваться функцией freopen, которая «переоткрывает» файл с новыми правами доступа.

Функции fprintf и fscanf отличаются от printf и scanf только тем, что принимают в качестве первого аргумента указатель на FILE, в который они будут выводить или из которого они будут читать данные. Здесь стоит сразу же добавить, что функции printf и scanf могут быть без проблем заменены функциями fprintf и fscanf. В ОС (мы рассматриваем самые распространённые и адекватные операционные системы) существует три стандартных потока: стандартный поток вывода stdout, стандартный поток ввода stdin и стандартный поток вывода ошибок stderr. Они автоматически открываются во время запуска приложения и связаны с консолью. Пример

Ошибка открытия файла

Если вызов функции fopen прошёл неудачно, то она возвратит NULL. Ошибки во время работы с файлами встречаются достаточно часто, поэтому каждый раз, когда мы окрываем файл, необходимо проверять результат работы

Проблему вызывает случай, когда открывается сразу несколько файлов: если один из них нельзя открыть, то остальные также должны быть закрыты

В простых случаях можно действовать влоб, как в предыдущем куске кода. В более сложных случаях используются методы, подменяющиее RAII из С++: обёртки, или особенности компилятора (cleanup в GCC) и т.п.

Буферизация данных

  • 1) Если он заполнен
  • 2) Если поток закрывается
  • 3) Если мы явно указываем, что необходимо очистить буфер (здесь тоже есть исключения:)).
  • 4) Также очищается, если программа завершилась удачно. Вместе с этим закрываются и все файлы. В случае ошибки выполнения этого может не произойти.

Форсировать выгрузку буфера можно с помощью вызова функции fflush(File *). Рассмотрим два примера – с очисткой и без.

Раскомментируйте вызов fflush. Во время выполнения откройте текстовый файл и посмотрите на поведение.

Буфер файла можно назначить самостоятельно, задав свой размер. Делается это при помощи функции

которая принимает уже открытый FILE и указатель на новый буфер. Размер нового буфера должен быть не меньше чем BUFSIZ (к примеру, на текущей рабочей станции BUFSIZ равен 512 байт). Если передать в качестве буфера NULL, то поток станет небуферизированным. Можно также воспользоваться функцией

  • _IOFBF - полная буферизация. Данные записываются в файл, когда он заполняется. На считывание, буфер считается заполненным, когда запрашивается операция ввода и буфер пуст.
  • _IOLBF - линейная буферизация. Данные записываются в файл когда он заполняется, либо когда встречается символ новой строки. На считывание, буфер заполняется до символа новой строки, когда запрашивается операция ввода и буфер пуст.
  • _IONBF – без буферизации. В этом случае параметры size и buffer игнорируются.

Пример: зададим свой буфер и посмотрим, как осуществляется чтение из файла. Пусть файл короткий (что-нибудь, типа Hello, World!), и считываем мы его посимвольно

Видно, что данные уже находятся в буфере. Считывание посимвольно производится уже из буфера.

Функция int feof (FILE * stream); возвращает истину, если конец файла достигнут. Функцию удобно использовать, когда необходимо пройти весь файл от начала до конца. Пусть есть файл с текстовым содержимым text.txt. Считаем посимвольно файл и выведем на экран.

Всё бы ничего, только функция feof работает неправильно. Это связано с тем, что понятие "конец файла" не определено. При использовании feof часто возникает ошибка, когда последние считанные данные выводятся два раза. Это связано с тем, что данные записывается в буфер ввода, последнее считывание происходит с ошибкой и функция возвращает старое считанное значение.

Этот пример сработает с ошибкой (скорее всего) и выведет последний символ файла два раза.

Решение – не использовать feof. Например, хранить общее количество записей или использовать тот факт, что функции fscanf и пр. обычно возвращают число верно считанных и сопоставленных значений.

Примеры

1. В одном файле записаны два числа - размерности массива. Заполним второй файл массивом случайных чисел.

2. Пользователь копирует файл, при этом сначала выбирает режим работы: файл может выводиться как на консоль, так и копироваться в новый файл.

3. Пользователь вводит данные с консоли и они записываются в файл до тех пор, пока не будет нажата клавиша esc. Проверьте программу и посмотрите. как она себя ведёт в случае, если вы вводите backspace: что выводится в файл и что выводится на консоль.

4. В файле записаны целые числа. Найти максимальное из них. Воспользуемся тем, что функция fscanf возвращает число верно прочитанных и сопоставленных объектов. Каждый раз должно возвращаться число 1.

Другое решение считывать числа, пока не дойдём до конца файла.

5. В файле записаны слова: русское слово, табуляция, английское слово, в несколько рядов. Пользователь вводит английское слово, необходимо вывести русское.

Файл с переводом выглядит примерно так

солнце sun
карандаш pen
шариковая ручка pencil
дверь door
окно windows
стул chair
кресло armchair

и сохранён в кодировке cp866 (OEM 866). При этом важно: последняя пара cлов также заканчивается переводом строки.

Алгоритм следующий - считываем строку из файла, находим в строке знак табуляции, подменяем знак табуляции нулём, копируем русское слово из буфера, копируем английское слово из буфера, проверяем на равенство.

6. Подсчитать количество строк в файле. Будем считывать файл посимвольно, считая количество символов '\n' до тех пор, пока не встретим символ EOF. EOF – это спецсимвол, который указывает на то, что ввод закончен и больше нет данных для чтения. Функция возвращает отрицательное значение в случае ошибки.
ЗАМЕЧАНИЕ: EOF имеет тип int, поэтому нужно использовать int для считывания символов. Кроме того, значение EOF не определено стандартом.

email

Всё ещё не понятно? – пиши вопросы на ящик

При выполнении кода на C++ могут возникать разные ошибки, которые не позволяют программе выполнять свою работу. Для работы с ошибками или исключениями в C++ используются ключевые слова try , catch и throw .

Содержание статьи:

Вступление: виды исключений и знакомство с try, catch и throw в C++


Есть два вида исключений, с которыми вы можете столкнуться в процессе:

  1. Синхронные исключения. Этот тип ошибок программа может контролировать сама. Например, ошибки в коде, допущенные программистом или неправильные параметры ввода.
  2. Асинхронные исключения. Этот тип ошибок не связан напрямую с кодом и программа не может их контролировать. Например, это может быть сбой диска, ошибка при открытии файла, сокета, выделения блока памяти.
  • try : позволяет определить блок кода, который будет проверяться на наличие ошибок во время его выполнения;
  • throw : нужен для создания и отображения исключений и используется для перечисления ошибок, которые генерирует функция, но не может самостоятельно обрабатывать исключения;
  • catch — блок кода, который выполняется при возникновенииопределенного исключения в блоке try .

Давайте посмотрим, как выглядит пример кода в С++ с использованием try catch и throw :

Вкратце объясним, как работают операторы try и catch в С++ на примере этого блока . Мы используем блок try для тестирования определенных строк кода: если переменная age меньше 18, мы генерируем исключение и обрабатываем его в блоке catch .

С помощью catch мы перехватываем ошибку и прописываем способ ее обработки. Оператор принимает параметр: в примере используется переменная типа int myNum для вывода значения возраста.

Если все данные соответствуют установленным параметрам, то ошибки не возникает. Например, если указанный возраст будет больше 18, а не 15, как указано в примере, то блок catch просто пропускается.

Если ошибка присутствует, то оператор throw выбросит ошибку. В throw можно прописать любое значение и оператор может выдать текст с пояснением, например:

Или установить числовое значение, например, то код ошибки будет выглядеть следующим образом:

После такой большой вводной части подробно рассмотрим генерацию исключений и как их обрабатывать, примеры использования try и catch в С++, подробно расскажем про задачи операторов.

Генерируем исключения в C++

Исключения могут быть выброшены в любом месте кода. Для этого в блоке нужно прописать throw .

Этот оператор определяет тип исключения и может быть любым выражением. Также throw сигнализирует об ошибке в коде и выводит исключение в консоль.

Помимо использования оператора throw , есть еще один способ мониторить ошибки в коде. Он более традиционный, но давайте рассмотрим и его, чтобы лучше понять механику обработки ошибок с помощью операторов.

Обычно мы разбиваем программу на несколько функций или подпрограмм, чтобы сделать ее читабельной и простой для понимания. Получается, что программа будет иметь связанные вызовы функций. То есть одна функция использует ту информацию, которую ей предоставляет другая функция.

Здесь возникают основные проблемы с обработкой ошибок при использовании оператора if :

  • Все задействованные функции должны возвращать одни и те же типы данных, например, целые числа. Из-за этого код становится длинным и громоздким, потому что приходится возвращать один и тот же вид данных.
  • Глобальную переменную нужно проверить сразу же после вызова функции в обработчике или кэшировать. Потому что она может обновиться, если в дальнейшем произойдет другая ошибка.
  • Обработка ошибки зависит от вызывающей стороны. Если исключение не обработать, оно может вызвать сбой в программе позже или программа продолжит работу неправильно.

Вот так выглядит обработка ошибок в коде при использовании оператора if :

А вот так будет выглядеть код с использованием try и catch в С++ (example):

По сравнению с несколькими строками кода в случае try и catch в С++ , предыдущий блок выглядит очень перегруженным и длинным. В целом при использовании оператора if обработка ошибок и программный код тесно взаимосвязаны. Из-за этого код становится беспорядочным, и трудно гарантировать, что все ошибки будут обработаны и программа будет работать нормально.

Метод try / catch , в свою очередь, обеспечивает четкое разделение между кодом, который знает об ошибке, и кодом, который знает, как обрабатывать ошибку. Таким образом, код, который находятся между этими операторами, может безопасно игнорировать ошибку.

Поэтому, запуская код в С++ Builder, лучше искать исключения с помощью try , catch и throw . Это сделает ваш код проще, чище и с меньшей вероятностью вы допустите ошибки в программе.

Ищем ошибки в коде

Для того чтобы проверить блок кода на ошибки и аномалии, используется оператор try . Так мы можем быть уверены, что если появится исключение в этой части кода, то try его заметит. Главная особенность оператора в том, что в отличие от if / else , которые смешиваются с обычным потоком данных, try отделяет обработку ошибок от обычного течения программы.

Блок try помещается вокруг кода, который может генерировать исключение, и закрывается другим оператором этой пары — catch . Код в блоке try / catch называется защищенным кодом , а синтаксис для использования связки этих операторов выглядит следующим образом:

С помощью метода try / catch можно перечислить и поймать сразу несколько видов исключений, если блок try вызывает несколько типов ошибок в разных ситуациях. Несмотря на то, что функция может генерировать множество исключений, вы можете обрабатывать не все, а только некоторые из них.

Обрабатываем ошибки с try и catch in С++

Блок catch , идущий в паре с оператором try , ловит и обрабатывает исключения. Чтобы указать, какой тип исключения вы хотите поймать и обработать, нужно прописать это в скобках после ключевого слова catch :

Приведенный выше код перехватит только исключение типа ExceptionName . Если вы хотите указать, что блок catch должен обрабатывать любой тип ошибок, который находит оператор try , просто поместите многоточие . между скобками:

Рассмотрим пример кода, в котором генерируется исключение деления на ноль:

Так как программа вызывает тип исключения const char * , в блоке catch необходимо указать const char * , чтобы ошибку можно было определить и обработать. Если скомпилировать и запустить этот блок кода, то в результате получим условие прописанное в throw :

Division by zero condition!

Как работают throw, try и catch в C++: примеры

Рассмотрим на примерах, как между собой взаимодействуют операторы throw , try и catch в С++. В блоке кода ниже приведен простой пример, демонстрирующий обработку исключений. Результат программы наглядно покажет, в какой последовательности происходит выполнение операторов :

В результате получается следующая последовательность:

Нужно не забывать прописывать одинаковые типы исключений в try / catch . Если исключение одного типа будет выброшено, а catch не сможет его поймать и обработать, то программа завершается ненормально:

В этом примере кода исключение является символом, но блок catch для захвата символа отсутствует. В результате блок вернет нам не исключение, а вот такое предупреждение:

С помощью try / catch можно указывать кастомные типы исключений, наследуя и переопределяя функциональность класса исключений. В примере ниже приведем код, который покажет, как вы можете использовать класс std :: exception для генерации собственной ошибки стандартным способом :

Результат выполнения кода выглядит так:

Еще немного о порядке обработке ошибок в C++

Когда мы прописываем операторы try / catch в коде, то исключение выбрасывается только при исполнении определенных условий. Рассмотрим как работают try , catch и throw в С++ на примере:

Пользователь может ввести число больше нуля, как и задумано. Тогда программа просто продолжит работать в нормальном режиме и пропустит блок с оператором catch . Допустим, пользователь ввел число 49. Тогда результат выполнения кода будет следующим:

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

Таким образом, строки кода с catch и throw выполняются только тогда, когда try обнаруживает исключение в коде. Если все данные удовлетворяют условиям кода, то блок с исключениями просто пропускается программой.

Подводим итоги

Как использовать try , catch и throw в С++, мы разобрались. Теперь кратко напомним, зачем все это нужно:

Видео: С++ try catch. Обработка исключений С++. Try catch: что это. Изучение С++ для начинающих

Highload нужны авторы технических текстов. Вы наш человек, если разбираетесь в разработке, знаете языки программирования и умеете просто писать о сложном!
Откликнуться на вакансию можно здесь .

Читайте также: