Как защитить хедер от повторного включения
Перейти к содержимому

Как защитить хедер от повторного включения

  • автор:

2.11 – Защита заголовков

В уроке «2.6 – Предварительные объявления и определения» мы отметили, что идентификатор переменной или функции может иметь только одно определение (правило одного определения). Таким образом, программа, которая определяет идентификатор переменной более одного раза, вызовет ошибку компиляции:

int main() < int x; // это определение переменной x int x; // ошибка компиляции: повторяющееся определение return 0; >

Точно так же программы, которые определяют функцию более одного раза, также вызовут ошибку компиляции:

#include int foo() // это определение функции foo < return 5; >int foo() // ошибка компиляции: повторяющееся определение < return 5; >int main()

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

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

// Мы не должны включать определения функций в заголовочные файлы, // но для примера сделаем это int getSquareSides()
#include "square.h"
#include "square.h" #include "geometry.h" int main()

Эта, казалось бы, невинно выглядящая программа не компилируется! Вот что происходит. Во-первых, main.cpp включает square.h , что копирует определение функции getSquareSides в main.cpp . Затем main.cpp включает geometry.h , который сам включает square.h . Это копирует содержимое square.h (включая определение функции getSquareSides ) в geometry.h , которое затем копируется в main.cpp .

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

int getSquareSides() // из square.h < return 4; >int getSquareSides() // из geometry.h (через square.h) < return 4; >int main()

Повторяющиеся определения и ошибка компиляции. Каждый файл по отдельности нормальный. Однако, поскольку main.cpp дважды включает через #include содержимое square.h , мы столкнулись с проблемами. Если для geometry.h требуется getSquareSides() , а для main.cpp требуются и geometry.h , и square.h , как бы вы решили эту проблему?

Защита заголовка

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

#ifndef SOME_UNIQUE_NAME_HERE #define SOME_UNIQUE_NAME_HERE // здесь идут ваши объявления (и некоторые типы определений) #endif

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

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

#ifndef SQUARE_H #define SQUARE_H int getSquareSides() < return 4; >#endif

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

#ifndef _IOSTREAM_ #define _IOSTREAM_ // тут идет содержимое файла #endif

Для продвинутых читателей

В больших программах возможно наличие двух отдельных заголовочных файлов (включенных из разных каталогов) с одинаковыми именами (например, directoryA\config.h и directoryB\config.h ). Если для защиты включения используется только имя файла (например, CONFIG_H ), эти два файла могут в конечном итоге использовать одно и то же защитное имя. Если это произойдет, любой файл, который включает (прямо или косвенно) оба файла config.h , не получит содержимое включаемого файла, который будет включен вторым. Это, вероятно, вызовет ошибку компиляции.
Из-за этой возможности конфликтов защитных имен многие разработчики рекомендуют использовать более сложные/уникальные имена в защитах заголовков. Некоторые хорошие предложения – это соглашение об именах ___H , __H или __H .

Обновление нашего предыдущего примера с помощью защиты заголовков

Вернемся к примеру со square.h , добавив в него защиту заголовков. Чтобы быть последовательными мы также добавим защиту заголовков и в geometry.h .

#ifndef SQUARE_H #define SQUARE_H int getSquareSides() < return 4; >#endif
#ifndef GEOMETRY_H #define GEOMETRY_H #include "square.h" #endif
#include "square.h" #include "geometry.h" int main()

После того, как препроцессор разрешит все включения, эта программа будет выглядеть так:

#ifndef SQUARE_H // square.h включен из main.cpp, #define SQUARE_H // SQUARE_H получает здесь определение // и весь этот контент будет включен int getSquareSides() < return 4; >#endif // SQUARE_H #ifndef GEOMETRY_H // geometry.h включен из main.cpp #define GEOMETRY_H #ifndef SQUARE_H // square.h включен из geometry.h, SQUARE_H уже определен выше #define SQUARE_H // поэтому ничего из этого контента не было включено int getSquareSides() < return 4; >#endif // SQUARE_H #endif // GEOMETRY_H int main()

