Нужно ли проверять каждую маленькую ошибку в C?

60

Как хороший программист, нужно писать надежные коды, которые будут обрабатывать каждый результат его программы. Однако почти все функции из библиотеки C будут возвращать 0, -1 или NULL в случае ошибки.

Иногда очевидно, что необходима проверка ошибок, например, когда вы пытаетесь открыть файл. Но я часто игнорирую проверку ошибок в функциях, таких как printfили даже mallocпотому, что я не чувствую необходимости.

if(fprintf(stderr, "%s", errMsg) < 0){
    perror("An error occurred while displaying the previous error.");
    exit(1);
}

Это хорошая практика, чтобы просто игнорировать определенные ошибки, или есть лучший способ обработать все ошибки?

Дерек 朕 會 功夫
источник
13
Зависит от уровня надежности, который требуется для проекта, над которым вы работаете. Системы, которые могут получать входные данные от ненадежных сторон (например, общедоступные серверы) или работать в средах, не являющихся полностью доверенными, необходимо кодировать очень осторожно, чтобы избежать превращения кода в бомбу замедленного действия (или взлом самого слабого звена) ). Очевидно, что хобби и учебные проекты не нуждаются в такой надежности.
Rwong
1
Некоторые языки предоставляют исключения. Если вы не перехватываете исключения, ваш поток будет прерван, что лучше, чем позволить ему продолжить работу с плохим состоянием. Если вы решите перехватывать исключения, вы можете охватить множество ошибок в многочисленных строках кода, включая вызванные функции и методы, одним tryоператором, поэтому вам не нужно проверять каждый отдельный вызов или операцию. (Также обратите внимание, что некоторые языки лучше других обнаруживают простые ошибки, такие как нулевая разыменование или индекс массива за пределами границ.)
Эрик Эйдт
1
Проблема в основном методологическая: вы обычно не пишете проверку ошибок, когда вы все еще выясняете, что вы должны реализовать, и вы не хотите добавлять проверку ошибок прямо сейчас, потому что вы хотите получить «счастливый путь» прямо первым. Но вы все равно должны проверять наличие malloc и co. Вместо того, чтобы пропустить этап проверки ошибок, вы должны расставить приоритеты в своих действиях по кодированию и позволить проверке ошибок быть неявным постоянным этапом рефакторинга в вашем списке TODO, который применяется всякий раз, когда вы удовлетворены своим кодом. Добавление комментария «/ * CHECK * /» может помочь.
coredump
7
Я не могу поверить, что никто не упоминается errno! В случае, если вы не знакомы, хотя это правда, что «почти все функции из библиотеки C будут возвращать 0 или -1 или NULLв случае ошибки», они также устанавливают глобальную errnoпеременную , к которой вы можете получить доступ, используя, #include <errno.h>а затем просто читая стоимость errno. Так, например, если openвозвращается (2) -1, вы можете проверить errno == EACCES, будет ли это указывать на ошибку прав доступа или на то ENOENT, что запрошенный файл не существует.
wchargin
1
@ErikEidt C не поддерживает try/ catch, хотя вы можете смоделировать его с помощью прыжков.
Мачта

Ответы:

29

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

В языках более высокого уровня с программной обработкой исключений это часто обозначается как «поймать исключение в методе, где вы действительно можете что-то с этим сделать». Если произошла ошибка файла, возможно, вы позволили ему скопировать стек в коде пользовательского интерфейса, который может фактически сказать пользователю «ваш файл не удалось сохранить на диск». Механизм исключений эффективно поглощает «каждую маленькую ошибку» и неявно обрабатывает ее в соответствующем месте.

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

Это хорошая практика, чтобы просто игнорировать определенные ошибки, или есть лучший способ обработать все ошибки?

Игнорировать определенные ошибки? Может быть. Например, разумно предположить, что запись в стандартный вывод не завершится неудачей. В случае неудачи, как бы вы сказали пользователю? Да, это хорошая идея - игнорировать определенные ошибки или защищаться от них, чтобы предотвратить их. Например, проверьте на ноль перед делением.

Есть способы обработать все или, по крайней мере, большинство ошибок:

  1. Вы можете использовать переходы, похожие на gotos, для обработки ошибок . Хотя это спорный вопрос среди профессионалов в области программного обеспечения, для них есть действительные применения, особенно во встроенном и критичном для производительности коде (например, ядре Linux).
  1. Каскадные ifс:

    if (!<something>) {
      printf("oh no 1!");
      return;
    }
    if (!<something else>) {
      printf("oh no 2!");
      return;
    }
  2. Проверьте первое условие, например, открытие или создание файла, а затем предположите, что последующие операции выполнены успешно.

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

