C++. Бархатный путь. Часть 2 - Разбор структуры класса

ОГЛАВЛЕНИЕ

 

Разбор структуры класса

Разбор структуры класса осуществляется транслятором в несколько этапов.

На первом этапе исследуется список данных-членов класса. Именно этот список и определяет общую структуру класса. До окончания этой стадии разбора класса, а фактически до завершения объявления класса, его имя в объявлении данных-членов может быть использовано лишь в таком контексте, где не используется информация о размерах класса. Это объявления указателей, ссылок и статических членов класса (о них после).

Таким образом, объект-представитель класса не может быть членом собственного класса, поскольку объект-представитель класса может быть объявлен как член класса лишь после того, как завершено объявление этого класса.

Функция-член класса существует в единственном экземпляре для всех объектов-представителей данного класса. Переобъявление и уточнение структуры класса в С++ недопустимо.

Серия простых примеров демонстрирует, что можно, а что нельзя делать при объявлении данных-членов класса.

class C1
{
C1 MyC;
// Это ошибка. В классе не допускается объявления данных-членов
// объявляемого класса.
C1* pMyC;
// А указатель на класс объявить можно.
};

Для объявления таких указателей или ссылок на объекты объявляемого класса достаточно неполного предварительного объявления класса. Указатели и ссылки имеют фиксированные размеры, которые не зависят от типа представляемого объекта.

class C2;
class C1
{
C1* pMyC1;
C2* pMyC2;
};
C2* PointOnElemOfClassC2;

Назначение неполного объявления подобно прототипу функции и используется исключительно в целях предварительного информирования транслятора. Очевидно, что создание объектов на основе предварительного неполного объявления невозможно. Однако это не снижает ценности уточнённого спецификатора.

На втором проходе трансляции объявления класса осуществляется проверка списков параметров в объявлениях функций-членов класса, и определяется размер класса. К этому моменту транслятору становится известна общая структура класса. И потому, как ни странно это выглядит, в классе может быть объявлена функция-член класса, которая возвращает значение объявляемого класса и содержит в списке параметров параметры этого же класса:

class C2;
class C1
{
C1 F1(C1 par1) {return par1;};
//Объявить данные-члены класса C1 нельзя, а функцию - можно!
C1* pMyC1;
C2* pMyC2;
// C1 MyC;
};
C2* PointOnElemOfClassC2;

Где бы ни располагалась объявляемая в классе функция-член, транслятор приступает к её разбору лишь после того, как он определяет общую структуру класса.

В соответствии с формальным определением создадим наш первый класс:

СпецификаторКласса ::= ЗаголовокКласса { [СписокЧленов] };        ::=
КлючевоеСловоКласса Идентификатор { ОбъявлениеЧленаКласса
ОбъявлениеЧленаКласса }; ::=
class FirstClass { СпецификаторОбъявления ОписательЧленаКласса;
ОписаниеФункции; }; ::=
class FirstClass { СпецификаторОбъявления ОписательЧленаКласса;
int FirstClassFunction(void);}; ::=
class FirstClass {
long int* PointerToLongIntVal;
int FirstClassFunction(void);
};

За исключением квалифицируемого имени синтаксис определения функции-члена класса вне класса ничем не отличается от определения обычной функции:

int FirstClass::FirstClassFunction(void) 
{
int IntVal = 100;
return IntVal;
};

Вот таким получилось построенное в соответствии с грамматикой C++ определение (или объявление) класса.

Заметим, что в C++ существует единственное ограничение, связанное с расположением определения функции-члена класса (конечно, если оно располагается вне тела класса): определение должно располагаться за объявлением класса, содержащего эту функцию. Именно "за объявлением"! Без каких-либо дополнительных ограничений типа "непосредственно за" или "сразу за".

Более того, в ряде случаев, например, когда требуется определить функцию-член, изменяющую состояние объекта другого класса, данная функция-член должна располагаться за объявлением класса, состояние объекта которого она изменяет. И это понятно. При разборе такой функции-члена транслятор должен иметь представление о структуре класса.

Допускается и такая схема расположения объявлений, при которой первыми располагаются неполные объявления классов, следом соответствующие объявления классов и лишь затем определения функций-членов. Подобные определения мы будем называть отложенными определениями. Позже мы рассмотрим пример программы, в которой отложенный вариант определения функции-члена является единственно возможным вариантом определения.

Класс - это то, что делает C++ объектно-ориентированным языком. На основе классов создаются новые производные типы и определяются функции, которые задают поведение типа. 

Рассмотрим несколько строк программного кода, демонстрирующих свойства производных типов.

class Class1 {int iVal;};
class Class2 {int iVal;};
/*
Объявление производных типов Class1 и Class2. Эти объявления
вводят в программу два новых производных типа. Несмотря на
тождество их структуры, это разные типы.
*/
void ff(Class1);
/* Прототип функции с одним параметром типа Class1.*/
void ff(Class2);
/*
Прототип функции с одним параметром типа Class2. Это совместно
используемые (или перегруженные) функции. Об этих функциях мы
уже говорили.
*/
Class1 m1; /* Объявление объекта m1 типа Class1. */
Class2 m2; /* Объявление объекта m2 типа Class2. */
int m3;
m1 = m2;
m1 = m3;
m3 = m2;
/*
Последние три строчки в данном контексте недопустимы.
Неявное преобразование с участием производных типов в C++
невозможно. Транслятор не имеет никакого понятия о том, каким
образом проводить соответствующее преобразование. При объявлении
классов необходимо специально определять эти алгоритмы.
*/
void ff (Class1 pp)
// Определение первой совместно используемой функции...
{
:::::
}
void ff (Class2 pp)
// Определение второй совместно используемой функции...
{
:::::
}
ff(m1);//Вызов одной из двух совместно используемых функций...
ff(m2);//Вызов второй функции...

Ещё один пример объявления класса.

class ClassX
{
ClassX Mm; //Здесь ошибка. Объявление класса ещё не завершено.
ClassX* pMm; //Объект типа "Указатель на объект". Всё хорошо.
ClassX FF(char char,int i = sizeof(ClassX));
/*
Прототип функции. Второму параметру присваивается значение по
умолчанию. И напрасно! Здесь ошибка. В этот момент ещё неизвестен
размер класса ClassX.
*/
// А вот вполне корректное определение встроенной функции.
int RR (int iVal)
{
int i = sizeof(ClassX);
return i;
}
/*
Полный разбор операторов в теле функции производится лишь после
полного разбора объявления класса. К этому моменту размер класса
уже будет определён.
*/
}