Справка по языку программирования C
Практический справочник по стандартному C17 с примечаниями о C23 и POSIX. Размеры типов, расширения компилятора и системные API зависят от платформы и реализации.
Содержание
- Содержание
- Общая характеристика
- Сборка и структура программы
- Типы, размеры и переносимость
- Объявления, области видимости, хранение и связывание
- Операторы и выражения
- Управляющие конструкции
- Функции
- Массивы и строки
- Указатели и динамическая память
- Структуры, объединения и перечисления
- Ввод, вывод и разбор данных
- Основные заголовки стандартной библиотеки
- Многофайловые проекты
- Ошибки, неопределённое поведение и безопасность
- Инструменты разработки
- Практические рекомендации
- Мини-проект
- Частые задачи
- Литература и стандарты
- Чек-лист перед выпуском
Общая характеристика
C — компилируемый язык общего назначения, созданный Деннисом Ритчи в Bell Labs в начале 1970-х годов. Он широко применяется в системном и встроенном программировании, библиотеках, драйверах, инструментах и производительном прикладном коде.
Основные свойства:
- процедурная и структурная модель программирования;
- статическая типизация, но множество неявных преобразований;
- ручное управление ресурсами;
- прямой доступ к представлению данных и памяти;
- небольшой уровень абстракции и высокая переносимость при соблюдении стандарта;
- отсутствие обязательных сборщика мусора, виртуальной машины и объектной модели.
Основные редакции стандарта: C89/C90, C95, C99, C11, C17 и C23. Международный стандарт C23 опубликован как ISO/IEC 9899:2024. Поддержка C23 зависит от версии компилятора; на практике часто выбирают C17 и явно включают нужные расширения.
C не гарантирует одинаковое поведение любого исходного текста на всех платформах. Стандарт различает:
- определённое поведение — результат задан стандартом;
- поведение, определяемое реализацией — реализация документирует выбор, например знаковость обычного
char; - неуточнённое поведение — допускается несколько вариантов без обязательного документирования;
- неопределённое поведение (UB) — стандарт не накладывает требований, например выход за границы массива.
Сборка и структура программы
Минимальная программа:
#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 — ошибку.
Этапы преобразования
Упрощённо сборка состоит из четырёх этапов:
- препроцессирование:
#include,#define, условная компиляция; - компиляция каждой единицы трансляции в объектный файл;
- ассемблирование, обычно выполняемое драйвером компилятора автоматически;
- компоновка объектных файлов и библиотек в программу.
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, если формат должен быть переносимым: размеры, выравнивание, дополнение и порядок байтов могут различаться.
Квалификаторы
constзапрещает изменение объекта через данный lvalue; это не всегда означает физически неизменяемую память.volatileзаставляет выполнять наблюдаемые обращения к объекту по правилам абстрактной машины. Он нужен, например, для memory-mapped I/O, но не заменяет атомики и синхронизацию потоков.restrict(C99) — обещание об отсутствии конфликтующего доступа через другие указатели в заданной области; нарушение обещания ведёт к UB._Atomicи<stdatomic.h>(C11) предоставляют атомарные операции, если они поддерживаются реализацией.
Объявления, области видимости, хранение и связывание
Не смешивайте три разных понятия:
- область видимости определяет, где имя доступно в исходном коде;
- длительность хранения определяет, как долго существует объект;
- связывание определяет, обозначают ли одинаковые имена одну сущность в разных областях или файлах.
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 |
| приведение | (тип)выражение |
| запятая | , |
Сокращённый порядок приоритета, от высокого к низкому:
- постфиксные:
(),[],.,->, постфиксные++и--; - унарные: префиксные
++/--,+,-,!,~,*,&,sizeof, приведение; - мультипликативные, аддитивные, сдвиги;
- отношения и равенство;
- побитовые
&,^,|; - логические
&&,||; ?:;- присваивания;
- оператор запятой.
Когда порядок важен, используйте скобки. Не пишите выражения, результат которых зависит от порядка вычисления операндов:
/* Плохо: неопределённое поведение из-за нескольких неупорядоченных
обращений к 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);
}breakзавершает ближайший цикл илиswitch;continueпереходит к следующей итерации ближайшего цикла;returnзавершает функцию;gotoпередаёт управление метке внутри той же функции.
В 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).
Правила владения:
- каждому успешному выделению должен соответствовать один
free; - после
freeвсе указатели на объект становятся висячими; - присваивание одному указателю
NULLне испраляет остальные псевдонимы; free(NULL)безопасен;- не используйте объект после освобождения и не освобождайте его дважды;
- документируйте, кто владеет возвращаемой или передаваемой памятью.
Структуры, объединения и перечисления
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Санитайзеры обычно применяют в тестовых сборках, а не в итоговом бинарном файле. Запускайте тесты как минимум с двумя компиляторами, если переносимость важна.
Практические рекомендации
- Зафиксируйте целевой стандарт и поддерживаемые платформы.
- Включите высокий уровень предупреждений и исправляйте причины, а не подавляйте сообщения без анализа.
- Проверяйте результаты
malloc,realloc,fopen,fclose, функций ввода-вывода и преобразований. - Передавайте длину вместе с указателем на массив или буфер.
- Для размеров и индексов применяйте
size_t, но внимательно обрабатывайте вычитание и обратные циклы беззнаковых значений. - Документируйте допустимость
NULL, владение памятью, диапазоны, единицы измерения и состояние объекта после ошибки. - Ограничивайте область видимости: локальные переменные,
staticдля внутренних функций и объектов файла. - Предпочитайте
constдля входных данных, которые функция не изменяет. - Не используйте
volatileдля синхронизации потоков; применяйте атомики или примитивы потоковой библиотеки. - Отделяйте стандартный C от POSIX, WinAPI и расширений компилятора.
- Используйте единый путь очистки, когда функция захватывает несколько ресурсов.
- Тестируйте границы: ноль, один элемент, максимальные размеры, пустые строки, ошибки ввода и нехватку памяти.
- Не используйте
atoiдля проверяемого ввода; выбирайтеstrtol,strtoul,strtodи проверяйтеend/errno. - Не применяйте
sprintf, если вместимость назначения неизвестна; проверяйте результатsnprintf. - Для бинарных форматов задавайте ширину полей, порядок байтов и правила кодирования явно.
Мини-проект
Задача: отсортировать студентов по оценке по убыванию, а при равных оценках — по имени.
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);
#endifsrc/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
./students1. 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.
Литература и стандарты
- Brian W. Kernighan, Dennis M. Ritchie — The C Programming Language, 2nd ed. Классика C89; некоторые практики устарели для современного защищённого кода.
- Jens Gustedt — Modern C. Современный взгляд на язык и библиотеку.
- Robert C. Seacord — Effective C и Secure Coding in C and C++.
- Peter van der Linden — Expert C Programming: Deep C Secrets.
- Andrew Koenig — C Traps and Pitfalls.
- Samuel P. Harbison III, Guy L. Steele Jr. — C: A Reference Manual.
- SEI CERT C Coding Standard — рекомендации по безопасному C.
- cppreference, раздел C — удобный справочник; это не нормативный текст стандарта.
- ISO/IEC 9899 и публичные проекты WG14: N1570 близок к C11, N3096 — финальный проект C23.
- POSIX.1 и системные
man-страницы — для POSIX API, отдельно от ISO C.
Стандарт задаёт нормативные требования, но он не является учебником. Для практики сочетайте справочник, документацию реализации, предупреждения компилятора, тесты и анализаторы.
Чек-лист перед выпуском
- Зафиксированы стандарт C, компиляторы, архитектуры и системные зависимости.
- Сборка проходит с выбранным набором предупреждений без необъяснённых диагностик.
- Отладочные тесты прошли с ASan и UBSan; при наличии потоков — с TSan в отдельном запуске.
- Выполнен статический анализ, а результаты разобраны.
- Проверены ошибки выделения памяти и открытия/чтения/записи/закрытия файлов.
- Нет выходов за границы, переполнений размеров, use-after-free и double free.
- Форматные строки соответствуют фактическим типам.
- Пользовательский ввод разбирается с полной проверкой диапазона и остатка.
- Контракты функций и владение ресурсами документированы.
- Публичные заголовки самодостаточны и защищены от повторного включения.
- Тесты покрывают обычные, граничные и ошибочные сценарии.
- Переносимые данные не зависят от сырых layout структуры и порядка байтов машины.
-
assertне используется вместо обработки внешних ошибок. - Сборка воспроизводима, зависимости и лицензии зафиксированы.
- Документация и примеры соответствуют текущему интерфейсу.