Сообщество
источник
21
Извините, мелкая гнида здесь - «запись в стандартный вывод не завершится неудачей. Если он произойдет, как вы скажете пользователю, в любом случае?» - написав на стандартную ошибку? Нет никакой гарантии, что отказ написать одному означает, что невозможно написать другому.
Damien_The_Unbeliever
4
@Damien_The_Unbeliever - тем более что он stdoutможет быть перенаправлен в файл или вообще не существовать в зависимости от системы. stdoutэто не поток «запись в консоль», это обычно так , но это не обязательно.
Давор Адрало
3
@JAB Вы отправляете сообщение для stdout... очевидного :-)
TripeHound
7
@JAB: Вы можете выйти с EX_IOERR или около того, если это было уместно.
Джон Маршалл
6
Что бы вы ни делали, не просто бегите, как будто ничего не случилось! Если команде dump_database > backups/db_dump.txtне удается записать в стандартный вывод в любой заданной точке, я бы не хотел, чтобы она продолжалась и успешно завершалась. (Не то, чтобы базы данных создавались таким образом, но точка зрения остается неизменной)
Бен С.
15

Однако почти все функции из библиотеки C будут возвращать 0, -1 или NULL в случае ошибки.

Да, но вы знаете, какую функцию вы вызывали ?

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

Вам не нужно делать это для каждого вызова функции. Но в первый раз, когда вы видите сообщение об ошибке «Произошла ошибка при отображении предыдущей ошибки», когда вам действительно нужна была полезная информация, это будет последний раз, когда вы увидите это сообщение об ошибке, потому что вы немедленно собираетесь изменить сообщение об ошибке на что-то информативное, что поможет вам устранить проблему.

