Почему мои включенные охранники не предотвращают рекурсивное включение и несколько определений символов?
два общих вопроса о включить охранников:
-
ПЕРВЫЙ ВОПРОС:
почему не включают охранников, защищающих мои заголовочные файлы от взаимное рекурсивное включение? Я продолжаю получать ошибки о несуществующих символах, которые, очевидно, есть или даже более странные синтаксические ошибки каждый раз, когда я пишу что-то вроде следующее:
"а.ч"
#ifndef A_H #define A_H #include "b.h" ... #endif // A_H
"б.ч"
#ifndef B_H #define B_H #include "a.h" ... #endif // B_H
"главная.ФКП"
#include "a.h" int main() { ... }
почему я получаю ошибки компиляции " main.cpp"? Что мне нужно сделать, чтобы решить мою проблему?
-
ВТОРОЙ ВОПРОС:
почему не включают охранников предотвращая несколько определений? Например, когда мой проект содержит два файла, которые включают один и тот же заголовок, иногда компоновщик жалуется на то, что какой-то символ определяется несколько раз. Например:
заголовок".h"
#ifndef HEADER_H #define HEADER_H int f() { return 0; } #endif // HEADER_H
"файлы source1.ФКП"
#include "header.h" ...
" source2.ФКП"
#include "header.h" ...
почему это происходит? Что мне нужно сделать, чтобы решить мою проблему?
1 ответ:
ПЕРВЫЙ ВОПРОС:
почему не включены охранники, защищающие мои заголовочные файлы от взаимное рекурсивное включение?
они.
что они не помогают с зависимости между определениями структур данных во взаимно включающих заголовках. Чтобы понять, что это значит, давайте начнем с базового сценария и посмотрим, почему включают охранники действительно помогают с взаимными включениями.
предположим, что ваши взаимно включая
a.h
иb.h
заголовочные файлы имеют тривиальное содержание, т. е. эллипсы в секции кода из текста вопроса заменяются пустой строкой. В этой ситуации, вашmain.cpp
будет с удовольствием компилировать. И это только благодаря вашим охранникам!если вы не уверены, попробуйте удалить их:
//================================================ // a.h #include "b.h" //================================================ // b.h #include "a.h" //================================================ // main.cpp // // Good luck getting this to compile... #include "a.h" int main() { ... }
вы заметите, что компилятор сообщит a отказ, когда он достигает предела глубины включения. Это ограничение зависит от конкретной реализации. В соответствии с пунктом 16.2 / 6 стандарта C++11:
директива #include preprocessing может появиться в исходном файле, который был прочитан из-за директивы #include в другом файле, до определенного реализацией предела вложенности.
так что же происходит?
- при разборе
main.cpp
, в препроцессор будет соответствовать директиве#include "a.h"
. Эта директива указывает препроцессору обработать заголовочный файлa.h
, принять результат этой обработки, и замените строку#include "a.h"
С этим результатом;- при обработке
a.h
, препроцессор будет соответствовать директиву#include "b.h"
, и тот же механизм применяется: препроцессор должен обрабатывать заголовочный файлb.h
, возьмите результат его обработки и замените наb.h
С пустой строкой, и будет отслеживать выполнение, пока он не заменит исходный наb.h
С пустой строкой, препроцессор начнет анализировать содержаниеb.h
и, в частности, определениеB
. К сожалению, определениеB
упоминает классA
, который никогда не встречался прежде точно , потому что включение охраны!объявление переменной-члена типа, который ранее не был объявлен, конечно, является ошибкой, и компилятор вежливо укажет на это.
что мне нужно сделать, чтобы решить мою проблему?
вам нужно вперед деклараций.
в самом деле определение класса
A
не требуется для определения классаB
, потому что a указатель доA
объявляется как переменная-член, а не объект типаA
. Поскольку указатели имеют фиксированный размер, компилятору не нужно будет знать точный макетA
ни вычислить его размер, чтобы правильно определить классB
. Следовательно, достаточно вперед-объявить классA
наb.h
и заставить компилятор знать о его существовании://================================================ // b.h #ifndef B_H #define B_H // Forward declaration of A: no need to #include "a.h" struct A; struct B { A* pA; }; #endif // B_H
код
main.cpp
теперь, конечно, компилировать. Пару Примечания:
- не только нарушение взаимного включения путем замены
#include
директива с прямым объявлением вb.h
было достаточно, чтобы эффективно выразить зависимостьB
onA
: использование прямых деклараций, когда это возможно / практически, также считается хорошая практика программирования, потому что это помогает избежать ненужных включений, тем самым уменьшая общее время компиляции. Однако после устранения взаимного включение,main.cpp
придется изменить на#include
иa.h
иb.h
(если последнее вообще нужно), потому чтоb.h
уже не косвенно#include
d черезa.h
;- в то время как прямое объявление класса
A
достаточно для компилятора объявить указатели на этот класс (или использовать его в любом другом контексте, где допустимы неполные типы), разыменовывая указатели наA
(например, для вызова функции-члена) или вычисления ее размера являются незаконно операции с неполными типами: если это необходимо, полное определениеA
должен быть доступен компилятору, что означает, что файл заголовка, который его определяет, должен быть включен. Вот почему определения классов и реализация их функций-членов обычно разделяются на файл заголовка и файл реализации для этого класса (class шаблоны являются исключением из этого правила): файлы реализации, которые никогда не#include
d другими файлы в проекте, можно смело#include
все необходимые заголовки, чтобы сделать видимыми определений. Заголовочные файлы, с другой стороны, не#include
другие заголовочные файлы если они действительно должны сделать это (например, чтобы сделать определение базовый класс visible), и будет использовать forward-объявления, когда это возможно/практически.ВТОРОЙ ВОПРОС:
почему не включают охранников предотвращение несколько определений?
они.
то, что они не защищают вас от нескольких определений в отдельных единицах перевода. Это также объясняется в это Q & A на StackOverflow.
тоже вижу, что, попробуйте удалить включить охранников и компиляции следующей, измененной версии
source1.cpp
(илиsource2.cpp
, для чего он имеет значение)://================================================ // source1.cpp // // Good luck getting this to compile... #include "header.h" #include "header.h" int main() { ... }
компилятор, безусловно, будет жаловаться здесь
f()
изменено. Это очевидно: его определение включается дважды! Однако вышеsource1.cpp
будет компилироваться без проблем, когдаheader.h
содержит соответствующие включить охранников. Это ожидаемо.тем не менее, даже когда охранники include присутствуют, и компилятор перестанет беспокоить вас сообщением об ошибке,линкер будет настаивать на дело в том, что при слиянии нескольких определений обнаруживается объектный код, полученный из компиляции
source1.cpp
иsource2.cpp
, и будет отказываться генерировать исполняемый файл.почему это происходит?
в принципе, каждый
.cpp
file (технический термин в этом контексте ЕП) в вашем проекте компилируется отдельно и самостоятельно. При анализе , в препроцессор будет обрабатывать все#include
директивы и развернуть все макро-вызовы, с которыми он сталкивается, и выход этой чистой обработки текста будет дан на входе компилятору для перевода его в объектный код. Как только компилятор закончит с созданием объектного кода для одной единицы перевода, он перейдет к следующей, и все определения макросов, которые были обнаружены при обработке предыдущей единицы перевода, будут забыты.на самом деле, компиляция проекта с помощью
n
единицы перевода (.cpp
files) - это как выполнение одной и той же программы (компилятора)n
раз, каждый раз с другим входом: разные исполнения одной и той же программы не будет делиться состоянием предыдущего выполнения программы(ов). Таким образом, каждый перевод выполняется независимо, и символы препроцессора, встречающиеся при компиляции одной единицы перевода, не будут запоминаться при компиляции других единиц перевода (если вы думаете об этом на мгновение, вы легко поймете, что это на самом деле желаемое поведение).поэтому, даже если включить охранники помогут вам предотвратить рекурсивные взаимные включения и резервные включения одного и того же заголовка в одну единицу перевода, они не могут определить, включено ли одно и то же определение в разные единица перевода.
тем не менее, при объединении объектного кода, созданного из компиляции всех
.cpp
файлы вашего проекта, компоновщик будет смотрите, что один и тот же символ определяется несколько раз, и так как это нарушает Одно Правило Определения. В соответствии с пунктом 3.2 / 3 стандарта C++11:каждая программа должна содержать ровно одно определение каждого non-inline функция или переменная, которая odr-используется в этой программе; не требуется диагностика. Определение может появиться явно в программа, она может быть найдена в стандартной или пользовательской библиотеке, или (при необходимости) она неявно определена (см. 12.1, 12.4 и 12.8). встроенная функция должна быть определена в каждой единице перевода, в которой она используется odr.
следовательно, компоновщик выдаст ошибку и откажется генерировать исполняемый файл вашей программы.
что мне нужно сделать, чтобы решить мою проблема?
если вы хотите сохранить определение функции в заголовочном файле, который является
#include
д несколько единицы перевода (заметьте, что никаких проблем не возникнет, если ваш заголовок#include
d просто по один блок перевода), вам нужно использоватьinline
ключевое слово.в противном случае, вам нужно сохранить только декларация функции в
header.h
, поставив его определение (тело) в один отдельные.cpp
только файл (это классический подход).The
inline
ключевое слово представляет собой необязательный запрос к компилятору, чтобы встроить тело функции непосредственно на сайте вызова, а не настраивать кадр стека для обычного вызова функции. Хотя компилятор не должен выполнять ваш запрос,inline
ключевое слово действительно успешно сообщает компоновщику о допуске нескольких определений символов. согласно пункту 3.2 / 5 стандарта C++11:может быть более одного определения тип класса (пункт 9), тип перечисления (7.2),встроенная функция с внешней компоновкой (7.1.2), шаблон класса (пункт 14), шаблон нестатической функции (14.5.6), статический элемент данных шаблона класса (14.5.1.3), функция-член шаблона класса (14.5.1.1) или специализация шаблона, для которой некоторые параметры шаблона не указаны (14.7, 14.5.5) в программе при условии, что каждое определение отображается в отдельной единице перевода, и при условии, что определения удовлетворяют следующим требованиям [...]
в приведенном выше абзаце в основном перечислены все определения, которые обычно помещаются в заголовочные файлы, потому что они могут быть безопасно включен в несколько единиц трансляции. Все другие определения с внешней связью, вместо этого, принадлежат исходным файлам.
с помощью
static
ключевое слово вместоinline
ключевое слово также приводит к подавлению ошибок компоновщика, предоставляя свою функцию внутренняя перелинковка, таким образом делая каждую единицу перевода держать частный скопировать этой функции (и ее локальных статических переменных). Однако это в конечном итоге приводит к большему исполняемому файлу и использованиюinline
следует предпочесть в целом.альтернативный способ достижения того же результата как с
static
ключевое слово поставить функциюf()
на Безымянное пространство имен. Согласно пункту 3.5 / 4 стандарта C++11:Безымянное пространство имен или пространство имен, объявленное прямо или косвенно в безымянном пространстве имен, имеет внутреннюю связь. Все остальные пространства имен имеют внешнюю связь. Имя, имеющее область пространства имен, которая не была задана внутренней связью выше, имеет ту же связь, что и заключающее пространство имен, если это имя:
- переменная; или
-функция; или
- именованный класс (пункт 9) или безымянный класс, определенный в объявлении typedef, в котором класс имеет имя typedef для целей компоновки (7.1.3); или
- именованное перечисление (7.2) или Безымянное перечисление, определенное в объявлении typedef, в котором перечисление имеет имя typedef для целей компоновки (7.1.3); или
- an перечислитель, принадлежащий перечислению со связью; или
- шаблон.
по той же причине, упомянутой выше,
inline
ключевое слово должно быть предпочтительным.