Есть следующий код:
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