Введение
Ошибки, увы, неизбежны, поэтому их обработка занимает очень важное место в программировании. И если алгоритмические ошибки можно выявить и исправить во время написания и тестирования программы, то ошибок времени выполнения избежать нельзя в принципе. Сегодня мы рассмотрим функции стандартной библиотеки (C Standard Library) и POSIX, используемые в обработке ошибок.
Переменная errno и коды ошибок
<errno.h>
errno – переменная, хранящая целочисленный код последней ошибки. В каждом потоке существует своя локальная версия errno, чем и обусловливается её безопастность в многопоточной среде. Обычно errno реализуется в виде макроса, разворачивающегося в вызов функции, возвращающей указатель на целочисленный буфер. При запуске программы значение errno равно нулю.
Все коды ошибок имеют положительные значения, и могут использоваться в директивах препроцессора #if. В целях удобства и переносимости заголовочный файл <errno.h>
определяет макросы, соответствующие кодам ошибок.
Стандарт ISO C определяет следующие коды:
- EDOM – (Error domain) ошибка области определения.
- EILSEQ – (Error invalid sequence) ошибочная последовательность байтов.
- ERANGE – (Error range) результат слишком велик.
Прочие коды ошибок (несколько десятков) и их описания определены в стандарте POSIX. Кроме того, в спецификациях стандартных функций обычно указываются используемые ими коды ошибок и их описания.
Нехитрый скрипт печатает в консоль коды ошибок, их символические имена и описания:
#!/usr/bin/perl use strict; use warnings; use Errno; foreach my $err (sort keys (%!)) { $! = eval "Errno::$err"; printf "%20s %4d %s\n", $err, $! + 0, $! }
Если вызов функции завершился ошибкой, то она устанавливает переменную errno в ненулевое значение. Если же вызов прошёл успешно, функция обычно не проверяет и не меняет переменную errno. Поэтому перед вызовом функции её нужно установить в 0
.
Пример:
/* convert from UTF16 to UTF8 */ errno = 0; n_ret = iconv(icd, (char **) &p_src, &n_src, &p_dst, &n_dst); if (n_ret == (size_t) -1) { VJ_PERROR(); if (errno == E2BIG) fprintf(stderr, " Error : input conversion stopped due to lack of space in the output buffer\n"); else if (errno == EILSEQ) fprintf(stderr, " Error : input conversion stopped due to an input byte that does not belong to the input codeset\n"); else if (errno == EINVAL) fprintf(stderr, " Error : input conversion stopped due to an incomplete character or shift sequence at the end of the input buffer\n"); /* clean the memory */ free(p_out_buf); errno = 0; n_ret = iconv_close(icd); if (n_ret == (size_t) -1) VJ_PERROR(); return (size_t) -1; }
Как видите, описания ошибок в спецификации функции iconv()
более информативны, чем в <errno.h>
.
Функции работы с errno
Получив код ошибки, хочется сразу получить по нему её описание. К счастью, ISO C предлагает целый набор полезных функций.
<stdio.h>
void perror(const char *s);
Печатает в stderr содержимое строки s
, за которой следует двоеточие, пробел и сообщение об ошибке. После чего печатает символ новой строки '\n'
.
Пример:
/* // main.c // perror example // // Created by Ariel Feinerman on 23/03/17. // Copyright 2017 Feinerman Research, Inc. All rights reserved. */ #include <stdio.h> #include <stdlib.h> #include <errno.h> int main(int argc, const char * argv[]) { // Generate unique filename. char *file_name = tmpnam((char[L_tmpnam]){0}); errno = 0; FILE *file = fopen(file_name, "rb"); if (file) { // Do something useful. fclose(file); } else { perror("fopen() "); } return EXIT_SUCCESS; }
<string.h>
char* strerror(int errnum);
Возвращает строку, содержащую описание ошибки errnum
. Язык сообщения зависит от локали (немецкий, иврит и даже японский), но обычно поддерживается лишь английский.
/* // main.c // strerror example // // Created by Ariel Feinerman on 23/03/17. // Copyright 2017 Feinerman Research, Inc. All rights reserved. */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> int main(int argc, const char * argv[]) { // Generate unique filename. char *file_name = tmpnam((char[L_tmpnam]){0}); errno = 0; FILE *file = fopen(file_name, "rb"); // Save error number. errno_t error_num = errno; if (file) { // Do something useful. fclose(file); } else { char *errorbuf = strerror(error_num); fprintf(stderr, "Error message : %s\n", errorbuf); } return EXIT_SUCCESS; }
strerror()
не безопасная функция. Во-первых, возвращаемая ею строка не является константной. При этом она может храниться в статической или в динамической памяти в зависимости от реализации. В первом случае её изменение приведёт к ошибке времени выполнения. Во-вторых, если вы решите сохранить указатель на строку, и после вызовите функцию с новым кодом, все прежние указатели будут указывать уже на новую строку, ибо она использует один буфер для всех строк. В-третьих, её поведение в многопоточной среде не определено в стандарте. Впрочем, в QNX она объявлена как thread safe.
Поэтому в новом стандарте ISO C11 были предложены две очень полезные функции.
size_t strerrorlen_s(errno_t errnum);
Возвращает длину строки с описанием ошибки errnum
.
errno_t strerror_s(char *buf, rsize_t buflen, errno_t errnum);
Копирует строку с описание ошибки errnum
в буфер buf
длиной buflen
.
Пример:
/* // main.c // strerror_s example -- works nowhere // // Created by Ariel Feinerman on 23/02/17. // Copyright 2017 Feinerman Research, Inc. All rights reserved. */ #define __STDC_WANT_LIB_EXT1__ 1 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> int main(int argc, const char * argv[]) { // Generate unique filename. char *file_name = tmpnam((char[L_tmpnam]){0}); errno = 0; FILE *file = fopen(file_name, "rb"); // Save error number. errno_t error_num = errno; if (file) { // Do something useful. fclose(file); } else { #ifdef __STDC_LIB_EXT1__ size_t error_len = strerrorlen_s(errno) + 1; char error_buf[error_len]; strerror_s(error_buf, error_len, errno); fprintf(stderr, "Error message : %s\n", error_buf); #endif } return EXIT_SUCCESS; }
Функции входят в Annex K (Bounds-checking interfaces), вызвавший много споров. Он не обязателен к выполнению и целиком не реализован ни в одной из свободных библиотек. Open Watcom C/C++ (Windows), Slibc (GNU libc) и Safe C Library (POSIX), в последней, к сожалению, именно эти две функции не реализованы. Тем не менее, их можно найти в коммерческих средах разработки и системах реального времени, Embarcadero RAD Studio, INtime RTOS, QNX.
Стандарт POSIX.1-2008 определяет следующие функции:
char *strerror_l(int errnum, locale_t locale);
Возвращает строку, содержащую локализованное описание ошибки errnum
, используя locale
. Безопасна в многопоточной среде. Не реализована в Mac OS X, FreeBSD, NetBSD, OpenBSD, Solaris и прочих коммерческих UNIX. Реализована в Linux, MINIX 3 и Illumos (OpenSolaris).
Пример:
/* // main.c // strerror_l example – works on Linux, MINIX 3, Illumos // // Created by Ariel Feinerman on 23/03/17. // Copyright 2017 Feinerman Research, Inc. All rights reserved. */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include <locale.h> int main(int argc, const char * argv[]) { locale_t locale = newlocale(LC_ALL_MASK, "fr_FR.UTF-8", (locale_t) 0); if (!locale) { fprintf(stderr, "Error: cannot create locale."); exit(EXIT_FAILURE); } // Generate unique filename. char *file_name = tmpnam((char[L_tmpnam]){0}); errno = 0; FILE *file = fopen(tmpnam(file_name, "rb"); // Save error number. errno_t error_num = errno; if (file) { // Do something useful. fclose(file); } else { char *error_buf = strerror_l(errno, locale); fprintf(stderr, "Error message : %s\n", error_buf); } freelocale(locale); return EXIT_SUCCESS; }
Вывод:
Error message : Aucun fichier ou dossier de ce type
int strerror_r(int errnum, char *buf, size_t buflen);
Копирует строку с описание ошибки errnum
в буфер buf
длиной buflen
. Если buflen
меньше длины строки, лишнее обрезается. Безопасна в многоготочной среде. Реализована во всех UNIX.
Пример:
/* // main.c // strerror_r POSIX example // // Created by Ariel Feinerman on 25/02/17. // Copyright 2017 Feinerman Research, Inc. All rights reserved. */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #define MSG_LEN 1024 int main(int argc, const char * argv[]) { // Generate unique filename. char *file_name = tmpnam((char[L_tmpnam]){0}); errno = 0; FILE *file = fopen(file_name, "rb"); // Save error number. errno_t error_num = errno; if (file) { // Do something useful. fclose(file); } else { char error_buf[MSG_LEN]; errno_t error = strerror_r (error_num, error_buf, MSG_LEN); switch (error) { case EINVAL: fprintf (stderr, "strerror_r() failed: invalid error code, %d\n", error); break; case ERANGE: fprintf (stderr, "strerror_r() failed: buffer too small: %d\n", MSG_LEN); case 0: fprintf(stderr, "Error message : %s\n", error_buf); break; default: fprintf (stderr, "strerror_r() failed: unknown error, %d\n", error); break; } } return EXIT_SUCCESS; }
Увы, никакого аналога strerrorlen_s()
в POSIX не определили, поэтому длину строки можно выяснить лишь экспериментальным путём. Обычно 300 символов хватает за глаза. GNU C Library в реализации strerror()
использует буфер длиной в 1024 символа. Но мало ли, а вдруг?
Пример:
/* // main.c // strerror_r safe POSIX example // // Created by Ariel Feinerman on 23/03/17. // Copyright 2017 Feinerman Research, Inc. All rights reserved. */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #define MSG_LEN 1024 #define MUL_FACTOR 2 int main(int argc, const char * argv[]) { // Generate unique filename. char *file_name = tmpnam((char[L_tmpnam]){0}); errno = 0; FILE *file = fopen(file_name, "rb"); // Save error number. errno_t error_num = errno; if (file) { // Do something useful. fclose(file); } else { errno_t error = 0; size_t error_len = MSG_LEN; do { char error_buf[error_len]; error = strerror_r (error_num, error_buf, error_len); switch (error) { case 0: fprintf(stderr, "File : %s\nLine : %d\nCurrent function : %s()\nFailed function : %s()\nError message : %s\n", __FILE__, __LINE__, __func__, "fopen", error_buf); break; case ERANGE: error_len *= MUL_FACTOR; break; case EINVAL: fprintf (stderr, "strerror_r() failed: invalid error code, %d\n", error_num); break; default: fprintf (stderr, "strerror_r() failed: unknown error, %d\n", error); break; } } while (error == ERANGE); } return EXIT_SUCCESS; }
Вывод:
File : /Users/ariel/main.c Line : 47 Current function : main() Failed function : fopen() Error message : No such file or directory
Макрос assert()
<assert.h>
void assert(expression)
Макрос, проверяющий условие expression
(его результат должен быть числом) во время выполнения. Если условие не выполняется (expression
равно нулю), он печатает в stderr значения __FILE__
, __LINE__
, __func__
и expression
в виде строки, после чего вызывает функцию abort()
.
/* // main.c // assert example // // Created by Ariel Feinerman on 23/03/17. // Copyright 2017 Feinerman Research, Inc. All rights reserved. */ #include <stdio.h> #include <stdlib.h> #include <assert.h> #include <math.h> int main(int argc, const char * argv[]) { double x = -1.0; assert(x >= 0.0); printf("sqrt(x) = %f\n", sqrt(x)); return EXIT_SUCCESS; }
Вывод:
Assertion failed: (x >= 0.0), function main, file /Users/ariel/main.c, line 17.
Если макрос NDEBUG
определён перед включением <assert.h>
, то assert()
разворачивается в ((void) 0)
и не делает ничего. Используется в отладочных целях.
Пример:
/* // main.c // assert_example // // Created by Ariel Feinerman on 23/03/17. // Copyright 2017 Feinerman Research, Inc. All rights reserved. */ #NDEBUG #include <stdio.h> #include <stdlib.h> #include <assert.h> #include <math.h> int main(int argc, const char * argv[]) { double x = -1.0; assert(x >= 0.0); printf("sqrt(x) = %f\n", sqrt(x)); return EXIT_SUCCESS; }
Вывод:
sqrt(x) = nan
Функции atexit(), exit() и abort()
<stdlib.h>
int atexit(void (*func)(void));
Регистрирует функции, вызываемые при нормальном завершении работы программы в порядке, обратном их регистрации. Можно зарегистрировать до 32 функций.
_Noreturn void exit(int exit_code);
Вызывает нормальное завершение программы, возвращает в среду число exit_code
. ISO C стандарт определяет всего три возможных значения: 0
, EXIT_SUCCESS
и EXIT_FAILURE
. При этом вызываются функции, зарегистрированные через atexit()
, сбрасываются и закрываются потоки ввода — вывода, уничтожаются временные файлы, после чего управление передаётся в среду. Функция exit()
вызывается в main()
при выполнении return или достижении конца программы.
Главное преимущество exit()
в том, что она позволяет завершить программу не только из main()
, но и из любой вложенной функции. К примеру, если в глубоко вложенной функции выполнилось (или не выполнилось) некоторое условие, после чего дальнейшее выполнение программы теряет всякий смысл. Подобный приём (early exit) широко используется при написании демонов, системных утилит и парсеров. В интерактивных программах с бесконечным главным циклом exit()
можно использовать для выхода из программы при выборе нужного пункта меню.
Пример:
/* // main.c // exit example // // Created by Ariel Feinerman on 17/03/17. // Copyright 2017 Feinerman Research, Inc. All rights reserved. */ #include <stdio.h> #include <stdlib.h> #include <math.h> void third_2(void) { printf("third #2\n"); // Does not print. } void third_1(void) { printf("third #1\n"); // Does not print. } void second(double num) { printf("second : before exit()\n"); // Prints. if ((num < 1.0f) && (num > -1.0f)) { printf("asin(%.1f) = %.3f\n", num, asin(num)); exit(EXIT_SUCCESS); } else { fprintf(stderr, "Error: %.1f is beyond the range [-1.0; 1.0]\n", num); exit(EXIT_FAILURE); } printf("second : after exit()\n"); // Does not print. } void first(double num) { printf("first : before second()\n") second(num); printf("first : after second()\n"); // Does not print. } int main(int argc, const char * argv[]) { atexit(third_1); // Register first handler. atexit(third_2); // Register second handler. first(-3.0f); return EXIT_SUCCESS; }
Вывод:
first : before second() second : before exit() Error: -3.0 is beyond the range [-1.0; 1.0] third #2 third #1
_Noreturn void abort(void);
Вызывает аварийное завершение программы, если сигнал не был перехвачен обработчиком сигналов. Временные файлы не уничтожаются, закрытие потоков определяется реализацией. Самое главное отличие вызовов abort() и exit(EXIT_FAILURE)
в том, что первый посылает программе сигнал SIGABRT
, его можно перехватить и произвести нужные действия перед завершением программы. Записывается дамп памяти программы (core dump file), если они разрешены. При запуске в отладчике он перехватывает сигнал SIGABRT
и останавливает выполнение программы, что очень удобно в отладке.
Пример:
/* // main.c // abort example // // Created by Ariel Feinerman on 17/02/17. // Copyright 2017 Feinerman Research, Inc. All rights reserved. */ #include <stdio.h> #include <stdlib.h> #include <math.h> void third_2(void) { printf("third #2\n"); // Does not print. } void third_1(void) { printf("third #1\n"); // Does not print. } void second(double num) { printf("second : before exit()\n"); // Prints. if ((num < 1.0f) && (num > -1.0f)) { printf("asin(%.1f) = %.3f\n", num, asin(num)); exit(EXIT_SUCCESS); } else { fprintf(stderr, "Error: %.1f is beyond the range [-1.0; 1.0]\n", num); abort(); } printf("second : after exit()\n"); // Does not print. } void first(double num) { printf("first : before second()\n"); second(num); printf("first : after second()\n"); // Does not print. } int main(int argc, const char * argv[]) { atexit(third_1); // register first handler atexit(third_2); // register second handler first(-3.0f); return EXIT_SUCCESS; }
Вывод:
first : before second() second : before exit() Error: -3.0 is beyond the range [-1.0; 1.0] Abort trap: 6
Вывод в отладчике:
$ lldb abort_example (lldb) target create "abort_example" Current executable set to 'abort_example' (x86_64). (lldb) run Process 22570 launched: '/Users/ariel/abort_example' (x86_64) first : before second() second : before exit() Error: -3.0 is beyond the range [-1.0; 1.0] Process 22570 stopped * thread #1: tid = 0x113a8, 0x00007fff89c01286 libsystem_kernel.dylib`__pthread_kill + 10, queue = 'com.apple.main-thread', stop reason = signal SIGABRT frame #0: 0x00007fff89c01286 libsystem_kernel.dylib`__pthread_kill + 10 libsystem_kernel.dylib`__pthread_kill: -> 0x7fff89c01286 <+10>: jae 0x7fff89c01290 ; <+20> 0x7fff89c01288 <+12>: movq %rax, %rdi 0x7fff89c0128b <+15>: jmp 0x7fff89bfcc53 ; cerror_nocancel 0x7fff89c01290 <+20>: retq (lldb)
В случае критической ошибки нужно использовать функцию abort()
. К примеру, если при выделении памяти или записи файла произошла ошибка. Любые дальнейшие действия могут усугубить ситуацию. Если завершить выполнение обычным способом, при котором производится сброс потоков ввода — вывода, можно потерять ещё неповрежденные данные и временные файлы, поэтому самым лучшим решением будет записать дамп и мгновенно завершить программу.
В случае же некритической ошибки, например, вы не смогли открыть файл, можно безопасно выйти через exit()
.
Функции setjmp() и longjmp()
Вот мы и подошли к самому интересному – функциям нелокальных переходов. setjmp()
и longjmp()
работают по принципу goto, но в отличие от него позволяют перепрыгивать из одного места в другое в пределах всей программы, а не одной функции.
<setjmp.h>
int setjmp(jmp_buf env);
Сохраняет информацию о контексте выполнения программы (регистры микропроцессора и прочее) в env
. Возвращает 0
, если была вызвана напрямую или value
, если из longjmp()
.
void longjmp(jmp_buf env, int value);
Восстанавливает контекст выполнения программы из env
, возвращает управление setjmp()
и передаёт ей value
.
Пример:
/* // main.c // setjmp simple // // Created by Ariel Feinerman on 18/02/17. // Copyright 2017 Feinerman Research, Inc. All rights reserved. */ #include <stdio.h> #include <stdlib.h> #include <setjmp.h> static jmp_buf buf; void second(void) { printf("second : before longjmp()\n"); // prints longjmp(buf, 1); // jumps back to where setjmp was called – making setjmp now return 1 printf("second : after longjmp()\n"); // does not prints // <- Here is the point that is never reached. All impossible cases like your own house in Miami, your million dollars, your nice girl, etc. } void first(void) { printf("first : before second()\n"); second(); printf("first : after second()\n"); // does not print } int main(int argc, const char * argv[]) { if (!setjmp(buf)) first(); // when executed, setjmp returned 0 else // when longjmp jumps back, setjmp returns 1 printf("main\n"); // prints return EXIT_SUCCESS; }
Вывод:
first : before second() second : before longjmp() main
Используя setjmp()
и longjmp
() можно реализовать механизм исключений. Во многих языках высокого уровня (например, в Perl) исключения реализованы через них.
Пример:
/* // main.c // exception simple // // Created by Ariel Feinerman on 18/02/17. // Copyright 2017 Feinerman Research, Inc. All rights reserved. */ #include <stdio.h> #include <stdlib.h> #include <math.h> #include <setjmp.h> #define str(s) #s static jmp_buf buf; typedef enum { NO_EXCEPTION = 0, RANGE_EXCEPTION = 1, NUM_EXCEPTIONS } exception_t; static char *exception_name[NUM_EXCEPTIONS] = { str(NO_EXCEPTION), str(RANGE_EXCEPTION) }; float asin_e(float num) { if ((num < 1.0f) && (num > -1.0f)) { return asinf(num); } else { longjmp(buf, RANGE_EXCEPTION); // | @throw } } void do_work(float num) { float res = asin_e(num); printf("asin(%f) = %f\n", num, res); } int main(int argc, const char * argv[]) { exception_t exc = NO_EXCEPTION; if (!(exc = setjmp(buf))) { // | do_work(-3.0f); // | @try } // | else { // | fprintf(stderr, "%s was hadled in %s()\n", exception_name[exc], __func__); // | @catch } // | return EXIT_SUCCESS; }
Вывод:
RANGE_EXCEPTION was hadled in main()
Внимание! Функции setjmp()
и longjmp
() в первую очередь применяются в системном программировании, и их использование в клиентском коде не рекомендуется. Их применение ухудшает читаемость программы и может привести к непредсказуемым ошибкам. Например, что произойдёт, если вы прыгните не вниз по стеку – в функцию верхнего уровня, а в параллельную, уже завершившую выполнение?
Информация
- стандарт ISO/IEC C (89/99/11)
- Single UNIX Specifcation, Version 4, 2016 Edition
- The Open Group Base Specifcations Issue 7, 2016 Edition (POSIX.1-2008)
- SEI CERT C Coding Standard
- cправочная информация среды программирования
- справочная информация операционной системы (man pages)
- заголовочные файлы (/usr/include)
- исходные тексты библиотеки (C Standard Library)
ссылка на оригинал статьи https://habrahabr.ru/post/324642/
Добавить комментарий