Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Комплект Информатика / Курс лекций.doc
Скачиваний:
128
Добавлен:
22.05.2015
Размер:
4.8 Mб
Скачать

Контрольные вопросы

1. Почему процедура считается основной единицей языков программирования?

2. Какие бывают параметры и как они могут передаваться?

3. В чем различие между функцией и процедурой?

4. Что понимается под термином «перегрузка функции»?

5. Каким образом осуществляется ввод-вывод данных в языках программирования?

6. Из каких стадий состоит процесс трансляции программы? Дайте их краткую характеристику.

Лекция № 21 Объектно – ориентированное и декларативное программирование

Цель лекции

Изучить основные понятия объектно – ориентированного и декларативного программирования.

План лекции

1. Объектно-ориентированное программирование.

2. Декларативное программирование.

1 Объектно-ориентированное программирование

Ранее мы говорили о том, что объектно-ориентированная парадигма программирования подразумевает создание активных программных единиц — объектов, которые состоят из процедур, описывающих, как объект должен отвечать на различные стимулы. При объектно-ориентированном подходе к решению задачи определяются необходимые объекты, которые затем описываются как самостоятельные единицы. В свою очередь объектно-ориентированные языки программирования предоставляют средства для выражения таких идей. В данной лекции мы познакомимся с некоторыми из этих средств и тем, как они представлены в трех наиболее известных объектно-ориентированных языках: C++, Java и С#.

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

Согласно объектно-ориентированной парадигме каждый лазер в игре будет реализован как объект, содержащий информацию об оставшемся запасе энергии и процедуры для наведения луча на Цель лекции и выстрела. Поскольку свойства всех этих объектов (лазеров) одинаковы, их можно создавать по одному шаблону. В объектно-ориентированном программировании такой шаблон называется классом (class). В языках C++, Java и С# класс описывается с помощью следующего оператора:

class Имя_класса

{

}:

где Имя_класса — это имя, по которому к классу обращаются в программе. В фигурных скобках описываются свойства класса.

Рисунок 1 - Структура класса, описывающего лазерное оружие в компьютерной игре

Например, класс LaserClass, описывающий лазеры в нашей компьютерной игре, изображен на рис. 1. (Более детальное описание этого класса приводится позднее на рис. 3.) Любой объект, созданный по этому образцу, будет включать в себя целочисленную переменную RemainingPower и три процедуры — turnRight, turnLeft и fire, описывающие шаги для выполнения поворота направо, поворота налево и выстрела. Переменная объекта, такая как RemainingPower, называется переменной экземпляра (instance variable), а процедура объекта называется методом (method) (или, согласно терминологии языка C++, функцией-членом (member function)). Обратите внимание на то, что на рис. 1 переменная экземпляра RemainingPower объявляется с помощью оператора описания, подобного тому, который мы обсуждали в разделе 5.3, а методы описываются как процедуры и функции, рассматриваемые нами там же. В самом деле, объявление переменных и описание методов, в сущности, являются понятиями императивного программирования.

Описав класс LaserClass, мы можем объявить три переменных типа LaserClass с помощью оператора

LaserClass Laserl, Laser2, Laser3;

Обратите внимание на то, что такая же конструкция используется для описания трех целочисленных переменных х, у и z:

int x, у, 2;

Оба эти оператора состоят из имени типа и списка объявляемых переменных. Различие же заключается в том, что элемент, присвоенный переменной х, создается согласно заранее определенному шаблону целого числа (предопределенный тип данных), в то время как элемент, присвоенный переменной Laserl, создается согласно шаблону, определенному в программе как класс.

В языке C++ в результате выполнения приведенного выше оператора будут созданы три объекта типа LaserClass и присвоены переменным Laserl, Laser2 и Laser3. Однако в языках Java и С# будут просто созданы переменные, которым затем можно присвоить значения. Для того чтобы создать объект типа LaserClass и присвоить его переменной Laserl, в этих языках нужно использовать оператор

LaserClass Laserl = new LaserClass( );

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

int х = 3;

не только описывает тип переменной х, но и присваивает новой переменной значение 3.

Вне зависимости от того, с каким языком мы работаем, создав объекты и назначив им имена Laserl, Laser2 и Laser3, мы можем продолжить нашу программу, вызывая соответствующие методы этих объектов. Например, чтобы объект Laserl выполнил метод fire, нужно использовать оператор

Laserl.fire ( ):

А для того чтобы объект Laser2 выполнил метод turnLeft, используется оператор

Laser2.turnLeft ( );

