Инициализация объекта: параметры и инициализаторы

Автор: manager Понедельник, Март 24th, 2008 Нет комментариев

Рубрика: C++. Бархатный путь

Совместно используемые функции различаются списками параметров. В этом смысле конструкторы подобны функциям. Рассмотрим определение конструктора с параметрами. Мы расположим его за пределами класса. При этом в классе располагается прототип конструктора, а его имя при определении заменяется квалифицированным именем:
class ComplexType {
:::::
public:
ComplexType(double keyReal,
double keyImag,
char keyCTcharVal,
int keyX);
:::::
};
:::::
ComplexType::ComplexType(double keyReal,
double keyImag,
char keyCTcharVal,
int keyX)
{
cout << "This is ComplexType("
<< keyReal << ","
<< keyImag << ","
<< (int)keyCTcharVal << ","
<< keyX << ")" << endl;
real = keyReal;
imag = keyImag;
CTcharVal = keyCTcharVal;
x = keyX;
};
А вот и подходящее определение. Мы расположим его в функции main:
ComplexType CDw2(100,100,0,0);
/* Создаётся объект типа ComplexType под именем CDw2 с
определёнными значениями. */
int iVal(10);
/* Аналогичным образом может быть определён и проинициализирован
объект основного типа */

Заметим, что к такому же результату (но только окольными путями) приводит и такая форма оператора определения:
ComplexType CDw2 = ComplexType(100,100,0,0);

И снова мы встречаем случай определения объекта посредством постфиксного выражения. Здесь опять можно говорить о явном обращении к конструктору с передачей ему параметров. Выражения явного приведения типа здесь построить невозможно, поскольку за заключённым в скобочки именем типа должно стоять унарное выражение.
Заметим, что не может быть операторов определения переменных с пустым списком инициализаторов:
ComplexType CDw1(); // Это ошибка!
int xVal(); // Это тоже не определение.

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

При объявлении и определении функций C++ позволяет производить инициализацию параметров. Аналогичным образом может быть модифицирован прототип конструктора с параметрами:
ComplexType(double keyReal = 0,
double keyImag = 0,
char keyCTcharVal = 0,
int keyX = 0);

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

Конструктор, управление которому передаётся в результате выполнения оператора определения без параметров, называется конструктором умолчания. К конструкторам умолчания относятся следующие конструкторы:
конструктор, автоматически создаваемый транслятором,

определяемый программистом конструктор с пустым списком параметров,
конструктор с проинициализированными по умолчанию параметрами.

Внесём ещё одно изменение в текст нашей программы. На этот раз мы добавим спецификатор const в объявление данного-члена класса x:
class ComplexType
{
:::::
const int x;
:::::
}

И опять возникают новые проблемы. На этот раз они связаны с попыткой присвоения значения константе. Как известно, объявление данного-члена класса не допускает инициализации, а для того, чтобы константный член класса в процессе создания объекта всё же мог получить требуемое значение, в C++ используется так называемый ctorИнициализатор (именно так называется эта конструкция в справочном руководстве по C++ Б.Строуструппа). Мы не будем гадать, в чём заключается смысл этого названия, а лучше заново воспроизведем несколько форм Бэкуса-Наура.
ОпределениеФункции ::= [СписокСпецификаторовОбъявления]
Описатель
[ctorИнициализатор]
ТелоФункции
ctorИнициализатор ::= : СписокИнициализаторовЧленовКласса
СписокИнициализаторовЧленовКласса ::= ИнициализаторЧленаКласса
[, СписокИнициализаторовЧленовКласса]
ИнициализаторЧленаКласса ::= ПолноеИмяКласса([СписокВыражений])
::= Идентификатор([СписокВыражений])
ПолноеИмяКласса ::= КвалифицированноеИмяКласса
::= :: КвалифицированноеИмяКласса

Для исследования свойств ctorИнициализатора, подвергнем нашу программу очередной модификации. Мы закомментируем все ранее построенные объявления и определения конструкторов и те из операторов определения объектов класса ComplexType, которые содержали значения, определяющие начальные значения данных-членов. И сразу же начинаем определение новых вариантов конструкторов.
ComplexType():x(1)
{
cout << "Здесь ComplexType():x(" << x << ")" << endl;
};
Перед нами конструктор с ctorИнициализатором. Эта конструкция позволяет решать проблемы начальной инициализации константных данных-членов. При работе с данными-членами класса транслятор рассматривает операцию присвоения как изменение начального значения члена. Инициализатор же отвечает непосредственно за установку этого САМОГО ПЕРВОГО значения.

