10 правил программирования на Си предотвращающих ошибки

30/07/2010 - 16:34

ПРАВИЛО #1 – ФИГУРНЫЕ СКОБКИ

   Блок программы, идущий после ключевых слов  if, else, switch, while, do и for следует всегда окружать фигурными скобками ({}), даже если он содержит только одиночные или пустые операторы. 

// Не следует так делать…
if (timer.done)
   // Одиночному оператору нужны скобки!       
   timer.control = TIMER_RESTART;

// А вот так правильно ...
while (!timer.done)
{
   // Даже пустой оператор должен быть окружён скобками. 
}

ПРАВИЛО #2 – Ключевое слово «const»

Ключевое слово const следует использовать:

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

const unsigned char
dirPort = 0xff;

Аргументация: Переменные объявленные с ключевым словом const будут защищены компилятором от непреднамеренных  изменений.

ПРАВИЛО #3 – Ключевое слово "static"

  Ключевое слово static следует использовать для описания всех функций и переменных, не используемых за пределами модуля, в котором они описаны.

static void InnerFunction(void)
{
   ….
}

Аргументация: Ключевое слово Си – static, имеет несколько значений. На уровне модуля глобальные переменные и функции, объявленные с ключевым словом static защищены от случайного доступа из других модулей. Это уменьшает связанность и способствует инкапсуляции.

ПРАВИЛО #4 – Ключевое слово "volatile"

Ключевое слово volatile нужно использовать везде, где уместно, включая:

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

volatile unsigned int timer;

Аргументация: Правильное использование спецификатора volatile устраняет целый класс трудных для определения ошибок. Это ключевое слово сообщает компилятору, что переменная может быть изменена в любой момент без какого-либо действия со стороны ближайшего кода и компилятор не должен выполнять на ней некоторые оптимизации. {1}