Как видно из примера, второе включение содержимого square.h (из geometry.h ) игнорируется потому, что SQUARE_H уже был определен при первом включении. Следовательно, функция getSquareSides включается только один раз.

Защита заголовков не препятствует одиночным включениям заголовка в разные файлы исходного кода.

Обратите внимание, что цель защиты заголовков – предотвратить получение файлом исходного кода более одной копии защищенного заголовка. По замыслу, защита заголовков не препятствует включению данного заголовочного файла (однократно) в отдельные исходные файлы. Это также может вызвать непредвиденные проблемы. Рассмотрим следующую возможность:

#ifndef SQUARE_H #define SQUARE_H int getSquareSides() < return 4; >// предварительное объявление для getSquarePerimeter int getSquarePerimeter(int sideLength); #endif
#include "square.h" // square.h включается сюда один раз int getSquarePerimeter(int sideLength)
#include "square.h" // square.h также включается один раз и сюда #include int main()

Обратите внимание, что square.h включается как из main.cpp , так и из square.cpp . Это означает, что содержимое square.h будет включено один раз в square.cpp и один раз в main.cpp .

Давайте разберемся, почему это происходит более подробно. Когда square.h включается из square.cpp , SQUARE_H определяется до конца square.cpp . Это определение предотвращает повторное включение square.h в square.cpp (что является целью защиты заголовков). Однако после завершения square.cpp SQUARE_H больше не считается определенным. Это означает, что когда препроцессор начинает работу над main.cpp , SQUARE_H в main.cpp изначально не определен.

Конечным результатом является то, что и square.cpp , и main.cpp получают копию определения getSquareSides . Эта программа будет компилироваться, но компоновщик будет жаловаться на то, что ваша программа имеет несколько определений для идентификатора getSquareSides !

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

#ifndef SQUARE_H #define SQUARE_H // предварительное объявление для getSquareSides int getSquareSides(); // предварительное объявление для getSquarePerimeter int getSquarePerimeter(int sideLength); #endif
#include "square.h" // фактическое определение getSquareSides int getSquareSides() < return 4; >int getSquarePerimeter(int sideLength)
#include "square.h" // square.h также включается один раз сюда #include int main()

Теперь, когда программа скомпилирована, функция getSquareSides будет иметь только одно определение (через square.cpp ), так что компоновщик будет счастлив. Файл main.cpp может вызывать эту функцию (даже если она находится в square.cpp ), потому что он включает square.h , в котором есть предварительное объявление для этой функции (компоновщик соединит вызов getSquareSides из main.cpp с определение getSquareSides в square.cpp ).

Разве мы не можем просто избежать определений в файлах заголовков?

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

В будущем мы покажем вам довольно много случаев, когда в файл заголовка необходимо поместить определения, не являющиеся функциями. Например, C++ позволяет вам создавать свои собственные типы. Эти пользовательские типы обычно определяются в файлах заголовков, чтобы эти определения можно было распространить на исходные файлы, которые должны их использовать. Без защиты заголовков ваши исходные файлы могут иметь несколько идентичных копий этих определений, что приведет к ошибке компиляции повторяющихся определений.

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

#pragma once

Многие компиляторы поддерживают более простую альтернативную форму защиты заголовков с помощью директивы #pragma :

#pragma once // здесь ваш код

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

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

В целях совместимости мы рекомендуем придерживаться традиционной защиты заголовков. Она не требуют много работы и гарантированно поддерживаются всеми компиляторами.

Резюме

Защита заголовков предназначена для того, чтобы содержимое заданного заголовочного файла не копировалось более одного раза в любой отдельный файл, чтобы предотвратить дублирование определений.

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

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

Небольшой тест

Вопрос 1

Добавьте защиту заголовка в этот заголовочный файл:

int add(int x, int y);
#ifndef ADD_H #define ADD_H int add(int x, int y); #endif

Как защитить хедер от повторного включения

Объясните достоинства обычной внутренней защиты #include

#if !defined(HEADER123_HPP) #define HEADER123_HPP //некий код #endif

