Правила MISRA C на практиці: що це таке, навіщо вони існують і як з ними жити
Скажіть «MISRA» у кімнаті, повній embedded-розробників, і половина зітхне, а друга половина запитає, яким інструментом ви користуєтеся. Репутація цих правил неоднозначна: для одних це бюрократія, що з'їдає тиждень перед кожним релізом, для інших — причина, чому автомобіль не перезавантажується посеред траси. Обидві групи частково мають рацію.
У цій статті я пройдуся по правилах, які трапляються мені найчастіше, одне за одним: чого вимагає правило, що ви за це отримуєте, як виглядає поганий код, як виглядає добрий і хто знаходить проблему за вас. Усі приклади — це справжній код на C, який я скомпілював і запустив.
Кілька слів про тексти правил
MISRA C — документ, захищений авторським правом консорціуму MISRA. Я не копіюю його формулювань. Кожне правило описано моїми словами, з моїми прикладами, а цитуються лише номери та категорії. Номери стосуються MISRA C:2012 (MISRA C:2023 об'єднує поправки, зберігає ці номери й додає кілька нових настанов). Перш ніж будувати щось на основі цієї статті, перевірте правило у власній копії стандарту, яку можна отримати на misra.org.uk. Ця стаття незалежна і не схвалена MISRA.
Що таке MISRA C (і чим вона не є)
MISRA розшифровується як Motor Industry Software Reliability Association. Перші настанови для C було опубліковано у 1998 році для автомобільної галузі, сьогодні їх використовують в авіакосмічній галузі, на залізниці, у медичних пристроях та промисловій автоматизації — по суті, скрізь, де збій програмного забезпечення комусь шкодить.
Центральна ідея проста. C — мова, яка дозволяє багато речей, що є легальними, але небезпечними: undefined behaviour, implementation defined behaviour, неявні перетворення, що змінюють значення, вказівники, які можуть указувати будь-куди. MISRA визначає підмножину C, у якій небезпечні частини заборонені або мають бути обґрунтовані. Це не інша мова, і програма, сумісна з MISRA, залишається звичайною програмою на C, яку збирає ваш звичайний компілятор.
Чим MISRA не є:
- Це не сертифікація. «Сумісність з MISRA» — це твердження, яке ви робите і документуєте, ніхто не ставить печатку на ваш код.
- Це не гарантія коректності. Правила усувають класи помилок, але не перевіряють, що термостат справді регулює температуру.
- Це не про стиль. Іменування, відступи та організація файлів не входять до MISRA, для цього існує coding style.
Як читати правило
Кожна настанова має три властивості, які варто знати, перш ніж читати першу з них:
| Властивість | Значення | Що означає |
|---|---|---|
| Вид | Directive / Rule | Правило (rule) можна перевірити, дивлячись лише на вихідний код. Директива (directive) потребує додаткової інформації (дизайну, процесу), тож інструмент може допомогти, але не може вирішити. |
| Категорія | Mandatory / Required / Advisory | Mandatory: жодних відхилень. Required: відхилення можливе за наявності письмового обґрунтування. Advisory: рекомендація, потрібно лише зафіксувати, що ви її не дотримуєтеся. |
| Вирішуваність | Decidable / Undecidable | Чи може інструмент дати точну відповідь. Для невирішуваних правил навіть найкращий аналізатор видає хибні спрацьовування або щось пропускає. |
Нумерація має вигляд Dir 4.12 для директив і Rule 9.1 для правил, де перше число — це тема (9 = ініціалізація, 10 = типи, 11 = вказівники, ...).
Правила одне за одним
Я згрупував їх за проблемою, яку вони вирішують, а не за номером.
Правила про значення, які не такі, як ви думаєте
Rule 9.1 - читання перед записом (Mandatory)
Правило: не читайте змінну з автоматичним часом зберігання, доки їй не було присвоєно значення.
Що ви отримуєте: значення такої змінної — сміття, а на іншому компіляторі чи рівні оптимізації це інше сміття. Помилки на кшталт «у debug працює, у release ламається» дуже часто закінчуються саме тут. Оскільки правило mandatory, жодне відхилення неможливе.
Погано, змінна встановлюється лише на одному шляху:
uint8_t Gain_Bad(bool isHigh)
{
uint8_t gain;
if (isHigh) { gain = 8u; }
return gain; /* indeterminate value when isHigh is false */
}
Добре, кожен шлях має визначене значення:
uint8_t Gain_Good(bool isHigh)
{
uint8_t gain = 1u;
if (isHigh) { gain = 8u; }
return gain;
}
Хто це знаходить: компілятор — іноді (-Wall -Wmaybe-uninitialized з оптимізацією), статичний аналізатор — надійно. У моїй тестовій збірці gcc промовчав на цій функції. Не покладайтеся на нього.
Rules 10.1, 10.3, 10.4 - essential types (Required)
Правила: автори MISRA вигадали модель essential type: тип, який вираз виглядає так, ніби має (uint8_t лишається в їхньому розумінні «беззнаковим 8-бітним»), на противагу типу, який C мовчки використовує (int, через integer promotion). Правила забороняють операції між невідповідними типами, присвоєння значення вужчому типу та змішування категорій (знакові, беззнакові, булеві, символьні, з рухомою комою).
Що ви отримуєте: integer promotion — найбільше джерело сюрпризів в embedded C. Усі значення, менші за int, перед будь-якою арифметикою розширюються до int, і ви цього не бачите. Два класичні результати:
bool Flags_Bad(uint8_t flags)
{
return (~flags == 0xF0u); /* ~flags is an int: 0xFFFFFFF0 - never equal */
}
~flags — це не 8-бітне значення, а int зі значенням 0xFFFFFFF0. Порівняння ніколи не спрацьовує, хоч би який був аргумент. Виправлення — сказати, що саме ви маєте на увазі:
bool Flags_Good(uint8_t flags)
{
return ((uint8_t)~flags == 0xF0u);
}
Другий класичний випадок — мовчазне звуження:
uint8_t Add_Bad(uint8_t first, uint8_t second)
{
uint8_t sum = first + second; /* int result silently narrowed */
return sum;
}
200 + 100 обчислюється як int (300), а присвоєння в uint8_t залишає лише 44. Ні попередження, ні помилки, а сума неправильна. Два коректні варіанти: розширити результат або виконати свідоме приведення типу й подумати про переповнення.
uint16_t Add_Good(uint8_t first, uint8_t second)
{
uint16_t sum = (uint16_t)first + (uint16_t)second;
return sum;
}
Хто це знаходить: хороший статичний аналізатор. Компілятор допомагає лише частково: -Wsign-compare (частина -Wextra) повідомив про перший приклад, а на другий gcc промовчав.
Directive 4.6 - типи з розміром і знаковістю (Advisory)
Директива: використовуйте typedef, що вказують розмір і знаковість (uint8_t, int32_t або ваші власні назви), замість char, short, int і long.
Що ви отримуєте: ширина int залежить від компілятора і MCU: 16 біт на малих 8- і 16-бітних контролерах, 32 біти на Cortex-M. Код, що працює на одній цільовій платформі, мовчки змінюється на іншій. З типами фіксованої ширини в коді видно, що він робить. У своїх проєктах я йду на крок далі й створюю власні назви типів для фізичних величин (temperature_t, humidity_t), щоб компілятор зупиняв мене, коли я їх змішую.
int counter; /* 16 or 32 bits? the answer depends on the target */
uint16_t counter; /* 16 bits everywhere */
Rule 1.3 - жодної undefined behaviour (Required)
Правило: програма не повинна містити undefined behaviour. Сюди входять переповнення знакових чисел, зсуви на надто велику кількість біт, ділення на нуль, використання «висячого» вказівника та багато іншого.
Що ви отримуєте: за undefined behaviour компілятор може зробити що завгодно, зокрема прибрати вашу захисну перевірку, бо «цього випадку не може бути». Найвідоміший приклад — переповнення знакових чисел: first + second для int32_t, що не вміщується, — це не «перехід через нуль», а ліцензія для оптимізатора.
int32_t Add_Bad2(int32_t first, int32_t second)
{
return first + second; /* signed overflow is undefined behaviour */
}
Коректний код перевіряє умову перед операцією і повідомляє про проблему викликачеві:
bool Add_Safe(int32_t first, int32_t second, int32_t *result)
{
bool isOk = true;
if (((second > 0) && (first > (INT32_MAX - second))) ||
((second < 0) && (first < (INT32_MIN - second))))
{
isOk = false;
}
else
{
*result = first + second;
}
return isOk;
}
Те саме стосується зсувів. Маска позиції біта визначена лише тоді, коли позиція менша за ширину типу:
uint32_t Bit_Mask(uint8_t position)
{
return (position < 32u) ? (1u << position) : 0u;
}
Хто це знаходить: частково компілятор (-fsanitize=undefined знаходить це під час виконання в тестах), частково аналізатор. Загалом правило невирішуване: інструмент може показати всі місця, які могли б бути проблемою.
Правила про умови та потік керування
Rule 14.4 - керівний вираз є булевим (Required)
Правило: умова в if, while і for має бути виразом з essential Boolean типом, тобто результатом порівняння або bool. Одне ціле число чи вказівник не дозволені.
Що ви отримуєте: намір читається. while (count) не говорить, чи ви хочете «поки не нуль», чи «поки не кінець». Явна форма виглядає однаково для чисел, вказівників і прапорців. (Константу я пишу ліворуч, 0u != count, щоб помилка з одним = не компілювалася.)
uint8_t Count_Bad(uint8_t count)
{
uint8_t steps = 0u;
while (count) { count--; steps++; }
return steps;
}
uint8_t Count_Good(uint8_t count)
{
uint8_t steps = 0u;
while (0u != count) { count--; steps++; }
return steps;
}
Хто це знаходить: кожен аналізатор MISRA, це вирішуване правило.
Rule 12.1 - робіть пріоритет операторів явним (Advisory)
Правило: не розраховуйте, що читач (і ви самі) пам'ятатимете таблицю пріоритетів, — використовуйте дужки.
Що ви отримуєте: пріоритет & та == у C протилежний тому, чого очікує людський мозок. Це помилка, яку кожен робить принаймні раз:
bool Masked_Bad(uint8_t flags)
{
return (flags & MASK == MASK); /* == binds stronger than & */
}
Спочатку обчислюється MASK == MASK і дає 1, тож функція перевіряє біт 0, а не маску. Вона повертає true для 0x01 і false для 0x0C — рівно навпаки. З дужками ця помилка не може існувати:
bool Masked_Good(uint8_t flags)
{
return ((flags & MASK) == MASK);
}
Хто це знаходить: компілятор (-Wparentheses входить до -Wall) та кожен аналізатор.
Rules 15.6, 15.7, 16.3 і 16.4 - завершені інструкції (Required)
Правила:
- 15.6: тіло
if,else,for,while— це завжди блок у фігурних дужках, - 15.7: кожен ланцюжок
if ... else ifзакінчуєтьсяelse, - 16.3: кожна гілка
switchзакінчуєтьсяbreak(або іншим безумовним переходом), - 16.4: кожен
switchмаєdefault.
Що ви отримуєте: правила змушують відповісти на запитання «що відбувається в усіх інших випадках?» у момент написання коду, а не в польових умовах. switch без break — класична помилка, enum, який отримує нове значення без default, — друга. Фігурні дужки усувають сім'ю помилок, коли під if додають другу інструкцію, і вона виглядає так, ніби теж умовна:
if (isError)
Led_On();
Buzzer_On(); /* indented like it belongs to the if - it does not */
А ось switch. Перша версія «провалюється» з MODE_ON у MODE_ERROR і встановлює неправильне значення, друга обробляє кожен випадок явно:
void Mode_Bad(mode_t_ mode)
{
switch (mode)
{
case MODE_ON:
gLeds = 1u;
case MODE_ERROR: /* falls through by accident */
gLeds = 3u;
break;
}
}
void Mode_Good(mode_t_ mode)
{
switch (mode)
{
case MODE_ON:
gLeds = 1u;
break;
case MODE_ERROR:
gLeds = 3u;
break;
case MODE_OFF:
default:
gLeds = 0u;
break;
}
}
У моєму тесті gcc знайшов відсутній default і fall-through (-Wswitch-default -Wimplicit-fallthrough). Правило про else і фігурні дужки перевіряє аналізатор або форматер.
Rule 17.2 - жодної рекурсії (Required)
Правило: функція не повинна викликати саму себе — ні безпосередньо, ні через інші функції.
Що ви отримуєте: на ПК стек має мегабайти. На мікроконтролері у вас кілька кілобайтів, а найгірший випадок глибини рекурсії залежить від вхідних даних. Якщо прибрати рекурсію, максимальне використання стека можна обчислити (достатньо статичного аналізу дерева викликів), і переповнення стека перестає бути сюрпризом.
uint32_t Factorial_Recursive(uint32_t value)
{
return (value <= 1u) ? 1u : value * Factorial_Recursive(value - 1u);
}
Ітеративна версія потребує сталого обсягу стека:
uint32_t Factorial_Iterative(uint32_t value)
{
uint32_t result = 1u;
for (uint32_t factor = 2u; factor <= value; factor++) { result *= factor; }
return result;
}
Хто це знаходить: аналізатор і linker map (деякі toolchain друкують граф викликів, -fstack-usage показує розмір кожного фрейма).
Правила про пам'ять і вказівники
Directive 4.12 і Rule 21.3 - жодної динамічної пам'яті (Required)
Правило: не використовуйте malloc, calloc, realloc і free.
Що ви отримуєте:
- купа (heap) фрагментується. Після тижнів роботи запит на 64 байти може завершитися невдачею, хоча вільно 10 кБ, лише не суцільним шматком,
- час виконання
mallocне детермінований, - витік пам'яті після тижнів роботи — найгірший вид помилки для пошуку,
- споживання пам'яті відоме під час лінкування, а не о 3-й ночі в полі.
Стандартна альтернатива — пул фіксованого розміру, який має ту саму форму API, що й алокатор:
#define FRAME_POOL_SIZE 4u
typedef struct
{
uint8_t data[8];
bool inUse;
} frame_t;
static frame_t framePool[FRAME_POOL_SIZE];
frame_t *Frame_Acquire(void)
{
for (uint8_t index = 0u; index < FRAME_POOL_SIZE; index++)
{
if (!framePool[index].inUse) { framePool[index].inUse = true; return &framePool[index]; }
}
return NULL; /* the caller has to handle "no frame" - at test time, not at 3 a.m. */
}
Функція повертає NULL, якщо всі фрейми зайняті, тож випадок «немає пам'яті» обробляється в коді й тестується на ПК. Точно те саме відбувається з malloc, лише ви не можете цього протестувати, бо на вашому ПК виділення пам'яті ніколи не завершується невдачею.
Хто це знаходить: аналізатор або просто лінкер, який взагалі не підключає malloc.
Rules 11.3 і 11.5 - обережно з приведенням вказівників (Required / Advisory)
Правила: не приводьте вказівник на об'єкт одного типу до вказівника на об'єкт іншого типу (11.3) і уникайте перетворення з void * на вказівник на об'єкт (11.5).
Що ви отримуєте: приведення байтового буфера до uint32_t * — найпоширеніший спосіб прочитати поле кадру зв'язку:
uint32_t ReadU32_Bad(const uint8_t *buffer)
{
return *(const uint32_t *)buffer; /* misaligned access, strict aliasing violation */
}
Це undefined behaviour двічі. Адреса не зобов'язана бути вирівняною на чотири байти (hard fault на Cortex-M0, повільний доступ деінде), а доступ через вказівник іншого типу порушує правила aliasing, тож оптимізатор може переставити або викинути його. Два коректні рішення: memcpy (компілятор перетворює його на одне завантаження там, де це дозволяє апаратура) або складання з байтів, що заодно фіксує порядок байтів і не залежить від endianness процесора:
uint32_t ReadU32_Good(const uint8_t *buffer)
{
uint32_t value;
(void)memcpy(&value, buffer, sizeof value);
return value;
}
uint32_t ReadU32LittleEndian(const uint8_t *buffer)
{
return (uint32_t)buffer[0] | ((uint32_t)buffer[1] << 8) | ((uint32_t)buffer[2] << 16) | ((uint32_t)buffer[3] << 24);
}
Приведення void * типове для загального контексту callback. Воно advisory, бо в C це іноді єдиний спосіб. Користь правила в тому, що ви помітите кожне таке місце і зробите приведення в одному рядку на початку функції (як у статті про SOLID), а не скрізь.
Хто це знаходить: кожен аналізатор. Компілятор знаходить таке приведення лише на деяких цільових платформах і з -Wcast-align.
Rule 8.13 - вказівник на const, коли це можливо (Advisory)
Правило: якщо функція не змінює дані за вказівником, вказівник оголошується як вказівник на const.
Що ви отримуєте: сигнатура — це контракт. Викликач бачить із прототипу, що функція лише читає, а компілятор це контролює. const буфер із flash можна передати лише у функцію з const, інакше код не скомпілюється (або, гірше, запише у flash і спричинить fault).
uint8_t Checksum_Bad(uint8_t *data, size_t length)
{
uint8_t sum = 0u;
for (size_t index = 0u; index < length; index++) { sum = (uint8_t)(sum + data[index]); }
return sum;
}
uint8_t Checksum_Good(const uint8_t *data, size_t length)
{
uint8_t sum = 0u;
for (size_t index = 0u; index < length; index++) { sum = (uint8_t)(sum + data[index]); }
return sum;
}
Правила про код, який ви написали, і код, якого немає
Rule 17.7 і Directive 4.7 - не ігноруйте результати (Required)
Правила: повернене значення має бути використане (17.7), а коли функція повертає код помилки, його треба перевірити (4.7).
Що ви отримуєте: Проігноровані повернені значення — це проігноровані помилки. Надсилання кадру на зайнятий UART, відмова запису у flash, тайм-аут I2C: усе це проходить мовчки, і застосунок продовжує працювати з хибним припущенням. Якщо вам справді байдуже, скажіть це за допомогою (void), щоб читач (і аналізатор) знав, що це було рішення, а не помилка:
bool Send_Checked(const uint8_t *data, size_t length)
{
if (UART_OK != Uart_Send(data, length)) { return false; }
return true;
}
(void)Uart_Send(message, sizeof message); /* a log message, loss is acceptable */
Хто це знаходить: аналізатор, у gcc — з __attribute__((warn_unused_result)) на вашому власному API.
Rules 2.1 і 2.2 - жодного недосяжного і мертвого коду (Required)
Правила: проєкт не повинен містити код, який ніколи не може виконатися (2.1), і код, який виконується, але не впливає на результат (2.2).
Що ви отримуєте: обидва види — сигнал помилки: умова, що ніколи не настає, забутий return або помилка копіювання й вставлення. Мертвий код також спотворює покриття тестами (не можна досягти 100 % рядків, які не виконує жоден вхід) і змушує читача про нього думати. Обидва види є в цій функції:
uint8_t Level_Get(uint8_t raw)
{
uint8_t level = 3u; /* dead: overwritten before it is read */
level = (uint8_t)(raw / 2u);
return level;
Level_Log(raw); /* unreachable */
}
Перше присвоєння level мертве, а останній рядок недосяжний.
Хто це знаходить: аналізатор. gcc з -Wall -Wextra у моєму тесті промовчав.
Rule 20.7 і Directive 4.9 - макроси з параметрами (Required / Advisory)
Правило: розгорнутий параметр макросу має бути в дужках (20.7), а функції віддається перевага перед функціоподібним макросом (4.9).
Що ви отримуєте: макрос — це текстова заміна. Якщо ви не захистите параметр, пріоритет визначає викликач:
#define SQUARE_BAD(x) x * x
#define SQUARE_OK(x) ((x) * (x))
SQUARE_BAD(2 + 1) /* 2 + 1 * 2 + 1 = 5, not 9 */
SQUARE_OK(2 + 1) /* ((2 + 1) * (2 + 1)) = 9 */
Навіть захищений макрос має проблему: SQUARE_OK(count++) інкрементує двічі. Тому директива 4.9 рекомендує функцію. Inline-функція має тип, обчислює аргумент один раз і коштує стільки ж:
static inline uint32_t Square(uint32_t value) { return value * value; }
Використання MISRA у реальному проєкті
Попередження компілятора — перший крок, а не останній
Я скомпілював усі наведені вище приклади з gcc -Wall -Wextra -Wconversion -Wshadow -Wswitch-default -Wimplicit-fallthrough -O2. gcc видав п'ять попереджень, і вони стосуються лише трьох «поганих» прикладів: порівняння з розширеним доповненням, switch (відсутній default, необроблене значення enum і fall-through) та відсутні дужки навколо == у перевірці маски. Він промовчав про звуження, неініціалізовану змінну, мертвий і недосяжний код, переповнення знакових чисел, рекурсію, приведення вказівника та макрос.
Моя рекомендація — компілятор з максимумом попереджень і -Werror у CI як базовий рівень. Це безкоштовно і усуває найочевидніші проблеми, але не замінює статичний аналіз.
Оберіть інструмент і додайте його в CI
Для перевірки правил потрібен статичний аналізатор. Є комерційні (Polyspace, Helix QAC, PC-lint Plus, Coverity, Parasoft C/C++test, LDRA, IAR C-STAT та інші) і безкоштовні, що покривають частину правил, наприклад cppcheck з його addon для MISRA. Addon потребує на вході текст правил, і через авторське право його треба надати зі своєї копії стандарту. Інструмент ніколи не буває ідеальним: для невирішуваних правил будуть хибні спрацьовування, а іноді порушення буде пропущене.
Перевірка належить до CI кожного коміту, а не до ручного запуску перед релізом. Інакше ви робите ту саму роботу тричі: раз, коли пишете, раз, коли знаходять порушення, і раз, коли їх виправляєте.
Відхилення — частина системи
Іноді правило доводиться порушувати. Доступ до апаратного регістра потребує приведення цілого числа до вказівника, bootloader має записувати у flash через вказівник. MISRA це знає і визначає відхилення (deviation): запис із правилом, місцем, причиною та оцінкою ризику. Запис має лежати поруч із кодом:
/* MISRA deviation: Rule 11.4 (Advisory), see DEV-012
* Reason: the address of the peripheral register is fixed by the hardware.
* Contained in this macro, the rest of the code uses the register type. */
#define UART_REGS ((uartRegisters_t *)0x40004400u)
Відхилення — це не поразка. Перелік добре обґрунтованих відхилень — ознака проєкту, в якому люди думають. Проєкт без жодного відхилення і без доступу до апаратури зазвичай або бреше, або дуже малий.
Legacy-код і код вендора
Ви не зробите 200 000 рядків старого проєкту сумісними за тиждень. Що працює:
- Не чіпайте старий код, увімкніть перевірку для нових і змінених файлів. Baseline аналізатора зберігає наявні порушення, і CI падає лише на нових.
- Починайте з правил mandatory і required, advisory — пізніше.
- Виключіть сторонній код і задокументуйте це. Згенерований код вендора (HAL, LL, CMSIS) виправляти не вам. Це ще одна причина для шарової архітектури зі статті про дизайн та архітектуру: коли код вендора лежить у RAL, а MCAL і все вище — ваш код, межа перевірки — це тека.
Що не допомагає
- Гонитва за кількістю порушень. Правило, виправлене так, що воно задовольняє інструмент, але ламає зміст (приведення типу, додане лише щоб заглушити попередження), гірше за оригінал.
- MISRA як аргумент. «Так каже MISRA» — це не причина. Якщо ви не можете пояснити, від чого правило вас захищає, ви не зможете вирішити, коли настав час для відхилення.
- Віра, що сумісність означає коректність. MISRA робить деякі помилки неможливими. Вимоги, архітектура, рев'ю та тести залишаються на вас.
Підсумок
| Правило | Коротко | Користь |
|---|---|---|
| 9.1 | Ініціалізуйте перед читанням | Жодних випадкових значень, однакова поведінка в debug і release |
| 10.x | Дотримуйтесь essential types | Жодних сюрпризів від integer promotion і звуження |
| Dir 4.6 | Типи фіксованої ширини | Той самий код означає те саме на кожній цільовій платформі |
| 1.3 | Жодної undefined behavior | Оптимізатор не може прибрати ваші перевірки |
| 14.4 | Явні умови | Намір читається |
| 12.1 | Дужки | Жодних помилок із пріоритетом |
| 15.6, 15.7, 16.3, 16.4 | Фігурні дужки, else, break, default | Обробляється кожен випадок |
| 17.2 | Жодної рекурсії | Використання стека можна обчислити |
| Dir 4.12, 21.3 | Жодної динамічної пам'яті | Жодної фрагментації, витоків і сюрпризів у таймінгах |
| 11.3, 11.5 | Жодних трюків з типами вказівників | Жодних помилок вирівнювання й aliasing |
| 8.13 | Вказівники const | Сигнатура — це контракт |
| 17.7, Dir 4.7 | Використовуйте повернені значення | Помилки не зникають |
| 2.1, 2.2 | Жодного недосяжного чи мертвого коду | Помилки видно, покриття має сенс |
| 20.7, Dir 4.9 | Безпечні макроси, надавайте перевагу функціям | Жодного прихованого подвійного обчислення чи помилок пріоритету |
Якщо ви запам'ятаєте лише одне: правило MISRA — це шрам від помилки, яку хтось уже мав. Вам не обов'язково любити правила, але добре знати, шрамом від чого є кожне з них. Тоді ви знатимете, коли його дотримуватися, а коли написати відхилення і взяти відповідальність на себе.
Описані тут правила — лише добірка. Повний набір містить понад 140 настанов, а решту (препроцесор, стандартну бібліотеку, структуру оголошень) варто один раз прочитати у вашій копії стандарту.