Препроцессор - это программа, которая принимает вашу программу, вносит некоторые изменения (например, включает файлы (#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 в main (это точно так же, как и в предыдущем случае: 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.
PS Обратите внимание, что у вас также есть циклические #include
s.Представьте, что препроцессор скопировал код Physics.h в GameObject.h, который видит #include "GameObject.h"
, что означает копирование GameObject.h
в себя.Когда вы копируете, вы снова получаете #include "Pysics.h"
, и вы застряли в цикле навсегда.Компиляторы предотвращают это, но это означает, что ваши #include
наполовину завершены.
Прежде чем сказать, как это исправить, вы должны знать другую вещь.
Если у вас есть:
#include "b.h"
class A
{
B b;
};
Тогда компилятору нужно знать все о b
, самое главное, какие переменные он имеет и т. Д., Чтобы он знал, сколько байтов он должен поместить вместо b
в A
.
Однако, если у вас есть:
class A
{
B *b;
};
Тогда компилятору на самом деле не нужно ничего знать о B
(поскольку указатели независимо от типа имеют одинаковый размер).Единственное, что нужно знать о B
, это то, что он существует!
Итак, вы делаете что-то, что называется «предварительным объявлением»:
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
}