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

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

  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
    

    "главная.cpp"

    #include "a.h"
    int main()
    {
        ...
    }
    

    почему я получаю ошибки компиляции " main.cpp"? Что мне нужно сделать, чтобы решить свою проблему?


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

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

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

что происходит?

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

    тоже см. Это, попробуйте удалить 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.

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

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

    если вы хотите сохранить свое определение функции в файле заголовка, который #included by несколько единицы перевода (заметьте, что никаких проблем не возникнет, если ваш заголовок #included просто один блок перевода), вам нужно использовать 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 ключевое слово должно быть предпочтительным.