Как избежать ошибок в языке Си?

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

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

Сравните == и назначьте =

Наверняка каждый из вас раньше совершал такую ​​ошибку. В if или while вместо сравнения (a == b) мы делали присвоение (a = b) . Программа скомпилирована правильно, но при запуске оказывается, что условие всегда выполняется, либо программа останавливается в бесконечном цикле. Если мы сравниваем две переменные, нам просто нужно быть осторожными. Однако часто бывает, что одно из сравниваемых значений является константой или результатом функции. Мы можем это увидеть, если мы напишем:

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

При такой записи компилятор предупредит нас об ошибке.

Пропуск break в конструкции switch-case

У нас есть код:

Если var == 1 , то будет выполнено 1-3 , а затем 4-6. Переключается после достижения оператора break. Начало нового кейса — это просто метка, указывающая компилятору, где перейти от начала переключателя. Обычно отсутствие перерыва не является преднамеренным. Компиляторы часто сигнализируют об этом предупреждением. Однако иногда мы хотим перейти к коду из следующего кейса без перерыва. Тогда это должно быть ясно указано в комментарии:

Коварное форматирование текста

В этом примере else относится к внутреннему if, хотя отступ предполагает иное:

Return будет выполнен независимо от значений a и b , потому что тело if представлено с точкой с запятой:

Опасность бесконечного цикла, указатель не инкрементируется с точкой с запятой через while.

Но через некоторое время точка с запятой может понадобиться:

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

Даже если мы не возьмем в привычку добавлять код на той же строке, что и закрывающей скобки } , то имеет смысл сделать исключение для цикла do-while.

Благодаря этому мы сразу понимаем, что while является частью ранее запущенного выражения, а не отдельным циклом.

Неопределенность оператора

Автор задумал в коде разделить на значение под индикатором. К сожалению, компилятор C всегда выбирает самую длинную строку для составления допустимого оператора, поэтому / * станет началом комментария.

Простой способ устранить этот тип ошибки — правильно расставить пробелы. Если у нас есть унарный оператор, мы не ставим пробелы на стороне аргумента, если оператор двоичный, мы добавляем пробелы с обеих сторон. Таким образом мы получаем:

Константы в восьмеричном диапазоне

Взглянем на запись:

а = 64;
а = 064;
а = 0x64;

Каждой строке переменной a был присвоен другой номер. Строки 1 и 3 не требуют комментариев. Десятичный и шестнадцатеричный диапазоны широко используются в различных программах. Однако не все знают, что добавление невинного нуля в начале вызывает переход в восьмеричный диапазон, где 64 равно 52 в десятичном диапазоне. Кажется, что число с нулем в начале все еще находится в десятичном диапазоне. Следовательно, восьмеричные переменные вообще не должны использоваться.

«Слабое» условие выхода из цикла

Если вы хотите, чтобы цикл выполнялся до тех пор, пока переменная не достигнет заданного значения, всегда сравнивайте с > <> = <= , а не ==. Это особенно важно при ожидании установки значения в прерывании или изменении переменной внутри цикла.

Если переменная i изменяется внутри цикла, возможно, что условие выхода никогда не будет выполнено.

Чтобы защитить код от ошибок, используйте оператор> =.

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

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

Числа с плавающей запятой

Число типов float и double сильно отличается от обычных INT. Они хранятся в памяти экспоненциально. Это затрудняет их точное определение. Если мы присвоим переменной float значение 2,5, возможно, что на самом деле оно будет немного больше или немного меньше. Следовательно, такое выражение:

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

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

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

Деление и операции по модулю

Распространенной ошибкой является игнорирование того факта, что при делении int округляется до целого числа. Следовательно:

Операция вернет 0, а не 250. Операции деления и по модулю могут сбивать с толку отрицательные числа. Каким будет результат этой операции:

а будет 2 или -2? b будет -1 или -2? Будут ли 5% -3 и -5% 3 одинаковыми?

Оператор запятая

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

Она заставляет выполнять несколько операций в одном выражении. В приведенном выше примере цикла for оператор позволял обрабатывать две переменные вместо одной. А теперь посмотрим на другой отрывок:

Функция fun была вызвана с параметрами a и c (в качестве значения принимается последняя десятичная операция), и дополнительно мы увеличили значение переменной b. Это очень опасно. Вызов функции теперь имеет странный побочный эффект, и самого оператора запятая можно спутать с запятой между аргументами функции.

Оператор запятая также может сделать опечатку:

Автор хотел присвоить переменной а номер 1.5 , к сожалению , оператор запятая делает сохраненное значение 5.

Размер структуры

Может показаться, что переменные a и b должны принимать одно и то же значение. Ведь структура состоит из 3-х байтов, поэтому выражения однозначны. Однако во многих процессорах, например ARM или x86, структуры выровнены по некоторому значению, например, 32 или 64 бита. В этом случае размер структуры будет больше суммы размеров всех ее элементов.

В пределах одной архитектуры две одинаковые структуры могут иметь разные размеры. Размер структуры всегда должен определяться размером всей структуры, а не суммой размеров элементов.

Оператор безусловного перехода

Оператор goto или стандартные библиотечные функции setjmp позволяют переходить в совершенно разные места вашего кода. Это точное представление о низкоуровневом поведении компьютера. В ассемблере переход к метке был единственным способом изменить поток кода. Циклы или подпрограммы реализованы на языке ассемблера с инструкциями перехода.

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

Поэтому практика нам говорит, что goto не следует использовать вообще.

Макросы

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

Но того, что задумал автор, не будет. Чтобы избежать этой проблемы, просто возьмите все аргументы макроса в круглые скобки. Однако бывают ситуации, когда даже это не поможет:

Операция ++ будет выполняться дважды, и разработчик может не знать об этом. Даже если мы написали макрос правильно и у него нет побочных эффектов, его все равно сложно отладить.

Поэтому даже для простых задач лучше использовать функции вместо макросов. Тем более, что компилятор все равно может оптимизировать код, и макрос будет совсем не эффективным.

Препроцессор

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

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

Другие опасные структуры

Если первое условие не выполняется, будет ли программа проверять дальнейшие условия и увеличивать переменные c и d на единицу?

Будет ли увеличение выполняться до или после присвоения нового значения?

После выполнения оператора continue программа переходит к следующей итерации. Но выполним ли мы ++ первым? В противном случае у нас будет неочевидный бесконечный цикл.

Вывод

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

Бывают случаи, когда опечатка незаметно меняет поведение кода. Если мы будем знать, какие ситуации потенциально опасны, нам будет легче исправить ошибки.

С Уважением, МониторБанк

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