C++.Бархатный путь

       

Конструкторы. Основные свойства


Сначала несколько форм Бэкуса-Наура.

Объявление ::= ОбъявлениеФункции

::= ОпределениеФункции

::= *****

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

[СпецификацияИсключения];

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

[ctorИнициализатор] [СпецификацияИсключения] ТелоФункции

Описатель ::= Описатель ([СписокОбъявленийПараметров]) ::= dИмя

dИмя ::= ИмяКласса

Используя это множество БНФ, можно строить объявления весьма странного вида:

ОбъявлениеФункции ::= Описатель; ::= Описатель (); ::= dИмя (); ::= ComplexType ();

Объявление… без спецификатора объявления.



ОпределениеФункции ::= Описатель ТелоФункции ::= Описатель () {} ::= dИмя () {} ::= ComplexType () {}

А это определение. Оно построено в соответствии с правилами построения функций. Не важно, что у него в теле нет ни одного оператора! Важно, что у него нет спецификатора объявления.

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

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

Отсутствие спецификаторов объявления означает, что конструктор не имеет абсолютно никакого отношения к вызову и возвращению значений. Конструктор не является функцией.

Так что объявления функций-членов класса ComplexType

void ComplexType(); ComplexType ComplexType();


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

А вот построенное нами объявление действительно является объявлением конструктора: ComplexType();

И наше определение действительно является определением конструктора: ComplexType(){}

Это ничего, что конструктор такой простой, зато он от начала и до конца правильный!

Как известно, в классе может быть не объявлено ни одного конструктора. В таком случае транслятор без участия программиста самостоятельно строит стандартный конструктор. Не существует классов без конструкторов, хотя классы с автоматически создаваемыми конструкторами, как ни странно, называются классами без конструкторов.

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

ComplexType(double rePar, double imPar); /* Объявление… */ ComplexType(double rePar, double imPar){/*…*/} /*Определение…*/

И ещё один вариант конструктора для класса ComplexType - на этот раз с одним параметром (его помощью, например, можно задавать значение мнимой части):

ComplexType(double imPar); /* Объявление… */ ComplexType(double imPar){/*…*/} /*Определение…*/

Здесь мы сознательно опять оставили пустыми тела конструкторов. Необходимо сначала выяснить, какие операторы могут, а какие не могут располагаться в конструкторе.

Отсутствие спецификации возвращаемого значения не означает запрета на использование оператора return в теле конструктора. В конце концов, это всего лишь оператор перехода. Но использование этого оператора в сочетании с выражением, задающим возвращаемое значение, например,

return NULL; либо return MyVal; либо return 125;



и т.д., недопустимо. Возвращаемое значение специфицируется по типу, а как раз про тип возвращаемого конструктором значения в объявлении конструктора ничего и не сказано. Поэтому то, что обычно называется выражением явного вызова конструктора, вызовом, по сути, не является.

Часто вообще невозможно сказать что-либо определённое по поводу того, что обеспечивает передачу управления конструктору - так называемое выражение вызова (или обращения к конструктору), либо выражение, которое используется для преобразования типа (постфиксный вариант выражения преобразования типа). Соответствующая БНФ уже приводилась ранее. Напомним её:

ПосфиксноеВыражение ::= ИмяПростогоТипа ([СписокВыражений])

ИмяПростогоТипа и имя конструктора совпадают. Поэтому имя простого типа можно рассматривать как имя конструктора. При вычислении значения выражения приведения для производных типов управление действительно передаётся одноименному конструктору. Без участия конструктора невозможно определить значение соответствующего выражения:

(ComplexType) 25; /* В этом случае мы имеем дело с выражением преобразования. При вычислении его значения производится обращение к конструктору ComplexType(double). */ (float) 25; /* Здесь нет никаких обращений к конструктору. Базовый тип float классом не является и конструкторов не имеет. Перед нами оператор, состоящий из выражения приведения (целочисленное значение приводится к типу float). */ float x = float(25); /* В этом случае для определения значения выражения явного преобразования типа, записанного в функциональной форме, также не требуется никаких обращений к конструктору. */ ComplexType (25); /* Казалось бы, здесь мы также имеем дело с функциональной формой выражения явного преобразования типа - оператором на основе постфиксного выражения. Для вычисления значения этого выражения необходимо обратиться к конструктору ComplexType(double). */