ПРАВИЛО #5 – Комментарии

   Комментарии не должны быть вложенными и их не следует использовать для отключения блока программы, даже временно. Для временного отключения блока программы используйте директивы условной компиляции (например, #if 0 ... #endif).

// Так делать нельзя...
/*
 a = a + 1;
 /* comment */
 b = b + 1;
*/


// Так правильно...
#if 0
 a = a + 1;
 /* comment */
 b = b + 1;
#endif

Аргументация: Вложенные комментарии и закомментированный код могут позволить неожиданным отрывкам кода быть прокомпилированными.
(Первый пример, кстати, не будет компилироваться. Компилятор принимает за комментарий все, что находится между первыми символами /* и первыми встреченными символами */. Pashgan)

ПРАВИЛО #6 – Тип данных с фиксированной разрядностью

  Всякий раз, когда разрядность целочисленных типов, в битах или байтах, в программе существенна, следует использовать вместо char, short, int, long, или long long тип данных с фиксированной разрядностью. Знаковые и беззнаковые целочисленные типы с фиксированной разрядностью должны быть такими, как показано в таблице.

Стандарт для знаковых и беззнаковых целочисленных типов
с фиксированной разрядностью
Аргументация: Разрядность фундаментальных типов данных языка Си - char, short, int, long, и long long  зависит от реализации (от конкретного компилятора и аппаратной платформы), а это приводит к проблемам переносимости программ.  Стандарт 1999 года не решил эту проблему, но ввел одинаковые имена типов, показанных в таблице. Они заданы в новом заголовочном файле <stdint.h>. Эти имена надо использовать, даже если придется создать typedef`ы вручную.

ПРАВИЛО #7 – Поразрядные операторы       

   Ни один из поразрядных операторов (другими словами, &, |, ~, ^, <<, и >>) не должен использоваться для управления целочисленными данными со знаком.

// Так делать нельзя...
int8_t  signed_data = -4;
signed_data >>= 1;  // не обязательно -2

Аргументация: Си-стандарт не определяет основной формат данных со знаком и оставляет определение эффекта некоторых поразрядных операторов на усмотрение авторов компилятора.

ПРАВИЛО #8 – Целые числа со знаком и без

   Целые числа со знаком не должны сочетаться с целыми числами без знаков в сравнениях  или выражениях. Десятичные константы, не подразумевающие наличие знаков, должны быть описаны с 'u' на конце.

// Так делать нельзя...
uint8_t  a = 6u;
int8_t   b = -9;

if (a + b < 4)
{
   // если бы -9 + 6 было -3 < 4 как ожидалось.
   // была бы выполнена эта ветвь

}
else
{
   //но поскольку -9 + 6 это  (0x100 - 9) + 6 = 253
   //то будет выполнена эта ветвь

}

Аргументация: Некоторые детали управления целочисленными данными со знаком зависят от реализации (то есть от от конкретного компилятора). Кроме того, в результате смешивания знаковых и беззнаковых данных могут возникать информационно-зависимые ошибки.

ПРАВИЛО #9 – Параметризированные макросы против inline функций

Не следует использовать параметризированный макрос, если для выполнения той же задачи может быть написана inline функция. {2}

// Так делать нельзя...
#define MAX(A, B)   ((A) > (B) ? (A) : (B))
// ... если вместо этого вы можете сделать так.
inline int Max(int a, int b)

Аргументация: С использованием директивы препроцессора #define ассоциируется множество рисков, и многие из них связаны с созданием параметризированного макроса. Систематическое применение круглых скобок (как показано в примере) важно, но это не устраняет вероятность двойного инкремента - MAX(i++, j++). Другие опасности неправильного использования макроса включают сравнение данных со знаком и без, или любой тест данных с плавающей запятой.

ПРАВИЛО #10 – Оператор-запятая

Оператор запятая (,) не должен использоваться внутри описания переменной.

// Так делать нельзя…
char * x, y; // вы хотите, чтобы «y» был указателем, или нет?

Аргументация: Цена размещения каждого объявления  в отдельной строке низкая, а риск, что компилятор неправильно «поймет» ваши намерения высокий.

СНОСКИ

1.  Исходя из опыта консультирования множества компаний, я подозреваю, что подавляющее большинство встроенных систем содержит ошибки из-за нехватки ключевых слов volatile. Такие виды ошибок обычно обнаруживают себя как «глюки».
2.  Ключевое слово Си++ inline было добавлено в Си-стандарт в 1999-м году.

Michael Barr  "Bug-Killing Coding Standard Rules for Embedded C". Вольный перевод ChipEnable.Ru

Comments   

# Guest 2010-07-31 15:09
*ушел переписывать все параметризирова нные макросы в инлайн функции*

давно взял себе за правило всегда объявлять все переменные с фиксированной разрядностью (uint8_t, etc.)
# Guest 2010-07-31 17:08
Есть замечательная книга.
"Веревка достаточной длины, чтобы выстрелить себе в ногу" Ален И. Голуб
Там есть все вышеперечисленн ое и даже больше. Обращайтесь к классике.
# Guest 2010-07-31 21:59
C возвращением Pashgan.
А то мы уже начали волноваться :)
# Guest 2010-07-31 22:02
Quoting tol:
Есть замечательная книга.
"Веревка достаточной длины, чтобы выстрелить себе в ногу" Ален И. Голуб
Там есть все вышеперечисленное и даже больше. Обращайтесь к классике.


Кто знает что есть такая книга :-) хорошо что есть такой сайт! Где можно узнать про книги и не только.
# Guest 2010-07-31 22:07
Да и в правду че то пропал Павел в последнее время ;-)
# Pashgan 2010-08-01 22:44
Quote:
C возвращением Pashgan. А то мы уже начали волноваться :)
Да я и сам уже начал волноваться :) А вообще приятно, что кто-то о тебе вспоминает.
# САБ 2010-08-03 18:26
Не совсем корректный перевод. Скорее не "// Так делать нельзя..." а "// Так делать не рекомендуется.. .". Ибо Стандарт говорит, что все кроме примера из №5 синтаксически правильно.
С правилом №5 не согласен - комментарий, что /* */, что // написать и удалить в процессе отлаки в разы быстрее и удобнее, чем #ifdef. При помощи #ifdef имеет смысл комментрировать (заодно и выделяя таким образом) наброски нереализованных кусков в процессе написания, но не отключаемый в процессе отладки код. Аналогично не согласен со многими тезисами у Голуба.

Для правила №6 есть дополнение - существуют еще типы uint_leastXX_t и uint_fastXX_t, про которые тоже неплохо бы написать. Например, счетчик цикла от 0 до 10 имеет смысл объявлять не int, а uint_fast8_t, который будет 8-битным беззнаковым для AVR и 32-битным беззнаковым для ARM.
# Pashgan 2010-08-03 20:13
Вложенные комментарии не использую, но с помощью комментирования часто отключаю куски кода. А #if удобно использовать для отладочных фрагментов программы (#if DEBUG ... #endif).
# САБ 2010-08-07 21:41
Нет, ну про условную компиляцию никто не спорит. Речь шла об использовании #if 0 для безусловного комментирования участков кода. Причем в примере заявлено /* */ - так делать нельзя. А ведь можно, и более того - часто гораздо удобнее, чем предложенный "правильный" вариант.
# unalex 2011-12-06 06:24
насчет комментариев, напугали, я часто использую такую конструкцию
если
Code:
/*
чтоб раскомментировать, добавь косую черту вначале кода
/*/

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