Класс. Объявление класса

Класс - это тип. Этот производный тип вводится в программу с помощью специального оператора объявления класса. В объявлении класса используется ранее описанный инструментальный набор средств для построения и преобразования производных типов.

Очередное множество форм Бэкуса-Наура определяет синтаксис объявления класса.

Объявление ::= [СписокСпецификаторовОбъявления] [СписокОписателей];

СписокСпецификаторовОбъявления
          ::= [СписокСпецификаторовОбъявления] СпецификаторОбъявления

СпецификаторОбъявления ::= СпецификаторТипа
                       ::= *****

СпецификаторТипа ::= СпецификаторКласса
                 ::= УточнённыйСпецификаторТипа
                 ::= *****

УточнённыйСпецификаторТипа ::= КлючевоеСловоКласса ИмяКласса
                           ::= КлючевоеСловоКласса Идентификатор
                           ::= enum ИмяПеречисления

КлючевоеСловоКласса ::= union
                    ::= struct
                    ::= class

ИмяКласса ::= Идентификатор

СпецификаторКласса ::= ЗаголовокКласса {[СписокЧленов]}

ЗаголовокКласса
           ::= КлючевоеСловоКласса [Идентификатор] [СпецификацияБазы]
           ::= КлючевоеСловоКласса ИмяКласса [СпецификацияБазы]

КлючевоеСловоКласса ::= union
                    ::= struct
                    ::= class

ИмяКласса ::= Идентификатор

Спецификатор класса представляет то, что называется объявлением класса. Уточнённый спецификатор типа объявляет расположенный за ним идентификатор именем класса. Уточнённый спецификатор обеспечивает неполное предварительное объявление класса и перечисления.

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

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

Класс считается объявленным даже тогда, когда в нём полностью отсутствует информация о членах класса (пустой список членов класса). Неименованный класс с пустым множеством членов - уже класс!

Имя класса можно употреблять как имя (имя типа) уже в списке членов этого самого класса.

Класс может быть безымянным.

Следующая последовательность операторов объявления

class {}; /* Объявлен пустой неименованный класс.*/
class {};
class {};
class {};
/* Это всё объявления. Их количество ничем не ограничивается. */
struct {};
/* Структура - это класс, объявленный с ключевым словом struct.
Опять же пустой и неименованный.*/

не вызывает у транслятора никаких возражений.

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

class {} Obj1, Obj2, Obj3;/* Здесь объявление пустого класса.*/
class {} Obj4, Obj5, Obj6;/* Просто нечего инициализировать.*/
class {} Obj1;
       /* ^ Ошибка. Одноименные объекты в области действия имени.*/

Неименованные классы также можно применять в сочетании со спецификатором typedef (здесь может быть объявление класса любой сложности - не обязательно только пустой). Спецификатор typedef вводит новое имя для обозначения безымянного класса. Описанное имя типа становится его единственным именем.

Сочетание спецификатора typedef с объявлением безымянного класса подобно объявлению класса с именем:

class MyClass {/*…*/};
typedef class {/*…*/} MyClass;

Правда в первом случае класс имеет собственное имя класса, а во втором - описанное имя типа. Использование описанного имени типа в пределах области действия имени делает эквивалентными следующие определения (и им подобные):

class {} Obj1;
MyClass  Obj1;

Класс считается объявленным лишь после того, как в его объявлении будет закрыта последняя фигурная скобка. До этого торжественного момента информация о структуре класса остаётся неполной.

Если можно ОБЪЯВИТЬ пустой класс, то можно ОПРЕДЕЛИТЬ и объект-представитель пустого класса. Эти объекты размещаются в памяти. Их размещение предполагает выделение объекту участка памяти с уникальным адресом, а это означает, что объекты пустого класса имеют ненулевой размер.

Действительно, значения выражений sizeof(MyClass) и sizeof(MyObj1) (это можно очень просто проверить) отличны от нуля.

А вот пустое объединение (ещё одна разновидность класса - класс, объявленный с ключевым словом union) не объявляется:

union {}; /* Некорректное объявление объединения. */

При объявлении объединения требуется детальная информация о внутреннем устройстве этого объединения.

Мы продолжаем формальное определение класса. Теперь рассмотрим синтаксис объявления членов класса.

СписокЧленов ::= ОбъявлениеЧленаКласса [СписокЧленов]
             ::= СпецификаторДоступа : [СписокЧленов]

ОбъявлениеЧленаКласса ::= [СписокСпецификаторовОбъявления]
                                      [СписокОписателейЧленовКласса];
                      ::= ОбъявлениеФункции
                      ::= ОпределениеФункции [;]
                      ::= КвалифицированноеИмя;

СписокОписателейЧленовКласса ::= ОписательЧленаКласса
                             ::= СписокОписателейЧленовКласса,
                                                 ОписательЧленаКласса

ОписательЧленаКласса ::= Описатель [ЧистыйСпецификатор]
                     ::= [Идентификатор] : КонстантноеВыражение

ЧистыйСпецификатор ::= = 0

КвалифицированноеИмяКласса ::= ИмяКласса
                          ::= ИмяКласса :: КвалифицированноеИмяКласса

СпецификаторДоступа ::= private
                    ::= protected
                    ::= public

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

В соответствии с синтаксическими правилами, членами класса могут быть как определения функций, так и их прототипы. Действительно:

ОбъявлениеЧленаКласса ::= 
[СписокСпецификаторовОбъявления] [СписокОписателейЧленовКласса]; ::=
СпецификаторОбъявления ОписательЧленаКласса; ::=
СпецификаторТипа Описатель; ::=
void Описатель (СписокОбъявленийПараметров); ::=
void ff (void);

С другой стороны,

ОбъявлениеЧленаКласса ::= 
ОпределениеФункции [;] ::=
Описатель (СписокОбъявленийПараметров) ТелоФункции ::=
ff (void) {int iVal = 100;}

В соответствии с синтаксическими правилами, членами класса могут быть как определения функций, так и их прототипы. Действительно:

ОбъявлениеЧленаКласса ::= 
[СписокСпецификаторовОбъявления] [СписокОписателейЧленовКласса]; ::=
СпецификаторОбъявления ОписательЧленаКласса; ::=
СпецификаторТипа Описатель; ::=
void Описатель (СписокОбъявленийПараметров); ::=
void ff (void);

С другой стороны,

ОбъявлениеЧленаКласса ::= 
ОпределениеФункции [;] ::=
Описатель (СписокОбъявленийПараметров) ТелоФункции ::=
ff (void) {int iVal = 100;}

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

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

Наличие функций-членов делает объявление класса подобным определению (как и любые функции, функции-члены определяются). Как сказано в Справочном руководстве по C++, "Если бы не исторические причины, объявление класса следовало называть определением класса".

Данные-члены класса не могут объявляться со спецификаторами auto, extern, register.

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

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

Описатель члена класса в объявлении класса не может содержать инициализаторов (это всего лишь объявление).

Структура является классом, объявленным с ключевым словом класса struct. Члены такого класса и базовые классы по умолчанию обладают спецификацией доступа public.

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

Объединение является классом, объявленным с ключевым словом класса union. Его члены также по умолчанию обладают спецификацией доступа public. В каждый момент исполнения программы объединение включает единственный член класса. В этом его специфика. Именно поэтому не может быть пустого объединения. Позже мы вернёмся к объединениям.

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

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

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

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

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

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

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

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;
}
/*
Полный разбор операторов в теле функции производится лишь после
полного разбора объявления класса. К этому моменту размер класса
уже будет определён.
*/
}

Назад | Содержание | Вперед