Идиомы кода и обработка ошибок
Идиомы кода и обработка ошибок
В С нет исключений (try/catch) и сборщика мусора. Если fopen не открыл файл или malloc вернул NULL, программа продолжает выполнение — автор должен явно проверить результат и решить: повторить, вернуть код ошибки вызывающему, записать в лог.
Идиома — устоявшийся приём в сообществе: «так принято писать на С», даже если язык допускает и другие варианты.
Надёжный код строится на явных контрактах: кто владеет памятью, кто закрывает файл, что означает код возврата. Ниже — практики, которые повторяются в ядрах ОС, в libc и в зрелых библиотеках (в том числе в коде, написанном в духе SQLite или nginx).
Мини-словарь
| Термин | Смысл |
|---|---|
| Код возврата | int из функции: 0 — успех, иначе ошибка |
errno | глобальная переменная с кодом последней системной ошибки (POSIX) |
| Владение (ownership) | кто обязан вызвать free / fclose |
| Инвариант | условие, которое всегда истинно, если функция вызвана правильно |
| UB (undefined behavior) | поведение стандарт не описывает; программа может упасть или «тихо» испортить данные |
Одна функция — одна задача
Функция должна делать одно логическое действие: «открыть конфиг», «разобрать строку», «записать буфер в сокет». Смешивание парсинга, сети и логирования в одном теле затрудняет тестирование и обработку ошибок.
Хороший уровень детализации:
int load_config(const char *path, Config *out);
Плохой — функция на двести строк, которая и читает файл, и валидирует JSON-подобный синтаксис, и заполняет глобальные переменные.
Владение ресурсом (ownership)
Для каждого ресурса (память, дескриптор файла, сокет) зафиксируйте правило:
| Ресурс | Кто выделяет | Кто освобождает |
|---|---|---|
malloc внутри модуля | модуль | тот же модуль (free) или явная функция module_destroy |
| буфер, переданный «наружу» | документировать в заголовке | вызывающий или пара create/destroy |
FILE * | кто вызвал fopen | тот же код вызывает fclose |
Правило: на каждый успешный путь выделения должен существовать ровно один путь освобождения. Дублирующий free или пропущенный fclose — классические дефекты.
Коды возврата и errno
Распространённые схемы:
0— успех, отрицательное или ненулевое — ошибка (стиль POSIX).- Указатель:
NULL— ошибка, иначе валидный объект (malloc,fopenв части API). - Булев стиль:
true/falseс деталями вerrnoпосле сбоя системного вызова.
После функций вроде open, read, strtol при ошибке смотрят errno (заголовок <errno.h>). Важно: errno актуален сразу после неудачного вызова; другие функции могут его перезаписать.
FILE *f = fopen(path, "r");
if (f == NULL) {
fprintf(stderr, "cannot open %s: %s\n", path, strerror(errno));
return -1;
}
Для библиотечного API предпочтительнее не полагаться на глобальный errno в публичных функциях, а возвращать собственный код или структуру статуса — так проще использовать библиотеку из нескольких потоков и из встраиваемых сред без полной POSIX-обвязки.
Разбор strerror(errno):
После неудачного fopen в errno лежит число (например, «файл не найден»). strerror переводит его в человекочитаемую строку для лога. Сохраняйте значение в локальную переменную сразу после сбоя, если между ошибкой и strerror будут другие вызовы.
errno_t saved = errno;
fprintf(stderr, "error %d: %s\n", saved, strerror(saved));
Выходные параметры вместо «двух возвратов»
С не умеет возвращать пару «значение + ошибка» как в Rust. Идиомы:
/* код ошибки; результат через указатель */
int parse_int(const char *s, int *out);
/* bool-стиль */
bool queue_pop(Queue *q, Item *out);
Вызывающий обязан проверять код возврата до использования *out.
Пример использования:
int value;
if (parse_int("42", &value) != 0) {
fprintf(stderr, "bad number\n");
return 1;
}
printf("parsed %d\n", value); /* безопасно только после проверки */
Цепочка очистки с goto
При нескольких ресурсах подряд вложенные if с дублированием free/fclose быстро становятся нечитаемыми. В ядре Linux и в руководствах по стилю С часто используют одну метку очистки:
int process_file(const char *path)
{
int ret = -1;
FILE *f = NULL;
char *buf = NULL;
f = fopen(path, "r");
if (f == NULL)
goto cleanup;
buf = malloc(4096);
if (buf == NULL)
goto cleanup;
/* ... работа ... */
ret = 0;
cleanup:
free(buf);
if (f != NULL)
fclose(f);
return ret;
}
goto здесь — не «прыжок в прошлое», а структурированный выход с единой точкой освобождения. Альтернатива — макросы-обёртки или отдельные функции-обёртки для каждого шага; для среднего размера функций метка cleanup остаётся самой прозрачной.
Порядок в cleanup: освобождать в обратном порядке создания (сначала buf, потом f).
assert и проверки в рантайме
assert(условие) из <assert.h> прерывает программу в отладочной сборке, если условие ложно. Подходит для нарушений, которые «не должны случиться» при корректном использовании API внутри команды (инварианты, NULL там, где контракт запрещает).
Не заменяет проверку внешних данных:
/* плохо: assert на пользовательский ввод */
assert(strlen(user_input) < 100);
/* хорошо: явная проверка с кодом ошибки */
if (strlen(user_input) >= 100)
return ERR_TOO_LONG;
В релизной сборке с NDEBUG утверждения вырезаются — на них нельзя опираться для безопасности.
Инварианты и ранний выход
Проверяйте предусловия в начале функции и выходите сразу:
int send_packet(Socket *s, const void *data, size_t len)
{
if (s == NULL || data == NULL || len == 0)
return -1;
/* основная логика */
}
Так уменьшается вложенность и проще читать «счастливый путь» внизу функции.
Неизменяемые входные данные
Помечайте указатели на данные, которые функция не меняет, как const:
size_t write_log(FILE *out, const char *message);
Это документирует намерение и помогает компилятору ловить случайные записи.
Модули — скрытое состояние
Глобальные изменяемые переменные усложняют тесты и многопоточность. Паттерн непрозрачный указатель (typedef struct Database Database; в заголовке, определение структуры только в .c) сужает API и держит состояние в одном месте.
Публичный заголовок объявляет операции; детали реализации и все free остаются внутри единицы компиляции — см. Архитектура программ на С.
Чего избегать
- Игнорирование кодов возврата (
mallocбез проверки наNULL). - «Утечка» при раннем
returnбез освобождения (лечитсяgoto cleanupили обёртками). - Смешение уровней абстракции: печать в
stderrвнутри низкоуровневой функции библиотеки — лучше возвращать код, а логировать наверху. setjmp/longjmpдля обычной обработки ошибок — оставить для специализированных парсеров и совместимости; для прикладного кода достаточно кодов возврата.
См. также: Память процесса, Многопоточность на С, Справочник.
См. также
Другие статьи этого же раздела в боковом меню (как на странице «О разделе»). История языка C - происхождение, ключевые идеи и влияние на развитие операционных систем и компиляторов. Язык С — это процедурный, компилируемый язык программирования, созданный в начале 1970-х годов Деннисом Ритчи в Bell Labs. Программирование на языке С требует понимания не только самого языка, но и всей совокупности программ, задействованных в процессе превращения исходного текста в исполняемый файл. Программа на языке С не выполняется напрямую процессором. Исходный текст проходит несколько этапов обработки, прежде чем превратится в машинный код, который может быть запущен операционной системой. Язык программирования С существует не как набор случайных правил, а как строго определённая спецификация, зафиксированная в международных стандартах. Как исполняемый файл на С раскладывается по областям памяти — код, данные, BSS, куча и стек — и что это даёт при отладке. Архитектура программ на C - организация модулей, процесс компиляции и взаимосвязь компонентов системы. Язык программирования С занимает особое место в истории и практике разработки программного обеспечения. Типизация, набор правил определения типа данных значений языка. Язык программирования С предоставляет механизм создания составных типов данных, позволяющих объединять разнородные элементы под единым именем. Этот механизм называется структурой. Реализация ассоциативного массива на С — хеш-функция, коллизии, цепочки, открытая адресация и изменение размера. Работа с встраиваемой SQL-библиотекой из программы на С — соединение, запросы, параметры и транзакции.История языка С
Основы языка С
Инструментальная цепочка компиляции С
Преобразование исходного кода в исполняемый файл
Стандарты языка С
Память процесса и сегменты
Архитектура программ на С
Компиляторы и среды разработки для С
Типы данных в С
Структуры и объединения
Хеш-таблица на С
Встраиваемая база данных из С