Урок №23. Header guards

  Юрий  | 

  Обновл. 11 Мар 2019  | 

 26712

 ǀ   19 

В этом уроке мы рассмотрим, что такое header guards, зачем они нужны и как их использовать.

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

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

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

Хотя эти ошибки легко исправить (достаточно просто удалить дублирование), с заголовочными файлами дела обстоят несколько иначе. Довольно легко можно попасть в ситуацию, когда определения одних и тех же заголовочных файлов будут подключаться больше одного раза в файл .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, который #include сразу два заголовочных файла с одним и тем же определением функции, мы столкнёмся с проблемами. Если для geometry.h нужна функция getSquareSides, а для main.cpp нужен как geometry.h, так и math.h, то какое же решение?

Header guards


На самом деле решение простое — header guards. 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 (293 оценок, среднее: 4,83 из 5)
Загрузка...

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

  1. Аватар Евгений:

    Хотел бы поинтересоваться у автора статьи вот чем… Вы постоянно используете выражения на английском языке в русскоязычной статье. И тут я говорю не про куски кода в примерах, а например про "Header guards". Почему нельзя это назвать по русски? Получается статья на двух языках. Конечно, сейчас нет проблем переводом англоязычных текстов, да и программисту полезно, и даже необходимо знать английский, но выглядит это, откровенно говоря, нелепо.

    1. Юрий Юрий:

      Значит это нелепые уроки, вы можете найти другие "лепые" уроки и спокойно их читать)

      1. Аватар Игорь:

        Не перевести такое простое выражение -это крайняя невежественность. Да и империалисты пишут программы почему-то на английском, а не русском. вот сволочи..

  2. Аватар zashiki:

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

  3. Аватар Easy:

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

  4. Аватар Easy:

    Я запутался с этим define . Почему в прошлом уроке он удалял и заменял , а здесь наоборот ?

  5. Аватар Alex:

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

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

    Продолжай!

    1. Юрий Юрий:

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

  6. Аватар Kaladgan:

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

  7. Аватар nickatin:

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

    1. Юрий Юрий:

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

  8. Аватар Коля:

    Добрый день, подскажите пожалуйста, в чём может быть проблема. Сделал все по уроку , проверил несколько раз , использовал и 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. Юрий Юрий:

          Пожалуйста 🙂

  9. Аватар Олег:

    Не совсем понятно. В предыдущем уроке вы говорили, что директивы распространяются только на код, написанный в данном файле, а здесь выходит так, что #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 не будет опубликован. Обязательные поля помечены *

telegram канал
НОВОСТИ RAVESLI