Справка по языку программирования C

Практический справочник по стандартному C17 с примечаниями о C23 и POSIX. Размеры типов, расширения компилятора и системные API зависят от платформы и реализации.

Содержание


Общая характеристика

C — компилируемый язык общего назначения, созданный Деннисом Ритчи в Bell Labs в начале 1970-х годов. Он широко применяется в системном и встроенном программировании, библиотеках, драйверах, инструментах и производительном прикладном коде.

Основные свойства:

Основные редакции стандарта: C89/C90, C95, C99, C11, C17 и C23. Международный стандарт C23 опубликован как ISO/IEC 9899:2024. Поддержка C23 зависит от версии компилятора; на практике часто выбирают C17 и явно включают нужные расширения.

C не гарантирует одинаковое поведение любого исходного текста на всех платформах. Стандарт различает:


Сборка и структура программы

Минимальная программа:

#include <stdio.h>

int main(void)
{
    puts("Hello, world!");
    return 0;
}

Допустимы две переносимые формы точки входа:

int main(void);
int main(int argc, char *argv[]);

Возврат из main эквивалентен вызову exit с тем же кодом. 0 и EXIT_SUCCESS означают успешное завершение, EXIT_FAILURE — ошибку.

Этапы преобразования

Упрощённо сборка состоит из четырёх этапов:

  1. препроцессирование: #include, #define, условная компиляция;
  2. компиляция каждой единицы трансляции в объектный файл;
  3. ассемблирование, обычно выполняемое драйвером компилятора автоматически;
  4. компоновка объектных файлов и библиотек в программу.
gcc -std=c17 -Wall -Wextra -Wpedantic -O2 main.c -o app

Для отладки:

gcc -std=c17 -Wall -Wextra -Wpedantic -Wconversion \
    -O0 -g3 -fsanitize=address,undefined main.c -o app

-Werror полезен в CI, но при переносе между версиями компилятора его обычно включают осознанно.

Препроцессор

#include <stdio.h>       /* системный заголовок */
#include "project.h"     /* заголовок проекта */

#define ARRAY_LEN(a) (sizeof(a) / sizeof((a)[0]))

#if defined(DEBUG)
#  define TRACE(msg) fprintf(stderr, "TRACE: %s\n", (msg))
#else
#  define TRACE(msg) ((void)0)
#endif

Макросы выполняют текстовую подстановку: заключайте параметры и всё выражение в скобки, не передавайте выражения с побочными эффектами в макрос, который может вычислить аргумент несколько раз. Для типизированных вычислений обычно безопаснее функция или static inline.


Типы, размеры и переносимость

Фундаментальные типы

Тип Гарантия стандарта / назначение
char ровно 1 байт; байт содержит не менее 8 бит
signed char, unsigned char минимальный диапазон −127…127 и 0…255
short не менее 16 бит
int не менее 16 бит; естественный целый тип платформы
long не менее 32 бит
long long не менее 64 бит
float вещественный тип одинарной точности
double вещественный тип двойной точности
long double точность не меньше, чем у double
void отсутствие значения; void * — универсальный указатель на объект

Точные пределы берите из <limits.h> и <float.h>, а размер — через sizeof. CHAR_BIT сообщает число бит в байте.

#include <limits.h>
#include <stdio.h>

printf("int: %zu bytes, %d..%d\n", sizeof(int), INT_MIN, INT_MAX);
printf("bits in byte: %d\n", CHAR_BIT);

Обычный char — отдельный тип; его знаковость определяется реализацией. Для сырых байтов используйте unsigned char.

Типы стандартной библиотеки

Тип Заголовок Назначение
size_t <stddef.h> и другие размер объекта и результат sizeof
ptrdiff_t <stddef.h> разность указателей внутри одного массива
int32_t, uint64_t <stdint.h> целые точной ширины, если реализация их предоставляет
int_least32_t <stdint.h> тип шириной не менее 32 бит
intmax_t, uintmax_t <stdint.h> наиболее широкий стандартный целый тип
bool <stdbool.h> в C99–C17 удобное имя для _Bool

Для печати типов из <stdint.h> используйте макросы <inttypes.h>:

#include <inttypes.h>
#include <stdint.h>
#include <stdio.h>

uint64_t n = UINT64_C(123);
printf("%" PRIu64 "\n", n);

Типичные модели данных

