Какие основные этапы обработки исходного кода выполняет компилятор C++?

Ответ

Компиляция C++ кода — это многоэтапный процесс трансляции человекочитаемого кода в машинные инструкции. Основные этапы:

  1. Препроцессинг:

    • Обрабатывает директивы, начинающиеся с #.
    • Подключает содержимое заголовочных файлов (#include).
    • Раскрывает макросы (#define).
    • Выполняет условную компиляцию (#ifdef, #if).
    • На выходе — «очищенный» исходный код (можно посмотреть с помощью g++ -E source.cpp).
  2. Компиляция (в узком смысле):

    • Синтаксический и семантический анализ препроцессированного кода.
    • Генерация промежуточного представления (чаще всего — ассемблерного кода для целевой архитектуры) или объектного кода.
    • На этом этапе возникают ошибки типа «undefined identifier» или «type mismatch».
  3. Ассемблирование (часто объединено с этапом 2):

    • Перевод ассемблерного кода в машинный код, упакованный в объектный файл (.o или .obj). Этот файл содержит код, данные, но адреса многих функций и переменных ещё не разрешены.
  4. Линковка:

    • Выполняется отдельной утилитой — линкером.
    • Объединяет несколько объектных файлов и библиотек (статических .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, и компилятор сам за кулисами прогоняет все эти четыре этапа. Но если что-то пойдёт не так, теперь ты хотя бы будешь понимать, на каком именно шаге ты накрылся медным тазом.