На последнее предложение следует обратить особое внимание. Дело в том, что аналогичный оператор на основе постфиксного выражения для основных типов языка C++ воспринимается транслятором как ошибка:



float (25); /* Это некорректный оператор! Для любого из основных типов C++ здесь будет зафиксирована ошибка. */

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

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

Таким образом, оператор float (25);

(и ему подобные операторы для основных типов) представляется транслятору объявлением с пропущенным описателем и альтернативной формой инициализатора. Чем-то, напоминающим следующую конструкцию: float = 25;

при разборе подобного предложения транслятор, естественно, не находит ожидаемого описателя и сообщает об ошибке в объявлении.

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

В C++ можно построить условное выражение на основе выражения явного преобразования к одному из основных типов. Основные типы имеют простую структуру, а потому значение такого выражения определить очень просто:



if (char(charVal)) {/*…*/} if (float(5)) {/*…*/} if ((int)3.14){/*…*/} if (double (0)){/*…*/}

Включение в условия условных операторов выражений, вычисление значений которых приводит к передаче управления конструкторам, требует дополнительных усилий со стороны прораммиста. У порождаемых конструкторами объектов сложная структура и неизвестные транслятору способы определения значений, представляемых такими объектами. Кроме того, определённый в языке набор операций приспособен исключительно для работы со значениями основных типов. Транслятор не имеет абсолютно никакого представления о том, каким образом следует, например, сравнивать значения того же самого ComplexType.

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

if (ComplexType()){/*…*/} if (ComplexType() 10 ComplexType() = 25 ){/*…*/}

Правда, в данном контексте за символами операций сравнения и даже за выражением "явного вызова конструктора" скрываются так называемые сокращённые формы вызова операторных функций, а не обычные операции C++.

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

В C++ невозможно сформулировать условие относительно сложного объекта "в целом", используя при этом стандартный набор операций, но легко можно определить значения данных-членов этого объекта. Для этого используется операция выбора компонента: if (ComplexType().real !ComplexType().imag){/*…*/}

Вот мы и узнали кое-что о свойствах объекта. Правда, объектов в условии целых два. У первого безымянного объекта мы поинтересовались значением данного-члена real, после чего он благополучно отошёл "в мир иной", у второго объекта выяснили значение данного-члена imag.

Выражения вызова функций типа void так же недопустимы в контексте условия, поскольку функции void "возвращают" пустые значения. Например,



void MyProc(); ::::: void MyProc() {/*…*/} ::::: if (MyProc()) {/*…*/} /* Здесь ошибка */ for ( ; MyProc(); ) {/*…*/} /* Здесь ошибка */ if (ComplexType()){/*…*/} /* Это тоже ошибка */

Выражение явного преобразования типа можно расположить справа от символа операции присвоения в операторе присвоения.

ComplexType MyVal = ComplexType (); ComplexType MyVal = ComplexType (25); ComplexType MyVal = (ComplexType) 25;

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

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

int MyVal = MyProc(); /* Ошибка */ int MyVal = (void)MyProc(); /* Ошибка */

И ещё одно сравнение между конструктором и void-процедурой. Поскольку тип void - это всё же тип, мы можем объявить указатель на void-процедуру.

void MyFunction (void); ::::: void (*MyFunctionPointer) (void);

Указатель на функцию можно настроить на адрес конкретной функции. Для этого существует операция взятия адреса:

MyFunctionPointer = MyFunction; /* Можно так. */ MyFunctionPointer = MyFunction; /* А можно и так. */

С конструктором всё по-другому. Мы можем определить адрес создаваемого конструктором объекта. Всё то же выражение явного преобразования типа обеспечивает обращение к конструктору, который создаёт в памяти безымянный объект, чей адрес и определяется операцией взятия адреса: if (ComplexType()) {/*…*/}

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


Содержание раздела