Модель int long указатель Где часто встречается
ILP32 4 4 4 32-битные системы
LP64 4 8 8 Linux/macOS 64-bit
LLP64 4 4 8 Windows 64-bit

Это распространённые модели, а не универсальная гарантия. Не сериализуйте структуры простым fwrite, если формат должен быть переносимым: размеры, выравнивание, дополнение и порядок байтов могут различаться.

Квалификаторы


Объявления, области видимости, хранение и связывание

Не смешивайте три разных понятия:

int public_counter;          /* статическая длительность, внешнее связывание */
static int file_counter;     /* статическая длительность, внутреннее связывание */

void example(void)
{
    int local = 0;           /* автоматическая длительность */
    static int calls = 0;    /* статическая длительность, без связывания */
    ++calls;
    (void)local;
}

Объекты со статической длительностью хранения по умолчанию инициализируются нулём. Неинициализированные автоматические объекты имеют неопределённое значение; чтение такого значения может быть UB.

int values[] = {1, 2, 3};
char text[] = "hello";                 /* 6 элементов с завершающим '\0' */
struct Point { int x, y; };
struct Point p = {.x = 10, .y = 20};   /* назначенная инициализация, C99 */

const int n = 100; не является целочисленной константой времени компиляции во всех контекстах C. Для размера обычного массива на уровне файла применяйте, например, enum { CAPACITY = 100 }; или макрос.


Операторы и выражения

