# Глава 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. Здесь можно задавать вопросы и общаться.
Задонатить. Если вам нравится курс, вы можете поддержать развитие площадки!