Прежде чем продолжить, давайте рассмотрим отношение понятий класса и объекта. Класс представляет собой шаблон, по которому создаются объекты: Один класс можно использовать для создания большого количества объектов. Часто объект называют экземпляром класса (instance). Следовательно, в нашей компьютерной игре имена Laserl, Laser2 и Laser3 используются для обозначения экземпляров класса LaserClass.

Конструкторы. После создания объекта нужно задать некоторые его характеристики. Например, в нашей компьютерной игре лазеры могут иметь разную начальную мощность. Значит, переменной экземпляра RemainingPower в разных объектах должно быть присвоено разное начальное значение. Инициализация переменных осуществляется специальными функциями, которые называются конструкторами (constructor). При создании объекта класса они вызываются автоматически. Конструктор задается внутри описания класса, и его имя совпадает с именем класса, к которому он принадлежит.

Более подробное описание класса LaserClass приведено на рис. 2. Обратите внимание на то, что конструктор определен как метод с именем LaserClass. Этот метод присваивает переменной экземпляра RemainingPower значение, которое передается ему в качестве параметра. Таким образом, при создании объекта класса выполняется этот метод, и переменной RemainingPower присваивается необходимое значение.

Фактические параметры, которые использует конструктор, обычно задаются в виде списка параметров в операторе создания объекта. Таким образом, чтобы создать два объекта типа LaserClass: один с именем Laserl и начальной мощностью 50, а другой с именем Laser2 и начальной мощностью 100, программист C++ напишет:

LaserClass Laserl (50), Laser2 (100);

Java- и С#-программисты для этой цели воспользуются операторами

LaserClass Laserl - new LaserClass (50);

LaserClass Laser2 = new LaserClass (100);

Рисунок 2 – Описание класса с конструктором

Дополнительные возможности. Предположим, мы хотим изменить нашу компьютерную игру так, чтобы игрок, набравший определенное количество очков, мог перезарядить один из лазеров. Эти лазеры будут обладать такими же свойствами, как и остальные, за исключением того, что их можно будет перезаряжать.

Для упрощения описания объектов с похожими, но все же разными характеристиками большинство объектно-ориентированных языков позволяют одному классу включать в себя свойства другого. Эта система называется наследованием (inheritance). Предположим, например, что для написания программы мы использовали язык Java. Прежде всего, мы можем использовать операторы, о которых говорилось ранее, чтобы определить класс LaserClass, описывающий свойства, общие для всех лазеров программы. Затем, чтобы описать другой класс, Rechargeablelaser, можно использовать выражение

Class RechargeableLaser extends LaserClass

{

}

В языках C++ и С# нужно просто заменить слово extends двоеточием. Этот оператор обозначает, что класс наследует все свойства класса LaserClass, а также содержит свойства, описанные в фигурных скобках. В нашем случае фигурные скобки будут содержать новый метод (например, recharge), описывающий действия, которые необходимо выполнить для возвращения переменной экземпляра первоначального значения. После того как все классы определены, для объявления переменных Laserl и Laser2, обозначающих обычные лазеры, можно использовать оператор

LaserClass Laserl, Laser2;

А для объявления переменных Laser3 и Laser4, обозначающих лазеры, которые имеют дополнительные свойства, описанные в классе RechargeableLaser, можно использовать оператор

RechargeableLaser Laser3, Laser4;

В результате применения наследования появляется множество объектов со сходными, но все-таки различными характеристиками, что напоминает явление перегрузки операций. Вспомним, что перегрузкой операций называется использование одного символа, например +, для представления разных операций в зависимости от типа операндов. Предположим, что пакет машинной графики включает в себя объекты, представляющие разные фигуры (окружность, прямоугольник, треугольник и т. д.). Отдельное изображение состоит из набора этих объектов. Каждый объект обладает такими характеристиками, как размер, местоположение и цвет, а также отвечает на указания переместиться в другое место или вывести себя на экран. Для того чтобы нарисовать изображение, нужно послать соответствующее сообщение каждому объекту этого изображения. Однако для выведения на экран объектов разной формы используются различные процедуры, так как процесс рисования окружности совершенно отличается от процесса рисования квадрата. Такая возможность по-разному интерпретировать сообщение в зависимости от объекта называется полиморфизмом (polymorphism), а само сообщение называется полиморфным.

Другая возможность объектно-ориентированного программирования — это инкапсуляция (encapsulation), то есть ограничение доступа к внутренним свойствам объекта. Инкапсуляция переменных и функций членов класса внутри объекта означает, что к ним может обращаться только сам объект. Члены класса, защищенные от внешних воздействий, называются закрытыми, или частными (private). Члены класса, которые доступны извне объекта, называются открытыми (public).

