Лекция 5. Компиляция
Раздельная компиляция — зачем
- Уменьшение количества строк в одном файле.
- Ускорение компиляции (пересобираются только изменённые модули).
- Разделение программы на логические модули, которые можно переиспользовать.
- Проще читать код.
- Разделение на
.h- и.cpp-файлы нужно для разных этапов компиляции.
Этапы трансляции (на самом деле их около 9)
- Препроцессор — создаёт блоки (единицы трансляции), включая
.h-файлы в.cpp-файлы. - Компиляция — получает из каждой единицы трансляции объектные файлы (
.o) — скомпилированный код, независимый для каждой единицы трансляции. - Линковщик (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 — время жизни ограничено потоком.
- Dynamic —
new/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
Итого по компиляции
- Взяли весь текст программы, включили хедеры, обработали препроцессором код, вставили макросы — создали единицы трансляции.
- Единицы трансляции скормили компилятору, который проверил их на корректность языку C++, получили объектные файлы.
- Линковщик связал объектные файлы, подставил адреса функций в места их вызова — получили один исполняемый файл.
Ошибки компиляции
- Текст программы после препроцессора не соответствует синтаксису языка.
- Определение функции с сигнатурой, отличной от объявления.
- Не все используемые конструкции хотя бы объявлены.
Ошибки линковки
- Не получилось связать место использования с местом определения.
- Отсутствие определения функции.
- Определение функции дважды (нарушение ODR).
На этапе препроцессора ошибок практически быть не может (исключение —
#includeнесуществующего файла).