# Глава 10.2. Хедеры
До C++20 проекты содержали файлы двух видов:
- Хедеры (headers, заголовочные файлы). Им принято давать расширение `.h`, `.hpp` или `.hxx`.
- Файлы реализации. Но на практике вы вряд ли услышите это название. Обычно говорят «файлы исходников» или просто «`cpp`-файлы». Варианты расширений: `.cpp`, `.cxx`, `.cc`.
Хедеры содержат объявления, а `cpp`-файлы — определения. Но в некоторых случаях определения можно или даже нужно помещать в хедеры. Каждый из них мы рассмотрим отдельно. А пока помните: если допустимы оба варианта, всегда выбирайте `cpp`-файл.
Кроме того, расширение файла — всего лишь договорённость. Оно может быть любым. И все же давайте файлам одно из общепринятых расширений. Например, `.h` для хедеров и `.cpp` для файлов реализации.
Хедеры можно назвать интерфейсом, а `cpp`-файлы — реализацией. В простейшем случае проект — это единственный `cpp`-файл, из которого компилятор создаёт исполняемый файл. Или единственный хедер, если проект — библиотека.
## Простой проект с хедерами
Для демонстрации взаимосвязи хедеров и `cpp`-файлов заведём проект `hello_compiler` из трёх файлов: {#block-hello-compiler}
```
hello_compiler/
├── hello_compiler.h
├── hello_compiler.cpp
└── main.cpp
```
В файле `hello_compiler.h` разместим объявление функции, которая выводит информацию о компиляторе. Она принимает строку. Класс `std::string` объявлен в хедере `string`, и нам надо подключить его через директиву препроцессора `#include`.
```cpp
// hello_compiler.h
#include <string>
namespace sys
{
void show_compiler_info(std::string compiler);
}
```
Функция вложена в пространство имён `sys`. Со временем в проект планируется добавить больше вспомогательных функций, и пространство имён подходит для их логической группировки.
Файл `hello_compiler.cpp` содержит определение `show_compiler_info()` и вспомогательную функцию `binary_name()`. Обратите внимание, что `show_compiler_info()` находится в пространстве имён `sys`, как и в хедере.
```cpp
// hello_compiler.cpp
#include <cstdlib>
#include <map>
#include <print>
#include "hello_compiler.h"
std::string binary_name(std::string compiler)
{
const static std::map<std::string, std::string> binaries = {
{"clang", "clang++"},
{"gcc", "g++"},
{"msvc", "cl.exe"}
};
return binaries.at(compiler);
}
namespace sys
{
void show_compiler_info(std::string compiler)
{
const std::string command = std::format("{} -v", binary_name(compiler));
const int code = std::system(command.c_str());
if (code != 0)
std::println("Couldn't get clang compiler info. Error code: {}", code);
}
}
```
В `hello_compiler.cpp` подключён наш хедер и три хедера стандартной библиотеки:
- `cstdlib`. Здесь объявлена функция [std::system()](https://en.cppreference.com/w/cpp/utility/program/system) для запуска консольной команды.
- `map`. Здесь объявлен шаблонный класс `std::map`.
- `print`. Здесь объявлена функция для вывода в консоль.
- `hello_compiler.h`. Так компилятор поймёт, что мы определяем функцию, уже объявленную в другом месте.
Теперь вызовем `show_compiler_info()` из `main.cpp`. Чтобы функция стала доступна в этом файле, нужно подключить хедер с её объявлением.
```cpp
// main.cpp
#include "hello_compiler.h"
int main()
{
sys::show_compiler_info("clang");
}
```
```
Debian clang version 20.1.7
Target: x86_64-pc-linux-gnu
Thread model: posix
...
```
## Правила организации проекта с хедерами
Состоящий из хедеров и `cpp`-файлов проект составляется по простым принципам:
- Объявления сущностей размещаются в хедерах, а их определения — в `cpp`-файлах.
- Если объявление вложено в [пространство имён,](/courses/cpp/chapters/cpp_chapter_0053/) определение должно повторять эту вложенность.
- Чтобы использовать объявление из хедера, нужно его подключить.
- Хедеры подключаются директивой препроцессора `#include`.
- Хедеры можно подключать в `cpp`-файлы и другие хедеры.
- При подключении хедера становятся доступны все его объявления.
- Если в хедер А подключён хедер Б, и хедер А подключён в `main.cpp`, это делает содержимое хедера Б доступным в `main.cpp`. То есть подключение хедеров [транзитивно.](https://ru.wikipedia.org/wiki/%D0%A2%D1%80%D0%B0%D0%BD%D0%B7%D0%B8%D1%82%D0%B8%D0%B2%D0%BD%D0%BE%D1%81%D1%82%D1%8C)
Чтобы использовать классы и функции из стандартной библиотеки, нужно подключать соответствующие хедеры. Например, `std::vector` объявлен в хедере `vector`, функция `std::sort()` — в `algorithm`, а тип `std::size_t` — в `cstddef`. На [cppreference](https://en.cppreference.com/w/cpp/types/numeric_limits/min.html) можно узнать, что в каком хедере находится.
Хедеры подключаются с помощью директивы препроцессора `#include`. Разберемся, как это устроено.
## Директивы препроцессора {#block-preprocessor}
Препроцессор — это часть компилятора, отвечающая за первичную обработку кода. Он обнаруживает и обрабатывает строки в коде, начинающиеся с символа решётки `#`. За ним следует ключевое слово и опционально параметры:
```cpp
#keyword params
```
Такие строки называют _директивами препроцессора._ Их обработка сводится к примитивной замене фрагментов кода. Покажем это на примере двух директив: [#include](https://en.cppreference.com/w/cpp/preprocessor/include.html) и [#define](https://en.cppreference.com/w/cpp/preprocessor/replace).
Ключевое слово `include` заменяет строку с директивой содержимым файла. Имя файла — обязательный параметр для ключевого слова `include`:
```cpp
#include "common/logging.hpp"
```
Правило хорошего тона гласит: используйте директиву `#include` _только_ для подключения хедеров. Хоть технически она сработает и для произвольного файла, будь то `.cpp`, `.txt` или `.json`.
А ключевое слово `define` объявляет _макрос_ — фрагмент кода, которому дано имя.
Заведем макрос `PI` и используем его при выводе в консоль: {#block-macro}
```cpp {.example_for_playground}
#include <print>
#define PI 3.1415926
int main()
{
std::println("{}", PI);
}
```
```
3.1415926
```
В процессе сборки проекта директива `#define` исчезнет, а по месту её использования произойдёт _макроподстановка_ — текстовая замена имени макроса на его тело:
```cpp
std::println("{}", 3.1415926);
```
Макросам принято давать имена заглавными буквами:`PI`, `MAX_JOBS` и т.д. Эта договорённость нужна, чтобы не путать макросы с переменными.
При макроподстановках препроцессор не проводит синтаксических или семантических (смысловых) проверок. После макроподстановок вы можете получить некорректный код и ошибку компилятора, которую трудно диагностировать и ещё труднее отладить. На заре появления C++ макросы позволяли писать обобщённый код. Они стали не нужны с появлением развитых средств для создания [шаблонов](/courses/cpp/chapters/cpp_chapter_0056/) и выполнения кода на этапе компиляции. Неудивительно, что хорошие практики современного C++ [настаивают](https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#res-macros2) на отказе от макросов. А подключение хедеров через директивы препроцессоров начиная с C++20 заменяется на импорт модулей.
Директивы препроцессора — это архаичный инструмент. Но важно знать, как работать с хедерами и макросами: вы будете встречать их во всех проектах, которые не мигрировали на модули. А таких подавляющее большинство.
## Подключение хедеров
На первых строках файла принято подключать хедеры, сущности из которых используются дальше по коду. В файле `hello_compiler.cpp` проекта `hello_compiler` мы подключили хедеры:
- `hello_compiler.h`, чтобы реализовать объявленный в нем интерфейс.
- `cstdlib`, `map` и `print` из стандартной библиотеки, чтобы воспользоваться предоставляемыми ими объявлениями.
```cpp
#include <cstdlib>
#include <map>
#include <print>
#include "hello_compiler.h"
```
Хедеры стандартной библиотеки не имеют расширений.
Также обратите внимание, что хедеры обрамляются треугольными скобками `<>` либо кавычками `""`. От этого зависит порядок, в котором препроцессор перебирает пути для поиска файла. Стратегия перебора зависит от реализации компилятора. Она [не определяется](https://eel.is/c++draft/cpp.include) стандартом C++. Однако у распространённых компиляторов стратегии схожи.
Порядок перебора путей `#include "file"`:
1. Директория, содержащая обрабатываемый файл с директивой `#include`.
2. Другие директории проекта.
3. Набор путей по умолчанию. Их список отличается в зависимости от компилятора и дистрибутива ОС. Это так называемые [стандартные системные директории.](https://gcc.gnu.org/onlinedocs/gcc-4.5.4/cpp/Search-Path.html) Например, `/usr/local/include` и `/usr/include/`.
Перебор директорий в случае `#include <file>` ограничивается стандартными системными директориями. Если препроцессор не находит в них нужный хедер, сборка проекта [завершается с ошибкой.](https://gcc.gnu.org/onlinedocs/cpp/Search-Path.html) {#block-system}
Пути для поиска хедеров можно дополнить или полностью переопределить с помощью [опций компилятора и переменных окружения.](https://gcc.gnu.org/onlinedocs/gcc/Directory-Options.html#Directory-Options)
При выборе способа подключения хедера руководствуйтесь правилами:
- В треугольных скобках `<>` и без расширения указывайте хедеры стандартной библиотеки.
- В двойных кавычках `""` указывайте хедеры внутри проекта.
Перед именем хедера зачастую задаётся относительный путь. Усложним структуру проекта `hello_compiler`: вынесем `hello_compiler.h` и `hello_compiler.cpp` в директорию `utils`.
```
hello_compiler/
├── main.cpp
└── utils
├── hello_compiler.cpp
└── hello_compiler.h
```
Тогда подключение `hello_compiler.h` в `main.cpp` будет выглядеть так:
```cpp
#include "utils/hello_compiler.h"
```
При подключении хедера технически допустимо указывать абсолютный путь, например `#include "/usr/include/boost/fusion/include/array.hpp"`. Но делать так _нельзя,_ потому что при изменении расположения хедера или при переносе проекта на другую систему сломается компиляция.
В проекте есть файл без расширения `vector`. Его имя совпадает с хедером стандартной библиотеки. Содержимое какого хедера препроцессор вставит по месту директивы `#include "vector"`? Введите букву: {.task_text}
- `s`, если хедер из стандартной библиотеки.
- `l`, если хедер внутри проекта.
```consoleoutput {.task_source #cpp_chapter_0102_task_0030}
```
Выше перечислены правила, позволяющие однозначно определить, к какому виду относится хедер. {.task_hint}
```cpp {.task_answer}
l
```
Реализуйте функцию `score_sum()`. Она принимает словарь, в котором ключ — это id абитуриента, а значение — его оценка за экзамен. Второй параметр функции — id интересующего абитуриента. Функция должна вернуть сумму баллов, которые он получил за все экзамены. Если студент не найден, функция должна вернуть 0. {.task_text}
Для суммирования используйте алгоритм [std::accumulate()](/courses/cpp/chapters/cpp_chapter_0081/#block-accumulate-overload). {.task_text}
Над функцией разместите подключение всех необходимых хедеров. Для справки используйте [cppreference](https://cppreference.com/). {.task_text}
```cpp {.task_source #cpp_chapter_0102_task_0040}
std::size_t score_sum(std::flat_multimap<std::string, std::size_t> applicants,
std::string id)
{
}
```
Вам пригодится метод [equal_range()](/courses/cpp/chapters/cpp_chapter_0073/#block-equal-range), который есть у `multi`-версий контейнеров, в том числе у класса [std::flat_multimap](/courses/cpp/chapters/cpp_chapter_0075/#block-flat). {.task_hint}
```cpp {.task_answer}
#include <flat_map>
#include <numeric>
#include <string>
#include <utility>
std::size_t fold(std::size_t left, std::pair<std::string, std::size_t> right)
{
return left + right.second;
}
std::size_t score_sum(std::flat_multimap<std::string, std::size_t> applicants,
std::string id)
{
auto[it, it_end] = applicants.equal_range(id);
return std::accumulate(it, it_end, 0, fold);
}
```
## Недостатки использования хедеров
Работа препроцессора сводится к примитивным текстовым заменам. Синтаксический и семантический анализ при этом не проводится. Вместо _каждой_ директивы `#include file` препроцессор рекурсивно подставляет содержимое файла. Рекурсивно — потому что один хедер в свою очередь может включать другие хедеры. Это приводит к **долгой сборке проекта.**
В проекте есть 3 `cpp`-файла и 2 хедера. Каждый из хедеров подключён во все `cpp`-файлы. Сколько раз в итоге препроцессор будет подставлять содержимое файла по месту директивы `#include`? {.task_text}
```consoleoutput {.task_source #cpp_chapter_0102_task_0050}
```
Каждый раз по месту директивы `#include` подставляется содержимое соответствующего заголовочного файла. Поэтому каждый из 2-х хедеров будет подставлен трижды. {.task_hint}
```cpp {.task_answer}
6
```
Содержимое одного и того же хедера может попасть в `cpp`-файл несколько раз. И не обязательно из-за ошибки. Например, если помимо хедера А подключён хедер Б, который тоже в свою очередь подключает А, прямо или косвенно. А повторное включение одного и того же кода чревато ошибками компиляции. Оно порождает **конфликтующие объявления и определения.** Чтобы этого избежать, содержимое хедера оборачивается специальной директивой препроцессора для защиты от повторного подключения (include guard). О ней мы поговорим в главе про директивы препроцессора.
Если макросы с одинаковым именем определяются в нескольких хедерах, то от _порядка включения_ хедеров зависит, тело какого макроса будет подставлено в код. Иными словами, результат подключения хедера зависит от контекста, в который он подключается. Это приводит к **нетривиальным ошибкам.**
При подключении хедера становятся доступны абсолютно все его объявления. **Отсутствуют механизмы инкапсуляции** для выбора, что скрывать, а что экспортировать.
Итак, у использования хедеров есть весомые недостатки. Поэтому в C++ был реализован принципиально другой механизм для подключения переиспользуемого кода — модули.
----------
## Резюме {#block-summary}
- Хедер можно назвать интерфейсом, а `cpp`-файл — реализацией.
- Чтобы использовать объявление из хедера, нужно его подключить.
- Хедеры подключаются директивой препроцессора `#include`.
- Препроцессор — компонент компилятора, который выполняет текстовые замены. Он находит и обрабатывает директивы препроцессора.
- Директива препроцессора `#define` определяет макрос.
Наша группа в telegram. Здесь можно задавать вопросы и общаться.
Задонатить. Если вам нравится курс, вы можете поддержать развитие площадки!