Skip to content

C Шпаргалка

Низкоуровневый язык для систем, встраиваемых устройств и разработки ОС.

01

Начало работы

Hello World

Каждая C-программа начинается в main(). #include <stdio.h> подключает стандартную библиотеку ввода-вывода (printf, scanf). main возвращает 0 при успехе, ненулевое при неудаче. Ключевое слово void явно объявляет, что main не принимает параметров.

c
#include <stdio.h>

int main(void) {
    printf("Hello, World!\n");
    return 0;
}

Переменные и типы

C статически типизирован. Частые типы: int, float, double, char. float требует суффикса f. char[] — строка (массив с завершающим нулём). long и short — модификаторы размера. unsigned означает неотрицательный. Размеры зависят от платформы; используйте <stdint.h> для фиксированных ширин.

c
int age = 30;
float height = 5.7f;
double pi = 3.14159;
char grade = 'A';
char name[] = "Alice";
long big = 100000L;
unsigned int count = 42;
printf("%s is %d\n", name, age);

Ввод и вывод

scanf требует адрес (&) переменной для сохранения ввода. Всегда ограничивайте длину строкового ввода (%49s для буфера 50 символов) для предотвращения переполнения буфера. scanf прекращает чтение строк на пробеле; используйте fgets для полных строк.

c
int n;
printf("Enter a number: ");
scanf("%d", &n);
printf("You entered %d\n", n);

char name[50];
printf("Enter name: ");
scanf("%49s", name);  // limit to prevent overflow
printf("Hi, %s!\n", name);

Спецификаторы формата printf

Спецификаторы формата управляют выводом: %d int, %f float, %c char, %s строка, %x hex, %p указатель. Ширина и точность (например, %5.2f) управляют выравниванием и количеством знаков. Несоответствие спецификатора и типа вызывает неопределённое поведение.

c
printf("%d\n", 42);        // integer
printf("%f\n", 3.14);       // float/double
printf("%.2f\n", 3.14159);  // 3.14 (2 decimals)
printf("%c\n", 'A');        // char
printf("%s\n", "hello");    // string
printf("%x\n", 255);        // ff (hex)
printf("%5d\n", 42);        // right-aligned, width 5
printf("%-5d|\n", 42);      // left-aligned

Препроцессор и заголовки

Препроцессор запускается перед компиляцией. #include вставляет заголовочные файлы, #define создаёт макросы и константы. Всегда используйте include guards (#ifndef/#define/#endif) в заголовках для предотвращения двойного включения. Макросы — текстовая подстановка, используйте скобки вокруг параметров.

c
#include <stdio.h>   // system header
#include "myheader.h"  // local header

#define PI 3.14159
#define SQUARE(x) ((x) * (x))

#ifndef GUARD_H
#define GUARD_H
// header content
#endif
02

Строки и string.h

Основы строк

C-строки — массивы char с завершающим нулём. strlen считает символы до '\0'; sizeof возвращает размер буфера. strcpy копирует до нулевого терминатора — всегда убедитесь, что destination достаточно велик для предотвращения переполнения.

c
#include <string.h>
char s[20] = "Hello";
printf("Length: %zu\n", strlen(s));   // 5
printf("Size: %zu\n", sizeof(s));     // 20

char dest[20];
strcpy(dest, s);   // copy
printf("%s\n", dest);  // Hello

Конкатенация и сравнение

strcat добавляет (в destination должно быть место). strcmp сравнивает лексикографически: возвращает 0 при равенстве, отрицательное при первом < второго, положительное при первом > второго. Никогда не используйте == для сравнения строк (сравнивает указатели, а не содержимое).

c
#include <string.h>
char s[30] = "Hello";
strcat(s, ", World!");   // s = "Hello, World!"
printf("%s\n", s);

int cmp = strcmp("apple", "banana");
// returns <0 if a<b, 0 if equal, >0 if a>b
if (strcmp(s, "Hello") == 0) {
    printf("Equal!\n");
}

sprintf и sscanf

sprintf форматирует в строковый буфер (как printf, но в строку). sscanf разбирает строку в переменные (как scanf, но из строки). Используйте snprintf вместо sprintf для предотвращения переполнения буфера указанием максимального размера.

c
char buf[100];
int age = 30;
char name[] = "Alice";
sprintf(buf, "%s is %d years old", name, age);
printf("%s\n", buf);

int a, b;
sscanf("10 20", "%d %d", &a, &b);
printf("a=%d, b=%d\n", a, b);  // a=10, b=20

strchr, strstr и strtok

strchr находит символ, strstr — подстроку. strtok разделяет строку разделителями, но модифицирует исходную строку (вставляет нулевые терминаторы) и не потокобезопасен — передавайте NULL при последующих вызовах для продолжения токенизации.

c
#include <string.h>
char s[] = "Hello, World!";
char *p = strchr(s, 'W');   // find first 'W'
printf("%s\n", p);          // World!

char *sub = strstr(s, "World");
printf("%s\n", sub);        // World!

char tokens[] = "a,b,c";
char *tok = strtok(tokens, ",");
while (tok) {
    printf("%s\n", tok);
    tok = strtok(NULL, ",");
}

fgets и безопасный ввод

fgets — безопасный способ чтения строк — принимает ограничение размера для предотвращения переполнения. В отличие от scanf, читает пробелы. Перевод строки включается в результат; strcspn находит и удаляет его. Всегда предпочитайте fgets вместо gets (удалён в C11).

c
char line[100];
printf("Enter text: ");
fgets(line, sizeof(line), stdin);
// removes trailing newline
line[strcspn(line, "\n")] = 0;
printf("You said: %s\n", line);
03

Числа и математика

Целочисленные типы и лимиты

Используйте <stdint.h> для типов фиксированной ширины (int32_t, int64_t), когда важен точный размер. <limits.h> предоставляет INT_MAX, INT_MIN и т. д. для платформо-специфичных границ. Суффикс LL отмечает литералы long long. Размеры int/long зависят от платформы.

c
#include <stdint.h>
#include <limits.h>
int32_t a = 100;
int64_t big = 9223372036854775807LL;
uint8_t byte = 255;

printf("INT_MAX = %d\n", INT_MAX);     // 2147483647
printf("INT_MIN = %d\n", INT_MIN);     // -2147483648
printf("UINT_MAX = %u\n", UINT_MAX);   // 4294967295

Плавающая точка

float — 4 байта (6-7 значащих цифр), double — 8 байт (15-16 цифр). Никогда не сравнивайте float через == из-за ошибок округления — используйте fabs(a - b) < epsilon. <float.h> предоставляет DBL_MAX, DBL_EPSILON для границ и точности.

c
#include <float.h>
double d = 3.141592653589793;
float f = 3.14f;

printf("DBL_MAX = %e\n", DBL_MAX);
printf("DBL_EPSILON = %e\n", DBL_EPSILON);

if (d == 0.1 + 0.2) {
    // likely false! floating point imprecision
}

Математические функции

<math.h> предоставляет стандартные математические функции. pow и sqrt возвращают double. fabs — float-версия abs (abs для int). Линкуйте с -lm на некоторых системах. Для финансовых расчётов избегайте плавающей точки — используйте целые центы.

c
#include <math.h>
double x = 2.5;
pow(x, 3);      // 15.625
sqrt(x);        // 1.581
fabs(-5.0);     // 5.0
floor(3.7);     // 3.0
ceil(3.2);      // 4.0
fmod(10.5, 3);  // 1.5
exp(1);         // 2.718 (e^1)
log(2.718);     // 1.0 (natural log)

Случайные числа

rand() возвращает псевдослучайное int от 0 до RAND_MAX. Инициализируйте через srand() один раз при запуске программы (используя time(NULL)). rand() % N имеет смещение по модулю и низкое качество; для серьёзного использования читайте /dev/urandom или используйте стороннюю PRNG-библиотеку.

c
#include <stdlib.h>
#include <time.h>

srand(time(NULL));  // seed once at start
int r = rand() % 100;       // 0-99
int dice = rand() % 6 + 1;  // 1-6

float fr = (float)rand() / RAND_MAX;  // 0.0 - 1.0

Преобразование типов и кастинг

Кастинг: (type)value. Целочисленное деление усекает — используйте float-операнд для получения float-результата. atoi/atof преобразуют строки в числа, но не проверяют ошибки; предпочитайте strtol/strtod, сообщающие об ошибках разбора через errno.

c
int i = 65;
char c = (char)i;        // 'A'
double d = 3.99;
int truncated = (int)d;  // 3

// Implicit promotion
int a = 5;
double result = a / 2.0;  // 2.5 (promoted to double)
int bad = a / 2;          // 2 (integer division)

// String to number
int n = atoi("42");
double f = atof("3.14");
04

Поток управления

If / Else

if/else if/else — стандартная условная конструкция. C трактует 0 как false, любое ненулевое как true. Используйте фигурные скобки даже для одиночных операторов для предотвращения багов при последующем добавлении строк. В C89 нет логического типа; C99 добавляет _Bool и <stdbool.h>.

c
int score = 85;
if (score >= 90) {
    printf("A\n");
} else if (score >= 80) {
    printf("B\n");
} else if (score >= 70) {
    printf("C\n");
} else {
    printf("F\n");
}

Switch

switch переходит к совпадающей метке case. Всегда используйте break для предотвращения провалов (case 6 и 7 разделяют код намеренно). default обрабатывает несовпадающие значения. switch работает только с целочисленными и char-типами, не строками или float.

c
int day = 3;
switch (day) {
    case 1: printf("Mon\n"); break;
    case 2: printf("Tue\n"); break;
    case 3: printf("Wed\n"); break;
    case 6:
    case 7: printf("Weekend\n"); break;
    default: printf("Invalid\n");
}

Цикл for

Цикл for имеет init; condition; update. sizeof(nums)/sizeof(nums[0]) вычисляет длину массива на этапе компиляции. Объявление i внутри for требует C99 или новее. Тело цикла выполняется ноль раз, если условие изначально ложно.

c
for (int i = 0; i < 5; i++) {
    printf("%d\n", i);
}

// Iterate an array
int nums[] = {10, 20, 30};
int n = sizeof(nums) / sizeof(nums[0]);
for (int i = 0; i < n; i++) {
    printf("%d\n", nums[i]);
}

While и do-while

while проверяет перед выполнением (может не выполниться ни разу). do-while сначала выполняет тело, затем проверяет (выполняется хотя бы один раз). do-while идеален для валидации ввода и циклов меню, где приглашение должно появиться до проверки условия.

c
int count = 0;
while (count < 3) {
    printf("%d\n", count++);
}

int x;
do {
    printf("Enter positive: ");
    scanf("%d", &x);
} while (x <= 0);  // runs at least once

Break, Continue и goto

break выходит из ближайшего цикла/switch; continue переходит к следующей итерации. В C нет помеченного break, поэтому goto — идиоматичный способ выхода из глубоко вложенных циклов. goto в остальных случаях не рекомендуется, но допустим для шаблонов очистки и выхода из вложенных циклов.

c
for (int i = 0; i < 10; i++) {
    if (i == 3) continue;  // skip 3
    if (i == 7) break;     // stop at 7
    printf("%d ", i);      // 0 1 2 4 5 6
}

// goto for breaking nested loops
for (int i = 0; i < n; i++) {
    for (int j = 0; j < m; j++) {
        if (found) goto done;
    }
}
done: printf("exited\n");
05

Функции

Определение и вызов

Функции должны быть объявлены (прототип) или определены до использования. void return означает отсутствие возвращаемого значения. const char *name означает, что функция не изменит строку. C передаёт аргументы по значению; используйте указатели для имитации передачи по ссылке.

c
int add(int a, int b) {
    return a + b;
}

void greet(const char *name) {
    printf("Hello, %s!\n", name);
}

int main(void) {
    int sum = add(3, 4);
    greet("Alice");
    return 0;
}

Рекурсия

Рекурсия вызывает сама себя с меньшим вводом. Каждой рекурсивной функции нужен базовый случай для остановки. Наивный fib выше — O(2^n), экспоненциальный. Используйте мемоизацию или итерацию для эффективности. Глубокая рекурсия может переполнить стек вызовов.

c
int factorial(int n) {
    if (n <= 1) return 1;       // base case
    return n * factorial(n - 1); // recursive case
}

int fib(int n) {
    if (n < 2) return n;
    return fib(n - 1) + fib(n - 2);
}
// factorial(5) == 120

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

Указатели на функции хранят адрес функции, обеспечивая callback'и и динамическую диспетчеризацию. Синтаксис int (*op)(int, int) объявляет указатель на функцию, принимающую два int и возвращающую int. Используется в qsort, обработчиках событий и плагинных системах.

c
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }

int (*op)(int, int) = add;
printf("%d\n", op(3, 4));  // 7

op = sub;
printf("%d\n", op(3, 4));  // -1

// As a parameter
int apply(int (*f)(int, int), int a, int b) {
    return f(a, b);
}

Вариативные функции

Вариативные функции принимают переменное число аргументов через <stdarg.h>. va_start инициализирует, va_arg извлекает следующий аргумент, va_end очищает. Нужно знать количество аргументов (например, параметр count или sentinel-значение). printf работает так.

c
#include <stdarg.h>
int sum(int count, ...) {
    va_list args;
    va_start(args, count);
    int total = 0;
    for (int i = 0; i < count; i++) {
        total += va_arg(args, int);
    }
    va_end(args);
    return total;
}
// sum(3, 10, 20, 30) == 60

static и inline

static для функции/переменной ограничивает её текущей единицей трансляции (файлом). static для локальной переменной делает её персистентной между вызовами (как глобальная, но ограниченная областью). inline предлагает компилятору встроить тело функции; современные компиляторы игнорируют это и решают сами.

c
// static: internal linkage (file-local)
static int counter = 0;
static int next_id(void) { return ++counter; }

// inline: hint to expand inline
static inline int square(int x) { return x * x; }

// static local: persists across calls
int call_count(void) {
    static int n = 0;
    return ++n;
}
06

Массивы и указатели

Массивы

Массивы фиксированного размера, индексируются с нуля и хранятся непрерывно в памяти. sizeof(arr)/sizeof(arr[0]) вычисляет длину, но работает только для реальных массивов, не указателей (массивы превращаются в указатели при передаче в функции, теряя информацию о размере).

c
int nums[5] = {1, 2, 3, 4, 5};
printf("%d\n", nums[0]);     // 1
printf("%d\n", nums[4]);     // 5

int len = sizeof(nums) / sizeof(nums[0]);  // 5

// Array of strings
char *fruits[] = {"apple", "banana", "cherry"};
printf("%s\n", fruits[1]);   // banana

Указатели

Указатели хранят адреса памяти. & получает адрес, * разыменовывает. Всегда инициализируйте указатели (используйте NULL, если ещё не присвоены). Разыменование NULL или неинициализированного указателя — неопределённое поведение (обычно краш). Проверяйте на NULL перед разыменованием.

c
int x = 10;
int *ptr = &x;     // ptr holds address of x
printf("%p\n", (void*)ptr);  // address
printf("%d\n", *ptr);        // 10 (dereference)

*ptr = 20;         // modify x through pointer
printf("%d\n", x);  // 20

int *p = NULL;     // null pointer (points to nothing)
if (p) { /* safe to dereference */ }

Арифметика указателей

Арифметика указателей масштабируется на размер элемента: p+1 перемещается к следующему элементу, а не следующему байту. Это делает p[i] эквивалентным *(p+i). Вычитание двух указателей на один массив даёт количество элементов. Арифметика указателей действительна только в пределах массива.

c
int arr[] = {10, 20, 30, 40, 50};
int *p = arr;       // points to arr[0]

printf("%d\n", *p);       // 10
printf("%d\n", *(p + 1)); // 20
printf("%d\n", *(p + 2)); // 30

p += 3;              // now points to arr[3]
printf("%d\n", *p);  // 40

int diff = (p - arr); // 3 (number of elements)

Массивы vs указатели

Имена массивов превращаются в указатели при передаче в функции или использовании в выражениях, теряя информацию о размере. Поэтому длину массива нужно передавать отдельно. sizeof(arr) даёт полный размер массива, только когда arr — истинный массив, а не указатель.

c
int arr[5] = {1, 2, 3, 4, 5};
int *ptr = arr;  // arr decays to &arr[0]

// These are equivalent
printf("%d\n", arr[2]);
printf("%d\n", ptr[2]);
printf("%d\n", *(arr + 2));

// But sizeof differs
printf("%zu\n", sizeof(arr));   // 20 (5 * 4 bytes)
printf("%zu\n", sizeof(ptr));   // 8 (pointer size)

Многомерные массивы

2D-массивы — массивы массивов, хранятся построчно. grid[i][j] обращается к строке i, столбцу j. При передаче в функции количество столбцов должно быть указано: void foo(int arr[][3], int rows). Для динамических 2D-массивов используйте массивы указателей.

c
int grid[2][3] = {
    {1, 2, 3},
    {4, 5, 6}
};

printf("%d\n", grid[0][1]);  // 2
printf("%d\n", grid[1][2]);  // 6

