44 атрибута хорошего С-кода

285b883bf6be6b43ee4967c3921e72bd.jpeg

Этот текст адресован когорте программистов на С (ях). Это не академические атрибуты из учебников это скорее правила буравчика оформления сорцов из реального prod (а). Некоторые приемы совпали с MISRA, некоторые с CERT-C. А кое-что является результатом множества итераций инспекций программ и перестроек после реальных инцидентов. В общем тут представлен обогащенный концентрат полезных практик программирования на С (ях).

1–Все функции должны быть менее 45 строк. Так каждая функция сможет уместиться на одном экране. Это позволит легко анализировать алгоритм и управлять модульность.

2–Не допускать всяческих  магических чисел в коде. Это уничтожает читаемость кода. Все константы надо определять в перечисления заглавными буквами.

3–На все сборки должна быть одна общая кодовая база (общак, репа). Модификация в одном компоненте должна отражаться на всех сборках организации, использующих компонент (например алгоритмы CRC). Это позволит сэкономить время на создание новых проектов для новых программ.

4–Все .с файлы должны быть оснащены одноименным .h файлом. Так эффективнее переносить, анализировать и мигрировать проекты на очередные аппаратные платформы. И сразу понятно, где следует искать прототипы функций из *.c фалов.

5–Аппаратно зависимый  код должен быть отделен от аппаратно независимого кода по разным файлам и разным папкам. Так можно тестировать на другой архитектуре платформа независимые функции и алгоритмы. Всякую математику и работу со строчками.

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

7–Не вставлять функции внутрь if () . Коды возврата приходится анализировать отладчиком до проверки условия.

это очень плохо:

if (MmGet (ID_IPv4_ROLE, tmp, 1, &tmp_len) != MM_RET_CODE_OK) {
    return ERROR_CODE_HARDWARE_FAULT;
}

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

8–Использовать static функции везде, где только можно. Это повысит модульность.

9–Используй препроцессорный #error для предупреждения о нарушении зависимостей между компонентами.

#ifndef ADC_DRV_H
#define ADC_DRV_H

#ifdef __cplusplus
extern "C" {
#endif

#include 
#include 

#include "adc_bsp.h"
#include "adc_types.h"

#ifndef HAS_MCU
#error "+ HAS_MCU"
#endif

#ifndef HAS_ADC
#error "+ HAS_ADC"
#endif

bool adc_init_channel(uint8_t adc_num, AdcChannel_t adc_channel);
bool adc_init(void);
bool adc_proc(void);
bool adc_channel_read(uint8_t adc_num, uint16_t adc_channel, uint32_t* code);

#ifdef __cplusplus
}
#endif

#endif /* ADC_DRV_H  */

10--Если что то можно проверить на этапе make файлов, то это надо проверить на этапе make файлов. Каждый компонент должен проверять, что подключены нужные зависимости. Это можно сделать через условные операторы make файлов.

$(info I2S_MK_INC=$(I2S_MK_INC))
ifneq ($(I2S_MK_INC),Y)
    I2S_MK_INC=Y

    mkfile_path := $(abspath $(lastword $(MAKEFILE_LIST)))
    $(info Build  $(mkfile_path) )

    I2S_DIR = $(WORKSPACE_LOC)bsp/bsp_stm32f4/i2s
    #@echo $(error I2S_DIR=$(I2S_DIR))

    INCDIR += -I$(I2S_DIR)
    OPT += -DHAS_I2S

    SOURCES_C += $(I2S_DIR)/i2s_drv.c

    ifeq ($(DIAG),Y)
        SOURCES_C += $(I2S_DIR)/i2s_diag.c
    endif

    ifeq ($(CLI),Y)
        ifeq ($(I2S_COMMANDS),Y)
            OPT += -DHAS_I2S_COMMANDS
            SOURCES_C += $(I2S_DIR)/i2s_commands.c
        endif
    endif
endif

11--Если что то можно проверить на этапе препроцессора, то это надо проверить на этапе препроцессора. Каждый компонент должен проверять, что подключены нужные зависимости. Это можно сделать через макросы компонентов.

12–Если что-то можно проверить на этапе компиляции, то это надо проверить на этапе компиляции (assert (ы)). Например можно проверить, что в конфигурациях скорость UART не равна нулю. В RunTime не должно быть проверок, которые можно произвести на этапе компиляции, препроцессора или make файлов.

13–Каждой set функции должна быть поставлена в соответствие get функция. Это позволит написать модульный тест для данного параметра.

14–Если переменная это физическая величина, то в суффиксе указывать размерность (timeout_ms). Это увеличивает понятность кода.

15–Все Си-функции должны всегда возвращать код ошибки. Минимум тип bool или числовой код ошибки. Так можно понять, где именно что-то пошло не так. Проще говоря, не должно быть функций, которые возвращают void. Функции void это, по факту, бомбы с часовым механизмом. В один день они отработают ошибочно, а вы об этом ничего даже не узнаете.

16–Для каждого программного компонента создавать несколько *.с *.h файлов:  

Файл

h

c

файл констант компонента

*

файл типов данных для данного компонента

*

файл команд CLI

*

*

файлы конфигурации компонента

*

*

файлы диагностики

*

*

файлы самого драйвера

*

*

Это позволит ориентироваться в коде и управлять модульностью.

17–Если функция получает указатель, то пусть сразу проверяет на нуль значение указателя. Так прошивки не будут падать при получение нулевых указателей. Это повысит надежность кода. Вы же не знаете как и кто этот код будет испытывать. Хорошая функция всегда проверяет то, что ей дают.

18–Если есть конечный автомат, то добавить счетчик циклов. Так можно будет проверить, что автомат вообще вертится.