Вернемся к нашему классу LaserClass (см. рис. 1). Он включает в себя переменную экземпляра RemainingPower и три метода — turnRight, turnLeft и fire. Эти методы должны быть доступны другим программным единицам, чтобы экземпляр класса выполнял соответствующие действия. Но значение переменной Remaining Power должно меняться только внутренними методами объекта. Никакая другая программная единица не должна иметь прямого доступа к этому значению. Для этого нужно указать, что переменная RemainingPower является закрытой, а методы turnRight, TurnLeft и fire — открытыми (рис. 3).

Рисунок 3 – Описание класса в языках Java C# с использованием ограниченного доступа

В качестве другого примера рассмотрим программу, имитирующую взаимодействие разных предприятий. Она включает в себя объекты класса MailOrderBusinessClass. Эти объекты содержат данные финансовых отчетов предприятия и метод, описывающий, как объект должен отвечать на помещение заказа. Другие объекты могут вызывать этот метод, когда помещают заказ, но они не должны иметь доступ к финансовым отчетам. Следовательно, финансовые отчеты должны быть защищены, а метод, размещающий заказы, должен быть открытым.

Параллельные операции. Предположим, нам необходимо написать программу для создания анимации в компьютерной игре, где атакуются вражеские космические корабли. Прежде всего можно создать одну программу, которая будет контролировать весь экран. Такая программа должна будет рисовать все корабли. И для того чтобы анимация была реалистичной, ей придется обрабатывать характеристики огромного числа космических кораблей. Другой подход к решению этой задачи — создать программу, контролирующую отображение на экране одного космического корабля. Его характеристики определяются параметрами, которые задаются в начале выполнения программы. Затем изображение на экране можно создать с помощью нескольких активизаций этой программы, каждая из которых имеет свой собственный набор параметров. Одновременное выполнение всех активизаций создаст иллюзию того, что по экрану перемещается множество космических кораблей.

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

Языки программирования, предусматривающие параллельную обработку, первоначально разрабатывались для создания операционных систем. Однако, как показывает наш пример, многие современные прикладные задачи легче решить, используя параллельную обработку, а не традиционную последовательность команд. В свою очередь, новейшие языки программирования предоставляют синтаксис для семантических структур, включающих в себя параллельные вычисления. Разработка таких языков требует выявления семантических структур и создания синтаксических структур для их представления.

Создатели языков программирования по-разному подходят к процессу параллельной обработки, что приводит к появлению разной терминологии. Например, то, что мы назвали активизацией, в языке Ada называется задачей (task), а в Java — потоком (thread). To есть в программе на Ada одновременные действия выполняются с помощью создания нескольких задач, а в программе на Java создается несколько потоков. В обоих случаях создаются и выполняются несколько процессов, почти как в многозадачной операционной системе. Поэтому в дальнейшем мы будем называть активизацию задачей, потоком или процессом.

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

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

Взаимодействие между процессами является предметом изучения специалистов в вычислительной технике уже долгое время. Различные подходы к решению этой задачи отражаются в новейших языках программирования. Рассмотрим в качестве примера трудности, которые могут возникнуть, когда два процесса манипулируют одними данными. В частности, если два процесса, выполняемые одновременно, должны прибавить число 3 к одному элементу данных, то необходимо убедиться в том, что когда один из процессов приступает к выполнению этой операции, другой процесс приостанавливает свою работу. В противном случае они оба будут производить вычисления над одним и тем же числом, и в результате к исходному элементу данных будет прибавлено число 3, а не 6. Данные, к которым в настоящий момент может иметь доступ только один процесс, называются данными с взаимоисключающим доступом (mutually exclusive access).

Один из способов разрешения этой трудности — написать программу, которая блокирует доступ оставшихся процессов к совместно используемым данным, когда какой-либо процесс обрабатывает их. (Часть процесса, которая получает доступ к общим данным, называется критической областью.) Опыт показал, что такой подход имеет один недостаток: задача обеспечения взаимоисключающего доступа к данным распределяется между разными частями программы. Для этого все программные единицы, получающие доступ к данным, должны быть правильно построены, иначе ошибка в одном сегменте может разрушить всю систему. По этой причине многие специалисты полагают, что лучше дать возможность данным самим контролировать доступ. В результате чего управление доступом будет сосредоточено в одной части программы, а не распределено между разными программными единицами. Элемент данных, который управляет доступом, называется монитором (data monitor).

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

В заключение следует заметить, что анимация является интересной, но не единственной областью исследования вопросов, связанных с параллельной обработкой. Методы параллельной обработки также используются при прогнозировании погоды, управлении воздушным движением, имитации сложных систем (от ядерных реакций до пешеходного движения), создании компьютерных сетей и баз данных.