Препроцессор - это программа, которая принимает вашу программу, вносит некоторые изменения (например, включают файлы (#include), расширение макроса (#define) и в основном все, что начинается с #
) и дает «чистый» результат к компилятору.
Препроцессор работает так, когда видит #include
:
Когда вы пишете:
#include "some_file"
Содержимое some_file
почти буквально получить копию, вставленную в файл, включая его. Теперь, если у вас есть:
a.h:
class A { int a; };
И:
b.h:
#include "a.h"
class B { int b; };
И:
main.cpp:
#include "a.h"
#include "b.h"
Вы получаете:
main.cpp:
class A { int a; }; // From #include "a.h"
class A { int a; }; // From #include "b.h"
class B { int b; }; // From #include "b.h"
Теперь вы можете увидеть, как переопределяется A
.
Когда вы пишете охранники, они становятся такими:
a.h:
#ifndef A_H
#define A_H
class A { int a; };
#endif
b.h:
#ifndef B_H
#define B_H
#include "a.h"
class B { int b; };
#endif
Итак, теперь давайте посмотрим, как #include
s в главном будет расширяться (это точно так же, как в предыдущем случае: copy-paste)
main.cpp:
// From #include "a.h"
#ifndef A_H
#define A_H
class A { int a; };
#endif
// From #include "b.h"
#ifndef B_H
#define B_H
#ifndef A_H // From
#define A_H // #include "a.h"
class A { int a; }; // inside
#endif // "b.h"
class B { int b; };
#endif
Теперь давайте следовать препроцессору и посмотреть, что из этого происходит «настоящий» код. Я пойду по строчке:
// From #include "a.h"
Комментарий. Игнорировать! Продолжить:
#ifndef A_H
Определен ли A_H
? Нет! Затем продолжаем:
#define A_H
Хорошо теперь A_H
. Продолжить:
class A { int a; };
Это не что-то для препроцессора, так что просто оставьте это. Продолжить:
#endif
Предыдущая if
завершена здесь. Продолжить:
// From #include "b.h"
Комментарий. Игнорировать! Продолжить:
#ifndef B_H
Определен ли B_H
? Нет! Затем продолжайте:
#define B_H
Хорошо теперь B_H
. Продолжить:
#ifndef A_H // From
Определен ли A_H
? ДА! Затем проигнорируйте до соответствующего #endif
:
#define A_H // #include "a.h"
Игнорировать
class A { int a; }; // inside
Игнорировать
#endif // "b.h"
Предыдущая if
завершена здесь. Продолжить:
class B { int b; };
Это не что-то для препроцессора, так что просто оставьте это. Продолжить:
#endif
. Предыдущая if
завершена здесь.
То есть, после того, как препроцессор сделан с файлом, это то, что видит компилятор:
main.cpp
class A { int a; };
class B { int b; };
Итак, как вы можете видеть, все, что может получить #include
d в том же файле дважды, прямо или косвенно, нужно охранять. Поскольку файлы .h
всегда очень часто включаются дважды, хорошо, если вы охраняете ВСЕ ваши .h-файлы.
P.S. Обратите внимание, что у вас также есть круговой #include
s. Представьте, что препроцессор скопировал код Physics.h в GameObject.h, который видит, что есть #include "GameObject.h"
, что означает копирование GameObject.h
в себя. Когда вы копируете, вы снова получаете #include "Pysics.h"
, и вы навсегда застреваете в цикле. Компиляторы предотвращают это, но это означает, что ваши #include
s наполовину сделаны.
Прежде чем говорить, как исправить это, вы должны знать другое.
Если у вас есть:
#include "b.h"
class A
{
B b;
};
Тогда компилятор должен знать все о b
, самое главное, какие переменные он имеет и т. д., чтобы он знал, сколько байтов должно помещать вместо b
в A
.
Однако, если у вас есть:
class A
{
B *b;
};
Тогда компилятору действительно не нужно ничего знать о B
(поскольку указатели, независимо от типа, имеют одинаковый размер ). Единственное, что ему нужно знать о B
, это то, что он существует!
Итак, вы делаете что-то под названием «forward declaration»:
class B; // This line just says B exists
class A
{
B *b;
};
Это очень похоже на многие другие вещи, которые вы делаете в заголовочных файлах, таких как:
int function(int x); // This is forward declaration
class A
{
public:
void do_something(); // This is forward declaration
}