Глава 4: Сообщения, экземпляры и инициализация
В главе 3 мы вкратце охарактеризовали объектно - ориентированное программирование в статике . А именно мы описали , как создаются новые типы , классы и методы . В этой главе мы продолжим исследование механизмов ООП , изучив динамические свойства . Они определяют то , как создаются новые переменные , как они инициализируются и взаимодействуют друг с другом путем пересылки сообщений .
В первом разделе мы рассмотрим обмен сообщениями . Затем мы исследуем создание и инициализацию новых переменных . Под созданием мы будем понимать выделение памяти под новый объект и связывание этого ее участка с именем переменной . Под
инициализацией мы подразумеваем не только установку начальных значений полей
данных объекта , что аналогично инициализации записей , но также и процесс более общего характера , который устанавливает объект в некое исходное начальное состояние . Степень скрытности , с которой это происходит в большинстве объектно - ориентированных языков , является важным аспектом инкапсуляции , которую мы считаем одним из главнейших достоинств ООП .
PDF created with pdfFactory Pro trial version www.pdffactory.com
4.1. Синтаксис пересылки сообщений
Мы используем термин пересылка сообщений ( иногда говорят о поиске методов ) для
обозначения динамического процесса обращения к объекту с требованием выполнить определенное действие . В главе 1 мы неформально описали пересылку сообщений и отметили , чем она отличается от обычного вызова процедуры . В частности :
∙ Сообщение всегда обращено к некоторому объекту , называемому полу - чателем или адресатом .
∙ Действие , выполняемое в ответ на сообщение , не является фиксированным и может варьироваться в зависимости от класса получателя . Различные объекты , принимая одно и то же сообщение , выполняют различные действия .
В процессе пересылки сообщений имеются три четко выделяемые компоненты : получатель ( объект , которому посылается сообщение ), селектор сообщения ( текст , идентифицирующий конкретное сообщение ) и список аргументов , которые используются
при реакции на сообщение .
Хотя эти понятия являются центральными для объектно - ориентированного программирования , но терминология и синтаксис , используемые в различных языках , варьируются в широких пределах . В последующих разделах мы опишем свойства , специфичны для каждого из рассматриваемых языков программирования , равно как и термины , используемые различными сообществами программистов .
4.1.1. Синтаксис пересылки сообщений в Object Pascal
В отличие от остального ООП - сообщества программисты на языке Delphi Pascal используют термин сообщение для специфической команды управления окном . Более традиционное его истолкование известно среди них как поиск метода .
Поиск метода в языке Object Pascal — это просто запрос , посылаемый объекту , чтобы вызвать один из его методов . Как мы заметили в главе 3, метод описывается при определении объекта так же , как поле данных в записи . Аналогично , стандартная синтаксическая конструкция с использованием точки , которая применяется для описания поля данных , расширена и обозначает также вызов метода . Селектор сообщения — то есть текст , следующий за точкой , — должен соответствовать одному из методов , определенных для класса или наследуемых от родительского класса ( мы изучаем наследование в главе 7). Тем самым если идентификатор aCard описан как объект класса Card, то следующая команда приказывает игральной карте нарисовать себя в указанном окне в нужной точке .
aCard.draw (win, 25, 37);
Объект слева от точки называют получателем сообщения . Заметим , что за исключением указания получателя , синтаксис сообщения идентичен вызову традиционной функции или процедуры . Если аргументы не заданы , то список в круглых скобках опускается . Например , следующая команда указывает игральной карте перевернуться :
PDF created with pdfFactory Pro trial version www.pdffactory.com
Если заданный метод объявлен как процедура , выражение также должно использоваться в качестве процедуры . Аналогично , если метод объявлен как функция , он и должен использоваться в этом качестве .
Компилятор проверяет корректность пересылки сообщений . Попытка переслать сообщение , которое не указано в описании класса объекта - получателя , приводит к ошибке . Внутри метода псевдопеременная self используется для ссылки на получателя сообщения . Эта переменная не описывается , но может быть использована как аргумент или как получатель других сообщений .
Например , метод color, который возвращает цвет игральной карты , может быть записан следующим образом :
function Card.color : colors; var ss : suits;
if (ss = Heart) or (ss = Diamond) then color:=Red
Здесь метод suit вызывается с целью получить значение масти . Это считается более удачной стратегией программирования , чем прямой доступ к полю данных suitValue. Delphi Pascal также позволяет возвращать результат функции , присваивая его специальной переменной Result, а не идентификатору функции (color в нашем случае ).
4.1.2. Синтаксис пересылки сообщений в C++
Как мы отметили в главе 3, несмотря на то что концепции методов и сообщений применимы к языку С ++, собственно методы и сообщения ( как термины ) редко используются в текстах по C++. Метод принято называть функцией — членом класса (member function); о пересылке сообщений говорят как о вызове функции - члена .
Описание класса напоминает определение структуры . Синтаксис вызова функции - члена аналогичен доступу к полям данным ( переменным экземпляра ). Синтаксическая форма состоит из получателя , за которым следует точка , селектора сообщения ( который должен соответствовать имени функции - члена для класса получателя ) и списка аргументов ( в круглых скобках ).
Если theCard описан как экземпляр класса Card, то следующий оператор
приказывает игральной карте отобразить себя в заданном окне в точке с координатами 25 и 37:
theCard.draw(win, 25, 37);
Даже если аргументы не требуются , круглые скобки все равно нужны , чтобы отличить вызов функции - члена от обращения к полям данных . Нижеследующий код определяет , лежит ли карта лицевой стороной вверх :
PDF created with pdfFactory Pro trial version www.pdffactory.com
Как и в языке Object Pascal, компилятор проверяет правильность пересылки сообщения , обращаясь к описанию класса получателя . Попытка передать сообщение , которое не является частью класса , отмечается как ошибка при компиляции .
Функция - член класса , которая описана с типом void, может использоваться как оператор ( то есть как вызов процедуры ). Функции - члены , которые имеют возвращаемое значение других типов , вызываются в соответствии со стилем языка C и как операторы ( процедуры ), и как функции ( подобно обращениям к традиционным функциям ).
С каждым методом ассоциирована псевдопеременная , в которой указан получатель сообщения . В языке C++ она называется this и является указателем на получателя , а не собственно получателем . Поэтому используется разыменование указателя ( операция ->) для пересылки последующих сообщений к тому же получателю . Например , метод color, который используется для определения цвета карты , может быть записан следующим образом ( если мы хотим избежать прямого доступа к внутреннему полю , содержащему значение масти ):
Переменная this также часто используется , если внутри метода надо передать получателя сообщения в качестве аргумента другой функции :
void aClass::aMessage(bClass b, int x)
// передать самого себя в качестве аргумента b->doSomething(this, x);
В языке C++ можно опускать использование this в качестве получателя . Внутри
тела метода вызов другого метода без явного указания получателя интерпретируется как сообщение для текущего получателя . Тем самым метод color может быть ( и обычно будет ) записан следующим образом :
PDF created with pdfFactory Pro trial version www.pdffactory.com
4.1.3. Синтаксис пересылки сообщений в Java
Синтаксис для пересылки сообщений в языке Java почти идентичен используемому в C++. Единственное заметное отличие состоит в том , что псевдопеременная this в языке C++ обозначает указатель на объект , а в языке Java является собственно объектом ( поскольку в языке Java нет указателей !).
4.1.4. Синтаксис пересылки сообщений в Smalltalk
Синтаксис языка Smalltalk отличен от того , который используется в языках C++ или Object Pascal при пересылке сообщений . По - прежнему первая часть выражения описывает получателя — объект , которому предназначается сообщение . В качестве разделителя применяется пробел , а не точка .
Как и в языке C++, селектор должен соответствовать одному из методов , определенных для класса получателя . Однако в отличие от C++ Smalltalk является языком программирования с динамическими типами данных . Это означает : проверка того что класс получателя понимает селектор сообщения , выполняется во время работы программы , а не на этапе компиляции .
Если идентификатор aCard — это переменная класса Card, то следующий оператор приказывает перевернуться соответствующей карте :
Как мы отметили в главе 3, аргументы метода выделяются ключевыми селекторами . За каждым ключевым словом - селектором следует двоеточие , а затем — аргумент . Следующее выражение приказывает переменной aCard нарисовать себя в окне в точке с координатами 25 и 37:
aCard drawOn: win at: 25 and: 37
В языке Smalltalk даже бинарные операторы , вроде + или *, интерпретируются как
сообщения , причем получателем является левый член выражения , а правый передается как аргумент 1 . Как и для ключевых слов или унарных сообщений ( сообщений без аргумента ), классы вправе наделить бинарные сообщения каким угодно смыслом . Есть приоритеты : унарные сообщения имеют наивысший приоритет , затем следуют бинарные сообщения и наконец ключевые слова .
В языке Smalltalk псевдопеременная self внутри метода обозначает получателя сообщения . Это значение часто используется как получатель других сообщений , что подразумевает , что получатель желает послать сообщение самому себе . Например , описанный ранее метод color может быть переписан следующим образом , чтобы избежать прямого обращения к переменной экземпляра suit:
4.1.5. Синтаксис пересылки сообщений в языке Objective-C
Синтаксис и терминология пересылки сообщений в языке Objective-C напоминает Smalltalk. Имеются ключевые слова и унарные сообщения , но в отличие от языка Smalltalk классы в Objective-C не могут переопределять бинарные операторы .
PDF created with pdfFactory Pro trial version www.pdffactory.com
Пересылка сообщений в языке Objective-C осуществляется только внутри вызовов сообщений . Такие вызовы — это выражения , заключенные в квадратные скобки [ ]. Например , если идентификатор aCard представляет собой экземпляр класса Card, то следующее выражение приказывает карте перевернуться ( обратите внимание на точку с запятой в конце ):
1 Язык C++ обеспечивает аналогичную перегрузку бинарных операторов ( таких , как +, -, < или оператор присваивания =). Эта тема выходит за рамки данной книги , хотя мы и упомянем о
перегрузке оператора присваивания в главе 12.
" вернуть цвет карты "
(self suit = #diamond)
(self suit = #club)
(self suit = #spade)
(self suit = #heart)
Подобно языку Smalltalk, Objective-C использует динамические типы данных . Это означает : проверка того , что получатель способен понять сообщение , выполняется во время работы программы , а не на этапе компиляции . Если получатель не распознал сообщения , генерируется сообщение об ошибке .
Синтаксис языка Smalltalk разрешает использовать сообщения с аргументами . Например , следующая команда приказывает карте aCard нарисовать себя в заданном окне в точке с координатами 25 и 36:
[ aCard drawOn: win at: 25 and: 36 ];
Квадратные скобки нужны только при вызове сообщения . Если пересылка сообщения требует возвращаемого значения , то оператор присваивания должен располагаться за пределами квадратных скобок . Так , нижеследующее выражение присваивает переменной newCard копию карты aCard:
newCard = [ aCard copy ];
Пересылка сообщения может использоваться везде , где разрешены обычные выражения языка C. Например , в следующем операторе языка C проверяется , лежит ли карта aCard картинкой вверх :
if ( [ aCard faceUp ] ) .
Подобно языку Object Pascal, внутри методов Objective-C используется идентификатор self для ссылки на получателя сообщения . Однако в отличие от других языков self является истинной переменной , которая может быть модифицирована пользователем . Мы увидим в разделе 4.3.3, посвященном « методам - фабрикам », что такая модификация бывает полезна .
4.2. Способы создания и инициализации
Перед тем как детально рассмотреть механизм создания и инициализации в каждом из изучаемых языков программирования , мы рассмотрим некоторые
PDF created with pdfFactory Pro trial version www.pdffactory.com
сопутствующие моменты . Зная о различных способах , читатель будет лучше подготовлен к восприятию свойств , которые присущи ( или не присущи ) тому или иному языку программирования . Мы будем рассматривать выделение памяти ( через стек или через « кучу » (heap)), освобождение памяти , работу с указателями и создание объектов с неизменяемым состоянием . 4.2.1. Стек против « кучи »
Вопрос о выделении памяти через стек или через « кучу » связан с тем , как
выделяется и освобождается память под переменные и какие явные шаги должны при этом предприниматься программистом . Различают автоматические и динамические переменные . Существенная разница между ними состоит в том , что память для автоматических переменных создается при входе в процедуру или блок , управляющий этими переменными . При выходе из блока память ( опять же автоматически ) освобождается . Многие языки программирования используют термин статический (static) для обозначения переменных , автоматически размещаемых в стеке . Мы будем придерживаться термина автоматический , потому что он более содержателен и потому что static — это ключевое слово , которое обозначает нечто другое в языках C и C++. В момент , когда создаются автоматические переменные , происходит связывание имени и определенного участка памяти , и эта связь не может быть изменена во время существования переменной .
Рассмотрим теперь динамические переменные . Во многих традиционных языках программирования (Pascal) динамические переменные создаются системной процедурой new(x), которая использует в качестве аргумента переменную - указатель . Пример :
form : (triangle, square); side : integer;
aShape : shape; begin
Здесь выделяется новый участок памяти , и в качестве побочного эффекта значение переменной - аргумента изменяется так , что теперь она указывает на новый участок памяти . Тем самым процессы выделения памяти и привязки к имени взаимосвязаны .
В других языках ( таких , как C) выделение памяти и привязка к имени не связаны друг с другом . Например , в C выделение памяти осуществляется системной библиотечной функцией malloc, которой в качестве аргумента передается объем выделяемой памяти . Функция malloc возвращает адрес блока памяти , который затем присваивается переменной - указателю с помощью обычного оператора присваивания . Следующий пример иллюстрирует этот процесс :
PDF created with pdfFactory Pro trial version www.pdffactory.com
aShape = (struct shape *)
Существенная разница между выделением памяти через стек и через « кучу » состоит в том , что память для автоматических переменных ( основанных на механизме стека ) отводится без какого - либо явного указания со стороны пользователя , а для динамической переменной память выделяется по специальному запросу . Способы освобождения памяти также различаются . Программисту почти никогда не требуется заботиться об уничтожении автоматических переменных , в то
время как он должен управлять освобождением памяти при выделении ее через « кучу ». Эта тема является предметом следующего раздела .
4.2.2. Восстановление памяти
Когда используется выделение памяти через « кучу », необходимо обеспечить специальные средства для возврата памяти , которая больше не нужна . В общем случае языки программирования разбиваются на две большие категории . Паскаль , C и C++ требуют , чтобы пользователь сам отслеживал , какие данные ему больше не нужны , и в явном виде освобождал эту память с помощью подпрограмм из системной библиотеки . К примеру , в языке Паскаль такая подпрограмма называется dispose, а в языке C — free.
Другие языки (Java, Smalltalk) могут автоматически отследить ситуацию , когда к объекту больше нет доступа ( тем самым он изолирован от последующих вычислений ). Такие объекты автоматически собираются и уничтожаются , а выделенная для них память помечается как свободная . Этот процесс называется сборкой мусора . Для такого восстановления памяти имеется несколько хорошо известных алгоритмов . Их описание выходит за пределы данной книги . Хороший обзор алгоритмов сборки мусора дается Коэном [Cohen 1981].
Как и в случае с динамически изменяемыми типами данных ( которые будут рассматриваться в главе 10), аргументы за и против сборки мусора основаны на противопоставлении эффективности и гибкости программного обеспечения . Автоматизация сборки мусора может быть дорогой ( с точки зрения компьютерных ресурсов ), и она требует от исполнительной системы управления памятью . С другой стороны , в системах , в которых управление динамической областью памяти поручается программисту , типичными являются следующие ошибки :
∙ Попытка использовать память , которая еще не выделена .
∙ Выделенная память больше никогда не освобождается ( проблема , известная как утечка памяти ).
∙ Попытка использовать память , которая уже была освобождена .
∙ Память освобождается двумя независимыми процедурами , что приводит к освобождению одного и того же участка дважды .
PDF created with pdfFactory Pro trial version www.pdffactory.com
Чтобы избежать этих проблем , зачастую достаточно обеспечить , чтобы каждый объект с динамически выделяемой памятью имел четко обозначенного владельца .
Владелец памяти гарантирует правильное использование памяти и ее освобождение , когда она больше не нужна . В больших программах , как и в реальной жизни , споры за право быть собственником разделяемого ресурса могут создать проблемы .
Указатели — эффективное средство работы с динамической информацией , и по этой причине они используются практически во всех объектно - ориентированных языках . Вопрос : « Указатели — это полезная абстракция или нет ?» активно дискутируется в сообществе программистов . Это нашло свое отражение в том факте , что не все объектно - ориентированные языки поддерживают указатели .
В некоторых языках (Java, Smalltalk) объекты представляются внутренним образом как указатели , но при этом они не используются в этом качестве программистами .
В других языках (C++, Object Pascal) приходится явно различать переменные ,
содержащие значения , и переменные - указатели . Как мы увидим в учебных примерах на языке C++, которые будут представлены ниже , в реализации объектно - ориентированных свойств языка указатели задействованы в явном виде . В языке Objective-C значения , объявленные как id, на самом деле являются указателями , но это в значительной степени скрыто от пользователя . Когда в Objective-C объекты описываются как явно принадлежащие тому или иному классу , программисту необходимо различать объекты и явные указатели на них .
4.2.4. Создание неизменяемого экземпляра объекта
В главе 3 мы описали класс Card и отметили одно свойство , желательное для абстракции игральных карт , — а именно , чтобы значения масти и ранга ( достоинства ) карты задавались бы лишь однажды и больше не менялись . Переменные , подобные полям данных suit и rank ( они не меняют своих значений во время выполнения программы ), называются переменными с однократным присваиванием (single-assignment variables) или же неизменяемыми переменными (immutable variables). Объект , у которого все переменные экземпляра являются неизменяемыми , в свою очередь называется неизменяемым объектом .
Неизменяемые переменные следует отличать от констант , хотя в значительной степени разница состоит только во времени связывания и области видимости . Константа в большинстве языков программирования ( например , в C и Паскаль ) должна быть известна на момент компилирования , иметь глобальную область видимости и оставаться неизменной . Неизменяемой переменной можно присвоить значение , но только единожды ; таким образом , значение остается неопределенным вплоть до момента выполнения программы ( когда создается объект , содержащий это значение ).
Конечно , неизменяемые объекты могут конструироваться просто по соглашению . Например , мы можем предусмотреть сообщение , задающее масть и ранг игральной карты , и затем просто полагаться на то , что пользователь будет использовать это средство лишь однажды при создании объекта , а не для последующего изменения состояния объекта . Более предусмотрительные объектно - ориентированные
разработчики предпочитают не полагаться на добрую волю своих потребителей и
PDF created with pdfFactory Pro trial version www.pdffactory.com
используют механизмы языка , которые гарантируют то , что нужно . Языки программирования , которые мы рассматриваем , отличаются по степени , в какой они обеспечивают подобную услугу . 4.3. Механизмы создания и инициализации
Выше мы описали некоторые тонкости , возникающие при создании и инициализации объектов в языках программирования . В последующих разделах мы вкратце опишем синтаксис , используемый в каждом конкретном языке .
4.3.1. Создание и инициализация в C++
Язык C++ следует C ( а также Pascal и другим алголоподобным языкам ), поддерживая и автоматические , и динамические переменные . Автоматической переменной память назначается при входе в блок , в котором находится ее объявление , а при передаче управления за пределы блока память освобождается . Одно изменение по сравнению с языком C: объявление не обязательно размещается в начале блока . Единственное требование состоит в том , что объявление должно появляться до первого использования переменной . Тем самым оно может передвигаться непосредственно к точке , в которой используется переменная .
Неявная инициализация обеспечивается в языке C++ за счет использования конструкторов . Как было отмечено в главе 3, конструктор — это метод , который имеет то же самое имя , что и класс объекта . Хотя определение конструктора — это часть определения класса , на самом деле конструктор задействуется в процессе инициализации . Поэтому мы обсуждаем конструкторы здесь . В частности , метод - конструктор автоматически и неявно вызывается каждый раз , когда создается объект , принадлежащий к соответствующему классу . Обычно это происходит при объявлении переменной , но также и в том случае , когда объект создается с помощью оператора new или когда по каким - то причинам применяются временные переменные .
Например , рассмотрим следующее описание класса , которое может быть использовано как часть абстрактного типа данных « комплексное число »:
// операции над числами
// поля данных double realPart; double imaginaryPart;
Здесь задаются три конструктора класса . Какой конструктор будет вызван , зависит от аргументов , используемых при создании переменной . Объявление без
PDF created with pdfFactory Pro trial version www.pdffactory.com
приводит к вызову первого конструктора . Его тело может выглядеть следующим образом :
// инициализировать поля нулями realPart=0.0; imaginaryPart=0.0;
Заметьте , что тело конструктора отличается от нормального метода еще и тем , что отсутствует явный тип возвращаемого результата . Механизм конструкторов делается значительно более мощным , когда он комбинируется со способностью языка C++ перегружать имена функций ( функция называется перегруженной , если имеются две или более функции с одним именем ). В языке C++ двусмысленность
при вызове перегруженных функций разрешается за счет различий в списке аргументов .
Эта особенность конструкторов часто используется , чтобы обеспечить несколько способов инициализации . Например , мы можем инициализировать комплексное число , задавая только вещественную часть , но иногда необходимо задать и вещественную , и мнимую части . Описание класса , приведенное выше , обеспечивает обе эти возможности . Вот как они используются :
Первые две инициализации вызывают конструктор , который имеет только один аргумент . Описание такого конструктора имеет вид :
// задать вещественную часть realPart = rp;
// задать нулевую мнимую часть imaginaryPart = 0.0;
Конструкторы , которые требуют двух и более аргументов , записываются в той же форме . Аргументом является список значений , задаваемый при создании переменной .
Тело конструктора часто представляет собой просто последовательность операторов присваивания . Эти операторы могут быть заменены инициализаторами в заголовке функции , подобно тому как это сделано в нижеследующем варианте конструктора . Каждый инициализатор представляет собой просто имя переменной экземпляра ; в круглых скобках стоит список значений , который используется для инициализации переменной .
Complex::Complex(double rp) : realPart(rp), imaginaryPart(0.0)
Переменные , описанные внутри блока , создаются автоматически при входе в блок и уничтожаются при выходе из него . Динамические переменные создаются с
PDF created with pdfFactory Pro trial version www.pdffactory.com
помощью ключевого слова new, за которым следует имя класса объекта и аргументы , которые используются конструктором . Следующий пример создает новое комплексное число :
c = new Complex(3.14159265359, -1.0);
Круглые скобки не являются обязательными , если никакие аргументы не передаются :
Complex *d = new Complex;
Массив значений может быть создан , если указать его размер в квадратных скобках . Он вычисляется во время работы программы и может быть произвольным целочисленным выражением . Значения , созданные таким способом , инициализируются с помощью конструктора « по умолчанию » ( то есть конструктора , не имеющего аргументов ):
Complex *carray = new Complex[27];
Память для динамически размещенных значений должна быть явным образом освобождена программистом с помощью оператора delete ( или delete[] в случае массива объектов ).
Значения в языке C++ могут быть объявлены неизменяемыми с помощью ключевого слова const. Такие данные становятся константами — их не разрешается изменять . Переменные экземпляра , описанные как константы , обслуживаются инициализаторами , поскольку константа не может встречаться в левой части оператора присваивания .
В языке C++ могут определяться функции , которые автоматически вызываются при освобождении памяти , выделенной под объект . Они называются деструкторами . Для автоматических переменных память освобождается при выходе из процедуры , в которой описана переменная . Для динамически размещаемых переменных память возвращается с помощью оператора delete. Функция - деструктор получает имя класса с предшествующим знаком « тильда » (
). Она не имеет аргументов и редко вызывается в явном виде .
Следующий простой , но нетривиальный пример иллюстрирует использование конструкторов и деструкторов . Класс Trace определяет простое средство , которое может использоваться для трассировки вызовов процедур и функций . Конструктор класса получает аргумент ( строку ), идентифицирующую точку трассировки , и печатает сообщение , когда для соответствующего объекта Trace выделяется память ( что происходит при входе в процедуру с ее описанием ). Второе сообщение печатается , когда память освобождается , что происходит при выходе из процедуры .