Урок 23. Header guards

  Юрий Ворон  | 

    | 

  Обновл. 17 Апр 2017  | 

 13613

 ǀ   13 

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

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

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

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

math.h:

geometry.h:

main.cpp:

Эта, казалось бы, невинная программа, не скомпилируется! Проблема кроется в определении функции в файле math.h. Давайте детальнее рассмотрим, что на самом деле происходит. Во-первых, main.cpp #include «math.h», в котором определение функции getSquareSides копируется в main.cpp. Затем main.cpp #include «geometry.h», который соответственно #include «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 (“охранники заголовков”, другое название — include guards). Header guards — это директивы условной компиляции, которые состоят из следующего:

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

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

math.h:

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

Вернемся к примеру с math.h, но уже с использованием header guards.

math.h

geometry.h:

main.cpp:

Теперь, когда main.cpp #include «math.h», препроцессор будет видеть, что MATH_H еще не был определен. Содержание math.h копируется в main.cpp и MATH_H определяется. Затем main.cpp #include «geometry.h», который #include «math.h «. Препроцессор видит, что MATH_H уже был ранее определен и содержимое между header guards игнорируется.

Таким образом, используя header guards, мы предотвратили дублирование определений.

#pragma once


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

#pragma once используется, как и header guards, но имеет дополнительные преимущества – она короче и менее подвержена ошибкам. Файл stdafx.h, который мы постоянно подключаем в Visual Studio в проектах, использует именно эту директиву вместо header guards.

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

В целях полной совместимости, мы рекомендуем использовать header guards.

Выводы

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

Обратите внимание, что дублирование ОБЪЯВЛЕНИЙ (не определений) не вызывают те же проблемы, что с определениями, так как объявление может быть объявлено несколько раз без инцидентов, но даже если ваш заголовочный файл состоит из одних только объявлений, по-прежнему считается хорошим тоном использовать header guards.

Тест

1) Используя приведенный выше пример с math.h, добавьте header guards в файл geometry.h

Ответ

Чтобы увидеть результат, нажмите на ссылку.

Ответ 1

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

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

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

  1. Alex:

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

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

    Продолжай!

    1. Юрий Ворон Юрий Ворон:

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

  2. Kaladgan:

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

  3. nickatin:

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

    1. Юрий Ворон Юрий Ворон:

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

  4. Коля:

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

          Пожалуйста 🙂

  5. Олег:

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