Date Редакция Категория comp Теги Cpp

Есть следующий код:

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