Компилятор — это программа, которая переводит исходный код, написанный на языке программирования, в машинный код или другое низкоуровневое представление, готовое к выполнению. Компиляция — это сам процесс такого перевода: компилятор читает текст программы целиком, проверяет его на ошибки и порождает результат (чаще всего исполняемый файл), который процессор способен выполнить. Ниже разберём, из каких этапов состоит компиляция, чем компилятор отличается от интерпретатора и JIT и как это выглядит на практике на примерах gcc, clang, javac и rustc.
Содержание
Процессор понимает только машинный код — последовательности нулей и единиц для конкретной архитектуры. Писать программы напрямую в нём почти невозможно, поэтому разработчики используют языки высокого уровня, а компилятор служит переводчиком между текстом программы и «железом».
Этапы компиляции
Классический компилятор обрабатывает программу в несколько шагов. В реальных инструментах эти шаги могут частично перекрываться или объединяться, но логически принято выделять следующие фазы.
- Лексический анализ (лексер). Компилятор разбивает текст на токены — неделимые единицы: имена переменных, ключевые слова, числа, операторы, скобки. Например, строка
x = 10 + 2превращается в токеныx,=,10,+,2. - Синтаксический анализ (парсер). Из потока токенов строится дерево разбора (AST, абстрактное синтаксическое дерево), которое отражает структуру программы: где выражение, где присваивание, где вызов функции. Здесь ловятся ошибки вида «пропущена скобка» или «нет точки с запятой».
- Семантический анализ. Проверяется смысл: совпадают ли типы, объявлена ли переменная до использования, существует ли вызываемая функция. На этом этапе всплывают ошибки вроде «нельзя сложить строку и число» или «переменная не объявлена».
- Оптимизация. Компилятор преобразует промежуточное представление кода, чтобы итоговая программа работала быстрее или занимала меньше места: убирает недостижимый код, сворачивает константы, разворачивает циклы. Глубину оптимизации обычно регулируют флагами (например, у gcc и clang это
-O0,-O1,-O2,-O3). - Генерация кода. Из оптимизированного промежуточного представления порождается машинный код (или ассемблерный код, который затем собирается в объектный файл). Отдельный этап — компоновка (линковка): объектные файлы и подключённые библиотеки собираются в один исполняемый файл.
Важно: строгая последовательность «этап за этапом» — это упрощение. Современные компиляторы часто используют общее промежуточное представление (например, инфраструктура LLVM, на которой построен clang) и могут выполнять анализ и оптимизацию в несколько проходов. Но набор задач — разбор, проверка, оптимизация, генерация — остаётся тем же.
Компилятор, интерпретатор и JIT — в чём разница
Компиляция — не единственный способ выполнить программу. Есть ещё интерпретация и промежуточный подход JIT. Разница в том, когда и во что переводится исходный код.
| Признак | Компилятор (AOT) | Интерпретатор | JIT-компилятор |
|---|---|---|---|
| Когда переводит код | заранее, до запуска (AOT, ahead-of-time) | во время выполнения, по мере чтения | во время выполнения, «горячие» участки |
| Во что переводит | в машинный код целиком | ничего не сохраняет, выполняет напрямую | в машинный код на лету, с кэшированием |
| Скорость запуска | медленнее (нужна сборка) | быстрый старт | быстрый старт, разгон по ходу работы |
| Скорость работы | высокая | ниже | близка к скомпилированной после «прогрева» |
| Поиск ошибок | много ошибок до запуска | часть ошибок только при выполнении строки | как у интерпретируемых, плюс профилирование |
| Примеры | gcc, clang, rustc | классический CPython, Ruby | JVM (HotSpot), движок V8 в JavaScript, .NET CLR |
Отдельно про Java и байт-код: javac компилирует исходный код не в машинный код, а в байт-код (файлы .class). Затем виртуальная машина JVM исполняет этот байт-код, а её JIT-компилятор на ходу переводит часто выполняемые участки в машинный код. Похожая схема — у C# и платформы .NET. Это гибрид: сначала компиляция в промежуточный формат, потом исполнение с JIT.
Термин AOT (ahead-of-time) означает «компиляция заранее», в отличие от JIT (just-in-time, «точно в срок»). AOT-подход применяют и к языкам с виртуальной машиной: например, GraalVM собирает нативный исполняемый файл из Java-программы заранее, без прогрева JIT.
Пример: от исходника к исполняемому файлу
Возьмём простую программу на C и посмотрим, что делает компилятор. Файл hello.c:
#include <stdio.h>
int main(void) {
printf("Hello, Otus\n");
return 0;
}
Компилируем компилятором gcc и запускаем результат:
$ gcc hello.c -o hello
$ ./hello
Hello, Otus
Команда gcc hello.c -o hello прошла все этапы (разбор, проверка, оптимизация, генерация, линковка) и создала исполняемый файл hello. Именно его запускает ./hello, а не исходный текст. Тот же результат даст clang: clang hello.c -o hello.
Теперь покажем, как компилятор ловит ошибку. Уберём точку с запятой после printf:
printf("Hello, Otus\n")
return 0;
Компилятор откажется собирать программу и выдаст примерно такое сообщение:
hello.c: In function 'main':
hello.c:5:5: error: expected ';' before 'return'
Исправление — вернуть точку с запятой:
printf("Hello, Otus\n");
return 0;
Это ключевое преимущество компиляции: синтаксические и многие смысловые ошибки видны до запуска, а не тогда, когда программа уже работает у пользователя.
Виды компиляторов
- AOT-компиляторы переводят программу в машинный код заранее. Так работают gcc и clang (для C, C++ и других языков), а также rustc — компилятор языка Rust, построенный на инфраструктуре LLVM.
- JIT-компиляторы встроены в среды выполнения и компилируют код по ходу работы: JVM для Java, V8 для JavaScript, CLR для C#.
- Кросс-компиляторы собирают программу под другую платформу, а не ту, на которой запущен компилятор. Например, на компьютере с процессором x86 можно собрать программу для микроконтроллера ARM. GCC и Clang умеют работать как кросс-компиляторы.
- Транспиляторы переводят код с одного высокоуровневого языка на другой (например, TypeScript в JavaScript). Это тоже компиляция, только целевой язык — не машинный код.
- Компиляторы компиляторов (генераторы парсеров, например Bison или ANTLR) порождают части компилятора по формальному описанию грамматики языка.
Выводы
- Компилятор переводит исходный код в машинный код или другое исполнимое представление заранее, целиком, до запуска программы; компиляция — это сам процесс перевода.
- Компиляция логически делится на лексический анализ, синтаксический анализ, семантический анализ, оптимизацию и генерацию кода, но в реальных инструментах эти фазы часто перекрываются и используют общее промежуточное представление.
- Главное отличие от интерпретатора — момент перевода: компилятор переводит всё заранее и ловит много ошибок до запуска, интерпретатор выполняет код построчно во время работы.
- JIT — гибрид: код компилируется в машинный на лету, во время выполнения; так работают JVM, движок V8 и платформа .NET. AOT и JIT различают компиляцию «заранее» и «в момент выполнения».
- gcc и clang компилируют C и C++ в машинный код, rustc — язык Rust, а javac переводит Java в байт-код для JVM, а не сразу в машинный код.
Где применяется / связь с практикой
Понимание того, как работает компилятор, полезно на практике каждый день: по его сообщениям об ошибках вы находите баги ещё до запуска, флагами оптимизации управляете скоростью и размером программы, а выбором между компилируемыми и интерпретируемыми языками — балансом между скоростью разработки и скоростью работы. Особенно это заметно в C и C++, где компилятор и его настройки напрямую влияют на производительность.
Разобраться в компиляции глубже и научиться писать быстрый код на C++ помогает курс C++ Developer. Professional: там подробно разбирают устройство сборки, оптимизации и работу с памятью. Если хочется сначала присмотреться к теме без обязательств, посмотрите записи и расписание бесплатных вебинаров Otus — там регулярно разбирают языки программирования и инструменты разработчика.
Смежные темы: Двоичный код: что это такое, как работает и где применяется, Java — что нужно знать новичку о языке программирования, Где писать программный код: программы для программирования в 2026 году.
FAQ
Компиляция и сборка (build) — это одно и то же?
Не совсем. Компиляция — это перевод исходного кода в объектный или машинный код. Сборка обычно шире: она включает компиляцию всех файлов, линковку с библиотеками, а иногда запуск тестов и упаковку. В обиходе слова часто используют как синонимы, но строго сборка — это процесс, частью которого является компиляция.
Можно ли декомпилировать исполняемый файл обратно в исходный код?
Частично. Декомпиляторы восстанавливают структуру программы и приблизительный код, но не исходный текст с комментариями и понятными именами переменных — при компиляции они теряются. Для байт-кода (например, Java) восстановление точнее, чем для нативного машинного кода.
Почему одна и та же программа может по-разному работать после компиляции разными компиляторами?
Компиляторы отличаются набором оптимизаций, версией и трактовкой неопределённого поведения языка. Один и тот же код gcc и clang могут собрать в чуть разный машинный код, а разные флаги оптимизации меняют скорость и иногда поведение программы, если в ней есть ошибки, зависящие от неопределённого поведения.