for (int i = 0; i < 2; i++) {
    for (int j = 0; j < 3; j++) {
        printf("%d ", grid[i][j]);
    }
    printf("\n");
}
07

Структуры и объединения

Структуры

Структуры группируют связанные переменные разных типов. Члены доступны через оператор точки (.). Инициализируйте через фигурные скобки. Структуры передаются по значению (копируются); передавайте по указателю (struct Point *) для избежания копирования и модификации оригинала.

c
struct Point {
    int x;
    int y;
};

struct Point p = {3, 4};
printf("(%d, %d)\n", p.x, p.y);  // (3, 4)

p.x = 10;
p.y = 20;
printf("(%d, %d)\n", p.x, p.y);  // (10, 20)

typedef

typedef создаёт алиас для типа, поэтому можно писать Student вместо struct Student. Обычно используется со структурами для упрощения синтаксиса. typedef может также алиасить типы указателей на функции, делая callback'и намного читаемее.

c
typedef struct {
    char name[50];
    int age;
    float gpa;
} Student;

Student s = {"Alice", 20, 3.8};
printf("%s: %d, GPA %.1f\n", s.name, s.age, s.gpa);

// typedef for other types
typedef unsigned long ulong;
typedef int (*CompareFn)(const void*, const void*);

Указатели на структуры

Когда есть указатель на структуру, используйте оператор стрелка (->) для доступа к членам. ptr->x — сокращение для (*ptr).x. Передавайте указатели на структуры в функции для эффективности (избегает копирования больших структур) и для возможности модификации.

c
typedef struct {
    int x, y;
} Point;

Point p = {3, 4};
Point *ptr = &p;

// Arrow operator (->) for pointer members
printf("%d\n", ptr->x);   // 3
ptr->y = 10;
printf("%d\n", p.y);      // 10

// Equivalent: (*ptr).x

Объединения

Объединения накладывают несколько типов на одну память — одновременно действителен только один член. Установка одного члена перезаписывает другие. Полезно для type punning (переинтерпретация битов) и экономии памяти, когда одновременно нужен только один из нескольких типов.

c
union Value {
    int i;
    float f;
    char bytes[4];
};

union Value v;
v.i = 65;
printf("%d\n", v.i);          // 65
printf("%c\n", v.bytes[0]);   // 'A' (same memory)

v.f = 3.14f;
printf("%d\n", v.i);  // reinterpreted bits!

Битовые поля и enum

Битовые поля упаковывают несколько маленьких полей в один int, экономя память (часто в протоколах и аппаратных регистрах). Enum определяют именованные целочисленные константы (0, 1, 2... по умолчанию). Используйте enum вместо #define для лучшей отладки и типобезопасности.

c
struct Flags {
    unsigned int bold : 1;
    unsigned int italic : 1;
    unsigned int size : 6;  // 0-63
};

struct Flags f = {1, 0, 12};
printf("bold=%d, size=%d\n", f.bold, f.size);

enum Color { RED, GREEN, BLUE };
enum Color c = GREEN;
printf("%d\n", c);  // 1
08

Управление памятью

malloc и free

malloc выделяет кучу и возвращает void-указатель (или NULL при неудаче). Всегда проверяйте на NULL. Каждый malloc должен быть в паре с free для предотвращения утечек памяти. Установка указателя в NULL после free предотвращает баги use-after-free.

c
#include <stdlib.h>
int *arr = malloc(5 * sizeof(int));
if (arr == NULL) {
    fprintf(stderr, "malloc failed\n");
    return 1;
}
for (int i = 0; i < 5; i++) arr[i] = i * 2;
free(arr);  // release memory
arr = NULL; // avoid dangling pointer

calloc и realloc

calloc выделяет и обнуляет память (безопаснее malloc, который содержит мусор). realloc меняет размер: может переместить блок, возвращая новый указатель. Если realloc не удался, он возвращает NULL, но оригинальный блок всё ещё валиден — используйте временный указатель для избежания утечек.

c
#include <stdlib.h>
// calloc: zero-initialized
int *arr = calloc(5, sizeof(int));  // all zeros

// realloc: resize
arr = realloc(arr, 10 * sizeof(int));
if (!arr) { /* handle failure, original still valid */ }

free(arr);

Стек vs куча

Стек автоматический (выделяется/освобождается с вызовами функций) и быстрый, но ограничен (часто 1-8 МБ). Куча управляется вручную через malloc/free, намного больше, но медленнее и склонна к утечкам. Используйте стек для маленьких, короткоживущих данных; кучу — для больших или долгоживущих.

c
// Stack: automatic, fast, limited size
int local_var = 42;
int arr[100];  // on the stack

// Heap: manual, large, slower
int *heap_arr = malloc(1000000 * sizeof(int));

// Stack frame is freed when function returns
// Heap memory persists until explicitly freed

Утечки памяти и висячие указатели

Утечки памяти возникают при потере единственного указателя на выделенную память (невозможно free). Висячие указатели указывают на освобождённую память — разыменование неопределённое поведение. Двойное освобождение также неопределённое поведение. Инструменты вроде Valgrind и AddressSanitizer обнаруживают эти баги.

c
// Memory leak: lost the pointer, can't free
void leak(void) {
    int *p = malloc(100 * sizeof(int));
    // function returns without free -> leaked!
}

// Dangling pointer: using freed memory
int *p = malloc(sizeof(int));
free(p);
*p = 42;  // UNDEFINED BEHAVIOR!

// Double free: also undefined
free(p);  // crash likely

Динамические массивы и строки

Динамическое выделение позволяет создавать строки/массивы, размер которых определяется во время выполнения. Вызывающий отвечает за освобождение памяти. Всегда выделяйте strlen+1 для строк (нульевой терминатор). Этот шаблон (выделить, вернуть, вызывающий освобождает) распространён в C-API.

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

// Dynamic string copy
char *dup_str(const char *s) {
    char *copy = malloc(strlen(s) + 1);  // +1 for null
    if (copy) strcpy(copy, s);
    return copy;  // caller must free
}

char *name = dup_str("Alice");
printf("%s\n", name);
free(name);
09

Файловый ввод-вывод

fopen и fclose

fopen открывает файл и возвращает FILE-указатель (или NULL при неудаче). Режимы: r (чтение), w (запись/усечение), a (добавление), r+ (чтение/запись), b (бинарный). Всегда проверяйте на NULL. fclose сбрасывает буферы и закрывает файл. fgets безопасно читает одну строку.

c
#include <stdio.h>
FILE *f = fopen("data.txt", "r");
if (!f) {
    perror("fopen failed");
    return 1;
}

char line[256];
while (fgets(line, sizeof(line), f)) {
    printf("%s", line);
}

fclose(f);

fprintf и fscanf

fprintf и fscanf работают как printf/scanf, но с файлами. fscanf хрупок — несоответствие формата вызывает проблемы. Для надёжного разбора читайте строки через fgets, затем разбирайте через sscanf. Всегда закрывайте файлы после завершения для сброса буферов и освобождения ресурсов.

c
FILE *f = fopen("output.txt", "w");
fprintf(f, "Name: %s\n", "Alice");
fprintf(f, "Age: %d\n", 30);
fclose(f);

FILE *in = fopen("output.txt", "r");
char name[50];
int age;
fscanf(in, "Name: %49s\n", name);
fscanf(in, "Age: %d\n", &age);
printf("%s, %d\n", name, age);
fclose(in);

fread и fwrite (бинарные)

fread/fwrite читают/пишут raw-байты — идеально для бинарных данных и структур. Аргументы: буфер, размер элемента, количество, файл. Бинарные файлы компактны, но непереносимы между архитектурами (порядок байтов, padding структур). Всегда открывайте бинарные файлы в режиме 'b'.

c
typedef struct { int id; float score; } Record;

Record r = {1, 95.5f};
FILE *f = fopen("data.bin", "wb");
fwrite(&r, sizeof(Record), 1, f);
fclose(f);

Record r2;
FILE *in = fopen("data.bin", "rb");
fread(&r2, sizeof(Record), 1, in);
printf("id=%d, score=%.1f\n", r2.id, r2.score);
fclose(in);

fseek, ftell и rewind

fseek перемещает файловую позицию: SEEK_SET (от начала), SEEK_CUR (относительно), SEEK_END (от конца). ftell возвращает текущую позицию. rewind — сокращение для fseek(f, 0, SEEK_SET). Они обеспечивают произвольный доступ в файлах, полезный для БД и индексного поиска.

c
FILE *f = fopen("data.txt", "r");
fseek(f, 0, SEEK_END);   // jump to end
long size = ftell(f);     // get position = file size
printf("Size: %ld bytes\n", size);

rewind(f);                // back to start
// or: fseek(f, 0, SEEK_SET);

fseek(f, 10, SEEK_SET);   // 10 bytes from start
char c = fgetc(f);
printf("Char at 10: %c\n", c);
fclose(f);

stderr и стандартные потоки

Каждая C-программа имеет три потока: stdin (клавиатура), stdout (экран), stderr (экран, небуферизованный). Запись ошибок в stderr отделяет их от нормального вывода, обеспечивая перенаправление: program 2> errors.log. stderr небуферизованный, поэтому сообщения появляются до крашей.

c
#include <stdio.h>
// Three standard streams: stdin, stdout, stderr
fprintf(stdout, "Normal output\n");
fprintf(stderr, "Error: something went wrong\n");

int c;
while ((c = fgetc(stdin)) != EOF) {
    fputc(c, stdout);  // echo input
}

// stderr is unbuffered (appears immediately)
// stdout is line-buffered (flushes on newline)
10

Препроцессор и макросы

Константы и макросы #define

#define создаёт макросы текстовой подстановки. Константы вроде PI улучшают читаемость и поддерживаемость. Макросы-функции должны оборачивать параметры в скобки для предотвращения багов приоритета: SQUARE(2+3) без скобок был бы 2+3*2+3=11. Предпочитайте const-переменные и inline-функции макросам.

c
#define MAX_SIZE 100
#define PI 3.14159
#define VERSION "2.0"

// Function-like macro
#define SQUARE(x) ((x) * (x))
#define MAX(a, b) ((a) > (b) ? (a) : (b))

int area = SQUARE(5);    // 25
int big = MAX(3, 7);     // 7

Условная компиляция