19–И идеале все переменные должны иметь разные имена. Так было бы очень удобно делать поиск по grep. Но тут надо искать компромисс с наглядностью.

20–У каждой функции должен быть только 1 return. Это позволит дописать какой-то функционал в конце, зная, что он точно вызовется.

21–Не использовать операторы >, >= Вместо них использовать <, <= просто поменяв местами аргументы там, где это нужно. Это позволит интуитивно проще анализировать логику по коду. Человеку еще со времен школьной математики понятнее когда, то что слева то меньше, а то, что справа то больше. Так как ось X стрелкой показывала вправо. Особенно удобно при проверке переменной на принадлежность интервалу. Получается, что > и >= это вообще два бессмысленных оператора в языке С.

22–В проекте обязательно должны быть модульные тесты. Тесты это просто функции, которые вызывают другие функции в run-time. Это позволит сделать безболезненную перестройку кода, когда архитектура начнет скрипеть. Тесты можно вызывать как до запуска приложения так и по команде из UART- CLI.

23–Избегайте бесконечных циклов while (1) при блокирующем ожидании чего-либо. Например ожидание прерывания по окончании отправки в UART. Прерывания могут и не произойти из-за сбоя.  while (1) это просто капкан в программировании.  Всегда должен быть предусмотрен аварийный механизм выхода по TimeOut (у).

24–Использовать макро функции препроцессора для код генерации одинаковых функций или пишите код-генераторы, если препроцессор запрещен. Копипаста — причина программных ошибок №1.

25--Все высокоуровневые функции в конец .с файла. Это избавит от нужды указывать отдельно прототипы static функций.

26–Скрывать область видимости локальных переменных по максимуму.

27–Если код не используется, то этот код не должен собираться. Это уменьшит размер артефактов.

28–Если вы в С передаете что-то через указатель или возвращаете через указатель, то указываете направление движения данных приставками in, io или out.

Например:

void proc_some_data(unsigned char* inBuffer, unsigned char* outBuffer, int len, int *outLen)

Это позволит легче читать прототипы, не погружаясь в тело функции

29–Давайте переменным осмысленные имена, чтобы было удобно grep (ать) по кодовой базе

30--Если в коде есть список чего-либо (прототипы функций, макросы, перечисления), то эти строки должны быть отсортированы по алфавиту. Если сложно сортировать вручную, то можно прибегнуть к помощи утилиты sort. Это позволит сделать визуальный бинарный поиск и найти нужную строчку. Также при сравнении 2x отсортированных файлов отличия будут минимальные.

31–Внизу файла самые высокоуровневые функции. Вверху файла самые простые функции.

32–Функции CamelCase переменные snake_case

33–Все .h файлы снабжать защитой препроцессора от повторного включения. Это же касается *.mk файлов. 

34–Сборка из Makefile (ов) является предпочтительнее чем сборка из GUI-IDE. Makе позволяет по-полной управлять модульностью кодовой базы.

36–Соблюдать программную иерархичность. Низкоуровневый модуль не должен управлять (вызывать функции) более высокоуровневого модуля. UART не должен вызывать функции LOG. И компонент LOG не должен вызывать функции CLI. Управление должно быть направлено в сторону от более высокоуровневого компонента к более низкоуровневому компоненту. Например CLI→LOG→UART. Не наоборот.

37–Использовать автоматические форматировщики отступов исходного кода. Подойдет например бесплатная утилита clang-format. Это позволит делать более простые выражения при поиске по коду утилитой grep. И будет минимальный diff при сравнении истории файлов. Придерживаться какого-нибудь одного стиля форматирования. Пусть будет «единообразно безобразно».

38--При сравнении переменных с константой константу ставьте слева от оператора ==.

неправильно: if (val == 10) doSmth=1;

правильно: if (10 == val) doSmth=1;

Когда константа на первом месте, то компилятор выдаст ошибки присвоение к константе в случае опечатки

if (10=val ) doSmth=1;

Такая конструкция 

 if (val = 10) doSmth=1;

незаметно собирается и вызовет трагедию во время исполнения.

39–В каждом if всегда обрабатывать else вариант даже если else тривиальный. Это позволит предупредить многие осечки в программе.

40–Всегда инициализировать локальные переменные в стеке. Иначе там просто будут случайные значения, которые могут что-нибудь повредить.

41–Тесты и код разделять на разные компоненты. То есть код и тесты должны быть в разных папках. Включаться и отключаться одной строчкой в make-файле. 

42–В хорошем С-коде в принципе не должно быть комментариев. Лучший комментарий к коду — это имена функций и переменных. 

43–Собирать артефакты как минимум 2 мя компиляторами (CCS + IAR) или (GCC+GHS) или (Clang+GCC) и тп. Если один компилятор пропустил ошибку, то второй может ее найти.

44–Прогонять кодовую базу через статический анализатор. Хотя бы бесплатный CppCheck. Может найдется очередная загвоздка.

Аномалии оформления сорцов из реальной жизни (War Stories)

1–Магические циферки на каждой стороне

2–Переиспользование глобальных переменных

3--Доступ к регистрам в каждом файле проекта

4–Повторяемость кода

5--Очевидные комментарии

6–«заборы» из комментариев

7--.с файлы оснащены разноименными .h файлами.

8--Макросы маленькими буквами

9--Код без модульных тестов 

10–Код как в миксере перемешанный с тестами

11--Функции от 1000 до 5000 строк и даже более

12--Вставка препроцессором #include *.c файлов.

13--Вся прошивка в одном main.c файлике 75000 строк аж подвисает текстовый редактор.

14--С-функции с именами литературных персонажей

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

Если вы практикуете еще вспомогательные эффективные приемы написания программ на С (ях), то пишите их в комментариях

© Habrahabr.ru