Глава 4: Сообщения, экземпляры и инициализация

Глава 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 выделяется память ( что происходит при входе в процедуру с ее описанием ). Второе сообщение печатается , когда память освобождается , что происходит при выходе из процедуры .

📎📎📎📎📎📎📎📎📎📎