5 плохих правил программирования на Си

08/08/2013 - 22:17 Павел Бобков

Введение

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

Плохое правило №1: не делите, используйте сдвиг вправо

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

   В качестве примера, Плохой Советчик показывает код для вычисления среднего значения по 16 целочисленным значениям, которые накапливаются в переменную Sum во время 16 итераций цикла. А на 17 итерации среднее значение вычисляется путем сдвига суммы вправо на 4 бита (что равносильно делению на 2^4 = 16). Возможно худшая вещь в этом примере кода, насколько он привязан к паре дефайнов для магических чисел 16 и 4. Попытка изменить количество усредняемых образцов с 16 на 15 нарушит весь этот пример - вам придется менять правый сдвиг на полноценное деление. Также легко представить, что кто-то поменяет значение AVG_COUNT с 16 на 15, не понимая о сдвиге и вы получите ошибку, потому что сумма по-прежнему будет сдвигаться вправо на 4 бита. 

   Лучшее правило: сдвигайте биты, если вы подразумеваете операцию сдвига и делите, когда вы подразумеваете деление. 

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

Плохое правило №2: используйте тип переменной в зависимости от максимального значения этой переменной

   Плохой Советчик приводит пример переменной seconds, которая хранит целочисленное значение от 0 до 59. И показывает, что выбирает для нее тип char, вместо int. Целью этого правила, по его заявлению, является снижение используемой памяти.

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

   Невозможно понять замысел программиста, исходя из unsigned char seconds. Он выбрал char потому что этот тип достаточно большой или по каким то другим причинам? (Помните также, что обыкновенный char может быть знаковым или беззнаковым в зависимости от компилятора. Возможно программист даже знает, что char его компилятора по умолчанию беззнаковый и опускает ключевое слово unsigned). Намерение, стоящее за объявлением переменной типа short или long, тоже трудно разгадать. Short может быть 16 или 32 бита (или что-то еще) в зависимости от компилятора

   Лучшее правило: когда ширина переменной имеет целочисленное значение, используйте целочисленные типы фиксированной ширины определенные стандартом C99.

   Переменная, объявленная uint16_t, не оставляет сомнений относительно исходного замысла, потому что это четко означает контейнер для целого числа без знака не шире 16 бит. Это добавляет новой и полезной информации к исходному коду программы, и делает его более читабельным и переносимым. Теперь, когда C99 имеет стандартизированные имена целочисленных типов фиксированной ширины, объявления с участием short и long не должны больше использоваться. Даже char должен быть использован только для символьных данных (например, ASCII).

Плохое правило №3: избегайте >= и используйте <

   В описанной формулировке, я не могу сказать, что понимаю это правило или его цели, но для иллюстрации Плохой Советчик дает конкретный пример if-else if, где он рекомендует использовать код if (speed < 100) ... else if (speed >99), вместо if (speed < 100) .. else if (speed >=100). Что он имеет ввиду? Прежде всего, почему нельзя просто использовать if else конкретно в этом примере, если speed должна быть или ниже или выше 100.

   Даже если мы предположим, что нам нужно проверить условие на "больше чем 100" и "меньше или равно 100", зачем кому-то в здравом уме предпочитать использовать "больше чем 99"? Это бы сбило с толку любого читателя кода. Лично для меня это выглядит как ошибка и я должен возвращаться несколько раз, что бы найти логические проблемы с видимым несоответствием диапазонов проверок. Кроме того, я считаю, что краткое обоснование Плохого Советчика - "преимущество: меньший код", не соответствует действительности. Любой более менее приличный компилятор в состоянии оптимизировать любые сравнения так, как необходимо для применяемого процессора. 

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

Плохое правило №4: при объявлении переменных избегайте инициализации

   Плохой Советчик говорит, что это правило сделает инициализацию быстрее. Он приводит пример переменной unsigned char MyVariable = 100; против 

#define INITIAL_VALUE 100
unsigned char MyVariable;
//до входа в бесконечный цикл в main`е
MyVariable = INITIAL_VALUE

   Хотя из данного примера неясно, давайте предположим, что MyVariable является локальной переменной. (Она также может быть глобальной, как написано в его псевдо коде.) Я не думаю что здесь должен быть заметный прирос эффективности. Зато я думаю, что это правило создает дыру, чтобы забывать инициализировать переменную или непреднамеренно размещать инициализационный код внутри условного выражения. 

   Лучшее правило: инициализируйте каждую переменную как можно раньше, если вы знаете ее начальное значение.

   Я бы предпочел, чтобы каждой переменной при создании было присвоено начальное значение, даже с возможно максимальной отсрочкой создания этой переменной. Если вы используете компилятор C99 или С++, вы можете объявлять переменную в любом месте внутри тела функции. 

Плохое правило №5: используйте #define для константных чисел

   Пример, приведенный для этого правила, состоит в определении трех фиксированных значений, в том числе #define ON 1 и #define OFF 0. Объясняется это "повышенным удобством изменения значений в одном месте для нескольких файлов и лучшей структурированностью кода." И я согласен, что использование именованных констант вместо магических чисел где-нибудь в коде является хорошей практикой. Однако, я думаю, есть еще лучший способ делать это. 

   Лучшее правило: объявляйте константы, используя const и enum.

   Чтобы объявить переменную любого типа, которая не может менять свое значение во время выполнения программы, можно использовать ключевое слово const. Это предпочтительный путь объявления констант, так как в этом случае их можно безопасно использовать в выражениях сравнения и вызовах функций, ведь компилятор будет выполнять проверку типов данных. А если требуются группы целочисленных констант, лучше использовать перечисления, такие как enum{ OFF = 0, ON = 1}; 

Заключение

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

Michael Barr "Don’t Follow These 5 Dangerous Coding Standard Rules"
Вольный перевод ChipEnable.Ru

Comments   

# Icy Snake 2013-08-22 17:59
Возможно, что эти правила лучше применимы к C++.
# JoJo 2013-08-22 18:31
Почему?
# Степ 2015-02-05 06:19
Эти правила для конкретного компилятора микроконтроллер а возможно и дадут лучшее быстродействие, на AVR многое из этого делал и даже смотрел генерируемый код для выбора лучшей конструкции < или >=, и деление сдвигом осуществлял. По моему главное такие фишки сразу комментировать чтобы и самому не позабыть что да как.
# Dmitriy Vovk 2015-05-25 01:23
Так ведь иногда приходится каждый такт считать(

У вас недостаточно прав для комментирования.