- •Глава 1
- •1.2. Процедурные языки
- •1.3. Языки, ориентированные на данные
- •1.4. Объектно-ориентированные языки
- •1.5. Непроцедурные языки
- •1.6. Стандартизация
- •1.7. Архитектура компьютера
- •1.8. Вычислимость
- •1.9. Упражнения
- •Глава 2
- •2.2. Семантика
- •2.3. Данные
- •2.4. Оператор присваивания
- •2.5. Контроль соответствия типов
- •2.7. Подпрограммы
- •2.8. Модули
- •2.9. Упражнения
- •Глава 3
- •3.1. Редактор
- •3.2. Компилятор
- •3.3. Библиотекарь
- •3.4. Компоновщик
- •3.5. Загрузчик
- •3.6. Отладчик
- •3.7. Профилировщик
- •3.8. Средства тестирования
- •3.9. Средства конфигурирования
- •3.10. Интерпретаторы
- •3.11. Упражнения
- •Глава 4
- •4.1. Целочисленные типы
- •I: Integer; -- Целое со знаком в языке Ada
- •4.2. Типы перечисления
- •4.3. Символьный тип
- •4.4. Булев тип
- •4.5. Подтипы
- •4.6. Производные типы
- •4.7. Выражения
- •4.8. Операторы присваивания
- •4.9. Упражнения
- •Глава 5
- •5.1. Записи
- •5.2. Массивы
- •5.3. Массивы и контроль соответствия типов
- •Подтипы массивов в языке Ada
- •5.5. Строковый тип
- •5.6. Многомерные массивы
- •5.7. Реализация массивов
- •5.8. Спецификация представления
- •5.9. Упражнения
- •Глава 6
- •6.1. Операторы switch и case
- •6.2. Условные операторы
- •6.3. Операторы цикла
- •6.4. Цикл for
- •6.5. «Часовые»
- •6.6. Инварианты
- •6.7. Операторы goto
- •6.8. Упражнения
- •Глава 7
- •7.1. Подпрограммы: процедуры и функции
- •7.2. Параметры
- •7.3. Передача параметров подпрограмме
- •7.4. Блочная структура
- •7.5. Рекурсия
- •7.6. Стековая архитектура
- •7.7. Еще о стековой архитектуре
- •7.8. Реализация на процессоре Intel 8086
- •7.9. Упражнения
- •Глава 8
- •8.1 . Указательные типы
- •8.2. Структуры данных
- •8.3. Распределение памяти
- •8.4. Алгоритмы распределения динамической памяти
- •8.5. Упражнения
- •Глава 9
- •9.1. Представление вещественных чисел
- •9.2. Языковая поддержка вещественных чисел
- •9.3. Три смертных греха
- •Вещественные типы в языке Ada
- •9.5. Упражнения
- •Глава 10
- •10.1. Преобразование типов
- •10.2. Перегрузка
- •10.3. Родовые (настраиваемые) сегменты
- •10.4. Вариантные записи
- •10.5. Динамическая диспетчеризация
- •10.6. Упражнения
- •Глава 11
- •11.1. Требования обработки исключительных ситуаций
- •11.2. Исключения в pl/I
- •11.3. Исключения в Ada
- •11.5. Обработка ошибок в языке Eiffei
- •11.6. Упражнения
- •Глава 12
- •12.1. Что такое параллелизм?
- •12.2. Общая память
- •12.3. Проблема взаимных исключений
- •12.4. Мониторы и защищенные переменные
- •12.5. Передача сообщений
- •12.6. Язык параллельного программирования оссаm
- •12.7. Рандеву в языке Ada
- •12.9. Упражнения
- •Глава 13
- •13.1. Раздельная компиляция
- •13.2. Почему необходимы модули?
- •13.3. Пакеты в языке Ada
- •13.4. Абстрактные типы данных в языке Ada
- •13.6. Упражнения
- •Глава 14
- •14.1. Объектно-ориентированное проектирование
- •В каждом объекте должно скрываться одно важное проектное решение.
- •14.3. Наследование
- •14.5. Объектно-ориентированное программирование на языке Ada 95
- •Динамический полиморфизм в языке Ada 95 имеет место, когда фактический параметр относится к cw-типу, а формальный параметр относится к конкретному типу.
- •14.6. Упражнения
- •Глава 15
- •1. Структурированные классы.
- •15.1. Структурированные классы
- •5.2. Доступ к приватным компонентам
- •15.3. Данные класса
- •15.4. Язык программирования Eiffel
- •Если свойство унаследовано от класса предка более чем одним путем, оно используется совместно; в противном случае свойства реплицируются.
- •15.5. Проектные соображения
- •15.6. Методы динамического полиморфизма
- •15.7. Упражнения
- •5Непроцедурные
- •Глава 16
- •16.1. Почему именно функциональное программирование?
- •16.2. Функции
- •16.3. Составные типы
- •16.4. Функции более высокого порядка
- •16.5. Ленивые и жадные вычисления
- •16.6. Исключения
- •16.7. Среда
- •16.8. Упражнения
- •Глава 17
- •17.2. Унификация
- •17.4. Более сложные понятия логического программирования
- •17.5. Упражнения
- •Глава 18
- •18.1. Модель Java
- •18.2. Язык Java
- •18.3. Семантика ссылки
- •18.4. Полиморфные структуры данных
- •18.5. Инкапсуляция
- •18.6. Параллелизм
- •18.7. Библиотеки Java
- •8.8. Упражнения
1. Структурированные классы.
• Абстрактные классы используются для создания абстрактного интерфейса, который можно реализовать с помощью одного или нескольких наследуемых классов.
• Родовые подпрограммы (Ada) и шаблоны (C++) можно комбинировать с наследованием для параметризации классов другими классами.
• Множественное наследование: класс может быть производным от двух или нескольких родительских классов и наследовать данные и операции каждого из них.
2. Доступ к приватным компонентам: Являются компоненты в закрытой части пакета или класса всегда приватными, или их можно экспортировать производным классам или клиентам?
3. Данные класса. В этом разделе обсуждаются создание и использование компонентов данных в классе.
4. Eiffel. Язык Eiffel был разработан для поддержки ООП как единственного метода структурирования программ; поучительно сравнить конструкции языка Eiffel с конструкциями языков Ada 95 и C++, где поддержка ООП была добавлена к уже существующим языкам.
5. Проектные соображения. Каковы компромиссы между использованием класса и наследованием из класса? Для чего может использоваться наследование? Каковы взаимоотношения между перегрузкой и замещением?
В заключение приводится сводка методов динамического полиморфизма.
15.1. Структурированные классы
Абстрактные классы
Когда класс порождается из базового класса, предполагается, что базовый класс содержит большую часть требуемых данных и операций, тогда как производный класс всего лишь добавляет дополнительные данные, а также добавляет или изменяет некоторые операции. Во многих проектах лучше рассматривать базовый класс как некий каркас, определяющий общие операции для всего семейства производных классов. Например, семейство классов операций ввода/вывода или графики может определять такие общие операции, как get и display, которые будут определены для каждого производного класса. И Ada 95, и C++ поддерживают такие абстрактные классы.
Мы продемонстрируем абстрактные классы, описывая несколько реализаций одной и той же абстракции; абстрактный класс будет определять структуру данных Set, и производные классы — реализовывать множества двумя различными способами. В языке Ada 95 слово abstract обозначает абстрактный тип и абстрактные подпрограммы, связанные с этим типом:
Ada |
type Set is abstract tagged null record;
function Union(S1, S2: Set) return Set is abstract;
function Intersection(S1, S2: Set) return Set is abstract;
end Set_Package;
Вы не можете объявить объект абстрактного типа и не можете вызвать абстрактную подпрограмму. Тип служит только каркасом для порождения конкретных типов, а подпрограммы должны замещаться конкретными подпрограммами.
Сначала мы рассмотрим производный тип, в котором множество представлено булевым массивом:
with Set_Package;
package Bit_Set_Package is
type Set is new Set_Package.Set with private;
function Union(S1, S2: Set) return Set;
function lntersection(S1, S2: Set) return Set;
Ada |
type Bit_Array is array(1..100) of Boolean;
type Set is new Set_Package.Set with
record
Data: Bit_Array;
end record;
end Bit_Set_Package;
Конечно, необходимо тело пакета, чтобы реализовать операции.
Производный тип — это конкретный тип с конкретными компонентами данных и операциями, и он может использоваться как любой другой тип:
with Bit_Set_Package; use Bit_Set_Package;
procedure Main is
S1.S2, S3: Set;
Ada |
S1 := Union(S2, S3);
end Main;
Предположим теперь, что в другой части программы требуется другая реализация множеств, которая использует связанные списки вместо массивов. Вы можете породить дополнительный конкретный тип из абстрактного типа и использовать его вместо или в дополнение к предыдущей реализации:
with Set_Package;
package Linked_Set_Package is
type Set is new Set_Package.Set with private;
function Union(S1, S2: Set) return Set;
Ada |
private
type Node;
type Pointer is access Node;
type Set is new Set_Package.Set with
record
Head: Pointer;
end record;
end Linked_Set_Package;
Новая реализация может использоваться другим модулем; фактически, вы можете изменить реализацию, используемую в существующих модулях, просто заменяя контекстные указания:
Ada with Linked_Set_Package; use Linked_Set_Package;
Ada |
S1.S2, S3: Set;
begin
S1 := Union(S2, S3);
end Main;
В C++ абстрактный класс создается с помощью объявления чистой виртуальной функции, обозначенной «начальным значением» 0 для функции.
Абстрактный класс для множеств в языке C++ выглядит следующим образом:
class Set {
C++ |
virtual void Union(Set&, Set&) = 0;
virtual void lntersection(Set&, Set&) = 0;
};
У абстрактных классов не бывает экземпляров; абстрактный класс может только быть базовым для производных классов:
class Bit_Set: public Set {
public:
virtual void Union(Set&, Set&);
virtual void lntersection(Set&, Set&);
C++ |
int data[100];
};
class Linked_Set: public Set {
public:
virtual void Union(Set&, Set&);
virtual void lntersection(Set&, Set&);
private:
int data;
Set *next;
};
Конкретные производные классы можно использовать как любой другой класс: __
void proc()
{
C++ |
Linked_Set 11,12,l3;
b1.Union(b2,b3);
H.Union(l2,I3);
}
Обратите внимание на разницу в синтаксисе двух языков, которая вызвана разными подходами к ООП. В языке Ada 95 определяется обычная функция, которая получает два множества и возвращает третье. В языке C++ одно из множеств — отличимый получатель сообщения. Для
b1.Union(b2,b3);
подразумевается, что экземпляр b1, отличимый получатель операции Union, получит результат операции от двух параметров — Ь2 и bЗ — и использует его, • чтобы заменить текущее значение внутренних данных.
Возможно, вы предпочтете перегрузить предопределенные операции, например «+» и «*», вместо того чтобы использовать имена Union и Intersection. Это можно сделать как в C++, так и в Ada 95.
Все реализации абстрактного класса покрываются типом класса (CW-типом) Set'Class. Величины абстрактного CW-типа будут диспетчеризованы к правильному конкретному типу, т. е. к правильной реализации. Таким образом, абстрактные типы и операции дают возможность программисту писать программное обеспечение, не зависящее от реализации.
Родовые возможности
В разделе 10.3 мы обсуждали родовые подпрограммы в языке Ada, которые позволяют программисту создавать шаблоны подпрограмм и затем конкретизировать их для различных типов. Родовые возможности чаще всего находят приложение в пакетах Ada; например, пакет работы со списком может быть родовым в отношении типа элементов списка. Кроме того, он может быть родовым в отношении функций, сравнивающих элементы, с тем
чтобы элементы списка можно было сортировать:
generic
type Item is private;
with function "<"(X, Y: in Item) return Boolean;
Ada |
type List is private;
procedure Put(l: in Item; L: in out List);
procedure Get(l: out Item; L: in out List);
private
type List is array( 1.. 100) of Item;
end List_Package;
Этот пакет теперь может быть конкретизирован для любого типа элемента:
Ada |
Конкретизация создает новый тип, и можно объявлять и использовать объекты этого типа:
lnt_List_1, lnt_List_2: lnteger_List.List;
lnteger_List.Put(42, lnt_List_1 );
lnteger_List.Put(59, lnt_List_2);
В языке Ada есть богатый набор нотаций для написания родовых формальных параметров, которые используются в модели контракта, чтобы ограничить фактические параметры некоторыми классами типов, такими как дискретные типы или типы с плавающей точкой. В языке Ada 95 эти средства обобщены до возможности специфицировать в родовом формальном параметре классы типов, задаваемые программистом:
with Set_Package;
Ada |
type Set_Class is new Set_Package.Set; package Set_IO is
…
end Set_IO;
Эта спецификация означает, что родовой пакет может быть конкретизирован с любым типом, производным от тегового типа Set, такого как Bit_Set и Linked_Set. Все операции из Set, такие как Union, могут использоваться внутри родового пакета, потому что из модели контракта мы знаем, что любая конкретизация будет с типом, производным от Set, и, следовательно, она наследует или замещает эти операции.
Шаблоны
В языке C++ можно определять шаблоны классов:
Ada |
class List {
void put(const Item &);
};
Как только шаблон класса определен, вы можете определять объекты этого класса, задавая параметр шаблона:
C++ |
// lnt_List1 является экземпляром класса List с параметром int
Так же как и язык Ada, C++ позволяет программисту для объектов-экземпляров класса задать свои программы (процесс называется специализацией, specialization) или воспользоваться по умолчанию подпрограммами, которые существуют для класса. Есть важное различие родовых пакетов Ada и шаблонов C++. В языке Ada конкретизация родового пакета, который определяет тип, даст вам конкретный пакет, содержащий конкретный тип. Чтобы получить объект, потребуется еще один шаг. В C++ конкретизация дает объект сразу, не определяя конкретного класса. Чтобы определить другой объект, нужно просто конкретизировать шаблон снова:
C++ |
Компилятор и компоновщик отвечают за то, чтобы отследить пути всех конкретизации одного и того же типа и гарантировать, что код для операций шаблона класса не тиражируется для каждого объекта.
Следующее различие между языками состоит в том, что C++ не использует модель контракта, поэтому не исключено, что конкретизация вызовет ошибку компиляции в самом шаблоне (см. раздел 10.3).
Множественное наследование
Ранее обсуждалось порождение классов от одного базового класса, так что семейство классов образовывало дерево. При объектно-ориентированном проектировании, вероятно, класс будет иметь характеристики двух или нескольких существующих классов, и кажется допустимым порождать класс из нескольких базовых классов. Это называется множественным наследованием (multiple inheritance). На рисунке 15.1 показано, что Airplane (самолет) может
быть многократно порожден из Winged_Vehicle (летательный аппарат с крыльями) и Motorized_Vehicle (летательный аппарат с мотором), в то время как Winged_Vehicle также является (единственным) базовым классом для Glider (планер). Задав два класса:
class Winged_Vehicle {
public:
void display(int);
C++ |
int Wing_Length; // Размах крыла
int Weight; // Bec
};
class Motorized_Vehicle {
public:
void display(int);
protected:
int Power; // Мощность
int Weight; // Bec
};
можно породить класс с помощью множественного наследования:
class Airplane:
C++ |
public:
void display_all();
};
Чтобы использовать множественное наследование, необходимо решить, что делать с данными и операциями, такими как Weight и display, которые наследуются из нескольких базовых классов. В языке C++ неоднозначность, вызванная многократно определенными компонентами, должна быть явно разрешена с помощью операции уточнения области действия:
void Airplane: :display_all()
{
C++ |
Winged_Vehicle::display(Winged_ Vehicle:: Weight);
Motorized_ Vehicle:: display(Power);
Motorized_ Vehicle:: display(Motorized_ Vehicle:: Weight);
};
Это нельзя считать удачным решением, так как вся идея наследования в том, чтобы допускался прямой доступ к данным и операциям базы, если не требуется их модификации. Реализовать множественное наследование намного труднее, чем простое наследование, которое мы описали в разделе 14.4. Более подробно см. разделы с 10.1с по 10.1с упомянутого ранее справочного руководства по языку C++.
Значение множественного наследования в ООП является предметом для дискуссии. Некоторые языки программирования, такие как Eiffel, поддерживают использование множественного наследования, в то время как языки, подобные Ada 95 и Smalltalk, не имеют таких средств. При этом утверждается, что проблемы, которые можно решить с помощью множественного наследования, изящно решаются с использованием других средств языка. Например, выше мы отмечали, что родовые параметры теговых типов в языке Ada 95 можно использовать для создания новых абстракций, комбинируя уже существующие абстракции. Очевидно, что наличие возможности множественного наследования оказывает глубокое влияние на проектирование и программирование объектно-ориентированной системы. Таким образом, трудно говорить об объектно-ориентированном проекте, не зависящем от языка; даже на самых ранних стадиях проектирования вам следует ориентироваться на конкретный язык программирования.