Контекст
Правила, который перечислены здесь, не являются абсолютными. Они появились из типичного практического контекста, в котором мы пишем код. Хороший код должно быть:
-
Легко переиспользовать. Это очень экономит время на отладку программ, не нужно тестировать новые функции и меньше шанс сделать ошибку, добавляя новую логику.
Примеры нарушений:
- глобальные переменные;
- функции, которые делают слишком много разнообразной работы;
- функции, которые одновременно и кодируют логику, и используют ввод-вывод.
-
Легко модифицировать. Ошибки частые гости в программах, сложно модифицировать значит сложно исправлять ошибки.
Примеры нарушений:
- глобальные переменные;
- дублирование кода (модифицировать надо все дубликаты, как их найти?)
-
Легко читать. Код постоянно читают ваши коллеги (и вы сами).
Примеры нарушений:
- смесь
camelCase
иsnake_case
. - большие функции
- магические константы. Читаете код и видите число 428, что оно означает в контексте? Лучше использовать именную константу.
- смесь
-
Легко тестировать.
Примеры нарушений:
- большие функции
- глобальные переменные
- функции, которые возвращают свой результат через указатель, который получают в параметрах.
void f(int* return_value ) { *return_value = 42; }
Иногда эти правила не применимы. Например, код для микроконтроллеров часто компилируется проприетарными компиляторами низкого качества, которые не умеют оптимизировать код должным образом. Поэтому приходится делать код "менее красивым" чтобы, например, удовлетворять требованиям производительности, или ужать размер программы, чтобы она поместилась в крохотную память.
Но обычно эти правила дают разумный критерий оценки того, хорошее ли вы приняли архитектурное решение или нет. Если вы их нарушаете, вы должны это осознавать и понимать, чего вы этим хотите добиться.
Правила
Мы подготовили для вас список эмпирических правил, которые помогут вам повысить качество кода. При выполнении заданий по С нужно их обязательно соблюдать.
1. Структура программ
- Функции должны получать все необходимые им данные через аргументы.
- Не используйте изменяемые глобальные переменные (константные можно).
- Нельзя смешивать логику вычислений и ввод-вывод.
- Нельзя использовать
typedef
для определения структур (объяснение), кроме структур из одного поля, которые являются аналогомtypedef
, но без неявных преобразований (объяснение). - В заданиях указан только минимально необходимый набор функций. Вы можете добавлять любое число вспомогательных функций для удобства, это поощряется.
- Проверьте архитектуру. Решение внутри одного файла приниматься не будет.
- Пишите маленькие функции, каждая из которых делает что-то одно.
- Про каждую функцию задайте себе вопрос: что она делает? Если ответ длиннее нескольких слов, возможно, функцию надо разбить на функции поменьше.
- Именование.
- Выбирайте хорошие имена для функций и переменных, максимально краткие но информативные (это непросто, хорошие имена приходят в голову не сразу).
- Всегда именуйте функции и переменные единообразно.
- Делайте маленькие функции даже для тех кусочков кода, которые вы не планируете переиспользовать. Маленькая функция имеет имя, которое быстро даёт понять, что она делает.
- К каждой функции и переменной доступ должен быть в наименьшей возможной части программы. Как следствие:
- Функции и глобальные переменные, которые предназначены только для использования в одном модуле, должны быть помечены
static
- Не нужно создавать переменные-индексы вне циклов (это было необходимо в стандарте C89).
- Полезно использовать opaque types.
- Структура файлов:
- В заголовочных файлах нужен Include guard.
- Любой заголовочный файл
header.h
должен быть независим от остальных. Это значит, что файл, состоящий из одной строчки:
#include "header.h"
должен компилироваться.
-
Примерная структура заголовочного файла:
- Includes
- Макросы.
- Определения типов.
- Глобальные переменные.
- Функции.
-
Примерная структура файла
.c
:- Includes
- Макросы (которые не должны использоваться в других файлах).
- Определения локальных для файла типов.
- Глобальные переменные.
- Статические глобальные переменные.
- Функции.
- Статические функции.
-
В каком порядке включать заголовочные файлы для модуля
file.c
:-
file.h
(соответствующий ему заголовочный файл). - Файлы из стандартной библиотеки
- Другие заголовочные файлы.
-
2. Типы
- Всё, что может быть помечено
const
, должно быть помечено. Исключение можно делать для аргументов функций, которые не являются указателями. - Для индексов используйте тип
size_t
. - Используйте только платформо-независимые типы, такие, как
int64_t
илиint_fast64_t
. Численные типы с пометкойfast
предпочтительны, т.к. их существование гарантируется на всех платформах. - Используйте правильные спецификаторы ввода и вывода.
- Не используйте спецификаторы
PRI...
для ввода вместоSCN...
, и наоборот.
3. Использование ввода-вывода
- Сообщения об ошибках должны выводиться в
stderr
. Общее правило: результаты вычислений — вstdout
, информация о том, как происходят вычисления — вstderr
. - Нельзя смешивать ввод-вывод и логику. Одна функция считает, другая — выводит.
4. Компиляция
- Мы пишем код для стандарта C17 (или C18, что почти то же самое).
- Код должен компилироваться с флагами
-std=c18 -pedantic -Wall -Werror
(gcc) или-std=c17 -pedantic -Wall -Werror
(clang). - Пользователям MS Visual Studio придётся тяжко, поддержка C11/C17 пока есть только в Visual Studio 2019 version 16.8 Preview 3. Установите флаги
/W4
(warning level 4) и/WX
(warnings as errors).
Можете попробовать использовать cl-clang
.
- Не забудьте написать
Makefile
. Он должен позволять при изменении одного.c
файла пересобрать часть проекта не пересобирая всё остальное.
Проверять Ваш код мы будем с помощью gcc
и Makefile
. Разрешается cmake
.
5. Отправка решения
Пожалуйста, присылайте решение в виде pull-request. Инструкция.