В список инициализаторов разрешено включать все нестатические членам класса (объявленным без спецификатора static), но не более одного раза. Так что следующий вариант конструктора будет восприниматься как ошибочный:
ComplexType():x(1), x(2) // Ошибка.
{
:::::
}
Нетерминальный символ ПолноеИмяКласса определяет синтаксис инициализации нестатических объектов так называемого базового класса (об этом позже). В этом случае список выражений как раз обеспечивает инициализацию членов базового класса.

Добавим в объявление нашего класса объявление массива. Инициализация массива-члена класса при определении объекта не вызывает особых проблем (здесь следует вспомнить раздел, посвящённый массивам-параметрам). Однако в C++ отсутствует возможность инициализации нестатического константного массива-члена класса. Так что можно не стараться выписывать подобные объявления:
const int xx[2]; // Бессмысленное объявление.
всё равно массив xx[2] невозможно проинициализировать. Все варианты инициализации константного нестатического массива будут отвергнуты.
ComplexType():xx(1,2) {/*┘*/};
ComplexType():xx({1,2}) {/*┘*/};
ComplexType():xx[0](1), xx[1](2) {/*┘*/};

Согласно БНФ, в состав инициализатора могут входить только имена или квалифицированные имена. Для обозначения элемента массива этого недостаточно. Как минимум, здесь требуется выражение индексации, которое указывало бы номер элемента массива.

И всё же выход из такой ситуации существует. Можно объявить константный указатель на константу, которому в выражении инициализации можно присвоить имя ранее определённого массива:
:::::
const int DefVal[2] = {1,2};
class ComplexType
{
:::::
const int const * px;
/* Объявили константный указатель на константу. */
:::::
ComplexType():px(DefVal) {/*┘*/};
:::::
};
Окольными путями мы всё же достигаем желаемого результата. Константный указатель на константу контролирует константный массив.

Услугами инициализатора могут пользоваться не только константные члены, а инициализирующие значения можно строить на основе самых разных выражений. Главное, чтобы используемые в этих выражениях имена располагались в соответствующих областях видимости:
ComplexType():px(DefVal),
x(px[0]), // Транслятор уже знает, что такое px.
CTcharVal(32),
real(100),
imag(real/25) // И здесь тоже всё в порядке.
{
// Здесь располагается тело конструктора.
:::::
}
Конструктор копирования

Если определить переменную основного типа и присвоить ей значение, то выражение, состоящее из имени переменной, получит соответствующее значение. Имя означенной переменной можно расположить справа от знака операции присвоения. В результате выполнения этой операции присвоения, леводопустимое выражение окажется равным значению ранее объявленной и проинициализированной нами переменной. Произойдёт копирование значений объектов.
int iVal1;
int iVal2;
iVal1 = 100;
iVal2 = iVal1;
Это нам давно известно. Это тривиально. Менее тривиальным оказывается результат выполнения операции присвоения для объектов-представителей класса.

Вернёмся к старой версии конструктора (её проще повторно воспроизвести, чем описывать словами) и снова модифицируем main процедуру нашей программы. Мы определяем новый объект, используем операцию присвоения и наблюдаем за результатами:
ComplexType()
{
real = 0.0;
imag = 0.0;
CTcharVal = 0;
x = 0;
cout << "Здесь ComplexType() конструктор!" << endl;
}
:::::
void main()
{
ComplexType CDw1;
ComplexType CDw2 = CDw1;
cout << "(" << CDw1.real << ", " << CDw1.imag << "i)" << endl;
cout << (int)CDw1.CTcharVal << ", " << CDw1.x << "┘" << endl;
cout << "(" << CDw2.real << ", " << CDw2.imag << "i)" << endl;
cout << (int)CDw2.CTcharVal << ", " << CDw2.x << "┘" << endl;
}
Наша программа состоит из двух операторов определения, один из которых содержит описатель-инициализатор, и двух пар операторов вывода, которые сообщают о состоянии новорожденных объектов.

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

Действительно, судя по поступающим сообщениям, оба объекта успешно были созданы и существуют с одинаковыми значениями данных-членов. При этом мы имеем дело с разными объектами, которые располагаются по разным адресам. В этом можно убедиться, если добавить оператор вывода в конец функции main:
if (&CDw1 != &CDw2) cout << "OK!" << endl;
/* Сообщить о разных адресах.*/

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

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