Чем она лучше такой:

#if defined(HEADER123_HPP) #error wrong dependence detected #endif #define HEADER123_HPP //некий код

Re: защита от повторного include

От: jazzer Skype: enerjazzer
Дата: 26.02.06 18:42
Оценка:

Здравствуйте, Dmi_3, Вы писали:

D_>Объясните достоинства обычной внутренней защиты #include

D_>

D_>#if !defined(HEADER123_HPP) D_>#define HEADER123_HPP D_>//некий код D_>#endif D_>

D_>Чем она лучше такой:

D_>

D_>#if defined(HEADER123_HPP) D_>#error wrong dependence detected D_>#endif D_>#define HEADER123_HPP D_>//некий код D_>

Это защита не от того, о чем ты подумал.
Представь, что ты включил хедер a.h в b.h и в c.h, а потом включаешь b.h и c.h в d.h.

jazzer (Skype: enerjazzer) Ночная тема для RSDN
Автор: jazzer
Дата: 26.11.09

You will always get what you always got If you always do what you always did

Re: защита от повторного include

От: zitz
Дата: 26.02.06 20:03
Оценка:

Здравствуйте, Dmi_3, Вы писали:

D_>Объясните достоинства обычной внутренней защиты #include

D_>

D_>#if !defined(HEADER123_HPP) D_>#define HEADER123_HPP D_>//некий код D_>#endif D_>

Читай по порядку: если КодЗадефайнен (т.е. мы его уже вставляли), то его пропустить, иначе вставить код и задефайнить.

D_>Чем она лучше такой:

D_>

D_>#if defined(HEADER123_HPP) D_>#error wrong dependence detected D_>#endif D_>#define HEADER123_HPP D_>//некий код D_>

Читай по порядку: если КодЗадезайнен вывести еггог и дальше вставить некий код, иначе задефайнить и вставить код.

Помоему разница налицо! В первом случае повторная вставка кода будет игнорироваться, а во втором высыпать куча ошибок.
Хотя если нужно чтобы так и было то второй вариант лучше, т.е. как написано в топике «защита от повторного include», хотя с какой целью писать хеадер чтобы его нельзя было использовать в нескольких частях проги непонятно.

Re[2]: защита от повторного include

От: jazzer Skype: enerjazzer
Дата: 26.02.06 20:21
Оценка:

Здравствуйте, zitz, Вы писали:

D_>>

D_>>#if defined(HEADER123_HPP) D_>>#error wrong dependence detected D_>>#endif D_>>#define HEADER123_HPP D_>>//некий код D_>>

Z>Читай по порядку: если КодЗадезайнен вывести еггог и дальше вставить некий код, иначе задефайнить и вставить код.
Z>Помоему разница налицо! В первом случае повторная вставка кода будет игнорироваться, а во втором высыпать куча ошибок.

Не так.
#error выдает ошибку и останавливает компиляцию.
Так что кучи ошибок не будет, будет всего одна (если у тебя, конечно, не параллельная сборка 🙂 ).

jazzer (Skype: enerjazzer) Ночная тема для RSDN
Автор: jazzer
Дата: 26.11.09

You will always get what you always got If you always do what you always did

Re: защита от повторного include

От: Андрей Тарасевич
Дата: 26.02.06 20:52
Оценка:

Здравствуйте, Dmi_3, Вы писали:

D_>Объясните достоинства обычной внутренней защиты #include

D_>

D_>#if !defined(HEADER123_HPP) D_>#define HEADER123_HPP D_>//некий код D_>#endif D_>

D_>Чем она лучше такой:

D_>

D_>#if defined(HEADER123_HPP) D_>#error wrong dependence detected D_>#endif D_>#define HEADER123_HPP D_>//некий код D_>

Повторное включение заголовочного фала в одну и ту же единицу трансляции не является чем-то ненормальным. Поэтому ругаться на это через ‘#error’ — это непонятная и странная идея.

