На этом уроке мы рассмотрим, что такое header guards и #pragma once
в языке C++, а также зачем они нужны и как их правильно использовать.
Проблема дублирования объявлений
Как мы уже знаем из урока о предварительных объявлениях, идентификатор может иметь только одно объявление. Таким образом, программа с двумя объявлениями одной переменной получит ошибку компиляции:
1 2 3 4 5 6 7 |
int main() { int x; // это объявление идентификатора x int x; // ошибка компиляции: дублирование объявлений return 0; } |
То же самое касается и функций:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#include <iostream> int boo() { return 7; } int boo() // ошибка компиляции: дублирование определений { return 7; } int main() { std::cout << boo(); return 0; } |
Хотя вышеприведенные ошибки легко исправить (достаточно просто удалить дублирование), с заголовочными файлами дела обстоят несколько иначе. Довольно легко можно попасть в ситуацию, когда определения одних и тех же заголовочных файлов будут подключаться больше одного раза в файл .cpp. Очень часто это случается при подключении одного заголовочного файла другим.
Рассмотрим следующую программу:
math.h:
1 2 3 4 |
int getSquareSides() { return 4; } |
geometry.h:
1 |
#include "math.h" |
main.cpp:
1 2 3 4 5 6 7 |
#include "math.h" #include "geometry.h" int main() { return 0; } |
Эта, казалось бы, невинная программа, не скомпилируется! Проблема кроется в определении функции в файле 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 будет выглядеть следующим образом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
int getSquareSides() // из math.h { return 4; } int getSquareSides() // из geometry.h { return 4; } int main() { return 0; } |
Мы получим дублирование определений и ошибку компиляции. Если рассматривать каждый файл по отдельности, то ошибок нет. Однако в main.cpp, который подключает сразу два заголовочных файла с одним и тем же определением функции, мы столкнемся с проблемами. Если для geometry.h нужна функция getSquareSides(), а для main.cpp нужен как geometry.h, так и math.h, то какое же решение?
Header guards
На самом деле решение простое — использовать header guards (защиту подключения в языке C++). Header guards — это директивы условной компиляции, которые состоят из следующего:
1 2 3 4 5 6 |
#ifndef SOME_UNIQUE_NAME_HERE #define SOME_UNIQUE_NAME_HERE // Основная часть кода #endif |
Если подключить этот заголовочный файл, то первое, что он сделает — это проверит, был ли ранее определен идентификатор 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:
1 2 3 4 5 6 7 8 9 |
#ifndef MATH_H #define MATH_H int getSquareSides() { return 4; } #endif |
Даже заголовочные файлы из Стандартной библиотеки С++ используют header guards. Если бы вы взглянули на содержимое заголовочного файла iostream, то вы бы увидели:
1 2 3 4 5 6 |
#ifndef _IOSTREAM_ #define _IOSTREAM_ // основная часть кода #endif |
Но сейчас вернемся к нашему примеру с math.h, где мы попытаемся исправить ситуацию с помощью header guards:
math.h:
1 2 3 4 5 6 7 8 9 |
#ifndef MATH_H #define MATH_H int getSquareSides() { return 4; } #endif |
geometry.h:
1 |
#include "math.h" |
main.cpp:
1 2 3 4 5 6 7 |
#include "math.h" #include "geometry.h" int main() { return 0; } |
Теперь, при подключении в 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:
1 2 3 |
#pragma once // основная часть кода |
#pragma once
используется в качестве header guards, но имеет дополнительные преимущества — она короче и менее подвержена ошибкам.
Однако, #pragma once
не является официальной частью языка C++, и не все компиляторы её поддерживают (хотя большинство современных компиляторов поддерживают).
Я же рекомендую использовать header guards, чтобы сохранить максимальную совместимость вашего кода.
Тест
Добавьте header guards к следующему заголовочному файлу:
add.h:
1 |
int add(int x, int y); |
Ответ
1 2 3 4 5 6 |
#ifndef ADD_H #define ADD_H int add(int x, int y); #endif |
Добрый день, Юрий.
При использовании #pragma once, компилятор выдает предупреждение:
warning: #pragma once in main file
При этом код компилируется и программа работает.
Компилятор gcc (MinGW.org GCC Build-2) 9.2.0
Что это?
Насколько я помню в компиляторе GCC pragma_once не поддерживается
Юрий, добрый день! Вы написали функцию в заголовочном файле и написали вызов заголовочного файла в заголовочном файле дабы показать нам пример использования директив препроцессора. В предыдущих уроках я прочитал советы по написанию собственных заголовочных файлов, так вот следуя этим советам такой ситуации, как в примере с header guards получится не должно. Так как мы не вызываем в заголовочном файле другие заголовочные файлы и не пишем функции в заголовочных файлах. Интересно было бы узнать, есть ли другие примеры, когда может возникнуть надобность в header guards. Но несомненно мы по умолчанию её используем, дабы предотвратить ошибку.
Спасибо за уроки, очень выручают
Пожалуйста 🙂
Я рад, что нашёл это, когда я смотрел урок за уроком, моя реакция была что-то типа: "Вааауу, так вот оно что", всё понятно описано и даже такой ленивый человек, как я смог прочитать всё и извлечь из этого пользу, спасибо
Пожалуйста))
2019 VS и твои уроки — просто! БОМБА! Давай больше материалов и создай раздел отдельный для тестов, хочу больше задач!
Вот задачки по С++ уже есть, решай 😉
Решил поэкспериментировать и убрал header guards с заголовочного файла, оставил чисто прототип функции. Затем подключил заголовочный файл несколько раз. И это не вызвало конфликта имен, почему? Пробовал и в visual studio и в code::blocks
Получилось вот что:
_________________________main.cpp
___________________________input.cpp
_________________________________input.h
Вчера читал уроки с 20 по 23 и не понял почти ничего, в голове все перемешалось. Сегодня перечитал и понял абсолютно все. Ура! Классная форма изложения! Непонятка остается только в одном — объявление или все же определение функций в заголовочных файлах.
Объявление в заголовочных файлах, определение в основном в файлах .cpp.
Здравствуйте, кстати, а почему прототип функции можно дублировать сколько угодно?
Суть define плохо раскрыта. Он выполняет разные действия и что между этими действиями общего здесь не показано . Ранее он удалял то,что рядом написано ,например функция define NAMEFUNCTION , которая указана рядом , будет удалена в будущем. А теперь он копирует содержимое заголовочного файла? Define переводится как "Определять". По логике для безопасного копирования заголовочного файла достаточно #ifndef NAMEFILE_H …..если он ещё не был скопирован , то копируется , а если уже был скопирован, тогда не копируется.Поэтому мне не ясно для чего после #ifndef NAMEFILE_H нужен #define NAMEFILE_H
Старина!
Крутой курс!
Реально простым языком!
Круто, что ты пишешь о таких вещах, о которых обычно никто и не пишет(например header guards).
все по-порядку, и обо всех тонкостях!
Это реально "для начинающих".
Буда многих курсов- они сразу приучают будущих быдлокодеров в принципу "не думай как работает эта библиотека/функция, просто юзай ее". Я ни разу не встречал чтоб кто-то хоть чуток попытался объяснить зачем эти namespace, #ifdef.
Дружище, я с радостью приобрету PDF вариеант, но в полном объеме, все 200+ уроков, что есть на данный момент.
Не проблема заплатить в 2 раза больше, я вижу содержаник курса и у меня перья шевелятся от осознания как много времени ты потратил на подготовку материала!!!
Мне напрашивается мысль о трех вариантах PDFок- 100 уроков, и 200 уроков, и 200+ уроков, с разными ценниками.
Продолжай!
Хорошо, зафиксировал. Спасибо, что читаешь 🙂
На счет добавления pch.h во все файлы проекта, у меня почему-то компилятор ругался на все файлы в которых не было этой строчки #include "pch.h" Когда добваил во все 3 файла только тогда все нормально отработало.
Юрий, очень приятно читать ваши уроки. Все очень доступно!
И мне приятно, что читаете 🙂
Добрый день, подскажите пожалуйста, в чём может быть проблема. Сделал все по уроку , проверил несколько раз , использовал и header guards и #pragma once, все равно выдает ошибку. Использую Visual Studio 2017.
Код добавьте.
math.h
geometry.h
main.cpp
Во-первых, предварительно скомпилированный заголовок pch.h подключается в угловых скобках, а не в кавычках:
Во-вторых, зачем вы его подключаете во всех файлах одного и того же проекта? Это вызовет ошибку дублирования кода. Подключать нужно только в main.cpp.
В-третьих, зачем вы подключаете в main.cpp файлы math.h и geometry.h? Файл geometry.h уже подключает math.h и используется для того, чтобы не прописывать в main.cpp строчку:
Перечитайте внимательнее этот урок и два предыдущих.
Перечитал, и правда был не внимателен в пред идущих уроках, все получилось, спасибо вам, за ваш проект
Пожалуйста 🙂
Не совсем понятно. В предыдущем уроке вы говорили, что директивы распространяются только на код, написанный в данном файле, а здесь выходит так, что #ifndef из geometry.h видит #define из math.h.
Он видит #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.
Я наверное так "хорошо" объяснил этот момент, что стало только еще более непонятно?))
А вообще хотелось бы услышать и ответ Юрия — так оно или не так работает? Вдруг я не правильно понял, но решил что правильно только потому, что оно у меня работает?)) Хотя если работает, то видимо таки правильно)