Так и есть! Такой конструктор существует и называется конструктором копирования. Вместе с конструктором умолчания, конструктор копирования входит в обязательный набор конструкторов для любого класса. Реализация механизма копирования значений для транслятора не является неразрешимой задачей. Конструктор копирования всего лишь создаёт копии объектов. Этот процесс реализуется при помощи стандартного программного кода. И построить такой код транслятор способен самостоятельно.

Здесь и далее, в примерах нами будет применяться операция присвоения = . В определённом смысле эта операция подобна конструктору. Реализующий эту операцию код автоматически создаётся на этапе трансляции для любого класса. Как и генерация кода стандартных конструкторов, это не самая сложная задача.

Подобно конструктору умолчания, конструктор копирования наряду с уже известной нам формой вызова
ComplexType CDw2 = CDw1;
имеет несколько альтернативных, приводящих к аналогичному конечному результату вызовов:
ComplexType CDw2(CDw1);
ComplexType CDw3 = ComplexType(CDw1);

Обе альтернативные формы вызова напоминают нам уже известные формы вызова конструкторов с параметрами. Чтобы восстановить структуру заголовка конструктора копирования, мы должны лишь определить тип его параметра.
На первый взгляд, здесь всё просто. В качестве значения параметра конструктору передаётся имя объекта, значит можно предположить, что тип параметра конструктора копирования соответствует данному классу. Так, в нашем случае, конструктор копирования класса ComplexType должен был бы иметь параметр типа ComplexType. Однако это не так. И вот почему.
В C++ конструктор копирования является единственным средством создания копий объекта.

С другой стороны, конструктор копирования — это конструктор, который поддерживает стандартный интерфейс вызова функций. Это означает, что параметры при обращении к конструктору, подобно параметрам функции передаются по значению. Если выражение вызова содержит значения параметров, то в ходе его реализации в области активации функции создаётся копия этих значений.

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

Итак, КОНСТРУКТОР КОПИРОВАНИЯ КЛАССА X НЕ МОЖЕТ ИМЕТЬ ПАРАМЕТР ТИПА X. Это аксиома.
На самом деле, в конструкторе копирования класса X в качестве параметра используется ссылка на объект этого класса. Причём эта ссылка объявляется со спецификатором const. И в этом нет ничего странного. Как известно, выражение вызова функции с параметром типа X ничем не отличается от выражения вызова функции, у которой параметром является ссылка на объект типа X. При вызове такой функции не приходится копировать объекты как параметры. Передача адреса не требует копирования объекта, а значит, при этом не будет и рекурсии.
Конструктор копирования — обязательный элемент любого класса. Он также может быть переопределён подобно конструктору умолчания. При этом работа со ссылками в конструкторе копирования не требует явного использования операции разыменования. А спецификатор const (конструктор копирования работает с адресом объекта) предохраняет объект-параметр от случайной модификации в теле конструктора.

Переопределение конструктора копирования

Упомянутая нами в предыдущем разделе аксиома о конструкторе копирования имеет одно интересное следствие.
В классе X в принципе не может быть объявлено конструктора с ЕДИНСТВЕННЫМ параметром типа X. Это происходит из-за того, что выражение вызова такого конструктора просто невозможно будет отличить от выражения вызова конструктора копирования. Не бывает совместно используемых функций с неразличимыми выражениями вызова.

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

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

Мы можем построить собственную версию конструктора копирования. По традиции мы начинаем с ничего не делающего конструктора. Наш новый встроенный конструктор копирования лишь сообщает о собственном присутствии.
ComplexType(const ComplexType& ctVal)
{
cout << "Здесь конструктор копирования" << endl;
} ;
//^ В теле класса ComplexType мы имеем право на эту точку с запятой┘

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

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

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

ComplexType(const ComplexType& ctVal)
{
cout << "Здесь конструктор копирования" << endl;
real = ctVal.real;
imag = ctVal.imag;
CTcharVal = ctVal.CTcharVal;
x = ctVal.x;
}

Но конструктор, создающий подобные копии объектов, скорее всего, окажется непригодным для работы с объектами, содержащими указатели или ссылки. Не самым удачным решением является ситуация, при которой данные-члены типа char*, их нескольких объектов, возможно расположенных в различных сегментах памяти, в результате деятельности конструктора копирования настраиваются на один и тот же символьный массив.
В переопределяемом конструкторе копирования (а в классе он может быть только один) можно реализовывать разнообразные алгоритмы распределения памяти. Здесь всё зависит от программиста.
Указатель this