Под «защитой от посторного включения» имеется я виду не запрещение физического повторного включения, а избежание повторной компиляции содержимого заголовочного фала в ситуации, когда он оказался включенным повторно в одну и ту же единицу трансляции.

Best regards,
Андрей Тарасевич
Re[2]: защита от повторного include

От: Andrej Kalinicenko
Дата: 27.02.06 10:14
Оценка:

» Андрей Тарасевич » <2174@users.rsdn.ru>schrieb im Newsbeitrag news:1700156@news.rsdn.ru.
> D_>

> D_>#if defined(HEADER123_HPP) > D_>#error wrong dependence detected > D_>#endif > D_>#define HEADER123_HPP > D_>//некий код > D_>

>
> Повторное включение заголовочного фала в одну и ту же единицу трансляции не является чем-то ненормальным. Поэтому ругаться на это через ‘#error’ — это непонятная и странная идея.
>

Видел чудака который в таком заголовчном файле глобальные переменные описывал

Re: Re: задолбали разработчики, ниче не пишут че надо. сам вот взялся

[root@mobile100 cex]# c++ main.c f.c -o silly f.c:2:15: warning: extra tokens at end of #ifndef directive f.c:3:15: warning: ISO C requires whitespace after the macro name In file included from /usr/include/c++/3.2.2/backward/iostream.h:31, from f.c:4: /usr/include/c++/3.2.2/backward/backward_warning.h:32:2: warning: #warning This file includes at least one deprecated or antiquated header. Please consider using one of the 32 headers found in section 17.4.1.2 of the C++ standard. Examples include substituting the header for the header for C++ includes, or instead of the deprecated header . To disable this warning use -Wno-deprecated. [root@mobile100 cex]#

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

anonymous
( 17.11.03 20:18:43 MSK )

Re: Re: задолбали разработчики, ниче не пишут че надо. сам вот взялся

// f.c #ifndef header.h /*HEADER - это название твоего хидера*/ #define header.h #include #include "header.h" #endif void f () < cout [root@mobile100 cex]# c++ main.c f.c -o silly f.c:2:15: warning: extra tokens at end of #ifndef directive f.c:3:15: warning: ISO C requires whitespace after the macro name In file included from /usr/include/c++/3.2.2/backward/iostream.h:31, from f.c:4: /usr/include/c++/3.2.2/backward/backward_warning.h:32:2: warning: #warning This file includes at least one deprecated or antiquated header. Please consider using one of the 32 headers found in section 17.4.1.2 of the C++ standard. Examples include substituting the header for the header for C++ includes, or instead of the deprecated header . To disable this warning use -Wno-deprecated. [root@mobile100 cex]# вот так вот!! :( Может быть оно дает варнинг на то, что если я определю какюннить функцию, а она вдруг гдето зарезервированая в другой системной библиотеке? чето ниче не запостить. !!

vilfred ☆☆
( 17.11.03 20:19:51 MSK )

Re: Re: задолбали разработчики, ниче не пишут че надо. сам вот взялся

по этому поводу: // f.c #include #include "header.h" #endif void f () < cout дает ошибку [root@mobile100 cex]# c++ main.c f.c -o silly f.c:2:18: stream: No such file or directory f.c: In function `void f()': f.c:5: `cout' undeclared (first use this function) f.c:5: (Each undeclared identifier is reported only once for each function it appears in.) [root@mobile100 cex]# на это // f.c #include #include "header.h" void f () < cout дает ошибку File f.c not changed so no update needed. [root@mobile100 cex]# c++ main.c f.c -o silly f.c: In function `void f()': f.c:5: `cout' undeclared (first use this function) f.c:5: (Each undeclared identifier is reported only once for each function it appears in.) [root@mobile100 cex]# Че делать? Короче, в принципе оно конешно работает, но как то после #!/usr/bin/perl -w или use strict; use warnings непонятно, откуда это все, че ей не нравится. Может команда cout устарела просто со времен страустрапа? Причем, если я кимпилю gcc, выскакивает ошибка что нет либы. Если я компилю cc выскакивает что нет либы stream или iostream не помогало даже принудительно прописывание #include д

