лекции (2003) (Глазкова) (1160821), страница 24
Текст из файла (страница 24)
Иногда конструкторы могут иметь пустое тело: Х() {}. Это значит, что у конструктора пользовательская часть пустая, но у него есть системная часть. В любом конструкторе системная часть состоит в следующем: вначале вызывается конструктор базового класса (при этом, если не указано, какой именно конструктор, вызывается конструктор умолчания), затем вызываются конструкторы подобъектов и только после этого вызывается пользовательская часть (которая может исходить из того, что базовый класс и все подобъекты проинициализированы).
Возникает вопрос: любой ли класс обладает конструктором умолчания? Нет.
Другой вопрос: любой ли класс обладает хотя бы одним конструктором? Да.
Пусть класс Х не обладает конструктором умолчания, либо класс обладает конструктором умолчания, но мы хотим вызывать какой-то другой конструктор. Есть явный конструктор умолчания, который пишется самим программистом – это функция-член (Х( ) {...}).
В языке С++ некоторые конструкторы могут генерироваться автоматически. Генерация имеет смысл только потому, что у специальных функций есть своя системная семантика.
Конструктор умолчания генерируется тогда и только тогда, когда отсутствуют другие явно заданные конструкторы. Если в классе есть конструктор (не конструктор умолчания), то конструктор умолчания генерироваться не будет. Возникает вопрос: имеет ли смысл делать классы без конструктора умолчания? Да.
Пример:
class Vector{
int size;// размер
int *body;// тело вектора
pubic:
// конструктор
Vector(int sz)// по умолчанию размер задавать бесcмысленно
{if (sz>=0) body = new int [sz=size];
else// реакция на ошибку – возбуждение исключения
error(); // глобальная функция, которая вызывает исключительную ситуацию.
}
//Здесь есть захват ресурсов – динамической памяти, следовательно, надо их //освобождать
//деструктор
~Vector() {delete body;}
// т.о., все проблемы с инициализацией и разрушением объекта решены (с точки зрения //ресурсов)
//для безопасной индексации перекрываем операцию индексирования
int& operator[](int index)
{ if (index < 0 || index > = sz)
return body [index];
}
...
}
VectorX(20);
цикл
for(int t=0;i<20;i++){...X[i];..}
В некоторых случаях контроль индексов из соображений эффективности не нужен.
Тогда можно написать
int& elem (int index)
{return body [index];}
Это не совсем безопасно, но эффективно, если мы знаем, что проверку делать не надо.
Синтаксис и семантика конструктора в С++ расширены.
Общий вид конструктора следующий:
имя_класса:: имя_класса (аргументы):[инициализация]
{…тело…}
Инициализация имеет вид:
имя_класса (аргументы) либо имя_подобъекта (аргументы).
Пример
сlass Y: publicX{
Х с;
...
}
Если у класса Х отсутствует конструктор умолчания или мы хотим вызывать параметрический конструктор, то в конструкторе класса Y нужно явно вызвать,
например, Х(1), т.е. Y():Х(1);
Такой синтаксис допустим и для переменных-подобъектов: int i;...
i(0);//допустимо.
Есть еще один случай, когда соответствующее поле – член может инициализироваться только в списке инициализаторов. Это случай, если речь идет о ссылке.
Пример:
class X{
int& i;
X(int& j):i(j){i=j;}
...
}
Если в классе отсутствует конструктор умолчания, и мы забываем вызвать конструктор явно в конструкторе другого класса, то компилятор выдаёт сообщение об ошибке.
Пример:
class Y{
...
Vector v;
…
}
Если в конструкторе класса Y явно не указать Y():Vector(50), то компилятор выдаст сообщение об ошибке.
В каких контекстах в С++ происходит размещение объекта?
-
объявление переменной
Х х;
х – ОД (статический, квазистатический, динамический, базовый объект, подобъект)
-
При передаче параметров
void f(X x)
{...}
Объект класса Х передаётся по значению. Есть запись активации, в которой отводятся места для формальных параметров. Перед началом выполнения функции происходит копирование фактических параметров в формальные. Какой конструктор здесь должен работать? Конструктор копирования – конструктор, который должен опираться на значение другого объекта этого же класса.
Конструктор копирования
Конструктор копирования – это конструктор, у которого параметр – ссылка на сам объект: Х(Х&). Конструктор копирования есть у любого класса. Если конструктор копирования не задан явно, он генерируется.
Побитовая семантика копирования: для простых ТД – побитовое копирование; если класс состоит только из объектов простых типов, то сгенерированный конструктор побитового копирования просто побитово копирует каждый из его членов.
Более сложная ситуация, если в классе есть – подобъект какого-то другого класса (например, Х х). Для всех объектов простых типов применяется семантика побитового копирования, а для класса Х рекурсивно смотрится, какой у него конструктор копирования, и применяется конструктор копирования этого объекта.
В общем случае этот процесс рекурсивный: конструктор копирования обращается к другим конструкторам копирования и т.д. Проблема копирования состоит в том, что в зависимости от структуры данных есть два вида копирования:
-
поверхностное (shallow)
-
глубинное (deep)
Поверхностное копирование – это побитовое копирование в языке С++, когда просто элементы структуры данных копируются как поля битов. В то же время, не для всех структур данных поверхностное копирование проходит. Поверхностное копирование ссылки – две ссылки на один и тот же объект. Если стек задан в виде массива, то побитовое копирование для него вполне удовлетворительно. Если же стек задан в виде линейного списка (память под стек отводится динамически), то поверхностное копирование здесь не работает. Почему?
У нас копируется поле tор, копируется указатель, но самое тело не копируется. Поэтому для нашего класса Vector обычная семантика копирования не работает. Почему?
Напишем функцию
void f (Vector v);
Далее
Vector X(20);
f(X);
Что произойдёт? Ситуация, которая называется висячей ссылкой.
Т.к. в классе Vector нет явного конструктора копирования, генерируется побитовый конструктор копирования. При выходе из f формальный параметр v перестаёт существовать, следовательно, вызывается деструктор.
Как исправить эту ситуацию? Вектор – это такая структура данных, для которой стандартная семантика побитового копирования не проходит, потому что она содержит в себе ссылки на другие структуры данных. Здесь нам необходимо глубинное копирование – вместо ссылок копируются сами объекты.
Если в процессе инициализации мы отводим динамическую память под члены, то стандартная семантика копирования не проходит. Следовательно, наряду с деструктором, программист должен написать соответствующий конструктор копирования.
Пример конструктора копирования для класса Vector
Vector (const Vector& v)
{
body = new int [size=v.size]
memcpy (body, v.body, size*sizeof(int));
}
Пример
Vector X(10);
Vector v=x;//вызов конструктора копирования для вектора v, т.к. здесь определяется новый объект v.
...
Vector c(25);
v=c;//это операция присваивания, а не конструктор копирования
Чем отличается операция присваивания от конструктора копирования?
Конструктор исходит из того, что слева у нас есть кусок памяти, поэтому что там до этого было нас не волнует. Операция присваивания должна позаботиться ещё и о том, чтобы освободить память.
Поэтому в языке С++ есть правило: поскольку конструктор копирования и операция присваивания имеют дело с семантикой копирования, то, если нас не устраивает стандартная семантика копирования, мы должны сами явно определить конструктор копирования и операцию присваивания (старую память под v надо уничтожить и после этого только произвести копирование). Поэтому просто побитовая семантика нас не устраивает по двум причинам:
-
она некорректно копирует ссылки;
-
она при присваивании не уничтожает старую память.
Правило языка С++ гласит: если вы явно переопределили конструктор копирования, вы должны явно переопределить операцию присваивания.
Vector& operator=(const Vector& v)
{
delete body;
body=new int[size=v.size];
memcpy (body, v.body, size *sizeof(int);
return *this;
}
Такое не определение может привести к неприятным ошибкам. В каком случае? v=v;
Вспомним алгоритм нахождения максимума. Вполне допустимо сначала
max=a[0];
а потом начать цикл не с 1, а с 0, т.е. max=a[0];
Т.е. ситуация когда лева и справа ссылка на одно и то же значение вполне могут встречаться. Именно в этом, в частности, причина, что во многих ЯП отсутствует механизм переопределения стандартных знаков операций. Поэтому корректная операция присваивается сначала должна проверять, идёт ли речь об одном и том же объекте. И если это так, то ничего не делать.
Тем не менее возможность явного управления проблемой копирования в языке С++ есть, причём она решается не на уровне пользователя структуры данных, а на уровне её разработчика.
Лекция 18
Мы рассматриваем определение новых ТД с помощью понятия класса.
Именно классовые ЯП оказались наиболее адекватными с точки зрения гибкости для определения новых ТД. И не последнюю роль в этой гибкости играет понятие специальных функций-членов. С одной стороны, специальные функции-члены – это обычные функции-члены, к которым применимы те же самые правила, что и к обычным функциям (например, правило видимости, правило именования и т.д.). Специальность этих функций состоит в том, что компилятор обладает дополнительной информацией о семантике этих функций. Именно за счет этой специализации и достигается гибкость. Мы обсуждаем, что существуют некоторые технологические проблемы, в частности, проблема инициализации и связанная с ней – разрушения. Проблема состоит в том, что инициализировать и разрушать (т.е. освобождать) ресурсы необходимо для любых мало-мальски сложных структур данных. Проблема в том, что программисту – пользователю структуры надо не забыть вовремя инициализировать и вовремя разрушить. А если он забыл, язык должен либо подсказать ему, либо обеспечить механизм вызова соответствующих функций. Именно по этому пути пошёл создатель С++ Страуструп – соответствующие функции вызываются автоматически. Специальные функции, которые решают проблему инициализации и разрушения – это конструкторы и деструкторы. Понятие этих специальных функций наиболее развиты в языке С++.
Конструктор – функция, которая автоматически вызывается при создании объекта.
Деструктор – это функция, которая автоматически вызывается при разрушении объекта. В С++ выделяются особые виды конструкторов. Откуда в ЯП появляется классификация конструкторов? Компилятор не явно вставляет вызов конструктора/деструктора. В зависимости от правил неявной вставки этих функций конструкторы и делятся на несколько категорий. Заметим, что у деструктора таких категорий нет. У деструктора есть всего две категории: деструктор умолчания и все остальные.
Вспомним, что роль конструктора умолчания следующая: когда программист явно не указывает, какой конструктор вызвать, должен вызываться конструктор умолчания. Мы знаем, что у любого конструктора кроме пользовательской семантики существует стандартная (системная) семантика. Она состоит в том, что вначале вызываются конструкторы базовых классов, а после этого конструкторы подобъектов. Аналогично вызывается деструктор умолчания – когда мы явно не указываем, какой деструктор объекта вызвать, компилятор генерирует вызов деструктора умолчания (~X();).
Как и у конструктора, у деструктора есть системная семантика; вначале прорабатывает тело деструктора, после этого прорабатывают деструкторы подобъектов, а после этого деструкторы базовых классов. Т.е. их работа организована по стековому принципу: что было последним создано, первым разрушается.
То, что классификация деструкторов тривиальна, говорит о том, что концепция конструкторов более мощная. В прошлый раз мы закончили на обсуждении конструктора копирования. Мы отметили, что по умолчанию семантика копирования побитовая.
Отметим ещё раз, что операция Х а=b; - не операция присваивания. Это вызов конструктора копирования для класса Х, у которого в качестве параметра подставляется объект b (для инициализации объекта а).
Если конструктор копирования не описан явно (т.е. если нас устраивает стандартная семантика), то он генерируется. Копирование подразделяется на поверхностное и глубинное. В случае, если нас не устраивает стандартная побитовая семантика копирования (например, если структура содержит ссылки на подструктуры), нам необходимо глубинное копирование. Автоматически сгенерировать глубинное копирование невозможно, потому что, не зная семантики структур данных и операции, нельзя определить, нужно ли полностью копировать этот объект. Если необходимо глубинное копирование надо переопределить как конструктор копирования, так и операцию присваивания X& operator=(X& );
Т.о., конструктор копирования решает проблему, связанную с копированием: в языках, где есть побитовое копирование, умолчательной семантики копирования не хватает. Заметим, что если в языках отсутствует операция побитового копирования, то такой проблемы и не существует. Заметим, что во всех рассмотренных ОО ЯП, за исключением Oberon и С++, семантика всех классов ссылочная, и поэтому проблем с копированием там не существует.
Конструктор преобразования
Третий особый вид конструктора – это конструктор преобразования. Конструктор преобразования решает проблему управления пользовательскими преобразованиями. Заметим, что понятие конструктора преобразования есть только в тех языках, где есть операции неявного преобразования. Практически во всех ЯП, если есть ТД Т1 можно написать функцию Т2 Тo_Т2 (Т1 х); Это есть операция преобразования: из объекта типа Т1 мы получаем объект типа Т2. Это функция явного преобразования: вызываем её так – b = Тo_Т2(а); (а – типа Т1; b – типа Т2). Но в некоторых ЯП разрешено понятие неявного преобразования.