Лекция 5

05.09.2025 Обновлено: 05.09.2025

Лекция 5. Компиляция

Раздельная компиляция — зачем

  • Уменьшение количества строк в одном файле.
  • Ускорение компиляции (пересобираются только изменённые модули).
  • Разделение программы на логические модули, которые можно переиспользовать.
  • Проще читать код.
  • Разделение на .h- и .cpp-файлы нужно для разных этапов компиляции.

Этапы трансляции (на самом деле их около 9)

  1. Препроцессор — создаёт блоки (единицы трансляции), включая .h-файлы в .cpp-файлы.
  2. Компиляция — получает из каждой единицы трансляции объектные файлы (.o) — скомпилированный код, независимый для каждой единицы трансляции.
  3. Линковщик (Linker) — собирает объектные файлы в одну программу.
a.hpp  b.hpp  c.hpp  d.hpp
  ↓      ↓      ↓
a.cpp  b.cpp  c.cpp
  ↓      ↓      ↓
─── Preprocessor ───
  ↓      ↓      ↓
d.hpp  c.hpp  d.hpp
b.hpp  b.hpp  c.hpp
a.hpp  a.cpp  b.cpp
a.cpp  b.cpp  c.cpp
  ↓      ↓      ↓
──── Compiler ────
  ↓      ↓      ↓
 a.o    b.o    c.o
       ↓
     Linker
       ↓
    a.exe
  • Раздельная компиляция позволяет перекомпилировать только изменённые части.

Запуск этапов трансляции отдельно (на примере clang)

ФлагДействие
clang++ -Eтолько препроцессор
clang++ -Sпрепроцессор + компиляция
clang++ -cпрепроцессор + компиляция + ассемблирование
clang++ --Xlinkerзапуск линковщика

Declaration vs Definition

  • Declaration — задаёт имя и прочие атрибуты для сущностей (например, сигнатуру функции). Может встречаться сколько угодно раз.
  • Definition — полностью определяет сущность; является одновременно и объявлением. Должно быть ровно одно (ODR — One Definition Rule).

Заголовочные файлы (.h / .hpp)

Содержат:

  • Объявления (иногда определения) функций.
  • Переменные.
  • Типы.
  • Статические объявления и определения.
  • Шаблоны (только определения, т.к. они требуются для инстанциации).

Этап препроцессора

  • Анализирует код и ищет части, соответствующие языку препроцессора (лексический анализ).
  • Исполняет директивы (команды языка препроцессора).
  • Препроцессор не знает ничего о C++, он знает только свой язык.

Директива #include — включает весь контекст файла в другой файл:

  • <...> — для файлов из системных директорий.
  • "..." — для файлов пользователя (поиск начинается с текущей директории).

Директива #define — определяет имя, которое может быть переменной или функцией:

  • Просто делает контекстную замену.
  • Ищет знакомые слова в коде и выполняет вокруг них определённые инструкции.
  • Опасность: на этом моменте ничего не известно о типах данных (препроцессор «тупенький» в плане синтаксиса C++), поэтому, например, сравнения макросов могут давать неожиданные результаты.

Стражи (include guards) — помогают препроцессору не включать повторно один и тот же файл:

#ifndef HEADER_MATH_H
#define HEADER_MATH_H
// ... содержимое ...
#endif

Альтернатива — #pragma once (не входил в стандарт до C++17, но поддерживается большинством компиляторов).

#pragma pack — позволяет кучнее располагать в памяти данные структуры (контроль выравнивания).

Этап компиляции

  • Смотрит на код, переданный препроцессором.
  • Код компилируется отдельно, независимо файл от файла.
  • Компилируются только .cpp-файлы (заголовочные уже в их составе).
  • На выходе получаем объектные файлы (.o) — бинарные файлы со скомпилированным кодом.
  • Промежуточный результат — ассемблерный код.

Этап линковки

  • Все объектные файлы объединяются в один исполняемый.
  • При этом происходит подстановка адресов функций в места их вызова (поэтому для использования функции достаточно лишь её объявления — определение нужно линковщику).
  • По каждому объектному файлу строится таблица всех функций, которые в нём определены.

Linkage (связывание)

Свойство идентификатора, позволяющее компилятору создавать для нескольких одинаковых имён в разных единицах трансляции одну и ту же сущность.

  • External linkage — доступность из всех единиц трансляции.
  • Internal linkage — доступность только из текущей единицы трансляции.
  • No linkage — только текущий скоуп.

Storage duration

Свойство объекта, описывающее, когда тот попадает в память и когда её освобождает.

  • Automatic — время жизни ограничено скоупом объявления.
  • Static — время жизни от запуска программы до её окончания.
  • Thread — время жизни ограничено потоком.
  • Dynamicnew / delete.

Storage class specifier

  • static — static duration + internal linkage.
  • extern — static duration + external linkage.
  • thread_local — thread storage duration.

mutable

Можно добавить к переменным-членам класса для указания того, что данная переменная может изменяться даже в константном контексте:

const struct {
    int n1;
    mutable int n2;
} x = {0, 0};  // const object with mutable member
x.n2 = 4;  // OK: mutable member of a const object isn't const

Итого по компиляции

  1. Взяли весь текст программы, включили хедеры, обработали препроцессором код, вставили макросы — создали единицы трансляции.
  2. Единицы трансляции скормили компилятору, который проверил их на корректность языку C++, получили объектные файлы.
  3. Линковщик связал объектные файлы, подставил адреса функций в места их вызова — получили один исполняемый файл.

Ошибки компиляции

  • Текст программы после препроцессора не соответствует синтаксису языка.
  • Определение функции с сигнатурой, отличной от объявления.
  • Не все используемые конструкции хотя бы объявлены.

Ошибки линковки

  • Не получилось связать место использования с местом определения.
  • Отсутствие определения функции.
  • Определение функции дважды (нарушение ODR).

На этапе препроцессора ошибок практически быть не может (исключение — #include несуществующего файла).