Категория Операторы
арифметические + - * / %
сравнение < <= > >= == !=
логические `! &&
побитовые `~ & ^
присваивание `= += -= *= /= %= <<= >>= &= ^=
указатели & — взять адрес, * — разыменовать
доступ . и ->
индексирование и вызов [] и ()
условный оператор условие ? значение1 : значение2
размер и выравнивание sizeof, _Alignof в C11–C17
приведение (тип)выражение
запятая ,

Сокращённый порядок приоритета, от высокого к низкому:

  1. постфиксные: (), [], ., ->, постфиксные ++ и --;
  2. унарные: префиксные ++/--, +, -, !, ~, *, &, sizeof, приведение;
  3. мультипликативные, аддитивные, сдвиги;
  4. отношения и равенство;
  5. побитовые &, ^, |;
  6. логические &&, ||;
  7. ?:;
  8. присваивания;
  9. оператор запятой.

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

/* Плохо: неопределённое поведение из-за нескольких неупорядоченных
   обращений к i, одно из которых изменяет i. */
i = i++ + 1;

Знаковое целочисленное переполнение — UB. Беззнаковая арифметика выполняется по модулю 2^N. Перед делением проверяйте делитель; отдельно учитывайте INT_MIN / -1.


Управляющие конструкции

if (x < 0) {
    puts("negative");
} else if (x == 0) {
    puts("zero");
} else {
    puts("positive");
}
switch (command) {
case 'q':
    quit();
    break;
case 'h':
    show_help();
    break;
default:
    report_unknown(command);
    break;
}

Падение из одного case в следующий допустимо, но его следует явно комментировать.

while (condition) {
    work();
}

do {
    work_once();
} while (condition);

for (size_t i = 0; i < count; ++i) {
    process(i);
}

В C goto иногда полезен для единой очистки нескольких ресурсов:

int do_work(const char *path)
{
    int status = -1;
    FILE *file = fopen(path, "r");
    if (file == NULL)
        return -1;

    char *buffer = malloc(4096);
    if (buffer == NULL)
        goto cleanup_file;

    /* работа */
    status = 0;

    free(buffer);
cleanup_file:
    if (fclose(file) == EOF)
        status = -1;
    return status;
}

Функции

int add(int a, int b);       /* объявление */

int add(int a, int b)        /* определение */
{
    return a + b;
}

int f(void) означает отсутствие параметров. В C17 запись int f() означает, что типы параметров не указаны; в новом коде так писать не следует.

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

bool divide(double a, double b, double *result)
{
    if (result == NULL || b == 0.0)
        return false;
    *result = a / b;
    return true;
}

Переменное число аргументов

Перед ... должен быть именованный параметр в переносимом коде C17:

#include <stdarg.h>

long sum_ints(size_t count, ...)
{
    va_list args;
    va_start(args, count);

    long total = 0;
    for (size_t i = 0; i < count; ++i)
        total += va_arg(args, int); /* тип обязан совпадать после promotions */

    va_end(args);
    return total;
}

Функция с ... не знает фактическое число и типы дополнительных аргументов. Контракт должен передавать эту информацию отдельно, как форматная строка printf или параметр count.

Указатели на функции

typedef int (*BinaryOp)(int, int);

int apply(BinaryOp op, int a, int b)
{
    return op(a, b);
}

Массивы и строки

int numbers[5] = {1, 2, 3, 4, 5};
size_t count = sizeof numbers / sizeof numbers[0];

В большинстве выражений массив преобразуется в указатель на первый элемент. Исключения включают операнды sizeof и унарного &. После передачи в функцию размер массива автоматически не сохраняется:

long sum(const int values[], size_t count)
{
    long result = 0;
    for (size_t i = 0; i < count; ++i)
        result += values[i];
    return result;
}

Строка C — последовательность char, завершаемая нулевым символом \0.

char mutable_text[] = "hello";       /* изменяемый массив */
const char *literal = "hello";       /* строковый литерал изменять нельзя */

Для копирования и конкатенации нужно знать вместимость назначения. strcpy и strcat не проверяют границы. strncpy не является универсальной «безопасной заменой»: при усечении он может не добавить \0, а остаток буфера заполняет нулями.

Простой проверяемый вариант копирования:

#include <stdbool.h>
#include <stdio.h>

bool copy_text(char *dst, size_t capacity, const char *src)
{
    if (dst == NULL || src == NULL || capacity == 0)
        return false;
    int written = snprintf(dst, capacity, "%s", src);
    return written >= 0 && (size_t)written < capacity;
}

snprintf возвращает число символов, которое потребовалось бы без учёта завершающего нуля. Отрицательный результат означает ошибку кодирования/вывода.


Указатели и динамическая память

int value = 42;
int *pointer = &value;
*pointer = 43;

Указатель может быть нулевым, указывать на объект, функцию либо позицию сразу за последним элементом массива. Указатель «на один за концом» разрешено получать и сравнивать, но нельзя разыменовывать.

Арифметика и порядковое сравнение указателей определены только в пределах одного массива (объект можно рассматривать как массив из одного элемента). Вычитание таких указателей даёт ptrdiff_t.

Выделение памяти

#include <stdint.h>
#include <stdlib.h>

int *make_array(size_t count)
{
    if (count > SIZE_MAX / sizeof(int))
        return NULL;

    int *values = malloc(count * sizeof *values);
    if (values == NULL && count != 0)
        return NULL;

    return values;
}

В C результат malloc приводить к типу указателя не нужно. Проверка умножения предотвращает переполнение размера.

Безопасное применение realloc:

int *tmp = realloc(values, new_count * sizeof *values);
if (tmp == NULL && new_count != 0) {
    /* values по-прежнему действителен */
    handle_error();
} else {
    values = tmp;
}

Сначала отдельно проверьте new_count > SIZE_MAX / sizeof *values. Поведение realloc(ptr, 0) исторически различалось и менялось между редакциями стандарта; для ясности освобождайте память явным free(ptr).

Правила владения:


Структуры, объединения и перечисления

struct Point {
    int x;
    int y;
};

struct Point p = {.x = 10, .y = 20};
struct Point *pp = &p;
pp->x = 30;

У структуры может быть дополнение между полями и в конце. Сравнивайте поля, а не сырое представление через memcmp, если дополнение не инициализировано или представление типов допускает несколько эквивалентных вариантов.

typedef struct Node Node;
struct Node {
    int value;
    Node *next;
};

Объединение хранит разные члены в одной области памяти:

union Number {
    int i;
    float f;
};

Чтение члена, отличного от последнего записанного, имеет тонкие правила и часто зависит от реализации. Для переносимого преобразования битового представления используйте memcpy; для просмотра байтов любого объекта разрешён доступ через unsigned char *.

enum Color {
    COLOR_RED,
    COLOR_GREEN,
    COLOR_BLUE
};

Перечисление задаёт именованные целочисленные константы, но не гарантирует строгую проверку допустимых значений во время выполнения.


Ввод, вывод и разбор данных

Форматированный вывод

Значение printf
int %d
unsigned int %u
long %ld
long long %lld
size_t %zu
double %f, %e, %g
long double %Lf
строка %s
указатель %p, аргумент (void *)ptr

У printf аргумент float продвигается до double, поэтому %f печатает double. У scanf указатели должны точно соответствовать формату: %f ожидает float *, %lf — double *, %Lf — long double *.

Проверяйте результат:

if (printf("value=%d\n", value) < 0) {
    /* ошибка вывода */
}

Безопасный разбор целого числа

Для интерактивного ввода обычно надёжнее fgets + strtol, чем scanf или atoi:

#include <errno.h>
#include <limits.h>
#include <stdbool.h>
#include <stdlib.h>

bool parse_int(const char *text, int *result)
{
    if (text == NULL || result == NULL)
        return false;

    errno = 0;
    char *end = NULL;
    long value = strtol(text, &end, 10);

    if (end == text || errno == ERANGE || value < INT_MIN || value > INT_MAX)
        return false;

    while (*end == ' ' || *end == '\t' || *end == '\n' || *end == '\r')
        ++end;
    if (*end != '\0')
        return false;

    *result = (int)value;
    return true;
}

Файлы

#include <stdio.h>

int print_file(const char *path)
{
    FILE *file = fopen(path, "r");
    if (file == NULL) {
        perror(path);
        return -1;
    }

    char line[256];
    while (fgets(line, sizeof line, file) != NULL)
        fputs(line, stdout);

    int status = 0;
    if (ferror(file)) {
        perror("read");
        status = -1;
    }
    if (fclose(file) == EOF) {
        perror("close");
        status = -1;
    }
    return status;
}

Режимы: r — чтение, w — создание/усечение, a — добавление; + добавляет чтение и запись, b выбирает бинарный режим. На системах, различающих текстовые и бинарные файлы, используйте rb/wb для бинарных данных.


Основные заголовки стандартной библиотеки

Заголовок Назначение
<assert.h> диагностические утверждения assert
<ctype.h> классификация и преобразование байтовых символов
<errno.h> errno, EDOM, ERANGE и другие коды
<float.h> свойства вещественных типов
<inttypes.h> форматные макросы и функции для максимальных целых типов
<limits.h> пределы целых типов
<math.h> математические функции
<setjmp.h> нелокальные переходы
<signal.h> сигналы стандартного C
<stdarg.h> вариативные функции
<stdbool.h> bool, true, false в C99–C17
<stddef.h> size_t, ptrdiff_t, NULL, offsetof
<stdint.h> целые типы заданной/минимальной ширины
<stdio.h> потоки и форматированный ввод-вывод
<stdlib.h> память, преобразования, сортировка, завершение
<string.h> строки, память, strerror
<time.h> календарное время и часы
<wchar.h> широкие символы и строки

C11 также добавил <stdatomic.h>, <threads.h>, <uchar.h> и другие заголовки. Наличие полезного API в заголовке не означает, что конкретная реализация полностью поддерживает все необязательные возможности.

qsort:

#include <stdlib.h>

static int compare_ints(const void *lhs, const void *rhs)
{
    int a = *(const int *)lhs;
    int b = *(const int *)rhs;
    return (a > b) - (a < b); /* без риска переполнения a - b */
}

qsort(values, count, sizeof values[0], compare_ints);

rand не подходит для криптографии, токенов и секретов. rand_r, strdup, getline, сокеты, fork, pthread и strerror_r относятся к POSIX или иным расширениям, а не к классическому стандартному C (часть функций могла быть стандартизована в более новых редакциях). Указывайте требуемую платформу и feature-test macros.

Функции <ctype.h> требуют EOF либо значение, представимое как unsigned char:

if (isalpha((unsigned char)ch)) {
    /* ... */
}

Многофайловые проекты

Структура:

project/
├── include/student.h
├── src/main.c
├── src/student.c
└── Makefile

Заголовок содержит публичный интерфейс и защиту от повторного включения:

#ifndef PROJECT_STUDENT_H
#define PROJECT_STUDENT_H

#include <stddef.h>

typedef struct {
    char name[64];
    double grade;
} Student;

void sort_students_by_grade(Student students[], size_t count);

#endif

Файл реализации первым подключает собственный заголовок: это помогает обнаружить недостающие зависимости заголовка.

CC       := cc
CPPFLAGS := -Iinclude
CFLAGS   := -std=c17 -Wall -Wextra -Wpedantic -O2
LDFLAGS  :=
LDLIBS   :=

TARGET := app
OBJ    := src/main.o src/student.o

$(TARGET): $(OBJ)
    $(CC) $(LDFLAGS) $^ $(LDLIBS) -o $@

src/%.o: src/%.c
    $(CC) $(CPPFLAGS) $(CFLAGS) -MMD -MP -c $< -o $@

-include $(OBJ:.o=.d)

.PHONY: clean
clean:
    rm -f $(TARGET) $(OBJ) $(OBJ:.o=.d)

Не определяйте обычные функции и объекты с внешним связыванием в заголовке. Исключения проектируют явно: static inline, объявления extern, определения типов и константные макросы.


Ошибки, неопределённое поведение и безопасность

Проблема Пример Защита
выход за границы a[count] для массива из count элементов хранить и проверять размер
use-after-free чтение *p после free(p) ясное владение, ASan
double free два вызова free для одного объекта единая точка освобождения
нулевой/невалидный указатель *p без проверки контракта проверка и документированный контракт
переполнение размера n * sizeof *p проверить n > SIZE_MAX / sizeof *p
знаковое переполнение INT_MAX + 1 проверка до операции
неверный формат printf("%d", size) при size_t %zu, предупреждения формата
незавершённая строка буфер без \0 перед strlen отслеживать длину и ёмкость
перекрывающиеся области memcpy для перекрытия memmove
неверный sizeof sizeof(ptr) вместо массива передавать длину отдельно
сравнение строк через == сравниваются адреса strcmp
неинициализированное значение чтение локальной переменной инициализация, анализатор
неверная обработка realloc p = realloc(p, n) временный указатель

scanf("%s", buf) без ограничения ширины может переполнить буфер. Даже с шириной остаются сложности обработки остатка строки; для пользовательского ввода обычно применяйте fgets и явный разбор.

assert предназначен для внутренних инвариантов, а не для проверки пользовательских данных или ошибок среды. При NDEBUG выражение assert не вычисляется, поэтому в нём не должно быть необходимых побочных эффектов.

Компилятор вправе оптимизировать исходя из предположения, что UB не происходит. «Работает в отладочной сборке» не доказывает корректность.


Инструменты разработки

Инструмент Назначение
GCC, Clang, MSVC компиляторы
GDB, LLDB отладчики
Make, CMake, Meson системы сборки
AddressSanitizer выходы за границы, use-after-free и часть других ошибок памяти
UndefinedBehaviorSanitizer многие виды UB
MemorySanitizer чтение неинициализированной памяти, прежде всего в Clang
ThreadSanitizer гонки данных
Valgrind Memcheck динамический анализ памяти на поддерживаемых платформах
clang-tidy, Clang Static Analyzer, Cppcheck статический анализ
gcov, llvm-cov покрытие тестами
ctags, cscope, clangd навигация и индексирование кода

Полезные команды:

# Отладочная сборка с санитайзерами
cc -std=c17 -Wall -Wextra -Wpedantic -Wconversion \
   -O1 -g3 -fno-omit-frame-pointer \
   -fsanitize=address,undefined *.c -o app

# Статический анализ Clang
clang --analyze -std=c17 -Wall -Wextra src/*.c

# Valgrind (Linux, если доступен)
valgrind --leak-check=full --show-leak-kinds=all ./app

Санитайзеры обычно применяют в тестовых сборках, а не в итоговом бинарном файле. Запускайте тесты как минимум с двумя компиляторами, если переносимость важна.


Практические рекомендации

  1. Зафиксируйте целевой стандарт и поддерживаемые платформы.
  2. Включите высокий уровень предупреждений и исправляйте причины, а не подавляйте сообщения без анализа.
  3. Проверяйте результаты malloc, realloc, fopen, fclose, функций ввода-вывода и преобразований.
  4. Передавайте длину вместе с указателем на массив или буфер.
  5. Для размеров и индексов применяйте size_t, но внимательно обрабатывайте вычитание и обратные циклы беззнаковых значений.
  6. Документируйте допустимость NULL, владение памятью, диапазоны, единицы измерения и состояние объекта после ошибки.
  7. Ограничивайте область видимости: локальные переменные, static для внутренних функций и объектов файла.
  8. Предпочитайте const для входных данных, которые функция не изменяет.
  9. Не используйте volatile для синхронизации потоков; применяйте атомики или примитивы потоковой библиотеки.
  10. Отделяйте стандартный C от POSIX, WinAPI и расширений компилятора.
  11. Используйте единый путь очистки, когда функция захватывает несколько ресурсов.
  12. Тестируйте границы: ноль, один элемент, максимальные размеры, пустые строки, ошибки ввода и нехватку памяти.
  13. Не используйте atoi для проверяемого ввода; выбирайте strtol, strtoul, strtod и проверяйте end/errno.
  14. Не применяйте sprintf, если вместимость назначения неизвестна; проверяйте результат snprintf.
  15. Для бинарных форматов задавайте ширину полей, порядок байтов и правила кодирования явно.

Мини-проект

Задача: отсортировать студентов по оценке по убыванию, а при равных оценках — по имени.

include/student.h

#ifndef PROJECT_STUDENT_H
#define PROJECT_STUDENT_H

#include <stddef.h>

typedef struct {
    char name[64];
    double grade;
} Student;

void sort_students_by_grade(Student students[], size_t count);

#endif

src/student.c

#include "student.h"

#include <stdlib.h>
#include <string.h>

static int compare_students(const void *lhs, const void *rhs)
{
    const Student *a = lhs;
    const Student *b = rhs;

    if (a->grade < b->grade)
        return 1;
    if (a->grade > b->grade)
        return -1;
    return strcmp(a->name, b->name);
}

void sort_students_by_grade(Student students[], size_t count)
{
    qsort(students, count, sizeof students[0], compare_students);
}

src/main.c

#include "student.h"

#include <stdio.h>

int main(void)
{
    Student students[] = {
        {.name = "Alice", .grade = 3.7},
        {.name = "Bob",   .grade = 3.2},
        {.name = "Carol", .grade = 3.9},
    };
    size_t count = sizeof students / sizeof students[0];

    sort_students_by_grade(students, count);

    for (size_t i = 0; i < count; ++i)
        printf("%zu. %s — %.2f\n", i + 1, students[i].name,
               students[i].grade);

    return 0;
}

Сборка и ожидаемый результат:

cc -Iinclude -std=c17 -Wall -Wextra -Wpedantic -O2 \
   src/main.c src/student.c -o students
./students
1. Carol — 3.90
2. Alice — 3.70
3. Bob — 3.20

Частые задачи

Задача Рекомендуемый подход
прочитать строку fgets, затем проверить усечение и перевод строки
разобрать число strtol/strtoul/strtod с проверкой
форматировать в буфер snprintf с проверкой результата
динамический массив структура {data, size, capacity} и проверяемый realloc
сортировка qsort и корректный компаратор
двоичный поиск bsearch для отсортированного массива либо собственный поиск
связанный список структура узла, ясные правила владения
хеш-таблица собственная реализация или проверенная библиотека
многопоточность C11 <threads.h> при поддержке либо явно POSIX/WinAPI
сокеты и процессы платформенный API, например POSIX; это не стандартный C
криптографическая случайность API ОС или проверенная криптографическая библиотека, не rand
сериализация явный формат, фиксированные ширины и порядок байтов

Шаблон динамического массива

struct IntVector {
    int *data;
    size_t size;
    size_t capacity;
};

При росте сначала проверяйте арифметику размера, затем вызывайте realloc через временный указатель. Инвариант: size <= capacity, а data == NULL допустим при capacity == 0.


Литература и стандарты

  1. Brian W. Kernighan, Dennis M. Ritchie — The C Programming Language, 2nd ed. Классика C89; некоторые практики устарели для современного защищённого кода.
  2. Jens Gustedt — Modern C. Современный взгляд на язык и библиотеку.
  3. Robert C. Seacord — Effective C и Secure Coding in C and C++.
  4. Peter van der Linden — Expert C Programming: Deep C Secrets.
  5. Andrew Koenig — C Traps and Pitfalls.
  6. Samuel P. Harbison III, Guy L. Steele Jr. — C: A Reference Manual.
  7. SEI CERT C Coding Standard — рекомендации по безопасному C.
  8. cppreference, раздел C — удобный справочник; это не нормативный текст стандарта.
  9. ISO/IEC 9899 и публичные проекты WG14: N1570 близок к C11, N3096 — финальный проект C23.
  10. POSIX.1 и системные man-страницы — для POSIX API, отдельно от ISO C.

Стандарт задаёт нормативные требования, но он не является учебником. Для практики сочетайте справочник, документацию реализации, предупреждения компилятора, тесты и анализаторы.


Чек-лист перед выпуском