vilfred ☆☆
( 17.11.03 20:25:44 MSK )

Re: Re: Re: задолбали разработчики, ниче не пишут че надо. сам вот взялся

#include . std::cout
abbr ★
( 17.11.03 20:37:08 MSK )

Re: Re: Re: Re: задолбали разработчики, ниче не пишут че надо. сам вот взялся

vilfred ☆☆
( 17.11.03 20:51:44 MSK )

Не того Страустроупа читал, ищи 3-ю редакцию

Читай не просто С++, а стандарт ISO С++. Там все стандартные заголовочные файлы заменены на такие же, но без раширений. И все помещено в namespace std.

anonymous
( 24.11.03 15:19:42 MSK )
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.

Похожие темы

  • Форум ошибка при компеляции (2004)
  • Форум warning: #warning This file includes at least. (2005)
  • Форум Языками не владею (2004)
  • Форум Проблеммы компиляции в C++ (2004)
  • Форум Заголовочный файл (2006)
  • Форум [ Perl ] Проблема с использованием inline CPP (2008)
  • Форум контейнеры (2005)
  • Форум проблема с с++ (2004)
  • Форум не компилируются 5 строчек кода =( (2005)
  • Форум начинаюшему с++ помогите (2006)

Руководство Google по стилю в C++. Часть 11

Не используйте #pragma once. Вместо этого используйте стандартную защиту от повторного включения, описанную в руководстве от Google. Компоненты пути в имени для макроопределения защиты должны быть относительными к корню проекта.

Какие-то вредные советы.

Всего голосов 6: ↑6 и ↓0 +6
Ответить Добавить в закладки Ещё
Показать предыдущий комментарий

Вполне возможная причина это компиляторы, неподдерживающие эту прагму.

Гугл компания немолодая, мало ли какое старьё у них в закромах :)

Комментарий пока не оценивали 0
Ответить Добавить в закладки Ещё
Показать предыдущий комментарий

Даже компилятор из поставки Visual C++ 6.0 поддерживает

Всего голосов 2: ↑2 и ↓0 +2
Ответить Добавить в закладки Ещё
Показать предыдущий комментарий

И, кстати, насколько позволяет вспомнить мой склероз, в исходниках, которые генерировала VC6, комбинировались оба подхода: в хедер в начале записывалась прагма, а остальная часть включалась в #ifdef-«скобки» (извините, что я использую такое просторечное выражение вместо гугловского «макроопределение защиты» 8-P).

Комментарий пока не оценивали 0
Ответить Добавить в закладки Ещё
Показать предыдущий комментарий

Это правильный и переносимый вариант.

ifdef умеют все компиляторы, а если знакомы с pragma once, то используем.

И волки сыты и овцы целы.

Всего голосов 1: ↑1 и ↓0 +1
Ответить Добавить в закладки Ещё

Не используйте венгерскую нотацию (например, именование целочисленной переменной как iNum)

Это НЕ венгерская нотация, а то, что с ней стало, когда она попала в руки профанов. Изначально Чарльз Симоньи предлагал использовать такие префиксы, как rw для записей (rows), например, rwPosition, us (unsafe string) для строк, нуждающихся в санации (на предмет инъекций и т.п.), d (delta) для разницы зачений, например, dY. Большинство префиксов были семантические и никакой IDE никогда не смог бы верно указать эту семантику при наведении курсора. (Как любят аргументировать противники венгерки). Ну, разве что такие префиксы как pX говорят о типе, но тип указателя — сам по себе семантический.

Потом всё это дело опопсело и упростилось, и действительно стало дублировать информацию о типе, после чего венгерка разделилась на т.н. прикладную и системную. Последнюю иначе как профанацией идеи назвать трудно.

Что касается первой, она, насколько можно судить, во многих местах жива и поныне. Просто теперь префикс принято записывать целиком, не экономя ширину экрана: deltaY и т.д.

Составителям правил оформления, тем более из Гугл, знать такие тонкости, по идее, не помешало бы.

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

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