Почему мои включенные охранники не предотвращают рекурсивное включение и несколько определений символов?


два общих вопроса о включить охранников:

  1. ПЕРВЫЙ ВОПРОС:

    почему не включают охранников, защищающих мои заголовочные файлы от взаимное рекурсивное включение? Я продолжаю получать ошибки о несуществующих символах, которые, очевидно, есть или даже более странные синтаксические ошибки каждый раз, когда я пишу что-то вроде следующее:

    "а.ч"

    #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"? Что мне нужно сделать, чтобы решить мою проблему?


  1. ВТОРОЙ ВОПРОС:

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

    заголовок".h"

    #ifndef HEADER_H
    #define HEADER_H
    
    int f()
    {
        return 0;
    }
    
    #endif // HEADER_H
    

    "файлы source1.ФКП"

    #include "header.h"
    ...
    

    " source2.ФКП"

    #include "header.h"
    ...
    

    почему это происходит? Что мне нужно сделать, чтобы решить мою проблему?

1 64

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 в другом файле, до определенного реализацией предела вложенности.

так что же происходит?

  1. при разборе main.cpp, в препроцессор будет соответствовать директиве #include "a.h". Эта директива указывает препроцессору обработать заголовочный файл a.h, принять результат этой обработки, и замените строку #include "a.h" С этим результатом;
  2. при обработке 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 теперь, конечно, компилировать. Пару Примечания:

    1. не только нарушение взаимного включения путем замены #include директива с прямым объявлением в b.h было достаточно, чтобы эффективно выразить зависимость B on A: использование прямых деклараций, когда это возможно / практически, также считается хорошая практика программирования, потому что это помогает избежать ненужных включений, тем самым уменьшая общее время компиляции. Однако после устранения взаимного включение, main.cpp придется изменить на #include и a.h и b.h (если последнее вообще нужно), потому что b.h уже не косвенно #included через a.h;
    2. в то время как прямое объявление класса A достаточно для компилятора объявить указатели на этот класс (или использовать его в любом другом контексте, где допустимы неполные типы), разыменовывая указатели на A (например, для вызова функции-члена) или вычисления ее размера являются незаконно операции с неполными типами: если это необходимо, полное определение A должен быть доступен компилятору, что означает, что файл заголовка, который его определяет, должен быть включен. Вот почему определения классов и реализация их функций-членов обычно разделяются на файл заголовка и файл реализации для этого класса (class шаблоны являются исключением из этого правила): файлы реализации, которые никогда не #included другими файлы в проекте, можно смело #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д несколько единицы перевода (заметьте, что никаких проблем не возникнет, если ваш заголовок #included просто по один блок перевода), вам нужно использовать 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 ключевое слово должно быть предпочтительным.