Роберт Харви
источник
5
Этот ответ заставил меня улыбнуться, потому что это правда, но не отвечает на вопрос.
RubberDuck
Как это не отвечает на вопрос?
Роберт Харви
2
одной из причин, по которой я думаю, что C (и другие языки) перепутали логические 1 и 0, является та же причина, по которой любая приличная функция, возвращающая код ошибки, возвращает «0» без ошибок. но тогда ты должен сказать if (!myfunc()) {/*proceed*/}. 0 не было ошибкой, а все ненулевые были своего рода ошибкой, и у каждой ошибки был свой ненулевой код. как сказал мой друг, «есть только одна Истина, но много лжи». «0» должно быть «true» в if()тесте, а все, что не равно нулю, должно быть «false».
Роберт Бристоу-Джонсон
1
нет, делая это по-своему, есть только один возможный отрицательный код ошибки. и много возможных кодов no_error.
Роберт Бристоу-Джонсон
1
@ robertbristow-johnson: Тогда прочитайте это как "Там не было никакой ошибки." if(function()) { // do something }читается как «если функция выполняется без ошибок, то что-то делать». или, что еще лучше, «если функция выполняется успешно, сделайте что-нибудь». Серьезно, тех, кто понимает соглашение, это не смущает. Возврат false(или 0) при возникновении ошибки не позволит использовать коды ошибок. Ноль означает ложь в C, потому что так работает математика. См. Programmers.stackexchange.com/q/198284
Роберт Харви
11

TLDR; Вы почти никогда не должны игнорировать ошибки.

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

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

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

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

Это та же проблема, что и при этом:

try
{
    run();
} catch (Exception) {
    // comments expected here!!!
}

Если вы видите, что без пустых комментариев внутри пустого блока catch это, безусловно, проблема. Нет никаких оснований полагать, что вызов malloc()будет выполнен успешно в 100% случаев.

Alex
источник
Я согласен, что ремонтопригодность - действительно важный аспект, и этот абзац действительно ответил на мой вопрос. Спасибо за подробный ответ.
Дерек 朕 會 功夫
Я думаю, что миллиарды программ, которые никогда не проверяют свои вызовы malloc и тем не менее никогда не дают сбоя, являются хорошим поводом для лени, когда настольная программа использует только несколько МБ памяти.
whatsisname
5
@whatsisname: просто потому, что они не аварийно завершают работу, не означает, что они не портят данные молча. malloc()это одна функция, которую вы определенно хотите проверить возвращаемые значения!
TMN
1
@ TMN: если malloc не получится, программа немедленно прекратит работу и прекратит работу, она не продолжит работу. Это все о риске и о том, что уместно, а не "почти никогда".
whatsisname
2
@Alex C не имеет недостатка в обработке ошибок. Если вы хотите исключения, вы можете использовать их. Стандартная библиотека, в которой не используются исключения, является преднамеренным выбором (исключения вынуждают вас автоматически управлять памятью, и часто вы не хотите этого для низкоуровневого кода).
Мартинкунев
9

Вопрос на самом деле не зависит от языка, а скорее от пользователя. Подумайте над вопросом с точки зрения пользователя. Пользователь делает что-то, например, вводит имя программы в командной строке и нажимает ввод. Что ожидает пользователь? Как они могут определить, что что-то пошло не так? Могут ли они позволить себе вмешаться в случае ошибки?

Во многих видах кода такие проверки излишни. Однако в высоконадежном коде безопасности, таком как коды для ядерных реакторов, проверка патологических ошибок и запланированные пути восстановления являются частью повседневного характера работы. Считается, что стоит потратить время на то, чтобы спросить: «Что произойдет, если Х выйдет из строя? Как мне вернуться в безопасное состояние?» В менее надежном коде, таком как код для видеоигр, вы можете избежать гораздо меньшего количества ошибок.

Другой похожий подход - насколько вы можете улучшить состояние, улавливая ошибку? Я не могу сосчитать количество программ на C ++, которые гордо отлавливают исключения, только для того, чтобы просто выбросить их, потому что они на самом деле не знали, что с ними делать ... но они знали, что должны обрабатывать исключения. Такие программы ничего не получили от лишних усилий. Добавляйте только код проверки ошибок, который, по вашему мнению, может справиться с ситуацией лучше, чем просто не проверять код ошибки. Тем не менее, в C ++ есть особые правила для обработки исключений, возникающих во время обработки исключений, для того, чтобы перехватить их более осмысленным образом (и под этим я имею в виду вызов terminate (), чтобы убедиться, что погребальный костер, который вы создали для себя, загорелся в своей славе)

Насколько патологическим вы можете получить? Я работал над программой, которая определила «SafetyBool», чьи истинные и ложные значения были тщательно выбраны, чтобы иметь равномерное распределение единиц и нулей, и они были выбраны так, чтобы любой из множества аппаратных сбоев (однобитовые перевороты, шина данных следы поломки и т. д.) не приводили к неверному истолкованию логического значения. Излишне говорить, что я бы не стал утверждать, что это практика программирования общего назначения, используемая в любой старой программе.

Корт Аммон
источник
6
  • Различные требования безопасности требуют разных уровней правильности. В авиационном или автомобильном программном обеспечении проверяются все возвращаемые значения, ср. MISRA.FUNC.UNUSEDRET . В быстром доказательстве концепции, которая никогда не покидает вашу машину, вероятно, нет.
  • Кодирование стоит времени. Слишком много нерелевантных проверок в небезопасном программном обеспечении - это усилия, которые лучше потратить в другом месте. Но где сладость между качеством и стоимостью? Это зависит от инструментов отладки и сложности программного обеспечения.
  • Обработка ошибок может затенить поток управления и внести новые ошибки. Мне очень нравятся функции оболочки Ричарда "сети" Стивенса, которые по крайней мере сообщают об ошибках.
  • Обработка ошибок редко может быть проблемой производительности. Но большинство вызовов библиотеки C займет столько времени, что затраты на проверку возвращаемого значения неизмеримо малы.
Питер - Восстановить Монику
источник
3

Немного абстрактного взгляда на вопрос. И это не обязательно для языка Си.

Для больших программ у вас будет слой абстракции; возможно, часть двигателя, библиотеки или в рамках. Этот слой не будет заботиться о погоде вы получите достоверные данные или результат будет значение по умолчанию некоторое: 0, -1, и nullт.д.

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

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

Поэтому я считаю, что иногда лучше полностью исключить обработку ошибок из части кода, потому что эта часть просто не выполняет эту работу. А потом есть какой-нибудь процессор, который оценил бы вывод и укажет на ошибку.

В основном это делается для разделения обязанностей, что приводит к удобочитаемости кода и лучшей масштабируемости.

Творческая Магия
источник
0

В общем, если у вас нет веских причин не проверять это состояние ошибки, вы должны это сделать. Единственная веская причина, по которой я могу подумать, чтобы не проверять состояние ошибки, - это когда вы не можете сделать что-то значимое в случае сбоя. Это очень сложная планка, потому что всегда есть один жизнеспособный вариант: выйти изящно.

Умный Неологизм
источник
Я бы вообще не согласился с вами, так как ошибки - это ситуации, которые вы не учли. Трудно понять, как может проявиться ошибка, если вы не знаете, при каких условиях возникла ошибка. И, таким образом, обработка ошибок перед фактической обработкой данных.
Творческая магия
Например, я сказал, что если что-то возвращает или выдает ошибку, даже если вы не знаете, как исправить ошибку и продолжить, вы всегда можете изящно завершить работу с ошибкой, зарегистрировать ошибку, сообщить пользователю, что она не работает должным образом, и т. Д Дело в том, что ошибка не должна остаться незамеченной, не заблокирована, «тихо провалиться» или вызвать сбой приложения. Вот что означает «изящный провал» или «изящный выход».
Умный неологизм