Найти тему

Основы С++: Способы защиты заголовочных файлов

Оглавление

В статье "Предварительное объявление функций и изменение порядка их определения" мы рассмотрели правило одного определения (ODR), которое гласит что каждая переменная или функция может иметь только одно определение. Повторная идентификация функции, закономерно, приведет к ошибке компиляции.

Такую ошибку легко исправить, если...

Если она не вызвана определением через #include, то есть через заголовочные файлы. В случае с заголовочными файлами, можно легко попасть в ситуацию, когда внутри заголовка, который вы решили использовать, включен еще один заголовок. Вы об этом не знаете, потому что это сторонний заголовочный файл. И используете включенный в первый файл заголовок сначала отдельно, а потом посредством включения первого файла. Программа не компилируется.

Мудрено описал ситуацию, да?

Хорошо, давайте посмотрим эту ситуацию на практическом примере.

Программист Иван написал код, который при компиляции печатает в консоли приветствие и небольшой рассказ о нем. Иван не читал мою предыдущую статью, в заключение которой я говорил, что определение функций в заголовочных файлах не рекомендуется.

И вот что у него получилось:

Сначала он решил создать функцию, которая здоровается, в отдельном файле. Он использовал для этого заголовочный файл:

Он вызвал функцию из этого файла в main, предварительно включив его через #include

-2

Потом он подумал, что будет неплохо еще кое-что о себе рассказать и создал новый файл. И, черт возьми, снова заголовочный и он снова объявил там функцию!

-3

Он решил, что неплохо будет включить в monolog.h через #include файл hello.h

И потом все это вместе вызвать в main:

-4

Вроде бы все замечательно, можно компилировать и запускать? О да, но правило ODR так не думает. И вот что получается в процессе компиляции:

-5

Из-за включенного заголовка hello.h внутри заголовка monolog.h, препроцессор, выполняя команды, напечатал определение функции hello два раза подряд. А это противоречит правилу одного определения (ODR). На что тут же стал ругаться сборщик и в итоге компиляции прервалась ошибкой.

Что же делать?

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

Ну, почти так, но не совсем...

Несмотря на то, что я рекомендую себе и вам не определять внутри заголовков функции. Надеюсь, что в будущем мы вместе с вами изучим, как создавать собственные типы данных. К чему я это? К тому, что пользовательские типы данных определяются в заголовках. И на это тоже есть своя причина (потом мы о ней узнаем). И вот тут мы подходим к защите заголовков. Нам придется выучить ее заранее.

Пока она нам не пригодится, но потом...

Короче, просто выучим ее заранее.

Защита заголовочных файлов на примере программы Ивана, который не читал мою статью про заголовочные файлы

Иван добавил несколько команд для препроцессора в оба заголовочных файла. Посмотрите, как теперь они стали выглядеть:

-6
-7

Вот так и выглядит защита заголовочных файлов.

И главное, что код Ивана теперь успешно исполняется:

-8

Как она работает на практике?

Эта конструкция на практике работает по типу наличия/отсутствия флага. Мы уже разбирали эти директивы недавно, в этой статье. Если все забыли, можно перейти по ссылке и освежить память.

#ifndef ИМЯ_ОБЪЕКТА_H
#define ИМЯ_ОБЪЕКТА_H

ваш код

#endif

Флагом в данном случае выступает #define ИМЯ_ОБЪЕКТА_H

Когда директива #ifndef принимается препроцессором к исполнению. Она говорит ему: проверь код до начала директивы на наличие макроса #define с именем, которое указано в директиве. Если объекта с указанным именем нет до начала действия директивы, оставь включенный код без изменений. Если же есть, не печатай ничего до того, пока не примешь к исполнению директиву #endif.

Препроцессор оставляет код без изменений и печатает его, так как перед этим получил команду #include относящуюся к этому коду. В том числе препроцессор печатает и флаг #define ИМЯ_ОБЪЕКТА_H

После того, как препроцессор выполнит печать всего включенного файла, если для него поступит директива на повторное включение, то он не будет повторно печатать код из включенного файла, так как обнаружит выше флаг #define с именем этого файла.

Есть ли другие способы защиты заголовков?

Да, многие IDE поддерживают директиву #pragma once, которая выставляется в начале заголовочных файлов.

-9

Она выглядит проще и выполняет те же функции, что и первый пример защиты заголовков.

#define (include guard) или #pragma once?

Если сравнивать два метода защиты заголовков, у обоих есть как существенные плюсы, так и минусы.

#define (include guard)

Плюсы:
  • Кроссплатформенность
  • Соответствие стандарту
  • Портируемость
Недостатки:
  • Возникновение ошибок (конфликта имен) при наличии нескольких заголовочных файлов с одинаковыми именами в одном проекте.

#pragma once

Плюсы:
  • Повышает скорость компиляции за счет прямого взаимодействия с компилятором.
  • Исключает конфликты имен.
Минусы:
  • Не поддерживается всеми компиляторами, в связи с чем могут возникнуть проблемы с портируемостью кода.

Что выбирать?

В процессе обучения я лично пользуюсь #pragma once, так как у меня стоит Visual Studio, который ее поддерживает. А вы выбирайте тот способ, который подходит вашему компилятору. Когда вы придете на свою первую работу, просто уточните у руководителя проекта: Какой способ защиты заголовков будет использоваться в проекте?

Не беспокойтесь, это совершенно нормальный и компетентный вопрос.

На этом мы сегодня закончим. Спасибо за внимание.

Если вам понравился статья, оставляйте лайки.

Подписывайтесь на канал, чтобы не пропустить новые статьи.

Задавайте вопросы в комментариях, если что-то было непонятно.