Продолжаем определение класса ComplexType. Теперь объявим и определим функцию-член PrintVal, которая будет выводить значение чисел-объектов.
Прототип функции разместим в классе:
void PrintVal();
При определении функции используется квалифицированное имя:
void ComplexType::PrintVal()
{
cout << "(" << real << ", " << imag << "i)" << endl;
cout << (int)CTcharVal << ", " << x << "┘" <PrintVal();

сопровождается сообщением о значениях собственных данных-членов. Заметим, что «собственные» данные-члены объектов, как и те функции-члены класса, с которыми мы уже успели познакомиться, считаются нестатическими данными и функциями-членами класса. Существуют также и статические члены класса, к изучению свойств которых мы обратимся в недалёком будущем.

Автоматическое определение принадлежности данных-членов конкретному объекту характерно для любой нестатической функции-члена класса. Объекты являются «хозяевами» нестатических данных и потому каждая нестатическая функция-член класса должна уметь распознавать «хозяйские» данные.
Вряд ли алгоритм распознавания хозяина данных очень сложен. Здесь проблема заключается совсем в другом: этот алгоритм должен быть реализован практически для каждой нестатической функции-члена класса. Он используется везде, где производится обращение к данным-членам объектов, а это означает, что на программиста может быть возложена дополнительная обязанность по кодированию. Несколько обязательных строк для каждой функции-члена? Да никогда┘

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

На первом этапе каждая нестатическая функция-член преобразуется в функцию с уникальным именем и дополнительным параметром — константным указателем на объект класса. Затем преобразуются обращения к нестатическим данным-членам в операторах функции-члена. Они переопределяются с учётом нового параметра. В C++ при подобном преобразовании для обозначения дополнительного параметра-указателя (константного указателя) и постфиксного выражения с операциями обращения для обращения к нестатическим данным-членам используется одно и то же имя this. Вот как могла бы выглядеть функция-член PrintVal после её переопределения:
void ComplexType::ComplexType_PrintVal(ComplexType const *this)
{
cout << "(" <real << "," <imag << "i)" << endl;
cout <CTcharVal) << "," << x << "┘" <PrintVal();
преобразуется к виду
ComplexType_PrintVal(&(*pCD));
что эквивалентно следующему оператору:
ComplexType_PrintVal(pCD);

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

Причина изменения имени для функций-членов класса очевидна. В разных классах могут быть объявлены одноименные функции-члены. В этих условиях обращение к функции-члену класса непосредственно по имени может вызвать конфликт имён: в одной области действия имени одним и тем же именем будут обозначаться различные объекты — одноименные функции-члены разных классов. Стандартное преобразование имён позволяет решить эту проблему.
Указатель this можно использовать в теле функции-члена без его дополнительного объявления. В частности, операторы функции ComplexType::PrintVal() могут быть переписаны с использованием указателя this:
void ComplexType::PrintVal()
{
cout << "(" <real << "," <imag << "i)" << endl;
cout <CTcharVal) << "," << x << "┘" <ВЫРАЖЕНИЕ
(*this).ВЫРАЖЕНИЕ

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

Следует помнить о том, что this указатель является константным указателем. Это означает, что непосредственное изменение его значение (перенастройка указателя, например, this++) недопустимо. Указатель this с самого начала настраивается на определённый объект.

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

В объявлении нестатической функции-члена this указателю можно задавать дополнительные свойства. В частности, возможно объявление константного this указателя на константу. Синтаксис языка C++ позволяет сделать это. Среди БНФ, посвящённых синтаксису описателей, есть и такая форма:
Описатель ::=
Описатель (СписокОбъявленийПараметров) [СписокCVОписателей]
::= *****
CVОписатель ::= const
::= *****
Так что небольшая модификация функции-члена PrintVal, связанная с добавлением cvОписателя const:
void PrintVal() const;
в прототипе и
void ComplexType::PrintVal() const
{
:::::
}

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

this->CTcharVal = 125;
real = imag*25;
imag++;

cvОписатель const в заголовке функции не допускает непосредственной модификации значений принадлежащих объекту данных.

Заметим также, что this указатель включается также в виде дополнительного параметра в список параметров конструктора. И в этом нет ничего удивительного, поскольку его значением является всего лишь область памяти, занимаемая объектом.

Оставить комментарий

Чтобы оставлять комментарии Вы должны быть авторизованы.

Похожие посты