|
|
||
Ядро Linux, ядро Windows, ядро macOS. Базы данных Oracle, PostgreSQL, MySQL, SQLite. Интерпретатор Python. Веб-сервер Nginx. Виртуальные машины, компиляторы, драйверы, микроконтроллеры, системы голосового управления в автомобилях. Всё это написано на одном языке, которому больше пятидесяти лет. Язык C не умер и не умирает. Он перестал быть модным, но стал фундаментом, на котором стоит почти всё, что вы запускаете на компьютере.
Если вы знаете Python и хотите понять, что происходит под капотом вашей программы, этот курс для вас. Python прячет от вас память, типы, указатели, адреса и машинные инструкции. C раскрывает их. Прелесть C в том, что он не абстрагирует компьютер. Он показывает его устройство. Каждая переменная в C имеет адрес в памяти. Каждая функция имеет адрес в кодовом сегменте. Каждый массив занимает непрерывный блок байтов. Поняв C, вы начинаете понимать, как процессор работает с памятью, как операционная система управляет процессами и почему высокоуровневые языки жертвуют скоростью ради удобства.
Python даёт вам инструменты. C даёт понимание, как эти инструменты устроены. Список в Python за O(1) добавляет элемент в конец? Под капотом C-массив перевыделяет память через realloc. Строка в Python неизменяема? В C это просто указатель на первый байт, за которым идёт последовательность символов и нулевой байт в конце. Хеш-таблица в Python работает быстро? В C вы напишете её сами и увидите, почему она работает.
Этот курс не повторяет университетский план. Он рассчитан на человека, который уже программировал. Мы не объясняем, что такое цикл или переменная. Мы объясняем, как цикл и переменная устроены в C, чем они отличаются от Python и почему это отличие принципиально. Курс разбит на 25 тем, каждая по одному принципу: краткая теория, практическое задание, домашняя работа. Пройдя их, вы не просто выучите синтаксис C. Вы поймёте, как компьютер работает на уровне, ближайшем к железу.
Python запускается поверх интерпретатора, который написан на C. Стандартная библиотека Python, виртуальная машина CPython, модули ctypes и cffi, расширения на Cython, bindings к C-библиотекам через SWIG. Всё это разные уровни взаимодействия с одним и тем же языком. Знание C превращает вас из пользователя Python в человека, который понимает, почему list.append работает за O(1) в среднем и что происходит при переполнении буфера под капотом.
C остаётся языком выбора для задач, где важны три вещи: скорость, контроль над памятью и прямой доступ к операционной системе. В мире встраиваемых систем, операционных ядер, драйверов и сетевых стеков альтернатив C практически нет. Rust активно наступает, но в наследовании кода, существующем тридцать-сорок лет, C остаётся стандартом. Даже Rust привязан к C через FFI: все системные вызовы операционной системы экспортируются в виде C-функций.
Есть и образовательный аргумент. Язык C выстроен так, что он почти однозначно транслируется в машинный код. Каждая строка C соответствует нескольким инструкциям процессора. Это делает C идеальным инструментом для изучения архитектуры компьютера. Вы видите, где данные лежат, как они выровнены, почему целочисленное переполнение молча проходит и почему указатели на разные типы нельзя смешивать без явного приведения.
Программирование работает на нескольких уровнях абстракции. На вершине сидит Python: вы пишете result = sum(numbers), не думая ни о памяти, ни о типах, ни о процессоре. Python сам решает, где выделить память, как освободить, как преобразовать типы. Это удобно, но создаёт иллюзию, что компьютер работает с данными как абстрактными объектами.
Следующий уровень вниз - это C. Здесь вы управляете выделением памяти, указываете компилятору точный размер каждого значения, передаёте указатели и работаете с адресами. Вы определяете, когда создать буфер, когда его расширить и когда вернуть память операционной системе. Вы можете написать программу, которая читает конкретный байт по конкретному адресу, меняет его значение и записывает обратно. В Python это невозможно без специальных модулей.
Ещё ниже - ассемблер и машинный код. C не опускается до ассемблера, но даёт инструменты для прямого взаимодействия с железом: inline-ассемблер, указатели на аппаратные регистры, управление выравниванием данных. Большинство программистов никогда не опускаются ниже C, и это нормально. C даёт достаточно контроля, чтобы понимать, как железо взаимодействует с программой, не теряя при этом читаемость.
macOS поставляется с компилятором Clang - частью LLVM. Clang - это современный компилятор с открытым исходным кодом, который поддерживает все актуальные стандарты C, включая C23 (ISO/IEC 9899:2024, опубликован в октябре 2024 года). Вам не нужно устанавливать GCC. Clang на macOS - это нативный и рекомендуемый инструмент.
Для начала установите средства командной строки Xcode. Откройте терминал и выполните команду:
xcode-select --install
Появится диалоговое окно с предложением установить инструменты разработчика. Согласитесь и дождитесь завершения. После установки проверьте, что Clang доступен:
clang --version
Вы увидите версию LLVM/Clang, обычно 15.x или 16.x на современных macOS. Если Clang не найден, значит установка не завершилась или инструменты недоступны в вашей версии macOS. Для macOS Sonoma и Sequoia установка занимает несколько минут и требует около 2 ГБ свободного места.
Для управления несколькими версиями компиляторов или установки свежего LLVM (актуально для C23) можно использовать Homebrew:
brew install llvm
Это установит последнюю версию LLVM, которая включает Clang с полной поддержкой C23. По умолчанию Homebrew ставит её в /opt/homebrew/opt/llvm/ (Apple Silicon) или /usr/local/opt/llvm/ (Intel). Добавьте путь в PATH, если хотите использовать именно эту версию вместо системного Clang.
Visual Studio Code - бесплатный редактор с поддержкой C через расширение C/C++ от Microsoft. Установите VS Code с официального сайта, затем откройте панель расширений (Cmd+Shift+X) и найдите "C/C++". Установите расширение от Microsoft. Для удобства также поставьте расширение "C/C++ Extension Pack", которое добавляет шаблоны проектов и интеграцию с CMake.
Создайте папку для проекта, откройте её в VS Code и создайте файл hello.c:
#include <stdio.h>
int main(void) { printf("Hello, C!\n"); return 0; }
Для компиляции и отладки в VS Code нужно создать конфигурацию. Нажмите Cmd+Shift+P, введите "Tasks: Configure Default Build Task" и выберите "C/C++: clang build active file". VS Code создаст файл .vscode/tasks.json с конфигурацией сборки. Для отладки создайте launch.json через меню Run → Add Configuration → "C++ (GDB/LLDB)". VS Code автоматически использует LLDB на macOS.
Вот пример tasks.json для компиляции с поддержкой C23 и отладочной информацией:
{
"version": "2.0.0",
"tasks": [{
"type": "cppbuild",
"label": "C: clang build (C23)",
"command": "/usr/bin/clang",
"args": [
"-std=c2x", "-Wall", "-Wextra", "-g",
"${file}", "-o", "${fileDirname}/${fileBasenameNoExtension}"
],
"group": { "kind": "build", "isDefault": true }
}]
}
Флаг -std=c2x включает экспериментальную поддержку C23 (до выхода финального стандарта использовался флаг -std=c2x, теперь поддерживается -std=c23 в свежих версиях Clang). -Wall -Wextra включает расширенные предупреждения, -g - отладочную информацию для LLDB. После сборки нажмите F5 для запуска под отладчиком.
На macOS стандартный отладчик - это LLDB, часть LLVM. Он заменяет GDB, который традиционно используется на Linux. LLDB интегрирован в VS Code через расширение C/C++, но его можно использовать и из терминала. Основные команды LLDB для C:
lldb ./hello # запустить отладчик
breakpoint set --name main # поставить точку останова
run # запустить программу
next # выполнить строку (без входа в функции)
step # войти в функцию
print variable # вывести значение переменной
frame variable # показать все локальные переменные
continue # продолжить выполнение
Для поиска утечек памяти и ошибок работы с памятью на macOS используется AddressSanitizer (ASan). Это инструмент анализа памяти, встроенный прямо в Clang. Он перехватывает обращения к памяти и сообщает о выходе за границы, использовании освобождённой памяти и утечках. Включается одним флагом:
clang -fsanitize=address -g -o program program.c
./program
Если программа обращается к памяти за пределами выделенного буфера, ASan выведет детальный отчёт с указанием строки кода и типа ошибки. На Linux вместо ASan часто используют Valgrind, но на macOS Valgrind не работает - ASan является основной альтернативой.
Ещё один полезный инструмент - UndefinedBehaviorSanitizer (UBSan). Он ловит неопределённое поведение: переполнение знаковых чисел, деление на ноль, null-указатели. Включается аналогично: -fsanitize=undefined. Можно комбинировать с ASan: -fsanitize=address,undefined.
Язык C развивается медленно, но верно. Стандарт C89 (он же ANSI C) - это базовый C, который вы встретите в университетских учебниках. C99 добавил возможность объявлять переменные в любом месте функции (раньше - только в начале блока), комментарии //, типы long long, заголовок <stdint.h> с типами фиксированной ширины и массивы переменной длины. C11 ввёл атомарные операции, потоки, поддержку Unicode и анонимные структуры. C17 - это исправления к C11 без новых возможностей.
C23, опубликованный как ISO/IEC 9899:2024, - это самый значительный пересмотр за десятилетие. Он добавляет ключевые слова nullptr, true, false, constexpr, typeof, двоичные литералы 0b1010, разделители цифр 1'000'000, атрибуты C++-стиля [[nodiscard]], [[deprecated]], пустые инициализаторы int arr[] = {} и убирает K&R-объявления функций. По сути, C23 приближает C к современному виду, сохраняя его компактность и эффективность. Clang 16+ и GCC 13+ поддерживают большинство возможностей C23.
В этом курсе мы ориентируемся на C11 и C23. Где возможности C23 существенны, мы их отмечаем и показываем разницу с предыдущим стандартом. Используйте флаг -std=c2x или -std=c23 при компиляции, чтобы включить современные возможности.
Python и C решают разные задачи. Python - это язык высокого уровня с динамической типизацией, автоматическим управлением памятью и богатой стандартной библиотекой. C - это язык низкого уровня со статической типизацией, ручным управлением памятью и минимальной стандартной библиотекой. Но между ними есть мосты: интерпретатор CPython написан на C, и расширения Python на C - обычная практика.
В таблице ниже - ключевые отличия, к которым мы будем возвращаться на протяжении курса:
| Аспект | Python | C |
| Типизация | динамическая (проверяется во время выполнения) | статическая (проверяется при компиляции) |
| Память | сборщик мусора, авто-управление | ручное: malloc/free, стек/куча |
| Строки | объект str, неизменяемый, Unicode | char[] - массив байтов с терминатором \0 |
| Списки | list - динамический массив объектов | нет встроенного списка, массивы фиксированного размера |
| Указатели | скрыты, доступны через ctypes | основа языка, явное управление |
| Скорость | в 10-100 раз медленнее C | близка к скорости машинного кода |
| Сборка | интерпретация на лету | компиляция в машинный код |
| Ошибки | исключения (try/except) | коды возврата, errno, signal |
| Заголовки | импорт модулей (import) | #include заголовочных файлов |
Каждая тема курса будет отмечать, где Python делает что-то автоматически, а C заставляет вас делать это вручную. Это не упрёк C, а его достоинство: ручное управление даёт контроль, который в Python недоступен.
Курс состоит из 25 тем. Каждая тема построена по схеме: краткая теория (синтаксис, механика, отличия от Python), практика (код, который нужно набрать и запустить) и домашнее задание (задача для самостоятельного решения). Темы идут от простых к сложным. Первые пять - это базовый синтаксис, который вы освоите быстро, если знаете Python. Дальше начинается специфика C: указатели, память, модели представления данных. Финальные темы посвящены структурам данных, безопасности памяти и современным возможностям C23.
Рекомендуется проходить темы последовательно. Каждая последующая тема опирается на предыдущую. Код всех примеров компилируется флагами -std=c2x -Wall -Wextra. Если что-то не компилируется - проверьте версию Clang: clang --version. Для полной поддержки C23 нужен Clang 16 или новее.
Программа на C состоит из функций. Одна из них обязательно называется main - это точка входа. Функция main возвращает целое число: 0 означает успешное завершение, ненулевое значение - ошибку. Заголовочные файлы подключаются через #include - это директива препроцессора, которая вставляет содержимое файла в ваш код до компиляции.
Процесс сборки C-программы состоит из четырёх этапов: препроцессор (обработка #include, #define, макросов), компиляция (превращение кода в ассемблер), ассемблирование (ассемблер в объектный файл) и линковка (объединение объектных файлов и библиотек в исполняемый файл). В Python этот процесс скрыт: интерпретатор читает файл и выполняет его напрямую. В C каждый этап можно увидеть и контролировать.
В Python вы пишете print("Hello") и запускаете python hello.py. В C вы сначала компилируете: clang hello.c -o hello, затем запускаете: ./hello. Два шага вместо одного. Это кажется лишним, но именно компиляция в машинный код даёт C его скорость.
#include <stdio.h>
int main(void) {
printf("Hello, C!\n");
printf("Compiled with clang.\n");
return 0;
}
Скомпилируйте: clang -std=c2x -Wall -o hello hello.c. Запустите: ./hello. Вывод - две строки. Теперь посмотрите промежуточные этапы: clang -E hello.c покажет результат препроцессора, clang -S hello.c сгенерирует ассемблерный код hello.s, clang -c hello.c создаст объектный файл hello.o.
Напишите программу, которая выводит ваше имя, возраст и текущий язык программирования. Каждый факт на отдельной строке. Добавьте комментарий в начале файла с датой написания. Скомпилируйте с флагом -Wall и убедитесь, что нет предупреждений.
C - язык со статической типизацией. Каждая переменная объявляется с указанием типа, и тип не меняется до конца жизни переменной. В Python тип определяется во время выполнения: x = 5, затем x = "hello" - это нормальный Python, но ошибка в C.
Основные типы C: int (целое, обычно 4 байта), char (один байт, часто используется для символов), float (32-битное число с плавающей точкой), double (64-битное число с плавающей точкой), long (расширенное целое), short (короткое целое). Для точного контроля размера в C99 добавлен заголовок <stdint.h> с типами int32_t, uint64_t, int8_t и так далее. В C23 тип bool стал полноценным ключевым словом вместо _Bool, а true и false - встроенными константами.
В Python x = 5 создаёт объект int в куче и привязывает имя x к нему. В C int x = 5; выделяет 4 байта на стеке (если переменная локальная) и записывает туда значение 5. Нет объекта, нет ссылочного счётчика, нет сборщика мусора. Просто байты по адресу.
#include <stdio.h>
#include <stdint.h>
#include <stdbool.h> // C23: можно не подключать, bool встроен
int main(void) {
int a = 42;
double pi = 3.14159;
char grade = 'A';
int64_t big = 9223372036854775807LL;
bool flag = true; // C23: ключевое слово, не макрос
printf("int: %d, размер %zu байт\n", a, sizeof(a));
printf("double: %f, размер %zu байт\n", pi, sizeof(pi));
printf("char: %c, размер %zu байт\n", grade, sizeof(grade));
printf("int64: %lld, размер %zu байт\n", big, sizeof(big));
printf("bool: %d, размер %zu байт\n", flag, sizeof(flag));
return 0;
}
Оператор sizeof возвращает размер типа в байтах. Обратите внимание: на разных платформах размеры int и long различаются. Типы из <stdint.h> гарантируют фиксированный размер.
Объявите переменные всех базовых типов. Выведите их значения и размеры через sizeof. Поменяйте значение int на максимальное и добавьте 1. Что произойдёт? Объясните результат.
Арифметические операторы в C те же, что в Python: + - * / %. Но есть отличия. Деление целых в C даёт целое: 7 / 2 равно 3, а не 3.5. В Python 7 // 2 даёт 3, а 7 / 2 даёт 3.5. В C для дробного деления нужно хотя бы один операнд сделать дробным: 7.0 / 2 или (float)7 / 2.
Побитовые операторы в C: & (И), | (ИЛИ), ^ (исключающее ИЛИ), ~ (НЕ), << (сдвиг влево), >> (сдвиг вправо). В Python те же операторы, но Python также имеет << и >> без ограничений. В C сдвиг имеет ограничение: сдвиг на количество бит больше размера типа - неопределённое поведение.
Логические операторы в C: && (и), || (или), ! (не). В Python это and, or, not. В C нет булева типа в том смысле, в каком он есть в Python: ноль - ложь, любое ненулевое значение - истина.
#include <stdio.h>
int main(void) {
int a = 10, b = 3;
printf("a / b = %d (целочисленное деление)\n", a / b);
printf("a %% b = %d (остаток)\n", a % b);
printf("(float)a / b = %.2f\n", (float)a / b);
int mask = 0xF0;
int val = 0x0A;
printf("mask & val = 0x%02X\n", mask & val);
printf("mask | val = 0x%02X\n", mask | val);
printf("~mask = 0x%02X\n", (unsigned char)~mask);
// C23: разделитель цифр
int million = 1'000'000;
int binary = 0b1010'1010;
printf("million = %d, binary = %d\n", million, binary);
return 0;
}
Напишите программу, которая обменивает значения двух целых переменных без использования третьей переменной. Используйте побитовый XOR. Выведите значения до и после обмена.
Конструкция if/else в C идентична Python по логике, но отличается по синтаксису: вместо отступов используются фигурные скобки {}. В Python отступ - часть синтаксиса, в C отступ - только для читаемости. Это значит, что пропущенные скобки в C - частый источник багов.
Оператор switch - это множественный выбор по значению целочисленной переменной. В Python аналога нет (ближайший - match/case в Python 3.10+). В C switch работает только с целыми типами и требует break в каждом случае, иначе выполнение провалится в следующий (fall-through). Это известная ловушка C.
#include <stdio.h>
int main(void) {
int score = 85;
if (score >= 90) {
printf("Отлично\n");
} else if (score >= 70) {
printf("Хорошо\n");
} else {
printf("Удовлетворительно\n");
}
// switch с явными break
int day = 3;
switch (day) {
case 1: printf("Понедельник\n"); break;
case 2: printf("Вторник\n"); break;
case 3: printf("Среда\n"); break;
case 4: printf("Четверг\n"); break;
case 5: printf("Пятница\n"); break;
default: printf("Выходной\n");
}
return 0;
}
Напишите калькулятор, который читает два числа и оператор из ввода (используйте scanf). Реализуйте выбор операции через switch. Обработайте деление на ноль.
C имеет три типа циклов: for, while и do-while. В Python цикл for итерирует по коллекциям: for x in [1, 2, 3]. В C цикл for - это计数ный цикл с инициализацией, условием и шагом: for (int i = 0; i < 10; i++). Цикл while работает одинаково в обоих языках. do-while - это цикл с постусловием: тело выполняется хотя бы один раз, затем проверяется условие. В Python такого цикла нет.
Ключевые слова break и continue работают так же, как в Python. В C23 появился атрибут [[fallthrough]] для явного указания намеренного провала в switch.
#include <stdio.h>
int main(void) {
// for: классический счётный цикл
int sum = 0;
for (int i = 1; i <= 100; i++) {
sum += i;
}
printf("Сумма 1..100 = %d\n", sum);
// while: пока условие истинно
int n = 1024;
int bits = 0;
while (n > 0) {
n >>= 1;
bits++;
}
printf("Потребовалось %d бит\n", bits);
// do-while: хотя бы один раз
int val;
do {
printf("Введите число > 0: ");
scanf("%d", &val);
} while (val <= 0);
printf("Вы ввели: %d\n", val);
return 0;
}
Напишите программу, которая выводит таблицу умножения 1-9 с использованием двух вложенных циклов for. Формат: 3 x 7 = 21.
Функции в C объявляются с типом возвращаемого значения, именем и параметрами. Если функция используется до её определения в файле, нужно объявить прототип - сигнатуру функции без тела. В Python порядок функций в файле не важен, потому что интерпретатор читает весь файл перед выполнением. В C компилятор идёт сверху вниз, и если функция вызвана до определения, компилятор не знает её сигнатуры.
В C нет значений по умолчанию для параметров (как в Python). Все аргументы передаются по значению. Чтобы изменить значение аргумента внутри функции, нужно передать указатель на него. Это ключевое отличие от Python, где всё передаётся по ссылке на объект.
#include <stdio.h>
// Прототип - объявление без тела
int factorial(int n);
// Определение функции
int factorial(int n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
}
// Функция, меняющая значение через указатель
void swap(int *a, int *b) {
int tmp = *a;
*a = *b;
*b = tmp;
}
int main(void) {
printf("5! = %d\n", factorial(5));
int x = 10, y = 20;
swap(&x, &y); // передаём адреса
printf("x = %d, y = %d\n", x, y); // x=20, y=10
return 0;
}
Обратите внимание: swap принимает указатели int*, а вызов передаёт адреса через &. В Python аналог - swap(a, b) работает через упаковку/распаковку кортежа, но механизм совершенно иной.
Напишите функцию int gcd(int a, int b), которая вычисляет наибольший общий делитель по алгоритму Евклида. Добавьте прототип. Вызовите из main для пары чисел и выведите результат.
Массив в C - это непрерывный блок памяти фиксированного размера. В отличие от списка Python, C-массив не может расти и не хранит метаданные о своём размере. Вы должны сами отслеживать длину. Размер массива известен при компиляции, но не доступен во время выполнения при передаче массива в функцию (массив превращается в указатель).
Строка в C - это массив char, заканчивающийся нулевым байтом '\0'. Этот терминатор называется NUL (не путать с NULL - указателем). Строковый литерал "hello" - это массив из 6 байтов: h e l l o \0. В Python строка - это объект с известной длиной, кодировкой и методами. В C строка - просто байты, и вы сами отвечаете за терминатор.
#include <stdio.h>
#include <string.h>
int main(void) {
// Массив фиксированного размера
int nums[5] = {10, 20, 30, 40, 50};
for (int i = 0; i < 5; i++) {
printf("nums[%d] = %d\n", i, nums[i]);
}
// Строка - массив char
char greeting[] = "Hello";
printf("Длина строки: %zu\n", strlen(greeting)); // 5
printf("Размер массива: %zu\n", sizeof(greeting)); // 6 (вкл. \0)
// Копирование и объединение
char buf[20];
strcpy(buf, greeting);
strcat(buf, ", World!");
printf("%s\n", buf);
return 0;
}
Функция strlen считает длину до NUL, sizeof возвращает реальный размер массива. При передаче массива в функцию sizeof вернёт размер указателя, а не массива - это частая ловушка.
Напишите функцию int my_strlen(const char *s), которая считает длину строки без использования string.h. Напишите функцию void reverse_string(char *s), которая переворачивает строку на месте.
Указатель - это переменная, хранящая адрес в памяти. Если int x = 42 хранит значение 42, то int *p = &x хранит адрес, по которому лежит 42. Оператор & берёт адрес переменной, оператор * разыменовывает указатель (читает значение по адресу). В Python прямого аналога указателей нет, но id(obj) возвращает адрес объекта в памяти CPython.
Указатели - то, ради чего многие учат C. Они дают прямой доступ к памяти, но требуют дисциплины. Неверный указатель - причина большинства багов в C. Указатель на неинициализированную память, освобождённый указатель (dangling pointer), null-указатель - все это может привести к падению или тихой ошибке.
В C23 появился nullptr - типобезопасный null-указатель, заменяющий макрос NULL. Он неявно преобразуется в указатель любого типа, но не в целое число, что устраняет неоднозначности.
#include <stdio.h>
int main(void) {
int x = 42;
int *p = &x; // p хранит адрес x
printf("Значение x: %d\n", x);
printf("Адрес x: %p\n", (void*)&x);
printf("Значение p: %p\n", (void*)p);
printf("Значение *p: %d\n", *p);
// Меняем через указатель
*p = 100;
printf("Теперь x = %d\n", x); // 100
// C23: nullptr вместо NULL
int *null_ptr = nullptr;
if (null_ptr == nullptr) {
printf("Указатель пуст\n");
}
return 0;
}
Создайте переменную типа double, указатель на неё. Выведите значение, адрес и размер указателя. Измените значение через указатель. Выведите новое значение. Объясните, почему размер указателя не зависит от типа, на который он указывает.
Имя массива в C - это указатель на первый элемент. int arr[5] - arr эквивалентно &arr[0]. Арифметика указателей: arr + 1 указывает на второй элемент, arr + i - на i-й. Разыменование *(arr + i) равносильно arr[i]. В Python такого нет: индексация - это метод объекта, а не арифметика адресов.
Это значит, что массив и указатель в C взаимозаменяемы в большинстве контекстов. При передаче массива в функцию вы передаёте указатель, а не копию. Функция не знает размер массива - вы должны передать его отдельным аргументом.
#include <stdio.h>
// Массив как указатель - размер не передаётся
int sum_array(int *arr, int size) {
int total = 0;
for (int i = 0; i < size; i++) {
total += *(arr + i); // то же, что arr[i]
}
return total;
}
int main(void) {
int data[5] = {1, 2, 3, 4, 5};
printf("Сумма: %d\n", sum_array(data, 5));
// Арифметика указателей
int *p = data;
printf("p[0]=%d, p[1]=%d, p[2]=%d\n", *p, *(p+1), *(p+2));
// p++ сдвигает на sizeof(int) байт
p++;
printf("После p++: *p = %d\n", *p); // 2
return 0;
}
Напишите функцию int find_max(int *arr, int size), которая находит максимум в массиве, используя только арифметику указателей (без arr[i]). Объявите указатель, двигайте его по массиву и сравнивайте значения.
Локальные переменные и массивы в C размещаются на стеке. Стек ограничен (обычно 1-8 МБ) и автоматически очищается при выходе из функции. Для больших данных или данных, чей размер известен только во время выполнения, используется куча - динамическая память.
Функция malloc(size) выделяет size байт и возвращает указатель на начало блока. calloc(n, size) выделяет n * size байт и заполняет их нулями. realloc(ptr, new_size) меняет размер блока. free(ptr) освобождает память. В Python сборщик мусора делает это автоматически. В C вы обязаны освободить каждый выделенный блок - иначе утечка памяти.
Правило: на каждый malloc должен приходиться один free. Освобождать можно только память, выделенную через malloc/calloc/realloc. Освобождение одного блока дважды - неопределённое поведение. Освобождение NULL безопасно.
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int n;
printf("Сколько чисел? ");
scanf("%d", &n);
// Динамический массив
int *arr = malloc(n * sizeof(int));
if (arr == nullptr) {
printf("Не удалось выделить память\n");
return 1;
}
for (int i = 0; i < n; i++) {
arr[i] = i * i;
}
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
printf("\n");
free(arr); // обязательно!
arr = nullptr; // хорошая практика
return 0;
}
Напишите программу, которая читает N чисел из ввода в динамический массив, затем удваивает размер массива через realloc и заполняет вторую половину нулями. Выведите весь массив. Не забудьте free.
Память процесса в C разделена на четыре логические области. Сегмент кода (text) содержит скомпилированные инструкции. Сегмент данных (data) хранит глобальные и статические переменные, инициализированные при запуске. Сегмент BSS - неинициализированные глобальные и статические переменные, заполненные нулями. Стек - локальные переменные, аргументы функций, адреса возврата. Куча - динамически выделенная память.
Стек растёт вниз (от больших адресов к меньшим), куча - вверх. Стек управляется автоматически: при вызове функции выделяется фрейм, при возврате - освобождается. Куча управляется вручную через malloc/free. В Python стек используется для вызовов функций, но все объекты лежат в куче - даже локальные переменные являются ссылками на объекты в куче.
Понимание модели памяти - фундамент для работы с указателями. Вы должны знать, где живёт каждая переменная: на стеке (локальная), в data (глобальная статическая), в BSS (глобальная нулевая) или в куче (динамическая). Возврат указателя на стековую переменную из функции - классическая ошибка, потому что стек очищается после возврата.
#include <stdio.h>
#include <stdlib.h>
int global_var = 42; // data segment
int global_zero; // BSS (ноль по умолчанию)
int *create_heap_int(int val) {
int *p = malloc(sizeof(int));
*p = val;
return p; // OK - память в куче, переживёт функцию
}
int *bad_stack_ptr(void) {
int local = 99;
return &local; // ОШИБКА - local умрёт при возврате!
}
int main(void) {
int stack_var = 7; // стек
printf("global_var @ %p = %d\n", (void*)&global_var, global_var);
printf("global_zero @ %p = %d\n", (void*)&global_zero, global_zero);
printf("stack_var @ %p = %d\n", (void*)&stack_var, stack_var);
int *heap = create_heap_int(123);
printf("heap @ %p = %d\n", (void*)heap, *heap);
free(heap);
return 0;
}
Скомпилируйте с ASan: clang -fsanitize=address -g -o model model.c. ASan поймает возврат указателя на стек. Запустите и посмотрите отчёт об ошибке.
Напишите программу, в которой объявлены глобальная, статическая, локальная и динамическая переменные. Выведите их адреса. Объясните, почему адреса делятся на группы. Попробуйте вернуть указатель на локальную переменную и посмотрите, что скажет ASan.
Строки в C - массивы char с нулевым терминатором. Стандартная библиотека <string.h> предоставляет функции: strlen (длина), strcpy/strncpy (копирование), strcat/strncat (конкатенация), strcmp/strncmp (сравнение), strchr (поиск символа), strstr (поиск подстроки). В Python все это - методы строкового объекта. В C - функции, работающие с указателями и байтами.
Главное правило: строки неизменяемы по своей природе - строковые литералы хранятся в read-only памяти. Попытка изменить литерал "hello"[0] = 'H' - неопределённое поведение (обычно segfault). Чтобы изменить строку, поместите её в изменяемый массив char buf[] = "hello".
#include <stdio.h>
#include <string.h>
int main(void) {
char s1[] = "Hello";
char s2[] = "World";
char buf[20];
// Длина
printf("len(s1) = %zu\n", strlen(s1));
// Копирование
strcpy(buf, s1);
printf("buf = %s\n", buf);
// Конкатенация
strcat(buf, " ");
strcat(buf, s2);
printf("buf = %s\n", buf);
// Сравнение
int cmp = strcmp(s1, s2);
printf("strcmp = %d (%s)\n", cmp, cmp < 0 ? "s1 < s2" : cmp > 0 ? "s1 > s2" : "равны");
// Поиск
char *p = strchr(buf, 'W');
if (p) printf("Найден 'W' на позиции %ld\n", p - buf);
// Безопасное копирование (C11)
strncpy(buf, s1, sizeof(buf) - 1);
buf[sizeof(buf) - 1] = '\0'; // гарантия терминатора
return 0;
}
Напишите функцию char* my_strdup(const char *s), которая выделяет новую строку в куче и копирует туда содержимое s. Не забудьте free результат в main. Реализуйте int my_strcmp(const char *a, const char *b) без string.h.
Структура struct - это пользовательский тип, объединяющий несколько переменных под одним именем. Аналог в Python - класс с атрибутами без методов (dataclass). В C структура хранит данные плотно в памяти, с возможным выравниванием (padding) между полями для эффективности процессора.
Объединение union - это тип, в котором все поля разделяют одну область памяти. В любой момент времени активен только один член union. Размер union равен размеру его наибольшего члена. В Python аналога нет, но концепция похожа на типобезопасный enum с данными в других языках.
#include <stdio.h>
struct Point {
int x;
int y;
};
// C11: анонимная структура внутри union
union Value {
int i;
double d;
char s[8];
};
int main(void) {
struct Point p = {3, 4};
printf("Point: (%d, %d), размер %zu\n", p.x, p.y, sizeof(p));
// Доступ через . (точка)
p.x = 10;
p.y = 20;
printf("New Point: (%d, %d)\n", p.x, p.y);
// Union: все разделяют память
union Value v;
v.i = 42;
printf("v.i = %d\n", v.i);
v.d = 3.14;
printf("v.d = %f (v.i теперь мусор: %d)\n", v.d, v.i);
printf("sizeof union = %zu\n", sizeof(v));
return 0;
}
Определите структуру Student с полями: имя (char[50]), возраст (int), средний балл (double). Создайте массив из 3 студентов. Выведите их данные. Посчитайте средний балл всех студентов.
Указатель на структуру - обычный указатель, хранящий адрес структуры. При работе через указатель используется оператор -> вместо точки: p->x равносильно (*p).x. Это синтаксический сахар, но он используется повсеместно. В Python аналога нет - объекты и так передаются по ссылке.
Указатели на структуры - основа для связанных списков, деревьев и других структур данных. Структура может содержать указатель на собственный тип: struct Node { int val; struct Node *next; };. Так строятся все классические структуры данных в C.
#include <stdio.h>
#include <stdlib.h>
struct Node {
int val;
struct Node *next;
};
// Создание узла
struct Node *node_new(int val) {
struct Node *n = malloc(sizeof(struct Node));
n->val = val;
n->next = nullptr;
return n;
}
int main(void) {
// Построение списка: 1 -> 2 -> 3
struct Node *head = node_new(1);
head->next = node_new(2);
head->next->next = node_new(3);
// Обход
struct Node *cur = head;
while (cur != nullptr) {
printf("%d -> ", cur->val);
cur = cur->next;
}
printf("NULL\n");
// Освобождение
cur = head;
while (cur != nullptr) {
struct Node *tmp = cur;
cur = cur->next;
free(tmp);
}
return 0;
}
Добавьте в связанный список функцию void list_push_front(struct Node **head, int val), которая добавляет элемент в начало. Напишите функцию void list_print(struct Node *head). Протестируйте: добавьте 5 элементов, выведите список.
Файловый ввод-вывод в C осуществляется через тип FILE* и функции fopen, fread/fwrite, fgets/fputs, fprintf/fscanf, fclose. В Python файлы - это объекты с методами .read(), .write(), .close(), управляемые через with. В C вы открываете файл, получаете дескриптор FILE* и работаете с ним через функции.
Режимы открытия: "r" - чтение, "w" - запись (стирает файл), "a" - дозапись, "rb"/"wb" - бинарные режимы. При ошибке открытия fopen возвращает NULL - нужно проверять. Не забыть закрыть файл через fclose.
#include <stdio.h>
int main(void) {
// Запись
FILE *out = fopen("data.txt", "w");
if (out == nullptr) {
perror("fopen");
return 1;
}
fprintf(out, "Имя: %s\n", "Студент");
fprintf(out, "Возраст: %d\n", 21);
fprintf(out, "Балл: %.1f\n", 4.5);
fclose(out);
// Чтение
FILE *in = fopen("data.txt", "r");
if (in == nullptr) {
perror("fopen");
return 1;
}
char line[256];
while (fgets(line, sizeof(line), in) != nullptr) {
printf("%s", line);
}
fclose(in);
return 0;
}
Напишите программу, которая копирует текстовый файл. Имена исходного и целевого файлов передаются через аргументы командной строки. Обрабатывайте ошибки открытия. Проверьте результат на файле размером более 1000 строк.
Указатель на функцию - это переменная, хранящая адрес функции. Фожет быть вызвана через указатель. В Python функции - объекты первого класса: можно передавать как аргументы, хранить в переменных. В C функции не являются объектами, но их адреса можно брать и использовать через указатели.
Синтаксис объявления указателя на функцию неочевиден: int (*cmp)(int, int) - указатель на функцию, принимающую два int и возвращающую int. Указатели на функции используются для callback-ов, виртуальных таблиц (vtable), реализации полиморфизма и в стандартной библиотеке (например, qsort принимает функцию сравнения).
#include <stdio.h>
#include <stdlib.h>
// Функция сравнения для qsort
int cmp_asc(const void *a, const void *b) {
return *(const int*)a - *(const int*)b;
}
int cmp_desc(const void *a, const void *b) {
return *(const int*)b - *(const int*)a;
}
// C23: typedef для читаемости
typedef int (*cmp_fn)(const void*, const void*);
int main(void) {
int arr[] = {5, 2, 8, 1, 9, 3, 7, 4, 6, 0};
int n = 10;
// Сортировка по возрастанию
qsort(arr, n, sizeof(int), cmp_asc);
printf("Asc: ");
for (int i = 0; i < n; i++) printf("%d ", arr[i]);
printf("\n");
// Сортировка по убыванию через другой указатель
qsort(arr, n, sizeof(int), cmp_desc);
printf("Desc: ");
for (int i = 0; i < n; i++) printf("%d ", arr[i]);
printf("\n");
return 0;
}
Напишите функцию void apply(int *arr, int size, int (*fn)(int)), которая применяет функцию fn к каждому элементу массива. Создайте три функции: квадрат, куб, инкремент. Вызовите apply с каждой из них и выведите результат.
Список Python - это динамический массив: он автоматически растёт при добавлении элементов. В C нет встроенного динамического массива, но его можно реализовать через malloc, realloc и отслеживание размера и вместимости. Это одна из ключевых задач, которая учит понимать, как работают высокоуровневые контейнеры.
Паттерн: структура содержит указатель на буфер, текущий размер (количество элементов) и вместимость (размер буфера). При добавлении, если размер равен вместимости, буфер перевыделяется через realloc с увеличенным размером (обычно в 1.5-2 раза). Это амортизированная O(1) операция, как в Python-списке.
#include <stdio.h>
#include <stdlib.h>
typedef struct {
int *data;
size_t size;
size_t capacity;
} Vec;
Vec vec_new(void) {
Vec v = { malloc(4 * sizeof(int)), 0, 4 };
return v;
}
void vec_push(Vec *v, int val) {
if (v->size >= v->capacity) {
v->capacity *= 2;
v->data = realloc(v->data, v->capacity * sizeof(int));
}
v->data[v->size++] = val;
}
void vec_free(Vec *v) {
free(v->data);
v->data = nullptr;
v->size = v->capacity = 0;
}
int main(void) {
Vec v = vec_new();
for (int i = 1; i <= 10; i++) {
vec_push(&v, i * 10);
}
for (size_t i = 0; i < v.size; i++) {
printf("%d ", v.data[i]);
}
printf("\nsize=%zu, cap=%zu\n", v.size, v.capacity);
vec_free(&v);
return 0;
}
Добавьте в Vec функцию vec_pop (удаление последнего элемента) и vec_at (доступ по индексу с проверкой границ). Напишите функцию vec_shrink_to_fit, которая уменьшает вместимость до размера. Протестируйте все функции.
Функция main может принимать аргументы командной строки: int main(int argc, char *argv[]). argc - количество аргументов (включая имя программы), argv - массив строк. В Python это sys.argv - список строк, где argv[0] - имя скрипта. В C те же правила, но argv - массив C-строк, заканчивающийся NULL.
Аргументы передаются как строки. Для преобразования в числа используйте atoi, atol, strtol, strtod. strtol предпочтительнее atoi, так как она обрабатывает ошибки и разные системы счисления.
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[]) {
printf("argc = %d\n", argc);
for (int i = 0; i < argc; i++) {
printf("argv[%d] = %s\n", i, argv[i]);
}
if (argc < 3) {
fprintf(stderr, "Использование: %s <a> <b>\n", argv[0]);
return 1;
}
int a = atoi(argv[1]);
int b = atoi(argv[2]);
printf("%d + %d = %d\n", a, b, a + b);
return 0;
}
Запуск: ./prog 5 7. Выведет 5 + 7 = 12. Попробуйте без аргументов - получите сообщение об использовании.
Напишите программу-калькулятор, которая принимает операцию и два числа через аргументы: ./calc add 3 5, ./calc mul 4 6. Поддержите операции: add, sub, mul, div. Обработайте деление на ноль и неверное количество аргументов.
Препроцессор C - это текстовый процессор, работающий до компиляции. Он обрабатывает директивы #include (вставка файла), #define (макросы), #ifdef/#ifndef/#endif (условная компиляция), #pragma (управление компилятором). В Python нет препроцессора, но концепция условного импорта есть через try/except ImportError.
Макросы #define - это текстовая замена. #define PI 3.14159 заменит все вхождения PI на 3.14159. Макросы с параметрами: #define SQUARE(x) ((x) * (x)). В C23 рекомендуется использовать constexpr и static_assert вместо многих макросов. Также в C23 появились #elifdef и #elifndef для удобства.
#include <stdio.h>
#define VERSION "1.0"
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define MIN(a, b) ((a) < (b) ? (a) : (b))
// C23: constexpr вместо макроса
constexpr double E = 2.71828;
#ifdef DEBUG
#define LOG(msg) printf("[DEBUG] %s\n", msg)
#else
#define LOG(msg) ((void)0)
#endif
int main(void) {
printf("Version: %s\n", VERSION);
printf("MAX(3, 7) = %d\n", MAX(3, 7));
printf("MIN(3, 7) = %d\n", MIN(3, 7));
printf("E = %f\n", E);
LOG("Это сообщение видно только в debug-сборке");
return 0;
}
Скомпилируйте с -DDEBUG и без. Сравните вывод. Флаг -D определяет макрос из командной строки.
Напишите макрос ARRAY_LEN(arr), который вычисляет длину статического массива через sizeof(arr) / sizeof(arr[0]). Объясните, почему этот макрос не работает для динамических массивов (указателей).
В C проект разбивается на файлы: .c - исходный код (определения функций), .h - заголовочные файлы (объявления, прототипы, типы). Заголовочный файл включается через #include в каждый .c, который использует его содержимое. В Python модули импортируются через import, и система сама находит файлы. В C вы управляете связями вручную.
Защита от повторного включения (include guard) - стандартная практика. В C23 появился #pragma once как стандартизированная альтернатива include guards. Ключевое слово extern объявляет переменную или функцию, определённую в другом файле.
Файл math_utils.h:
#pragma once // C23: стандартизированный include guard int gcd(int a, int b); int lcm(int a, int b);
Файл math_utils.c:
#include "math_utils.h"
int gcd(int a, int b) {
while (b != 0) {
int t = b;
b = a % b;
a = t;
}
return a;
}
int lcm(int a, int b) {
return (a / gcd(a, b)) * b;
}
Файл main.c:
#include <stdio.h>
#include "math_utils.h"
int main(void) {
printf("gcd(12, 18) = %d\n", gcd(12, 18));
printf("lcm(12, 18) = %d\n", lcm(12, 18));
return 0;
}
Компиляция: clang -std=c2x -Wall main.c math_utils.c -o app. Компилятор собирает оба файла и линкует их вместе.
Создайте заголовочный файл string_utils.h и реализацию string_utils.c с функциями: my_strlen, my_strcpy, my_strcmp. Напишите main.c, который использует все три. Скомпилируйте многофайловый проект.
Связанный список - это структура данных, где каждый узел содержит значение и указатель на следующий узел. В Python список - это встроенный динамический массив, а не связанный список. В C вы строите связанный список вручную через структуры и указатели. Это учит работать с динамической памятью и указателями в связке.
Кроме односвязного списка, бывают двусвязные (указатели на следующий и предыдущий), циклические (последний указывает на первый), стеки (LIFO через голову списка), очереди (FIFO через голову и хвост). Все они строятся из узлов, malloc и указателей.
#include <stdio.h>
#include <stdlib.h>
typedef struct Node {
int val;
struct Node *next;
} Node;
typedef struct {
Node *head;
int size;
} List;
List *list_create(void) {
List *l = malloc(sizeof(List));
l->head = nullptr;
l->size = 0;
return l;
}
void list_push(List *l, int val) {
Node *n = malloc(sizeof(Node));
n->val = val;
n->next = l->head;
l->head = n;
l->size++;
}
void list_print(List *l) {
Node *cur = l->head;
while (cur) {
printf("%d ", cur->val);
cur = cur->next;
}
printf("\n");
}
void list_free(List *l) {
Node *cur = l->head;
while (cur) {
Node *tmp = cur;
cur = cur->next;
free(tmp);
}
free(l);
}
int main(void) {
List *l = list_create();
for (int i = 1; i <= 5; i++) list_push(l, i * 10);
list_print(l);
printf("size = %d\n", l->size);
list_free(l);
return 0;
}
Добавьте функцию list_remove(List *l, int val), которая удаляет первое вхождение значения. Напишите функцию list_reverse(List *l), которая разворачивает список на месте. Протестируйте обе функции.
C не проверяет границы массивов, не инициализирует память автоматически и не отслеживает освобождённые указатели. Это делает его быстрым, но опасным. Переполнение буфера - самая распространённая уязвимость в C-коде. Использование освобождённой памяти (use-after-free), двойное освобождение, неинициализированные переменные - все это потенциальные источники сбоев и уязвимостей.
AddressSanitizer (ASan) - инструмент от Google, встроенный в Clang и GCC. Он инструментирует код при компиляции и проверяет каждый доступ к памяти во время выполнения. ASan ловит: переполнение буфера (heap и stack), use-after-free, double-free, использование неинициализированной памяти (через MemorySanitizer), утечки памяти (через LeakSanitizer). Замедление программы - 2-3x, но для отладки это приемлемо.
#include <stdio.h>
#include <stdlib.h>
// Баг 1: переполнение буфера
void buffer_overflow(void) {
int *arr = malloc(5 * sizeof(int));
for (int i = 0; i <= 5; i++) { // i <= 5 - выход за границу!
arr[i] = i;
}
free(arr);
}
// Баг 2: use-after-free
void use_after_free(void) {
int *p = malloc(sizeof(int));
*p = 42;
free(p);
printf("Значение: %d\n", *p); // BUG!
}
// Баг 3: утечка памяти
void memory_leak(void) {
int *p = malloc(100 * sizeof(int));
p[0] = 1;
// нет free - утечка
}
int main(void) {
// Раскомментируйте по одной функции и запустите с ASan
// buffer_overflow();
// use_after_free();
// memory_leak();
printf("Все функции с багами закомментированы\n");
return 0;
}
Скомпилируйте: clang -fsanitize=address,undefined -g -o bugs bugs.c. Раскомментируйте по одной функции, запустите, изучите отчёт ASan. Каждая ошибка будет показана с указанием строки и типа.
Напишите программу с тремя функциями, в каждой из которых есть один из багов: переполнение стека (большой локальный массив), неинициализированная переменная, двойное освобождение. Скомпилируйте с ASan и UBSan. Зафиксируйте, какие ошибки и на каких строках они находят. Исправьте все баги.
typedef создаёт псевдоним для типа. Это улучшает читаемость: вместо unsigned long long - u64. В Python аналога нет (но есть type в Python 3.12 через PEP 695). В C typedef не создаёт новый тип, а лишь даёт имя существующему.
enum - перечисление, набор именованных констант. В Python аналогичен enum.Enum из модуля enum. В C23 enum может иметь указанный базовый тип: enum Color : int { RED, GREEN, BLUE };. Также в C23 добавлена возможность хранить значения, не помещающиеся в int.
#include <stdio.h>
#include <stdint.h>
// typedef для читаемости
typedef uint32_t u32;
typedef uint64_t u64;
typedef int (*cmp_fn)(int, int);
// enum для состояний
typedef enum {
STATE_IDLE = 0,
STATE_RUNNING = 1,
STATE_STOPPED = 2,
STATE_ERROR = -1
} State;
// C23: enum с указанным типом
enum Status : uint8_t {
OK = 0,
WARNING = 1,
FAIL = 2
};
const char *state_name(State s) {
switch (s) {
case STATE_IDLE: return "IDLE";
case STATE_RUNNING: return "RUNNING";
case STATE_STOPPED: return "STOPPED";
case STATE_ERROR: return "ERROR";
default: return "UNKNOWN";
}
}
int main(void) {
u32 counter = 0;
State current = STATE_RUNNING;
enum Status st = OK;
printf("Counter: %u, State: %s, Status: %d\n",
counter, state_name(current), st);
return 0;
}
Создайте typedef enum Direction { NORTH, SOUTH, EAST, WEST }. Напишите функцию, которая принимает Direction и возвращает название направления как строку. Создайте typedef для struct Point { int x, y; } под именем Point. Используйте оба типа в программе.
Variadic функция - функция с переменным числом аргументов. В Python *args собирает лишние аргументы в кортеж. В C variadic-функция использует заголовок <stdarg.h> и макросы va_list, va_start, va_arg, va_end. Функция printf - variadic: первый аргумент определяет, сколько и каких типов аргументов последует.
В C23 упростили variadic-функции: va_start больше не требует второго аргумента (имени последнего параметра перед ...), а в прототипе можно писать ... без обязательного именованного параметра перед ним.
#include <stdio.h>
#include <stdarg.h>
// Функция, суммирующая N чисел
int sum_n(int count, ...) {
va_list args;
va_start(args, count); // C23: можно без второго аргумента
int total = 0;
for (int i = 0; i < count; i++) {
total += va_arg(args, int);
}
va_end(args);
return total;
}
// Аналог printf с префиксом
void log_msg(const char *prefix, const char *fmt, ...) {
printf("[%s] ", prefix);
va_list args;
va_start(args, fmt);
vprintf(fmt, args);
va_end(args);
}
int main(void) {
printf("sum = %d\n", sum_n(4, 10, 20, 30, 40));
log_msg("INFO", "x=%d, y=%d\n", 5, 7);
log_msg("ERROR", "код %d\n", 42);
return 0;
}
Напишите функцию int max_n(int count, ...), которая находит максимум среди переданных чисел. Напишите функцию void print_all(const char *first, ...), которая печатает все строки до NULL. Протестируйте обе функции.
C23 (ISO/IEC 9899:2024) - самый значительный пересмотр стандарта за десятилетие. Он добавляет возможности, которые приближают C к современным языкам, не разрушая его компактности. Ключевые нововведения: nullptr вместо NULL, true/false как ключевые слова, constexpr для констант времени компиляции, typeof для вывода типа, двоичные литералы 0b1010, разделители цифр 1'000'000, атрибуты C++-стиля [[nodiscard]], [[deprecated]], [[fallthrough]], пустые инициализаторы int arr[] = {}, #embed для включения бинарных файлов.
C23 удаляет устаревшие возможности: K&R-объявления функций (функции без прототипа), неявный int, макрос NULL остаётся, но nullptr предпочтительнее. Заголовок <stdbool.h> больше не нужен - bool, true, false стали ключевыми словами. Эти изменения делают C безопаснее и читаемее, не жертвуя производительностью.
#include <stdio.h>
// C23: bool, true, false - встроены, stdbool.h не нужен
// constexpr вместо #define
constexpr int BUFFER_SIZE = 256;
// [[nodiscard]] - компилятор предупредит, если результат проигнорирован
[[nodiscard]] int compute_important(void) {
return 42;
}
// [[maybe_unused]] - подавить предупреждение о неиспользуемом параметре
void debug_print([[maybe_unused]] int verbose, int value) {
printf("value = %d\n", value);
}
// typeof - вывод типа
#define SWAP(a, b) do { \
typeof(a) tmp = (a); \
(a) = (b); \
(b) = tmp; \
} while (0)
int main(void) {
// Двоичный литерал и разделитель цифр
int mask = 0b1010'1010;
int million = 1'000'000;
printf("mask = %d, million = %d\n", mask, million);
// bool как ключевое слово
bool active = true;
if (active) printf("Активен\n");
// Пустой инициализатор (все нули)
int zeros[5] = {};
for (int i = 0; i < 5; i++) printf("%d ", zeros[i]);
printf("\n");
// [[nodiscard]]
int result = compute_important(); // если убрать присваивание - предупреждение
// typeof через макрос SWAP
int x = 10, y = 20;
SWAP(x, y);
printf("x=%d, y=%d\n", x, y);
// nullptr - типобезопасный null
int *p = nullptr;
if (p == nullptr) printf("Указатель пуст\n");
return 0;
}
Скомпилируйте: clang -std=c2x -Wall -Wextra -o modern modern.c. Если Clang старше 16, некоторые возможности могут не работать. Проверьте версию: clang --version.
Перепишите программу из темы 1, используя все возможности C23: constexpr для констант, bool для флагов, nullptr для указателей, [[nodiscard]] для функций, двоичные литералы и разделители цифр. Сравните с оригинальной версией: что стало чище и безопаснее.
25 тем этого курса - не предел, а фундамент. Дальше идёт системное программирование: работа с POSIX API, сокеты, процессы, потоки, мьютексы, условные переменные. Затем - углубление в архитектуру: выравнивание данных, кэш-линейки, векторизация, SIMD-инструкции. Потом - встраиваемые системы: прямая работа с регистрами, прерывания, DMA, реальное время.
C - не конечная точка, а отправная. После C легче понять Rust, который решает проблемы безопасности памяти, не жертвуя производительностью. Знание C делает вас лучшим Python-программистом: вы понимаете, что стоит за ctypes, cffi, Cython и как писать расширения на C для Python. Вы видите, почему list амортизированно O(1) и как сборщик мусора находит недостижимые объекты.
Главное, что даёт этот курс, - не синтаксис, а модель мышления. Вы начинаете видеть программу как поток байтов в памяти, управляемый указателями и инструкциями. Это уровень абстракции, который недоступен из Python, Java или JavaScript. И именно он отличает программиста, который понимает компьютер, от программиста, который умеет пользоваться инструментами.
Учите C. Не потому что он модный, а потому что он показывает, как устроена машина. А машина - это то, на чём вы работаете каждый день.
|