На этом уроке мы рассмотрим, что такое 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 |
не знаю правда как это работает я исключил файл из проекта geometry.h потом добавил обратно и все заработало
Добрый день ,полностью скопировал ваш последний пример с header guards ,он почему то у меня не работает
Есть вопрос, когда использовать заголовочные файлы, а когда .cpp?
Привет, я тоже буквально недавно начал изучать c++. Если бы этот вопрос был не 2024 то не отвечал бы.
Ответ посмотри в уроках 20, 21, 22, 23!
Но если инклюдим один файл два раза директивы уже не отбрасываются?
Ключевое слово «завершит свое выполнение», в данном примере он ещё не выполнил весь код
Добрый день, Юрий.
При использовании #pragma once, компилятор выдает предупреждение:
warning: #pragma once in main file
При этом код компилируется и программа работает.
Компилятор gcc (MinGW.org GCC Build-2) 9.2.0
Что это?
Насколько я помню в компиляторе GCC pragma_once не поддерживается
Просто в main файле(где прописана main функция) не нужно писать #pragma once
Можно все в файл с точкой входа прописывать, просто внимание на что, может дублироваться #pragma once
Юрий, добрый день! Вы написали функцию в заголовочном файле и написали вызов заголовочного файла в заголовочном файле дабы показать нам пример использования директив препроцессора. В предыдущих уроках я прочитал советы по написанию собственных заголовочных файлов, так вот следуя этим советам такой ситуации, как в примере с header guards получится не должно. Так как мы не вызываем в заголовочном файле другие заголовочные файлы и не пишем функции в заголовочных файлах. Интересно было бы узнать, есть ли другие примеры, когда может возникнуть надобность в header guards. Но несомненно мы по умолчанию её используем, дабы предотвратить ошибку.
Иногда приходится указывать другие заголовкив своих .h, например заголовки стандартной библиотеки. Так что на всякий случай защита заголовков нужна.
Спасибо за уроки, очень выручают
Пожалуйста 🙂
Я рад, что нашёл это, когда я смотрел урок за уроком, моя реакция была что-то типа: "Вааауу, так вот оно что", всё понятно описано и даже такой ленивый человек, как я смог прочитать всё и извлечь из этого пользу, спасибо
Пожалуйста))
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
Ну, после #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.
Я наверное так "хорошо" объяснил этот момент, что стало только еще более непонятно?))
А вообще хотелось бы услышать и ответ Юрия — так оно или не так работает? Вдруг я не правильно понял, но решил что правильно только потому, что оно у меня работает?)) Хотя если работает, то видимо таки правильно)