Почему мои охранники include не предотвращают рекурсивное включение и несколько определений символов?
два общих вопроса о включить охранников:
-
ПЕРВЫЙ ВОПРОС:
почему не включают охранников, защищающих мои файлы заголовков от взаимное рекурсивное включение? Я продолжаю получать ошибки о несуществующих символах, которые, очевидно, существуют или даже более странные синтаксические ошибки каждый раз, когда я пишу что-то вроде следующий:
"а.ч"
#ifndef A_H #define A_H #include "b.h" ... #endif // A_H
"б.ч"
#ifndef B_H #define B_H #include "a.h" ... #endif // B_H
"главная.cpp"
#include "a.h" int main() { ... }
почему я получаю ошибки компиляции " main.cpp"? Что мне нужно сделать, чтобы решить свою проблему?
-
ВТОРОЙ ВОПРОС:
почему не включают охранников предотвращая несколько определений? Например, когда мой проект содержит два файла, содержащих один и тот же заголовок, иногда компоновщик жалуется на то, что какой-то символ определяется несколько раз. Например:
заголовок".h"
#ifndef HEADER_H #define HEADER_H int f() { return 0; } #endif // HEADER_H
"файлы source1.cpp"
#include "header.h" ...
"файл source2.cpp"
#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()
{
...
}
вы заметите, что компилятор сообщит сбой при достижении предельной глубины включения. Это ограничение зависит от конкретной реализации. В соответствии с пунктом 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
, возьмите результат его обработки и замените еще не определен, он будет продолжать обрабатывать следующий текст. Последующая директива (#defines A_H
) определяет макросA_H
. Тогда препроцессор будет соответствовать директиве#include "b.h"
: препроцессор теперь должен обработать файл заголовкаb.h
, возьмите результат его обработки и замените наb.h
С пустой строкой и будет отслеживать выполнение до тех пор, пока он не заменит исходный наb.h
С пустой строкой, препроцессор начнет анализировать содержаниеb.h
и, в частности, определениеB
. К сожалению, определениеB
упоминает классA
, который никогда не встречался прежде точно , потому что включение охраны!объявление переменной-члена типа, который ранее не был объявлен, конечно, является ошибкой, и компилятор вежливо укажет на это.
что мне нужно сделать, чтобы решить мою проблему?
вам нужно вперед деклараций.
в самом деле определение класса
A
не требуется для определения классаB
, а потому указатель toA
объявляется как переменная-член, а не объект типа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.
тоже см. Это, попробуйте удалить include guards и скомпилировать следующую, измененную версию
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
d by несколько единицы перевода (заметьте, что никаких проблем не возникнет, если ваш заголовок#include
d просто один блок перевода), вам нужно использоватьinline
ключевое слово.в противном случае вам нужно сохранить только декларация функции в
header.h
, поставив его определение (тело) в один отдельные.cpp
только файл (это классический подход).на
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
ключевое слово должно быть предпочтительным. - не только нарушение взаимного включения путем замены