Pull to refresh

10 главных правил убивания жуков

Reading time 5 min
Views 42K
Понимаю, что заголовок выглядит как машинный перевод, но лучшего эквивалента " Top 10 Bug-Killing Coding Standard Rules " придумать не смог.

10 главных правил по избеганию багов — подсказали в комментах

Данный пост представляет собой вольный пересказ ключевых понятий книги Michael Barr «Embedded C Coding Standard», изложенных в его выступлении на вебинаре в июне этого года (не знаю как поставить тэг «перевод»).
Часть правил применима только к C++ и расширениям C, а часть и к стандарту языка.

Правило 1

Фигурные скобки { } ВСЕГДА должны обрамлять блок кода после операторов if,else,switch,while,do,for.
Следствие: Одиночный оператор и пустое выражение также должны быть обрамлены фигурными скобками.

Доказательство: рассмотрим простой пример кода
if (5 == foo)
   bar();
always_run();


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

if (5 == foo)
//   bar();
always_run();


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

if (5 == foo) {
//   bar();
};
always_run();


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

Правило 2

Используйте ключевое слово const везде, где это возможно
Следствия: Используем при описании переменных, которые не будут изменяться после инициализации; при описании параметров функции, которые не должны модифицироваться; при описании полей структур и объединений, которые не могут модифицироваться (в том числе структур описания регистров устройств); как рекомендованная замену #define для числовых констант.

Доказательство: это ключевое слово не стоит ничего при исполнениии, в некоторых системах позволяет сэкономить оперативную память путем размещения в ПЗУ, позволяет компилятору обнаружить недопустимые операции с данными, дает читателю дополнительную информацию, занимает немного места (не требует отдельной строки) — короче, нет никаких оснований пренебрегать этим правилом./*1/

char const *gp_model_name =  “Acme  9000”;
int const g_build_number = CURRENT_BUILD_NUMBER();
char *strncpy (char *p_dest, char const *p_src, size_t count);
size_t const HEAP_SIZE = 8192;


Правило 3

Используйте ключевое слово static везде, где это возможно.
Следствие: Использовать для ВСЕХ нелокальных переменных и функций, которые не должны быть видны где либо, кроме модуля, их декларирующего.

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

static uint32_t g_next_timeout = 0;
static uint32_t add_timer_to_active_list(void)


Правило 4

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

Доказательство: несколько замедляет исполнение команд с такими переменными (вот откуда примечание), но позволяет избежать труднообнаружимых ошибок, дает дополнительную информацию читателю, *1.

uint16_t volatile g_state = SYSTEM_STARTUP;
typedef struct {
...
} my_fpga_t;
my_fpga_t volatile *const p_timer =  ...


Правило 5

Комментарии не должны использоваться для отключения блока кода, даже временно.
Следствие: Используйте директиву условной компиляции препроцессора; не используйте вложенные комментарии, даже если ваш диалект С это позволяет; используйте систему контроля версий для экспериментов с кодом; не оставляйте закомментированый код в релизе.
Вроде бы правило понятно и несложное к выполнению, но рука так и тянется вставить /* и */, (тем более, что другое правило — комментариии при помощи // я уже принял) — надо бороться с вредной привычкой.

#if 0
a = a + 1;
#endif


Правило 6

Когда длина, в битах или байтах, целых чисел существенна для программы, используйте типы данных с определенной длиной (С99 типы).
Следствие: Ключевые слова short и long не должны никогда использоваться; ключевое слово char применяем только для определения строк.
Доказательство: ничего не стоит при исполнении, облегчает переносимость, *1.

#include < stdint.h >
uint16_t data;


Правило 7

Никогда не применяйте битовые операции ( &, |, ~, <<, >> ) к знаковым типам.
Следствие: Вам не придется исследовать неопределенное поведение этих операций.
Доказательство: ничего не стоит при исполнении, облегчает переносимость, *1.

Правило 8

Никогда не смешивайте знаковые с беззнаковыми типами в выражениях или сравнении.
Следствие: Цифровые константы беззнакового типа должны иметь постфикс u.
Доказательство: ничего не стоит при исполнении, вам не придется помнить правила привеления типов, *1.

int s = - 9;
unsigned int u = 6u;
if (s + u < 4) {
// эта ветка НЕ будет исполнена, хотя -9+6=-3 < 4, как мы могли бы ожидать
}
else {
// А вот эта ветка - будет
}


Правило 9

Вместо применения параметризованных макросов следует использовать inline функции.
Следствие: в сочетании с правилом 2 исключает применение #define чуть менее, чем полностью (остаются параметры компиляции).
Доказательство: макросы неотлаживаемы в принципе и это очень веский аргумент, отсутствует контроль типов при вызове макроса, возможны побочные эффекты при модификации параметра, но некоторые С компиляторы воспринимают директиву inline как пожелание, а не как руководство к действию — в результате время исполнения может возрасти. Тем не менее, настоятельно рекомендуется к применению.

#define SQUARE(A) ((A)*(A))
inline uint32_t square(uint16_t a)
{
  return (a * a);
}


Правило 10

Декларируем каждую переменную на одной строке.
Следствие: Запятая недопустима в списке переменных.
Доказательство: вам не придется помнить, насколько действует *, улучшается читаемость кода.
Тем не менее, на мой взгляд, самое неоднозначное правило, поскольку увеличивает видимый размер кода. Даже если декларации и собраны в начале модуля, (а есть еще и рекомендации объявлять локальные переменные непосредственно перед использованием), удлинение кода лично я к лучшим сторонам отнести не могу, поэтому продолжил бы использовать объявления вроде:

 int i,j,k;


Но, конечно, следующая строка неприемлема:

 int *pi, pj;


Подводя итог, можно сказать, что перед нами способ несколько укоротить имеющуюся у нас веревку, при этом не стреноживая нас до невозможности движения вообще. Конечно, до фанатизма в следовании рекомендациям доходить не следует, просто надо иметь в виду, что тут как в Уставе — каждая строчка написана кровью людей, которые делали по-своему. При этом ни одно из правил в противоречие со здравым смыслом не входит, скорее, они иногда конфронтируют с въевшимися привычками, совершенно не обязательно, что рациональными.
Tags:
Hubs:
+12
Comments 71
Comments Comments 71

Articles