Ответ
Компиляция C++ кода — это многоэтапный процесс трансляции человекочитаемого кода в машинные инструкции. Основные этапы:
-
Препроцессинг:
- Обрабатывает директивы, начинающиеся с
#. - Подключает содержимое заголовочных файлов (
#include). - Раскрывает макросы (
#define). - Выполняет условную компиляцию (
#ifdef,#if). - На выходе — «очищенный» исходный код (можно посмотреть с помощью
g++ -E source.cpp).
- Обрабатывает директивы, начинающиеся с
-
Компиляция (в узком смысле):
- Синтаксический и семантический анализ препроцессированного кода.
- Генерация промежуточного представления (чаще всего — ассемблерного кода для целевой архитектуры) или объектного кода.
- На этом этапе возникают ошибки типа «undefined identifier» или «type mismatch».
-
Ассемблирование (часто объединено с этапом 2):
- Перевод ассемблерного кода в машинный код, упакованный в объектный файл (
.oили.obj). Этот файл содержит код, данные, но адреса многих функций и переменных ещё не разрешены.
- Перевод ассемблерного кода в машинный код, упакованный в объектный файл (
-
Линковка:
- Выполняется отдельной утилитой — линкером.
- Объединяет несколько объектных файлов и библиотек (статических
.a/.libили указаний на динамические.so/.dll). - Разрешает символы: находит определение для каждого объявленного имени (функции, переменной).
- Формирует итоговый исполняемый файл или библиотеку.
Упрощённый пример команд:
# Только препроцессинг
cpp source.cpp > source.i
# Компиляция в ассемблер
g++ -S source.i -o source.s
# Ассемблирование в объектный файл
as source.s -o source.o
# Линковка (часто всё делается одной командой)
g++ source.o -o my_program Ответ 18+ 🔞
А, ну ты про то, как из этой нашей писанины на плюсах получается та штука, которую можно запустить и она тебе что-то сделает, а может и не сделать, а просто выплюнет ошибку сегментации? Ёпта, давай разжую, как бабушка манную кашу.
Представь, что твой код — это какой-то сырой, необработанный кусок мяса с костями и жилами. Сразу его в духовку не сунешь, надо подготовить. Вот компилятор — это такой суровый повар с топором.
Этап первый: Препроцессинг.
Это когда повар начинает всё это дело чистить и резать. Он видит в твоём коде всякие строчки с решёткой (#), типа #include <iostream> или #define PI 3.14. Его реакция: «Ага, понятно». Он берёт файл iostream, вываливает всё его содержимое прямо в твой код, на место этой строчки. Макрос PI везде, где встретится, заменяет на 3.14. Директивы вроде #ifdef DEBUG он проверяет — и если DEBUG не определён, то просто выкидывает целый кусок кода нахуй, как ненужные потроха. На выходе получается один огромный, раздутый текстовый файл, в котором уже нет никаких #include. Посмотреть на это безобразие можно командой g++ -E source.cpp. Там будет овердохуища текста, потому что один твой #include <vector> развернётся в тысячи строк. Волнение ебать, когда это впервые видишь.
Этап второй: Собственно компиляция.
Теперь повар смотрит на эту простыню текста и начинает её анализировать. Он проверяет, не написал ли ты какую-то хуйню: объявил переменную int a, а потом пытаешься вызвать её как функцию a(). Или использовал имя, которого вообще нет. Если находит такое — сразу кричит: «Э, сабака сука! error: ‘someVar’ was not declared in this scope». Доверия ебать ноль у него к тебе. Если всё синтаксически и семантически чисто, он переводит твой красивый C++ на язык ассемблера — примитивные команды для процессора вроде «положи это в регистр», «сравни», «перейди туда». Это уже почти машинный код, но ещё читаемый (если ты, конечно, не пидарас шерстяной, который в ассемблере не шарит).
Этап третий: Ассемблирование.
Тут уже почти финишная прямая. Полученный ассемблерный код (source.s) переводят в настоящие, честные байты — машинные инструкции, которые процессор будет кушать без вопросов. Результат складывают в объектный файл (.o или .obj). Но файл этот пока ещё ущербный, полупидор. В нём есть код твоих функций, но когда ты вызываешь, например, printf, компилятор просто оставляет пометку: «здесь должна быть функция printf». Где она? Хуй с горы знает. Её адрес неизвестен. Это как запись в блокноте: «взять деньги у Васька». А кто такой Васька и где он живёт — не написано.
Этап четвёртый, главный: Линковка.
Вот здесь приходит линкер — второй такой же суровый чувак, который берёт все твои объектные файлы и начинает их склеивать. Его задача — найти всех этих «Васьков». Он смотрит: «Ага, тут main.o просит printf. Пойду поищу». И идёт по стандартным библиотекам (вроде libc.so), по твоим другим .o-файлам, ищет, где же определена эта самая printf. Находит — подставляет правильные адреса. Если не находит — выдаёт легендарную ошибку undefined reference to..., после которой хочется кричать «ёб твою мать!». Когда все символы разрешены, линкер упаковывает всё в аккуратный (или не очень) исполняемый файл — a.out или my_program. Вот его уже можно запускать.
А чтобы ты не думал, что это какая-то манда с ушами, вот как это выглядит в командной строке, если делать вручную, как в старину:
# Развернул все include и макросы
cpp source.cpp > source.i
# Превратил в ассемблер
g++ -S source.i -o source.s
# Собрал из ассемблера объектный файл
as source.s -o source.o
# И наконец склеил всё в одну программу
g++ source.o -o my_program
Но в жизни, конечно, никто так не делает. Все просто пишут g++ source.cpp -o my_program, и компилятор сам за кулисами прогоняет все эти четыре этапа. Но если что-то пойдёт не так, теперь ты хотя бы будешь понимать, на каком именно шаге ты накрылся медным тазом.