Лекция 11.
Специальные функции-члены класса
Вопросы
1. Конструкторы
2. Деструкторы
3. Преобразования
4. Пример. Разработка
класса для работы со стеком
Некоторые функции-члены класса являются
специальными в том смысле, что они влияют на создание, копирование и
уничтожение объектов класса или задают способ приведения значений к значениям
других типов. Часто такие специальные функции вызываются неявно.
1. Конструкторы
Функция-член класса с тем же именем, что и у класса,
называется конструктором. Она используется для построения объектов этого класса. Конструктор не должен возвращать
никакого значения, даже void
.
Если
класс имеет конструктор, все объекты этого класса будут проинициализированы.
Если конструктору требуются параметры, они должны быть предоставлены.
Когда
для класса объявлен конструктор, нельзя пользоваться списком инициализации в
качестве инициализатора.
1.1.
Конструктор умолчания
Конструктор
умолчания класса Х – это конструктор класса Х, вызываемый без параметров. Конструктор умолчания
обычно имеет вид Х::Х(), однако
и конструктор, который может вызываться без параметров, потому что имеет
параметры с умолчанием, например, Х::Х(int = 0), также считается конструктором умолчания. При
отсутствии других объявленных конструкторов, конструктор умолчания генерируется
компилятором.
1.2.
Конструктор копирования
Конструктор
копирования для класса Х – это конструктор, который может быть вызван
для копирования объекта класса Х,
т.е. такой конструктор, который может быть вызван с одним параметром – ссылкой
на объект класса Х. Например, X::X(const
X&) и X::X(X&,
int = 0) являются конструкторами
копирования.
При
отсутствии объявленных конструкторов копирования компилятор генерирует публичный конструктор
копирования. Генерируемый конструктор копирования выполняет побитовое
копирование объекта. Этот метод годится лишь при отсутствии в объекте указателей, которые
хранят адреса динамически распределенной памяти. Сгенерированный конструктор
скопирует адрес, а не содержимое памяти, таким образом, два разных объекта
будут ссылаться на один и тот участок памяти и меняться синхронно, что не
является ожидаемым поведением. В таком случае программист обязательно должен
сам написать конструктор копирования, который будет, в частности, копировать
содержимое динамически распределённой памяти.
Однако
в тех случаях, когда копирующий конструктор по умолчанию имеет правильный
смысл, лучше полагаться на это умолчание. Это короче, а читающие код должны
понимать умолчания. Кроме того, компилятор знает об этом умолчании и о
возможностях его оптимизации. А написание почленного копирования классов с
большим количеством членов данных вручную – занятие утомительное, и при этом
можно наделать массу ошибок.
Конструктор
копирования – и определяемый пользователем, и генерируемый компилятором –
используется:
Семантика
этих операций по определению совпадает с семантикой инициализации.
От
вызовов конструктора копирования легко избавиться. С тем же успехом можно
записать следующее.
Пример
конструктора копирования см. в примере в конце лекции.
2. Деструкторы
Функция-член
класса Х с именем ~Х называется деструктором. Она используется для
разрушения значения класса Х непосредственно перед разрушением содержащего его
объекта. Деструктор не имеет параметров и возвращаемого типа, нельзя задавать
даже void.
Деструкторы
автоматически вызываются, когда
Деструктор
может также вызываться явным образом.
3. Преобразования
Преобразования
(изменения типа) объектов класса
выполняются конструкторами и преобразующими функциями.
Такие
преобразования, называемые пользовательскими, часто неявно
применяются в дополнение к стандартным преобразованиям. Например, функция,
ожидающая параметр типа Х, может вызываться не только с параметром
типа Х, но и с параметром типа Т, если существует
преобразование из Т в Х. Кроме того,
пользовательские преобразования применяются для приведения инициализаторов,
параметров функций, возвращаемых функциями значений, операндов в выражениях,
управляющих выражений, операторах цикла и выбора и
для явного приведения
типов.
Пользовательские
преобразования применяются только там, где они однозначны.
3.1.
Преобразование посредством конструктора
Конструктор
с одним параметром задаёт преобразование типа своего параметра к типу своего
класса.
Конструктор
с одним параметром не обязательно вызывается явно.
Однако
неявное преобразование может быть нежелательно в некоторых случаях.
Неявное
преобразование можно подавить, объявив конструктор с модификатором explicit.
Такой конструктор будет вызваться только явно.
3.2.
Преобразующие функции
Функция-член
класса Х, имя которой имеет вид operator <имя типа>,
определяет преобразование из Х в тип, заданный именем
типа. Такие функции называются преобразующими функциями или функциями
приведения.
Для
такой функции не могут быть заданы ни параметры, ни возвращаемый тип.
3.3.
Разрешение неоднозначности
Присваивание
значения типа V объекту класса X допустимо в
том случае, если имеется оператор присваивания X::operator= (Z) такой,
что V является Z или существует единственное
преобразование V в Z. Инициализация
рассматривается аналогично.
В
некоторых случаях значение требуемого типа может быть создано при помощи
повторного использования конструкторов или операторов преобразования. Это
должно осуществляться при помощи явных преобразований – допустим только один
уровень неявных преобразований, определяемых пользователем. В некоторых случаях
значение требуемого типа может быть создано более чем одним способом – такое
недопустимо.
Правила
преобразования не являются ни самыми простыми для реализации из всех возможных,
ни самыми легкими для документирования, ни настолько общими, как можно себе
представить. Однако они довольно безопасны, и их применение не приводит к
неприятным сюрпризам. Гораздо легче вручную разрешить неоднозначности, чем
найти ошибку, вызванную преобразованием, о котором и не подозревали.
4. Пример. Разработка класса для работы со стеком
Первый
вариант
Разработку
класса необходимо начинать с интерфейса, т.е. с открытых
(публичных) функций, которые будут использоваться остальной частью программы
для взаимодействия с разрабатываемым классом. Для стека определены две
операции: взятие элемента из стека и добавление элемента в стек. Также
определим конструктор (умолчания) и деструктор (обычно их объявляют для всех
классов), а также вспомогательные функции, проверяющие пуст ли стек и была ли
ошибка при работе со стеком. Таким образом, интерфейс класса следующий:
Теперь
можно разрабатывать структуру данных для класса. Для хранения элементов стека
будем использовать массив. Также нам понадобятся переменная, указывающая на
текущий элемент стека, и переменная, хранящая признак ошибки.
Кроме
этого, необходимо, естественно, определить функции, входящие в класс. После
этого можно использовать разработанный класс.
Второй
вариант
После
некоторого размышления разработчик пришел к выводу, что использование
статического массива для хранения элементов стека ограничивает разработанный
класс, и решил использовать динамически распределяемый массив, размер которого
можно будет при необходимости увеличить. Таким образом, вместо массива мы
объявляем указатель и ещё одну переменную, которая будет хранить размер
динамически распределённого массива.
В
результате этих изменений разработчику пришлось также изменить конструктор, в
который добавлены операторы для динамического распределения памяти, деструктор,
в который добавлены операторы освобождения памяти, и функцию добавления
элемента в стек, которая при необходимости перераспределяет выделенную память.
Также был добавлен конструктор копирования. Остальные функции остались без
изменений.
Обратите
внимание, – и это самое главное, – что основная программа осталась без
изменений. Поскольку разработчик поступил правильно и начал с разработки
интерфейса, изменения в самом классе не затронули остальную программу. Конечно,
для стека всё просто, т.к. набор необходимых функций мал и хорошо известен. Но
разработку класса всегда надо начинать с разработки интерфейса. Чем лучше будет
продуман интерфейс используемых в программе классов, чем меньше будет проблем
при разработке программы.
Лекция 12.htm
Перейти к
лекции 12