Есть следующий код:
classA.h
#ifndef CLASSA_H
#define CLASSA_H
#include "classB.h"
class ClassA : public ClassB
{
public:
ClassA() {}
int val;
};
#endif // CLASSA_H
classB.h
#ifndef CLASSB_H
#define CLASSB_H
#include "classA.h" // [1]
//class ClassA; // [2]
class ClassB
{
public:
ClassB() {}
ClassA *ptr;
};
#endif // CLASSB_H
main.cpp
```C++ linenum
include "classA.h"
int main( int, char *[] ) { ClassA t;
return 0;
}
При попытке компиляции выдает:
```bash
user@user:~/work/sep_compilation$ g++ main.cpp -o main
In file included from classA.h:4:0,
from main.cpp:1:
classB.h:13:5: ошибка: «ClassA» не является именем типа
Казалось бы, все вполне логично: раз classB
использует classA
, то необходимо включить в classB.h
описание classA
при помощи #include
(строка 1). Однако, если мы посмотрим повнимательней, то заметим, что classB
использует лишь указатель на classA
, поэтому для "знакомства" с classA
ему достаточно лишь предварительного объявления последнего (раскомментируем строку 2 и закомментируем строку 1). В таком виде программа успешно компилируется.
Если бы у нас были файлы с реализацией классов, то #include
можно было бы поместить туда, но не в заголовок.
Все это известно и подробно описано у Саттера, но хотелось бы самому посмотреть, что здесь происходит. Для этого обработаем файлы препроцессором
g++ -E main.cpp -o main.in
и посмотрим, как включаются файлы по директиве #include
. Результат обработки препроцессором будет находиться в файле main.in
. Сначала обработаем успешный вариант:
# 1 "main.cpp"
# 1 "<command-line>"
# 1 "main.cpp"
# 1 "classA.h" 1
# 1 "classB.h" 1
class ClassA;
class ClassB
{
public:
ClassB() {}
ClassA *ptr;
};
# 5 "classA.h" 2
class ClassA : public ClassB
{
public:
ClassA() {}
int val;
};
# 2 "main.cpp" 2
int main( int, char *[] )
{
ClassA t;
return 0;
}
В файле main.in
содержится журнал обработки исходного файла препроцессором. Точнее: исходный код main.cpp, вставляемых в него файлов и последовательность вставки. Последовательность вставки описывается строками вида:
# номер_строки имя_файла [флаги]
которые означают, что после строки номер_строки
файла имя_файла
выполняется некоторое действие, в соответствие с указанным флагом. Флаги (их может не быть, а может быть один или несколько), означают:
- '1' начало нового файла;
- '2' возвращение в исходный файл (после включения другого файла);
- '3' следующий далее код находится в системном заголовочном файле (некоторые предупреждения будут подавлены);
- '4' следующий далее код следует рассматривать как обернутый в extern "C" блок.
Флаги разделяются пробелом.
Итак, из main.cpp
мы попадаем в classA.h
, а из него -- в classB.h
. Проходим предварительное объявление classA
, тело classB
и возвращаемся на 5-ую строку файла classA.h
, сразу же после #include
. Проходим тело classA
(родитель -- classB
-- нам уже известен) и возвращаемся в main.cpp
, сразу после #include
.
Теперь рассмотрим файл, компилирующийся с ошибкой:
# 1 "main.cpp"
# 1 "<command-line>"
# 1 "main.cpp"
# 1 "classA.h" 1
# 1 "classB.h" 1
# 1 "classA.h" 1
# 5 "classB.h" 2
class ClassB
{
public:
ClassB() {}
ClassA *ptr;
};
# 5 "classA.h" 2
class ClassA : public ClassB
{
public:
ClassA() {}
int val;
};
# 2 "main.cpp" 2
int main( int, char *[] )
{
ClassA t;
return 0;
}
Здесь мы также попадаем из main.cpp
в classA.h
, а затем в classB.h
. Но из classB.h
мы вновь попадаем в classA.h
(вот он #include
!), а подключить этот файл уже нельзя -- мешает ранее объявленный CLASSA_H
. В результате мы возвращаемся в classB.h
и проходим тело classB
так и не "познакомив" его с classA
-- вот и причина ошибки.
Для Visual Studio.
Запуск Microsoft Visual C++ в командной строке:
cl.exe
Опции cl.exe
для обработки препроцессором:
/E
-- вывод результатов вstdout
;/P filename
-- вывод в файлfilename
.
Комментарии
comments powered by Disqus