Условная компиляция (#if, #ifdef, #ifndef) включает/исключает код на этапе компиляции. Используется для платформо-специфичного кода, отладочных сборок и feature-флагов. #ifdef проверяет, определён ли макрос; #if вычисляет его значение. #elif и #else предоставляют альтернативы.

c
#define DEBUG 1

#if DEBUG
    printf("Debug: x=%d\n", x);
#endif

#ifdef _WIN32
    // Windows-specific code
#elif defined(__linux__)
    // Linux-specific code
#endif

#ifndef BUFFER_SIZE
#define BUFFER_SIZE 1024
#endif

Include guards

Include guards предотвращают двойное включение заголовков, что вызывало бы ошибки переопределения. Шаблон #ifndef/#define/#endif — стандартный C. #pragma once — более простая, широко поддерживаемая альтернатива (не стандартная, но работает во всех основных компиляторах).

c
// myheader.h
#ifndef MYHEADER_H
#define MYHEADER_H

struct Point { int x, y; };
void init_point(struct Point *p);

#endif // MYHEADER_H

// Alternative (non-standard but widely supported):
#pragma once

#pragma и подсказки компилятору

#pragma предоставляет специфичные для компилятора директивы. #pragma once — более простой include guard. #pragma pack управляет расположением структуры в памяти (важно для бинарных протоколов). __attribute__ (GCC/Clang) аннотирует функции для оптимизации, депрекации и предупреждений.

c
#pragma once              // include guard
#pragma pack(1)            // struct packing (no padding)
#pragma GCC diagnostic ignored "-Wunused-variable"

// Common pragmas
#pragma message("Compiling " __FILE__)

// C99 __attribute__ (GCC/Clang)
__attribute__((deprecated)) void old_func(void);
__attribute__((noreturn)) void fatal(void);

Строкирование и склейка токенов

# (строкирование) превращает аргумент макроса в строковый литерал. ## (склейка токенов) объединяет токены в новый идентификатор. Двухуровневый шаблон STR/XSTR обеспечивает раскрытие макросов перед строкированием. Используется в генерации кода и логирующих макросах.

c
#define STR(x) #x
#define XSTR(x) STR(x)
#define CONCAT(a, b) a##b

printf("%s\n", STR(Hello World));  // "Hello World"
printf("%s\n", XSTR(VERSION));      // expands VERSION first

int CONCAT(foo, bar) = 42;  // creates variable foobar
printf("%d\n", foobar);     // 42
11

Битовые операции

Базовые побитовые операторы

Побитовые операторы манипулируют отдельными битами. AND (&) маскирует биты (оставляет только установленные), OR (|) устанавливает биты, XOR (^) переключает биты, NOT (~) инвертирует все биты. Сдвиг влево (<<) умножает на степени 2, сдвиг вправо (>>) делит (для unsigned). Всегда используйте unsigned-типы для битовых манипуляций — знаковый сдвиг вправо implementation-defined (может расширять знак). Битовые операции крайне быстры (один CPU-цикл) и используются во флагах, аппаратных регистрах, сжатии и криптографии. Бинарные литералы (префикс 0b) — C23/C++14; используйте hex (0x) или десятичные в старом C.

c
#include <stdio.h>

int main() {
    unsigned int a = 0b1100;  // 12
    unsigned int b = 0b1010;  // 10

    // AND: both bits must be 1
    printf("%u\n", a & b);   // 8  (0b1000)

    // OR: either bit is 1
    printf("%u\n", a | b);   // 14 (0b1110)

    // XOR: bits differ (exclusive or)
    printf("%u\n", a ^ b);   // 6  (0b0110)

    // NOT: flip all bits
    printf("%u\n", ~a);      // 4294967283 (on 32-bit)

    // Left shift: multiply by 2^n
    printf("%u\n", a << 2);  // 48 (12 * 4)

    // Right shift: divide by 2^n (unsigned)
    printf("%u\n", a >> 1);  // 6  (12 / 2)

    return 0;
}

Установка, очистка и переключение битов

Битовые флаги упаковывают несколько булевых опций в одно целое, экономя память. Три основные операции: SET (|= mask), CLEAR (&= ~mask), TOGGLE (^= mask), CHECK (& mask). Используйте #define с (1 << n) для читаемых имён флагов. Этот шаблон повсеместен в системном программировании (права файлов, управление устройствами, опции конфигурации). Например, права файлов Unix (rwxr-xr-x = 0755) используют битовые флаги. Всегда используйте unsigned-целые для флагов во избежание проблем расширения знака. Это эффективнее по памяти, чем массив bool (1 бит vs 8 бит на флаг).

c
#include <stdio.h>

// Flag definitions (powers of 2)
#define FLAG_READ    (1 << 0)  // 0b0001
#define FLAG_WRITE   (1 << 1)  // 0b0010
#define FLAG_EXECUTE (1 << 2)  // 0b0100
#define FLAG_ADMIN   (1 << 3)  // 0b1000

int main() {
    unsigned int permissions = 0;

    // SET a bit (OR with mask)
    permissions |= FLAG_READ | FLAG_WRITE;  // 0b0011

    // CHECK if a bit is set (AND, compare to 0)
    if (permissions & FLAG_READ) {
        printf("Read permission granted\n");
    }

    // CLEAR a bit (AND with inverted mask)
    permissions &= ~FLAG_WRITE;  // 0b0001

    // TOGGLE a bit (XOR with mask)
    permissions ^= FLAG_EXECUTE;  // 0b0101 (execute now on)
    permissions ^= FLAG_EXECUTE;  // 0b0001 (execute now off)

    // SET multiple bits at once
    permissions = FLAG_READ | FLAG_EXECUTE | FLAG_ADMIN;

    printf("Permissions: 0x%X\n", permissions);  // 0xD
    return 0;
}

Трюки битовых манипуляций

Битовые трюки используют бинарное представление для скорости. x & 1 проверяет чётность/нечётность (быстрее модуля). x & (x-1) очищает младший установленный бит — полезно для проверки степеней 2 и подсчёта битов. __builtin_popcount (GCC/Clang) или __popcnt (MSVC) считают установленные биты одной инструкцией на современных CPU. XOR-обмен (a^=b; b^=a; a^=b) избегает временной переменной, но медленнее на современных CPU и менее читаем — избегайте его. Трюк 'округление вверх до степени 2' распространяет старший установленный бит на все младшие биты, затем добавляет 1. Эти трюки полезны во встраиваемых системах, игровых движках и критичном к производительности коде.

c
#include <stdio.h>

int main() {
    int x = 42;

    // Check if odd/even (faster than x % 2)
    if (x & 1) printf("odd\n"); else printf("even\n");

    // Check if power of 2 (only one bit set)
    // x & (x-1) clears the lowest set bit
    if (x && !(x & (x - 1))) printf("power of 2\n");

    // Count set bits (popcount / Hamming weight)
    unsigned int n = 0b10110110;
    int count = 0;
    while (n) { count += n & 1; n >>= 1; }
    printf("Set bits: %d\n", count);  // 6
    // Or use __builtin_popcount(n) (GCC/Clang)

    // Swap two values without temp (XOR swap)
    int a = 5, b = 10;
    a ^= b; b ^= a; a ^= b;
    // a=10, b=5 (avoid in practice — less readable)

    // Get lowest set bit
    unsigned int lowest = x & (-x);  // isolates lowest 1-bit

    // Round up to next power of 2
    unsigned int v = 5;
    v--; v |= v >> 1; v |= v >> 2; v |= v >> 4; v |= v >> 8; v |= v >> 16; v++;

    return 0;
}

Битовые поля в структурах

Битовые поля упаковывают несколько маленьких значений в одну структуру, экономя память. Синтаксис двоеточия (unsigned int field : N) задаёт ширину в битах. Компилятор автоматически обрабатывает извлечение/вставку битов. Это полезно для систем с ограниченной памятью, сетевых протоколов и маппинга аппаратных регистров. Однако расположение битовых полей implementation-defined (порядок байтов, выравнивание, padding) — не используйте битовые поля для кроссплатформенной бинарной совместимости. Используйте явное битовое маскирование (#define + & |) для переносимых бинарных форматов. Безымянные поля (: 5) добавляют padding. Общий размер округляется до выравнивания структуры.

c
#include <stdio.h>

// Bit fields: pack multiple small fields into one int
struct Date {
    unsigned int day   : 5;   // 0-31  (5 bits)
    unsigned int month : 4;   // 0-15  (4 bits)
    unsigned int year  : 12;  // 0-4095 (12 bits)
    unsigned int is_leap : 1; // 0 or 1 (1 bit)
};  // Total: 22 bits (padded to 32)

struct Flags {
    unsigned int visible  : 1;
    unsigned int editable : 1;
    unsigned int locked   : 1;
    unsigned int          : 5;  // unnamed padding (5 bits)
    unsigned int priority : 4;  // 0-15
};

int main() {
    struct Date d = { 15, 6, 2024, 0 };
    printf("Size: %zu bytes\n", sizeof(d));  // 4 bytes
    printf("Date: %u/%u/%u\n", d.day, d.month, d.year);

    struct Flags f = { .visible = 1, .editable = 0, .locked = 1, .priority = 7 };
    printf("Size: %zu bytes\n", sizeof(f));  // 4 bytes

    return 0;
}

Практическая битовая манипуляция (RGB-цвет)

Упаковка нескольких значений в одно целое распространена в графике, сетях и встраиваемых системах. RGB-цвета упаковывают три 8-битных канала в 24 бита (0xRRGGBB). Сдвиг влево (<<) позиционирует каждый канал, OR (|) объединяет их. Сдвиг вправо (>>) и маскирование (& 0xFF) извлекают отдельные каналы. Это экономит память (1 int vs 3 байта) и обеспечивает атомарные операции. Тот же шаблон применяется к сетевому порядку байтов, доступу к аппаратным регистрам и сжатию данных. Всегда используйте типы фиксированной ширины (uint8_t, uint32_t) для переносимости — размер int зависит от платформы.

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

// Pack RGB into a single 32-bit integer (0xRRGGBB)
uint32_t make_color(uint8_t r, uint8_t g, uint8_t b) {
    return ((uint32_t)r << 16) | ((uint32_t)g << 8) | b;
}

// Extract components
uint8_t get_red(uint32_t color)   { return (color >> 16) & 0xFF; }
uint8_t get_green(uint32_t color) { return (color >> 8)  & 0xFF; }
uint8_t get_blue(uint32_t color)  { return color & 0xFF; }

// Blend two colors (50/50 mix)
uint32_t blend(uint32_t c1, uint32_t c2) {
    uint8_t r = (get_red(c1) + get_red(c2)) / 2;
    uint8_t g = (get_green(c1) + get_green(c2)) / 2;
    uint8_t b = (get_blue(c1) + get_blue(c2)) / 2;
    return make_color(r, g, b);
}

int main() {
    uint32_t red   = make_color(255, 0, 0);    // 0xFF0000
    uint32_t blue  = make_color(0, 0, 255);    // 0x0000FF
    uint32_t purple = blend(red, blue);          // 0x7F007F

    printf("Red:   0x%06X\n", red);
    printf("Blue:  0x%06X\n", blue);
    printf("Mix:   0x%06X (R=%d G=%d B=%d)\n",
           purple, get_red(purple), get_green(purple), get_blue(purple));

    return 0;
}
12

Обработка сигналов

Базовая обработка сигналов

Сигналы — программные прерывания, посылаемые процессу (например, Ctrl+C посылает SIGINT, деление на ноль — SIGFPE). signal() регистрирует функцию-обработчик. Внутри обработчика разрешены только async-signal-safe функции — printf, malloc и большинство stdlib-функций НЕ безопасны, так как основная программа может быть прервана в середине вызова. Используйте write() для вывода. Частые сигналы: SIGINT (Ctrl+C), SIGTERM (запрос завершения), SIGKILL (принудительное завершение, нельзя перехватить), SIGSEGV (segfault), SIGALRM (таймер). Предпочитайте sigaction() вместо signal() для переносимости и контроля.

c
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

// Signal handler function (must match signature)
void handler(int sig) {
    // WARNING: only async-signal-safe functions allowed here!
    // printf is NOT safe — use write() instead
    const char *msg = "Caught SIGINT\n";
    write(STDOUT_FILENO, msg, 14);
}

int main() {
    // Register handler for Ctrl+C (SIGINT)
    signal(SIGINT, handler);

    // Ignore SIGINT entirely
    // signal(SIGINT, SIG_IGN);

    // Reset to default behavior (terminate)
    // signal(SIGINT, SIG_DFL);

    printf("PID %d waiting. Press Ctrl+C...\n", getpid());
    while (1) {
        sleep(1);
    }
    return 0;
}

sigaction (переносимая обработка сигналов)

sigaction() — современный, переносимый способ обработки сигналов (поведение signal() зависит от платформы). Обработчик sa_sigaction получает siginfo_t с деталями: si_pid (PID отправителя), si_uid (UID отправителя), si_signo (номер сигнала), si_code (причина). Флаг SA_SIGINFO включает трёхаргументный обработчик. sa_mask блокирует указанные сигналы во время выполнения обработчика (предотвращает вложенные прерывания). Другие флаги: SA_RESTART (авто-перезапуск прерванных syscall), SA_NOCLDWAIT (нет zombie-детей). Всегда используйте sigaction() в продакшен-коде — signal() ненадёжен на некоторых платформах.

c
#include <stdio.h>
#include <signal.h>
#include <string.h>

void handler(int sig, siginfo_t *info, void *context) {
    // siginfo_t provides details about the signal
    printf("Signal %d from PID %d\n", sig, info->si_pid);
}

int main() {
    struct sigaction sa;
    memset(&sa, 0, sizeof(sa));

    sa.sa_sigaction = handler;  // use sa_sigaction (not sa_handler)
    sa.sa_flags = SA_SIGINFO;   // enable siginfo_t parameter

    // Block other signals during handler execution
    sigemptyset(&sa.sa_mask);
    sigaddset(&sa.sa_mask, SIGQUIT);  // block SIGQUIT during handler

    // Register (more portable than signal())
    sigaction(SIGINT, &sa, NULL);

    // Send a signal to self
    raise(SIGINT);  // like kill(getpid(), SIGINT)

    printf("Done\n");
    return 0;
}

Отправка сигналов и alarm

alarm(seconds) планирует SIGALRM через указанное время — полезно для тайм-аутов. pause() блокирует до поступления любого сигнала. volatile sig_atomic_t — единственный безопасный способ разделить данные между обработчиком сигнала и основным кодом — volatile предотвращает оптимизацию компилятором, sig_atomic_t гарантирует атомарный доступ. kill(pid, signal) посылает сигнал другому процессу. raise(sig) посылает сигнал себе. SIGKILL (9) и SIGSTOP нельзя перехватить или игнорировать — они всегда работают. SIGTERM (15) — вежливый запрос завершения (программы могут перехватить его для очистки). Используйте alarm() для простых тайм-аутов; используйте setitimer()/timer_create() для большего контроля.

c
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

volatile sig_atomic_t got_alarm = 0;

void alarm_handler(int sig) {
    got_alarm = 1;  // safe: sig_atomic_t is atomic
}

int main() {
    signal(SIGALRM, alarm_handler);

    // Set a timer: deliver SIGALRM after 3 seconds
    alarm(3);
    printf("Waiting for alarm...\n");

    // Wait for the alarm
    while (!got_alarm) {
        pause();  // sleep until any signal arrives
    }
    printf("Alarm fired!\n");

    // Send signal to another process
    // kill(pid, SIGTERM);  // request termination
    // kill(pid, SIGKILL);  // force kill (can't be caught)

    // Send signal to self
    raise(SIGUSR1);

    return 0;
}

Справочник частых сигналов

Понимание сигналов необходимо для Unix-программирования. SIGKILL (9) и SIGSTOP нельзя перехватить — это последнее средство. SIGTERM — стандартный сигнал корректного завершения (перехватывайте для сохранения состояния). SIGINT — Ctrl+C (интерактивное прерывание). SIGCHLD срабатывает при выходе дочернего процесса — если не wait() для него, дочерний процесс становится zombie. Установка SIGCHLD в SIG_IGN авто-собирает дочерние процессы (или используйте SA_NOCLDWAIT). SIGPIPE срабатывает при записи в закрытый pipe/socket — большинство серверов игнорируют его (signal(SIGPIPE, SIG_IGN)) и проверяют возвращаемые значения write(). Используйте _exit() (не exit()) в обработчиках сигналов — exit() запускает atexit-обработчики, которые могут быть не signal-safe.

c
#include <signal.h>

// Common POSIX signals:
// SIGINT  (2)  - Ctrl+C interrupt (terminate)
// SIGQUIT (3)  - Ctrl+\ quit (core dump)
// SIGKILL (9)  - Force kill (CANNOT be caught/ignored)
// SIGSEGV (11) - Segmentation fault (invalid memory access)
// SIGPIPE (13) - Write to broken pipe
// SIGTERM (15) - Termination request (graceful shutdown)
// SIGSTOP (19) - Pause process (CANNOT be caught/ignored)
// SIGCONT (18) - Resume paused process
// SIGCHLD (17) - Child process exited
// SIGALRM (14) - Timer alarm
// SIGUSR1 (10) - User-defined signal 1
// SIGUSR2 (12) - User-defined signal 2
// SIGFPE  (8)  - Arithmetic error (divide by zero)
// SIGBUS  (7)  - Bus error (misaligned access)

// Graceful shutdown pattern:
void cleanup_handler(int sig) {
    // Save state, close files, release resources
    // Then exit cleanly
    _exit(0);  // use _exit in signal handlers (not exit)
}

// In main:
signal(SIGTERM, cleanup_handler);
signal(SIGINT, cleanup_handler);

// Prevent zombie children:
signal(SIGCHLD, SIG_IGN);  // auto-reap children

Трюк self-pipe (signal-safe пробуждение)

Трюк self-pipe решает фундаментальную проблему: обработчики сигналов не могут безопасно выполнять сложную работу, но нужно реагировать на сигналы в главном цикле. Решение: обработчик записывает байт в pipe, а главный цикл использует select()/poll() для обнаружения. Это интегрирует сигналы с циклом событий безопасно. Обработчик вызывает только write() (async-signal-safe). Современные альтернативы: signalfd() (специфично для Linux, превращает сигналы в файловые дескрипторы напрямую) или pselect() (атомарно блокирует сигналы во время select). Этот шаблон используется в событийно-ориентированных серверах (nginx, Redis) для обработки сигналов без состояний гонки.

c
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <fcntl.h>

int pipe_fd[2];  // [0]=read, [1]=write

void handler(int sig) {
    // Write one byte to the pipe — wakes up select()/poll()
    write(pipe_fd[1], &sig, sizeof(sig));
}

int main() {
    pipe(pipe_fd);
    // Make read end non-blocking
    fcntl(pipe_fd[0], F_SETFL, O_NONBLOCK);

    signal(SIGINT, handler);
    signal(SIGTERM, handler);

    printf("Waiting (select-based)...\n");

    while (1) {
        fd_set readfds;
        FD_ZERO(&readfds);
        FD_SET(pipe_fd[0], &readfds);

        // select() blocks until pipe is writable (signal received)
        int ready = select(pipe_fd[0] + 1, &readfds, NULL, NULL, NULL);
        if (ready > 0 && FD_ISSET(pipe_fd[0], &readfds)) {
            int sig;
            read(pipe_fd[0], &sig, sizeof(sig));
            printf("Handled signal %d in main loop\n", sig);
            if (sig == SIGTERM) break;
        }
    }
    return 0;
}
13

Разветвление процессов и exec

Основы fork()

fork() создаёт точную копию текущего процесса — единственное отличие — возвращаемое значение: 0 в дочернем, PID дочернего в родительском. Оба процесса продолжают с вызова fork(). Дочерний получает копию памяти родителя (copy-on-write оптимизирует это). Всегда проверяйте все три случая: pid < 0 (ошибка), pid == 0 (дочерний), pid > 0 (родительский). waitpid() блокирует до выхода дочернего и получает его статус. WIFEXITED проверяет, вышел ли он нормально, WEXITSTATUS получает код выхода. Если не wait(), дочерний процесс становится zombie до сбора.

c
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main() {
    pid_t pid = fork();  // create a child process

    if (pid < 0) {
        perror("fork failed");
        return 1;
    } else if (pid == 0) {
        // CHILD process (fork returned 0)
        printf("Child: PID=%d, Parent PID=%d\n",
               getpid(), getppid());
        sleep(2);
        printf("Child exiting\n");
        return 42;  // child exit code
    } else {
        // PARENT process (fork returned child's PID)
        printf("Parent: PID=%d, Child PID=%d\n",
               getpid(), pid);

        int status;
        waitpid(pid, &status, 0);  // wait for child

        if (WIFEXITED(status)) {
            printf("Child exited with code %d\n",
                   WEXITSTATUS(status));
        }
    }
    return 0;
}

Семейство exec (замена образа процесса)

exec заменяет текущий образ процесса новой программой — PID остаётся, но код, данные и стек заменяются. exec возвращает только при неудаче. Соглашение об именовании: 'l' = список аргументов (вариативный, NULL-терминированный), 'v' = массив аргументов, 'p' = искать в PATH исполняемый файл, 'e' = кастомное окружение. Первый аргумент по соглашению — имя программы (argv[0]). fork()+exec() — Unix-способ запуска программ — fork создаёт процесс, exec загружает новую программу. Это разделение позволяет настроить файловые дескрипторы, окружение и сигналы между fork и exec.

c
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main() {
    pid_t pid = fork();

    if (pid == 0) {
        // CHILD: replace self with a new program
        // exec never returns on success (only on failure)

        // execlp: search PATH, list arguments
        execlp("ls", "ls", "-la", "/tmp", NULL);

        // execvp: search PATH, array of arguments
        char *args[] = {"ls", "-la", "/tmp", NULL};
        execvp("ls", args);

        // execl: full path, list arguments
        execl("/bin/ls", "ls", "-la", NULL);

        // Only reached if exec failed
        perror("exec failed");
        _exit(1);
    } else {
        wait(NULL);  // parent waits for child
        printf("Child finished\n");
    }
    return 0;
}

// exec variants:
// execl  (path, arg1, arg2, ..., NULL)        — list args, full path
// execlp (file, arg1, arg2, ..., NULL)        — list args, search PATH
// execv  (path, argv[])                        — array args, full path
// execvp (file, argv[])                        — array args, search PATH
// execve (path, argv[], envp[])                — array args, custom env

Zombie- и orphan-процессы

Zombie возникают, когда дочерний процесс выходит, а родитель не вызвал wait() — ядро сохраняет запись таблицы процессов (PID, статус выхода) до сбора. Zombie тратят PID и могут исчерпать таблицу процессов. Решение: всегда wait() для дочерних процессов или установите SIGCHLD в SIG_IGN (ядро авто-собирает). Orphan возникают, когда родитель выходит раньше дочернего — init/systemd (PID 1) усыновляет orphan и собирает его при выходе. Шаблон двойного fork (fork, дочерний fork'ает снова, первый дочерний выходит) создаёт daemon, который автоматически переподчиняется init, отсоединяясь от терминала. Мониторьте zombie через 'ps aux | grep Z' или 'top'.

c
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main() {
    pid_t pid = fork();

    if (pid == 0) {
        printf("Child PID=%d\n", getpid());
        _exit(0);  // child exits immediately
    }

    // If parent doesn't wait(), child becomes a ZOMBIE
    // (process table entry remains until reaped)
    sleep(5);  // parent sleeps — child is now a zombie
    // Run 'ps' during this window to see the zombie (state 'Z')

    // Reap the zombie:
    int status;
    waitpid(pid, &status, 0);
    printf("Zombie reaped\n");

    // Orphan: if parent exits before child
    pid_t pid2 = fork();
    if (pid2 == 0) {
        sleep(3);  // parent will exit first
        printf("Orphan adopted by init (PID 1), new parent=%d\n",
               getppid());
        _exit(0);
    }
    // Parent exits immediately — child becomes orphan
    // init/systemd (PID 1) adopts and reaps it

    // Prevent zombies: ignore SIGCHLD
    // signal(SIGCHLD, SIG_IGN);  // kernel auto-reaps children
    // Or use SA_NOCLDWAIT with sigaction

    return 0;
}

Создание daemon-процесса

Daemon'ы — фоновые процессы, работающие без терминала (например, веб-серверы, БД). Шаги daemonization: fork+exit для отсоединения от shell, setsid() для создания новой сессии (без управляющего терминала), fork снова для безопасности, chdir('/') для избежания удержания файловой системы, set umask для предсказуемых прав файлов и close/redirect stdio в /dev/null. Двойной fork — Unix-соглашение для предотвращения повторного захвата daemon'ом терминала через open(). Современные системы предоставляют systemd service files для управления daemon'ами, но понимание ручной daemonization всё ещё важно для встраиваемых систем и переносимого кода. Логируйте в файлы (не stdout), так как stdout — /dev/null.

c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>

void daemonize() {
    // 1. Fork and exit parent (child continues in background)
    pid_t pid = fork();
    if (pid > 0) exit(0);  // parent exits
    if (pid < 0) exit(1);

    // 2. Create new session (detach from controlling terminal)
    setsid();

    // 3. Fork again (prevent reacquiring a terminal)
    pid = fork();
    if (pid > 0) exit(0);
    if (pid < 0) exit(1);

    // 4. Change working directory to / (don't hold filesystem)
    chdir("/");

    // 5. Set umask to 0 (full control over file permissions)
    umask(0);

    // 6. Close standard file descriptors (detach from terminal)
    close(STDIN_FILENO);
    close(STDOUT_FILENO);
    close(STDERR_FILENO);

    // 7. Redirect them to /dev/null (in case code writes to them)
    open("/dev/null", O_RDWR);  // fd 0 = stdin
    dup(0);                      // fd 1 = stdout
    dup(0);                      // fd 2 = stderr
}

int main() {
    daemonize();
    // Now running as a daemon (background, no terminal)
    while (1) {
        // Daemon work here (e.g., log to file, listen on socket)
        sleep(60);
    }
    return 0;
}

Обзор межпроцессного взаимодействия (IPC)

IPC позволяет процессам взаимодействовать. Pipes — простейшие (родитель-дочерний, однонаправленные). Именованные pipes (FIFO) работают между несвязанными процессами через путь файловой системы. Разделяемая память — самая быстрая (zero-copy), но требует синхронизации (семафоры/mutex). Сокеты — самые гибкие (двунаправленные, сетевые). Очереди сообщений обеспечивают структурированное, message-boundary общение. Сигналы — минимальны (только число). Выбирайте по потребностям: pipes для простого родитель-дочернего, разделяемая память для высокопроизводительного разделения данных, сокеты для сетевого общения. System V IPC (shmget, semget) старше; POSIX IPC (shm_open, sem_open) чище, но менее универсально доступно.

c
#include <stdio.h>
// C provides several IPC mechanisms:

// 1. PIPES: unidirectional byte stream between parent/child
//    pipe(fd) creates fd[0]=read, fd[1]=write
//    Only works between related processes (fork)

// 2. NAMED PIPES (FIFOs): like pipes but have a filesystem path
//    mkfifo("/tmp/myfifo", 0666);
//    Works between unrelated processes

// 3. SHARED MEMORY: fastest IPC (both processes access same RAM)
//    shmget/shmat (System V) or shm_open/mmap (POSIX)

// 4. MESSAGE QUEUES: structured messages (not byte stream)
//    msgget/msgsnd/msgrcv (System V) or mq_open (POSIX)

// 5. SEMAPHORES: synchronization (not data transfer)
//    semget/semop (System V) or sem_open (POSIX)

// 6. SOCKETS: bidirectional, works across machines (network)
//    socket/bind/listen/accept/connect

// 7. SIGNALS: minimal data (just signal number)
//    kill(pid, SIGUSR1)

// Choosing IPC:
// - Same machine, related processes → pipes
// - Same machine, unrelated processes → named pipes, shared memory
// - Different machines → sockets
// - Need synchronization → semaphores, mutexes
// - Need structured messages → message queues
14

Каналы и IPC

Анонимные каналы (родитель-дочерний)

Pipes обеспечивают однонаправленное общение между связанными процессами (созданными fork). pipe(fd) создаёт два файловых дескриптора: fd[0] для чтения, fd[1] для записи. Критично: закрывайте неиспользуемый конец в каждом процессе — родитель закрывает конец чтения, дочерний закрывает конец записи. Если конец записи не закрыт, read() дочернего блокируется навсегда (ожидая больше данных). read() возвращает 0 (EOF), только когда все концы записи закрыты. Pipes имеют фиксированный буфер (обычно 64 КБ) — write() блокируется, если буфер полон. Pipes идеальны для родитель-дочернего общения и конвейеризации shell-команд (ls | grep).

c
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>

int main() {
    int fd[2];  // fd[0]=read end, fd[1]=write end
    pipe(fd);   // create pipe

    pid_t pid = fork();

    if (pid == 0) {
        // CHILD: read from pipe
        close(fd[1]);  // close unused write end

        char buf[256];
        int n = read(fd[0], buf, sizeof(buf));
        buf[n] = '\0';
        printf("Child received: %s", buf);

        close(fd[0]);
    } else {
        // PARENT: write to pipe
        close(fd[0]);  // close unused read end

        const char *msg = "Hello from parent!\n";
        write(fd[1], msg, strlen(msg));

        close(fd[1]);  // close write end → child's read returns 0 (EOF)
        wait(NULL);
    }
    return 0;
}

Именованные каналы (FIFO)

Именованные pipes (FIFO) — pipes с именем файловой системы — они работают между несвязанными процессами. mkfifo() создаёт файл pipe; open() блокирует, пока и читатель, и писатель не подключены (синхронизация встроена). FIFO персистентны до unlink (в отличие от анонимных pipes, исчезающих при выходе процессов). Полезны для простого IPC между отдельными программами. Блокирующее поведение open() гарантирует, что писатель не стартует, пока читатель не готов. Используйте O_NONBLOCK для неблокирующего open. FIFO однонаправленные — для двунаправленного общения используйте два FIFO или сокеты. Именованные pipes часто используются в shell-скриптах и системных сервисах.

c
#include <stdio.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#include <string.h>

// Process A (writer):
int writer_main() {
    mkfifo("/tmp/myfifo", 0666);  // create named pipe

    int fd = open("/tmp/myfifo", O_WRONLY);
    write(fd, "Hello via FIFO!", 15);
    close(fd);
    return 0;
}

// Process B (reader) — can be a completely separate program:
int reader_main() {
    int fd = open("/tmp/myfifo", O_RDONLY);
    char buf[256];
    int n = read(fd, buf, sizeof(buf));
    buf[n] = '\0';
    printf("Received: %s\n", buf);
    close(fd);
    return 0;
}

// Named pipes persist in the filesystem (use unlink to remove):
// unlink("/tmp/myfifo");

// open() blocks until BOTH a reader and writer are connected
// (unless O_NONBLOCK is used)

Разделяемая память (самый быстрый IPC)

Разделяемая память — самый быстрый IPC — оба процесса мапят одну физическую RAM, поэтому передача данных zero-copy. shmget() создаёт сегмент, shmat() подключает его к адресному пространству процесса, shmdt() отключает, shmctl(IPC_RMID) уничтожает. Критическое предупреждение: разделяемая память НЕ обеспечивает синхронизацию — если оба процесса обращаются одновременно, возникают гонки данных. ВЫ ДОЛЖНЫ использовать семафоры, mutex или другую синхронизацию для координации доступа. ftok() генерирует ключ из пути файла (оба процесса должны согласовать ключ). Всегда уничтожайте разделяемую память после завершения (она персистентна после выхода процессов, утекая память). POSIX shared memory (shm_open/mmap) — более чистая альтернатива.

c
#include <stdio.h>
#include <sys/shm.h>
#include <sys/ipc.h>
#include <string.h>
#include <unistd.h>

#define SHM_SIZE 1024

int main() {
    key_t key = ftok("/tmp/shmfile", 65);  // generate unique key

    // Create shared memory segment
    int shmid = shmget(key, SHM_SIZE, 0666 | IPC_CREAT);

    pid_t pid = fork();

    if (pid == 0) {
        // CHILD: attach and read
        char *shared = (char *)shmat(shmid, NULL, 0);
        sleep(1);  // wait for parent to write
        printf("Child reads: %s\n", shared);
        shmdt(shared);  // detach
    } else {
        // PARENT: attach and write
        char *shared = (char *)shmat(shmid, NULL, 0);
        strcpy(shared, "Hello from shared memory!");
        printf("Parent wrote to shared memory\n");
        shmdt(shared);
        wait(NULL);

        // Destroy shared memory after use
        shmctl(shmid, IPC_RMID, NULL);
    }
    return 0;
}

// WARNING: shared memory has NO synchronization!
// Use semaphores or mutexes to prevent race conditions.

dup2 и перенаправление

dup2(oldfd, newfd) делает newfd копией oldfd — так работает shell-перенаправление. Для перенаправления stdout в pipe: dup2(pipe_write, STDOUT_FILENO) — теперь printf/write в stdout идёт в pipe. Для перенаправления stdin из pipe: dup2(pipe_read, STDIN_FILENO) — теперь scanf/read из stdin берётся из pipe. Это именно так shell реализует pipes (ls | sort), перенаправление (ls > file) и ввод (sort < file). После dup2 закройте оригинальный fd (он продублирован). Этот шаблон фундаментален для программного построения Unix-конвейеров и используется shell'ами, popen() и библиотеками управления процессами.

c
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/wait.h>

// Implement shell-like pipeline: ls | sort
int main() {
    int pipefd[2];
    pipe(pipefd);

    pid_t pid1 = fork();
    if (pid1 == 0) {
        // First child: ls (writes to pipe instead of stdout)
        close(pipefd[0]);                    // close read end
        dup2(pipefd[1], STDOUT_FILENO);      // stdout → pipe write
        close(pipefd[1]);                    // close original (dup'd)
        execlp("ls", "ls", NULL);
    }

    pid_t pid2 = fork();
    if (pid2 == 0) {
        // Second child: sort (reads from pipe instead of stdin)
        close(pipefd[1]);                    // close write end
        dup2(pipefd[0], STDIN_FILENO);       // stdin → pipe read
        close(pipefd[0]);                    // close original (dup'd)
        execlp("sort", "sort", NULL);
    }

    // Parent: close both ends and wait
    close(pipefd[0]);
    close(pipefd[1]);
    waitpid(pid1, NULL, 0);
    waitpid(pid2, NULL, 0);

    return 0;
}

popen (высокоуровневый pipe)

popen() — высокоуровневая обёртка над fork+pipe+exec+shell — запускает команду через /bin/sh и возвращает FILE* для чтения вывода ('r') или записи ввода ('w'). Гораздо проще ручного fork/pipe/exec, но проходит через shell, поэтому НИКОГДА не передавайте ненадёжный ввод (риск shell-инъекции). Используйте fgets/fprintf с возвращённым FILE* как с обычным файлом. pclose() закрывает pipe и ждёт выхода дочернего процесса (возвращает его статус). Для ненадёжного ввода используйте fork+execvp напрямую (без shell). popen идеален для быстрых скриптов, инструментов системного администрирования и чтения вывода команд. Для двунаправленного общения используйте socketpair() или два pipe.

c
#include <stdio.h>
#include <stdlib.h>

int main() {
    // popen opens a process with a pipe (like shell command | ...)
    FILE *fp = popen("ls -la /tmp", "r");
    if (fp == NULL) {
        perror("popen failed");
        return 1;
    }

    // Read command output line by line
    char buf[256];
    while (fgets(buf, sizeof(buf), fp) != NULL) {
        printf(">> %s", buf);
    }

    pclose(fp);  // closes pipe and waits for child

    // Writing to a process (like ... | command):
    FILE *wp = popen("grep hello", "w");
    fprintf(wp, "hello world\n");
    fprintf(wp, "goodbye\n");
    pclose(wp);  // grep outputs "hello world"

    return 0;
}

// popen is simpler than fork+pipe+exec but:
// - Runs via /bin/sh (shell injection risk with user input!)
// - Less control over the child process
// - Use fork+exec directly for untrusted input
15

Makefile и инструменты сборки

Базовая структура Makefile

Make автоматизирует компиляцию. Makefile содержит правила: target (файл для сборки), prerequisites (зависимости) и recipe (shell-команды, с отступом TAB). Переменные (CC, CFLAGS) централизуют конфигурацию. Автоматические переменные: $@ (имя target), $< (первая зависимость), $^ (все зависимости). Pattern rules (%.o: %.c) обобщают компиляцию для всех исходных файлов. .PHONY объявляет target'ы, не являющиеся файлами (clean, all, install). Первое правило — по умолчанию (make без аргументов собирает 'all'). Make отслеживает временные метки файлов — пересобирает только при новой зависимости. Инкрементальная сборка экономит время на больших проектах.

c
# Makefile — build automation for C/C++ projects
# Rule syntax: target: prerequisites
#               recipe (must start with TAB, not spaces)

# Variables
CC = gcc
CFLAGS = -Wall -Wextra -g -O2
TARGET = myapp
SRCS = main.c utils.c parser.c
OBJS = $(SRCS:.c=.o)  # substitute .c with .o

# Default target (first rule)
all: $(TARGET)

# Link object files into executable
$(TARGET): $(OBJS)
	$(CC) $(CFLAGS) -o $@ $^

# Compile each .c to .o
%.o: %.c
	$(CC) $(CFLAGS) -c $< -o $@

# Clean build artifacts
clean:
	rm -f $(OBJS) $(TARGET)

# Run the program
run: $(TARGET)
	./$(TARGET)

# Phony targets (not files)
.PHONY: all clean run

# Usage:
# $ make        — builds 'all' (the default)
# $ make clean  — removes build files
# $ make run    — builds and runs
# $ make -j4    — parallel build (4 jobs)

Автоматические переменные и pattern rules

Автоматические переменные делают Makefiles лаконичными и поддерживаемыми. $@ (target), $< (первая зависимость) и $^ (все зависимости) — наиболее частые. Pattern rules (%.o: %.c) позволяют написать одно правило для всех исходных файлов — % совпадает с любой строкой. $(wildcard) находит файлы по glob, $(patsubst) преобразует строки — вместе они авто-обнаруживают источники. Префикс @ подавляет эхо команды. Static pattern rules (target: %.o: %.c) применяются к конкретному списку. Понимание этих возможностей устраняет повторяющиеся правила и масштабирует Makefiles до больших проектов. Всегда используйте TAB (не пробелы) для отступа recipe — Make строг к этому.

c
# Automatic variables in recipes:
# $@   — the target filename
# $<   — the first prerequisite
# $^   — all prerequisites (no duplicates)
# $+   — all prerequisites (with duplicates)
# $?   — prerequisites newer than the target
# $*   — the stem (matching % part)

# Example showing all automatic variables:
program: main.o utils.o
	@echo "Target: $@"        # program
	@echo "First dep: $<"     # main.o
	@echo "All deps: $^"      # main.o utils.o
	@echo "Newer deps: $?"    # (whichever changed)
	gcc -o $@ $^

# Pattern rule: compile any .c to .o
%.o: %.c
	gcc -c $< -o $@
# $< = source (.c file), $@ = target (.o file)

# Static pattern rule (specific files):
$(OBJS): %.o: %.c
	gcc -c $< -o $@

# Built-in functions:
SRCS = $(wildcard src/*.c)           # find all .c files
OBJS = $(patsubst src/%.c,build/%.o,$(SRCS))  # path substitution
DIRS = $(sort $(dir $(SRCS)))         # unique directories

Зависимости и заголовочные файлы

Отслеживание заголовочных зависимостей критично — без этого изменение .h файла не вызывает перекомпиляцию .c файлов, его включающих, что ведёт к устаревшим сборкам. Решение: gcc -MMD -MP генерирует .d файлы, перечисляющие все зависимости (включая заголовки). -include подтягивает их в Makefile. -MP добавляет phony-target'ы для заголовков (предотвращает ошибки при удалении заголовка). Это стандартный подход для C/C++ проектов. Без этого пришлось бы вручную перечислять каждую заголовочную зависимость — неуправляемо для больших проектов. Первая сборка не будет иметь .d файлов ('-' в -include подавляет ошибку); они создаются при компиляции и используются при последующих сборках.

c
# When a header file changes, dependent .c files must recompile
# Make doesn't track this automatically — use gcc -MMD

CC = gcc
CFLAGS = -Wall -MMD -MP  # generate .d dependency files

SRCS = $(wildcard src/*.c)
OBJS = $(SRCS:.c=.o)
DEPS = $(OBJS:.o=.d)    # dependency files

all: myapp

myapp: $(OBJS)
	$(CC) -o $@ $^

%.o: %.c
	$(CC) $(CFLAGS) -c $< -o $@

# Include auto-generated dependency files
-include $(DEPS)  # '-' means don't error if missing

clean:
	rm -f $(OBJS) $(DEPS) myapp

.PHONY: all clean

# How it works:
# 1. gcc -MMD creates main.d next to main.o
# 2. main.d contains: main.o: main.c utils.h parser.h
# 3. -include pulls these in, so Make knows header dependencies
# 4. If utils.h changes, main.o rebuilds automatically

Makefile для мульти-каталожного проекта

Реальные проекты охватывают несколько каталогов. Этот Makefile авто-обнаруживает источники (wildcard), мапит их в каталог сборки (patsubst) и создаёт каталоги по необходимости. Синтаксис | создаёт order-only prerequisites — $(BUILDDIR) создаётся перед компиляцией, но изменение его временной метки не вызывает пересборку (без |, создание каталога вызывало бы пересборку всего каждый раз). -Iinclude указывает gcc, где искать заголовки. -MMD генерирует файлы зависимостей в каталоге сборки. Эта структура разделяет исходный, сборочный и бинарный каталоги — легко чистить (rm -rf build) и не загрязняет дерево исходников. Для очень больших проектов рассмотрите CMake или Meson.

c
# Project structure:
# project/
#   src/       — source files
#   include/   — header files
#   build/     — object files (created by make)
#   bin/       — final executable

CC = gcc
CFLAGS = -Wall -Iinclude -g
SRCDIR = src
INCDIR = include
BUILDDIR = build
BINDIR = bin

TARGET = $(BINDIR)/myapp
SRCS = $(wildcard $(SRCDIR)/*.c)
OBJS = $(patsubst $(SRCDIR)/%.c,$(BUILDDIR)/%.o,$(SRCS))
DEPS = $(OBJS:.o=.d)

all: $(TARGET)

$(TARGET): $(OBJS) | $(BINDIR)
	$(CC) -o $@ $^

# Order-only prerequisite: create build dir before compiling
$(BUILDDIR)/%.o: $(SRCDIR)/%.c | $(BUILDDIR)
	$(CC) $(CFLAGS) -MMD -c $< -o $@

$(BUILDDIR):
	mkdir -p $(BUILDDIR)

$(BINDIR):
	mkdir -p $(BINDIR)

-include $(DEPS)

clean:
	rm -rf $(BUILDDIR) $(BINDIR)

.PHONY: all clean

# Order-only prerequisites (| syntax) create directories
# without triggering rebuilds when the dir timestamp changes

Основы CMake (альтернатива Make)

CMake — мета-система сборки — генерирует Makefiles (или проекты Ninja, Visual Studio, Xcode) из файла CMakeLists.txt. Это де-факто стандарт для C/C++ проектов, так как обрабатывает кросс-платформенную компиляцию, обнаружение зависимостей и интеграцию с IDE. Ключевые команды: project() задаёт имя проекта, add_executable() определяет target сборки, target_include_directories() добавляет пути заголовков, target_link_libraries() линкует библиотеки. Out-of-source сборки (mkdir build && cd build && cmake ..) сохраняют дерево исходников чистым. CMake авто-обнаруживает компиляторы и флаги для каждой платформы. Для новых C/C++ проектов предпочитайте CMake вместо сырых Makefiles — он более поддерживаемый и переносимый.

c
# CMakeLists.txt — CMake is a cross-platform build generator
# It generates Makefiles (or Ninja, VS, Xcode projects)

cmake_minimum_required(VERSION 3.10)
project(MyApp C)

set(CMAKE_C_STANDARD 11)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Wextra")

# Add executable from source files
add_executable(myapp src/main.c src/utils.c src/parser.c)

# Include directory
target_include_directories(myapp PRIVATE include)

# Link a library
target_link_libraries(myapp m)  # math library (-lm)

# Build type flags
set(CMAKE_C_FLAGS_DEBUG "-g -O0")
set(CMAKE_C_FLAGS_RELEASE "-O3 -DNDEBUG")

# Usage:
# $ mkdir build && cd build
# $ cmake ..            # generate Makefiles
# $ make                # build
# $ make install        # install (optional)
# $ cmake .. -DCMAKE_BUILD_TYPE=Debug  # debug build

# Out-of-source builds keep source tree clean
# CMake is the standard for C/C++ cross-platform projects
16

Углублённое изучение указателей на функции

Синтаксис и использование указателей на функции

Указатели на функции хранят адрес функции, обеспечивая диспетчеризацию во время выполнения. Синтаксис int (*fp)(int, int) общеизвестно запутан — читайте как 'fp — указатель на функцию, принимающую (int, int) и возвращающую int'. typedef упрощает это: typedef int (*math_func)(int, int) создаёт читаемый алиас. Имена функций превращаются в указатели (как имена массивов), поэтому 'add' и '&add' эквивалентны. Указатели на функции обеспечивают callback'и, обработчики событий, паттерны strategy и таблицы диспетчеризации (массивы указателей на функции для switch-подобной диспетчеризации). Это основа компаратора qsort и GUI-событийных систем.

c
#include <stdio.h>

// Function pointer syntax: return_type (*name)(param_types)
int add(int a, int b) { return a + b; }
int subtract(int a, int b) { return a - b; }

int main() {
    // Declare a function pointer
    int (*operation)(int, int);

    // Assign (function name decays to pointer)
    operation = add;          // or &add
    printf("5 + 3 = %d\n", operation(5, 3));  // 8

    operation = subtract;
    printf("5 - 3 = %d\n", operation(5, 3));  // 2

    // typedef for readability
    typedef int (*math_func)(int, int);
    math_func fn = add;
    printf("Result: %d\n", fn(10, 20));

    // Array of function pointers (dispatch table)
    math_func ops[] = {add, subtract};
    printf("op[0](4,2)=%d  op[1](4,2)=%d\n",
           ops[0](4, 2), ops[1](4, 2));

    return 0;
}

Callback'и (пример qsort)

qsort — классический пример указателей на функции как callback'ов. Компаратор получает const void* указатели (универсальные) и возвращает целое число, указывающее порядок. qsort вызывает ваш компаратор для определения порядка элементов — вы управляете поведением сортировки, передавая различные функции. Это паттерн strategy в C: алгоритм (qsort) фиксирован, но логика сравнения внедряется. void* обеспечивает обобщённое программирование (сортировка любого типа). Компаратор должен быть чистой функцией (без побочных эффектов) и согласованным (если a<b и b<c, то a<c). Этот паттерн используется по всей стандартной библиотеке C (bsearch, atexit, signal).

c
#include <stdio.h>
#include <stdlib.h>

// Comparator function for qsort
// Returns: negative if a<b, 0 if equal, positive if a>b
int compare_asc(const void *a, const void *b) {
    return (*(int *)a - *(int *)b);
}

int compare_desc(const void *a, const void *b) {
    return (*(int *)b - *(int *)a);
}

int main() {
    int arr[] = {5, 2, 8, 1, 9, 3, 7, 4, 6};
    int n = sizeof(arr) / sizeof(arr[0]);

    // qsort takes a function pointer as the comparator
    qsort(arr, n, sizeof(int), compare_asc);
    printf("Ascending: ");
    for (int i = 0; i < n; i++) printf("%d ", arr[i]);
    printf("\n");

    // Same array, different comparator (descending)
    qsort(arr, n, sizeof(int), compare_desc);
    printf("Descending: ");
    for (int i = 0; i < n; i++) printf("%d ", arr[i]);
    printf("\n");

    return 0;
}

Структуры с указателями на функции (ООП в C)

C может имитировать ООП, используя структуры с указателями на функции — так внутри работает vtable в C++. Структура содержит указатель на 'vtable' (таблицу виртуальных функций), хранящую указатели на функции. Каждый 'подкласс' (Circle, Square) имеет собственную vtable со своими реализациями. Приведение Circle* к Shape* обеспечивает полиморфизм — print_shape() вызывает правильную функцию area() через vtable. Этот паттерн используется в реальном C-коде: ядро Linux (драйверы устройств), GObject (GTK) и SQLite. Он обеспечивает инкапсуляцию, наследование (через встраивание структур) и полиморфизм. Хотя это многословнее по сравнению с C++, это даёт полный контроль над расположением в памяти и виртуальной диспетчеризацией.

c
#include <stdio.h>

// Simulating OOP with structs + function pointers
typedef struct Shape Shape;

// Virtual function table (vtable)
typedef struct {
    double (*area)(Shape *);
    double (*perimeter)(Shape *);
    void (*describe)(Shape *);
} ShapeVTable;

struct Shape {
    const ShapeVTable *vtable;  // pointer to virtual functions
    char name[32];
};

// Circle implementation
typedef struct {
    Shape base;      // inherit from Shape
    double radius;
} Circle;

double circle_area(Shape *s) {
    return 3.14159 * ((Circle *)s)->radius * ((Circle *)s)->radius;
}

double circle_perimeter(Shape *s) {
    return 2 * 3.14159 * ((Circle *)s)->radius;
}

static const ShapeVTable circle_vtable = {
    circle_area, circle_perimeter, NULL
};

Circle *circle_create(double r) {
    Circle *c = malloc(sizeof(Circle));
    c->base.vtable = &circle_vtable;
    strcpy(c->base.name, "Circle");
    c->radius = r;
    return c;
}

// Polymorphic function (works with any Shape)
void print_shape(Shape *s) {
    printf("%s: area=%.2f, perimeter=%.2f\n",
           s->name, s->vtable->area(s), s->vtable->perimeter(s));
}

Событийно-ориентированное программирование с callback'ами

Указатели на функции обеспечивают событийно-ориентированную архитектуру в C — паттерн publish/subscribe. Обработчики регистрируются через on_event() (подписка), а emit_event() вызывает все зарегистрированные обработчики (публикация). Это развязывает производителей событий от потребителей — эмиттер не знает, что делают обработчики. Этот паттерн лежит в основе GUI-фреймворков (клик по кнопке → обработчик), игровых движков (коллизия → callback) и асинхронного ввода-вывода (данные готовы → обработчик чтения). Сигнатура обработчика (имя события + void* data) достаточно универсальна для любого типа события. В продакшене добавьте обработку ошибок (что если обработчик упадёт?), упорядочивание по приоритету и возможность отписки. Так работают libuv, libevent и Node.js под капотом.

c
#include <stdio.h>

// Event system using function pointers
typedef void (*EventHandler)(const char *event, void *data);

// Simple event emitter
#define MAX_HANDLERS 10
static EventHandler handlers[MAX_HANDLERS];
static int handler_count = 0;

void on_event(EventHandler handler) {
    if (handler_count < MAX_HANDLERS) {
        handlers[handler_count++] = handler;
    }
}

void emit_event(const char *event, void *data) {
    for (int i = 0; i < handler_count; i++) {
        handlers[i](event, data);  // call each registered handler
    }
}

// Concrete handlers
void log_handler(const char *event, void *data) {
    printf("[LOG] Event: %s\n", event);
}

void alert_handler(const char *event, void *data) {
    if (strcmp(event, "error") == 0) {
        printf("[ALERT] Error occurred!\n");
    }
}

int main() {
    // Register handlers (subscribe)
    on_event(log_handler);
    on_event(alert_handler);

    // Emit events (publish)
    emit_event("click", NULL);
    emit_event("error", NULL);
    emit_event("scroll", NULL);

    return 0;
}

Ловушки указателей на функции

Указатели на функции имеют несколько ловушек. Вызов NULL-указателя на функцию приводит к сбою (segfault) — всегда проверяйте на NULL перед вызовом. Приведение к неверной сигнатуре — неопределённое поведение (calling convention может отличаться). Сравнение указателей на функции на равенство допустимо (та же функция), но упорядочивание (<, >) не определено. Используйте typedef последовательно — синтаксис указателей на функции подвержен ошибкам, а typedef делает объявления читаемыми и поддерживаемыми. В C указатели на функции — единственный способ добиться полиморфизма во время выполнения и callback'ов, поэтому их освоение необходимо. C++ добавляет std::function, лямбды и виртуальные функции как более безопасные альтернативы.

c
#include <stdio.h>

// PITFALL 1: Calling a NULL function pointer (crash!)
void bad_call() {
    void (*fp)(void) = NULL;
    fp();  // SEGFAULT — always check for NULL
    if (fp) fp();  // safe
}

// PITFALL 2: Wrong signature (undefined behavior)
void takes_int(int x) { printf("%d\n", x); }
void wrong_sig() {
    void (*fp)(void) = (void (*)(void))takes_int;  // WRONG cast
    fp();  // UB: missing argument, garbage value
}

// PITFALL 3: Function pointer to a local function (dangling)
typedef int (*callback_t)(int);
callback_t get_callback() {
    // Returning pointer to local function is OK (functions aren't local)
    // But returning a pointer to a local VARIABLE is not
    return NULL;  // functions have static storage, safe to return
}

// PITFALL 4: Comparing function pointers
int f1(int x) { return x; }
int f2(int x) { return x; }
void compare_fps() {
    int (*p1)(int) = f1;
    int (*p2)(int) = f1;
    if (p1 == p2) printf("Same function\n");  // OK
    // Comparing p1 == f2 is valid but they're different functions
}

// GOOD: Always use typedef for complex function pointers
// typedef int (*comparator_t)(const void *, const void *);
// This makes declarations readable and consistent
17

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

Базовые вариативные функции (stdarg)

Вариативные функции принимают переменное число аргументов, используя stdarg.h. va_list содержит список аргументов, va_start инициализирует его (требуется последний именованный параметр перед ...), va_arg извлекает следующий аргумент с указанным типом, va_end выполняет очистку. Функция должна знать, сколько аргументов читать — либо через параметр счётчика (как форматная строка printf), либо через значение-сигнал (NULL-терминатор). '...' всегда должен быть последним параметром. va_arg не проверяет типы — передача неверного типа — неопределённое поведение. Так работают printf, fprintf и execl.

c
#include <stdio.h>
#include <stdarg.h>

// Variadic function: takes variable number of arguments
// The '...' must be the LAST parameter
int sum(int count, ...) {
    va_list args;           // argument list type
    va_start(args, count);  // initialize (needs last named param)

    int total = 0;
    for (int i = 0; i < count; i++) {
        int val = va_arg(args, int);  // get next argument (as int)
        total += val;
    }

    va_end(args);  // cleanup
    return total;
}

int main() {
    printf("%d\n", sum(3, 10, 20, 30));      // 60
    printf("%d\n", sum(5, 1, 2, 3, 4, 5));   // 15
    printf("%d\n", sum(0));                    // 0
    return 0;
}

// The 'count' parameter tells the function how many args follow.
// Without it, the function can't know when to stop.

Реализация пользовательского printf

vprintf/vfprintf/vsprintf — вариативные помощники, принимающие va_list вместо ... — они позволяют создавать пользовательские printf-подобные функции. Пример log_msg оборачивает printf с префиксом уровня логирования. Пример print_values показывает, как обрабатывать смешанные типы: передавайте тег типа перед каждым значением, затем switch по тегу для вызова va_arg с правильным типом. Это необходимо, потому что va_arg требует точный тип — нет информации о типах во время выполнения. Паттерн тегов типов используется в полиморфных C-API (например, bind-функции SQLite). Всегда точно совмещайте типы va_arg — int vs long, float vs double (float повышается до double в varargs).

c
#include <stdio.h>
#include <stdarg.h>
#include <string.h>

// Custom logging function with format string
void log_msg(const char *level, const char *format, ...) {
    printf("[%s] ", level);

    va_list args;
    va_start(args, format);

    // vprintf: like printf but takes va_list instead of ...
    vprintf(format, args);

    va_end(args);
    printf("\n");
}

// Variadic function with mixed types
void print_values(int count, ...) {
    va_list args;
    va_start(args, count);

    for (int i = 0; i < count; i++) {
        int type = va_arg(args, int);  // type tag
        switch (type) {
            case 0:  // int
                printf("int: %d\n", va_arg(args, int));
                break;
            case 1:  // double
                printf("double: %f\n", va_arg(args, double));
                break;
            case 2:  // string
                printf("string: %s\n", va_arg(args, char *));
                break;
        }
    }
    va_end(args);
}

int main() {
    log_msg("INFO", "User %s logged in from %s", "Alice", "192.168.1.1");
    log_msg("ERROR", "Failed to open %s (code %d)", "config.txt", 13);

    print_values(2, 0, 42, 2, "hello", 1, 3.14);
    return 0;
}

Вариативные функции с сигнальным завершением

Вариативные функции с сигнальным завершением используют специальное значение (обычно NULL) для отметки конца аргументов вместо счётчика. Это чище для API, активно работающих со строками — вызывающему не нужно считать аргументы. Семейство exec (execl, execlp) использует NULL как сигнал. Недостаток: если вызывающий забудет NULL, функция читает мусорную память (неопределённое поведение). Некоторые компиляторы (GCC) поддерживают __attribute__((sentinel)) для предупреждения об отсутствующих сигналах. Всегда документируйте, что требуется NULL. Параметр размера буфера предотвращает переполнения буфера — всегда передавайте размер назначения и проверяйте границы перед strcat.

c
#include <stdio.h>
#include <stdarg.h>
#include <string.h>

// Sentinel-terminated: last argument is NULL
void concat_strings(char *dest, size_t size, ...) {
    va_list args;
    va_start(args, size);

    dest[0] = '\0';  // start with empty string
    size_t used = 0;

    while (1) {
        char *s = va_arg(args, char *);
        if (s == NULL) break;  // sentinel — stop reading

        size_t len = strlen(s);
        if (used + len < size) {
            strcat(dest, s);
            used += len;
        }
    }

    va_end(args);
}

int main() {
    char result[256];
    concat_strings(result, sizeof(result),
                   "Hello, ", "world", "! ", "How are you?", NULL);
    printf("%s\n", result);
    // Hello, world! How are you?

    // Common C APIs using sentinels:
    // execl("/bin/ls", "ls", "-l", NULL);  // exec family
    // sqlite3_exec(db, sql, callback, NULL, NULL);
    return 0;
}

Перенаправление вариативных аргументов

Перенаправление вариативных аргументов требует va_copy (не присваивания) — va_list может быть непрозрачным типом, который нельзя скопировать с помощью =. va_copy позволяет обходить список аргументов несколько раз (например, сначала для измерения, затем для печати). Макрос LOG использует __VA_ARGS__ для перенаправления всех аргументов в fprintf. Расширение GCC ##__VA_ARGS__ удаляет предшествующую запятую, когда вариативные аргументы не предоставлены (поэтому LOG("msg") работает без висящей запятой). Этот паттерн повсеместен в C-макросах логирования. C99 требует хотя бы один аргумент перед ...; C11/C23 и GCC допускают ноль. Для типобезопасных альтернатив в C++ используйте вариативные шаблоны или std::format.

c
#include <stdio.h>
#include <stdarg.h>

// Wrapper that forwards varargs to another function
// Use va_copy for the copy (needed for multiple passes)
void custom_printf(const char *format, ...) {
    va_list args1, args2;
    va_start(args1, format);
    va_copy(args2, args1);  // copy for second use

    // First pass: count characters that would be printed
    int len = vsnprintf(NULL, 0, format, args1);
    printf("[len=%d] ", len);

    // Second pass: actually print
    vprintf(format, args2);

    va_end(args1);
    va_end(args2);
}

// Macro forwarding (common pattern for logging)
#define LOG(fmt, ...) \
    fprintf(stderr, "[LOG] %s:%d: " fmt "\n", \
            __FILE__, __LINE__, ##__VA_ARGS__)

// The ## operator removes the comma if __VA_ARGS__ is empty
// (GCC extension, widely supported)

int main() {
    custom_printf("Value: %d, Name: %s\n", 42, "test");
    LOG("Simple message");           // no extra args
    LOG("With value: %d", 100);      // with args
    return 0;
}

Вариативные макросы (C99)

Вариативные макросы C99 используют __VA_ARGS__ для захвата всех аргументов, соответствующих ... в определении макроса. ##__VA_ARGS__ (расширение GCC, теперь стандарт в C20) удаляет запятую, когда вариативные аргументы не переданы. Макрос COUNT использует хитрый трюк: он отображает N аргументов на N, 5, 4, 3, 2, 1 и N-я позиция даёт счёт. Идиома do { ... } while (0) в ASSERT заставляет макрос вести себя как один оператор (безопасно в if/else без фигурных скобок). #строкифицирует аргументы макроса. Вариативные макросы необходимы для логирования, отладки и обобщённого программирования в C. Они — основа многих API библиотек и системы логирования ядра Linux.

c
#include <stdio.h>

// C99 variadic macros: __VA_ARGS__ captures all extra args
#define DEBUG_PRINT(fmt, ...) \
    printf("DEBUG: " fmt "\n", ##__VA_ARGS__)

#define MAX(...) (max_of(__VA_ARGS__))

// Assert macro with message
#define ASSERT(cond, fmt, ...) \
    do { \
        if (!(cond)) { \
            fprintf(stderr, "Assertion failed: %s\n" fmt "\n", \
                    #cond, ##__VA_ARGS__); \
            exit(1); \
        } \
    } while (0)

// Count arguments (GCC __VA_OPT__ or recursive macros)
#define COUNT(...) COUNT_N(__VA_ARGS__, 5, 4, 3, 2, 1)
#define COUNT_N(_1, _2, _3, _4, _5, N, ...) N

// Stringification of all args
#define STR(...) #__VA_ARGS__

int main() {
    DEBUG_PRINT("x = %d", 42);
    DEBUG_PRINT("no args");  // ## removes comma

    ASSERT(x > 0, "x was %d", x);

    printf("Count: %d\n", COUNT(a, b, c));  // 3
    printf("Stringified: %s\n", STR(hello, world));  // "hello, world"

    return 0;
}
18

Трюки битовых манипуляций

Распространённые битовые трюки

Битовые манипуляции работают напрямую с двоичным представлением. n & 1 проверяет младший бит на чётность/нечётность. Сдвиг влево (<<) умножает на 2; сдвиг вправо (>>) делит. XOR-обмен избегает временной переменной, но менее читаем. n & (n-1) очищает младший установленный бит, полезно для проверки степеней двойки и popcount. Эти трюки быстры, но в прикладном коде приоритет — читаемость.

c
// Check if odd
int is_odd(int n) { return n & 1; }

// Multiply/divide by powers of 2
int doubled = x << 1;    // x * 2
int halved = x >> 1;     // x / 2
int times8 = x << 3;     // x * 8

// Swap without temp variable
void swap(int *a, int *b) {
    *a ^= *b; *b ^= *a; *a ^= *b;
}

// Check if power of 2
int is_pow2(int n) { return n > 0 && (n & (n-1)) == 0; }

// Count set bits (population count)
int popcount(unsigned int n) {
    int count = 0;
    while (n) { n &= (n-1); count++; }
    return count;
}

Битовые флаги и маски

Битовые флаги упаковывают несколько булевых опций в одно целое, экономя память. Каждый флаг — степень двойки (один бит). OR (|) устанавливает флаги, AND (&) проверяет, XOR (^) переключает, AND NOT (&= ~) очищает. Этот паттерн повсеместен в системном программировании: права доступа к файлам (O_RDONLY, O_CREAT), опции сокетов и состояние GPU. Используйте именованные константы для читаемости.

c
#define FLAG_READ    (1 << 0)  // 0x01
#define FLAG_WRITE   (1 << 1)  // 0x02
#define FLAG_EXECUTE (1 << 2)  // 0x04

// Set flags
unsigned int perms = FLAG_READ | FLAG_WRITE;

// Check if flag is set
if (perms & FLAG_WRITE) { /* write allowed */ }

// Toggle a flag
perms ^= FLAG_EXECUTE;

// Clear a flag
perms &= ~FLAG_APPEND;

// Check if ALL flags in mask are set
int has_all = (perms & (FLAG_READ|FLAG_WRITE))
            == (FLAG_READ|FLAG_WRITE);

Битовые поля в структурах

Битовые поля упаковывают маленькие значения в минимальное количество бит внутри структур. Синтаксис с двоеточием указывает ширину битового поля. Это экономит память для структур данных со многими небольшими полями (даты, флаги, аппаратные регистры). Однако расположение битовых полей зависит от реализации: порядок байтов, выравнивание и padding различаются у разных компиляторов. Избегайте битовых полей для переносимых форматов данных; используйте явные битовые маски.

c
// Pack multiple small fields into one int
struct Date {
    unsigned int day   : 5;   // 0-31 (5 bits)
    unsigned int month : 4;   // 0-15 (4 bits)
    unsigned int year  : 23;  // 0-8M (23 bits)
};  // Total: 32 bits = 4 bytes

struct Date today = { 21, 6, 2025 };
printf("Size: %zu bytes\n", sizeof(today));  // 4

today.day = 15;
if (today.month == 12) {
    today.year++;
    today.month = 1;
}

Преобразование порядка байтов

Порядок байтов определяет последовательность: little-endian (x86, ARM по умолчанию) хранит LSB первым; big-endian (сетевой, некоторые MIPS) хранит MSB первым. Сетевые протоколы используют big-endian (сетевой порядок байтов). Используйте htonl/ntohl для переносимого сетевого кода. Ручные обмены байтов через сдвиги и маски работают на любой платформе. Определяйте порядок байтов во время компиляции с __BYTE_ORDER__ для оптимизированных веток кода.

c
#include <arpa/inet.h>  // htonl, ntohl

// Host to network byte order (big-endian) and back
uint32_t host_val = 0x12345678;
uint32_t net_val = htonl(host_val);
uint32_t back = ntohl(net_val);

// Manual byte swap (portable)
uint32_t swap32(uint32_t v) {
    return ((v & 0xFF000000) >> 24) |
           ((v & 0x00FF0000) >> 8)  |
           ((v & 0x0000FF00) << 8)  |
           ((v & 0x000000FF) << 24);
}

// Detect endianness at compile time
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
    printf("Little-endian system\n");
#endif

Битовые хаки

Безветвленные битовые хаки избегают условных переходов ради производительности в плотных циклах. Трюк abs использует арифметический сдвиг вправо для создания маски. next_pow2 заполняет все биты ниже старшего установленного, затем добавляет 1. Реверс битов использует «разделяй и властвуй»: обмен ниблами, затем парами, затем отдельными битами. Это полезно в криптографии, хешировании и DSP. Современные CPU часто имеют встроенные инструкции (POPCNT, LZCNT), которые быстрее.

c
// Absolute value without branching
int abs_val(int n) {
    int mask = n >> (sizeof(int)*8 - 1);
    return (n ^ mask) - mask;
}

// Round up to next power of 2
unsigned int next_pow2(unsigned int n) {
    n--;
    n |= n >> 1;  n |= n >> 2;
    n |= n >> 4;  n |= n >> 8;
    n |= n >> 16;
    return n + 1;
}

// Reverse bits in a byte
uint8_t reverse_byte(uint8_t b) {
    b = (b >> 4) | (b << 4);
    b = ((b & 0xCC) >> 2) | ((b & 0x33) << 2);
    b = ((b & 0xAA) >> 1) | ((b & 0x55) << 1);
    return b;
}
19

Продвинутая обработка сигналов

Наборы сигналов и блокировка

sigprocmask блокирует сигналы, чтобы они ставились в очередь (не терялись) и доставлялись позже. Это защищает критические секции от прерывания. SIG_BLOCK добавляет к маске, SIG_UNBLOCK удаляет, SIG_SETMASK заменяет. Используйте sigpending для проверки ожидающих сигналов. Блокируйте сигналы лишь кратковременно; длительная блокировка может пропустить важные события. Маски сигналов — на процесс и наследуются через fork.

c
#include <signal.h>

sigset_t mask, oldmask;
sigemptyset(&mask);
sigaddset(&mask, SIGINT);
sigaddset(&mask, SIGTERM);

// Block signals (they will be queued)
sigprocmask(SIG_BLOCK, &mask, &oldmask);

// Critical section - signals are deferred
do_critical_work();

// Unblock - pending signals are now delivered
sigprocmask(SIG_SETMASK, &oldmask, NULL);

// Check for pending signals
sigpending(&mask);
if (sigismember(&mask, SIGINT)) {
    printf("SIGINT is pending\n");
}

Безопасные обработчики сигналов

Обработчики сигналов должны быть async-signal-safe: используйте только реентерабельные функции (write, _exit, signal). Избегайте printf, malloc и большинства библиотечных функций — они могут быть прерваны в середине операции и повредить состояние. Используйте volatile sig_atomic_t для флагов, устанавливаемых обработчиком. SA_RESTART автоматически перезапускает прерванные системные вызовы. sigaction предпочтительнее signal для переносимого, чётко определённого поведения.

c
#include <signal.h>
#include <unistd.h>

// Only safe type in handlers
volatile sig_atomic_t got_signal = 0;

void handler(int sig) {
    // ONLY use async-signal-safe functions!
    // write() is safe; printf() is NOT
    const char msg[] = "Signal received\n";
    write(STDERR_FILENO, msg, sizeof(msg)-1);
    got_signal = 1;
}

int main() {
    struct sigaction sa;
    sa.sa_handler = handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_RESTART;  // Restart interrupted syscalls
    sigaction(SIGINT, &sa, NULL);

    while (!got_signal) pause();
    return 0;
}

Self-pipe и signalfd

signalfd (Linux) преобразует сигналы в файловые дескрипторы, интегрируя их в циклы событий (epoll, select). Сначала заблокируйте сигнал, затем создайте signalfd. Трюк self-pipe переносим: обработчик пишет байт в канал, а основной цикл его читает. Оба подхода перемещают обработку сигналов из ограниченного контекста обработчика в обычный код, где можно безопасно вызывать любую функцию.

c
#include <sys/signalfd.h>

// signalfd: handle signals as file descriptors (Linux)
int setup_signalfd() {
    sigset_t mask;
    sigemptyset(&mask);
    sigaddset(&mask, SIGINT);
    sigaddset(&mask, SIGTERM);
    sigprocmask(SIG_BLOCK, &mask, NULL);  // Must block first

    int fd = signalfd(-1, &mask, SFD_CLOEXEC);
    return fd;
}

// In event loop:
struct signalfd_siginfo si;
read(signalfd_fd, &si, sizeof(si));
printf("Got signal %d\n", si.ssi_signo);

// Self-pipe trick (portable):
// Write to pipe in handler, read in main loop
int pipefd[2];
void pipe_handler(int sig) {
    write(pipefd[1], &sig, sizeof(sig));
}

Таймеры и SIGALRM

setitimer доставляет SIGALRM с интервалами. ITIMER_REAL использует реальное время; ITIMER_VIRTUAL — процессорное время; ITIMER_PROF — CPU + системное время. Таймер повторяется, пока не отменён. Для современного кода предпочитайте timer_create с SIGEV_THREAD для таймеров на поток или используйте выделенный timer fd (timerfd_create на Linux) для интеграции с циклами событий. Всегда обрабатывайте сигнал, чтобы избежать завершения по умолчанию.

c
#include <sys/time.h>

volatile sig_atomic_t timer_fired = 0;

void timer_handler(int sig) { timer_fired = 1; }

int main() {
    struct sigaction sa = {0};
    sa.sa_handler = timer_handler;
    sigaction(SIGALRM, &sa, NULL);

    // Set interval timer: 2 sec initial, 1 sec repeat
    struct itimerval timer;
    timer.it_value.tv_sec = 2;
    timer.it_interval.tv_sec = 1;
    setitimer(ITIMER_REAL, &timer, NULL);

    int count = 0;
    while (count < 5) {
        pause();
        if (timer_fired) {
            printf("Timer %d\n", ++count);
            timer_fired = 0;
        }
    }
    return 0;
}

Отправка сигналов

kill отправляет сигнал процессу по PID. kill(0, sig) отправляет всей группе процессов. raise отправляет сигнал вызывающему процессу. sigqueue отправляет сигнал с присоединёнными данными (siginfo). Всегда используйте waitpid для сбора дочерних процессов и проверки статуса завершения. WIFSIGNALED отличает смерть от сигнала от нормального выхода. Отправка SIGTERM (не SIGKILL) позволяет корректное завершение.

c
#include <signal.h>
#include <sys/wait.h>

pid_t child = fork();
if (child == 0) {
    while (1) { sleep(1); }
} else {
    sleep(3);
    kill(child, SIGTERM);  // Send SIGTERM to child

    int status;
    waitpid(child, &status, 0);
    if (WIFSIGNALED(status))
        printf("Killed by signal %d\n", WTERMSIG(status));
}

// Send signal to self
raise(SIGSTOP);  // Stop (resume with SIGCONT)

// Send to process group
kill(0, SIGUSR1);  // 0 = own process group

// Send with data (sigqueue)
union sigval value = { .sival_int = 42 };
sigqueue(child, SIGUSR1, value);
20

Продвинутое управление процессами

Паттерны fork и exec

Паттерн fork-exec создаёт дочерний процесс (fork) и заменяет его образ новой программой (exec). fork дублирует процесс; exec загружает новую программу. Дочерний процесс должен вызвать _exit (не exit) при ошибке exec, чтобы избежать сброса буферов родителя. waitpid блокируется до выхода дочернего процесса. WEXITSTATUS извлекает код выхода. Так оболочки запускают команды.

c
#include <unistd.h>
#include <sys/wait.h>

int run_program(const char *path, char *const args[]) {
    pid_t pid = fork();
    if (pid < 0) { perror("fork"); return -1; }

    if (pid == 0) {
        // Child: exec replaces process image
        execvp(path, args);
        perror("execvp");  // Only on failure
        _exit(127);
    }

    // Parent: wait for child
    int status;
    waitpid(pid, &status, 0);
    return WIFEXITED(status) ? WEXITSTATUS(status) : -1;
}

// Usage
char *args[] = {"ls", "-la", "/tmp", NULL};
int result = run_program("ls", args);

Демонизация

setsid создаёт новую сессию и группу процессов, отсоединяясь от управляющего терминала. Паттерн двойного fork предотвращает повторное получение терминала демоном. После демонизации закройте стандартные файловые дескрипторы и перенаправьте на /dev/null или в лог-файлы. chdir в / предотвращает блокировку размонтирования. umask(0) обеспечивает предсказуемые права доступа к файлам. Это стандартный паттерн демона.

c
#include <unistd.h>

int main() {
    pid_t child = fork();
    if (child > 0) _exit(0);  // Parent exits
    if (child < 0) return 1;

    // First child: create new session
    setsid();

    // Second fork (prevent reacquiring terminal)
    pid_t grandchild = fork();
    if (grandchild > 0) _exit(0);

    // Daemon process
    chdir("/");
    umask(0);
    close(STDIN_FILENO);
    close(STDOUT_FILENO);
    close(STDERR_FILENO);

    while (1) {
        sleep(60);
        // Do daemon work
    }
    return 0;
}

Альтернатива posix_spawn

posix_spawn — более эффективная альтернатива fork+exec на системах без MMU (встроенные) или с большим объёмом памяти (fork копирует таблицы страниц). Он объединяет создание процесса и exec в одном вызове с файловыми действиями (перенаправление, закрытие), применяемыми атомарно. Используйте posix_spawn, когда не нужно изменять состояние дочернего процесса между fork и exec. Это POSIX-стандарт, доступный на Linux, macOS и большинстве Unix-систем.

c
#include <spawn.h>
#include <sys/wait.h>

extern char **environ;

int spawn_child(const char *cmd, char *const argv[]) {
    pid_t pid;
    posix_spawn_file_actions_t actions;
    posix_spawn_file_actions_init(&actions);
    posix_spawn_file_actions_addclose(&actions, STDIN_FILENO);

    posix_spawnattr_t attr;
    posix_spawnattr_init(&attr);

    int ret = posix_spawnp(&pid, cmd, &actions, &attr, argv, environ);

    posix_spawn_file_actions_destroy(&actions);
    posix_spawnattr_destroy(&attr);

    if (ret != 0) return -1;

    int status;
    waitpid(pid, &status, 0);
    return WIFEXITED(status) ? WEXITSTATUS(status) : -1;
}

Предотвращение зомби-процессов

Зомби-процессы возникают, когда дочерний процесс завершается до вызова wait родителем. Предотвратите их: (1) обработкой SIGCHLD с waitpid в цикле (WNOHANG избегает блокировки), (2) установкой SIGCHLD в SIG_IGN (ядро автоматически собирает), или (3) двойным fork, чтобы внук остался сиротой и был усыновлён init (PID 1), который собирает его автоматически. Всегда сохраняйте и восстанавливайте errno в обработчиках сигналов.

c
#include <sys/wait.h>
#include <signal.h>

// Reap zombies with SIGCHLD handler
void sigchld_handler(int sig) {
    int saved_errno = errno;
    while (waitpid(-1, NULL, WNOHANG) > 0) {
        // Reap all available zombies
    }
    errno = saved_errno;
}

// Setup: handle SIGCHLD
struct sigaction sa;
sa.sa_handler = sigchld_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART | SA_NOCLDSTOP;
sigaction(SIGCHLD, &sa, NULL);

// Alternative: explicitly ignore (auto-reap)
// signal(SIGCHLD, SIG_IGN);

// Double-fork to prevent zombies
pid_t inter = fork();
if (inter == 0) {
    if (fork() == 0) {
        // Grandchild does the work
        execlp("sleep", "sleep", "10", NULL);
        _exit(1);
    }
    _exit(0);  // First child exits immediately
}
waitpid(inter, NULL, 0);

Лимиты ресурсов

setrlimit накладывает ограничения на ресурсы процесса: процессорное время (RLIMIT_CPU), виртуальная память (RLIMIT_AS), размер файла (RLIMIT_FSIZE), открытые файлы (RLIMIT_NOFILE), размер стека и размер core-дампа. Мягкий лимит принудителен; жёсткий лимит — это потолок. Превышение процессорного времени отправляет SIGXCPU; превышение памяти вызывает ошибку malloc. Используйте лимиты в дочерних процессах, чтобы предотвратить истощение ресурсов из-за багов или атак.

c
#include <sys/resource.h>

void set_limits() {
    struct rlimit lim;

    // Limit CPU time to 10 seconds
    lim.rlim_cur = 10; lim.rlim_max = 10;
    setrlimit(RLIMIT_CPU, &lim);

    // Limit memory to 256 MB
    lim.rlim_cur = 256*1024*1024;
    lim.rlim_max = 256*1024*1024;
    setrlimit(RLIMIT_AS, &lim);

    // Limit open files
    lim.rlim_cur = 32; lim.rlim_max = 64;
    setrlimit(RLIMIT_NOFILE, &lim);
}

void show_limits() {
    struct rlimit lim;
    getrlimit(RLIMIT_NOFILE, &lim);
    printf("Max files: soft=%lu, hard=%lu\n",
           lim.rlim_cur, lim.rlim_max);
}
21

Продвинутые каналы и IPC

Анонимные каналы

Анонимные каналы обеспечивают однонаправленное взаимодействие между родительскими и дочерними процессами. Всегда закрывайте неиспользуемый конец: писатель должен закрыть конец чтения, и наоборот. Закрытие конца записи сигнализирует EOF читателю (read возвращает 0). Каналы имеют фиксированный буфер (обычно 64 КБ); запись блокируется при заполнении. Каналы — только для связанных процессов (родитель-потомок). Для несвязанных процессов используйте именованные каналы (FIFO) или сокеты.

c
#include <unistd.h>

int main() {
    int pipefd[2];
    pipe(pipefd);

    pid_t pid = fork();
    if (pid == 0) {
        // Child: read from pipe
        close(pipefd[1]);  // Close unused write end
        char buf[256];
        ssize_t n = read(pipefd[0], buf, sizeof(buf)-1);
        buf[n] = '\0';
        printf("Child received: %s", buf);
        close(pipefd[0]);
    } else {
        // Parent: write to pipe
        close(pipefd[0]);  // Close unused read end
        const char *msg = "Hello from parent!\n";
        write(pipefd[1], msg, strlen(msg));
        close(pipefd[1]);  // EOF signal to reader
        wait(NULL);
    }
    return 0;
}

Именованные каналы (FIFO)

Именованные каналы (FIFO) — специальные файлы, действующие как каналы между несвязанными процессами. mkfifo создаёт файл; open блокируется, пока не появятся и читатель, и писатель. Используйте O_NONBLOCK для неблокирующего открытия. FIFO сохраняются в файловой системе до удаления связи (unlink). Они полезны для простого IPC между независимыми программами, но для сложного взаимодействия рассмотрите Unix domain sockets или очереди сообщений.

c
#include <sys/stat.h>
#include <fcntl.h>

// Create a named pipe (persists in filesystem)
mkfifo("/tmp/myfifo", 0666);

// Writer process
int fd = open("/tmp/myfifo", O_WRONLY);
write(fd, "Hello FIFO", 10);
close(fd);

// Reader process (can be unrelated to writer)
int fd = open("/tmp/myfifo", O_RDONLY);
char buf[256];
ssize_t n = read(fd, buf, sizeof(buf));
close(fd);

// Non-blocking open
int fd = open("/tmp/myfifo", O_RDONLY | O_NONBLOCK);

// Clean up
unlink("/tmp/myfifo");

Разделяемая память

Разделяемая память — самый быстрый IPC: процессы отображают одну физическую память в свои адресные пространства. shm_open создаёт POSIX-объект разделяемой памяти; mmap отображает его. Изменения сразу видны всем мапперам. Используйте семафоры или мьютексы (с PTHREAD_PROCESS_SHARED) для синхронизации. Всегда отменяйте отображение и unlink, чтобы избежать утечек. Разделяемая память идеальна для больших данных; накладные расходы — только начальное отображение.

c
#include <sys/mman.h>
#include <fcntl.h>

// Create shared memory object
int fd = shm_open("/my_shm", O_CREAT | O_RDWR, 0666);
ftruncate(fd, 4096);  // Set size

// Map into process address space
char *shared = mmap(NULL, 4096, PROT_READ | PROT_WRITE,
                    MAP_SHARED, fd, 0);
close(fd);  // Can close after mmap

// Write data (visible to other processes)
sprintf(shared, "Shared data at %p", (void*)shared);

// Synchronize (flush to backing store)
msync(shared, 4096, MS_SYNC);

// Unmap and clean up
munmap(shared, 4096);
shm_unlink("/my_shm");

Unix domain sockets

Unix domain sockets обеспечивают двунаправленный потоково-ориентированный IPC на одной машине. Они быстрее TCP (нет сетевых накладных расходов) и поддерживают передачу файловых дескрипторов между процессами через SCM_RIGHTS. Используйте SOCK_STREAM для надёжных потоков, SOCK_DGRAM для дейтаграмм. Путь сокета — это запись файловой системы; делайте unlink перед bind, чтобы избежать ошибок «адрес занят». Unix sockets — основа коммуникации Docker, X11 и systemd.

c
#include <sys/socket.h>
#include <sys/un.h>

// Server
int sfd = socket(AF_UNIX, SOCK_STREAM, 0);
struct sockaddr_un addr;
addr.sun_family = AF_UNIX;
strcpy(addr.sun_path, "/tmp/mysocket");
unlink("/tmp/mysocket");
bind(sfd, (struct sockaddr*)&addr, sizeof(addr));
listen(sfd, 5);

int cfd = accept(sfd, NULL, NULL);
char buf[256];
read(cfd, buf, sizeof(buf));
write(cfd, "Response", 8);
close(cfd);

// Client
int sock = socket(AF_UNIX, SOCK_STREAM, 0);
struct sockaddr_un addr;
addr.sun_family = AF_UNIX;
strcpy(addr.sun_path, "/tmp/mysocket");
connect(sock, (struct sockaddr*)&addr, sizeof(addr));
write(sock, "Request", 7);
close(sock);

unlink("/tmp/mysocket");

Очереди сообщений

POSIX-очереди сообщений обеспечивают упорядоченный по приоритету, основанный на сообщениях IPC. Каждое сообщение имеет приоритет; сообщения с более высоким приоритетом принимаются первыми. mq_send и mq_receive атомарны для отдельных сообщений. Используйте O_NONBLOCK для неблокирующей работы или mq_timedreceive для таймаутов. Очереди сообщений сохраняются до unlink, в отличие от каналов, которые умирают вместе с процессами. Они идеальны для диспетчеризации задач и уведомлений о событиях.

c
#include <mqueue.h>

// Create/open a message queue
struct mq_attr attr = {
    .mq_maxmsg = 10,
    .mq_msgsize = 256
};
mqd_t mq = mq_open("/my_queue", O_CREAT | O_RDWR, 0666, &attr);

// Send a message (priority-based)
mq_send(mq, "Hello MQ", 8, 1);  // priority 1

// Receive a message (highest priority first)
char buf[256];
unsigned int prio;
ssize_t n = mq_receive(mq, buf, sizeof(buf), &prio);
printf("Received (priority %u): %.*s\n", prio, (int)n, buf);

// Non-blocking receive
mqd_t mq_nb = mq_open("/my_queue", O_RDONLY | O_NONBLOCK);

// Timed receive
struct timespec ts = { .tv_sec = time(NULL) + 5 };
mq_timedreceive(mq, buf, sizeof(buf), &prio, &ts);

mq_close(mq);
mq_unlink("/my_queue");
22

Продвинутые Makefile и сборка

Автоматические переменные и шаблоны

Автоматические переменные делают Makefile краткими и поддерживаемыми. Правила-шаблоны (%.o: %.c) определяют, как собрать любой файл, соответствующий шаблону. -MM генерирует файлы зависимостей (.d), отслеживающие зависимости заголовков, поэтому редактирование заголовка вызывает перекомпиляцию зависимых .c файлов. Директива -include тихо включает файлы зависимостей, если они существуют. Это основа надёжных систем сборки C/C++.

c
# Automatic variables:
# $@ = target name
# $< = first prerequisite
# $^ = all prerequisites
# $? = prerequisites newer than target

# Pattern rule: compiles any .c to .o
%.o: %.c %.h
	$(CC) $(CFLAGS) -c $< -o $@

OBJS = main.o utils.o parser.o

program: $(OBJS)
	$(CC) $(LDFLAGS) $^ -o $@ $(LDLIBS)

# Automatic dependency generation
%.d: %.c
	@$(CC) -MM $< > $@

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

clean:
	rm -f $(OBJS) program

Переменные и условные операторы

Используйте := для немедленного вычисления (быстрее, предсказуемо) и = для ленивого вычисления (допускает forward-ссылки). ?= устанавливает переменную, только если не задана, позволяя пользовательские переопределения из командной строки. Условные операторы (ifeq, ifdef) включают debug/release сборки. Трюк Q заглушает эхо команд, если не установлен VERBOSE. MAKECMDGOALS содержит цели из командной строки.

c
# = recursive (evaluated when used)
# := simple (evaluated when defined)
# ?= conditional (only if not already set)

CC := gcc
CFLAGS ?= -Wall -Wextra -O2
DEBUG := $(filter debug,$(MAKECMDGOALS))

ifeq ($(DEBUG),debug)
    CFLAGS += -g -DDEBUG -O0
else
    CFLAGS += -DNDEBUG
endif

ifdef VERBOSE
    Q =
else
    Q = @
endif

build:
	$(Q)$(CC) $(CFLAGS) -c main.c

Функции и обработка текста

Функции Make обеспечивают преобразование текста: patsubst для замены по шаблону, filter для выбора файлов, wildcard для globbing, foreach для итерации. Функция shell выполняет команды во время парсинга — полезно для встраивания информации о версии. Ссылки на подстановку ($(VAR:.c=.o)) — краткая альтернатива patsubst для простых изменений суффиксов.

c
SRCS = main.c utils.c parser.c

# Substitution
OBJS = $(patsubst %.c,%.o,$(SRCS))  # main.o utils.o parser.o
# Or shorthand:
OBJS = $(SRCS:.c=.o)

# Filter files by extension
C_SRCS = $(filter %.c,$(wildcard *.c))

# Add prefix
FLAGS = $(addprefix -I,$(INCLUDE_DIRS))

# Foreach
DIRS = src lib include
CREATE = $(foreach dir,$(DIRS),mkdir -p $(dir);)

# Shell function
GIT_HASH = $(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown")
CFLAGS += -DGIT_HASH=\"$(GIT_HASH)\"

Подкаталоги и рекурсивный make

Рекурсивный make (Makefile в подкаталогах) традиционен, но может быть медленным и подверженным ошибкам при параллельных сборках. Нерекурсивный подход (единый Makefile с vpath) предпочтителен для корректности и скорости. Если используете рекурсивный make, передавайте переменные явно и используйте MAKECMDGOALS для распространения целей. Для крупных проектов рассмотрите CMake или Meson вместо raw Make для лучшего отслеживания зависимостей и поддержки IDE.

c
SUBDIRS = lib src tests

.PHONY: all $(SUBDIRS) clean test

all: $(SUBDIRS)

# Pass variables to sub-makes
$(SUBDIRS):
	$(MAKE) -C $@ $(MAKECMDGOALS)

# Parallel build: make -j4

clean: $(SUBDIRS)
	rm -f *.o program

# Non-recursive alternative (single Makefile)
vpath %.c src:lib
vpath %.h include

CFLAGS += -Iinclude -Ilib

program: main.o lib/utils.o
	$(CC) $^ -o $@

Интеграция CMake

CMake генерирует Makefile (или проекты Ninja, VS, Xcode) из декларативного CMakeLists.txt. Современный CMake использует целевые команды (target_include_directories, target_link_libraries) вместо глобальных переменных. Generator expressions ($<$<CONFIG:Debug>:...) включают флаги для каждой конфигурации. CMake — де-факто стандарт для проектов C/C++ с лучшей интеграцией IDE и кроссплатформенной поддержкой, чем raw Makefile.

c
# CMakeLists.txt (modern CMake 3.15+)
cmake_minimum_required(VERSION 3.15)
project(MyApp C)

set(CMAKE_C_STANDARD 11)
set(CMAKE_C_STANDARD_REQUIRED ON)

add_executable(myapp src/main.c src/utils.c)

target_include_directories(myapp PRIVATE include)
target_compile_options(myapp PRIVATE -Wall -Wextra)

# Per-configuration flags
target_compile_options(myapp PRIVATE
    $<$<CONFIG:Debug>:-g -O0 -DDEBUG>
    $<$<CONFIG:Release>:-O2 -DNDEBUG>
)

# Find and link library
find_package(MATH REQUIRED)
target_link_libraries(myapp PRIVATE m)

install(TARGETS myapp DESTINATION bin)
23

Отладка с GDB

Запуск и точки останова

Компилируйте с -g для встраивания отладочных символов и -O0 для отключения оптимизаций (иначе переменные могут быть оптимизированы). break устанавливает точки останова на функциях, строках или условиях. watch (точки данных) срабатывают при изменении переменной — мощно для поиска повреждения памяти. Условные точки останова (break func if cond) срабатывают только при истинности условия, полезно для циклов. tbreak — одноразовая точка останова.

c
# Compile with debug symbols
gcc -g -O0 -o program program.c

# Start GDB
gdb ./program
gdb --args ./program arg1 arg2

# Breakpoints
(gdb) break main           # Function
(gdb) break 42             # Line 42
(gdb) break file.c:50      # Specific file
(gdb) break func if x > 10 # Conditional
(gdb) tbreak main          # Temporary (removed after hit)
(gdb) watch x              # Break when x changes

# Manage breakpoints
(gdb) info breakpoints
(gdb) delete 2             # Delete breakpoint #2
(gdb) disable 1            # Temporarily disable
(gdb) enable 1

Шаги и инспекция

next проходит по вызовам функций; step входит в них. finish выполняется до конца текущей функции. Форматы print: /x (hex), /c (char), /s (строка), /t (двоичный). Оператор @ печатает срезы массива: arr@5 показывает 5 элементов. display автоматически печатает переменные на каждой остановке. backtrace показывает стек вызовов; frame N переключает контекст для инспекции этого кадра.

c
(gdb) run                  # Start program
(gdb) continue             # Continue to next breakpoint
(gdb) next                 # Step over (no function entry)
(gdb) step                 # Step into (enters functions)
(gdb) finish               # Run until function returns
(gdb) until 50             # Run until line 50

# Inspect variables
(gdb) print x              # Print variable
(gdb) print *ptr           # Dereference pointer
(gdb) print arr[0]@5       # First 5 elements
(gdb) print/x x            # Print in hex
(gdb) display x            # Auto-print at each stop
(gdb) info locals          # All local variables

# Backtrace
(gdb) backtrace            # Call stack
(gdb) frame 2              # Switch to frame #2
(gdb) up / down            # Navigate stack

Исследование памяти

Команда examine (x) инспектирует сырую память. Формат указывает счётчик, формат отображения и размер единицы. x/10i $pc дизассемблирует 10 инструкций от счётчика команд. x/s трактует память как нуль-терминированную строку. info proc mappings показывает расположение виртуальной памяти (text, data, heap, стек, разделяемые библиотеки). Это необходимо для отладки переполнений буфера и повреждения памяти.

c
# x/[count][format][size] address
(gdb) x/10x &arr           # 10 words in hex
(gdb) x/20cb &str          # 20 bytes as chars
(gdb) x/4xw ptr            # 4 words in hex
(gdb) x/s str              # String

# Format: x(hex) d(decimal) u(unsigned) o(octal)
#         t(binary) f(float) a(addr) i(instr) c(char) s(string)
# Size: b(byte) h(half/2) w(word/4) g(giant/8)

# Disassemble
(gdb) disassemble main
(gdb) disassemble /r main  # With raw bytes

# Memory map
(gdb) info proc mappings   # Process memory layout

Core-дампы

Core-дампы захватывают состояние процесса во время сбоя для пост-мортемной отладки. Включите их через ulimit -c unlimited. Загрузите core-файл с gdb program core. backtrace показывает, где произошёл сбой; info locals показывает значения переменных. Для многопоточных программ thread apply all bt показывает все состояния потоков — необходимо для анализа взаимных блокировок.

c
# Enable core dumps
ulimit -c unlimited
echo "core.%p" > /proc/sys/kernel/core_pattern

# Run program until it crashes
./program                  # Produces core.12345

# Analyze core dump
gdb ./program core.12345

(gdb) bt                   # Backtrace at crash
(gdb) bt full              # With local variables
(gdb) frame 0              # Top frame (crash location)
(gdb) info locals          # Variables at crash
(gdb) print *ptr           # What was the pointer?

# Multithreaded
(gdb) info threads         # List all threads
(gdb) thread 3             # Switch to thread 3
(gdb) thread apply all bt  # Backtraces for ALL threads

Скрипты GDB и автоматизация

.gdbinit автоматизирует общие настройки при запуске. define создаёт пользовательские команды для повторяющихся задач. commands присоединяет действия к точкам останова (например, логировать переменную и продолжать). GDB поддерживает Python-скриптинг для сложного анализа: автоматизация тестовых запусков, визуализация структур данных или извлечение статистики. Python-скрипты могут обращаться к внутренностям GDB через модуль gdb. Используйте скрипты для стандартизации рабочих процессов отладки в команде.

c
# .gdbinit file (loaded on startup)
set pagination off
set print pretty on
set print element 0        # No limit on string display

# Define custom commands
define print_array
    set $i = 0
    while $i < $arg0
        printf "[%d] = %d\n", $i, $arg1[$i]
        set $i = $i + 1
    end
end
# Usage: print_array 10 my_array

# Commands attached to breakpoints
break main
commands 1
  silent
  printf "x = %d\n", x
  continue
end

# Python scripting (GDB 7+)
python
import gdb
gdb.execute("break main")
gdb.execute("run")
val = gdb.parse_and_eval("x")
print(f"x = {int(val)}")
end
24

Выравнивание памяти и битовые поля

Выравнивание и padding структур

Компилятор вставляет padding, чтобы каждый член был естественно выровнен (обычно по своему размеру: char=1, short=2, int=4, double=8). Переупорядочивание членов от большего к меньшему минимизирует padding. Используйте offsetof для инспекции расположения. На 64-битных системах указателям нужно 8-байтовое выравнивание. Чрезмерный padding тратит память и вредит производительности кэша. Всегда упорядочивайте члены структуры по убыванию размера.

c
#include <stddef.h>

// Without alignment consideration (padded)
struct Bad {
    char a;     // 1 byte + 7 padding
    double b;   // 8 bytes
    char c;     // 1 byte + 7 padding
};  // sizeof = 24

// Reordered for efficiency
struct Good {
    double b;   // 8 bytes
    char a;     // 1 byte
    char c;     // 1 byte + 6 padding
};  // sizeof = 16

printf("Bad: a=%zu b=%zu c=%zu total=%zu\n",
    offsetof(struct Bad, a),
    offsetof(struct Bad, b),
    offsetof(struct Bad, c),
    sizeof(struct Bad));

Управление выравниванием

C11 alignas указывает минимальное выравнивание для типов или переменных — полезно для SIMD (16/32-байтовое выравнивание) и DMA. Упакованные структуры (__attribute__((packed)) или #pragma pack) удаляют весь padding, экономя место, но потенциально замедляя доступ (невыровненный доступ к памяти может вызывать ошибку на некоторых архитектурах). Используйте packed для сетевых протоколов и форматов файлов, где важно точное расположение. Никогда не пакуйте структуры, нуждающиеся в быстром доступе.

c
#include <stdalign.h>

// C11 explicit alignment
struct alignas(16) Aligned16 {
    int data[4];  // 16 bytes, 16-byte aligned
};

// Check alignment
printf("Alignment of int: %zu\n", alignof(int));     // 4
printf("Alignment of struct: %zu\n",
       alignof(struct Aligned16));  // 16

// Packed struct (no padding) - GCC/Clang
struct __attribute__((packed)) Packed {
    char a;
    int b;    // No padding after a
    char c;
};  // sizeof = 6

// MSVC packed
#pragma pack(push, 1)
struct PackedMSVC { char a; int b; char c; };
#pragma pack(pop)

Гибкие члены-массивы

Гибкие члены-массивы (C99) позволяют структуре иметь массив переменной длины в качестве последнего члена. Выделите через malloc(sizeof(struct) + желаемая_длина). Массив использует одно выделение, поэтому один free освобождает всё. Это эффективнее и чище, чем отдельный указатель + malloc. Распространено в динамических массивах, строках и заголовках сетевых пакетов. sizeof(struct) исключает гибкий массив.

c
#include <stdlib.h>

// C99 flexible array member (last member with no size)
struct Buffer {
    size_t size;
    char data[];  // Flexible array
};

// Allocate with space for data
size_t data_len = 1024;
struct Buffer *buf = malloc(sizeof(struct Buffer) + data_len);
buf->size = data_len;
memset(buf->data, 0, data_len);

// Use data
strcpy(buf->data, "Hello");

// Single free (no separate allocation for data)
free(buf);

Type punning через объединения

Объединения накладывают членов в одну память, обеспечивая type punning (переинтерпретацию битов как другого типа). Чтение члена объединения, отличного от последнего записанного, в C разрешено (implementation-defined). Type punning через объединения законно при строгом алиасинге, в отличие от приведения указателей. Анонимные объединения (C11) предоставляют членов напрямую без имени члена. Используйте объединения для тегированных вариантов и доступа к внутренностям IEEE 754 float.

c
#include <stdio.h>

// Union: members share the same memory
union Data {
    int i;
    float f;
    char bytes[4];
};

int main() {
    union Data d;
    d.i = 0x41424344;

    printf("As int: %d\n", d.i);
    printf("As float: %f\n", d.f);
    printf("As bytes: %02x %02x %02x %02x\n",
           d.bytes[0], d.bytes[1], d.bytes[2], d.bytes[3]);

    // Safe type punning (legal in C)
    union { float f; int i; } u;
    u.f = 3.14f;
    printf("Float bits: 0x%08x\n", u.i);

    // Anonymous unions (C11)
    struct Variant {
        int type;
        union { int i; float f; char *s; };
    };
}

Расположение в памяти и порядок байтов

Порядок байтов определяет последовательность байтов в памяти: little-endian (x86, ARM по умолчанию) хранит LSB первым; big-endian хранит MSB первым. При записи переносимых бинарных форматов сериализуйте через явные сдвиги вместо memcpy. Используйте dump_hex для инспекции сырой памяти при отладке. Сетевые протоколы используют big-endian (сетевой порядок байтов); используйте htonl/ntohl для переносимого кода. Всегда тестируйте на обоих порядках байтов при написании переносимого бинарного ввода-вывода.

c
#include <stdint.h>

void dump_hex(void *ptr, size_t len) {
    unsigned char *bytes = ptr;
    for (size_t i = 0; i < len; i++) {
        printf("%02x ", bytes[i]);
        if ((i + 1) % 16 == 0) printf("\n");
    }
}

int main() {
    uint32_t val = 0x12345678;
    printf("Value: 0x%08x\n", val);
    printf("Bytes: ");
    dump_hex(&val, sizeof(val));

    // Little-endian: 78 56 34 12 (LSB first)
    // Big-endian:    12 34 56 78 (MSB first)

    // Serialize to big-endian (network order)
    unsigned char be[4];
    be[0] = (val >> 24) & 0xFF;
    be[1] = (val >> 16) & 0xFF;
    be[2] = (val >> 8)  & 0xFF;
    be[3] = val & 0xFF;

    // Deserialize from big-endian
    uint32_t recovered = (be[0]<<24)|(be[1]<<16)|(be[2]<<8)|be[3];
    printf("Recovered: 0x%08x\n", recovered);
    return 0;
}
25

Продвинутые вариативные функции

Основы va_list

Вариативные функции используют va_list для доступа к переменным аргументам. va_start инициализирует список последним именованным параметром. va_arg извлекает следующий аргумент с указанным типом. va_end выполняет очистку. Вызывающий должен сообщить счётчик и типы (например, printf использует спецификаторы формата). Вариативные функции лишены типобезопасности — несоответствие типов вызывает неопределённое поведение.

c
#include <stdarg.h>

// Variadic function: variable number of arguments
int sum(int count, ...) {
    va_list args;
    va_start(args, count);  // Initialize with last named param

    int total = 0;
    for (int i = 0; i < count; i++) {
        total += va_arg(args, int);  // Get next argument
    }

    va_end(args);  // Cleanup
    return total;
}

// Usage
int result = sum(3, 10, 20, 30);  // 60
int result2 = sum(5, 1, 2, 3, 4, 5);  // 15

Вариативные функции с сигнальным завершением

Значение-сигнал (часто NULL) отмечает конец списка аргументов, устраняя необходимость в параметре счётчика. Это распространено в C-API, таких как execl. GCC __attribute__((sentinel)) предупреждает, если последний аргумент не NULL. Всегда документируйте ожидаемый сигнал. Недостаток — сигнал не может появляться как допустимое значение данных.

c
#include <stdarg.h>

// Sentinel value marks the end
void log_messages(const char *first, ...) {
    va_list args;
    va_start(args, first);

    const char *msg = first;
    while (msg != NULL) {  // NULL is the sentinel
        printf("%s\n", msg);
        msg = va_arg(args, const char *);
    }

    va_end(args);
}

// Usage: NULL terminates the list
log_messages("Error 1", "Error 2", "Error 3", NULL);

// GCC attribute to enforce sentinel
void log_messages(const char *first, ...)
    __attribute__((sentinel));

vfprintf и форматные строки

vprintf/vfprintf/vsnprintf принимают va_list вместо ..., обеспечивая пользовательские printf-подобные функции. Всегда используйте vsnprintf (с ограничением) вместо vsprintf для предотвращения переполнений буфера. Перенаправляйте va_list напрямую. Этот паттерн используется в библиотеках логирования, отчётах об ошибках и пользовательских форматировщиках. Уязвимость форматной строки (управляемый пользователем формат) — риск безопасности — никогда не передавайте пользовательский ввод как формат.

c
#include <stdarg.h>
#include <stdio.h>

// Custom printf-like function
void logf(const char *format, ...) {
    va_list args;
    va_start(args, format);

    // Add timestamp prefix
    printf("[LOG] ");
    vprintf(format, args);  // Pass va_list to vprintf
    printf("\n");

    va_end(args);
}

// Write to string with vsnprintf
void format_msg(char *buf, size_t size, const char *fmt, ...) {
    va_list args;
    va_start(args, fmt);
    vsnprintf(buf, size, fmt, args);  // Safe: bounded
    va_end(args);
}

// Usage
logf("User %s logged in (id=%d)", username, id);
char msg[256];
format_msg(msg, sizeof(msg), "Error %d: %s", code, text);

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

Указатели на функции обеспечивают callback'и и полиморфизм в C. Синтаксис return_type (*name)(params) объявляет указатель на функцию. qsort использует callback сравнения для обобщённой сортировки. Массивы указателей на функции реализуют таблицы диспетчеризации (альтернатива switch). Всегда обеспечивайте точное совпадение сигнатуры callback'а. Указатели на функции — основа обработчиков событий, плагинов и паттерна strategy в C.

c
#include <stdlib.h>

// Function pointer type
typedef int (*compare_fn)(const void *, const void *);

// Comparison function for qsort
int cmp_int(const void *a, const void *b) {
    return *(const int *)a - *(const int *)b;
}

int main() {
    int arr[] = {5, 2, 8, 1, 9, 3};
    size_t n = sizeof(arr) / sizeof(arr[0]);

    // qsort takes a function pointer
    qsort(arr, n, sizeof(int), cmp_int);

    // Array of function pointers
    double (*ops[])(double, double) = {
        add, subtract, multiply, divide
    };
    double result = ops[2](10.0, 3.0);  // multiply

    // Function pointer as parameter
    void apply(int *arr, size_t n, int (*fn)(int)) {
        for (size_t i = 0; i < n; i++) arr[i] = fn(arr[i]);
    }
    apply(arr, n, square);
    return 0;
}

Вариативные макросы

Вариативные макросы (__VA_ARGS__) принимают переменные аргументы, полезны для логирования и отладки. __VA_OPT__ (C2x) обрабатывает случай нуля аргументов, условно включая запятую. Расширение GCC ##__VA_ARGS__ удаляет предшествующую запятую, когда аргументы не переданы. Отладочные макросы, компилирующиеся в пустоту в release-сборках, устраняют накладные расходы без изменения кода. Всегда защищайте форматные строки от атак через форматную строку.

c
// C99 variadic macros
#define LOG(fmt, ...) printf("[LOG] " fmt "\n", __VA_ARGS__)

// Usage
LOG("Value: %d", x);
LOG("User %s, age %d", name, age);

// GCC __VA_OPT__ (C99/C2x): handle zero arguments
#define LOG2(fmt, ...)     printf("[LOG] " fmt "\n" __VA_OPT__(,) __VA_ARGS__)

// Count arguments (GCC extension)
#define COUNT(...) NARG_(__VA_ARGS__, 5, 4, 3, 2, 1)
#define NARG_(_1, _2, _3, _4, _5, N, ...) N

// Debug macro that compiles out in release
#ifdef NDEBUG
    #define DEBUG(fmt, ...) ((void)0)
#else
    #define DEBUG(fmt, ...) fprintf(stderr, fmt, ##__VA_ARGS__)
#endif

// ## removes comma if no variadic args (GCC extension)
DEBUG("just a message");
DEBUG("value = %d", x);

Was this helpful?

Learning path

Learn from scratch

Learn this language from the ground up with structured lessons.