Сборка программы — три инструмента, три модели ошибок
Когда вы нажимаете «Build» в IDE или запускаете g++, кажется, что исходник мгновенно превращается в программу. На деле это цепочка из трёх принципиально разных инструментов — препроцессор, компилятор, компоновщик — и у каждого собственный язык ошибок. Большинство «загадочных» ошибок («undefined reference», «multiple definition», «use of undeclared identifier») становится очевидным, как только понятно, на каком этапе они возникают.
Ключевая абстракция всего этого — единица трансляции (Translation Unit): один .cpp после препроцессирования вместе со всеми включёнными заголовками. Компилятор работает с одной TU за раз и ничего не знает о других — поэтому #include буквально вставляет текст, а имена обязаны быть либо объявлены в каждой TU, либо разрешены позже компоновщиком. Из этого вытекают правила ODR, linkage, inline, static в глобальной области, и связанные с ними виды ошибок.
C++ также уникален тем, что часть программы выполняется до запуска — на этапе компиляции: constexpr, consteval, constinit, static_assert, шаблоны. Это даёт нулевую стоимость во время выполнения, но требует понимания, где именно происходит каждое вычисление. Полная карта — в слоях ниже.
Карта темы
- Препроцессор —
#include,#define, условная компиляция, header guards /#pragma once, почему#include <iostream>замедляет сборку. - Компилятор — этапы компиляции, ключевые флаги GCC/Clang/MSVC, диагностика, оптимизации, ABI.
- Компоновщик — статическая vs динамическая компоновка, разрешение символов, ODR,
undefined referencevsmultiple definition. - Видимость и связывание — scope, linkage (internal/external/none),
staticв глобальной области,extern, anonymous namespace. - Compile-time вычисления —
constexpr,consteval,constinit,static_assert, шаблонные вычисления и where the work happens.
Частые ошибки и ловушки
| Ошибка | Этап | Причина |
|---|---|---|
undefined reference to 'foo' | Компоновщик | Объявление есть, определение не слинковано (забыли .o или библиотеку) |
multiple definition of 'foo' | Компоновщик | Нарушение ODR — определение в заголовке без inline попало в несколько TU |
use of undeclared identifier | Компилятор | Имя не объявлено в этой TU — нет нужного #include |
error: 'foo' redefined | Компилятор | Заголовок включён дважды без include-guard / #pragma once |
Глобальная не-static переменная в .h | Компоновщик | multiple definition — нужен inline (C++17) или extern + определение в .cpp |
static_assert в шаблоне без зависимости от параметра | Компилятор | Срабатывает всегда, не только при инстанцировании |
consteval вызов с runtime-аргументом | Компилятор | consteval обязан выполниться во время компиляции |
| Полагаться на порядок инициализации глобалов между TU | Runtime | Static initialization order fiasco — порядок не определён |
#include <bigheader> в часто включаемом заголовке | Компилятор | Каждая TU перепарсит мегабайты кода — медленная сборка |
| Смешать сборки с разным ABI/stdlib | Компоновщик/Runtime | Ломаются std::string, исключения, RTTI между объектниками |
Значение для собеседований
Сборка программы — фундаментальная тема, проверяемая со стороны junior и regularly возвращающаяся для middle/senior уже в контексте больших проектов и шаблонов. Интервьюер смотрит не на знание флагов наизусть, а на способность объяснить «почему оно так устроено».
Что обычно проверяют:
- Чем отличается ошибка компилятора от ошибки компоновщика — и как это понять по тексту.
- Что такое единица трансляции и зачем нужна раздельная компиляция.
- Правило одного определения (ODR) — где можно дать определение, а где нельзя.
- Почему функция в заголовке требует
inline, а класс — нет. - Разница между статической и динамической библиотекой.
- Что значит
staticв глобальной области vs внутри класса vs внутри функции. - Что делает
extern "C", и зачем. - Где происходит вычисление: compile-time vs runtime;
constexprvsconstevalvsconstinit.
Типичный неверный ответ: «undefined reference — значит я забыл #include». Это сразу запускает обсуждение разницы между объявлением (которое решает проблему use of undeclared identifier на этапе компиляции) и определением (которое должно быть скомпоновано — undefined reference приходит от линкера).