Урок №23. Header guards и #pragma once

  Юрий  | 

  |

  Обновл. 11 Сен 2021  | 

 130213

 ǀ   39 

На этом уроке мы рассмотрим, что такое header guards и #pragma once в языке C++, а также зачем они нужны и как их правильно использовать.

Проблема дублирования объявлений

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

То же самое касается и функций:

Хотя вышеприведенные ошибки легко исправить (достаточно просто удалить дублирование), с заголовочными файлами дела обстоят несколько иначе. Довольно легко можно попасть в ситуацию, когда определения одних и тех же заголовочных файлов будут подключаться больше одного раза в файл .cpp. Очень часто это случается при подключении одного заголовочного файла другим.

Рассмотрим следующую программу:

math.h:

geometry.h:

main.cpp:

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

   Сначала main.cpp подключает заголовочный файл math.h, вследствие чего определение функции getSquareSides копируется в main.cpp.

   Затем main.cpp подключает заголовочный файл geometry.h, который, в свою очередь, подключает math.h.

   В geometry.h находится копия функции getSquareSides() (из файла math.h), которая уже во второй раз копируется в main.cpp.

Таким образом, после выполнения всех директив #include, main.cpp будет выглядеть следующим образом:

Мы получим дублирование определений и ошибку компиляции. Если рассматривать каждый файл по отдельности, то ошибок нет. Однако в main.cpp, который подключает сразу два заголовочных файла с одним и тем же определением функции, мы столкнемся с проблемами. Если для geometry.h нужна функция getSquareSides(), а для main.cpp нужен как geometry.h, так и math.h, то какое же решение?

Header guards


На самом деле решение простое — использовать header guards (защиту подключения в языке C++). Header guards — это директивы условной компиляции, которые состоят из следующего:

Если подключить этот заголовочный файл, то первое, что он сделает — это проверит, был ли ранее определен идентификатор SOME_UNIQUE_NAME_HERE. Если мы впервые подключаем этот заголовок, то SOME_UNIQUE_NAME_HERE еще не был определен. Следовательно, мы определяем SOME_UNIQUE_NAME_HERE (с помощью директивы #define) и выполняется основная часть заголовочного файла. Если же мы раньше подключали этот заголовочный файл, то SOME_UNIQUE_NAME_HERE уже был определен. В таком случае, при подключении этого заголовочного файла во второй раз, его содержимое будет проигнорировано.

Все ваши заголовочные файлы должны иметь header guards. SOME_UNIQUE_NAME_HERE может быть любым идентификатором, но, как правило, в качестве идентификатора используется имя заголовочного файла с окончанием _H. Например, в файле math.h идентификатор будет MATH_H:

math.h:

Даже заголовочные файлы из Стандартной библиотеки С++ используют header guards. Если бы вы взглянули на содержимое заголовочного файла iostream, то вы бы увидели:

Но сейчас вернемся к нашему примеру с math.h, где мы попытаемся исправить ситуацию с помощью header guards:

math.h:

geometry.h:

main.cpp:

Теперь, при подключении в main.cpp заголовочного файла math.h, препроцессор увидит, что MATH_H не был определен, следовательно, выполнится директива определения MATH_H и содержимое math.h скопируется в main.cpp. Затем main.cpp подключает заголовочный файл geometry.h, который, в свою очередь, подключает math.h. Препроцессор видит, что MATH_H уже был ранее определен и содержимое geometry.h не будет скопировано в main.cpp.

Вот так можно бороться с дублированием определений с помощью header guards.

#pragma once

Большинство компиляторов поддерживают более простую, альтернативную форму header guards — директиву #pragma:

#pragma once используется в качестве header guards, но имеет дополнительные преимущества — она короче и менее подвержена ошибкам.

Однако, #pragma once не является официальной частью языка C++, и не все компиляторы её поддерживают (хотя большинство современных компиляторов поддерживают).

Я же рекомендую использовать header guards, чтобы сохранить максимальную совместимость вашего кода.

Тест


Добавьте header guards к следующему заголовочному файлу:

add.h:

Ответ

Оценить статью:

Звёзд: 1Звёзд: 2Звёзд: 3Звёзд: 4Звёзд: 5 (962 оценок, среднее: 4,88 из 5)
Загрузка...

Комментариев: 39

  1. Эдуард:

    не знаю правда как это работает я исключил файл из проекта geometry.h потом добавил обратно и все заработало

     

  2. Эдуард:

    Добрый день ,полностью скопировал ваш последний пример с  header guards ,он почему то у меня не работает

  3. MrBudson:

    Есть вопрос, когда использовать заголовочные файлы, а когда .cpp?

    1. Eldar:

      Привет, я тоже буквально недавно начал изучать c++. Если бы этот вопрос был не 2024 то не отвечал бы.

      Ответ посмотри в уроках 20, 21, 22, 23!

  4. Серёга:

    После того, как препроцессор завершит свое выполнение, все идентификаторы (определенные с помощью #define) из этого файла — отбрасываются

    Но если инклюдим один файл два раза директивы уже не отбрасываются?

    1. Arxangel:

      Ключевое слово «завершит свое выполнение», в данном примере он ещё не выполнил весь код

  5. Andrew:

    Добрый день, Юрий.
    При использовании #pragma once, компилятор выдает предупреждение:

    warning: #pragma once in main file

    При этом код компилируется и программа работает.
    Компилятор gcc (MinGW.org GCC Build-2) 9.2.0
    Что это?

    1. Сергей:

      Насколько я помню в компиляторе GCC pragma_once не поддерживается

    2. Lodi:

      Просто в main файле(где прописана main функция) не нужно писать #pragma once

      1. Андрей:

        Можно все в файл с точкой входа прописывать, просто внимание на что, может дублироваться #pragma once

  6. Антон:

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

    1. Grave18:

      Иногда приходится указывать другие заголовкив своих .h, например заголовки стандартной библиотеки. Так что на всякий случай защита заголовков нужна.

  7. Ярослав:

    Спасибо за уроки, очень выручают

    1. Фото аватара Юрий:

      Пожалуйста 🙂

  8. Роман:

    Я рад, что нашёл это, когда я смотрел урок за уроком, моя реакция была что-то типа: "Вааауу, так вот оно что", всё понятно описано и даже такой ленивый человек, как я смог прочитать всё и извлечь из этого пользу, спасибо

    1. Фото аватара Юрий:

      Пожалуйста))

  9. Artemiy:

    2019 VS и твои уроки — просто! БОМБА! Давай больше материалов и создай раздел отдельный для тестов, хочу больше задач!

    1. Фото аватара Юрий:

      Вот задачки по С++ уже есть, решай 😉

  10. Sasha:

    Решил поэкспериментировать и убрал header guards с заголовочного файла, оставил чисто прототип функции. Затем подключил заголовочный файл несколько раз. И это не вызвало конфликта имен, почему? Пробовал и в visual studio и в code::blocks

    1. Дмитрий:

      Спустя год…
      По сути, ты просто указал несколько прототипов функции подряд.

  11. Владимир:

    Получилось вот что:

    _________________________main.cpp

    ___________________________input.cpp

    _________________________________input.h

  12. Владимир:

    Вчера читал уроки с 20 по 23 и не понял почти ничего, в голове все перемешалось. Сегодня перечитал и понял абсолютно все. Ура! Классная форма изложения! Непонятка остается только в одном — объявление или все же определение функций в заголовочных файлах.

    1. Фото аватара Юрий:

      Объявление в заголовочных файлах, определение в основном в файлах .cpp.

  13. zashiki:

    Здравствуйте, кстати, а почему прототип функции можно дублировать сколько угодно?

  14. Easy:

    Суть define плохо раскрыта. Он выполняет разные действия и что между этими действиями общего здесь не показано . Ранее он удалял то,что рядом написано ,например функция define NAMEFUNCTION , которая указана рядом , будет удалена в будущем. А теперь он копирует содержимое заголовочного файла? Define переводится как "Определять". По логике для безопасного копирования заголовочного файла достаточно #ifndef NAMEFILE_H …..если он ещё не был скопирован , то копируется , а если уже был скопирован, тогда не копируется.Поэтому мне не ясно для чего после #ifndef NAMEFILE_H нужен #define NAMEFILE_H

    1. Александр:

      Ну, после #ifndef NAMEFILE_H нужен #define NAMEFILE_H,, чтобы при следующей проверке это название уже было определено, и поэтому последующий участок кода повторно уже не выполнялся.

  15. Alex:

    Старина!
    Крутой курс!
    Реально простым языком!
    Круто, что ты пишешь о таких вещах, о которых обычно никто и не пишет(например header guards).
    все по-порядку, и обо всех тонкостях!
    Это реально "для начинающих".
    Буда многих курсов- они сразу приучают будущих быдлокодеров в принципу "не думай как работает эта библиотека/функция, просто юзай ее". Я ни разу не встречал чтоб кто-то хоть чуток попытался объяснить зачем эти namespace, #ifdef.

    Дружище, я с радостью приобрету PDF вариеант, но в полном объеме, все 200+ уроков, что есть на данный момент.
    Не проблема заплатить в 2 раза больше, я вижу содержаник курса и у меня перья шевелятся от осознания как много времени ты потратил на подготовку материала!!!
    Мне напрашивается мысль о трех вариантах PDFок- 100 уроков, и 200 уроков, и 200+ уроков, с разными ценниками.

    Продолжай!

    1. Фото аватара Юрий:

      Хорошо, зафиксировал. Спасибо, что читаешь 🙂

  16. Kaladgan:

    На счет добавления pch.h во все файлы проекта, у меня почему-то компилятор ругался на все файлы в которых не было этой строчки #include "pch.h" Когда добваил во все 3 файла только тогда все нормально отработало.

  17. nickatin:

    Юрий, очень приятно читать ваши уроки. Все очень доступно!

    1. Фото аватара Юрий:

      И мне приятно, что читаете 🙂

  18. Коля:

    Добрый день, подскажите пожалуйста, в чём может быть проблема. Сделал все по уроку , проверил несколько раз , использовал и header guards и #pragma once, все равно выдает ошибку. Использую Visual Studio 2017.

    1. Фото аватара Юрий:

      Код добавьте.

      1. Коля:

        math.h

        geometry.h

        main.cpp

        1. Фото аватара Юрий:

          Во-первых, предварительно скомпилированный заголовок pch.h подключается в угловых скобках, а не в кавычках:

          Во-вторых, зачем вы его подключаете во всех файлах одного и того же проекта? Это вызовет ошибку дублирования кода. Подключать нужно только в main.cpp.

          В-третьих, зачем вы подключаете в main.cpp файлы math.h и geometry.h? Файл geometry.h уже подключает math.h и используется для того, чтобы не прописывать в main.cpp строчку:

          Перечитайте внимательнее этот урок и два предыдущих.

      2. Коля:

        Перечитал, и правда был не внимателен в пред идущих уроках, все получилось, спасибо вам, за ваш проект

        1. Фото аватара Юрий:

          Пожалуйста 🙂

  19. Олег:

    Не совсем понятно. В предыдущем уроке вы говорили, что директивы распространяются только на код, написанный в данном файле, а здесь выходит так, что #ifndef из geometry.h видит #define из math.h.

    1. Андрей:

      Он видит #define из math.h потому, что весь math.h был #include в geometry.h. В том числе и со всеми его #define. А geometry.h в свою очередь уже был #include в main.cpp, т.е. полностью переписан туда. Так же, как и math.h.
      В итоге, когда препроцессор закончит (надеюсь так правильно говорить) со всеми нашими .h файлами (но не закончит еще с main.cpp), мы получим какой-то такой "ужас", хотя по сути и не увидим его глазами(наверное поэтому сложно это воспринимать?):

      И только потом будут выполнены следующие команды препроцессора в main.cpp(помним, что они выполняются по очереди, т.е. сначала переписывается весь #include, а потом ниже по тексту все наши #define), после чего все это уже компилируется и отрабатывает. А cout вполне отлично будет выводить циферку 4.

      P.S.
      Я наверное так "хорошо" объяснил этот момент, что стало только еще более непонятно?))
      А вообще хотелось бы услышать и ответ Юрия — так оно или не так работает? Вдруг я не правильно понял, но решил что правильно только потому, что оно у меня работает?)) Хотя если работает, то видимо таки правильно)

Добавить комментарий

Ваш E-mail не будет опубликован. Обязательные поля помечены *