Всего на сайте:
183 тыс. 477 статей

Главная | Информатика

Abort, Retry, Fail?  Просмотрен 196

 

В предыдущих главах мы использовали для обработки ошибок функции вроде eprintf и est rdup — просто выводили некие сообщения перед тем, как прервать выполнение программы. Например, функция eprintf ведет себя так же, как fprintf (stderr, .. .), но после вывода сообщения выходит из программы с некоторым статусом ошибки. Она использует заголовочный файл <stdarg. h> и библиотечную функцию vfprintf для вывода аргументов, представленных в прототипе многоточием (...). Использование библиотеки stelarg должно быть начато вызовом va_start и завершено вызовом va_end. Мы еще вернемся к этому интерфейсу в главе 9.

#include <stdarg.h>

#include <string.h>

#include <errno.h>

 

/* eprintf: печать сообщения об ошибке и выход */

void eprintf(char *fmt, ...)

{

va_list args;

 

fflush(stdout);

if (progname() ! = NULL)

fprintf(stderr, "%s: ", progname());

 

va_start(args, fmt);

vfprintf (stderr, fmt, args);

va_end(args);

 

if (fmit[0] != ‘\0' && fmt[strlen(fmt)-1] == ' : ')

fprintf(stderr, " %s", strerror(errno));

fprintf(stderr, "\n");

exit(2); /* общепринятое значение */

/* для ненормального завершения работы */

}

 

Если аргумент формата оканчивается двоеточием ( : ), то eprintf вызы­вает стандартную функцию strerror, которая возвращает строку, содер­жащую всю доступную дополнительную системную информацию об ошибке. Мы написали еще функцию weprintf, сходную с eprintf, кото­рая выводит предупреждение, но не завершает программу. Интерфейс, схожий с printf, удобен для создания строк, которые могут быть напе­чатаны или выданы в окне диалога.

Сходным образом работает estrdup: она пытается создать копию стро­ки и, если памяти для этого не хватает, завершает программу с сообще­нием об ошибке (с помощью eprintf):

 

/* estrdup: дублирует строку; */

/* при возникновении ошибки сообщает об этом */

char *estrdup(char *s)

{

char *t;

 

t = (char *) malloc(strlen(s)+1);

if (t == NULL)

eprintf("estrdup(\"%.20s\") failed:", s);

strcpy(t, s);

return t;

}

 

Функция emallос предоставляет аналогичные возможности для вызова malloc:

/* emalloc: выполняет malloc; */

/* при возникновении ошибки сообщает об этом */

void *emalloc(size__t n)

{

void *p;

p = malloc(n);

if (p == NULL)

eprintf("malloc of %u bytes failed:", n);

return p;

}

Эти функции описаны в заголовочном файле eprintf. h:

 

/* eprintf.h: функции, сообщающие об ошибках */

extern void eprintf(char *, ...);

extern void weprintf(char *, ...);

extern char *estrdup(char *);

extern void *emalloc(size_t);

extern void *erealloc(void *, size_t);

extern char *progname(void);

extern void setprogname(char *);

 

Он включается в любой файл, вызывающий одну из функций, которые сообщают об ошибке. Каждое сообщение об ошибке содержит имя программы, определенное вызывающим кодом, — оно устанавливается и извлекается простейшими функциями setprogname и progname, описанными в том же заголовочном файле и определенными в исходном файле вместе с eprintf:

 

static char *name = NULL; /* имя программы для сообщений */

/* setprogname: устанавливает хранимое имя программы */

void setprogname(char *str)

{

name = estrdup(str);

}

 

/* progname: возвращает хранимое имя программы */

char *progname(void)

{

return name;

}

 

Типичный пример использования выглядит примерно так:

int main(int argc, char *argv[])

{

setprogname("markov");

f = fopen(argv[i], "r");

if (f == NULL)

eprintf("can't open %s:", argv[i]);

}

 

что приводит к появлению сообщений вроде

 

markov: can't open psalm.txt: No such file or directory

 

Мы считаем эти "оберточные" функции вполне подходящими для на­ших собственных программ, поскольку они унифицируют обработку ошибок; кроме того, само их присутствие вдохновляет на поиск ошибок. Ничего сложного или особо выдающегося в них нет, так что вы можете запросто придумать для себя какие-то более подходящие варианты.

 

Представим теперь, что вместо создания функций для собственного использования нам надо разработать библиотеку, с которой будут рабо­тать другие программисты. Что должна делать функция из этой библио­теки при возникновении ошибки? Те функции, что мы только что напи­сали, выводят сообщение и умирают. Для многих программ, особенно для небольших самостоятельных утилит, такое поведение может быть вполне приемлемым. Для других же программ простой выход не годит­ся, поскольку при этом другие части программы лишаются возможности хотя бы попытаться вернуться в нормальное состояние; характерным примером являются текстовые редакторы, — в них стоит приложить максимум усилий для сохранения редактируемого документа.

В некото­рых ситуациях библиотечные функции не должны даже выдавать ника­кого сообщения, поскольку существуют системы, где такое сообщение будет мешать отображению полезной информации или же, наоборот, просто сгинет бесследно. Для подобных случаев полезно записывать со­общения в некий отдельный журнальный файл (log file), который можно просматривать независимо.

Обнаруживайте ошибки на низком уровне, обрабатывайте на высо­ком.Существует общий принцип: ошибки должны обнаруживаться на самом низком уровне, какой только возможен; обрабатывать же их надо на высоком уровне. В большинстве случаев определять способ обработ­ки ошибки должен вызывающий код, а не вызываемый. Библиотечные функции могут помочь в этом, обеспечивая приемлемую реакцию при сбоях, — например, при получении несуществующего поля в качестве аргумента не прерывать работу всей программы, а возвращать NULL. Или, как в csvgetline, возвращать NULL вне зависимости от того, сколько раз эта функция была вызвана после достижения конца файла.

Не всегда очевидно, какие же значения должны возвращаться при ошибках; мы уже сталкивались с проблемой возвращаемого значения у функции csvgetline. Хотелось бы, конечно, возвращать как можно более содержательную информацию, но при этом в такой форме, чтобы овстальная часть программы могла использовать ее без труда. В С, C++ и Java это значит, что информация должна возвращаться в качестве результата функции и, возможно, в значениях параметров-ссылок (указателей). Многие библиотечные функции умеют различать обычные значения и специальные значения ошибок. Функции ввода типа getchar

возвращают значение, конвертируемое в char для нормальных данных, и некоторое неконвертируемое в char значение, например EOF, для обозначения конца файла или ошибки.

Этот механизм, однако, не работает, если функция может возвращать любые значения из возможного диапазона. Например, математические функции вроде log могут возвращать любое число с плавающей точкой. В стандарте IEEE для чисел с плавающей точкой предусмотрено специальное значение NaN ("not a number" — не число), означающее ошибку, — это значение и возвращается функциями в случае ошибки.

Некоторые языки, такие как Perl и Tel, предоставляют несложный способ группировки двух и более значений в кортеж (tuple). В таких языках значение функции и код ошибки можно без проблем передавать совместно. В C++ STL имеется тип данных pair, который можно использовать таким же образом.

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

Именно такой подход используется в Unix и стандартной библиотеке С: многие системные вызовы и библиотечные функции возвращают в случае ошибки -1 и при этом устанавливают глобальную переменную errno; функция strerro r возвращает строку, соответствующую номеру ошибки. В нашей системе программа

 

#include <stdio.h>

#include <string.h>

#include <errno.h>

#include <math.h>

 

/* errno main: тестирование библиотеки */

int main(void)

{

double f;

 

errno = 0; /* очищаем переменную кода ошибки */

f = log(-1.23);

printf("%f %d %s\n", f, errno, strerror(errno));

return 0;

}

 

напечатает

 

nаn0х10000000 33 Domain error

 

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

Используйте исключения только для исключительных ситуаций.В не­которых языках для отлова нестандартных ситуаций и восстановления после них имеется специальный механизм исключений, или исключи­тельных ситуаций (exceptions); таким образом предоставляется альтер­нативный способ управления работой программы при возникновении каких-либо проблем.

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

 

String fname = "someFileName";

try {

FilelnputStream in = new FilelnputStream(fname);

int c;

while ((c = in. read()) != -1)

System.out.print((char) c);

in.close();

} catch (FileNotFoundException e) {

System.err. println(fname + " not found");

} catch (I0Exception e) {

System.err. println("I0Exception: " + e);

e.pnntStackTrace();

}

Этот цикл считывает символы, пока не будет достигнут конец фай­ла — ожидаемое событие, которое функция read отмечает возвратом зна­чения -1. Однако, если файл не может быть открыт, возникает (или, как принято говорить, возбуждается) исключение, а не установка перемен­ной in в null, как это было бы сделано в С или C++. Наконец, если в бло­ке try происходит какая-то другая ошибка ввода, также возбуждается исключение, обрабатываемое в блоке I0Exception.

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

В С пара функций — setjmp и longjmp — предоставляет возможность реализовать механизм исключений на гораздо более низком уровне, но это настолько сложно, что мы не будем описывать, как это сделать.

Как насчет восстановления ресурсов при возникновении ошибки? Должна ли библиотека предпринимать попытки такого восстановления, если что-то идет не так, как надо? Как правило, нет, однако очень неплохо предусмотреть какой-то механизм, позволяющий удостовериться, что информация сохранилась в максимально корректной форме. Естественно, неиспользуемое пространство памяти должно быть высвобождено. Если же к каким-то переменным еще возможен доступ, они долж­ны быть установлены в осмысленные значения. Распространенной причиной ошибок является использование указателя на уже освобожденную память. Чтобы не попасться на эту удочку, достаточно в коде об­работки ошибки, который высвобождает что-то, установить указатель, адресующийся к этому чему-то, в ноль. Функция reset во второй версии библиотеки CSV как раз и являлась нашей попыткой преодолеть неко­торые из описанных проблем. Обобщая же все вышесказанное, отметим: надо добиваться того, чтобы библиотека оставалась пригодна к использованию даже после возникновения ошибки.

Предыдущая статья:Управление ресурсами Следующая статья:Пользовательские интерфейсы
page speed (0.2334 sec, direct)