Обработка ошибок в C

от автора

Введение

Ошибки, увы, неизбежны, поэтому их обработка занимает очень важное место в программировании. И если алгоритмические ошибки можно выявить и исправить во время написания и тестирования программы, то ошибок времени выполнения избежать нельзя в принципе. Сегодня мы рассмотрим функции стандартной библиотеки (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() в первую очередь применяются в системном программировании, и их использование в клиентском коде не рекомендуется. Их применение ухудшает читаемость программы и может привести к непредсказуемым ошибкам. Например, что произойдёт, если вы прыгните не вниз по стеку – в функцию верхнего уровня, а в параллельную, уже завершившую выполнение?

Информация

ссылка на оригинал статьи https://habrahabr.ru/post/324642/


Комментарии

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

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