Программирование на C++. Классика CS 546900189X

Эта книга написана для программистов, уже владеющих языком С++ и желающих поднять свою квалификацию на новый уровень. Да

870 98 12MB

Russian Pages [479] Year 2005

Report DMCA / Copyright

DOWNLOAD FILE

Polecaj historie

Программирование на C++. Классика CS
 546900189X

Table of contents :
Содержание
Предисловие
Изучение языка программирования
О книге
Структура книги
Благодарности
От издателя перевода
Глава 1. Введение
1.1. C++ как развивающийся язык
1.2. Решение проблемы сложности при помощи идиом
1.3. Объекты для 90-х
1.4. Проектирование и язык
Литература
Глава 2. Абстракция и абстрактные типы данных
2.1. Классы
2.2. Объектная инверсия
2.3. Конструкторы и деструкторы
2.4. Подставляемые функции
2.5. Инициализация статических переменных
2.6. Статические функции классов
2.7. Область видимости и константность
2.8. Порядок инициализации глобальных объектов, констант и статических членов классов
2.9. Обеспечение константности функций классов
Логическая и физическая константность
2.10. Указатели на функции классов
2.11. Правила организации программного кода
Упражнения
Литература
Глава 3. Конкретные типы данных
3.1. Ортодоксальная каноническая форма класса
3.2. Видимость и управление доступом
3.3. Перегрузка - переопределение семантики операторов и функций
Пример перегрузки оператора индексирования
Перегрузка операторов в классах и глобальная перегрузка
3.4. Преобразование типа
3.5. Подсчет ссылок
Идиома класса-манипулятора
Экономное решение
Подсчет указателей
Реализация подсчета ссылок в существующих классах
Классы конвертов и синглетные письма
3.6. Операторы new и delete
3.7. Отделение инициализации от создания экземпляра
Упражнения
Литература
Глава 4. Наследование
4.1. Простое наследование
Настройка операций класса Complex для семантики класса lmaginary
Использование кода базового класса в производном классе
Изменение функций производного класса для повышения эффективности
4.2. Видимость и управление доступом
Вертикальное управление доступом при наследовании
Горизонтальное управление доступом при наследовании
Создание экземпляров путем наследования и управления доступом
4.3. Конструкторы и деструкторы
Порядок выполнения конструкторов и деструкторов
Передача параметров конструкторам базовых классов и внутренних объектов
4.4. Преобразование указателей на классы
4.5. Селектор типа
Упражнения
Литература
Глава 5. Объектно-ориентированное программирование
5.1. Идентификация типов на стадии выполнения и виртуальные функции
5.2. Взаимодействие деструкторов и виртуальные деструкторы
5.3. Виртуальные функции и видимость
5.4. Чисто виртуальные функции и абстрактные базовые классы
5.5. Классы конвертов и писем
Классы конвертов и делегированный полиморфизм
Имитация виртуальных конструкторов
Другой подход к виртуальным конструкторам
Объекты переменного размера
Делегирование и классы конвертов
Итераторы и курсоры
5.6. Функторы
Функциональное и аппликативное программирование
5.7. Множественное наследование
Пример абстракции окна
Неоднозначность при вызове функций класса
Неоднозначность в данных
Виртуальные базовые классы
Предотвращение лишних вызовов функций виртуальных базовых классов
Виртуальные функции
Преобразование указателей на объекты при множественном наследовании
5.8. Каноническая форма наследования
Упражнения
Пример итератора очереди
Простые классы счетов для банковского приложения
Литература
Глава 6. Объектно-ориентированное проектирование
6.1. Типы и классы
6.2. Основные операции объектно­ориентированного проектирования
6.3. Объектно-ориентированный и доменный анализ
Причины увеличения объема проектирования
Способы расширения абстракций
Балансировка архитектуры
Результаты хорошей балансировки архитектуры
6.4. Отношения между объектами и классами
Отношения "IS-A"
Отношения "HAS-A"
Отношения "USES-A"
Отношения "CREATES-A"
Контекстное изучение отношений между объектами и классами
Графическое представление отношений между объектами и классами
6.5. Субтипы, наследование и перенаправление
Наследование ради наследования - ошибка потери субтипов
Случайное наследование - омонимы в мире типов
Поучительный пример
Решения без побочного наследования
Наследование с расширением и подменой
Наследование с сокращением
Потребность в функциях классов с ссистинной» семантикой
Пример 1. Операции get и set
Пример 2. Открытые данные
Наследование и независимость классов
6.6. Практические рекомендации
Упражнения
Литература
Глава 7. Многократное использование программ и объекты
7.1. Об ограниченности аналогий
7.2. Многократное использование архитектуры
7.3. Четыре механизма многократного использования кода
7.4. Параметризованные типы, или шаблоны
7.5. Закрытое наследование и многократное использование
7.6. Многократное использование памяти
7.7. Многократное использование интерфейса
7.8. Многократное использование, наследование и перенаправление
7.9. Архитектурные альтернативы для многократного использования исходных текстов
7.10. Общие рекомендации относительно многократного использования кода
Упражнения
Литература
Глава 8. Прототипы
8.1. Пример с прототипами класса Employee
8.2. Прототипы и обобщенные конструкторы
8.3. Автономные обобщенные конструкторы
8.4. Абстрактные баз.овые прототипы
8.5. Идиома фреймовых прототипов
8.6. Условные обозначения
8.7. Прототипы и администрирование программ
Упражнения
Простой анализатор с прототипом
Фреймовые прототипы
Литература
Глава 9. Эмуляция символических языков на С++
9.1. Инкрементное программирование на С++
Инкрементный подход и объектно­ориентированное проектирование
Сокращение затрат на компиляцию
Сокращение затрат на компоновку и загрузку
Ускоренные итерации
9.2. Символическая каноническая форма
Класс Top
Класс Thing
Символическая каноническая форма для классов приложений
9.3. Пример обобщенного класса коллекции
9.4. Код и идиомы поддержки инкрементной загрузки
Загрузка виртуальных функций
Обновление структуры класса и функция cutover
Добавление нового поля в класс
Существенные изменения в представлении класса
Управление загрузкой функций и преобразованием объектов
Инкрементная загрузка и автономные обобщенные конструкторы
9.5. Уборка мусора
Пример иерархии геометрических фигур с уборкой мусора
9.6. Инкапсуляция примитивных типов
9.7. Мультиметоды в символической идиоме
Упражнения
Литература
Глава 10. Динамическое множественное наследование
10.1. Пример оконной системы с выбором технологии
10.2. Предостережение
Глава 11. Системные аспекты
11.1. Статическая системная структура
Транзакционные диаграммы
Модули
Подсистемы
Каркасы
Библиотеки
11.2. Динамическая системная структура
Планирование
Задачи С++
Программные потоки
Контексты
Взаимодействие между пространствами имен
Снова о процессах и объектах
Прозрачность
Обработка исключений
Уборка мусора как средство повышения надежности
Контекст как единица восстановления
Зомби
Литература
Приложение А. С в среде С++
А.1. Вызовы функций
А.2. Параметры функций
А.З. Прототипы функций
А.4. Передача параметров по ссылке
А.5. Переменное количество параметров
А.6. Указатели на функции
А.7. Модификатор const
Пример 1. Использование модификатора const вместо директивы #define
Пример 2. Модификатор const и указатели
Пример 3. Объявление функций с константными аргументами
А.8. Взаимодействие с кодом С
А.8.1. Архитектурные аспекты
А.8.2. Языковая компоновка
А.8.3. Вызов функций С++ из С
А.8.4. Совместное использование заголовочных файлов в С и С++
А.8.5. Импорт форматов данных С в С++
А.8.6. Импорт форматов данных С++ в С
Упражнения
Литература
Приложение Б. Программа Shapes
Приложение В. Ссылочные возвращаемые значения операторов
Приложение Г. Поразрядное копирование
Приложение Д. Иерархия геометрических фигур в символической идиоме
Приложение Е. Блочно-структурное программирование на С++
Е.1. Концепция блочно-структурного программирования
Е.2. Основные строительные блоки структурного программирования на С++
Е.З. Альтернативное решение с глубоким вложением областей видимости
Е.4. Проблемы реализации
Упражнения
Код блочно-структурной видеоигры
Литература
Приложение Ж. Список терминов
Алфавитный указатель

Citation preview

Advanced С++ Programming

Styles and ldioms James О. Coplien

" тт

Addison-Wesley

Дж. Коплиен ПРОГРАММИРОВАНИЕ

++

НА

�пп11:р·

Москва· Санкт-Петербург· НижниА Новгород· Воронеж Новосибирск



Ростов-на-Дону Киев



Харьков

2005

• •

Екатеринбург Минск



Самара

ББК 32.973-018.1 УДК 681.3.06 К65

К65

КоплиенДж.

Программирование на С++. Классика CS.

ISBN

-

5-469-00189-Х

СПб.: Питер, 2005.

Эта книга написана для программистов, уже владеющих языком

С++

-

479

с.:

ил.

и желающих поднять

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

С++

С++

можно использовать и для разработки

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

С++,

например функциональное и

фреймовое программирование, а также расширенные методы уборки мусора.

ББК 32.973-018.1 УДК 681.3.06

Права на издание поnучены по согnашению с Addisoп-Wesley Loпgmaп. Все права защищены. Никакая часть данной книги не может бьггь воспроизведена в какой бы то ни быnо форме без письменного разрешения вnадеnьцев авторских прав. Информация, содержащаяся в данной книге, поnучена из исrочников, рассматриваемых издатеnьсrвом как надежные. Тем не менее. имея в виду возможные чеnовеческие иnи технические ошибки, издатеnьсrво не может гарантировать абсоnютную точность и поnноту приводимых сведений и не несет ответсrвенности за возможные ошибки, связанные с испоnьзованием книги.

ISBN 0201548550 (ангn.) ISBN 5-469-00189-Х

© 1992 Ьу

АТ&Т Вell Telephone LaЬoratoгies, lпco rpoгated

©Перевод на русский язык ЗАО Издатеnьский дом «Питер», ©Издание на русском языке, оформnение

2005 ЗАО Издатеnьский дом

«Питер»,

2005

К р аткое с одержа н и е Предисловие

12

Глава 1. Введение

19

Глава 2. Абстракция и абстрактные типы данных .

25

Глава 3. Конкретные типы данных

52

Глава 4. Наследование

97

.

.

.. . .

126

Глава 5. Объектно -ориентированное программирование . Глава 6. Объектно-ориентированное проектирование .. .

203

Глава 7. Многократное использование программ и объекты

247

Глава 8. Прото типы

277

. . . . .. . . .. . . . . .. . .

Глава 9. Эмуляция символических языков на С ++ ...

303

Глава 1 0. Динамическое множественное наследование

349

Глава 1 1 . Системные аспекты

355

Приложение А.С в среде С ++

381

Приложение Б. Программа Shapes .

403

Приложение В. Ссылочные возвращаемые значения операто ров

41 4

Приложение Г . Поразрядное копирование .. . . .... ...

416

.

Приложение д. Иерархия геометрических фи гур в символической идиоме

41 8

Приложение Е. Блочно -структурное программирование на С ++

451

Алфавитный указатель . . .. .

472

Приложение Ж. Список терминов

.

467

Содержа н и е Предисловие

12

Изучение языка п рограммирования О книге . . . . . Структура книги . . . Благодарности . . . . От издателя перевода

12 13 14 17 18

Гпава 1 . Введение .

.

19

1 . 1 . С++ как развивающийся язык . . 1 .2. Решение проблемы сложности при помощи идиом 1.3. Объекты для 90-х . . . 1.4. Проектирование и язык . . . . . . . . . . . . . . Литература . . . . . . . . . . . .

19 20 22 23 24

Гпава 2. Абстракция и абстрактные типы данных .

25

2.1. Классы . . . . . . . 2.2. Объектная инверсия . . . . . 2.3. Конструкторы и деструкторы 2.4. Под ставляемые функции . 2 .5. Инициализация статических переменных 2.6. Статические функции классов . . . . . 2. 7. Область видимости и константность . . . 2.8. Порядок инициализа ции глобальных объектов , констант и статических членов классов . . . . . . . . . . . . 2.9. Обеспечение константности функций классов . Логическая и физическая константность . 2 . 1 0 . Указатели на функции классов . . . . . . 2 .1 1 . Правила органи зации программного кода Упражнения . . . . . . . . . . . . . . Литература . . . . . . . . . .

26 29 31 36 38 39 40

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

Гпава 3. Конкретные типы данных .

52

3.1. Ортодоксальная каноническая форма класса 3.2. Видимость и управление доступом . . . 3.3. Перегрузка - переопределение семантики операторов и функций Пример перегрузки оператора индексирования . . . . . . Пере грузка операторов в к лассах и глобальная перегрузка 3.4. Преобразование типа . 3.5. Подсчет ссылок . . . . . . . . Идиома к ласса -манипулятора Экономное решение . . . .

.

.

.

.

.

.

41 42 43 45 49 50 51

.

53 60 62 64 66 67 71 72 75

Содержание

3

Подсчет указателей . . . . . . . . . . . . . . . . . . Реализация подсчета ссылок в существующих классах Классы конвертов и синглетные письма . . . . . . 3.6. Операторы пе w и delete . . . . . . . . . . . . . . . 3.7. Отделение инициализации от создания экземпляра . Упражнения . . . . . . . Ли тература . . . . . . .

78 81 83 84 91 94 96

Глава 4. Наследование

97

.

4. 1 . П ростое наследование . . . 99 102 Настройка операций класса Complex для семантики класса lmagiпary . . 102 Использование кода базового класса в производном классе . . . . . . . 103 Изменение функций производного класса для повышения эффективности 4.2. Видимость и управление доступом . . . . . . . . . . . . . 105 106 Вертикальное управление доступом при наследовании 110 Горизонтальное управление доступом при наследовании 115 Создание экземпляров путем наследования и управления доступом . 116 4.3. Конструкторы и деструкторы . . . . . . . . . . . . . . . . . . . . . 116 Порядок выполнения конструкторов и деструкторов . . . . . . . . . Передача параметров конструкторам базовых классов и вн утренних объектов 117 119 4.4. Преобразование указателей на классы 121 4.5. Селектор типа 124 Упражнения . . . . . . . . . . 125 Литература . . . . . . . . . . . . . . . . .

.

.

.

.

.

.

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

.

5.1. Идентификация типов на стадии выполнения и вир туальные функции 5.2. Взаимоде йствие деструкторов и виртуальные деструкторы 5.3. Виртуальные функции и видимость . . . . . . . . . . . . . 5.4. Чисто виртуальные функции и абстрактные базовые классы 5.5. Классы конвертов и писем . . . . . . . . . . . . . . Классы конвертов и делегированный полиморфизм Имитация виртуальных конструкторов Другой по дход к вир туальным конструкторам Деле ги рование и классы конвертов . Итераторы и курсоры . . . . . . . . . . . . 5.6. Функторы . . . . . . . . . . . . . . . . . . . . Функциональное и аппликативное программирование 5.7. Множественное наследование . . . . . . . . . Пример абстракции окна . . . . . . . . . . . Неоднозначность при вызове функций класса Неоднозначность в данных . . . . . . . . . . Виртуальные базовые классы . . . . . . . . . Предотвращение лишних вызовов функций виртуальных базовых классов Виртуальные функции . . . . . . . . . . . . . . . . . . . . . . . . . . . Преобразование указателей на объекты при множественном нас ледовании 5.8. Каноническая форма наследования Упражнения . . . . . . . . . . . . . . . . . . . . . . Пример итератора очереди . . . . . . . . . . . . . . Простые классы счетов для банковского приложения . Литература . . . . . . . . . . . . . . . . . . . . . . .

.

1 26

128 135 136 139 1 41 1 42 148 155 166 170 171 175 1 83 184 187 188 1 88 189 191 192 193 197 198 200 202

8

Содержание

Глава 6 . Объектно-ориентированное проектирование



203



6. 1. Типы и классы . . . . . . . . . . . . . . . . . . 6 .2. Основные операции объектно-ориентированно го проектирования 6.3. Объектно-ориентированный и доменный анализ . Причины увеличения объема проектирования Способы расширения абстракций . . . Балансировка архитектуры . . . . . . . . . . . Результаты хорошей балансировки архитектуры 6 .4. Отношения между объектами и классами Отношения cclS-A • . . Отношения ccHAS-A» . . . Отношения « USES-A» . . Отношения ccCREATES-A» Контекстное изучение отношений между объектами и классами Графическое представление отношений между объектами и классами 6.5. Субтипы, наследование и перенаправление . . . . . . . . . . . Наследование ради наследования - ошибка потери субтипов . Случайное наследование - омонимы в мире типов . . . . Потребность в функциях классов с «истинной» семантикой Наследование и независимость классов 6.6. Практические рекомендации . Упражнения . . . . . . . . . . . . . . . . . . Литература . . . . . . . . . . . . . . . . . Глава 7. Многократное испопьэование программ и объекты

204 209 21 2 21 2 21 3 21 4 21 5 215 21 5 21 7 220 220 220 221 224 224 228 239 241 244 245 246 .

.

Глава 8. Прототипы



.

8. 1 . П ример с п рототипами класса Employee 8.2. П рототипы и обобщенные конструкторы 8.3. Автономные обобщенные конструкторы 8.4. Абстрактные базовые прототипы . 8.5. Идиома фреймовых прототипов . . . . 8.6. Условные обозначения . . . . . . . . . 8.7. Прототипы и администрирование программ Упражнения . . . . . . . . . . . . Простой анализатор с прототипом Фреймовые п рототипы Литература . . . . . . . . . . . .

247

249 251 253 256 264 267 268 270

7.1 . Об ограниченности аналогий . . . . . . . . . . . . ; . 7 .2. Многократное использование архитектуры . . . . . . . 7.3. Четыре механизма многократного использования кода 7.4. Параметризованные типы, или шаблоны . . . . . . . . 7.5. Закрытое наследование и многократное использование 7.6. Многократное использование памяти . . . . . . . . . 7.7. Многократное использование интерфейса . . . . . . . 7.8 . Многократное использование, наследование и перенаправление 7.9. Архитектурные альтернативы для многократного использования исходных текстов . . . . . . . . . . . . . . . . . . . . . . . . 7 . 1 0 . Общие рекомендации относительно многократного использования кода . Упражнения . . . . . . Литература . . . . . . . .

271 274 275 276 .

.

277

280 285 287 289 292 294 296 297 298 300 302

9

Содержание Глава 9. Эмуляция символических языков на С++





















303



. . . . . 9. 1. И нкрементное программирование на С++ . . . . . . . Инкрементный подход и объектно-ориентированное проектирование Сокращение затрат на компиляцию . . . . . Сокращение затрат на компоновку и загрузку Ускоренные итерации . . . . . 9.2. Символическая каноническая форма . Класс Тор . . . . . . . . . . . .. Класс Thing . . . . . . . . . . . Символическая каноническая форма для классов приложений 9.3. Пример обобщенного класса коллекции . . . . . 9.4 . Код и идиомы по,одержки инкрементной загрузки Загрузка виртуальных функций . . . . . . . . Обновление структуры класса и функция cutover И нкрементная загрузка и автономные обобщенные конструкторы 9.5. Уборка мусора . . . . . . . . . . . . . . . . П ример иерархии геометрических фигур с уборкой мусора 9 .6. Инкапсуляция примитивных типов . . . . 9.7. Мультиметоды в символической идиоме . Упражнения . . . . . . . . . . . Литература . . . . . . . . . . . . . . . .

305 305 305 306 306 307 309 31 0 311 318 323 324 327 332 333 336 342 343 347 348

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

Глава 1 О. Динамическое множественное наследование

.



349

1О.1. П ример оконной системы с выбором технологии 10.2. П редостережение . . . . . . .

. 350 . . . 353

Глава 1 1 . Системные аспекты

.





.

.

.

.

.

.

.

.

.

Приложение А. С в среде С++

А.1 . А.2. А.3 . А.4. А.5. А.6. А. 7.

.

.



Вызовы функций . . Параметры функций . . . . . П рототипы функций . . . . . . Передача параметров по ссылке . Переменное количество параметров Указатели на функции . . . . . . Модификатор coпst . . . . . . . . . Пример 1. Использование модификатора const вместо директивы #define Пример 2. Модификатор const и указатели . . . . . . . . . . П ример 3. Объявление функций с константными аргументами . . . . . . . .

.

355

356 357 359 361 362 364 365 365 371 372 375 379 379

11 .1 . Статическая системная структура . Транзакционные диаграммы Модули . . . Подсистемы Каркасы Библиотеки . 11 .2. Динамическая системная структура . Планирование . . . . . . . . . . Контексты . . . . . . . . . . . . . Взаимодействие между пространствами имен . Обработка исключений Зомби . . . . . . . . . . . Литература . . . . . •

381

381 382 382 384 385 386 388 388 388 389



Содержание

А.В. Взаимодействие с кодом С . . . А.8.1. Архитектурные аспекты . А.8.2. Языковая компоновка . . А.8.3. Вызов функций С++ ИЗ с А.8.4. Совместное использование заголовочных файлов в С и С++ . А.8.5. Импорт форматов данных С в С++ А.8.6. Импорт форматов данных С++ в С Упражнения . . . . . . . . . . . . . . Литература . . . . . . . . . . .

390 390 394 396 396 400 400 402 402

Приложение &. Программа Shapes

403

Приложение Г. Поразрядное копирование

416

.

.

.

Приложение В. Ссылочные возвращаемые значения операторов

414

Приложение Д. Иерархия геометрических фигур в символической идиоме 4 1 8 Приложение Е . Блочно-структурное программирование на С++ .

.

451

Е . 1 . Концепция блочно-структурного программирования . . . . . . . . . . Е.2. Основные строительные блоки структурного программирования на С++ . Е.3. Альтернативное решение с глубоким вложением областей видимости Е.4. П роблемы реализации . . . . . Упражнения . . . . . . . . . . . . . Код блочно-структурной видеоигры Литература . . . . . . . . . . . . .

451 452 456 460 460 461 466

Приложение Ж. Список терминов

4 67

.

Алфавитный указатель

.

472

Посвящается Сандре, Кристоферу, Лорелее и Эндрю Maй1'lly с любовью

Предисловие Эта книга написана для программистов, уже владеющих языком С

++

и желаю­

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

и

в том, как использовать язык для эффективного решения

задач программирования.

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

к

и

курсы для неопытных родителей не

воспитанию ребенка. Конечно, мы должны

освоить минимальные, самые необходимые навыки, и все же самое интересное, трудное и полезное выходит за рамки базовых знаний. Ни одна книга или •руко­ водство пользователя• не поможет понять, почему ваша трехлетняя дочь намаза­ ла зубной пастой голову своего младшего брата или зачем они оба положили свои носки в холодильник. То же самое относится к языкам программирования. Синтаксис языка до опре­ деленной степени формирует наше восприятие, но простое описание синтаксиса в •руководстве пользователя• станет всего лишь отправной точкой. Структура наших программ (а следовательно, определяется

cmwzeм

и

тех систем, которые мы строим) в основном

и идиомами, используемыми для выражения архитектур­

ных концепций. Стиль отличает истинное мастерство от простой удачи. Эффективный стиль вос­ питания ребенка, программирования и вообще всего на свете строится на основе личного опыта или опыта других. Программист, который умеет правильно свя­ зывать возможности языка программирования с потребностями приложения, пишет превосходные программы. Но чтобьi выйти на этот уровень, необходимо от правил и.механического запоминания перейти к идиомам и стилю, а в конеч­ ном счете - к концептуальным и структурным абстракциям. Правила, идиомы и концепции определяют структуру тех систем, которые мы создаем, - они предоставляют в наше распоряжение модель для их разработки. Модель декомпозиции задачи

и

формирования системы является парадигмой -

О книге

13

образцом для разбиения предметной области на части, с которыми удобно рабо­

С++ является •мультипарадигменным• языком. Программисты С исполь­ С++ как •улучшенный язык С•, а сторонники объектно-ориентированного подхода ценят в С++ полиморфизм. Однако эффективное и элегантное решение задачи обычно требует разных подходов.

тать.

зуют

Изучение языка программирования имеет много общего с изучением естествен­ ного языка. Знание базового синтаксиса позволяет программисту писать про­ стые процедуры и строить из них нетривиальные программы - подобно тому, как человек со словарным запасом в несколько тысяч иностранных слов спосо­ бен написать нетривиальный рассказ. Но настоящее мастерство - совсем другое дело. Рассказ может быть нетривиальным, но от этого он не станет читаться сна одном дыхании•, подчеркивая свободу владения языком его автора. Изучение синтаксиса и базовой семантики языка сродни 13-часовым курсам немецкого для начинающих: после прохождения таких курсов вы сможете заказать колба­ ски в ресторане, но для работы в Германии журналистом или поэтом их навер­ няка окажется недостаточно. Различие кроется в в синтаксисе

идиоматике языка. Например, С нет ничего, что бы определяло следующую конструкцию как

один из базовых •строительных блоков• программ:

wh i l e

(*cpl++

=

*ср2++):

Но если программист не знаком с этой конструкцией, его нельзя считать полно­ ценно владеющим

В

С.

программировании, как и в естественных языках, пригодность и выразитель­

ность языковых конструкций базируется на важных идиомах. Хорошие идиомы упрощают работу прикладного программиста, подобно тому как идиомы любого языка обогащают наше общение. Программные идиомы являются свыражения­ ми• семантики программирования, пригодными для мноrократного использова­ ния в том же смысле, в котором классы служат для многократного использова­ ния архитектурных решений и кода. Простые идиомы (вроде упомянутого цикла whiLe) всего лишь обеспечивают удобную запись, но редко играют сколь-нибудь заметную роль в архитектуре программы.

В

книге основное внимание уделяется

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

идиомы относительно сложны и запутанны; в них приходится учитывать много тонкостей. Но после того как вы их освоите, они станут удобным и мощным ин­ струментом программирования.

О к ни ге Предполагается, что читатель владеет базовым синтаксисом

С ++. Давая пред­

ставление о стиле и идиоматике языка, книга знакомит читателя с теми нетриви­ альными знаниями, которые опытные программисты

С++ получают на личном

14

П редисл овие

опыте. Она показывает, что С++ можно использовать и для разработки простых абстракций данных, и для полноценной реализации абстрактных типов данных, и для объектно-ориентированного программирования различных стилей. Кроме того, в ней исследуются идиомы, не поддерживаемые напрямую на базовом уровне С++, например функциональное и фреймовое программирование, а так­ же расширенные методы уборки мусора.

Стру ктура к ни г и Вместо •одномерного• подхода к описанию нетривиальных возможностей С+ + с упорядочением по языковым средствам книга описывает абстракции с точки зрения возможностей С++, необходимых для их поддержки. Каждая глава кни­ ги посвящена отдельному семейству таких идиом, причем идиомы, описанные в начале книги, используются как основа для изложения материала последую­ щих глав. В главе 1 приводится исторический обзор идиом С++. Из нее читатель узнает, почему появились идиомы, а также познакомится с разными вариантами интер­ претации идиом как внешних конструкций или как составляющих языка. В главе 2 представлены главные •строительные блоки• языка С++ - классы и функции классов. Хотя в этой главе в основном излагается базовый материал, к некоторым идиомам и терминам мы вернемся в последующих главах. Среди прочего рассматриваются системы контроля типов компилятора, их связь с поль­ зовательскими типами и классами с точки зрения архитектуры. Кроме того, рас­ сматриваются идиомы, связанные с использованием ключевого слова const. Глава 3 отдана идиомам, которые превращают классы в •полноценные• типы. В процессе эволюции С++ операции копирования и инициализации объектов постепенно автоматизировались, но для большинства нетривиальных классов программистам все равно приходится настраивать присваивание и конструктор по умолчанию. В этой главе описана общая схема такой настройки. Представ­ ленные идиомы называются каноническими формами; это означает, что они опре­ деляют принципы и стандарты, образующие базовую механику работы объектов. Помимо самой распространенной ортодоксальной канонической формы пред­ ставлены идиомы реализации подсчета ссылок в новых и существующих клас­ сах - первые идиомы в книге, выходящие за рамки базового синтаксиса С++. Одна из разновидностей подсчета ссылок - подсчет указателей - несколько ос­ лабляет связь С++ с аппаратурой (компьютером); вместо обычных указателей программист использует их более интеллектуальные объектные аналоги. В завер­ шение главы читатель .узнает, как отделить создание объекта от его инициализа­ ции. Для человека, хорошо знакомого с базовым языком С ++, такое разделение выглядит противоестественно, поскольку в С++ эти две операции тесно связаны. Необходимость их разделения возникает при проектировании драйверов уст­ ройств и систем с взаимозависимыми ресурсами.

Структура книги

Глава

4

посвящена наследованию, а в главе

5

15

наследование обогащается поли­

морфизмом в рамках знакомства с объектно-ориентированным программирова­ нием. Многие неопытные программисты

С++ склонны злоупотреблять насле­

до ванием и применять его при любом удобном случае. Хотя наследование требу­

ется в основном для поддержки объектной парадигмы, у него имеется другое,

никак не связанное с объектной парадигмой применение - многократное исполь­

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

В

главе

6

что же

С++ рассматриваются с точки зрения В этой главе мы попытаемся ответить на вопрос,

конструкции, стили и идиомы

архитектуры и проектирования.

ОЗНllfШют

классы на уровне приложения, который гораздо выше уровня

синтаксиса. Правильное понимание связей между архитектурными абстракциями приложения с одной стороны и классами/объектами его реализации с другой способствует появлению надежных легко развивающихся систем. Другим клю­ чевым фактором эволюции систем является расширение архитектуры за пределы конкретного приложения до предметной области в целом; важную часть главы

составляют принципы доменного анализа. Также здесь приводятся рекомендации по применению наследования - эта тема часто вызывает трудности у неопытных

программистов

С + + . Читатели, уже знакомые с объектно-ориентированным

проектированием, оценят рекомендации по превращению результатов проек­ тирования в код

С ++. Кроме того, в главе рассматривается инкапсуляция как

альтернатива наследованию в отношении многократного использования и по­ лиморфизма. Глава

7 посвящена теме многократного использования кода и архитектурных ре­

шений. Мы разберем четыре механизма многократного использования, обращая особое внимание на достоинства и недостатки •скороспелого наследования•. Представленные идиомы позволяют существенно сократить объем кода, сгене­ рированного параметризованными библиотеками типов с помощью шаблонов.

В оставшейся части книги рассматриваются нетривиальные рования, выходящие далеко за рамки традиционного языка

идиомы программи­

С++ . В главе 8 пред­

ставлены прототипы - специальные объекты, которым отводится роль классов

С++ и которые позволяют решать некоторые стандартные задачи (такие как имитация виртуальных конструкторов). Помимо этого прототипы закладывают основу

для

освоения более совершенных приемов проектирования, рассматри­

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

для

программирования на

С++, включая сильную типизацию и непосредственное управление памятью. Идио­ мы этой главы ближе к стилю Smalltalk и Lisp, отходя от канонов традиционного программирования на С ++ . Конечно, можно сказать, что тот, кто хочет програм­ мировать в стиле Smalltalk, должен программировать на Smalltalk. Действитель­ но, если вы хотите пользоваться всеми возможностями Smalltalk - пишите на

16

Предисловие

Smalltalk. И все же некоторая экзотичность идиом, представленных в этой главе, вовсе не означает, что они редко применяются на практике. Иногда нужно, чтобы небольшая часть системы могла опереться на гибкость и полиморфизм символических языков. В таких ситуациях приходится выходить за рамки фи­ лософии С++, оставаясь в границах семантики и системы контроля типов С++. В главе 9 приводится упорядоченное описание таких идиом, чтобы их в случае необходимости не приходилось создавать заново.

В главе 9 также представлены идиомы, обеспечивающие частичное обновление системы на стадии выполнения. Реализации этих идиом заведомо зависят от многих факторов, связанных с целевой платформой, поэтому материал призван познакомить читателя с технологическим уровнем, на котором должны решаться вопросы оперативной дозагрузки. Представленный пример достаточно типичен, а с точки технологии он не слишком сложен, но и не тривиален. Для любых платформ, кроме рабочих станций Sun, он потребует серьезной переработки, а в некоторых средах он вообще работать не будет. Но от этой идиомы не зави­ сит ни одна другая идиома в книге, поэтому читатель можно принять или от­ вергнуть ее, руководствуясь исключительно ее собственной ценностью. Глава 9 написана не для того, чтобы превратить С++ в Smalltalk; делать этого не стоит (да и не получится). Идиомы этой главы не столь безопасны по отношению к ти­ пам на стадии компиляции, а в общем случае менее эффективны, чем «традици­ онный• код С++; с другой стороны, они обладают большей гибкостью и автома­ тизируют управление памятью. Глава 10 посвящена динамическому множественному наследованию. По поводу множественного наследования в С++ до сих пор идут споры, поэтому его дина­ мическая разновидность была выделена в отдельную главу, чтобы не смешивать­ ся с материалом других глав. Статическое множественное наследование, описан­ ное в главе 5, тоже применяется на практике, но динамическое множественное наследование решает проблемы комбинаторного роста числа сочетаний классов. Этот подход приносит пользу во многих реальных программах, включая ре­ дакторы, системы автоматизации проектирования и управления базами данных. В последней главе объекты рассматриваются с высокоуровневой системной точ­ ки зрения. Уровень абстракции поднимается от масштаба классов С++ до более крупных и обобщенных единиц программной архитектуры и организации. Мы рассмотрим ряд важных системных аспектов, таких как планирование, обработка исключений и распределенные вычисления. Также здесь приводятся некоторые рекомендации по поводу модульности и многократного использования, связан­ ные с материалом глав 6 и 7. Попутно обсуждаются проблемы выбора структуры и сопровождения библиотек. В приложении А основные концепции С++ сравниваются с аналогами из языка С. Несомненно, многие читатели уже знакомы с этим материалом или смогут найти подробности в других книгах. Сравнение языков включено в книгу по двум при­ чинам. Во-первых, такой материал может послужить справочником на случай, если вам потребуется разобраться с какой-нибудь непростой конструкцией.

Благодарности

17

Во-вторых, поскольку стили программирования С и С++ рассматриваются с точ­ ки зрения проектирования, вы узнаете, как смешивать процедурный и объектно­ ориентированный код. Это особенно важно для программистов С++, работаю­ щих с готовым кодом С. Приводимые в книге примеры основаны на стандарте С++ версии 3.0. Они бы­ ли протестированы под управлением системы 3 АТ &Т USL Language System на многих аппаратных платформах, а также в других средах С++. Многие примеры тестировались в GNU С++ версии 1 .39.0 и Zortech С++ 2.0, хотя примеры, ори­ ентированные на специфические возможности версии 3.0, еще ожидают выхода обновленных версий этих компиляторов. В некоторых примерах используются библиотеки классов общего назначения для работы с множествами, отображения­ ми, списками и т. д. Многие разработчики предлагают эффективные версии таких бибщютек, однако достаточно функциональные версии, предназначенные для учеб­ ных целей, можно написать «с нуля•. Из приводимых примеров можно извлечь заготовки, а иногда и готовые реализации многих классов общего назначения.

Бл а года рн о сти Книга обязана своим существованием многим. Исходная идея принадлежит Крису Карсону ( Chris Carson) из Bell Laboratories; именно он как никто иной за­ служивает звания «крестного отuа• этой книrи. Автор благодарен ему за ини­ циативу и поддержку, оказанную в начале проекта. Книга создавалась под при­ смотром Кейт Уоллман (Keith Wollman), изобретательного и великодушного редактора, а также Хелен Уайт (Helen Wythe), руководителя проекта. Лорен Миньяччи (Lorraine Mignacci) внесла значительный вклад в материал главы 6, а обсуждение с ней материала других глав принесло огромную пользу. Книга многим обязана группе добросовестных и дотошных рецензентов, делившихся со мной своими идеями, среди которых Тим Борн (Tim Bom), Маргарет А. Эллис ( Margaret А. Ellis), Билл Хопкинс ( Bill Hopkins), Эндрю Кениг (Andrew Koenig), Стен Липман (Stan Lippman), Барбара Му (Barbara Моо), Бонни Прокопович (Bonnie Prokopowicz), Ларри Шутт (Larry Schutte) и Бьярн Страуструп (Bjarne Stroustrup). Алексис Лейтон ( Alexis Layton), Джим Эдкок Qim Adcock) и Майк Экройд ( Mike Ackroyd) дали множество советов и предложений относительно того, как сделать книгу более содержательной, и автор глубоко благодарен им за это. Масса других усовершенствований появилась благодаря рецензиям Мирона Абрамовича (Miron Abramovich), Мартина Кэррола ( Martin Carroll), Брайана Кернигана (Brian Kernighan), Эндрю Клейна ( Andrew Кlein), Дуга Макилроя (Doug Mcllroy), Денниса Манкла (Dennis Mancl), Уоррена Монтгомери (Warren Montgomeгy), Тома Мюллера (Tom Mueller), Анил Пал (Anil Pal), Пегги Куинн ( Peggy Quinn) и Бена Ровеньо (Ben Rovegno). Мэри Крэбб (Магу Crabb), Джин Оуэн Qean Owen) и Крис Скассел (Chris Scussel) поделились своим опытом в об­ ласти форматирования текста. Бретт Л. Шухерт (Brett L. Schuchert) и Стив Вино­ ски (Steve Vinoski) постарались сообщить обо всех ошибках, допущенных в первых

18

П редисл овие

материалах; их труд значительно повысил качество материала в последующих итерациях. Особая благодарность автора компании Corporate Desktop Services из Глен Эллин, штат Иллинойс, за ликвидацию ошибок в готовых гранках. Боль­ шое спасибо Джуди Марксхаузен Qudy Marxhausen) за консультации по специ­ альным вопросам. Автор благодарен своему начальству из АТ & Т за поддержку и за выделение времени и ресурсов для работы над книгой. Спасибо Полу Зислису (Paul Zislis) и Бену Ровеньо ( Ben Rovegno) за поддержку на первых порах, а также Уоррену Монтгомери (Warren Montgomery), Джеку Вангу Qack Wang) и Эрику Саммеру­ младшему (Eric Summer Jr.) за поддержку, идеи и терпение. Студенты многих ведущихся автором курсов помогли ему в выборе и оценке ма­ териалов, которые позднее вошли в книгу. Автор особенно благодарен студентам курсов С++, которые он вел в АТ&Т Bell Laboratories в Напервилле, штат Илли­ нойс, и Колумбусе, штат Огайо, в 1 989 году.

От издателя пере во да Ваши замечания, предложения, вопросы отправляйте по адресу электронной почты com [email protected] (издательство •Питер•, компьютерная редакция). Мы будем рады узнать ваше мнение! Все исходные тексты, приведенные в книге, вы сможете найти по адресу http:// www.piter.com/download. Подробную информацию о наших книгах вы найдете на веб-сайте издательства http://www.piter.com.

Глава 1 Введение Идиомы являются важной составляющей как естественных языков, так и языков программирования. Эта книга посвящена идиомам, делающим язык С++ более выразительным, и тому стилю программирования, который придает программе особенную структуру, или если хотите - «характер�>. Программировать на С++ можно и без идиом, однако именно стиль и идиомы в значительной степени оп­ ределяют выразительность, эффективность и эстетическую ценность программы. Они порождены многолетним опытом применения языка, многократными реше­ ниями одних и тех же задач и обменом информацией. Многие идиомы имеют довольно занятную, но поучительную историю. В данной главе две разновидности идиом С++ рассматриваются с точки зрения истории языка. К первой категории относятся идиомы, непосредственно повлиявшие на развитие С++, а ко второй - идиомы поддержки объектных конструкций, выхо­ дящих за пределы базовой модели С++.

1 1. С++ ка к ра зв ив ающий с я яз ык .

Язык С++ появился в нужное время и в нужном месте. Его предок, «С с классами�. родился в 1 980 году (также ставшем поворотной точкой в истории Smalltalk), а его росту способствовало бурное распространение «объектного феномена� в на­ чале 1 980-х годов. Язык развивался по мере осмысления объектной парадигмы отраслью. К лету 1983 года язык «С с классами� прочно вошел в академический и исследовательский мир. Так начался диалог, который продолжает влиять на формирование языка и в наши дни. Стремясь к максимальной гибкости и возможности развития, С++ эффективно использовал опыт собственных пользователей и достижения объектно-ориенти­ рованного программирования в других компьютерных технологиях. Языки про­ граммирования открывали новые горизонты в множественном наследовании, методах инкапсуляции и в других важных областях. Поскольку первые попытки стандартизации С++ начались только в 1989 году, ничто не мешало проектиров­ щикам формировать язык на базе постоянно расширяющегося опыта объектно­ ориентированного программирования. Конкретным примером влияния опыта разработки на развитие языка служит за­ щищенная (protected) область видимости. Ранее для того чтобы предоставить

20

Глава 1 . Введение

производному классу доступ, нарушающий инкапсуляцию базового класса, обыч­ но использовались дружественные (friend) отношения. А после применения клю­ чевого слова friend вполне естественным стало включение в язык поддержки ключевого слова protected. Обратная связь с сообществом пользователей помог­ ла усовершенствовать модель ограничения доступа в С++ и сделать ее одной из лучших для своего времени. Аналогичные события проложили дорогу поддерж­ ке множественного наследования и шаблонов, а также улучшению множества второстепенных деталей языка. «Роль С++ в истории�.. в значительной мере обусловлена происхождением этого языка от С. Сегодня С является одним из самых распространенных языков про­ граммирования, у него почти нет конкурентов в широкой области системного программирования. Программы, написанные на С, отличаются эффективностью и тесной связью с оборудованием; то же самое должно быть характерно и для С++. Среди других улучшений «классического языка С» С++ обеспечивает про­ верку типов на стадии компиляции. Данное обстоятельство в сочетании с эф­ фективностью ставит С++ на особое место среди распространенных объектно­ ориентированных языков. Однако эти два фактора, отразившихся на развитии языка, - развитие объ­ ектной парадигмы и влияние неизменно популярного языка С - иногда вступа­ ли в противоречие. Разработчики С++ никогда не стремились (и не стремятся) сделать его языком «на все случаи жизни�.., поэтому им приходилось искать компромисс между традициями С и новыми направлениями, прокладываемыми языками вроде Smalltalk и Flavors. В большинстве случаев предпочтение отдава­ лось зрелым или хорошо знакомым технологиям; хотя в культурном контексте С++ является порождением эпохи Smalltalk, его корни уходят к языкам Simula ( 1967), Algol 68 ( 1968) и С ( 1 973).

У этих решений был один важный аспект: где скрывать сложность, связанную с реализацией нетривиальных возможностей? Ее размещение в компиляторе упрощает разработку для пользователя: компилятор может автоматизировать решение повседневных задач, связанных с управлением памятью, инициализа­ цией, зачисткой, преобразованием типов, вводом-выводом и т. д. Один из прин­ ципов, определявших направление эволюции С++, гласил, что компилятор не следует упрощать, если это создает неудобства для пользователя. Но если ком­ пилятор чрезмерно перегружается интеллектом, возникает дилемма: либо поль­ зователь ограничивается одной моделью некоторого аспекта программирования ( «диктаторский» язык), либо сам язык приходится расширять для выражения всего богатства альтернатив («анархический�.. язык).

1 . 2 . Р е ш ение пр облем ы сл ожн ости пр и п о м ощ и ИДИОМ Чтобы предоставить в распоряжение пользователей всю мощь высокоуровне­ вых языков, но обойтись без жесткости, присущей «тираническим» языкам, для С++ были разработаны «стандартные блоки�.., которые могли использоваться

1 .2. Решение проблемы сложности при помощ и идиом

21

программистами для реализации собственных моделей вычислений. Функцио­ нальность, в знач�лъной степени зависевшая от поддержки со стороны компи­ лятора (например, преобразования типов), была интегрирована непосредственно в язык. С другой стороны, другие •языковые возможности•, в том числе в основ­ ном модель управления памятью, ввод-вывод, а также отчасти инициализация данных объектов, в сложных приложениях могут контролироваться пользова­ телем. Многие стандартные задачи программирования становятся идиомати ­ ческими; конструкции С + + используются для выражения функциональности, не входящей непосредственно в язык, и создают иллюзию того, что она является частью языка. Хорошим примером служит идиома копирования объектов и управления памя­ тью. Когда объект присваивается другому объекту или передается в параметре функции, компилятор автоматически генерирует код присваивания или инициа­ лизации полей нового объекта на основании полей исходного объекта по прин­ ципу •один в один•. Для больших объектов такое решение может оказаться неэффективным, к тому же оно создает аномалии в поведении указателей, хра­ нящихся в переменных класса: компилятор по умолчанию использует поверхно­ стное копирование, тогда как в отдельных случаях требуется глубокое копирова­ ние. Обе проблемы решаются при помощи идиом подсчета ссылок, разделяющей класс на две части: первая (манипулятор) обеспечивает управление памятью, а вторая (тело) решает прикладные задачи. Класс-манипулятор использует пре­ доставленные программистом версии кода присваивания, инициализации и за­ чистки объектов, которые автоматически применяются для управления памятью по мере необходимости. Эта идиома описана в главе 3. Другой пример - ввод-вывод, который (как и в С) не является частью языка. Но на практике операторы > часто перегружаются для поддержки идиомы потоковых объектов ввода и вывода. Все классы поддерживают ввод-вывод по единой схеме. В данном случае сложность сосредоточена не в компиляторе и не в пользовательском приложении, а в библиотеке общего назначения. Чтобы по­ лучить доступ к библиотеке, программист директивой #i nctude включает в про­ грамму файл, который перегружает операторы и создает впечатление, будто опе­ раторы сдвига изначально были спроектированы в С++ для ввода и вывода. Ключ к успеху этих идиом - их прозрачность для конечного пользователя, то есть полная иллюзия, будто они являются частью самого языка. Такие идиомы позволяют сочетать всесторонность и мощь •анархических• языков с прозрачно­ стью •диктаторских• языков. Пользователь не ограничивается единой моделью управления памятью, которая абсолютно универсальна, но в отдельных слу­ чаях неприемлема из-за малой эффективности; он может выбрать нужное реше­ ние среди целого спектра альтернатив. Все компромиссы между эффективно­ стью, совместимостью с интерфейсом С и удобством использования находятся под контролем программиста. Более того: те аспекты, которые в С++ не опреде­ ляются, обеспечивают гибкость, причем этой гибкости было бы трудно добиться в языках с более высокой интеграцией моделей управления памятью. Язык С++ богат не столько возможностями, сколько мета-возможностям и простыми -

22

Глава

1.

В ведение

стандартными блоками, объединение которых создает новую целостную форму, превосходящую по своей мощи сумму отдельных составляющих. Эти мета-возможности порождены не единым грандиозным замыслом, а практи­ ческим опытом, полученным сначала в образовательных и исследовательских кругах, а затем в ходе пробных разработок. С++ стал языком с •инструментари­ ем• вспомогательных средств для определения семантики стандартных задач программирования. Некоторые языковые средства (скажем, конструкторы или перегруженные операторы) обладают самостоятельной ценностью, но являются низкоуровневыми конструкциями. Только идиомы, объединяющие конструкторы с перегрузкой операторов присваивания, обеспечивают настоящую мощь высо­ коуровневого языка. Многие идиомы, как и части языковых конструкций, были разработаны пользователями ранних версий С++. Конечным итогом эволюции языка С+ в свете объектно-ориентированных приложений, а также эволюции приложений в области использования новых возможностей С++. стало опреде­ ление языка в спецификации [ 1 ]. Эта спецификация достаточно хорошо прорабо­ тана, чтобы послужить основой для формальной стандартизации, а приведенные в ней идиомы используются настолько часто, что могут считаться общеприняты­ ми для повседневного программирования на С++. Случайные пользователи С++ и те, кто изучал С++ как •улучшенный язык С•, могут пройти мимо этих идиом при самостоятельном освоении С++. Язык про­ граммирования не заставляет вас пользоваться идиомами; он лишь предоставля­ ет основные конструкции, благодаря которым идиомы становятся доступными. В этом смысле идиомы нетривиальны, поскольку они выходят далеко за пределы того, что требует (или просто поощряет) язык. Главная цель настоящей книги познакомить читателя с классическими идиомами С++ как с самостоятельными инструментами программирования. Идиомы занимают в книге центральное ме­ сто, а языковые средства их поддержки описываются по мере необходимости (а не наоборот). Подобная структура поможет неопытным программистам С++ быстрее освоить идиоматические конструкции, а программисты, хорошо знако­ мые с синтаксисом, смогут перейти к более мощной семантике.

1 . 3 . О бъ екты

для

90-х

Упомянутые ранее классические идиомы С++ берут свое начало в происхож­ дении от языка С. Другой класс идиом появился благодаря росту численности и квалификации сообщества пользователей С++ и не без влияния со стороны других языков программирования. Сегодня С++ сохраняет совместимость с С на культурном уровне, а объектная ориентация С++ сильна ровно настолько, чтобы не нарушить эту совместимость. С++ занимает важную нишу, внося объектно­ ориентированные конструкции в традиционную культуру разработки С с ее ком­ пиляторами, операционными системами и системами управления реального вре­ мени. С++ обладает богатой поддержкой объектно-ориентированного програм­ мирования на уровне языка, хотя мы видели, что большая часть этой поддержки

1 .4. П роектирование и язык

23

обусловлена соглашениями, стилем и идиомами. Многие идиомы поддержива­ ются на уровне мета-возможностей языка. По мере развития отрасли репутация �языка объектно-ориентированного про­ граммирования• все выше поднимала планку требований. Язык С++ рос и раз­ вивался, но сообщество пользователей развивалось еще быстрее. Пользователи обнаружили, что для новоизобретенных применений объектно-ориентированных технологий им нужны все более мощные конструкции. В качестве примера мож­ но привести потребность в виртуальных конструкторах, для поддержки которых необходима степень полиморфизма, более характерная для языков со слабой ти­ пизацией, нежели для модели сильной типизации С++. Слабая, или динамиче­ ская, типизация имитируется в С ++ введением дополнительных уровней абст­ ракции и идиоматических конструкций. Сложность большинства таких идиом инкапсулируется в классах, которые их используют, - с точки зрения пользовате­ ля приложения они ничем не отличаются от обычных классов, хотя и проявляют гибкость, характерную для языков программирования более высокого уровня.

Идиомы, разработанные для поддержки этих нетривиальных возможностей, ме­ нее �идиоматичны•, нежели идиомы, происходящие от развития самого языка С++; это означает, что они поддерживаются меньшим количеством мета-возмож­ ностей. Данный факт в значительной степени обусловлен происхождением С++ от С. С++ это прежде всего С и кое-что еще, поэтому С++ не может одновре­ менно воплотить философии проектирования, характерные для С и Smalltalk. Прямая поддержка моделей типов Smalltalk или Flavors потребовала бы принци­ пиальных изменений в философии, и как следствие - отхода от объектной па­ радигмы С++. При этом снова бы возникла дилемма между •диктаторскими• и �анархическими• языками. Опыт показал, что идиомы могут эффективно ис­ пользоваться и без специальной поддержки со стороны языка, с инкапсуляцией подробностей реализации в закрытых членах классов. -

Отсюда следует вторая цель книги: представление идиом С++, которые многими считаются необходимыми для логического завершения объектной модели С++ и набора его классических идиом. Однако не стоит полагать, что эти идиомы пред­ ставляют интерес не только для тех, кто желает эмулировать конструкции Smalltalk на С++. Они предназначены для решения архитектурных и технических задач, типичных для любой программы С++ с сильной объектной ориентацией.

1 4 П р о е кти р овани е и язык .

.

С развитием языка развивалось также понимание связи между объектно-ориен­ тированной архитектурой и объектно-ориентированным языком программиро­ вания в сообществе программистов. На ранней стадии объектная парадигма в ос­ новном использовалась как инструмент для получения доступа к абстракции, инкапсуляции и гибкости, выходящей за рамки процедурных языков. Позднее объекты (или точнее сущности, их аналоги в предметной области проектиро­ вания) стали рассматриваться как архитектурные конструкции. -

24

Глава 1 . Введе н и е

Язык С++ также прошел по этому эволюционному пути. Концепция класса С++ изначально проектировалась для отражения архитектурных конструкций, а мно­ гократное использование кода (посредством закрытого наследования) считалось дополнительным, но второстепенным обстоятельством. По мере накопления опыта модель выделения подтипов С++ также развивалась для отражения отно­ шений между объектами и классами, полученными в результате объектно-ориен­ тированного анализа и проектирования. Модель С++ с мощной проверкой типов на стадии компиляции С++ помогает защититься от распространенных ошибок при наследовании. Проектирование не является основной темой книги, хотя оно и неоднократно упоминается в тексте. Но проектирование и программирование тесно связаны в объектной парадигме, и хотя книга вроде бы посвящена «программированию1>, мы не можем не обращать внимания на важность выбора архитектурных ре­ шений. В главе 6 рассматривается процесс проектирования с точки зрения С++; в главе 7 говорится о многократном использовании кода, что в большей мере от­ носится к факторам проектирования; наконец, в главе 1 1 описаны системные проблемы, стоящие за «поиском объектов�> и правильным применением наследо­ вания. Эти вопросы, как и синтаксис языка, семантика и идиомы, появились в результате многолетнего опыта практического программирования. Язык С++ находится на пике популярности в плане объектно-ориентированного проектирования, и, как уже было отмечено, его успех в значительной степени обусловлен этим обстоятельством. К сожалению, выбирая объектно-ориентиро­ ванные решения, программисты слишком часто забывают об альтернативах. Другие парадигмы не умерли и не утратили актуальности. Они по-прежнему мо­ гут применяться, если толковый проектировщик поймет, что они хорошо подхо­ дят для конкретной задачи. Многие идиомы С ++, представленные в следующих главах, практически не связаны с объектной парадигмой. Например, функторы (см. 5.6), методика компоновки подсистем (см. главу 1 1 ) параметрические кон­ струкции (см. главу 7) остаются нетривиальными применениями С++ в контек­ сте основных положений и духа языка. ,

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

Л и терату р а Ellis, Margaret А., Stroustrup В. «The Annotated С++ Reference Manuali> . Reading Mass: Addison-Wesley, 1990.

Гл ава 2

Абст р ак ц и я и а бст р актн ы е ти п ы дан н ы х Типизованные языки программирования появились в 1 950-х годах не как 4Наи­ лучшие на сегодняшний день>) инструменты качественного проектирования или структурирования систем, а лишь как средство генерирования эффеюпивного объ­ ектного кода из исходного текста программ. Постепенно отношение к системам типизации стало меняться, и в них усмотрели самостоятельную ценность для про­ верки типов и типовой безопасности интерфейсов. В 1 960-х годах типы разви­ лись до такой степени, что стали рассматриваться наравне с процедурами среди основных компонентов программ. Это привело к появлению новых более абст­ рактных синтаксических концепций и конструкций в языках программирования. +

А бстраюпные типы даниых (АТД) определяют интерфейс к абстракции данных без указания подробностей реализации. Абстрактный тип данных может иметь несколько реализаций для разных потребностей или для работы в раз­ ных средах.

+ Классы реализуют абстрактные типы данных. Поддержка классов в языках программирования впервые появилась в языке Simula 67. +

Улучшенные системы типизации позволяют АТД занять свое законное место среди языковых абстракций. Перегрузка операторов и другие усовершенство­ вания подняли мощь и выразительность АТД на новый уровень. Эти средства впервые появились в языках Bliss, Algol 68 и Simula, и были позднее усовер­ шенствованы в Mesa и Clu.

В С++ встроенные типы (int, short, Long, fLoat, double, Long double, char и указатели) обеспечивают проверку интерфейса на стадии компиляции и эффективность сгенерированного кода. Перечисленные абстракции существуют в любой среде С++, вы можете сразу задействовать их в качестве счетчиков и индексов или воспользоваться ими как строительными блоками для числового анализа, об­ работки символьной информации и т. д. Классы С++ позволяют объединить ло­ гически связанные данные с операторами и функциями и тем самым создать программную единицу, поддерживаемую на уровне языка. По своей мощи и удоб­ ству классы не уступают встроенным типам: они могут использоваться для объяв­ ления или динамического создания новых экземпляров, на них распространяется

26

Глава

2.

Абстракция и абстрактн ые типы данных

механизм проверки типов. Интерфейс класса отделяется от его реализации и слу­ жит тем же целям, что и спецификация АТД. Впрочем, это не означает, что экземпляры классов так же универсальны, как объекты встроенных типов int и double. Проектировщик класса может опреде­ лить конструкции, интегрирующие новый класс в систему типов компилятора. Например, компилятор можно •научить• выполнять преобразование между встроенным типом double и экземпляром пользовательского класса CompLex. Его также можно •научить• генерировать эффективный код создания, присваивания и копирования экземпляров класса. Пеной дополнительных усилий (большей частью по готовым •рецептам•) класс преобразуется в пользовательский тип, который мы называем конкретным типом данных. В результате объекты класса объявляются, присваиваются и передаются в параметрах функций точно так же, как обычные встроенные типы С и С++. Класс приносит пользу как абстракция из области проектирования и реализации даже без механизмов его преобразования в конкретный тип данных. Преобразо­ вание в конкретный тип данных проходит относительно легко, а дополнительные удобства конкретных типов данных обычно оправдывают затраченные усилия. Но и •простой• класс является хорошей отправной точкой, а приведенная в этой главе информация станет хорошим подспорьем в построении ваших собственных нетривиальных классов. Тема данной главы развивается в главе 3, посвященной преобразованию классов в конкретные типы данных. В начале этой главы рассматривается концепция класса вероятно, основопола­ гающим механизмом абстракции в С++. Мы разберемся, как функции и данные объединяются в классе способом, вроде бы противоречащим традиционному подходу С. Далее описаны конструкции корректной инициализации и уничто­ жения переменных классов. Кроме того, в этой главе будут представлены другие конструкции и концепции С++, часто ассоциируемые с классами: подставляе­ мые функции, константные функции и константные объекты классов, статиче­ ские функции классов и указатели на функции классов. -

2 . 1 . Кл а ссы Конструкция С++ cLass тесно связана с конструкцией С struct. С++ поддержива­ ет объявления struct и сохраняет их семантику в языке С. Ключевое слово cLass обычно подчеркивает, что структура задействуется как пользовательский тип данных, а не как простой агрегат данных. А именно, при группировке функций и данных применяется ключевое слово cLass, а при группировке одних данных ключевое слово struct. Функции, сгруппированные в классе, называются функ ­ циями класса; они •принадлежат• содержащему их классу. В С++ структуры (struct), как и классы (cLass ) тоже могут содержать функции, но обычно они рас­ сматриваются в традиционном смысле С как простые записи данных. Формально классы и структуры в С++ различаются только по уровню доступа, принятому по умолчанию. В листинге 2 . 1 показано сходство между структурами С и класса­ ми С++ при использовании в механизмах инкапсуляции данных. ,

2. 1 . Классы

27

Листинг 2. 1 . Аналогия между структурами С и классами С ++ Код С :

Код С++ : struct { char name[ NAМE S I ZE ] : char i d [ I D_S I ZE J : short yea rs Experi ence : i nt gender : l : unsi gned cha r dependents : З : unsi gned char exempt i ons : 4 : foo :

struct

cl ass { puЬl i c : char name[ NAME S I ZE ] : cha r i d [ I D S I ZE ] : s hort yea rsExperi ence : i nt gender : l : unsi gned cha r dependents : З : unsi gned cha r exempti ons : 4 : ba r :

st ruct { char name [ NAМE S I ZE J : char i d [ I D_S I ZE ] : short yea rs Experi ence : i nt gender : l : uns i gned char dependent s : З : unsi gned char exempt i ons : 4 : foo :

char name[ NAME S I ZE ] : char i d [ I D S I ZE J : short yea rsExperi ence : i nt gender : l : unsi gned char dependent s : З : unsi gned cha r exempt i ons : 4 : ba r :

В отличие от С, С++ вводит в программу новый тип для каждой помеченной ( tagged) структуры или класса, словно после объявления в программе следует директива typedef: Код С :

st ruct Emplabel { char name[ NAМE S I ZE J : char i d [ I D_SI ZE J : i nt gender : l : foo :

Код С++ : st ruct Empl oyee { char name[ NAМE_S I ZE ] : char i d [ I D_S I ZE ] : i nt gender : 1 :

foo :

typedef struct Emplabel Empl oyee : st ruct Emplabel fred : Empl oyee l i sa :

st ruct Empl oyee fred : Empl oyee l i sa :

В С++ пометка и тип являются синонимами, тогда как в С они считаются раз­ ными именами. Структуры С++ ведут себя как классы, все члены которых открыты по умолча­ нию, тогда как члены классов по умолчанию являются закрытыми и доступны только внутри самого класса. Уровень доступа изменяется при помощи ключе­ вых слов private, public и protected. Одно из этих трех ключевых слов, за которым следует двоеточие, определяет уровень доступа всех последующих членов до конца класса или до следующего квалификатора. Уровень доступа по умолча­ нию действует до первого квалификатора. + Открытые (public) члены объекта доступны для всех функций, для кото­ рых доступен класс данного объекта, а также сам объект по правилам области видимости.

28

Глава

2.

Абстракция и абстрактные типы данных

+ Защищенные ( protected) члены объекта доступны только для функций класса

+

данного объекта или его производных классов. Объект производного класса не может обращаться к защищенным полям объекта базового класса и даже объекта собственноzо базового класса. Функции производных классов могут обращаться к защищенным членам только базовой части объекта своего класса. Закрытые ( private) члены объекта доступны только для функций класса этого объекта.

Инкапсуляцию, обеспечиваемую ключевыми словами private и protected, при желании можно нарушить при помощи механизма дружественных отношений (friend). Тема защиты подробно рассматривается в главе 3. Хотя классы можно рассматривать как синтаксические расширения структур, мы обычно рассматриваем их как средство передачи информации об архитектуре программы. Структуры применяются для упаковки логически связанных данных в любой методике структуризации. Например, при функциональной декомпози­ ции (см. приложение Е) каждый уровень функций обладает собственными дан­ ными, и структуры могут использоваться для объединения взаимосвязанных данных (или для создания нескольких группировок взаимосвязанных данных) на определенном уровне. Структуры данных, сгенерированные при функцио­ нальной декомпозиции, чаще отражают не состояние абстракций в приложении, а скорее некие уловки, примененные в реализации. Классы С++ обладают двумя возможностями, выходящими за пределы структур С: типизацией и абстракцией. Классы применяются для создания новых пользо ­ вательских типов в программах С - тех, которые мы называем абстрактными типами данных. Классы используются как компоненты при создании типов, по­ скольку наряду с представлением набора данных в них сохраняется информация о его поведении. Для примера рассмотрим класс комплексных чисел Com ptex:

c l ass Compl ex { puЫ i c : fri end Compl ex& operator+ ( douЫ e . const Compl ex& ) : Compl ex& operator+ ( const Compl ex& ) const : fri end Compl ex& operator - ( douЫ e . const Compl ex& ) : Compl ex& operator - ( const Compl ex& ) const : fri end Compl ex& operator* ( douЫ e . const Compl ex& ) : Compl ex& operator*( const Compl ex& ) const : fri end Compl ex& operator/ ( douЫ e . const Compl ex& ) : Compl ex& operator/ ( const Compl ex& ) const : douЫ e rpa rt ( ) const : douЫ e i pa rt ( ) const : Compl ex( douЫ e=O . O . douЬl e=0 . 0 ) : pri vate : douЫ e rea l Pa rt . i magi n a ryPa rt :

}:

Здесь класс отражает не только логическую связь между переменными reaLPart и i magi naryPart, на что способна обычная структура, но и связь между представле-

2.2.

Объектная инверсия

29

нием комплексного числа и его поведением (то есть операциями). Конструкции языка С не позволяют хранить эти связи в удобном и интуитивно понятном ви­ де, а лишь делают возможным их представление на уровне соглашений. Классы помогают программисту использовать программные конструкции более высокого уровня. чем отдельно взятые функции или структуры. Эти конструк­ ции служат абстракциями, а абстрагируемые сущности обычно тесно связаны со спецификой программируемого приложения. Применение классов выделяет отображение абстракций области приложения в абстракции области решения, на что простые структуры С не способны. Хороший проектировщик может мыслить в терминах абстрактных типов данных и программировать в терминах структур и функций. В это�I случае структура хо­ рошей программы С++ соответствует структуре хорошей програ.'1мы С, но как бы «выворачивается наизнанку�. Это явление, называемое обьектной инверсией, рассматривается в следующем разделе.

2 . 2 . Объ е кт ная инв ер сия В о многих программах имеются библиотеки и модули, построенные н а основе некоторой структуры данных. Для примера возьме�I простой стек. Данные стека хранятся в структуре, и с этой структурой связываются несколько процедур, ра­ ботающих с объявленными в ней переменными. В С++ все наоборот. Функции рассматриваются как принадлежащие структуре из-за тесной связи со структурой и ее данными. Вместо того чтобы передавать структуру Stack всем функциям, работающим с ней, мы сохраняем эти функции внутри структуры вместе с данны�ш. Такие функции называются функциями, или операторами, класса. В друптх языках (и прежде всего в Smalltalk) они называ­ ются методами. В листинге 2 . 2 приведен прш.1ер стека, оформленный в этом стиле. (Хотя этот фрагмент работоспособен, он слишком упрощен и приводится только в учебных целях. Стек имеет фиксированный размер, его элементы одно­ родны и относятся только к одному конкретному типу Long.) Листинг 2. 2. Эквивалентные реализации стека на С и С++ Код С :

#defi ne STACK_S I Z E 1 0 st ruct Stack { 1 ong i tems [STACK_S I ZE J : i nt sp : }: voi d Stack_i ni t i a 1 i ze ( s ) st ruct Stack *s : {

Код С++ : const i nt

STACK_S I ZE

=

10 :

c l ass Stack { pri vate : l ong i tems [ STACK_S I ZE J : i nt sp : puЫ i c : voi d i ni t 1 a l i ze ( ) : 1 ong top ( ) const : 1 ong рор ( ) : voi d push ( 1 ong ) :

продолжение.:Р

30

Глава

2.

Абстракция и абстрактные типы данных

Листинг 2 . 2 (продолжение)

s ->sp

=

-1:

}: voi d Stack : : i ni ti a l i ze ( ) sp = - 1 :

l ong Stack_top ( s ) struct Stack *s : { return s - >i tems [ s ->sp] :

l ong Stack_pop ( s ) struct Stack *s : { return s ->i tems [ s ->sp- - ] :

voi d Stack_push ( s . i ) struct Stack *s : l ong i : { s - >i tems [ ++s ->sp] = i :

i nt ma i n ( ) { struct Stack q : l ong i : Stack_i ni ti a l i ze ( &q ) : Stack_pus h < &q . l ) : i = Stack_top ( &q ) : Stack_pop ( &q ) :

l ong Stack : : top ( ) const return i tems [ sp] :

l ong Stack : : рор ( ) { return i tems [ s p - - J :

voi d Stack : : push ( l ong i ) i tems [++sp] = i :

i nt ma i n ( ) { Stack q : q . i ni ti a l i ze ( ) : q . push ( l ) : l ong i = q . top ( ) : Q . pop O :

Обратите внимание: в С++ функции класса (в частности, функция push класса Stack, обозначаемая как Stack::push) не разыменовывают указатель на структуру и вообще обходятся без явных обращений к объекту класса, соответствующему структуре в реализации С. Это объясняется тем, что в С++ для классов создается новая область видимости, и функции могут рассматриваться как существую­ щие внутри экземпляров данной структуры. Каждое объявление •переменной• класса Stack выделяет блок памяти (как для структуры). Например, экземпляр q в функции mai n программы С++ хранится в стеке времени выполнения. Этот эк­ земпляр называется обьектом класса Stack. Функции класса Stack применяются к объектам этого класса (например, q .i nitiatize() или q.push(1)). При всех упоми­ наниях членов класса Stack в функциях этого класса используются данные того объекта, для которого была вызвана функция (в нашем примере - это объект q).

2.3.

Конструкторы

и

деструкторы

31

Функции класса обращаются к данным своего объекта через замаскированный параметр с именем this - указатель на объект, для которого была вызвана функ­ ция. Хотя параметр this передается незаметно для программиста, к нему можно обращаться в программе. Параметр this служит тем же целям, что и указатель на структуру Stack в функциях С; он используется так часто, что компилятор авто­ матически организует его передачу. Более того, компилятор автоматически при­ соединяет квалификатор this-> ко всем обращениям к членам класса в функциях этого класса. Например, внутри Stack::i nitiatize выражение sp=-1 в действительно­ сти означает this->sp=-1. Явное разыменование this допустимо, но слишком длин­ но и обычно излишне. Следующие два фрагмента кода С++ эквивалентны: voi d Stack : : i ni ti a l i ze ( ) sp -1: } l ong Stack : : top ( ) const return i tems [ sp] : =

voi d Stack : : i ni t i a l i ze ( ) thi s ->sp = - 1 :

}

l ong Stack : : top ( ) const { return thi s - >i tems [thi s - >sp] :

Обратите внимание на обозначение const в функции Stack: :top. Оно утверждает, что функция не изменяет того объекта, для которого она вызывается (то есть объекта, на который указывает параметр this ). Конструкция const подробнее рас­ сматривается в 2.9.

2 . 3 . Кон структо р ы и дест рукто р ы При проектировании языка С++ были поставлены две важные цели: + переменные всегда должны инициализироваться автоматически; + классы должны контролировать операции с памятью для своих объектов.

Поддержка этих концепций на уровне языка позволяет авторам классов управ­ лять выделением и освобождением памяти, чтобы избавить пользователей от этих хлопот. Для инициализации 11 уннчтожения объектов определяются специ­ альные функции, называемые соответственно кон.стру'Кmором и деструктором.

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

32

Глава

2.

Абстракция и абстрактные ти п ы данных

Конструкторы и деструкторы также используют примитивы управления памя­ тью new и deLete для инициализации и уничтожения динамически созданных объектов. О братите внимание: конструкторы и деструкторы являются функциями класса, которые не имеют возвращаемого значения. Дело в том, что они практически ни­ когда не вызываются напрямую, а компилятор генерирует их вызовы по мере не­ обходимости. Казалось бы, для конструктора было бы естественно •возвращать• объект при завершении своей работы. Но конструктор с таким же успехом мо­ жет вызываться как для инициализации существующего блока памяти, зарезер­ вированного для объекта (например, глобального), так и для инициализации ди­ намически созданных объектов (например, при использовании оператора new). Получение конструктором адреса объекта и его возврат производится компиля­ тором незаметно для программиста. Впрочем, нестандартные языковые конст­ рукции позволяют вызывать конструкторы и деструкторы для заданных объек­ тов, что может быть полезно в особых ситуациях с нетривиальным управлением памятью, описанных в главе 3. И все же на практике чаще встречаются автомати­ чески сгенерированные вызовы.

Если класс используется для простого объединения или абстрагирования дан­ ных, определять для него конструкторы или деструкторы не обязательно. Их нужно определять только для классов с динамической внутренней структу­ рой, которые ведут себя как естественные типы языков программирования. Так, в приведенном ранее примере стека нет ни конструкторов, ни деструкторов; класс содержит функцию i nitiaLize, которая должна явно вызываться в программе для приведения объекта в нормальное состояние. Эту функцию можно заме­ нить конструктором, чтобы класс Stack сам инициализировал свою внутреннюю структуру, а пользователи класса Stack избавились от лишних хлопот. Сначала мы изменяем объявление класса:

cl ass Stack { puЬl i c : Stack C ) : l ong top C ) const : l ong рор ( ) : voi d push C l ong ) : pri vate : l ong i tems [ l O ] : i nt sp : }: А затем заменяем функцию i nitiaLize конструктором:

Stack : : Stack С ) sp = - 1 : Теперь функцию initiaLize не нужно вызывать в программе main; конструктор вы­ зывается автоматически при создании объекта Stack, как и при объявлении q.

2.3.

Конструкторы и деструкторы

33

Таким образом, вызов initiaLize просто исключается из mai n , и программа продол­ жает нормально работать. Перейдем к более общей реализации стека, которая поближе познакомит нас с кон­ структорами, а также даст начальное представление о деструкторах (листинг 2.3). Листинг 2.3. Объявление стека с конструкторами и деструктором

const i nt STACK_S I ZE cl a s s Stack { puЫ i c : Stac k ( ) : Stack ( i nt ) : -Stack ( ) : l ong top( ) const : l ong рор ( ) : voi d push ( l ong ) : pri vate : l ong *i tems : i nt sp :

=

10 :

1 1 Первый конструктор 11 Второй конструктор 11 Деструктор

}:

В исходной версии класс Stack имел фиксированный размер в десять элементов; мы хотим, чтобы в момент объявления можно было создать стек с большим или меньшим количеством элементов. Для этого в классе определяется конструктор с параметром, в котором передается размер стека. Конструктор динамически вы­ деляет память оператором С++ new: Stack : : Stack ( i nt s i ze ) { i tems = new l ong [ s i ze] : Так как конструктор динамически выделяет память, в программу нужно включить деструктор для освобождения памяти при уничтожении объекта. Для этого де­ структор применяет оператор deLete к указателю на динамически выделенный блок: Stack : : -Stack ( ) { del ete [ ] i tems : Квадратные скобки в синтаксисе deLete [] items сообщают компилятору о том, что items указывает на вектор типа Long, а не на одиночный динамически созданный экземпляр Long. Обратите внимание: новая версия класса содержит два конструктора с одинаковы­ ми именами! Имя Stack •наполняется• двумя смыслами - это называется пере­ грузкой. Наличие одноименных функций не создает никакой путаницы или дву­ смысленности; если по контексту должен вызываться конструктор с аргументом, будет вызван конструктор с аргументом. В противном случае вызывается другой конструктор, который создает стек со стандартным размером в 1 О элементов. Компилятор автоматически выбирает нужную функцию (вскоре вы поймете, как

34

Глава

2.

Абстракция и абстрактные типы данных

это делается). Реализация нового объявления класса Stack, приведенного в лис­ тинге 2.3, представлена в листинге 2.4. Листинr 2.4. Реализация стека

с конструкторами и деструктором

Stack : : Stack ( ) { i tems new l ong [ STACK-S I Z E J : sp -1: =

=

Stack : : Stack ( i nt s i ze ) { i tems = new l ong [ s i ze] : / / Аналог т и п и зованного вызоиа sbrk 1 1 или mal l oc . но с вызовом конструктора . 1 1 если он и меется sp -1: =

Stack : : -Stack ( > { del ete [ J i tems : l ong Stack : : top ( ) const return i tems [ s p J : l ong Stack : : рор ( ) { return i tems [ sp- - J : voi d Stack : : push ( l ong i ) i tems [++sp ] i: =

i nt ma i n ( ) { / / Вызов Stack : : Stack ( ) Stack q : Stacl< r ( 1 5 ) : / / Вызов Stack : : Stack ( i nt ) q . push ( l ) : i nt i q . top( ) : q . pop( ) : =

Если класс не содержит конструктора, то инициализация объектов класса не гарантирована. Если класс без конструктора содержит другие объекты классов, имеющих конструкторы по умолчанию, то эти объекты будут правильно инициа­ лизированы, однако содержимое других полей остается неопределенным. Для примера рассмотрим код в листинге 2.5. Объекты класса В не содержат внутрен­ них объектов классов, только экземпляры встроенных типов С (указатель на char и short). Но конструктор В берет на себя все хлопоты по инициализации полей своих объектов. Экземпляры С содержат только внутренний объект В и int; объекты класса D содержат только int. Программа mai n создает объекты классов

2.3. Конструкторы и деструкторы

35

С и D, и нетрудно заметить, что поля примитивных типов классов С и D остаются неинициализированными. Если бы эти классы имели конструкторы. и если бы конструкторы определяли порядок инициализации этих полей, то инициали­ зация была бы полной. Листинr 2 . 5 . Инициализация полей класса переменных типов

cl ass В puЫ i c : 8( ) { р i nt f ( ) : pri vate : cha r *р : short s : }:

=

cl ass С { puЬl i c : i nt g ( ) ; pri vate : i nt i : В Ь:

О: s = О: }

/ / Нет конструктора

}:

cl ass О { puЫ i c : i nt h O : pri vate : i nt j . k :

}:

С gc ;

1 1 р и s и н и циализ ирован ы . переменная

обнулена

i nt ma i n ( ) { С с ; 1 1 р и s и нициализ ированы . з начение i не определено / / j и k не и нициали з и рованы О d: c .gO ; i nt 1 =

Как упоминалось ранее, класс С содержит переменную класса В. Так как класс В содержит конструктор, поля С::Ь будут инициализироваться при каждом создании объекта С. Конструктор В::В вызывается из конструктора по умолчанию (не имею­ щего параметров), автоматически сгенерированного компилятором для класса С. Компилятор обязан предоставить этот конструктор, чтобы у него было место для вызова С::Ь. Но код, сгенерированный компилятором, инициализирует только те члены, у которых существуют конструкторы, поэтому при отсутствии конструктора будут неявно инициализированы только объектные члены классов. Члены, объяв­ ленные с встроенным типом, не иницuш�изируются автоматически. Такое пове­ дение логически согласовано с отсутствием инициализации структур в С.

36

Глава

2.

Абстракция и абстрактные типы да нных

2 . 4 . П одставляем ы е фу н к ци и Модификатор i n ti n e запрашивает подстановку функции, то есть ее расширение в точке вызова, чтобы предотвратить затраты на вызов. Компилятор пытается подставить код функции, объявленной с ключевым словом i ntine, перед ее исполь­ зованием, если точки объявления и использования находятся в одном исходном потоке (то есть являются частью одного исходного файла или совокупности ис­ ходных файлов, включенных директивами #inctude). Подстановка иногда приво­ дит к значительной экономии процессорного времени: так, один программист обнаружил, что переход на подставляемые функции в программном комплексе имитации оборудования ускорил работу в четыре раза! В языке С в течение долгого времени подставляемые функции имитировались при помощи макросов препроцессора. В следующем примере макрос имитирует функцию вычисления модуля (абсолютного значения): #defi ne abs ( x ) ( х < i n t ma i n ( ) i nt i . j ;

О ?

( -х) : х)

i = abs ( j ) ;

Подставляемые функции не только заменяют препроцессорные макросы, но и пре­ восходят их, потому что они подчиняются тем же правилам типизации и области видимости, что и обычные функции. Сгенерированный ими код не уступает ана­ логичным макросам по эффективности, а очень часто и превосходит их (см. уп­ ражнения в конце главы). Иногда подставляемые функции упрощают процесс отладки. Например, некоторые среды программирования на С++ позволяют отключить подстановку функций при помощи флага компилятора, чтобы все функции компилировались в автономные фрагменты кода. А это означает, что отладчик может легко устанавливать в них точки прерывания (тогда как в мак­ росах это обычно невозможно). Чтобы объявить функцию подставляемой, вставьте в ее определение ключевое слово i ntine: i n l i ne i nt abs ( i nt i ) { return i < О? - i : i ; } cl a s s Stack {

}:

l ong рор ( ) :

i n l i ne l ong Stack : : рор ( ) return i tems [ s p - - ] :

2.4 .

Подставляемые функции

37

Если подставляемой требуется объявить функцию класса, достаточно включить ее определение в объявление; впрочем, делать это не рекомендуется, потому что при этом усложняется администрирование программы (см. раздел 2. 1 1 ). Тем не менее, синтаксис «определения при объявлении• будет использоваться в книге для на­ глядности, без связи с подстановкой. Пример подставляемой функции класса:

cl ass Stack {

}:

l ong рор ( ) { return i tems [ sp - - J : } / / Определение подставл яемой фун кции

Определение подставляемой функции (и конечно, объявление функции с клю­ чевым словом i n Li n e) должно располагаться в исходном тексте программы до первого вызова этой функции. Объявление функции как подставляемой после вызова является ошибкой. Рассмотрим следующий пример:

cl ass С { puЫ i c : i n l i ne i nt а ( ) i nt Ь ( ) i nt с ( ) : i nt d ( ) : i nt е ( ) :

return 1 : return 2 :

1 1 Тоже подставл яемая

}:

i n l i ne i nt С : : d ( ) { return с ( ) + 1 :

11 Ошибка : С : :с вызывается до объ я влени я 1 1 фун кции подставляемой ( с м . н иже )

i nl i ne i nt С : : с ( ) { return О : } i nt С : : е ( ) { return с ( ) + 5 : i nt mai n ( ) с о: i nt i

=

о d() : .

1 1 Функци я С : : с подставляется

11 С : :d подставл я етс я . 1 1 а вложенный вызов С : : с - - нет

Некоторые реализации С++ устанавливают собственные ограничения на подста­ новку. Например, компилятор может запретить подстановку для функций в цик­ лах (хотя первая итерация может подставляться) или рекурсивных функций (хотя начальные итерации при частичной рекурсии тоже могут подставляться). В случае превышения некоторого порога сложности, определяемого компилято­ ром, подстановка запрещается. Подставляемые функции не являются панацеей в плане повышения быстро­ действия. За ускорение работы программы часто приходится расплачиваться

38

Глава 2. Абстракция и абстрактные типы данных

увеличением объема ее кода; коэффициент увеличения зависит от среды про­ граммирования. В данном случае действует правило 80/20: найдите 20 % кода, в котором ваша программа проводит 80 % времени, и преобразуйте в подстановку ВЬIЗовы функций из внутренних циклов. Выделение кандидатов на подстановку в сгорячих точках� программы является простым и эффективным способом по­ вышения быстродействия.

2 . 5 . И ни ц иализа ц ия стати ч е ских пе р е м е нн ых Классы С++ могут содержать статические переменные, которые являются кон­ цептуальными аналогами статических функций классов. Статические перемен­ ные объявляются в интерфейсе класса, обычно в заголовочных файлах (чаще всего с расширением h хотя это зависит от системы). Определения этих данных (возможно - с инициализаторами, хотя это и не обязательно) должны находить­ ся не в заголовках, а в файлах с расширением .с ( С СРР и т. д" в зависимости от того, какое расширение используется в вашей системе для файлов с исходными текстами С++). Пример приведен в листинге 2.6. .

,

.

,

.

Листинг 2. 6 . Инициализация статических переменных класса

Об\я вления в з а голо в оч н ых фа йл ах : st ruct Х { X ( i nt . i nt ) : }: struct s { stat i c const i nt а : stat i c Х Ь : i nt f( ) :

}:

Определен и я в фа йле с ра сш и ре н и е м . с : const i nt s : : a = 7 : Х s : : b0 . 2) : i nt s : : f( ) { return а : Статические переменные классов уменьшают количество глобальных имен в про­ грамме. Кроме того, они наделяют объекты некоторыми полезными свойствами обычных переменных, а также позволяют закрепить за ними данные, существую­ щие на уровне класса в целом, а не отдельного объекта, например, правила управ­ ления доступом. Статические переменные классов очень удобны для разработ­ чиков библиотек, потому что они предотвращают загромождение глобального пространства имен, упрощают программирование библиотек, а также использо­ вание нескольких библиотек в программе.

2 . 6.

Статические функции классов

39

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

2 . 6 . Стат и чес ки е фу н кци и кл асс ов Первый опыт использования С++ показал, что большинство имен, локальных по отношению к библиотекам, составляли имена функций. Оказалось, что для ими­ тации статических функций классов применялся непереносимый код вида: C C X * ) O ) ->f O : Этот «фокус� не работал в некоторых реализациях динамической компоновки, а язык не обеспечивал никакой разумной семантики. Статические функции классов С++ напоминают глобальные функции, область видимости которых определяется границами класса. По эффективности они не уступают вызовам глобальных функций и немного превосходят вызовы обыч­ ных функций классов: cl ass Х { рuЫ i c : 11 . . . voi d foo ( ) { fooCounter++ ; . . . } stati c voi d pri ntCounts C ) { pri nt f( " foo cal l ed %d t i mes\n " . fooCounter ) : } pri vate : stat i c i nt fooCounter :

}:

i nt Х : : fooCounter = О : i nt ma i n O { pri ntCounts ( ) :

/ / Ошибка ( если не сущест вует глобаль ной 11 функции с таким и менем ) Х : : pri ntCounts ( ) : / / Нормально

Статические функции классов обычно применяются для управления ресурсами, общими для всех объектов класса. Как правило, статические функции классов работают со статическими данными классов. Например, в переменных класса могут храниться счетчики вызова его функций; каждая функция класса ведет счет своих вызовов в собственной переменной static i nt. С другой стороны, одна или несколько статических функций используются для сброса, чтения или выво­ да значений счетчиков.

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

40

Глава

2.

Абстракция и абстрактные тип ы да нных

функций в объектах, и они могут не оnюситъся к ресурсам УJХ>ВНЯ проектирования (см. главу 6). В некотором смысле такие конструкции моделируют концепцию пакета в языке Ada. · Они будут подробно рассматриваться в главе 1 1 .

2 . 7 . Област ь ви ди м о ст и и кон стант но ст ь В языке С конструкция #defi ne заменяет константные значения символическими именами. Применение именованных констант лучше отвечает духу С++. Обычно именованные константы могут применяться везде, где в С допускается использо­ вание директив #defi ne. Константы С++ могут обладать областью видимости, по­ этому константы, специфические для конкретного класса, не загромождают гло­ бальное пространство имен - макросы таким свойством не обладают. Константы, используемые локально по отношению к файлу, можно объявлять и определять в этом файле; они останутся невидимыми для всех остальных фай­ лов той же программы: const i nt МAX_STACK_S I ZE = 1000 : const cha r *const paradi gm = "The Object Paradi gm" : Константы также могут совместно использоваться несколькими исходными файлами, для чего их следует объявить с ключевым словом extern . Такие объяв­ ления обычно размещаются в заголовке, включаемом во все файлы: extern const douЫ e е :

При этом единственное определение значения помещается в удобный исходный файл (с расширением .с, .С, .срр и т. д.): #i ncl ude extern const douЫ e е = exp ( l . 0 ) : Синтаксис константных статических членов классов усложняет объявление симво­ лических констант внутри класса. В частности, значение статических констант, яв­ ляющихся членами классов, не может задаваться на стадии компиляции. А это оз­ начает, что их значения не могут использоваться для объявления размера массива: stati c const i nt S I ZE l = 10 : cl ass С {

}:

stati c const i nt S I ZE2 : cha r vec l [ S I ZE l J : char vec2 [ S I ZE2] :

/ / ОК

/ / Нел ь з я . з начение S I ZE2 неи з в естно

const i nt C : : S I ZE2 = 10 : Чтобы преодолеть этот недостаток ключевого слова const, мы используем кон­ струкцию, которая является его синонимом: перечисляемые типы (enum) содержат фиксированные целочисленные значения и могут справиться с задачами, недос-

2.8.

П орядок инициализации глобальных объектов, констант

41

тупными для типа const i nt. Перечисляемый тип в С + + , как и в С, обычно опре­ деляется в виде набора символических целых констант, которые могут использо­ ваться в командах switch и if, но конкретные целые значения которых для нас не­ существенны. В объявлении enum символическим именам можно присвоить конкретные значения, наделив их свойствами символических констант. В сле­ дующем фрагменте в классе Stack создается •константа• StackSize: c l a s s Stack { puЫ i c : voi d push ( i nt ) : i nt рор ( ) : i nt top ( ) const : pri vate : enum { StackSi ze = 100 } : i nt rep [ StackS i zeJ . s p :

/ / И11ита ц и я константы

};

voi d Stack : : push ( i nt el ) i f ( sp >= StackSi ze ) error ( " stack overfl ow" ) ; el se rep [ s p++J = e l :

2 . 8 . П о р ядо к ин и ц иал и з а ци и глобал ь н ых объ е ктов , кон стант и стати ч е ских ч л е нов классов С + + гарантирует, что переменные инициализируются в порядке их следования в исходном файле, но при этом не существует гарантированного порядка инициа­ лизации глобальных объектов в нескольких исходных файлах. Это относится ко всем глобальным объектным переменным, но особенно важно для статических переменных классов, потому что инициализация глобальных объектов может за­ висеть от действительности значений статических констант их классов.

Любые предположения относительно порядка инициализации: могут приводить к трудноуловимым ошибкам. Для примера рассмотрим простой заголовочный файл AngLe . h , содержащий следующие объявления: #i ncl ude #i nc l ude extern const douЫ e pi ; c l a s s Ang l e { puЬl i c : Ang l e ( douЫ e deg rees ) { r = degrees * pi / 180 . 0 :

42

Глава

2.

Абстракция и абстрактные типы данных

voi d pri nt ( ) { pri ntf( " radi ans

}

%f\n " . r ) :

=

pri vate : douЫ e r : }: Теперь рассмотрим исходный файл pi 1.c с определением константы pi:

#i ncl ude extern const douЫ e pi

=

4 . 0 * ata n ( l . 0 ) :

Далее рассмотрим исходный файл pi2.c с кодом приложения:

#i ncl ude Ang l e northea st

=

45 :

i nt ma i n ( ) { Angl e northeast2 45 : northea st . pri nt ( ) : northea st2 . pri nt ( ) : return О : =

Если откомпилировать и запустить эту программу в системе автора, будет полу­ чен следующий результат:

radi ans radi ans

= =

0 . 000000 0 . 785398

Причина в том, что значение pi еще не было инициализировано к моменту вызо­ ва конструктора northeast Если вам действительно необходимо управлять порядком инициализации , восполь­ зуйтесь методикой, представленной в разделе 3.7. И все же лучше по возможности избегать этих зависимостей, особенно в коде библиотек, с которыми будут рабо­ тать другие пользователи.

2 . 9 . Об е с пе ч е н и е константности фун кц и й кл а ссов В версию 2 . 0 языка С + + были включены некоторые новые варианты примене­ ния модификатора const. В предыдущих версиях С++ значение константного объекта можно было изменить, вызвав для него функцию класса. Многие жало­ вались на проблемы, возникающие из-за этой ошибки. Если вообще запретить вызов функций класса для константных объектов, по­ следние станут бесполезными. Для сохранения полноты языка в версии 2.0 бы­ ли представлены константиые футщии Ю1ассов, которые могут вызываться для

2 . 9.

Обеспечение константности функци й классов

43

константных объектов. Компилятор гарантирует, что константная функция не из­ менит того объекта, для которого она вызывается, если только программист не по­ пытается сознательно обойти это ограничение (см. обсуждение .логической кон­ стантности• в следующем разделе). Кроме того, гарантируется, что для кон­ стантных объектов будут вызываться только константные функции. Пример приведен в листинге 2.7. Использование ключевого слова const в качестве суф­ фикса при объявлении списка параметров функции соответствует его примене­ нию в качестве суффикса для символа * . Из-за раздельной компиляции мы не можем рассчитывать на то, что сам компилятор (без помощи со стороны поль­ зователя) обнаружит вызов для константного объекта функции класса, моди­ фицирующей свой объект. Листинr 2. 7. Константные функции класса

st ruct s { i nt а : f ( i nt аа ) { return а аа : } g ( i nt аа ) const { return аа : } 11 h ( i nt аа ) const { return а }: =

=

аа :

voi d g ( )

{

s ol : const s о2 : ol . a 1: 11 о2 . а 2: ol . f( З ) : o2 . f( 4 ) : 11 ol . g ( 5 ) : о2 . g ( б ) : =

=

/* Если уда л и т ь при знаки комментария ( / / ) . ком п и л я тор выдаст следующие сообщен и я об ошибках : l i ne 5 : error : a s s i grvnent to memЬer s : : а of const struct s l i ne 13 : error : a s s i gnment to mernber s : : a of const s l i ne 15 : wa rni ng : non const member functi on s : : f( ) cal l ed for const obj ect */

Логическая и фи з ич еск ая конста нтн ость Состояние объекта - то есть совокупность значений всех его данных - опреде­ ляет работу функций класса. Состояние объекта имеет три составляющие: + данные, отражающие состояние приложения (например, факт поступления

некоторого сообщения или сигнала);

44

Глава

2.

Абстракция и абстрактные типы данных

+ данные, отдаленно относящиеся к состоянию приложения, но являющиеся

артефактами реализации (например, Stack: :sp ); + данные, используемые при отладке и администрировании и не связанные

с семантикой приложения (например, счетчик вызовов некоторой функции). Представленная классификация не идеальна. Например, такие данные, как счет­ чики ссылок, относятся как ко второй, так и к третьей категориям, а некоторые состояния ограничиваются первыми двумя категориями. Тем не менее, подобная классификация помогает подчеркнуть один важный аспект во взаимодействии функций класса с константными объектами. Для примера рассмотрим класс бинарного дерева BinaryTree с функцией fi nd N ode, которая возвращает копию узла, найденного по некоторому критерию. Алгоритм может быть оптимизирован так, чтобы при слишком большом времени поиска узла (которое определяется по пороговому значению, зависящему от количества узлов в дереве) балансировка дерева производилась заново. Вроде бы логично объявить функцию fi n d N ode константной, но из-за возможной повторной балан­ сировки такое объявление вызовет протест у компилятора: константные функ­ ции классов не могут модифицировать данные своих объектов. Такие случаи встречаются относительно редко, и для них существует элементар­ ный обходной путь. Фокус основан на обращении к локальному объекту через указатель, который является синонимом для this:

Т Bi naryTree : : fi ndNode ( St r i ng key ) const { i f ( needReBa l ance ) { 1 1 Сюда следует вста в и т ь ком ментари й . объ ясн яющи й . 1 1 зачем нарушается констант ность объекта Bi naryTree *Th i s ( Bi naryTree* ) th i s : =

Thi s - >l eft

=

Thi s - >l eft ->l eft :

Другое возможное решение основано на создании ссылки на this, которая ведет себя как синоним, но с меньшими ограничениями:

Т B i n a ryTree : : fi ndNode ( Stri ng key ) const { i f ( needReBa l ance ) { typedef Bi naryTree *Bi naryTreePoi nter : const Bi naryTreePoi nter &Th i s = ( Bi na ryTree* )thi s ; Th i s - >l eft

=

Th i s - >l eft - >l eft :

2 . 1 О.

Указатели на функции классов

45

Вторая форма подчеркивает, что This всего лишь является константным синони­ �юм для this. Скорее всего, сгенерированный код получ�пся таким же, как в первом с.'Iучае. Если функция объявлена подставляемой, одно из этих решений может генерировать меньший объем кода (в зависимости от реализации компилятора). Класс Stri ng дает обратный пример. Он имеет очевидную реализацию в виде указателя на динамически выделенный блок памяти, содержащий символьный вектор. Функция Stri n g : : getChar может определяться таким образом, чтобы она удаляла из строки и возвращала последний символ. Функция класса не может изменять состояние самого объекта, то есть содержимое указателя должно оста­ ваться неизменным. Однако логическое состояние объекта при этом изменяется, поэтому функцию getChar не следует объявлять константной, хотя компилятор и разрешит это сделать.

2 . 1 О . У казател и на фун кц и и кла ссов В некоторых ситуациях требуется изменить поведение функции н а стадИИ выпол­ нения программы. Для решения этой задачи можно воспользоваться указателя­ ми на функции классов. Рассмотрим класс, представляющий фильтр в электри­ ческой цепи из последовательно подключенных индуктивности, конденсатора и сопротивления '. Фильтр является особой разновидностью цепи, называемой RLС-цепью ( где R, L и С - обозначения соответственно для сопротивления, индуктивности и конденсатора, принятые в электротехнике). На практике часто требуется определить переходную характеристику фильтра, то есть ток, полу­ чаемый при мгновенном изменении поданного напряжения. Мы хотим создать объект, моделирующий поведение фильтра. Конструктор класса должен получать параметры, задающие индуктивность, емкость, сопротивление, начальную силу тока и напряжение, и т. д. Функция класса вычисляет силу тока, проходящего через цепь, как функцию времени. Возможны три характерных режима пове­ дения компонента: чрезмерное затухание, недостаточное затухание и критиче­ ское затухание. Режим работы фильтра определяется параметрами конструктора. Каждый из трех режимов описывается формулой, задающей силу тока как функ­ цию времени. Понимание этих формул не обязательно для данного примера, но на всякий случай приведем краткую сводку. Формула чрезмерного затухания:

i(t) = A, e•• t + А 2 е '' ' .

Формула критического затухания:

i( t) = е -01 (A1 t + А 2 ).

Наконец, для недостаточного затухания зависимость выглядит так:

i(t) = е- 01 (В, cos rod t + В2 sin rod t).

1

Спасибо Дону Стейну

( Don Stein)

з а некоторые идеи для этого раздела.

46

Глава 2. Абстракция и абстрактные типы данных

Здесь:

а =

(JJO =

R

-

2L

.

.J[C ' 1

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

В С+ + можно объявить указатель на функцию класса и использовать полу­ ченную переменную для вызова функции (при условии, что указатель должным образом инициализирован). Объявление указателя на функцию класса должно задавать как тип класса, содержащего функцию, так и сигнатуру (интерфейс) самой функции; даже указатели на функции участвуют в системе проверки ти­ пов. Во время выполнения программы указателю на функцию класса можно присваивать адреса разных функций класса, однако все эти функции должны иметь одинаковые типы параметров и возвращаемого значения. Ниже приводятся примеры объявлений функций и указателей для работы с ними:

Об\я вл ен и я фун к ц и й i nt St ri ng : : curCo l umn ( ) : i nt St ri ng : : 1 ength ( ) : i nt Stri ng : : hash ( ) :

Сов м ес т им ые об\я в лен и я ука з а теле й

char Stack : : рор( i nt ) :

char ( Stack : : *p2 ) ( i nt ) :

voi d Stack : : push ( cha r ) :

vo1 d C Stack : : *pЗ ) ( char ) :

i nt PathName : : error ( i nt . const char* . . . ) :

i nt C PathName : : *p4 ) C i nt . const char* . . . ) :

i nt ( St r i ng : : *pl ) ( ) :

Примеры иниuиализации этих указателей:

pl = &Stri ng : : 1 ength : р2 = &Stack : : рор : &Stack : : push : рЗ р4 = &PathName : : error : =

2. 1 0 .

Указатели на функции классов

47

А вот как эти функции вызываются:

i nt ma i n ( ) { St ri ng s : Stack t ; PathName name . *namePoi nter = new PathName ; р4 PathName : : er ror : =

i nt m = ( s . *pl ) ( ) : cha r с = ( t . *p2 ) ( 2 ) ; ( t . *рЗ ) ( ' а ' ) ; ( name . *p4 ) 0 . "at l i ne :td\ n " . _L I NE_) ; ( namePoi nte r - >*p4 ) ( 3 . " a nother error ( :td ) i n fi l e :t s " . _L I NE_ . _F I LE_) ; return О ; Также допускается объявление указателей на переменные класса:

c l ass ТаЫ е puЫ i c : sort ( ) ; };

cl ass х puЬl i c : tаЫ е tl . t2 : };

i nt ma i n ( ) { ТаЫ е X : : *taЫ ePoi nter &X : : t l ; Х а . *Ь new Х ; ( a . *taЫ ePoi nter ) . sort ( ) ; 1 1 а . t l . sort O ( b - >*ta Ы ePoi nter ) . sort ( ) ; 1 1 b - >t l . sort ( ) taЫ ePoi nter &X : : t2 ; ( a . *taЫ ePoi nter ) . sort ( ) : 1 1 а "t2 . sort O ( b - >*taЫ ePoi nter ) . sort ( ) : 1 1 b - >t2 . sort ( ) return О : =

=

=

Как же может выглядеть модель электрической цепи? В листинге 2.8 приве­ ден класс, определяющий реакцию фильтра. В параметрах конструктора клас­ са передаются значения сопротивления ( r), индуктивности (l ) и емкости (с), а также исходная сила тока в цепи. Мы создаем объект этого класса, а затем вызываем функцию current с параметром ti me, чтобы узнать силу тока в любой момент времени.

48

Глава

2.

Абстракция и абстрактные типы данных

Листинг 2 . 8 . Класс, характеризующий отклик резонансной системы

#i ncl ude typedef douЫ e t i me ; c l a s s Seri esRLCStepResponse puЫ i c : compl ex ( Seri es RLCStepResponse : : *current ) ( t i me t ) ; Seri esRLCStepRes ponse( douЫ e r . douЫ e 1 . douЫ e с . douЫ e i ni t i a l Current ) : douЫ e frequency ( ) const { return 1 . 0 / sqrt ( L * С ) ; pri vate : compl ex underDampedResponse ( t i me t ) { return exp ( - a l pha * t ) * ( Ьl * cos ( omegad * t ) + Ь2 * s i n C omegad * t ) ) ; } compl ex overDampedResponse ( t i me t ) { return a l * exp ( s l * t ) + а2 * exp ( s2 * t ) : } compl ex c r i t i ca l l yDampedResponse ( t i me t ) { return exp ( - a l pha * t ) * ( a l * t + а 2 ) : } douЫ e R . L . С . currentTO . a l pha : compl ex omegad . a l . Ы . а2 . Ь2 . s l . s2 : }: Тем не менее •Функция класса• current вообще не является функцией; это всего лишь указатель на функцию, значение которого задается конструктором. Конст­ руктор выбирает функцию отклика на основании параметров фильтра и устанав­ ливает указатель current на нужную функцию. Ниже приводится код конструкто­ ра с инициализацией указателя current:

Seri esRLCStepResponse : : Seri esRLCStepResponse( douЫ e r. douЫ e 1 . douЫ e с . douЫ e i ni t i a l Current ) R = r: L 1 ; С = с ; currentTO = i n i t i a l Current ; a l pha R / (L + L) ; omegad = sqrt ( frequency ( ) *frequency ( ) - a l pha*a l pha ) : . Выч исление a l . Ы . а2 . Ь2 и т . д . i f ( a l pha < frequency ( ) ) { current = &Seri esRLCStepResponse : : underDampedRespons e : } el se i f ( a l pha > frequency ( ) ) { current = &Seri esRLCStepResponse : : overDampedResponse : } el se { current = &Seri esRLCStepResponse : : cri ti ca l l yDampedResponse ; =

=

.

.

2. 1 1 .

Правила организации программ ного кода

49

Остается рассмотреть простое приложение, в котором используется класс фильтра:

i nt ma i n ( ) {

douЫ e R . L . С . I O : ci n >> R > > L >> С >> 10 : Seri esRLCStepResponse a F i l ter ( R . L . С . 1 0 ) : for ( t i me t = 1 . 0 : t < 100 : t += 1 . 0 ) { cout count=l : } Stri ng ( const Stri ng& s ) { rep = s . rep ; rep - >count++ ; } Stri ng& operator= ( const Stri ng& s ) s . rep ->count++ ; i f ( - - rep - >count count count = 1 : } St ri ng operator+ ( const Stri ng& s ) const { Stri ngRep у = *rep + *s . rep : return Str 1 ng (y . rep ) : i nt l ength ( ) const { return rep - >l ength ( ) : } pri vate : Stri ngRep *rep : };

Обратите внимание: •низкоуровневые• конструкторы (строящие объект Stri ng •на пустом месте• , а не на базе другого объекта Stri n g ) присваивают полю c o u nt класса представления значение 1. Операции, копирующие другую строку (String(const Stri ng&) и operator=(const String&)), заимствуют представление ориги­ нала и увеличивают его счетчик ссылок. Любые операции, требующие модифи­ кации представления, должны уменьшить счетчик ссылок и освободить память, если счетчик достигнет нуля. Одно из преимуществ такого решения состоит в том, что оно позволяет сосредо­ точить всю строковую специфику во внутреннем классе, тогда как внешний класс занимается в основном управлением памятью. В частности, это означает, что представленное решение хорошо подходит для реализации подсчета ссылок

3. 5. Подсчет ссылок

75

в существующих классах. Однако оно сопряжено с серьезными проблемами эф­ фективности; так, ради достижения выразительной простоты функция operator+ выполняет много лишней работы . В этой программе вызов operator+ приводит к трем операциям выделения памяти 11 созданию лишних временных объектов в стеке. О том, как этого избежать, рассказано в следующем разделе. Мы будем называть класс Stri ng ;чаиuпуляторо.м, а к.1асс StringRep телом, с ко­ торым работают при помощи манипулятора. Использование двух (а возможно, и более) классов в ситуации, когда экземпляр одного класса управляет экземп­ лярами другого класса, иллюстрирует идиому маиипулятор/тело. -

ПРИМЕЧАНИЕ Идиома « М анипулятор/тело » часто тре буется для разложения сложных а бстракци й н а мень­ шие классы, с которым и удо б нее ра б отать . В этой идиоме также может о тражаться со­ вместное использование одного ресурса несколькими классам и , управляющими доступом к этому ресурсу. И в се же чаще всего она применяется для подсчета ссылок ( см. далее ) .

Самым распространенным частны�1 случаем идио�1ы «манипулятор/тело» явля­ ется подсчет ссылок, как в примере класса String. Если класс-тело содержит счет­ чик ссылок, значение которого из�1еняется классом-��анипулятором, этот част­ ный случай иллюстрирует идиому подсчета ссьmок. ПРИМЕЧАНИЕ Подсчет ссылок тре б уется для классо в , экземпляры которых часто копируются посредст ­ в ом присваивания или передачи параметров, осо бенно есл и коп ирование обходится доро­ го из-за бол ьшого о бъема или сложности объекта. И спользуйте эту идиому, если в про­ грамме мо гут существовать несколько логических копий объе кта без копирования данных, например, дл я объекта экранной памяти , который может передаваться между функциями , и л и о б щих блоков памяти , совместно используемых несколькими процессорами , когда ка ­ жды й пользователь должен получить собственную копию объекта для ра б оты с ресурсом.

Эконом ное решение Второе решение строится более или менее «На пустом месте» и отличается высо­ кой эффективностью как по скорости работы, так и по затратам ресурсов. Класс Stri n g , с которым работает пользователь. строится по образцу из листинга 3. 1 . В его представление включается новое поле для счетчика ссылок (листинг 3.7), а при обращениях к нему используется дополнительный уровень абстракции. Счетчик и указатель на данные группируются в тривиальном классе с одним простым конструктором и одним простым деструктором. Л истинг 3. 7. Более эффективная реализация подсчета ссылок для String

c l ass Stri ngRep fri end Stri ng : pri vate : Stri ngRep C const cha r *s ) { : : st rcpy ( rep = new cha r [ : : st r l en C s J + l ] . s J : count = 1 :

продолжение.9

76

Глава 3. Кон кретные типы данных

Л исти нг 3.7. (продолжение)

} -St ri ngRep ( ) { del ete [ J rep ; } pri vate : char *rep : i nt count ;

};

c l ass St ri ng pu Ы i c : Stri ng ( ) { rep = new Stri ngRep ( " " ) : } St ri ng ( const St ri ng& s ) { rep = s . rep ; rep ->count++ : Stri ng& operator= ( const St ri ng& s ) { s . rep- >count++ ; i f ( - - rep - >count count rep ) : i nt l ength ( ) const pri vate : Stri ngRep *rep : }: Stri ng St ri ng : : operator+ ( const St ri ng& s ) const

{

char *buf = new cha r [ s . l ength ( ) + l ength ( ) + 1 ] : : : strcpy ( buf . rep ->rep ) ; : : st rcat ( buf . s . rep - >rep ) ; St ri ng retva l ( buf ) : / / Освобождение временной па м я т и del ete [ J buf ; return retva l :

В реализации можно предусмотреть �черный ход� для сохранения копии при вызове operator+(const String&) класса String. Мы создаем новый конструктор, в ар­ гументе которого передается ссылка на указатель на символьные данные. Иначе говоря, указатель передается по ссылке, конструктор �отбирает� те данные, на которые он указывает, и обнуляет оригинал (листинг 3.8). Л истинг 3 . 8 . Оптимизация конкатенации строк

#i ncl ude c l ass St ri ngRep { fri end c l ass St ri ng : pri vate : / / Чтобы не з а г ромождать г лобаль ное пространство имен typedef cha r *Cha r_p ;

3. 5. Подсчет ссылок

77

Stri ngRep ( Stri ngRep : : Cha r_p* const r ) rep = *r : * r = О : count = 1 : } puЫ i c : Stri ngRep ( const cha r *s ) { : : strcpy ( rep = new cha r [ : : strl en ( s ) + l ] . s ) : count = 1 : } -St ri ngRep ( ) { del ete [ ] rep : } pri vate : cha r *rep : i nt count : }: c l ass Stri ng { puЬl i c : Stri ng ( ) { rep = new St ri ngRep ( " " ) : } Str i ng ( const Stri ng& s ) { rep = s . rep : rep- >count++ : Stri ng& operator= ( const Stri ng& s ) { s . rep ->count++ : i f ( - - rep ->count count rep > : i nt l ength ( ) const pri vate : Stri ng ( Stri ngRep : : Cha r_p* const r ) { rep = new Stri ngRep ( r ) ; / / Вызов нового конструктора } St ri ngRep *rep : }: Stri ng Stri ng : : operator+ ( const St ri ng& s ) const

{

char *buf = new cha r [ s . l ength ( ) + l ength ( ) + 1 ] : : : st rcpy ( buf . rep ->rep ) : : : strcat ( buf . s . rep - >rep ) ; Stri ng retva l ( &buf ) : / / Вызов ново г о з а крыто го конструктора return retval :

В коде функции operator+(const String&) класса String используется специальная возможность, которую предоставляет конструктор String Rep(Char_p* const) класса StringRep, доступный благодаря дружественным отношениям между двумя клас­ сами. Две функции вступают в соглашение: String: :operator+(const Stri ng&) выде­ ляет память для хранения символов, указатель на нее передается конструктору String(const Char_p*), а тот в свою очередь передает его специальному конструктору

78

Глава З . Конкретные типы данн ы х

Stri n g Rep: :Stri n g Rep(Char_p* const) , сохраняющему указатель. Специальный кон­ структор StringRep обнуляет исходный указатель в функции operator+(buf). чтобы предотвратить возникновение висячих ссылок. Все эти действия позволяют избежать копирования строкового представления при построении объекта retval в Stri n g : : operator+.

П одсчет указателе й Методика подсчета указателей слегка отличается от двух предыдущих решений. Данная идиома была выработана в результате исследований общих методов уборки мусора в С++, а в ее основу заложено создание указателей на реальные объекты. Каждый раз, когда в программе копируется указатель, вызывается его операторная функция operator= или копирующий конструктор, поэтому количе­ ство ссылок на объект отслеживается в одном объекте представления, совместно используемом несколькими указателями. При выходе указателя из области ви­ димости вызывается его деструктор с соответствующим уменьшением счетчика ссылок. Когда счетчик уменьшается до нуля, блок памяти освобождается - либо на месте ; либо позднее в ходе уборки мусора. Единственная хитрость заключается в перехвате вызовов оператора -> для объекта посредством перегрузки. Перегруженный оператор -> отличается от других перегруженных операторов С++. Для всех остальных операторов значение, возвращаемое как результат операции, определяется реализацией. Но для оператора -> возвращаемое значение представ­ ляет собой промежуточный результат, к которому затем применяется базовая се­ мантика ->, что и дает окончательный результат. Рассмотрим такой фрагмент:

cl a s s А { puЫ i c : В *operator - > ( ) : }: i nt ma i n ( ) А а: . . . а - >Ь . . . Этот фрагмент означает следующее.

1 . Для объекта а вызывается функция A::operator->{}.

2. Возвращаемое значение сохраняется во временной переменной

х

типа В* .

3 . Вычисление х->Ь дает окончательный результат. Здесь Ь может быть заменено любым членом класса В (данными или функцией). Если класс В перегружает оператор ->, то описанный выше алгоритм из трех ша­ гов выполняется заново. Такой подход не только обладает преимуществами предыдущей методики под­ счета ссылок, но и предоставляет дополнительные возможности . Во-первых,

3. 5 . Подсчет ссылок

79

подсчитываемым указателям присущи все достоинства конкретных типов дан­ ных: их можно передавать в парам:етрах, создавать, уничтожать, присваивать и т. д. Во-вторых, они обладают свойствами, обычно ассоциируемыми с указателями, скажем, адресацией объектов. динамически создаваемых в куче. Но при этом объекты автоматически создаются без вызова оператора new и автоматически уничтожаются без вызова оператора deLete; другими словами, в этом отноше­ нии они ведут себя так же, как автоматические 11 статические экземпляры клас­ сов. Например, пусть класс Stri ng реализован как подсчитываемый указатель (см. далее), и объект String встроен в класс А:

c l ass А puЬl i c : s C " hel l o worl d " ) { } АО pri vate : St ri ng s : }:

Тогда каждый созданный объект класса А будет содержать подсчитываемый ука­ затель s класса Stri ng, который вроде бы указывает на Stri ng с содержимым " heLLo worLd " . Копии объекта класса А тоже вроде бы будут содержать указатель на тот же объект Stri n g , что и оригинал: копирование s выглядит как копирование указателя. Но при этом сохраняются все свойства настоящих объектов с под­ счетом ссылок. Например, изменение значения приведет к отказу от исполь­ зования общего представления. Другими словами подсчитываемые указатели ведут себя как указатели, у которых имеются конструкторы, поэтому при входе в область видимости они автоматически инициализируются и ассоциируются с вновь созданным объектом.

В листинге 3.9 приведена реализация класса StringRep для подсчета указателей. Она базируется на предыдущем примере, но конкатенация перемещена из Stri ng в StringRep ближе к той информации, с которой эта реализация работает. Кроме того, определен конструктор для создания Stri n g Rep на базе const char* и вспомо­ гательная функция print. Листинr 3 . 9 . • В нутренняя реализация• подсчета указателей на String

c l ass St ri ngRep fri end St ri ng : pri vate : St r i ngRep ( ) { * ( rep = new cha r [ l ] ) = \ О : } St ri ngRep ( const Str i ngRep& s ) { : : st rcpy ( rep=new cha r [ : : strl en ( s . rep ) +l ] . s . rep ) : '

'

}

-St r i ngRep ( ) { del ete [ J rep : } Stri ngRep ( const char *s ) { : : st rcpy ( rep=new cha r [ : : strl en ( s ) + l J . S ) :

}

Stri ng operator+ ( const Stri ng& ) const {

продолжение..9'

80

Глава 3 . Конкретные типы да нных

Л истинг 3 . 9 (продолжение)

char *buf new cha r [ s ->l ength ( ) + l ength ( ) + 1 ] : : : strcpy ( buf . rep ) : : : strcat ( buf . s - >rep ) : Stri ng retva l ( &buf ) : return retva l : =

i nt l ength ( ) const return : : str 1 en С rep ) : } voi d pri nt ( ) const : : pri ntf( " %s \ n " . rep ) : } pri vate : Stri ngRep C cha r** const r ) { rep = *r : *r = О : count 1: cha r *rep : i nt count : }: В листинге 3. 1 0 приведен класс String, из которого исключена операция Length; любой вызов операции Len gth для Stri n g перенаправляется Stri n g Rep вызовом функции operator->. То же относится к другим функциям, которые могут под­ держиваться классом: strchr (поиск символа в строке), hash и т. д. Класс Stri n g по-прежнему выполняет большую часть работы п о управлению памятью и под­ счету ссылок в экземпляре StringRep. В листинге 3.9 приводится реализация опе­ ратора конкатенации StringRep::operator+. Она выражается главным образом на основе членов Stri n g Rep, что позволяет обойтись без лишнего уровня абстракции по сравнению с реализацией из листинга 3.8. Так как класс Strin g перенаправляет вызовы функций operator+ классу Stri ngRep, такой подход требует затрат на лиш­ ний вызов функции при каждом вызове operator+. Впрочем, обычно эти затраты ликвидируются за счет оптимизации с подстановкой функций. Л истинг З . 1 О. Открытый интерфейс класса String с подсчетом указателей

cl ass St ri ng { fri end c l ass Stri ngRep : puЬl i c : Stri ng operator+ C const Stri ng &s ) const { return *р + s : { return р : } St ri ngRep* operator - > ( ) const Stri ng ( ) { С р = new St ri ngRep ( ) ) - >count = 1 : Stri ng ( const Stri ng& s ) { С р = s . p ) ->count++ : Stri ng ( const char *s ) { С р = new Stri ngRep ( s ) ) - >count = 1 : St ri ng operator= C const Stri ng& q J { 1 1 Реали з а ц и я сле г ка и з менена для ра знообра з и я i f ( - - p - >count < = О & & р ! = q . p J del ete р :

3.5. Подсчет ссылок

81

( p=q . p ) ->count++ : return *thi s : } -Stri ng ( ) { i f ( - - p - >count pri nt ( ) : pri ntf( " b i s " ) : b - >pri nt ( ) : pri ntf( " l ength of Ь i s %d\ n " . b - >l ength ( ) ) ; pri ntf ( " catenati on i s " ) ; C a+b ) - >pri nt ( ) ; return О :

Одно из преимуществ подсчета указателей состоит в том, что новые функции до­ бавляются только в класс-тело. Например, большинство новых строковых опера­ ций может быть реализовано на уровне функций StringRep; класс-манипулятор Stri ng автоматически получает эти операции благодаря оператору ->.

П РИМЕЧАНИ Е

������

Используйте подсчет указателей , что б ы из бавиться от хлопот с ручным осво б ождением динамически выделенной памяти . Подсчет указателе й также может пригодиться при по­ строении прототипов, когда приходится согласовывать изменения в сигнатуре тела с сиг­ натурой манипулятора. Подсчет указателе й в основном определяется стилем и лич ным вкусом п рограм миста , хотя э-fот механизм является важным компонентом идиом , пред­ ставленных в главе 9.

Реа л изаци я подсчета ссылок в существующ их классах Реализация подсчета ссылок в существующих классах иногда существенно улуч­ шает их быстродействие. На этом пути могут возникать различные препятствия, в том числе отсутствие доступа к исходным файлам, ограничения на размер или структуру объекта в памяти , которая будет нарушена добавлением поля счетчика, необходимость подсчета ссылок лишь в отдельных случаях и т. д. Для таких клас­ сов приходится вводить новый уровень с дополнительным классом-манипулятором.

82

Глава 3 . Конкретные типы данных

Предположим, библиотека уже содержит класс Stack без подсчета ссылок, но специфика приложения требует применения стека с подсчетом ссылок. Мы соз­ даем новую абстракцию стека с имене�1 CountedStack на базе существующего класса Stack: CountedStack rep

CountedStackRep

Stack

rep count

2

rep

Исходная абстракция Stack остается без изменений, а для управления использу­ ются два класса. Класс CountedStack обеспечивает управление памятью, а класс Cou ntedStackRep используется как простая структура данных для создания допол­ нительного уровня косвенных обращений: c l a s s CountedStack { рuЫ 1 с : CountedStack ( ) : rep ( new CountedStackRep) { } i nt рор ( ) { return rep - >rep - >pop ( ) : } voi d push ( i nt i ) { rep - >rep - >push ( i ) : } CountedStack ( const CountedStack &с ) : rep( c . rep ) rep - >count++ : CountedStack ( const Stack &с ) : rep ( new CountedStackRep ( new Stack ( c ) ) ) { } operator Stack ( ) { return *( rep ->rep ) : } -CountedStack ( ) { i f ( - - rep- >count count++ : i f ( - - rep- >count count++ : Stri ng ( const Stri ng& s > Stri ng& operator=( const Stri ng& s > s . rep->count++ : i f ( - - rep->count == 0 ) del ete rep : rep = s . rep : return *th i s : } -�t ri ng ( ) { i f ( - - rep - >count rep ) : i nt l ength ( ) const pri vate : c l ass Stri ngRep { puЫ i c : Stri ngRep ( const char *s ) { : : st rcpy ( rep = new char[ : : strl en ( s ) + l ] . s ) : count = 1 : " "

-

продолжение�

84

Глава З . Ко н кретные типы данн ых

Листин г 3 . 1 1 (продолжение)

-St ri ngRep ( ) { del ete rep : } char *rep : i nt count : }: Stri ngRep *rep : }:

ПРИМЕЧАНИ Е С инглетные классы писем используются в том случае, если пара «конверт/письмо• приме­ няется для подсчета ссылок, или если интерфе й с п исьма должен б ыть видимым только для конверта. Идиома приносит наи большую пол ьзу для усто й чивых аб стракци й , поскольку все изменения в п исьме требуют перекомпиляции конверта и всех его клиентов чаще, нежели при раздельном сопровоЖдении письма и конверта.

Кодирование функций класса в этом варианте не отличается от предыдущей версии Stri n g . Данное решение улучшает инкапсуляцию и уменьшает загромо­ ждение глобального пространства имен по сравнению с использованием двух глобальных классов. Оборотная сторона состоит в том, что изменения в ин­ терфейсе или данных класса письма требуют перекомпиляции большего объе­ ма кода. Конверт может содержать несколько вложенных классов писем. В главе 5 будет представлен простой класс, генерирующий атомарные лексемы. Класс General.Atom является конвертом по отношению к классам писем Pu nct, Name и Oper. В отли­ чие от класса Stri ng Rep, который остается невидимым для пользователя, эти клас­ сы могут напрямую задействоваться приложением. Письма могут объявляться в открытом интерфейсе конверта General.Atom. При обращении к таким вложенным классам должен присутствовать уточняющий префикс (вида General.Atom::Punct). Слишком большое количество классов писем в классе конверта усложняет ин­ терфейс последнего. Естественно, приложения, в которых классы писем являют­ ся производными от класса конверта (например, если классы CompLex, Biglnteger и DoublePrecision FLoat являются производными от класса N u m ber), не могут исполь­ зовать методику вложенных классов.

3 . 6 . О пер ато р ы

new

и delete

С++ предоставляет в распоряжение программиста возможность управлять вы­ делением памяти. Такое управление может играть важную роль для �Фокусов с памятью•, необходимых для эффективной работы объектов классов как кон­ кретных типов данных на некоторой платформе. Кроме того, управление памя­ тью позволяет контролировать выделение памяти на уровне классов согласо­ ванно с системой поддержки типов языка. По умолчанию программы, написан­ ные в большинстве реализаций С++, в конечном счете применяют примитивы maLLoc и free языка С для управления свободной памятью в адресном пространстве

3 .6. О ператоры

пользовательского процесса. Частичный объясняется несколькими причинами.

или

new и delete

85

полный отказ от этих примитивов

+ Может возникнуть необходимость в статическом распределении памяти ,

как во встроенных системах (скажем , в телекоммуникационной системе или системе управления полетом) , или в выделении памяти из фиксиро­ ванного пула. + Быстродействие функций mattoc и

free оказывается недостаточным для целей пользователя. Специализированный алгоритм может превзойти стандартные примитивы по производительности, особенно при большом количестве мел­ ких объектов.

+ Среда может не поддерживать функции управления памятью, поэтому их

приходится предоставлять программисту. Чтобы выделить память из динамического пула, программист вызывает опера­ тор new с указанием типа. Скажем, такая команда создает один объект типа i nt:

new i nt : А следующая команда создает вектор из десяти объектов типа int:

new i nt [ l O ] : При вызове new для типа компилятор обеспечивает возврат указателя на соот­ ветствующий тип; в нашем примере в обоих случаях это будет указатель i nt* . Показанная ниже команда выделяет память под вектор, размер которого был указан пользователем, и присваивает ia указатель на выделенный блок:

i nt *i a = new i nt [ s i ze ] : Динамически созданный вектор целых чисел не инициализируется, как и дина­ мически создаваемые экземпляры всех встроенных типов. Объекты классов, соз­ данные оператором new, инициализируются своими конструкторами. Чтобы вернуть память вектора объектов в динамический пул, следует выпол­ нить команду

del ete [ ] i a : При этом передается значение i a , полученное ранее при вызове new. Память, на которую ссылается указатель, возвращается в систему. Квадратные скобки при уничтожении динамически выделенного вектора объектов указывать жела­ телыю, а при уничтожении вектора объектов, для которых должен быть вызван деструктор, обязательно. Синглетный объект создается такой командой: -

ia

=

new i nt :

Этот объект освобождается командой

del ete i a : Для начала рассмотрим ситуацию, в которой мы хотим полностью контролиро­ вать управление памятью, например, если вы хотите использовать более быстрый

86

Глава 3. Конкретные типы да нных

алгоритм управления памятью, или если С + + работает в среде, не имеющей базовой поддержки (скажем, в автоно�шой программе на внесистемном микро­ процессоре). Обе задачи решаются переопределением операторов new и detete. Переопределенные версии этих операторов приведены в листинге 3. 12. Исполь­ зуется простой алгоритм, в которо�1 блоки памяти хранятся в едином списке и который удовлетворяет запросы выборкой первого блока достаточного раз­ мера. Блоки, находящиеся в списке, интерпретируются как экземпляры struct H ead специальной структуры данных для организации списка. -

Листинг З. 1 2 . Простой алгоритм распределения памяти для операторов new и delete

1 1 stddef . h для испол ь з о в а н и я s i ze_t #i nc l ude st ruct Head { l ong l ength : struct Head *next : }; stati c Head pool stat i c Head *h

=

=

{ О. О }:

( Head * ) HEAP_BASE_ADDRESS :

/* Быстрый . но г лупый а л г оритм выделени я перво г о блока дост аточ но г о раз мера */ typedef char *Cha r_p : 1 1 СЛЕДУЮЩАЯ СТРОКА ЗАВИСИТ ОТ ПЛАТФОРМЫ И КОМПИЛЯТОРА : const l ong WRDS I Z E = s i zeof ( voi d* ) : voi d* operator new ( s i ze_t nbytes ) { /* Нач инаем с поиска в списке */ i f ( pool . next ) { Head *prev = &pool : for ( Head *cu r &pool : cur : cur c u r - >next ) i f ( cu r - >l ength >= nbytes ) { prev - >next = cur->next : return ( voi d* ) ( Ch a r_p ( cu r ) + s i zeof( Head ) ) : el se prev cur : =

=

=

} 1 1 В сп иске нет подход яще г о блока . 1 1 запрос и т ь новый блок и з куч и h ( Head* ) ( ( ( i nt ) ( ( Ch a r_p ( h )+WRDS I ZE - 1 ) ) /WRDS I Z E > *WRDS I ZE ) : h - >next = О : =

3 .6. Операторы new и delete

87

h - >l ength = nbytes : h += nbytes + s i zeof ( Head ) : retu rn ( voi d* ) ( Char_p ( h ) + s i zeof( Head ) ) :

voi d operator del ete ( voi d *ptr )

{

Head *р ( Head * ) ( Char_p ( pt r ) - s i zeof( Head ) ) : p ->next pool . next : pool . next = р : =

=

Теперь каждый раз, когда в функuии С++ встретится вызов оператора new или delete, вместо операторов стандартной библиотеки будут вызываться перегру­ женные версии. Алгоритм управления памятью, задействованный перегружен­ ными версиями операторов, может быть абсолютно произвольным; алгоритмы, показанные в листинге 3. 1 2, используют блок кучи, который начинается с адреса H EAP_BASE_ADDRESS и распространяется до бесконечности. Другой распространенный трюк основан на специализированном распределе­ нии памяти на уровне классов. Он имеет свои преимущества даже в операuи ­ онных системах с собственными примитивами управления памятью, потому что пользователь может предоставить алгоритм, учитывающий особенности конкретного класса (в отличие от универсальных примитивов операционной системы). Для классов, часто создающих мелкие объекты (по эмпирической оценке - от 4 до 10 байт), применение перегруженных версий позволяет уско­ рить работу программы на порядок по сравнению со стандартными примити­ вами на базе функции m a LLoc. Кроме того, механизмы специализированного распределения памяти помогают более экономно расходовать память, посколь­ ку позволяют избегать присущих maUoc затрат от 4 до 8 байт на каждый выде­ ленный блок. Для любого класса создаваемые объекты всегда имеют одинаковый размер. Ино­ гда для объектов, создаваемых динамически оператором new, заранее выделяется память блоками, кратными размеру объекта класса. Каждый класс поддерживает собственный пул объектов, размер которых в точности соответствует запросам оператора new. В листинге 3. 1 3 объявляется класс Stri n g с собственными операторами выде­ ления памяти, подменяющими семантику операторов new и delete глобального уровня. В остальном открытый интерфейс String остается таким же, как и без механизма специализированного управления памятью. Объединение ( union) в реа­ лизации String позволяет рассматривать блок памяn1 либо как активный объект String (в этом случае содержимое объединения интерпретируется как указа­ тель на представление строки), либо как блок памяти в списке свободных бло­ ков (и тогда содержимое интерпретируется как ссылка в списке). Начало списка определяется закрытой статической переменной newlist.

88

Глава 3 . Кон кретные типы данных

Л истинг З. 1 З. Объявление объекта String со специализированными

операторами управления памятью

11 При м и т и в н а я строка ( не конкретный тип данных) 1 1 для демонстрации операторов new и del ete c l a s s Stri ng { рuЫ i c : St ri ng ( ) { rep = new char [ l ] : *rep = Stri ng ( const char *s ) { rep = new cha r [ : : st r l en ( s ) + l ] : : : st rcpy ( rep . s ) : } -St ri ng ( ) { del ete [ ] rep : voi d* operator new ( s i ze t ) : voi d operator del ete ( vo i d* ) : / / Дру г ие опера ции 11 . . . pri vate : stati c St ri ng *newl i st : uni on { Stri ng *freepo1 nter : cha r *rep : }: i nt l en : }:

'

\О : } '

В листинге 3. 1 4 представлены операторы new и detete для класса String; они замеща­ ют стандартные примитивы управления памятью при построении объектов String. Следующее выражение выделяет память для объекта Stri ng, после чего вызыва­ ется конструктор Stri n g для его инициализации:

new St ri ng В конструкторе память представления строки выделяется из кучи в виде сим­ вольного вектора; при этом вместо оператора new класса Stri ng используется сис­ темная версия. Приведенная здесь реализация ведет связанный список блоков памяти, размер которых соответствует размеру экземпляра Stri ng; уничтоженный объект Stri ng помещается в список, и запросы на создание новых экземпляров удовлетворяются удалением блоков из списка. Если список окажется исчерпан­ ным, создается новый пул из 1 00 объектов Stri n g ; как известно, однократный вы­ зов mattoc для большого блока обходится дешевле, чем многократный вызов для маленьких блоков. Листин г 3. 1 4 . Операторы new и delete класса Stri n g

Str1 ng *Stri ng : : newl i st =

О:

voi d St ri ng : : operator del ete ( voi d *р ) Stri ng *s = ( St r i ng * ) р : s - >freepoi nter = newl i st : newl i st = s :

3 .6. Операторы new и delete

89

voi d* Stri ng : : operator new ( s i ze_t s i ze ) { i f ( s i ze ! = s i zeof ( Stri ng ) ) { 1 1 Эта проверка п ри наследовании необходима 11 ( с м . главу 4 ) . ко гда произ водный класс 1 1 не содержи т опера тора new . 1 1 St ri ng : : operator del ete ( с м . выше ) 1 1 освободит увеличенный блок . return ma l l oc ( s i ze ) : } el se i f ( ! newl i st > { newl i st = ( Stri ng * ) new cha r [ l O O * s i zeof( St ri ng ) J : for ( i nt i = О : i < 99 : i ++ > { newl i st [ i ] . freepoi nter = & < newl i st [ i +l ] ) : } newl i st [ i J . freepoi nter =О : Stri ng *savenew = newl i st : newl i st = newl i st - >freepoi nter : return savenew : Абстракция Stri ng скрывает все подробности управления памятью, причем объ­ ект Stri ng может использоваться программами точно так же, как и без специаль­ ного механизма управления памятью. В следующем фрагменте компилятор ин­ терпретирует вызов new String как запрос на выделение памяти оператором new класса String, если такой оператор существует, и стандартной системной версией ::operator new при его отсутствии:

i nt ma i n O St ri ng del ete return

{ *р = new St ri ng ( " abcdef" ) : р: О:

Слегка видоизмененная схема распределения памяти работает в приложениях, в которых функции operator new и operator deLete используют статически выде­ ленную память. Более того, программа даже упрощается, поскольку при исчер­ пании всех заранее выделенных блоков происходит ошибка ( например, вызов системной функции восстановления) вместо попытки выделить дополнитель­ ные блоки и включить их в пул. Пример реализации такого рода приведен в лис­ тингах 3. 1 5 и 3. 1 6. Листин r 3 . 1 5 . Объявление класса String, использующеrо статический пул памяти

c l a s s Stri ng { pri vate : stat i c St ri ng *newl i st : uni on { Stri ng *freepoi nter : char *rep :

продолжение .9

90

Глава 3. Конкретные типы данных

Листинг З. 1 5 (продолжение)

puЬl i c :

}: i nt 1 en :

enum { POOLS I Z E = 1000 } : St ri ng ( ) { rep = new cha r [ l ] : *rep = ' \ О ' : } St ri ng ( const cha r *s ) { rep = new cha r [ : : strl en ( s ) + l J : : : strcpy ( rep . s ) : } -St ri ng ( ) { del ete[ J rep : voi d *operator new ( s i ze_t ) : voi d operator del ete ( voi d* ) : 11 . . . 1 1 Дру г ие оnера ции }; Л истинг 3 . 1 6 . Операторы new и delete класса String с о статическим пулом памяти

St ri ng* Stri ng : : newl i st = О : stat i c unsi gned char memPool [Stri ng : : POOLSI ZE * s i zeof ( Stri ng ) J : stat i c const unsi gned cha r *memPool End = memPool + St ri ng : : POOLS I ZE + s i zeof( Stri ng ) : voi d Stri ng : : operator del ete ( vo i d *р ) Stri ng *s = ( Stri ng * ) р : s ->freepoi nter = newl i st : newl i st = s : voi d* Stri ng : : operator new ( s i ze_t s i ze ) i f ( s i ze ! = s i zeof ( St r i ng ) ) { 1 1 Эта nроверка при н аследовании необходима 1 1 С с м . г л а ву 4 ) . когда n роиз водный класс 1 1 не содержи т оператора new . return : : ma l l oc ( s i ze ) : el se i f ( newl i st == memPoo l End ) Stri ngOutOfМemory C ) : return О el se i f ( ! newl i st ) { newl i st = ( Stri ng* ) memPool : for ( i nt i = О : i < POOLS I ZE - 1 : i ++ ) { newl i st [ i ] . freepoi nter = & ( newl i st [ i +l J ) : } newl i st [ i ] . freepoi nter memPool End : =

St ri ng *sa venew = newl i st : newl i st = newl i st - >freepoi nter : return sa venew :

3.7 . Отделение ини циализации от создания экземпляра

91

Векторы всех типов создаются глобальным оператором new. Даже если класс Stri ng содержит собственный оператор new, следующий вызов выделяет один блок памяти размером 25 *sizeof(String) байт, причем этот блок выделяется функ­ uией operator new:

St ri ng *sp = new St ri ng [ 25J : Дело в том, что объект выделяется как вектор, а векторы являются артефакта­ ми С, а не объектами классов С++.

3 . 7 . Отдел ен и е и н и ц и ал и з а ц ии от создан и я экз емп ля р а Иногда бывает важно отделить создтше эюе.�тляра, то есть процесс выделения основных ресурсов для объекта, от ииициш�u.зации настройки начального объ­ екта. Такое разделение играет важную роль при инициализаuии (исходной или повторной) в автономных :\1ИКропроцессорных системах. Кроме того, оно может пригодиться при инициализации объектов, содержащих взаимные ссылки. -

В качестве примера первого случая рассмотрим систему управления реального времени, работающую на автономном микропроцессоре. Система инициализиру­ ется подсистемой инициализации, задающей точный порядок вызова конструк­ торов для глобальных объектов. Например, глобальные таблицы, ссылки на ко­ торые хранятся в постоянной памяти, должны инициализироваться на ранней стадии с фиксированными адресами, чтобы они могли использоваться зашитым в постоянной памяти кодом инициализации. Размер и местонахождение этих объектов в памяти фиксируется, но содержимое инициализируется на стадии выполнения программы. Если во время выполнения в системе произойдет испра­ вимая ошибка, возможно, потребуется заново инициализировать испорченные объекты «на месте� . оставив остальные объекты без изменений. Например, сбой­ ное периферийное устройство может быть заменено без остановки процессора ( важный фактор в непрерывно работающих системах), после чего потребуется заново выполнить конструктор периферийного устройства. Другие объекты про­ должат обращаться к объекту периферийного устройства по исходному адресу. Рассмотрим другой пример: программу-редактор с классами Editor и Window; каж­ дый класс содержит члены, ссылающиеся на экземпляр другого класса. Для каж­ дого объекта существование другого является условием его собственной инициа­ лизации, поэтому между объектами возникает циклическая зависимость. Проблема может бьпь решена разделением инициализации между конструктором и функци­ ей класса и обработкой взаимных ссылок в функции после того, как оба объекта будут созданы и (частично) инициализированы. С другой стороны, это проти­ воречит главной причине создания конструкторов - гарантии автоматической инициализации объектов, чтобы программисту не приходилось помнить о вы­ зове функции инициализации. В нашем при:-.1ере появляется конструктор, по­ сле выполнения которого объект остается инициализированным лишь частично.

92

Глава 3. Конкретные типы данных

Подобные •эрзац-решения• неуклюжи и неполноценны - инициализацию про­ ще отделить от создания объекта средствами языка программирования. В этом разделе показано, как без применения доморощенных решений сначала создать экземпляры объектов обоих классов, а затем инициализировать оба объекта. Такой подход выходит за пределы 4СТандартного• набора приемов программиро­ вания на С++ и потому требует определенной осторожности. В С++ связывание инициализации с созданием объекта избавляет пользователя от необходимости при создании объекта помнить о вызове специальной функции инициализации. С другой стороны, аналогичные проблемы характерны и для уничтожения объекта с освобождением ресурсов, поэтому этот подход следует использовать только в том случае, если того требуют обстоятельства. Пример будет представлен далее, но мы еще вернемся к этой теме в главе 9 при рассмотрении более серьезных идиом. Вашему _вниманию предлагается листинг 3. 1 7 . Мы перегружаем оператор new с дополнительным параметром - адресом в памяти, по которому должен разме­ щаться объект. Тело оператора new просто возвращает свой аргумент. Новая вер­ сия оператора new вызывается только при вызове new с дополнительным пара­ метром в синтаксисе вида new ( адрес_о6ъекта ) тип ( параметры . . . ) Листинг 3 . 1 7 . Явное создание и уничтожение экземпляров объектов

#i ncl ude voi d *operator new ( s i ze_t . voi d *р ) { return р : } struct S { S( ) : -S ( ) : }; cha r buf [ s i zeof ( S ) J [ lOOJ : i nt bufl ndex = О : voi d f( ) S а:

1 1 Автома т ическое соз дание/ и н и ци ал и з а ц и я объекта

S *р = new S : del ete р : S *ррр = new ( buf) S : ppp - >S : : -S ( ) :

// 11 11 // 11

Я в ное соз дание объекта Оператор new находи т п а м я т ь Ав томатическа я инициализация Явное удаление Автоматическа я з а ч истка

/ / Яв ное соз дание объекта в buf ! / Автоматическа я инициализация / / Я в н а я зач ист ка

11 " а " а втомат ически деи н и циал и з и руется и унич тожается 11 при воз вращени и и з f ( )

3.7. Отделение инициализаци и от с оздания э кземпляра

93

Такой вызов противоречит семантике автоматического размещения объектов в памяти и сводится к простому вызову конструктора для объекта, местонахо­ ждение которого известно заранее. Аналогичным образом можно применить деструктор к объекту без освобождения памяти, для чего деструктор напрямую вызывается в программе:

адрес_обьекта - > тип : : -тип ( ) ; Общий результат: сначала в программе происходит выделение памяти, а затем инициализация как отдельная выполняемая по отдельному запросу операция. Объект может размещаться где угодно, а вместо buf может передаваться произ­ вольный указатель.

С ОВЕТ

Будьте внимательны и н икогда не вызыва й те оператор delete для о бъекто в , созданных подо б ны м способ ом. Диспетчер памяти оши б очно решит, что часть л окально й памяти на самом деле б ыла взята из кучи. С корее в сего, в озникнет хаос .

В листинге 3. 1 8 представлен другой пример с единственным глобальным объек­ том foobar типа Foo. Память для объекта резервируется на стадии компиляции; это можно сделать так, что программа будет работать на примитивной микропро­ цессорной платформе, для которой не существует операционных систем с под­ держкой динамического распределения памяти. А может быть, значения, пе­ редаваемые конструктору Foo при инициализации foobar, должны генерировать­ ся в результате неких вычислений в m ai n или в другой функции. Кроме того , в этом примере одна и та же память (foobar) в разное время может использовать­ ся двумя независимыми объектами класса Foo. Л истинг 3 . 1 8 . Явное с о зда ние и уничтожение объектов в заранее выделенной

памяти

#i ncl ude #i ncl ude voi d *operator new ( s i ze_t . voi d *р ) { return р : } cl ass Foo { рuЫ i c : Foo ( ) { rep = 1 : } Foo ( i nt i ) { гер = i : } -Foo ( ) { rep = О : } voi d pri nt ( ) { cout « гер « " \ n " : } pri vate : i nt гер : }: struct { i nt : O : } : 1 1 Маши н но - з а виси мое выра в н и вание cha r fooba r [ s i zeof C Foo ) J : Foo foefum :

продолжение.Р

94

Глава З. К онкретные типы данных

Л истинг 3 . 1 8 (продолжение)

i nt ma i n ( )

{

foefum . pri nt ( ) ; ( &foefum ) ->Foo : : -Foo ( ) : / / Вызывает преждев ременную зачистку 1 1 глобал ьного объекта foefum . pri nt ( ) ; 1 1 Неопределенный результат ! Foo *fooptr new ( fooba r ) Foo : foopt r - >Foo : : -Foo ( ) : fooptr new ( fooba r ) Foo ( l ) ; / / Не с в я зано с п редыдущим 11 созданием объекта foopt r - >Foo : : -Foo ( ) : / / Преждевремен н а я зачистка 1 1 НЕ ВЫЗЫВАЙТЕ del ete для foopt r ! foopt r = new Foo : del ete foopt r : =

=

Преждевременная зачистка объекта foefu m в листинге 3 . 1 8 не должна выпол­ няться, если среда вызывает деструкторы глобальных переменных при выходе из программы; в этом случае деструктор для foefum будет вызван дважды! Такое реше­ ние безопасно только при повторном создании foefum (командой new (&foefum) Foo ), если платформа не вызывает деструкторы при выходе из программы, или если объект был размещен в памяти, первоначально не выделенной для объекта класса (как в случае с footpr).

Уп р ажнен ия 1 . Опишите различия между структурами, классами, абстрактными типами дан­ ных и конкретными типами данных.

2. Перечислите, сравните и выделите различия между известными вам механиз­ мами абстракций в языке С++. 3. Измените класс Stri n g таким образом, чтобы он позволял извлекать подстро­ ки из существующей строки, оставляя ее без изменений:

St r i ng s

=

" a bcdefg " ;

Stri ng t = s ( j . k ) : Здесь j определяет начало подстроки ( 1 соответствует первому символу). Ес­ ли параметр j отрицателен, то значение задается от конца строки в обратном направлении, а последний символ задается значением - 1 (обозначение кон­ ца строк нуль-символами используется на более низком уровне в языке С). Если параметр k отрицателен, то j интерпретируется как индекс конца строки, а подстрока отсчитывается справа налево. 4. Подумайте, как решить проблемы с выходом за пределы строки в предыду­ щем примере.

Уп ражнени я

3.

95

Конструктор X::X(const Х&) является частью канонической формы класса. В ча­ стности, он используется для копирования параметров, переданных при вы­ зове функций, в стек и создания •копий• переменных вызывающей стороны в кадре стека. Покажите, что произойдет с объектом класса Х, у которого име­ ется конструктор Х::Х(Х) , но нет канонического конструктора X: :X(const Х&).

6. Напишите конструктор Node, который бы получал строку, созданную функ­

цией Stri ng: :Stri ng ( Node), и строил по ней все дерево.

7. Напишите распределитель памяти, использующий только статически выде­ ленную память, то есть память, местонахождение и размер которой фиксиру­ ется программой до запуска. Что делать, когда вся свободная память будет исчерпана? 8. Что произойдет, если оператор new выдешп блок большеrо размера, чем требу­

ется для объектов его класса? Сможете ли вы найти практические примене­ ния для такой возможности? А если будет выделен блок меньшего размера?

9. Усовершенствуйте функцию pri ntf, написанную вами для упражнения к при­ ложению А (или доработайте код pri ntf в своей версии С), чтобы функция поддерживала формат °loS и соответствующий аргумент String. 1 0 . Покажите, что если все классы будут следовать канонической форме, пред­

ставленной выше для Stri ng с оператором -> (см. листинг 3. 10), то программе С++ ни�огда не придется задействовать указатели на объекты класса на уров­ не приложения (то есть вне классов, использующих функцию operator->).

1 1 . Имеется следующий фрагмент: c l a s s Stri ng puЫ i c : Stri ng ( char* s > { . . . } operator const char*( ) const {

.

.

.

}

}: extern С { i nt open ( ch a r* . i nt . . . ) : "

"

i nt open ( Stri ng . i nt

..):

Что именно произойдет при выполнении каждой из следующих команд? Почему? Являются ли они семантически идентичными? Почему? ( Попро­ буйте включить в программу команды трассировки. чтобы разобраться в про­ исходящем). i nt i = open ( " abcd " . O_RDONLY ) : i nt j = open ( Stri ng ( " a bcd " . O_RDONLY ) : St ri ng a bcd = " a bcd " : i nt k = open ( abcd . O_RDONLY ) :

96

Глава З. Конкретные типы данных

Возможно, это упражнение поможет вам научиться избегать неоднозначно­ стей в своих разработках. В общем случае перегрузка должна рассматривать­ ся как системнъtй фактор проектирования, чтобы ваши объявления не кон­ фликтовали с объявлениями других программистов.

12. Возьмите достаточно большую программу С++ и измените ее таким образом,

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

13. Предложите другой способ реализации функции strten(Stri ng) с использова­ нием решений из раздела 3.3 (вместо преобразования типа). 14. Начните с класса Stri ng, приведенного на с. 53. Включите перегруженную версию функции operator&(), возвращающую объект StringPointer. Объекты StrintPointer должны работать в программе всюду, где может использоваться объект String*. Завершите реализацию Stri n g Poi nter, включив в нее перегруженные версии функций operator->() и operator* {). Внесите необходимые изменения в класс String. Где бы вы использовали эти два класса? Сравните это решение с тем, которое описано на с. 77.

1 5. Создайте новый класс Int с перегруженным оператором +, при котором сле­ дующие строки означали бы бит 25 OR бит 1 5 OR бит 14 OR бит 2, то есть 0200 140004: I nt j =

i

i

= О. j = О:

+

25

+

15

+

14

+

2:

Реализуйте аналогичную версию оператора - (минус) для сброса битов. Где можно использовать такой класс? Какие проблемы он создает? ( Задача была предложена в качестве примера в [ 1 ) ).

Л итература Lippman, Stanley В . �л С++ Primer•, Reading, Mass.: Addison-Wesley, 1989.

Гл ава 4

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

98

Глава

4.

Наследование

компилятором позволяет организовать замену типов. Как показано в главе 6, от­ крытое наследование класса В от класса А означает, что объекты класса В могут быть задействованы всюду, где допустимо использовать объекты класса А. Ис­ ходный класс, возглавляющий иерархию, называется базовым классом. В С++ базовым классом также называется непосредственный •предок• любого класса; в других языках программирования используется еще термин суперкласс. Новый класс, наследующий свойства родительского класса, то есть •потомок•, называ­ ется производиым классом; в других языках также встречается термин субкласс. В свою очередь, производный класс может быть базовым по отношению к друго­ му классу. Иерархии наследования могут иметь произвольную глубину, хотя на практике обычно используется не более трех-четырех уровней. Классы в иерархии наследования могут рассматриваться как разные формы аб­ стракции, задаваемой базовым классом, а код, написанный для базового класса, будет работать с любой из этих форм. Полимор физм, то есть способность ин­ терпретировать разные формы класса как одну форму, - мощный механизм абстракции, скрывающий от программиста подробности реализации производ­ ных классов. Наследование позволяет установить связи между классами прило­ жения, представляющими сходные ресурсы, поставщиков, клиентов и т. д. Такое применение наследования при проектировании упрощает управление 6оль­ шими системами. Наследование также может рассматриваться как средство определения новых классов, последовательно уточняющих существующий класс. Если вам потре­ буется новый класс, который ведет себя как существующий класс со слегка измененной реализацией, наследование позволит выразить эту концепцию. Механизм наследования дает возможность определить новый класс на основе существующего класса, слегка изменить то, что требуется, и использовать гото­ вый код остальной части класса, то есть наследование может рассматриваться как альтернатива условной компиляции с применением директив #if и #ifdef или принятию решений во время выполнения программы (см. раздел 7.9). Новый класс наследует поведение существующего класса и изменяет его, подменяя отдельные функции. Таким образом, наследование может применяться для двух слегка различающих­ ся целей. +

Наследоваиие служит для отражеиия семаитических отиошен.ий между клас­ сами. Иногда такие отношения возникают в результате эволюции, когда од­ ни функции производного класса уточняют или специализируют поведение базового класса, а другие функции наследуются без изменений. Также на­ следование может отражать существенное сходство между двумя существую­ щими абстракциями - настолько сильное, что объект производного класса может использоваться везде, где ожидается объект базового класса. При реа­ лизации подобных отношений обычно применяются: +

открытое наследование;



виртуальные функции классов.

4. 1 . Простое наследование

+

99

Наследоваиие обеспечивает м1юzократиое использоваиие кода. Хотя производ­ ный класс может не обладать всеми свойствами базового класса, он может быть настолько близок к нему, что возможность многократного использова­ ния готового кода оправдает наследование. Хотя классы очереди и цикличе­ ского списка не являются взаимозаменяемыми, они содержат достаточно большой объем общего кода, чтобы наследование упростило развитие этих двух абстракций. При реализации этих отношений обычно применяются: +

закрытое наследование;

+

невиртуальные функции классов.

Впрочем, эти рекомендации не следует считать истиной в последней инстанции. Они позволяют лишь дать представление о том, что вас ждет впереди, и пока­ зать, как эта глава связана со следующим материалом. Выбор между открытым и закрытым вариантами наследования в большей степени определяется соображе­ ниями проектирования, нежели реализации, поэтому в главе 6 они будут рас­ сматриваться отдельно. Рассмотрение виртуальных функций откладывается до знакомства с объектно-ориентированным программированием в главе 5. Из при­ меров этой главы класс Path Name ориентируется на многократное использование кода, а в классах Imaginary и TeLephone воплощено моделирование семантических связей между классами. Обсуждение каиоиической формы наследоваиия. тоже придется отложить до гла­ вы 5. Дело в том, что разобраться во всех нюансах можно лишь при понимании концепций виртуальных функций и множественного наследования, а эти вопро­ сы изучаются позже. Множественное наследование уместно рассматривать при описании объектно-ориентированного программирования. Но без виртуальных функций от множественного наследования особой пользы нет, поэтому разби­ рать здесь изощренные примеры с множественным наследованием было бы бес­ смысленно.

4 . 1 . П ростое наследование Наше знакомство с наследованием начнется с класса CompLex (листинг 4. 1 ) . Ком­ плексные числа применяются в физике и прикладных дисциплинах как обобще­ ние концепции рациональных, мнимых, вещественных и целых чисел. Код клас­ сов RationaL, Imaginary и т. д. будет строиться последовательной модификацией класса CompLex. Эти классы идеально подходят для демонстрации наследования. Листинr 4. 1 . Простой класс Complex

c l a s s Compl ex { fri end Imagi nary : puЬ l i c : Comp l ex ( douЫ e r = О . douЫ e i = 0 ) : rpa rt ( r ) . i pa rt ( i ) { } Compl ex < const Compl ex &с > : rpa rt ( c . rpa rt ) . i pa rt ( c . i part ) { }

продолжение..Р

1 00

Глава 4. Наследование

Листинr 4. 1 (продолжение)

Compl ex& operator= ( const Compl ex &с > { rpa rt = c . rpa rt : i part = c . i pa rt : return *thi s :

}

Compl ex operator+ ( const Compl ex &с ) const { return Compl ex ( rpa rt + c . rpa rt . i pa rt + c . i pa rt ) :

}

fri end Compl ex operator+ ( douЫ e d . const Compl ex &с > { return с + Compl ex ( d ) : fri end Compl ex operator+ ( i nt i . const Compl ex &с > { return с + Compl ex ( i ) :

}

Compl ex operato r - ( const Compl ex &cl ) const { return Compl ex( rpa rt - cl . rpa rt . i pa rt - c l . i pa rt ) :

}

fri end Compl ex operator - ( douЬl e d . const Compl ex &с ) { return - с + Compl ex ( d ) :

}

fri end Comp l ex operator - ( i nt i . const Compl ex &с ) { return - с + Compl ex ( i ) : } Compl ex operator* ( const Compl ex &с > const { return Compl ex ( rpa rt*c . rpa rt - i pa rt*c . i pa rt . rpa rt*c . i pa rt + c . rpa rt*i pa rt ) :

}

fri end Compl ex operator* ( douЫ e d . const Compl ex& с > return с * Compl ex ( d ) :

}

fri end Compl ex operator* ( i nt i . const Comp l ex& с > { return с * Compl ex ( i ) : } fri end ost ream& operator : -POTSPhone ( ) : pri vate : 1 1 Служебна я и нформация для под ключен и я телефона Frame frameNumberVal : Rack rackNumberVa l : Pa i r pa i rVa l : }:

1 1 I SDN - это цифровая сеть с конnлексныни услу г а м и cl ass I SDNPhone : puЫ i c Tel ephone { puЬl i c : I SDNPhone ( ) : I SDNPhone ( I SDNPhone& ) : -I SDNPhone ( ) : voi d sendBPacket ( ) : voi d sendDPacket ( ) : pri vate : Channel Ы . Ь2 . d :

}:

cl ass Pri ncessPhone : puЫ i c POTSPhone {

}:

4 . 2 . В идимость и у п равле н ие доступом И так, как было показано, наследование служит для объединения существующих типов и построения на их базе новых типов. Но если вспомнить о важности инкапсуляции и маскировки информации, мы также должны разобраться в том, как наследование влияет на доступ к членам классов - и напрямую связанных отношениями наследования, и классов, в которых один класс пользуется функ­ циональностью, предоставляемой другим классом. Доступ к членам классов, свя­ занных отношениями наследования, будет называться вертикальным; один из классов как бы находится выше, а друrой - ниже в иерархии наследования. Под горизонтальным доступом понимается внешний доступ к членам классов 4:ИЗ­ вне• , со стороны классов, равноправных с точки зрения структуры проrраммы. Наследование порождает целый ряд проблем в отношении доступа к членам классов, не характерных для простой схемы защиты, описанной в разделе 3.2. Например, доступен ли некоторый член класса для функций классов, производ­ ных от неrо? А для третьеrо класса, использующеrо функциональность объекта одноrо из производных классов?

1 06

Глава 4. Наследование

Верти кал ьное уп равление доступом при н аследовании Класс может выбирать, какие и з его членов должны быть доступны для произ­ водных классов. Производный класс не может обращаться к закрытым членам своего базового класса. Рассмотрим следующий фрагмент: c l ass А { puЫ i c : АО : -А О : pri vate : i nt v a l :

}:

c l ass В : puЬl i c А { puЫ i c : во : -в о :

}:

voi d func ( ) :

1 1 Не может обращаться к А : : va l

Н и одна функция класса В н е может обращаться к переменной va� хотя экземп­ ляр vaL хранится в каждом объекте В. • Заградительный барьер•, проходящий по границе вертикальной области видимости, полностью закрывает производным классам доступ к закрьпым членам базового класса. Производные классы не име­ ют права нарушать инкапсуляцию базового класса, как и все остальные классы . Спецификатор private делает переменную vaL недоступной для других классов, использующих классы А и В. Если бы переменная vaL была объявлена открытой, то она была бы доступна для функций класса В (и любого другого класса) . Если мы хотим открыть доступ к некоторому члену класса А, но не делать его обще­ доступным , следует объявить его защищенным (protected): c l ass А { puЬl i c : АО : -А ( ) : protected : i nt v a l :

}:

c l ass В : puЫ i c А { puЫ i c : во :

-В ( ) :

4.2. В идимость и управление доступом

}:

1 07

/ / Может обращаться к A : : va l

voi d func ( ) :

c l ass С : puЫ i c В { puЬl i c : со : -С ( ) :

}:

voi d func2 ( ) :

/ / Может обращат ься

к

A : : va l

Здесь переменная vaL по-прежнему ведет себя так. словно она является закрытой для всех, кроме функций производных классов и их друзей. Если потребуется ограничить доступ к переменной vaL так, чтобы она была дос­ тупна только для производного класса В. но не для С, можно воспользоваться конструкцией со спецификатором friend: cl ass А { fri end c l ass В : publ i c : pri 1,1ate : i nt v a l : }:

1 1 Недоступно для всех . кроме членов и друзей А .

c l ass В : puЫ i c А { puЫ i c :

}:

voi d func ( ) :

/ / Может обращаться к A : : va l

c l a s s С : puЫ i c А { puЫ i c : со :

-С О :

}:

voi d func3 ( ) :

/ / Не может обращаться

к

A : : va l

Здесь класс В связан с А особыми отношениями, разрешающими доступ к закры­ тым полям базового класса из производного класса; у класса С таких прав нет. При объявлении защищенного члена базового класса все производные классы как бы становятся дружественными для базового класса. Все они имеют доступ к защищенным членам базового класса, поэтому базовый класс не приходится изменять каждый раз, когда потребуется предоставить доступ к его членам ново­ му производному классу. Если бы мы использовали ключевое слово friend, при добавлении нового производного класса в базовый класс пришлось бы включать

1 08

Глава 4. Наследование

новую секцию friend. С другой стороны, доступ к защищенным членам предостав­ ляется всем производным классам без разбора, тогда как дружественные отно­ шения можно устанавливать на уровне отдельных классов. Защищенные члены доступны для друзей и функций производных классов, но только при обраще­ нии через указатель, ссылку или объект производного класса. Этим и объясняет­ ся то, что класс lmagi nary в листинге 4.1 был объявлен другом класса Comptex вместо объявления переменных базового класса защищенными. В частности, оператор присваивания производного класса должен иметь доступ к членам пе­ реданного объекта базового класса: I magi nary & I magi nary : : operator= ( const Compl ex &с ) rpa rt = О : i pa rt = c . i pa rt : return *th i s :

Если бы переменные базового класса rpart фрагмент вызвал бы ошибку компиляции.

и

ipart были защищенными, то этот

Рассмотрим еще один пример - класс PathName, созданный на базе класса String из главы 3: cl ass Stri ng puЬl i c : rep = new St ri ngRep ( " " ) : } Stri ng ( ) rep = s . rep : rep ->count++ : Stri ng ( const Stri ng& s ) { rep = new Stri ngRep ( s ) : } Stri ng( const char *s ) -St ri ng ( ) { i f ( - - rep - >count count++ : i f ( - - rep- >count rep : } St r i ng operator ( ) ( i nt . i nt ) const : 1 1 Выделение подстрок . см . упражнен и я к главе 3 { return : : strl en ( rep ->rep ) : i nt l ength ( ) const protected : Stri ngRep *rep : }:

Класс Path Name наследует свойства String: c l ass PathName : puЫ i c Stri ng puЫ i c : PathName ( const Stri ng& ) : PathName ( ) : baseNameRep ( " " ) . Stri ng ( ) { }

4.2.

В идимость и управление доступом

1 09

PathName ( const PathName &р > : Stri ng ( p ) . baseNameRep ( p . baseNameRep ) { /* Пусто */ PathName &operator= ( const PathName &р ) { Stri ng : : operator= ( p ) : / / Присваивание nодобъекта 11 базового класса baseNameRep p . baseNameRep ; return *th i s : } -PathName ( ) { /* Пусто */ } Stri ng baseName ( ) { return baseNameRep : Stri ng prefi x( ) : Stri ng suffi x( ) ; Stri ng ful l PathName ( ) { return *th i s ; } Stri ng di rName ( ) { return ( *thi s ) ( O . l ength ( ) - baseName ( ) . l ength ( ) ) ; } Stri ng changeBaseName ( const Stri ng &newFi l e ) ; pri vate : Stri ng baseNameRep : =

}:

Пользователь может вызвать любую открытую функцию класса String для объек­ та Path Name, потому что класс Path Name связан с классом String отношениями от­ крытого нас.ледования. Предположим, вы хотите запретить пользователю выпол­ нение операций с подстроками для Path Na me, чтобы полное имя файла нельзя было изменять напрямую, а лишь посредством высокоуровневых функций, рабо­ тающих на уровне компонентов пути вместо уровня отдельных символов ( ска­ жем, changeBaseName ). Один из вариантов - организовать перехват обращений во время выполнения и выводить сообщение об ошибке: c l ass PathName : puЬl i c Stri ng { puЬl i c : Stri ng operator ( ) ( i nt . i nt ) { pri ntf ( " l l l egal PathName substri ng\ n " ) ; return St r i ng ( " " ) :

}:

Но в этом случае недопустимые обращения обнаруживаются лишь на стадии вы­ полнения, тогда как подобные ошибки лучше перехватывать на стадии компиля­ ции. Чтобы получить гарантированную защиту от вызова operator()(int,int) для объектов PathName, следует переопределить защиту оператора в классе PathName и сделать ero недоступным. Получается, что функция operator()(int,int) может вы­ зываться для объектов String, но не для объектов PathN ame. Но из этого следует, что объекты PathName не наследуют свойства String, или, по крайней мере, насле­ дуют их не полностью.

1 1О

Глава 4. Наследование

Если нужно точно указать, какие компоненты интерфейса String должны быть доступны для кода, работающего с объектами класса PathName, следует приме­ нить закрытое наследование. Так, согласно следующему фрагменту только опе­ ратор преобразования const char* класса String «переноситсю> в интерфейс класса PathName: c l ass PathName : pri vate Stri ng { puЬl i c : Stri ng : : operator const char* : / / Из ба зового класса 11 Дру г ие функции PathName }: .

.

.

Такая конструкция называется спецификатором доступа. Она не создает новый оператор и не изменяет реализацию; просто оператор включается в открытый интерфейс PathName вместо того, чтобы скрываться из-за закрытого наследования. Другие операторы и функции класса - такие, как функция operator()(i nt,int) остаются закрытыми по отношению к Stri ng и не публикуются в интерфейсе Path Name. Если за пределами Stri ng или PathName встретится следующий фраг­ мент, компилятор выдаст сообщение о фатальной ошибке, в котором будет ска­ зано, что оператор объявлен закрытым: PathName р : .

.

.

p( l . 3) . . .

Тем не менее операторная функция базового класса Stri n g : :operator( ) (i nt,i nt) останется доступной для функций класса Path Name.

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

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

интерфейса базового класса будет доступна для клиентов производного класса. Эта возможность поддерживается двумя механизмами: разными режимами наследова­ ния, определяющими публикацию членов базового класса в интерфейсе производ­ ного класса, и использованием спецификаторов доступа на уровне отдельных членов. Самая распространенная конструкция управления доступом к унаследованным членам носит название открытого иаследования. Возьмем базовый класс А и про­ изводный класс В: c l ass А puЬl i c : }:

1 1 Базовый класс ( суnеркласс )

4 . 2. В идимость и управление доступом

111

c l ass В : puЬl i c А { puЫ i c : }:

При открытом наследовании все открытые члены А доступны как члены класса В. Но при использовании следующей записи ни один член класса А не будет дос­ тупен как член класса В пользователям класса В: c l ass А { puЫ i c :

// Базовый класс ( суперкласс )

}: c l ass В : pri vate А { puЫ i c : }:

Это называется закрытым наследованием. Впрочем, функции класса В сохраня­ ют доступ к открытым и защищенным функuиям класса А (см. далее).

Если класс закрыто наследует от другого класса, вы можете избирательно от­ крыть доступ к некоторым частям открытого интерфейса базового класса через открытый интерфейс производного класса. Для этого применяется второй вари­ ант управления доступом к унаследованным членам, а именно спеuификаторы доступа. Раньше этот вариант уже применялся для классов Stri ng и Path Name. Рассмотрим следующий класс: c l ass Li st { puЫ i c : v i rtual voi d *head ( ) : v i rtu a l voi d *tai l ( ) : v i rtua l i nt count ( ) ; vi rtual l ong has ( voi d* ) : v i rtua l voi d i nsert ( voi d* ) : }:

Рассмотрим еще один класс, закрыто наследующий от предыдущего: c l a s s Set : pri vate Li st { puЫ i c : voi d i nsert ( voi d *m ) : / / Специфика торы L i st : : count : / 1 доступа L i st : : has : }:

Класс множества Set использует функuиональность класса списка List в своей реализации, но посредством закрытого наследования скрывает некоторые откры­ тые функции своего базового класса (head и taiL), потому что они не имеют смыс­ ла в операциях с множествами. Остальные члены наследуются без изменений,

1 12

Глава 4. Наследование

кроме функции insert, семантика которой в множествах отличается от семантики в списках (список может содержать одинаковые элементы, а множество - нет). Обратите внимание: поступить наоборот нельзя. Например, следующий фрагмент недопустим: c l ass Set : puЬl i c L i st pri vate : 1 1 Недопустимо L i st : : head : 1 1 Недопустимо L i st : : ta; 1 : puЫ i c : voi d i nsert ( voi d *m ) :

}:

Компилятор выдаст для него сообщение об ошибке. Открытое наследование Set от List предполагает, что интерфейс Set содержит все компоненты, присутствую­ щие в интерфейсе List. А это означает, что объект Set должен уметь делать все, что делают объекты List. Система контроля типов С++ принимает объекты Set вместо объектов List, если они связаны открытым наследованием. Но при закры­ том наследовании возможны аномалии вроде voi d *l i sthead ( L i st 1 ) return l . head ( ) : i nt ma i n ( ) Set s : voi d *р = l i sthead ( s ) : return О :

Допустим ли вызов L.head()? Если компилятор разрешит его, это может препод­ нести сюрприз программисту, указавшему, что функция head является закрытой для объектов Set, и приведет к нарушению инкапсуляции. А так как Listhead мо­ жет компилироваться отдельно от mai n(), компилятор в этом примере даже не сможет выдать диагностику на стадии компиляции. Чтобы застраховаться от подобной ситуации, компилятор запрещает саму возможность ее возникнове­ ния. Таким образом, доступ к символическому имени в производном классе с от­ крытым наследованием не может быть ограничен более жестко, чем в базовом классе. Все эти излишне подробные (на первый взгляд) пояснения имеют убедительную аналогию в объектно-ориентированном проектировании. Мы вернемся к этому вопросу в главе 6. На практике почти всегда следует прибегать к открытому наследованию. За­ крытое наследование обычно применяется для многократного использования кода базового класса в производном классе. Существует ряд других конструкций, которые рекомендуется задействовать вместо закрытого наследования (см. главу 6). К сожалению, в С++ наследование является закрытым по умолчанию (если

4 . 2 . В идимость и у пра вле ние доступом

1 13

ключевое слово, определяющее тип наследования, не указано), поэтому обычно приходится специально делать его открытым. Базовый класс не может принудительно сделать свои операции доступными че­ рез члены производного класса: это решение находится под контролем произ­ водных классов. Производный класс может определить функцию с таким же именем, как у функ­ ции базового класса. С точки зрения внешнего пользователя эта новая функция замещает определение базового класса, даже если функции различаются по количе­ ству/типу аргументов и возвращаемых значений. Пример приведен в листинге 4.3. Функции класса В могут ссылаться на функцию f класса А только с уточнением класса, а именно A: :f. Листинг 4.3. Наследован ие однои менных фун кций с разны ми сигнатурами

c l ass А { puЫ i c : voi d f( i nt ) : voi d g ( voi d* ) : 11 . . . };

/ / Ба зовый класс ( суперкласс )

c l ass В : puЫ i c А { puЫ i c : voi d f ( douЬl e ) : voi d g ( i nt ) : 11 . . .

1 1 Произ водный класс ( субкласс )

}:

voi d В : : g ( i nt k ) f( k ) ; А : : f( k ) : thi s - >A : : f( k ) : : : f(k ) : i nt ma i n ( ) А *а ; В *Ь ; i nt i : 11 . . a ->f( l ) : а - >f ( З . 0 ) : b->f( 2 ) ; b->f( 2 . 0 ) : a ->g ( O ) ; b - >g ( O ) ; b - >g ( &i ) :

11 11 11 11

В : : f ( douЬl e ) с повышен ием А: : f ( i nt ) : А : : f ( i nt ) : Недопус т и мо

.

1 1 A : : f( i nt )

1 1 А : : f( i nt ) с понижением i nt ( З . 0 )

11 11 11 11 11

В : : f( douЬl e ) с повышением douЬl e ( 2 ) В : : f( douЬl e ) А : : g ( voi d* ) В : : g ( i nt ) Ошибка : В : : g с крывает А : : g

1 14

Глава 4. Наследование

Если программа обращается к объекту класса через ссылку или указатель на ба­ зовый класс, она сможет вызывать только функции базового класса. Но если функция вызывается напрямую для экземпляра производного класса с исполь­ зованием синтаксиса обьект.функция, то вместо функции базового класса вызы­ вается одноименная функция производного класса. Пользователь класса может явно подменить видимость функции и вызвать функ­ цию определенного уровня. В листинге 4.4 приведена слегка измененная вер­ сия предыдущего примера с добавлением глобальной функции f(). Глобальная функция, по своим аргументам точно соответствующая вызову f(б), игнорируется в пользу функции B::f(douЬLe), замещающей глобальную функцию. Для вызова глобальной версии f используется оператор уточнения области видимости ::f(5). Листинг 4 . 4 . Вызов одноименных функций с разными областями видимости

voi d f ( i nt j ) { /* . . . */ } cl ass А { puЫ i c : voi d f ( i nt ) : 11 . . . }:

1 1 Ба зовый класс ( суnеркласс )

cl ass В : puЬl i c А { puЬl i c : voi d f ( douЬl e ) : voi d g ( i nt ) : 11 . . . }:

1 1 Произ водный класс ( субкласс )

voi d В : : g ( i nt k ) f( k ) : A : : f( k ) : thi s ->A : : f ( k ) ; : : f( k ) ; i nt ma i n ( ) А *а ; В *Ь ; i nt i : // . . . a - >f( l ) : а - >Н З . 0 ) : b - >f ( 2 ) ; b - >f ( 2 . 0 ) ; b - >A : : f ( 7 . 0 ) ; b ->g ( l0 ) :

1 1 В : : f( douЬl e ) с nовыwением / / А: : f ( i nt ) : / / A : : f ( i nt ) : 1 1 : : f ( i nt ) :

11 11 11 11 11 11

А : : f ( i nt ) A : : f ( i nt ) с понижением i nt ( З . 0 ) В : : f ( douЬl e ) с повышением douЬl e ( 2 ) В : : f ( douЬl e ) А : : f ( i nt ) с понижен ием i nt ( 7 . 0 ) В : : g ( i nt )

4. 2 .

В идимость и управление доступом

115

Создан ие э кзем пляр о в п уте м н аследо ван ия и уп ра вл ения доступ о м В некоторых ситуациях базовый класс существует только как основа для по­ строения производных классов. Например, приведенный ранее класс Telephone служит заготовкой для построения моделей снастоящих• телефонов со специа­ лизированным поведением: ISDN Phone для цифровых, POTSPhone для тради­ ционных. В классе Telephone декларируются общие аспекты поведения всех теле­ фонов, определяемые семантикой их функций классов. Этот класс также может содержать реализацию, общую для всех телефонов и не зависящую от разновид­ ности (например, номер). -

-

Используя средства ограничения доступа С++, класс может предоставить свой интерфейс как основу для интерфейса производных классов, одновременно пре­ дотвращая создание своих экземпляров. Рассмотрим следующий пример класса Telephone: c l ass Tel ephone { puЫ i c : -Tel ephone O : voi d ri ng ( ) : protected : Tel ephone < > : pri vate :

}: c l ass I SDNPhone : puЫ i c Tel ephone {

}: c l ass POTSPhone : puЫ i c Tel ephone {

}: Обратите внимание: конструктор Telephone::TeLephone() объявлен защищенным, а это предполагает, что он может быть свызван• только в производном классе. Семантика подобного объявления такова, что объект класса Telephone не может быть объявлен в программе. Абстрактный телефон не может существовать как таковой; программа может объявлять и создавать экземпляры только конкретных, снастоящих• телефонов вроде ISDN Phone или POTSPhone. Свойства, общие для всех обобщенных сабстрактных• телефонов, являются частью среального• телефона; конструктор базового класса неявно вызывается при создании экземпляра произ­ водного класса. Но экземпляр базового класса сам по себе создаваться не может. В действительности подобная защита от создания объектов типа Telephone не идеальна: эти объекты могут создаваться функциями любого подкласса Telephone или функциями, дружественными для Telephone (если они существуют).

116

Глава 4. Наследование

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

4 . 3 . Конструкторы и деструкторы Конструкторы и деструкторы представляют собой специальные функции класса, которые автоматически инициализируют новые объекты и освобождают задей­ ствованные ресурсы в конце жизненного цикла объектов. Объект производного класса содержит переменные из нескольких разных классов; конструкторы и де­ структоры этих классов вносят свой вклад в инициализацию и уничтожение объекта. В данном разделе рассматриваются конструкторы и деструкторы в. кон­ тексте наследования, то есть порядок их выполнения и механизмы вызова пара­ метризованных конструкторов базового класса.

П оря док вы полнения конст рукто р о в и деструкторов При наследовании работа с объектом осуществляется функциями его класса, а также всех родительских классов, расположенных выше в иерархии. Таким об­ разом, каждый объект содержит несколько конструкторов и деструкторов, а это означает, что логика инициализации и уничтожения объектов не сосредоточива­ ется в одной функции, а имеет распределенную природу. Каждый конструктор, вызываемый для выполнения своей части инициализации, знает, какие конст­ рукторы вызывались до него, а следовательно - какие аспекты состояния он мо­ жет безопасно использовать. Порядок выполнения конструкторов классов в иерархиях наследования четко опре­ делен. Компилятор автоматически организует выполнение конструкторов базовых классов и объектов, встроенных в создаваемый объект. Непосредственный код кон­ структора не завершает всей картины; по мере необходимости компилятор также ге­ нерирует собственный код для выделения памяти и вызова других конструкторов.

Динамическое выделение памяти производится при создании объекта с ключевым словом new. При этом вызывается либо операторная функция operator new этого класса (если она существует), либо глобальная операторная функция ::operator new. Без динамического создания объекта конструктор использует память, пре­ доставленную конструируемой переменной (для автоматических и глобальных переменных), или выделенную заранее иным способом (например, конструкто­ ром производного класса). Если конструктор принадлежит производному классу, он прежде всего должен вызвать конструктор своего базового класса. Это происходит автоматически, и программисту не приходится заботиться об этом; тем не менее проrраммис1' может до определенной степени управлять вызовом конструктора (см. далее). Вызванный конструктор видит, что память уже выделена, и в свою очередь вы­ зывает конструкторы своих базовых классов.

4 . 3 . Констру кто р ы и де стру кто р ы

1 17

Далее конструктор инициализирует все переменные своего класса, являющиеся объектами классов, вызывая их конструкторы. Обычно такие объекты инициализи­ руются своим конструктором по умолчанию (см. раздел 3. 1 ), но вы можете повли­ ять на выбор при помощи списка инициализаторов, о котором речь пойдет далее. После того как конструктор обеспечит инициализацию своего базового класса и внутренних объектов, выполняется тело конструктора, определенное програм­ мистом. В нем ранее инициализированным полям могут быть присвоены новые значения. Кроме того, тело конструктора должно инициализировать все пере­ менные своего класса, относящиеся к встроенным типам ( i nt, s h o rt, Lo n g , c h a r и double), а также все указатели. После завершения конструктора управление возвращается в точку его вызова. Конструктор базового класса возвращает управление конструктору производно­ го класса, из которого он был вызван, и инициализация объекта производного класса продолжается с точки возврата. Последним выполняется код инициализа­ ции листового (то есть 4Самого производного•) класса в иерархии наследования. При компиляции конструктора компилятор обычно включает в него скрытый код для управления памятью, диспетчеризации функций класса, вызова конст­ рукторов базовых классов, инициализации переменных класса, которые также являются объектами классов. Обычно пользователи просто ипюрируют присут­ ствие этого кода, но так как его структура зависит от установленного экземпляра и версии, не стоит полагаться на его конкретные особенности. Деструкторы выполняются в порядке, противоположном порядку вызова конструк­ торов: сначала вызывается деструктор класса объекта, затем деструкторы нестати­ ческих внутренних объектов, в последнюю очередь - деструкторы базовых классов. Если класс операнда (листового класса в иерархии наследования) содержит (или наследует) оператор delete, то вместо стандартной системной версии будет вызва­ на специализированная версия. Освобождение памяти, занимаемой объектом­ операндом, выполняется в последнюю очередь перед возвратом из деструктора.

П ередач а п арамет ров констру кто р ам базо в ы х классов и вн ут рен н и х объ ектов Вернемся к синтаксису конструктора Imaginary::Imaginary(double) на с. 1 00. Обра­ тите внимание: этот конструктор имеет пустое тело и выполняется только ради неких побочных эффектов. Если бы пользователь написал следующее простое определение, компилятор организовал бы вызов конструктора базового клас­ са по умолчанию Com pLex::Com pLex() в самом начале вызова Imaginary: :Imaginary (перед присваиванием):

Imag i nary : : I mag i nary ( douЫ e d ) i pa rt d : =

Благодаря этому обстоятельству класс Imaginary знает, что класс CompLex полно­ стью инициализирован, и его данные могут использоваться для построения класса Imaginary.

1 18

Глава 4. Наследование

Теоретически формальные параметры конструктора Imaginary могут влиять на работу конструкторов его базовых классов. У программиста имеется воз­ можность приказать компилятору вызвать конкретный конструктор базового класса. Так, в примере на с. 1 00 программист явно указал, что перед вызовом Imagi nary::Imaginary(douЬLe) должен быть вызван конструктор CompLex(O,i). Следует отметить, что конструктор Com pLex: :CompLex(dou ble, dou ЬLe) (см. лис­ тинг 4 . 1 ) определяет инициализацию переменных rpart и ipart. Следующие два конструктора работают практически одинаково: Imagi nary : : l magi na ry ( douЫ e а . douЫ e Ь ) rpart = а : i pa rt = Ь

I magi na ry : : Imagi na ry ( douЫ e а . douЫ e Ь ) rpa rt ( a ) . i pa rt ( Ь )

! * Пусто */

Прямой вызов конструктора, как в правом варианте, позволяет избежать лишней операции копирования. Эффект становится более очевидным в примере класса Path Name, где вместо простых машинных слов в новый объект копируется после­ довательность символов: PathName : : PathName ( ) { rep = Str i ng ( " " ) : baseNameRep = Stri ng ( " " ) :

PathName : : PathName ( ) baseNameRep ( " " ) . St ri ng ( ) { /* Пусто */ }

Кроме того, перед присваиванием какого-либо значения объект baseNameRep не­ обходимо инициализировать ; в левом примере конструктор String::String() вызы­ вается для baseNameRep после инициализации базового класса String, но до вы­ полнения команд конструктора PathName. Это означает, что переменная rep тоже инициализируется дважды - при неявном вызове конструктора базового класса и при явном вызове конструктора PathName. В конструкторе Path Name (const PathN ame&) встречается аналогичный синтак­ сис: конструктор базового класса Stri ng::Stri ng(const String&) вызывается с пара­ метром р. Он выполняется до того, как будет выполнен какой-либо код конст­ руктора Path Name. Другой пример передачи параметров конструктором при вызове конструктора ·базового класса встречается в объявлении класса Square в примере Shapes из при­ ложения Б. Квадрат (Square) является частным случаем прямоугольника ( Rect). Единственное различие между ними связано со способом их конструирования: квадрату достаточно одного параметра, определяющего длину стороны. Пробле­ ма решается в конструкторе Square простым вызовом конструктора Rect: cl ass Squa re : puЫ i c Rect { puЫ i c : Squa re( Coordi nate ctr . l ong х ) : Rect ( ct r . х . х ) { } }:

4.4. Преобразование указателей н а классы

1 19

Новая абстракция, обладающая всеми возможностями и аспектами поведения Rect, создается всего в четырех строках программного кода.

4 . 4 . П реобра з ование ука з ателе й на классы Возможности наследования могут использоваться для получения архитектурных преимуществ, упрощающих проектирование и сопровождение больших систем. Мощь наследования во многом обусловлена способностью интерпретации объек­ тов классов, входящих в иерархию наследования, как взаимозаменяемых абстрак­ ций. Любой объект производного класса можно интерпретировать как экземпляр класса, находящегося в корне иерархии. Данное свойство называется полимор­ физмом (иначе - •многообразие форм•); разные формы объектов рассматрива­ ются так, словно они принадлежат одному классу. Это означает, что код приложе­ ния может игнорировать подробности реализации конкретных типов, например телефонов POTSPhone и ISDN Phone, и обходиться с ними так, словно все они отно­ сятся к классу TeLephone (а если в отдельных случаях потребуется использо­ вать специфику конкретной разновидности телефона, код приложения может •спуститься• на нижний уровень абстракции и сделать все необходимое). В этом и в следующем разделах будет заложена основа для более элегантного и мощно­ го подхода, представленного в главе 5. Полиморфизм базируется на механизме преобразования указателей на объекты классов, связанных в иерархию наследования. Дело в том, что большинство про­ грамм С++ работает с объектами через указатели, поскольку объекты часто соз­ даются в куче. Указатели С++ обладают двумя преимуществами, не присущими обычным переменным. Во-первых, указатель на базовый класс может ссылаться на объект любого класса, н аходящегося на более низком уровне иерархии. Во­ вторых, функции класса, вызываемые через указатель, могут выбираться не во время компиляции, а во время выполнения программы (для чего они должны быть объявлены виртушzьными). Эти два свойства имеют важнейшее значение для объектно-ориентированного программирования (см. главу 5). Если перемен­ ная содержит ссылку на объект класса, то функции класса, вызываемые через эту переменную, тоже выбираются полиморфно; тем не менее, на практике чаще применяются указатели. Прямой вызов функций для объекта в точечной записи (например, officePhone.ring) такой возможности не дает. В общем случае указатель на объект некоторого класса С может использоваться для хранения адреса объекта любого из его производных классов - прямого потомка, •внука• или любого производного класса п-го поколения. Обратное возможно не всегда. Например, указатель TeLephone * может ссылаться на объект TeLephone, ISDN Phone и даже POTSPhone. Класс POTSPhone может рассматриваться как частный случай класса TeLephone, потому что он обладает всеми возможно­ стями последнего. Но обратная интерпретация недопустима, поскольку не все аспекты поведения POTSPhone поддерживаются классом TeLephone; они не явля­ ются общими для всех телефонов. Ошибка может быть обнаружена на стадии компиляции .

1 20

Глава

4.

Наследование

Механизм явного преобразования типов позволяет преобразовать указатель на базовый класс в указатель на производный класс. Применяя этот механизм, вы сообщаете компилятору, что у вас имеется информация о типе объекта, которой не обл адает компилятор. Но подобные претензии часто опасны, особенно если они основаны на анализе разрозненных фрагментов кода. Если функции базового класса в равной степени применимы к объектам любых производных классов, то указатель на базовый класс можно использовать для вызова функций классов любых объектов иерархии, даже если конкретный тип объекта неизвестен на стад ии компиляции. Это удобно во многих ситуациях, на­ пример, функции, не входящие в иерархию TeLephone, могут применять общие операции •телефонных• классов к объектам телефонов, не зная, с какой кон­ кретной разновидностью телефона они работают в данный момент. В следую­ щем примере функция ri ng Phone получает при вызове вектор разнородных указа­ телей на ТeLephone: voi d ri ngPhones C Tel ephone *phoneArray [ ] )

{

for C Te l ephone *р p - >ri ng ( ) ;

=

phoneArray ; р ; р++ )

i nt ma i n ( ) { Tel ephone *phoneArray [ l O J ; i nt i = О : phoneArray [ i ++ ] = new POTSPhone ( " 5384" ) ; phoneArray[ i ++J = new POTSPhone C " 5 0 1 0 " ) : phoneArray [ i ++ J = new I SDNPhone ( " О " ) : phoneArray [ i ++J = new POTSPhone ( " 5383 " ) : phoneArray [ i ++J = О ; ri ngPhones ( phoneArray ) ;

Такое решение работает: для каждого объекта в массиве phoneArray будет вызва­ на функция TeLephone: : ring. С другой стороны, это решение не позволяет задейст­ вовать дополнительные возможности данных или функций, специфических для производных классов, потому что ri ng Phones знает лишь то, что известно о базо­ вом классе TeLephone на стадии компиляции. Для наглядности добавим в иерар­ хию класс OperatorPhone со специализированной операцией ri ng, а затем выпол­ ним слегка измененную программу main: cl ass OperatorPhone : puЫ i c I SDNPhone { puЫ i c : OperatorPhone ( ) ;

4.5. Селектор тиnа

}:

1 21

OperatorPhone ( OperatorPhone& ) : -OperatorPhone ( ) : voi d ri ng ( ) : / / Специальная опера ция ri ng 1 1 для класса OperatorPhone

i nt ma i n ( )

{

Tel ephone *phoneArray [ l O J : i nt i = О : phoneAr ray [ i ++J = new POTSPhone( " 5384 " ) : phoneArray [ i ++ ] = new POTSPhone ( " 50 1 0 " ) :

1 1 Имеет специализ и рованную фун кцию r i ng : phoneArray [ i ++ ] = new OperatorPhone( " O " ) : phoneArray [ i ++ ] phoneArray [ i ++ ]

=

=

new POTSPhone ( " 5383 " ) : О:

Так как функции ringPhones известно лишь то, что элементы вектора указывают на объекты TeLe p hone, она вызывает для каждого объекта функцию TeLe p hone::ring. Хотя мы хотели, чтобы для объекта O p eratorPhone вызывалась специальная функ­ ция ring, но у компилятора для этого было недостаточно информации. В главе 5 будет показано, как компилятор может автоматически передавать информацию о контексте вызова посредством виртуальных функций. Далее рассматривается другое (�неполноценное• ) решение, основанное на включении в каждый объект

поля селектора типа.

4 . 5 . С електор ти па Селектором типа называется переменная базового класса, значение которой определяет фактический тип содержащего ее объекта. Механизм виртуальных функций (см. главу 5) считается более предпочтительным решением, но селекто­ ры типов задействованы в некоторых нетривиальных идиомах. Мы рассмотрим их здесь как для использования в будущем, так и для логического моделирова­ ния механизма, автоматизируемого при помощи виртуальных функций. В листинге 4.5 приведена очередная версия класса TeLe p hone с полем селектора типа, идентифицирующим разные объекты телефонов. Селектор присутствует в объектах всех разновидностей телефонов, поэтому мы всегда можем обратить­ ся к объекту, на который указывает TeLe p hone* , и узнать его тип. Обратите вни­ мание: конструкторы всех классов, производных от TeLe p hone, заносят информа­ цию о себе в новое поле.

1 22

Глава 4. Наследование

Листинг 4. 5 . Иерархия классов Telephone с селектором типа

c l a s s Tel ephone { puЬl i c : enum PhoneType { POTS . I SDN . OPERATOR . ОТНЕR } phoneType ( ) { return phoneTypeVa l : voi d ri ng ( ) : Bool i sOnHook ( ) : Bool i sTa l k i ng C ) : Bool i sDi a l 1 ng ( ) : Di gi tStri ng col l ectDi g i ts ( ) : L i neNumber extens i on ( ) : / / Не подменяется -Tel ephone ( ) : protected : Li neNumber extens i onData : PhoneType phoneTypeVa l : Tel ephone ( ) : }: c l a s s POTSPhone : puЬl i c Tel ephone { puЫ i c : Bool runDi agnosti cs ( ) : POTSPhone ( ) : phoneTypeVa l C POTS )

. . .

{

} POTSPhone C POTSPhone &р) : phoneTypeVa l C POTS )

.

.

.

{

} -POTSPhone ( ) : pri vate : F rame frameNumberVa l : Rack rackNumberVa l : Pai r pa i rVa l : }: c l ass I SDNPhone : puЬl i c Tel ephone { puЬl i c : I SDNPhone ( ) : phoneTypeVa l C I SDN ) { } I SDNPhone ( I SDNPhone &р ) : phoneTypeVa l C I SDN ) .

} - I SDNPhone ( ) : voi d sendBPacket ( ) : voi d sendDPacket ( ) : pri vate : Channel Ы . Ь2 . d : }:

.

.

/ / Отпра вка пакета по каналу В / / Отпра в ка па кета по каналу О

4.5. Селектор ти п а

1 23

Теперь функцию ringPhones можно записать так : voi d r i ngPhones C Tel ephone *phoneArray[ ] )

{

for ( Te l ephone *р = phoneArray : р ; р++ ) swi tch ( p - >PhoneType ( ) ) { case Tel ephone : : POTS : ( ( POTSPhone * ) p ) - >ri ng ( ) : break : case Tel ephone : : I SDN : ( ( l SDNPhone * ) p ) ->ri ng ( ) : break : case Tel ephone : : OPERATOR : ( ( OperatorPhone * ) p ) ->r1 ng ( ) : brea k : case Tel ephone : : OTHER : defaul t : error ( . . . ) :

После этоrо для любой фактической разновидности телефона будет вызвана пра­ вильная операция ring. В функцию даже можно встроить некое подобие интеллек­ та: в первых двух случаях достаточно вызова p ->ring(), поскольку ни POTSPhone, ни в ISDN Phone не имеют специализированной операции ring. Но и в этом виде функция успешно работает; более тоrо, даже если в класс POTSPhone или ISDN Phone будет включена своя операция ring, код все равно останется работоспособным. В этом варианте функция ringPhones выбирает для объекта функцию на основании поля, содержимое которого проверяется на стадии выполнения. Но компилятор •знает• класс объекта при его создании и может сгенерировать код, связываю­ ший информацию о типе с каждым объектом. Если вызвать для такого •расши­ ренного• объекта функцию ring, компилятор на основании информации о типе объекта сможет автоматически выбрать вызываемую функцию rin'g. На этом прин­ ципе работают виртуальные функции С++ (эта тема главы 5). Обычно виртуаль­ ные функции считаются более предпочтительным решением, нежели селекторы типов. В сообществе С++ не принято пользоваться селекторами типов, поэтому предыдущий фрагмент приводится только для демонстрационных целей. Селекторы типов используются в идиомах, которые будут представлены позд­ нее, в том числе при имитащш мультиметодов (выборе функции на стадии вы­ полнения в зависимости от типа нескольких параметров) в разделе 9.7. Другой пример имеется среди примеров функторов в разделе 5.6. Идиома расширяемого поля типа довольно подробно описана в [ 1 ]. Рассмотрим основные недостатки решения с селектором типа. При выборе опе­ раций на уровне исходного текста, как в команде switch в ri n g P h ones, разви­ тие программы быстро становится утомительным и неудобным. Возможно, при включении операции ring в класс POTSPhone вам даже не придется переписывать программу, но в большинстве реализаций С++ ее необходимо перекомпилиро­ вать. Также представьте, к чему приведет создание нового класса телефона: вам

1 24

Глава

4.

Наследование

придется изменять функцию ring Phones, а также все аНШ1оzuчные функции. В пе­ речисляемый тип базового класса включается новый элемент, а разработчик но­ вого класса должен помнить о необходимости инициализировать поле типа во всех конструкторах. Весь код, связанный с изменениями в программе, придется перекомпилировать и протестировать заново.

Упраж н е н и я 1 . Охарактеризуйте возможные применения закрытого наследования.

2. Предложите способы имитации абстракпии данных и наследования в С. 3. Рассмотрите возможные применения идиомы •манипулятор/тело» для управ­ ления памятью и обеспечения необходимой гибкости на стадии выполнения (то есть ситуации, в которых основной класс использует внутренний объект вспомогательного класса, содержащий большую часть подробностей реализа­ ции). Иногда это делается для того, чтобы изолировать приложение от изме­ нений в реализации, например, от изменений в структуре данных. Общая функциональность объекта разделяется между манипулятором и телом по аналогии с разделением функциональности между базовым и производным классами при наследовании. Наследование может быть реализовано двумя способами: от внешнего класса манипулятора или от внутреннего класса тела; в обоих случаях используется один и тот же конверт. Проанализируйте оба варианта, их достоинства и не­ достатки. Существуют ли другие варианты?

4. Откомпилируйте следующую программу: #i nc l ude c l a s s ZooAni ma l protected : i nt zooloc : puЫ i c : i nt cnt :

}:

c l a s s Bea r : puЫ i c ZooAn i ma l puЬl i c : i nt fi nd ( ZooAni ma l * ) :

}:

i nt Bea r : : fi nd ( ZooAn i ma l *pz ) { i f ( cnt ) cout cnt ) cout zooloc :

Л итература

1 25

Что происходит при компиляции? Какой из этого можно сделать вывод отно­ сительно модели защиты С++ применительно к объектам и классам? Можно ли обойти эту защиту? И если можно, то как? (См. [2].) :) . Определите классы для системы управления лифтами, в которой одновре­ менно работают несколько лифтов. Какие операции должны быть определе­ ны для классов этой системы? Все ли класс ы вашей системы представляют •общие сущности•? Почему? 6. Проанализируйте (и откомпилируйте) следующую программу: c l ass А { puЬl i c : A( i nt i = 0 ) { а = i ; } А& operator= ( A& х ) { а = х . а : return *thi s : } protected : i nt а : }: c l a s s В : puЫ i c А { puЫ i c : B ( i nt i = 0 ) : A( i ) { Ь = i : } В& operator= ( B& х ) { Ь = х . Ь : return *thi s : } pri vate : i nt Ь : }: i nt ma i n ( ) { в ы . Ь2 ( 2 ) : Ы

=

Ы:

Какое значение будет присвоено переменной Ы.а? Какой из этого нужно сде­ лать вывод о реализации присваивания в условиях наследования? Как решить проблему в данном случае?

Л и тер атура 1. Stroustrup, В . •The С+ + Programming Language• , 2nd ed. Reading, Mass.: Addison-Wesley, 1 99 1 , ch. 13. 2. Lippman, Stanley В. • А С++ Primer•, Reading, Mass.: Addison-Wesley, 1989.

Глава 5

О б ъектн о - о р и ен ти ро в а н н о е п рогра м м и р ова н и е Объектно-ориентированным называется стиль программирования, отталкиваю­ щийся от инкапсуляции и абстрактных типов данных. В языке программирова­ ния и среде разработки абстрактные типы данных представлены системой кон­ троля типов. Эта система выполняет проверку типов, следя за тем, чтобы для выражения а +Ь, в котором оба слагаемых объявлены с типом double, была сгенери­ рована машинная команда суммирования с двойной точностью. Языки с систе­ мами контроля типов изначально создавались для генерирования кода , более эф­ фективного по сравнению с кодом нетипизованных программ, но •безопасность типов• тоже была достаточно важным фактором. Большинство систем контроля типов реализуется в компиляторе и сопровож­ дающих утилитах. Все, о чем уже рассказывалось, относится к стадии компиля­ ции и не проявляется напрямую во время работы программы. Однако компиля­ тор может выполнить преобразование тип а или оптимизацию, из-з а которой некоторые аспекты системы контроля типов проявятся во время выполнения. Рассмотрим простую последовательность команд: i nt i : douЫ e d : d

=

i;

Компилятор долже н сгенерировать код преобразования целого числа в веще­ ственное с двойной точностью во время вы.полиения. Сходство между типами int и double делает такое преобразование возможным и даже естественным; до опре­ деленной степени эти типы •совместимы• . В языках вроде С все типы определя­ ются заранее , компилятор знает все операции, применимые к этим типам, может интерпретировать их в текущем контексте и генерировать код выполнения опе­ раций или преобразования между типами. Но Jl С++ ситуация из-за классов осложняется. Программист может опреде­ лять новые типы (например, конкретные типы данных, о которых рассказыва­ лось в главе 3) и отношения между ними - такие как отношения наследования (см. гл аву 4). С++ позволяет конструировать системы, основанные на иерархи-

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

1 27

ческом принципе, - абстракции, естественной для человеческого разума. Эти иерархии и типы выходят за пределы того , что известно компилятору, поэтому, чтобы обеспечить нужную степень ссовместимости•, естественную для сходных классов, пользователь должен предоставить компилятору дополнительную ин­ формацию. Количество иерархий классов в большой системе должно быть разумным, даже если этого нельзя сказать о количестве отдельных классов. Чтобы в полной мере использовать мощь иерархии, программист д олжен иметь возможность эф­ фективно работать на верхних уровнях иерархии, не опускаясь к производным классам. Этот принцип распространяется как на проектирование, так и на реали­ зацию. Программа, написанная для высокоуровневой абстракции SignedQuantity (величина со знаком), на стадии выполнения может работать со значениями классов CompLex (комплексное число) или Infi nite P recision (число с неограни­ ченной точностью), если два последних класса входят в иерархию SignedQuantity. Если программист использует следующую запись , то операция сложения для соответствующего класса должна выбираться на основании контекста во время выполнения: voi d afunct i on < Si gnedQuanti ty &с . Si gnedQuant i ty &d ) { . . . c+d . . .

Такой подход выходит за рамки системы типов стадии компиляции, но ради эф­ фективности и безопасности типов средства времени выполнения «принимают эстафету• у средств времени компиляции. В этом заключается сущ1lость обь­ ектио-ориеитированного программироваиuя. Появляется новая система контро­ ля типов , основанная на инкапсуляции абстрактных типов данных, организован­ ных в иерархии, с поддержкой типов на стадии выполнения. С++ удерживает равновесие между системой контроля типов времени компиля­ ции, обеспечивающей эффективность кода, и системой времени выполнения, обеспечивающей гибкость и ссовместимость• программных компонентов. Язык предоставляет основные средства для объектно-ориентированного программиро­ вания, но сохраняет многие конструкции для «культурной• совместимости с С. Объектно-ориентированное программирование в каком-то смысле представляет собой • трюк•, основанный на косвенных обращениях; хорошие программисты пользовались этим трюком в течение многих лет. Особого внимания заслуживают два обстоятельства: важность поддержки этой методики на уровне языка и еще более важная роль тех принципов проектирован ия, которые стоят за ними. Во-первых, С++ выводит эту методику из области трюков и делает ее полноцен­ ной составляющей языка, благодаря чему упрощается чтение, написание и со­ провождение программ. Конечно, такое развитие можно только приветствовать, но для эффективного применения подобных приемов программист должен хоро­ шо вл адеть несколькими каноническими формами и языковыми идиомами, вы­ ходящими за пределы базового синтаксиса. С++ предоставляет дополнительный логический уровень при вызове функций; программисты, использующие это

1 28

Глава

5.

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

обстоятельство для повышения гибкости или для применения общих абстрак­ ций в другом измерении, также нуждаются в дополнительных идиомах. Такие идиомы описаны в этой главе. Они занимают одно из важнейших мест в книге. Во-вторых, важны не конкретные приемы, а принципы проектирования, которые в них воплощаются. Главным условием написания хорошей программы является не столько оптимальное применение синтаксиса, сколько квалифицированные глубокие мыслительные процессы ее создателей. В этом контексте данная глава закладывает основу для приемов проектирования, рассматриваемых в главе 6, где описаны центральные концепции всей книги. Но и программист, и проекти­ ровщик должны помнить, что приемы проектирования наиболее эффективно применяются в контексте описываемых далее идиом.

5 . 1 . И дентификация ти пов на стади и вып олн ен ия и виртуал ьн ы е фун кции

·

Механизм наследования, описанный в предыдущей главе, позволяет программи­ сту по-новому организовать программу, ориентированную на многократное ис­ пользование кода. Код, общий для нескольких классов, выделяется в базовый класс, а производные классы рас ширяют поведение базового класса для созда­ ния более специализированных абстракций. Переменная, объявленная как указатель на базовый класс, может применяться для обращения к объекту любого производного класса и вызова общих ( принадлежащих базовому классу) функ­ ций этого объекта. Таким образом становится возможным полиморфизм , когда один указатель годится для работы с объектами нескольких разных форм. Про­ граммист объявляет указатель, который может ссылаться на любой объект из на­ бора классов в иерархии наследования. Через этот указатель программа работает одинаково со всеми объектами, не зн ая их фактического класса. Каждый объект интерпретируется в контексте своего обобщенного типа, базового класса иерархии. Наследование позволяет применять общий набор функций к объектам, классы которых являются «соседями• и •дальними родственниками• в дереве наследо­ вания. Но если для поддержки полиморфизма используется только наследова­ ние, то он распространяется исключительно на функции с общей реализацией. Если производный класс содержит собственную версию реализации функции базового класса, то одно лишь наследование не приведет к автоматическому вы­ бору версии производного класса при вызове через переменную, объявленную с типом базового класса. В соответствии с заданным типом указатель знает толь­ ко о базовом классе и понятия не имеет не то что о структуре, но и о самом факте существования производных классов. Для примера рассмотрим иерархию с базовым классом Emptoyee, изображенную на рис. 5. 1 . Если программа спо­ нимает• общий класс E m p Loyee, но не знает о существовании производных классов SecurityGuard и Manager, она использует стандартную версию функции computeRaise для всех объектов в иерархии Emptoyee. У нее просто не хватает информации для более разумного поведения.

5. 1 . Идентификация типов на стадии выполнения и виртуал ьные функции

1 29

clpuЫi assc:Employee { douЫe sal ary() {retum sal{retum ; } 25; } douЫe computeRai s e{) Employee(douЬle salary) {sal salary; } private: };douЬle sal; =

clpuЫi asscManager: public Employee { : douЫe computeRai sae{)ry);{ retum 100; } Manager(douЬl e sal }; Employee(salary) О

clpuЫi asscSecuri tyGuard: puЫic Employee { : douЫe computeRai see()sal{retum 50; } Securi tyG uard(doubl a ry); }; Employee(salary) О

Manager *Ьoss1 arynewЬoss1 мanager(2000); douЬl e Ьoss1Sal salary(); 1111 резжнультат 2000 Empl o yee*Ьoss2 new Manager(2300); double boss2Salary Ьoss2 salary(); 11 мвоозsроащает 2300 douЫe Ьoss1Rai boss1 computeRai 1111 ррезультат езультат 25100 douЫe Ьoss2Raissee Ьoss2 computeRaisse(); e(); Если тип указатепя точнссылок вкласса о на объ кт озвращается правил ьный Н для указатель с и л о е е базовый баз rо класса; надруrи класс, вызываются ф нкц в у й нны ии обобщоденны о о для ука атепя е ноиме е функции =

=

->

=

->

=

результат.

=

->

=

->

// МОЖНО

соответствует типу объекта, производноrо

только в классе объекта невидимы

используется з

.

Рис. 5. 1 . Ограниченный полиморфизм с использованием наследования

Базовый класс должен иметь выразительный интерфейс, который бы характе­ ризовал поведеиие представляемой им абстракции в терминах имен, типов па­ раметров и возвращаемых типов ero функций. Открытые функции базовоrо класса либо становятся частью интерфейса производноrо класса, либо замеща­ ются в производном классе более подходящей функцией с тем же лоrическим поведением. Все аспекты поведения базового класса распространяются на объек­ ты производных классов, поэтому базовый класс определяет поведение классов, производных от неrо. Мы использовали схему наследования в rлаве 4 при определении разных версий функции ring для разных телефонов. П роизводный класс может подмеиитъ реа­ лизацию функции, заданную в базовом классе, иначе rоворя, он предоставляет собственную реализацию функции, входящей в открытый интерфейс базовоrо класса. Замена функции базовоrо класса одноименной функцией производного класса позволяет оптимизировать реализацию с сохранением основополаrающе­ rо смысла указанной функции. Применение функции ring к телефонам разных классов всеrда имеет одинаковый смысл, хотя подробности реализации меняют­ ся в зависимости от типа телефона.

1 30

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

Взаимозаменяемость этих объектов должна быть как можно более прозрачной на уровне компилятора. Компилятор знает, что некоторая функция входит в интерфейс базового класса, но мы хотим, чтобы компилятор автоматически вызывал вер­ сию этой функции из соответствующего производного класса - того, к которо­ му относился объект при создании. Выбор функции должен определяться классом обьекта, а не объявлением указателя, используемого для ссылки на него. В резуль­ тате программист может считать объекты всех производных классов эквива­ лентными в контексте базового класса, а в программе произойдет именно то, что требовалось: для объектов Manager будет вызываться своя версия функции computeRaise, хотя на стадии компиляции при вызове этой функции будет сгене­ рирован код, рассчитанный на обобщенный класс EmpLoyee. Этот механизм н азмвается идентификацией операций на стадии вьтолнения. В сочетании с наследованием он реализует вид полиморфизма, предоставляю­ щий в распоряжение программиста и проектировщика гибкий инструментарий для создания высокоуровневых программных компонентов. Стиль програм­ мирования, в котором этот механизм логически последовательно применяется к конкретным типам данных, называется обьектно-ориентированньt.М програм­ мированием; эта методика является наиболее прямолинейным способом под­ держки объектно-ориентированного проектирования в С++. Идентификация операций на стадии выполнения применима только к указате­ лям и ссылкам на объекты классов. Почему? Потому что только переменные, объявленные как указатели и ссылки, могут работать как с объектами своего класса, так и с объектами других производных классов. В частности, это можно объяснить тем, что информация о типе теряется только при использовании ука­ зателя: чтобы напрямую вызвать функцию класса для экземпляра, вы должн ы располагать объявлением класса объекта, и привязка вызова будет осуществ­ ляться на стадии компиляции. Но если указатель на объект находится далеко от точки создания объекта, пользователю не обязательно передавать весь �лишний груз• подробностей типа в любую часть программы, имеющую дело с объектом. Давайте вернемся к примеру класса TeLephone из главы 4 и реализуем его пра­ вw�ыщ используя объектную парадигму. На самом деле изменений будет отно­ сительно немного. Для удобства оригинал приведен в листинге 5. 1 . Листинr 5 . 1 . Иерархия с классом Telephone

c l a s s Tel ephone { puЬl i c : voi d ri ng ( ) : Bool i sOnHook ( ) . i sTa l k i ng ( ) . i sDi a l i ng ( ) : Di gi tStri ng col l ectDi g i t s ( ) : Li neNumber extens i on ( ) : -Tel ephone ( ) : protected : Tel ephone ( ) : L i neNumber extensi onData : }:

5.1 .

Идентификация типов на стадии вып олнения и виртуальные функции

1 31

c l ass POTSPhone : puЫ i c Tel ephone { puЫ i c : Bool runDi agnost i c s ( ) : POTSPhone ( ) : POTSPhone ( PQTSPhone& ) : -POTSPhone ( ) : pri vate : Frame frameNumberVal : Rack rackNumberVa l : Pa i r pai rVa l :

};

c l ass I SDNPhone : puЫ i c Tel ephone { puЫ i c : I SDNPhone ( ) : I SDNPhone ( I SDNPhone& ) : - I SDNPhone ( ) ; voi d sendBPacket ( ) . sendDPacket ( ) : pri vate : Channel Ы . Ь2 . d : }; c l ass OperatorPhone : puЬl i c I SDNPhone { puЫ i c : OperatorPhone ( ) ; OperatorPhone < OperatorPhone& ) : - Ope ratorPhone < ) ; voi d ri ng ( ) ; }; cl ass Pri ncessPhone : puЬl i c POTSPhone { };

О братите внимание: используются версии классов без встроенного селектора ти ­ па, представленного в главе 4. Теперь он стал лишним - компилятор автомати ­ чески обеспечивает идентификацию типов . Некоторые функции базового класса объявлены виртуальными; производные класс ы могут содержать спец иализ и­ рованные версии этих функций . П р и вызове виртуальных функций компилятор генерирует специальный код, чтобы нужная версия функ ции выбиралась во вре­ мя выполнения программы в зависимости от типа объекта. Кажд ый раз, когда в программе создается объект класса, содержащего виртуальные функции , ком­ пилятор сохраняет в нем • поле типа• . (Детали реализации зависят от компиля ­ тора , но на концептуал ьном уровне все сводится к применению селектора типа, упомин авш егося в р азделе 4 .5 . П оле используется компилятором, и язык не предоставляет программисту средств для работы с его содержимым.) Более того, компилятор работает с этим полем более эффективно , чем п ри ручном кодиро ­ ва нии, и с меньшими последствиями при модификации . Например, включение нового класса в программу с реализованным вручную полем типа потребует

1 32

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

глобального редактирования и перекомпиляции. При наличии виртуальных функций в большинстве сред программирования С++ достаточно перекомпили­ ровать исходный код, относящийся непосредственно к новому классу. Вручиую встав.ллт ь поля идентификации типов в ЮLассы не рекомендуется, поскольку это затрудняет дальнейшее развитие программы. Превращение класса Tetephone в объектно-ориентированный начинается с объяв­ ления виртуальных функций. Объявляя функции базового класса виртуальны­ ми, мы сообщаем компилятору, что их поведение и реализация определяются фактическим клаесом объекта , для которого эти функции вызываются. Естест­ венно, базовый класс не может предвидеть потребностей всех производных клас­ сов, которые могут быть созданы на протяжении жизненного цикла программы. Объявление виртуальных функций позволяет предоставить новые реализации этих функций в классах, производных от того базового класса, в котором функ­ ции объявлены виртуальными. Таким образом, виртуальная функция может существовать в множестве разных версий - в кр а йнем случае каждый класс в иерархии наследования данного базового класса получит собственную версию функции. В базовом классе обычно определяется тело виртуальной функции, которое по умолчанию наследуется производным классом, если последний не подменяет его реализацию. Если производный класс не переопределяет функцию, то он ведет себя так, словно функция базового класса является ч астью его интерфейса. Если же функция подменяется в одном или нескольких производных классах, то версия производного класса замещает версию б азового класса. Виртуальные функции отличаются от остальных функций тем, что в ерсия производного клас­ са доминирует всегда, даже если объект производного класс а используется в кон­ тексте, в котором ожидается объект базового класса . При использовании виртуальных функций компилятор организует необходи­ мую поддержку на стадии выполнения, чтобы при вызов е функции, объявлен­ ной виртуальной в базовом классе, вызывалась функция соответствующего про­ изводного кл асса. Операция должна соответствов ать типу объекта, к которому она применяется, а ие типу указателя, использов анного для вызова (рис. 5.2). Вот как выглядит базовый класс с объявлениями виртуальных функций: c l ass Tel ephone { puЬl i c : vi rtua l voi d ri ng ( ) ; v i rtua l Bool i sOnHook ( ) ; vi rtual Bool i sTa l k i ng ( ) ; vi rtua l Bool i sDi a l i ng ( ) ; vi rtua l Di gi tStri ng col l ectDi gi ts ( ) ; v i rtua l -Tel ephone ( ) ; L i neNumber extensi on ( ) ; protected : Li neNumber extens i onData ; Tel ephone ( ) ; }:

5. 1 . И денти ф и каци я типов на стадии выполнения и виртуальные функции

POTSPhone *J oseph Tel ephone *j oe

j oe

ri ng ( ) :

->

j oseph

->

- -

=

joseph

ri ng ( ) :

-r\ �

j oseph :

r i ng ( ) :

1 1



POTSPhone : : ri ng ( ) :



- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

new I SDNPhone :

->

=

идентктОбъект ф цируетс типоя м с фаPOTSPhoпe иически им

new POTSPhone :

-------------------------�

joseph j oe

D

>
PhoneType ( ) ) { case Tel ephone : : POTS : ( ( POTSPhone * ) p ) ->ri ng ( ) : brea k : case Tel ephone : : I SDN : ( ( l SDNPhone * ) p ) ->ri ng ( ) : brea k : case Tel ephone : : OPERATOR : ( ( QperatorPhone * ) p ) ->ri ng ( ) : brea k : case Tel ephone : : OTHER : defa u l t : error( . . . ) :

Новая версия гораздо проще: voi d ri ngPhones ( Tel ephone *phoneArray [ ] ) { for ( Tel ephone *р = phoneArray : р ; р++ ) p - >ri ng ( ) : Если указатель р ссылается на объект OperatorPhone, то конструкция p->nng() вы­ зовет функцию OperatorPhone: :nng(). Если он ссылается на любой другой объект в иерархии TeLephone, то вместо нее будет вызвана функция TeLephone::nng(), по­ скольку в других классах сохраняется реализация этой функции по умолчанию. Однако этот механизм работает только в том случае, если функция объявлена виртуальной в базовом классе. Рассмотрим следующий фрагмент:

Tel ephone *di gi tal Phone = new I SDNPhone : d i g i ta l Phone->sendPacket ( ) :

/ / Неверно . функци я не на й дена

Этот пример не работает, поскольку функция sendPacket не объявлена в базовом классе TeLephone. Даже если объявить ее виртуальной в своем классе, это не по­ м ожет (хотя и обеспечит виртуальное поведение в классах, производных от ISDNPhone). Чтобы операции идентифицировались на стадии выполнения, функ­ ция должна быть виртуальной. Функция класса может стать виртуальной одним из двух способов. Во-первых, функция считается виртуальной, если она была объ­ явлена таковой в классе, использованном для объявления указателя. через который вызывается операция. Во-вторых, функция класса также считается виртуальной, если выше в иерархии наследования присутствует одноименная функция с такими же типами параметров и возвращаемого значения (рис. 5.3). Виртуальность функ­ ции наследуется производными классами до самого дна иерархии наследования.

1 35

5 .2. Взаи модействие деструкторов и виртуальные деструкторы

А.

В. С. О. Е. F. G.

классов

Функции

v i rtua l ri ng v i rtual i sOnHook

at

Класс

v i rtua l i sTal k i ng v 1 rtua l i sOea l i ng

at

Tel ephone

v i rtual col l ectDi gi ts

Резул ьтат

at

Вызов

->

l i neCurrent ( )

->

->

ri ng O

sendDPacket ( )

vi rtua l l i neNL111b er

н

G

Оwибка комп иляции

L i neCur rent

Н . r i ng I . i sOnHook

J . i sTa l k i ng

К . i sOea l i ng L . col l ectDi g i ts

IS

���� e n

--

М. 1 i neNumЬer

-

Tel ephone �aPhone new I SDNPhone :

N . sendBPacket

О . sendOPacket

������ ��"!����

- - - - -

=

-

- -



Рис. 5 . 3 . Наследование « виртуал ьности • фун кций

Ключевое слово virtuaL можно рассматривать и иначе: так, словно оно объявляет не функцию класса, а имя функции класса. Объявления обычных (не виртуаль­ ных) функций создают функции, тело которых связывается с именем на стадии компиляции или компоновки. К виртуальным функциям это не относится. Свя­ зывание имени виртуальной функций с телом производится при ее вызове на стадии выполнения. В этом смысле виртуальные функции аналогичны селекто­ рам методов в языке SmallTalk, а тела функций - самим методам.

5 . 2 . В заимоде й ствие деструкторов и ви ртуал ь н ые де стру кто ры Обратите внимание: деструктор TeLephone::-TeLephone тоже объявлен виртуальным. На первый взгляд это выглядит несколько странно, потому что обычно мы не соби­ раемся вьtЗЬlвать деструктор в своей программе. Но на самом деле очень важно, чтобы деструктор был объявлен виртуальным. Рассмотрим следующий пример: c l ass Tel ephone { -Tel ephone ( ) : }; Tel ephone *di gi ta l Phone = new I SDNPhone : del ete di gi t a l Phone :

Что происходит? Вызванный оператор deLete не знает, что объект относится к типу ISDN Phone, а в классе TeLephone ничто не указывает, что деструктор тоже должен

1 36

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

выбираться на стадии выполнения. Следовательно, в программе будет вызван деструктор TeLephone. А это означает, что ресурсы, которые были выделены кон­ структором ISDN Phone и должны освободиться деструктором ISDN Phone, освобо­ ждены не будут и превратятся в •мусор�. Если объявить деструктор базового класса TeLephone виртуальным, то на стадии выполнения программа выберет деструктор в соответствии с фактическим ти­ пом объекта: c l ass Tel ephone { vi rtu a l -Tel ephone ( ) : }: Tel ephone *di gi ta l Phone = new I SDNPhone : del ete d i g i t a l Phone :

//

Работает . вызывается I SDNPhone : : - I SDNPhone ( )

В результате будет вызван собственный деструктор объекта, правильно осво­ бождающий ресурсы. Естественно, в соответствии с обычным поведением де­ структоров С++ сразу же после этого вызывается деструктор базового класса TeLephone: :-TeLephone (см. раздел 4.3).

5 . 3 . В и ртуал ьн ые фу н кци и и в и д имость 1 Объявление функции виртуальной может фактически изменить ее область ви­ димости. Это может вести к изменениям в семантике программы, которые необ­ ходимо учитывать при построении иерархий наследования. Прежде всего стоит заметить, что сигнатура любой функции, участвующей в иден­ тификации операций на стадии выполнения, обязана точно соответствовать сигнатуре виртуальной функции своего базового класса. Сигнатура функции представляет собой совокупность имени, упорядоченной спецификации типов параметров и типа возвращаемого значения (в С ++ тип возвращаемого значе­ ния формально не считается частью сигнатуры при разрешении вызовов пере­ груженных функций, но все равно используется при идентификации). Изменять тип возвращаемого значения виртуальной функции запрещено: c l ass Number { puЬl i c : v i rtua l fl oat add ( const Number& ) : Numbe r ( douЬl e ) :

}: c l ass Bi gNumbe r : puЫ i c Number {

1

Перед чтением этого раздела следует вспомнить материал разделов 3.2 и 4.2.

5. 3 . В иртуал ьные функции и видимость

puЬl i c : vi rtual douЫ e add ( const Number& ) : Bi gNumber( douЫ e > :

1 37

/ / Недопуст и мо

}: Такая замена недопустима, потому что тип выражения, полученного при вызове функции add для объекта, неизвестен во время компиляции, и компилятор не сможет сгенерировать код обработки возвращаемого значения: voi d someFuncti on ( Number &num ) Number а = 10 . *num : . . . num->add ( a ) . . . . / / douЫ e или fl oat?

}:

Механизм виртуальных функций также сломается• при изменении списка па­ раметров функции в производных кла сс ах. Вообще говоря, такое изменение возможно, но в результате появляются две разные функции, не связанные друг с другом: одна функция скрывает другую функцию с таким же именем на более высоком уровне иерархии наследования. Пример приведен в листинге 5.2 функция Big N u m ber: :add(dou ble) полно стью скрывает одноименную функцию базового класса, и последняя становится недоступной для пользователей базо­ вого класса. То же самое произошло бы, если бы функция BigN u m ber::add была объявлена виртуальной. -

Листинг 5 . 2 . Изменение сигнатуры нарушает наследование виртуальности

с 1 ass NumЬer { / / Ба зовый класс puЬl i c : v i rtua l voi d add ( i nt ) : 11 . . .

}:

c l a s s Bi gNumber : puЫ i c Number puЬl i c : voi d add ( douЬl e ) : 11 . . . }; i nt ma i n ( ) Number *а : Bi gNumber *Ь . Ьо : 11 . . . a - >add ( l ) ; a - >add ( З . O > : b - >add ( 2 ) : b - >add ( 2 . 0 ) :

11 11 11 11 11 11

1 1 Прои з водный класс

Number : : add ( i nt ) Number : : add ( i nt ) с понижением i nt ( З . 0 ) Bi gNumber : : add ( douЬl e ) с повышением douЬl e ( 2 ) Bi gNumber : : add ( douЬl e )

продолжение..Р

1 38

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

Листинr 5.2 (продолжение)

b - >Number : : add ( 7 . 0 ) : Ьо . add ( 8 ) : bo . add ( 9 . 0 ) : Ьo . Number : : add ( 9 ) : bo . Number : : add ( l 0 . 0 ) :

// 11 11 11 11 11 11 11

Number : : add ( i nt ) с понижен ием i nt ( 7 . 0 ) Bi gNumber : : add ( douЬl e ) с повышением douЬl e ( 8 ) Bi gNumber : : add ( douЬl e ) NumЬer : : add ( i nt ) Number : : add ( i nt ) с понижением i nt ( 10 . 0 )

return О :

Между видимостью и виртуальными функциями могут быть другие неочевид­ ные связи. Рассмотрим следуюший пример: #i ncl ude cl ass А { puЫ i c : v i rtual voi d f( i nt ) : } : voi d А : : f( i nt ) { cout « " А : : f\ n " : } c l a s s В : puЫ i c А { puЫ i c : voi d f ( douЬl e ) : } : voi d В : : f( douЬl e ) { cout « " В : : f\ n " : } c l ass С : puЫ i c В { puЫ i c : vi rtual voi d f ( i nt ) : } : voi d C : : f ( i nt ) { cout « " C : : f\ n " : } i nt ma 1 n ( ) { А *а : В *Ь : С *с = new С : c ->f ( 2 ) : c - >f ( 2 . 0 ) : ь = с: b - >f ( 2 ) : b - >f ( 2 . 0 ) : а = с: a - >f ( 2 ) : a ->f( 2 . 0 ) : return О :

11 С : : f 1 1 С : : f с пон ижением 11 B : : f с повышением 1 1 В: : f 11 С: :f 1 1 C : : f с понижением

В корневом базовом классе А определена виртуальная функция. В промежуточном классе В определяется невиртуальная функция с другой сигнатурой, а в листо­ вом классе С виртуальная функция определяется заново. Заслуживает внимания тот факт, что виртуальная функция, находящаяся высоко в иерархии, может скры­ ваться функциями в середине иерархии. На ситуацию можно взглянуть и так: объект класса С •несет� в себе функцию C: :f(int), но видимость функции C: :f(i nt) изменяется в зависимости от типа указателя, используемого для адресации объ­ екта. Такое представление противоречит нашей интуиции, которая вроде бы

5.4. Ч исто виртуал ь ные функции и абстрактные базовые классы

1 39

подсказывает, что при объявлении виртуальной функции для объекта всегда вы­ зывается одна и та же функция независимо от того, как вы на этот объект ссылае­ тесь. Многие современные компиляторы С++ предупреждают о том, что перегру­ женная функция замещает одноименную виртуальную функцию, объявленную выше в иерархии наследования.

5 . 4 . Ч исто в иртуальн ые фу нкции и абстрактные базовые классы Вернемся к примеру класса TeLephone из главы 4, в котором создание экземпля­ ров TeLephone предотвращалось объявлением защищенного конструктора (с. 1 1 5). Предполагалось, что экземпляры могут создаваться только на базе производных классов: POTSPhone, VideoPhone, ISDN Phone и т. д. С абстрактным классом TeLephone реально ничего нельзя сделать! Почему? Потому что мы не можем написать код функции ring для выдачи звонка на абстрактном телефоне. На разных телефонах эта операция выполняется по-разному. На традиционных телефонах подается напряжение; цифровые ISDN-телефоны принимают сообщение на выдачу звон­ ка и подсветку индикатора, и т. д. В С++ имеется синтаксис для сохранения семантики функций класса (таких, как ring) в объявлениях этих функций. Прежде всего такая функция должна быть виртуальной; мы хотим, чтобы программы, в которой используются разные типы объектов Tetephone, могла вызывать для них функцию ri n g , не заботясь о подробностях ее реализации для конкретного типа. А поскольку для такой функции заведомо н�ьзя написать обобщенную, стандартную реализацию, вме­ сто тела функция ассоциируется с нулевым указателем: c l ass Tel ephone { puЫ i c : v i rtua l voi d ri ng ( ) = О : / / Чисто виртуальная функция Bool i sOnHook ( ) ; Bool i sTa l k i ng ( ) : Bool i sOi a l i ng ( ) ; v i rtua l O i g i tStri ng col l ectDi g i ts ( ) : L i neNumber extens i on ( ) : v i rtual -Tel ephone ( ) ; / / С нова объ я вл яется открытым Tel ephone ( ) : };

Такие функции называются чисто виртуалыtыми. Объявление чисто виртуаль­ ной функции всего лишь несет информацию о том, что абстрактный телефон способен звонить, хотя мы не можем (на обобщенном уровне) сказать, как имен­ но выполняется эта операция. Термин •чистый• в данном случае означает, что функция не имеет тела, определен лишь интерфейс.

1 40

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

Теперь программа не сможет создать объект класса TeLephone просто потому, что компилятор не знает, что делать с таким объектом при вызове для него опера­ ции ri ng. Такие классы, определяющие некоторый аспект поведения без опре­ деления его реализации, называются абстрактиыми базовыми классами, или неполными классами. Они накладывают на производные классы обязательство определить реализации чисто виртуальнЫ'х функций. В сущности, абстрактный базовый класс определяет тип (абстрактный тип данных) без определения реа­ лизации. Если же все функции класса являются чисто виртуальными, то абст­ рактный базовый класс задает спецификацию абстрактного типа данных (АТД), и не более того. Чисто виртуальные функции делают нечто большее, чем скрытый конструктор из главы 4 (см. с. 1 1 5). Во- первых, они объявляют, что их класс не может исполь­ зоваться для создания экземпляров, а следовательно - не может использоваться в качестве типа формального параметра или возвращаемого значения функции. Скрытый конструктор тоже обладал этим свойством, но с одним исключением: объект базового конструктора мог создаваться в функциях производного класса. Во-в торых, чисто виртуальные функции должиы подменяться в производном классе, чтобы программа могла создавать экземпляры этого класса. Для чисто виртуальной функции мож1ю определить тело. Для вызова чисто виртуальной функции с телом используется оператор уточнения области види­ мости (: : ) : #i ncl ude cl ass Base { puЬl i c : vi rtua l voi d pure ( ) }:

=

О:

c l ass Deri ved : puЫ i c Base { puЬl i c : voi d pure ( ) { cout « " Deri ved : : pu re " voi d foo ( ) { Base : : pure( ) : } }: voi d Base : : pu re ( ) { cout i nt ma i n ( ) { Der i ved d : d . Base : : pure ( ) : d . foo O : d . pure ( ) : Base *Ь = &d : b - >pure ( ) : return О :

«

" Base : : pure"

«

«

end l : }

endl : }

1 41

5.5. Классы конвертов и писем

При запуске программа выводит следующий результат: Ba se : : pure Base : : pure Deri ved : : pure Deri ved : : pure

Если удалить тело Base::pure, произойдет ошибка компоновки. Данная возможность может пригодиться в примере класса Window (окно). Допус­ тим, класс Window настаивает на том, чтобы производные классы предоставляли собственную версию функции очистки окна cLear, но при этом может предостав­ лять базовую функциональность функции cLear, общую для всех типов окон. На­ пример, тело Window::cLear может устанавливать курсор в центр экрана, когда оно вызывается в качестве завершающего действия в функциях cLear производных классов.

5 . 5 . Кл ассы ко нверт о в и писем Идиома «манипулятор/тело• позволяет применять некоторые приемы проекти­ рования, обладающие большей гибкостью и быстродействием, а также меньши­ ми последствиями при внесении изменений по сравнению с ортодоксальной ка­ нонической формой проектирования (см. главу 3). В этой идиоме задействована пара классов, используемых как единое целое: внешний класс (манипулятор) , который является видимой частью, и внутренний класс (тело), в котором спрята­ ны подробности реализации. Говорят, что манипулятор передает запросы телу, находящемуся внутри него. В совокупности эти два класса образуют составиой обьект . В этом разделе представлено расширение идиомы «манипулятор/тело• идиома «конверт/письмо•, обеспечивающая дополнительную гибкость и качест­ во инкапсуляции по сравнению с идиомой «манипулятор/тело•. Идиома «манипулятор/тело• добавляет новый уровень косвенных обращений во взаимодействия объектов. Повышение гибкости обусловлено в основном этой косвенностью, то есть привязкой на стадии выполнения. Хотя на самом деле объектов несколько, с точки зрения пользователя существует один объект (ма­ нипулятор), управля ющий всей работой составного объекта. Такое разделение может потребоваться для того, чтобы изолировать «прослойку• управления памятью от «настоящей семантики• внутреннего объекта при нетривиальном управлении памятью, как в примере класса String (см. раздел 3.5). Кроме того, оно задействовано в механизмах уборки мусора и в схемах замены классов/объек­ тов во время выполнения, описанных в главе 9. Идиома «конверт/письмо• является частным случаем идиомы «манипулятор/те­ ло• для ситуаций, когда манипулятор и тело обладают общим поведением, но те­ ло специализирует или оптимизирует поведение манипулятора. При подобном использовании класс манипулятора назьшается коивертом, а класс тела писъмом. Отношения между этими классами во многом напоминают отношения между -

1 42

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

базовым классом (эта роль отводится конверту) и производным классом (пись­ мо), но с существенно большей гибкостью на стадии выполнения по сравнению с наследованием. Класс письма является производным от класса конверта, но его экземпляры также содержатся в экзем плярах конверта. Общности «конверт/письмо• обычно более замкнуты, чем классы в иерархии наследования. Обращение к большинству классов писем может осуществляться только через конверт. Это в определенной степени справедливо для классов в иерархии наследования, за исключением того, что вызовы конструкторов про­ изводных классов могут распределяться по всей программе. Вызовы конструкто­ ров классов писем чаще локализуются в коде класса конверта. П РИМЕЧАН И Е

В некоторых ситуациях необ ходима особая ги бкость , недостижимая при связывании на ста­ дии компиляции . Например , программисту может потре боваться контроль над функциями, традици о нно относимы ми к стади и компиляции , скажем , ассоциациям «О бъект/класс • . Иди о ма «кон верт/п исьмо• спосо б на о беспечить б олее вы соки й уровен ь полиморфизма и идентификации тип ов на стадии вып олнения , чем простое наследование с виртуальными функциями , а это спос о б ствует упрощению и о бо б щен и ю в заимоде й ствия п ользов ателя с пакето м взаимосвязанных классов. Кроме того , снижается потре б ность в редактировании и переко мпиляции программы при внесении изменени й .

Существуют две точки зрения на отношения между конвертом и классом. Во­ первых, можно представить, что конверт делегирует часть своей функциональ­ ности работающему с ним объекту письма. Во-вторых, письмо может рассматри­ ваться как «см ысловая нагрузка•, которую логично инкапсулировать в конверте. Вторая модель хорошо подходит для обеспечения инкапсуляции и имеет много общего с примерами из раздела 3.5. В следующем разделе конверты и письма рассматриваются более подробно, по­ скольку на этой основе строятся более мощные идиомы объектно-ориентирован­ ного программирования, нежели те, которые поддерживаются на уровне «моно­ литных• классов С++. Сначала будет обоснована потребность в связывании на стадии выполнения объекта со свойствами, обычно ассоциируемыми с классами. Затем м ы перейдем к частному случаю этой идиомы и посмотрим, как лучше создавать объекты, не зная их точного класса. Далее это решение будет расшире­ но: вы научитесь создавать объекты переменного размера без дополнительного уровня косвенных обращений. Специальный вспомогательный синтаксис изба­ вит вас от дублирования кода писем и конвертов. Раздел завершается усовер­ шенствованным вариантом реали�ации писем и конвертов с использование м вложенных классов С ++.

Кл ассы кон вертов и делеги р о в а н н ы й поли м ор ф изм В чистых объектно-ориентированных языках вроде Smalltalk переменные связы­ ваются с объектами на стадии выполнения. Связывая переменную с объектом, вы словно наклеиваете на последний временный «ярлык• . Когда в таком языке

5 . 5. Классы конвертов и писем

1 43

выполняется присваивание, с объ екта снимается од ин ярлык, а на его мес то наклеивается другой. Переменные в таких языках несут минимальный объем информации о типе, а компиля тор практически не следит за совместимостью типов. Конечно, у самих объектов имеются операции, функции и атрибуты, ко­ торые в совокупности могут рассматриваться как информация о типе, и это позволяет выполнять проверку типов на с тадии выполнения. К недостаткам языков, обладающих этим свойством, следует отнести неприятные с ю рпризы с типами, возникающие во время выполнения. С другой стороны, высокая степень гибкости, присущая таким языкам , приносит пользу при построении прототипов и доработке программы ; она же закладывает основу для боле е совершенных механизм ов управления памятью. Модель типов С++ не обладает такой гибко ­ стью по двум причинам : из-за раннего связывания символических имен с адреса­ ми и из -за ранней проверки совместимости типов. В С и С+ + перем енная является синонимом адреса или смещения, а не � меткой • объекта. Присваивание не сводится к простой смене ярлыков; на место старого содержимого объекта записывается новое содержимое. Чтобы компенсировать это обстоятельство, м ы вручную добавляем промежуточный логический уровень, об­ ращаясь к объектам через указатели. Хотя имена переменных по-прежнему связаны с блоками памяти, память определяется •простым • указателем, поэтому ассоциа­ ция м ежду и м енем и • настоящим• объектом легко изменяется замено й указате­ ля. В прочем, программ ирование с применением указателей порождает свои не­ удобства. Оно усложняет управление памятью; возникает опасность появления •висячих указателей•. Обращение к любым объектам через указатели затрудняет использование некоторых языковых конструкций, особенно перегрузку операторов: c l ass Number { puЫ i c : N umЬer ( ) : v i rtu a l N umber *operator*( const Number& ) : }: c l ass Compl ex : puЬl i c Number puЫ i c : Comp l ex( douЫ e . douЬl e ) : NumЬer *operator* ( const Number& ) : }: c l ass I nteger : puЬl i c Number { puЬl i c : I nteger ( i nt ) : NumЬer *operator* ( const Number& ) : }:

1 44

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

NumЬer *numberVector[5] . *numberVector2 [ 5 ] . *productVector[5] : numberVector [ O ] = new Compl ex( 0 . 10 ) ; numberVector [ l ] = new I nteger ( O ) ; numberVector(2] = О : numberVector2 [ 0 ] = new I nteger ( l O ) : numberVector2 [ 1 ] = new Compl ex ( l 0 . 10 ) : О : numberVector [ i ] && numberVector2 [ i ] : i ++ ) { for ( i nt i 1 1 Уродли в ый ( но необходи мый ) синтаксис : productVector[ i ] *numberVector[ i ] * *numberVector2 [ i ] : =

=

Существует и другая сложность: компилятор С++ следит за тем, чтобы объект мог предоставить функции, вызванные для переменной, через которую вы обра­ щаетесь к объекту. Для этой цели компилятор использует систему коитроля ти­ пов. •Система контроля типов• в этом контексте довольно слабо связана с кон­ цептуальными абстракциями приложения, которые могут рассматриваться как формальные типы, отражаемые программой в своей структуре классов (см. раз­ дел 6. 1 ), Системы контроля типов способствуют предотвращению ошибок; в ча­ стности, система контроля типов С++ выявляет несоответствия типов н а стадии компиляции. Эта система весьма консервативна в своей интерпретации струк­ туры классов, и как следствие - в своем представлении об отношениях между архитектурными абстракциями: она находит все реальные несоответствия типов предметной области, но при этом может заблокировать некоторые допустимые операции. Нежесткое связывание переменных с объектами и мощные системы контроля типов в символических языках избавляют программиста от необходимости спе­ циально объявлять о совместимости между типами. На практике часто требуется работать с группой взаимозаменяемых классов и интерпретировать все объ екты этих классов так, словно они относятся к одному классу. Виртуальные функции предоставляют такую возможность для указателей и ссылок. Однако указатели плохо защищены от злоупотреблений (их называют «командами GOTO в ми­ ре данных•), к тому же они не интегрируются с перегруженными операторами. В приведенном примере класса N umber указатели позволяют интерпретировать любую числовую величину как экземпляр N u m ber, хотя за это приходится рас­ плачиваться неудобным синтаксисом. На самом деле мы хотим, чтобы каждый объект автоматически применял свое умение выполнять различные операции: i nt ma i n ( ) Number Number Number r = О: return

{ c(l . O . -2 . 1 ) : r(5 . 0) ; product с * r: =

О:

1 45

5.5. Классы конвертов и писем

Но в этом случае возникает проблема - объекты должны идентифицировать свой фактический класс на стадии выполнения. Для переменной с - это тип Com pLex, для переменной r - DoublePrecision FLoat, а для переменной prod uct тот тип, который должен быть получен в результате операции (теоретически первые два случая могут быть разрешены особо проницательным компилято­ ром на стадии компиляции, но третий тип определяется только во время вы­ полнения). Но объекты разных классов занимают разный объем памяти, по­ этому смена класса объекта приведет к изменению его размера, а эта величина известна на стадии компиляции. Связывание объектов с виртуальными функ­ циями производится в процессе инициализации объекта и остается неизменным; данное обстоятельство тоже препятствует изменению типа объекта. Еще одна проблема заключается в том, что после идентификации класса объект может «передумать• и сменить свою принадлежность. Например, и сходное определе­ ние product предполагает, что этот объект представляет комплексное число, но после обнуления сохранять его принадлежность к категории комплексных чисел бессмысленно. И так, мы хотим создать механизм установления связи между объектом и его классом на стадии выполнения. Более того, связывание переменных с классом их объектов не должно быть жестким - иначе говоря, в процессе выполнения программы нужно разрешить переменным «менять свои типы• . Набор таких классов для нашего примера с числ ами представлен на рис. 5.4, а также в лис­ тингах 5.3 и 5.4.

\

r·�-��-�J··· · · ·· · · ··... .. н ие · · · · ·· ·· .. : · спедо�ва�1ни• е · · · . ::: :::: :: -�-�-:: . .. · . . На-��'!1 · · · · ·· · ·· · · -..· ·.·--: -··-ю:;;�:-f -�;�- - :f · · · · ·· · -. .��. Biglnteger : ex : :1 -Compl оэдмпаляниера ------� 1: - - - - - - - � к С зе а э о ни � эСкземэдпляреа - - - - - -"" - а и о мпаляниреа Скзоеэдмп няреа к С эд зе э э л NumЬer* Указатель Указатель а 2

/'

1

1

1

1

1

1

Конверт

Number а = NumЬe r ( l . 2 ) : NumЬer Ь

=

З:

письмо

Конверт

Рис. 5 .4. Полиморфные числа

1

- - -

1

- -

з

письмо

1 46

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

Листинг 5.3. Часть реализации класса Number

st ruct BaseConstructor { Ba seConstructorC i nt=O ) { } } : c l ass Number рuЫ i c : Number ( ) { rep = new Rea l Number C 0 . 0 ) : ) Numbe r C douЫ e d ) { rep = new Rea l Number ( d ) : Numbe r C douЫ e rpa rt . douЫ e i pa rt ) { гер = new Compl ex ( rpa rt . i pa rt ) : } Number operator= C Number & n ) n . rep- >referenceCount++ : i f ( - - rep - >referenceCount == 0 ) del ete rep : rep = n . rep : return *th i s : } Number ( Number & n ) { n . rep - >referenceCount++ : гер = n . rep : vi rtual Number operator+ C const Number &n ) return rep - >operator+ C n ) : } v i rtua l Number compl exAdd ( Number &n ) return rep- >comp l exAdd ( n ) : v i rtual ost ream& operatoroperatorrea l Add ( n ) : } voi d redefi ne ( Number *n ) { i f ( - - rep- >referenceCount == 0 ) del ete rep : rep = n : protected : Number( BaseConstructo r ) { referenceCount = 1 : } pri vate : Number *rep : short referenceCount : }: Листинг 5 . 4. Часть реализации класса Complex

c l ass Compl ex : puЬl i c Number { puЫ i c : Compl ex ( douЫ e d . douЫ e е ) : Numbe r C BaseConst ructo r ( ) ) {

5.5. Классы конвертов и писем

1 47

rpa rt = d : i pa rt = е : referenceCount = 1 : } Number operator+( Number &n ) { return n . compl exAdd ( *th i s ) : } Number rea l Add ( Number &n ) { Number retva l : Compl ex *cl = new Compl ex C *th i s ) : Rea l Number *с2 = ( Rea l Number* J &n : c l - >rpa rt += c 2 - >r : retva l . redefi ne ( c l ) : return retva l : } Number compl exAdd C Number &n ) pri vate : douЫ e rpa rt . i pa rt : }:

Для сохранения разумной семантики сигнатура всех классов нашей иерархии должна быть сходна с сигнатурой класса N u m ber. В С++ это требование выража­ ется определением этих классов как производных от абстрактного базового клас­ са Number. На базе N u m ber строится иерархия числовых типов. Объекты таких классов упаковываются в «конверт», сохраняющий «Идентичность» (адрес, если хотите) величины, пока ее значение - а возможно, и тип - изменяется с приме­ нением к ней различных операций. «Конверт» обладает такой же сигнатурой, что и содержащиеся в нем объекты, поэтому один класс N umber играет сразу две роли: абстрактного базового класса для классов «писем» и «конверта», скрываю­ щего подробности реализации от пользователя. Модель изображена на рис. 5.4, а пример кода приведен в листинге 5.3. В соответствии с идиомой подсчета ссылок (см. раздел 3.5) основная логика управления п амятью для объектов N u m ber сосредоточена в классе конверта, а семантика приложения размещается в классе письма (см. листинги 5.3 и 5.4). Конструкторы строят разновидность объекта письма для заданных параметров. В операторе присваивания и копирующем конструкторе также используется идиома подсчета ссылок; в переменной класса referenceCount хранится счетчик ссылок на общее представление. Класс N u m ber перенаправляет операции письму, хранящемуся «внутри» него, как показано в реализации функции operator+ и дру­ гих операторных функций в листинге 5.3. Класс N u m ber (см. листинг 5.3) служит одновременно классом объекта «универ­ сального числа», который виден пользователю, и базовым классом для классов писем; некоторые из его функций отражают особый сервис, предоставляемый производным классам. Специальный конструктор N umber::Number(BaseConstructor) инициализирует экземпляры N u m ber, образующие подобъект базового класса в классах писем. Он замещает конструктор по умолчанию N u mber: : N u m ber для

1 48

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

предотвращения бесконечной рекурсии при создании нового экземпляра класса, производного от N u m ber. Функции com pLexAdd, reaLAdd и redefine являются спе цифическими функциями приложения и предоставляют необходимый сервис производным классам. Тем не менее, эти функции не объявляются защищенными, потому что они вызыва­ ются между экземплярами. Объект N u m ber должен быть готов принять экземп­ ляр любой �разновидности• N u m ber в качестве операнда своей операции. Рас­ смотрим следующий код: Number aCompl ex ( l . O . 2 . 0 ) : Number a Rea l ( З . 0 ) : Number resul t = aCompl ex + a Rea l :

Когда операторная функция operator+ объекта aCompLex вызывается с параметром a ReaL, она перенаправляет запрос своему объекту письма. Тот вызывает опера­ торную функцию operator+ класса CompLex с операндом aCom pLex и параметром aReaL. В свою очередь , CompLex::operator+ вызывает операцию compLexAdd для сво­ его параметра , передавая ей *this (значение aCo m p Lex). Управление передается функции compLexAdd класса Nu m ber, которая просто пер е направляет вызов функ­ ции compLexAdd класса ReaLN u m ber. Располагая дружественным доступом к обоим объектам, функция compLexAdd класса ReaLNumber генерирует результат соответ­ ствующего типа и возвращает его. Теперь вы примерно представляете, как этот механизм функционирует. О том, как такие полиморфные объекты появляются на свет, рассказано в следующем разделе.

И м ита ция в и ртуал ьн ы х конструкт о р о в Один из важных принципов объектно-ориентированного проектирования (см. гла­ ву 6) гласит, что каждый класс должен уметь выполнять свои операции. Мощь объектно-ориентированного программирования в значительной мере обуслов­ лена полиморфизмом, создаваемым сочетанием наследования и виртуальных функций. Наследование и виртуальные функции позволяют осуществлять все взаимодействие пользователя с объектом исключительно через интерфейс, опре­ деленный в базовом классе. Компилятор обеспечивает • волшебный• механизм перенаправления вызовов соответствующим функциям производных классов на стадии выполнения. Конструкции С++ отделяют пользователя семейства клас­ сов от служебной информации о количестве и природе производных классов, а также от подробностей реализации этих классов. Впрочем, в С++ имеется одно исключение, нарушающее этот принцип, который считается основополагающим в •чистых• объектно-ориентированных языках. Для примера возьмем класс Num ber с производными классами BigN u m ber и CompLex, где многие функции производных классов также присутствуют в виде виртуаль­ ных функций в базовом классе. Пользователь, располагающий ссьmкой или ука­ зателем на N u mber, может связать его с объектом класса N u mber, BigN umber или

5.5. Классы конвертов и писем

1 49

CompLex, и спокойно работать с любым из этих объектов, используя виртуальные функции Number. Пользователь не обязан знать строение иерархии N u m ber; он одинаково работает со всеми экземплярами в контексте свойств, унаследован­ ных этими экземплярами от NumЬer. Но как появился указатель, ссылающийся на объект производного класса? Где был создан этот объект, и как его создатель указал , какой из производных классов требуется? Пользователь не мог создать объект конструкцией new Big N u m ber или new CompLex; это было бы нарушением принципа, в соответствии с которым поль­ зователю неизвестны детали иерархии производных классов. Объект не мог быть получен от третьей стороны, поскольку третья сторона знает о производных клас­ сах N u m ber ничуть не больше, чем пользователь. Следовательно, указатель дол­ жен быть получен внутри самой комбинации классов N u mber/Bi g N u mber/Comptex. Можно представить, что конструктор Number создал объект BigNumber, потому что переданный параметр превысил заданный порог, или же объект Comptex был создан внутри N umber при вычислении квадратного корня для отрицательной величины. Итак, возникает предположение, что при использовании конструкции new Number конструктор N umber создает объект Big N u m ber или Comptex, делая так, что возвра­ щаемое значение оператора new ссылается на один из этих объектов. Но такого быть не может! Посмотрите: NumЬer : : NumЬe r < > { i f ( некоторое условие ) th i s = new Bi gNumЬer :

/ / Недо пуст и мо

Присваивать новое значение this в С++ запрещено. Для решения можно воспользоваться дополнительной абстракцией, которая вы­ полняет функции агента для взаимодействия с семейством Number/BigNumЬer/Comptex по поручению клиента, создающего объект. На роль такого агента подходит объ­ ект класса N u m ber, используемого в качестве класса конверта. Каждый экземпляр N u m ber указывает на объект письма, созданный на базе одного из его произ­ водных классов, например, Big N umber или Comptex. Указатель объявляется с ти­ пом Number*: c l ass NumЬer { pri vate : NumЬer *rep :

Вызовы функций Number перенаправляются объекту, на который ссылается за­ крытое поле rep: puЫ i c : NumЬer operator* ( const Number &n ) return rep - >operator* ( n ) :

1 50

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

Объявления функций класса письма в точности соответствуют объявлениям их базового класса N u mber. Закрытое поле rep инициализируется конструктором конверта, который конструирует BigNumber или CompLex в зависимости от некото­ рого условия и помещает указатель на полученный объект в закрытое поле rep. Для примера возьмем класс Number, для которого было бы естественно определить операцию направления в поток вывода (тип ostream распространенной библиоте­ ки С++ iostreams). Но при этом нам также хотелось бы иметь возможность ини­ циализации Number на базе istream (потока ввода) - то есть наделить N umber воз­ можностью прочитать себя из потока и создать новый объект в процессе чтения. Следовательно, в классе должен присутствовать конструктор следующего вида: c l ass Number puЬl i c : Number ( i st ream& s ) char buf[ l O J : s » buf : swi tch ( numPa rse ( buf) > { case COMPLEX : rep = new Compl ex ( buf) : break : case FLOAT : rep = new DouЫ ePrec i s i onF l oat ( buf ) : brea l< :

pri vate : enum NumType { СОМРLЕХ . FLOAT . B I G I NT } : NumType numPa rse < ch a r * ) : 1 1 Определение типа ч исла Number *rep ; }:

( Конечно, в реальном приложении значение rep будет присваиваться в теле n u m Parse, чтобы избежать дублирования информации обо всех числовых классах между этой функцией и функцией Num ber: : N u mber(istream&} .) Теперь все выглядит так, будто класс создает разные типы объектов, свойства которых определяются контекстом, переданным конструктору (в данном случае потоком байтов из файла или другого источника). Рассмотренная идиома наделяет классы той гибкостью, которой виртуальные функции наделяют объекты. Эта идиома известна под названием виртуш�ьноzо конструктора. Виртуальные конструкторы являются шагом вперед по направле­ нию к имитации •полноценных� классов в С++; при этом язык С++ использует­ ся так, словно он является моноиерархической системой (то есть состоящей ис­ ключительно из объектов без типов, которые являются не классами, а -«чем-то иным*:). Идиома виртуального конструктора подробно проанализирована (и рас­ ширена) в главе 8.

5.5. Классы конвертов и писем

1 51

П Р И М ЕЧАНИ Е Идиома виртуального конструктора используется в тех с итуациях, когда тип объекта должен определяться по контексту конструирования. В качестве примеров можн о привести построе­ н ие о бъе кта окна прав и льн о го т и па и размера в зав и с и мости от типа и размера экрана, используемого программо й , создание о бъекта в интерактивном режиме или п о неформа­ тированным данным и з файла.

Следующий пример позаимствован из арх итектуры класса Atom, используемого для представления атомов (неделимы х лексем) при разборе текста. Класс Atom выполняет большую часть диспетчерских операций в простой системе разбора. Вот как выглядит определение базового класса, все функции которого объявле­ ны виртуальными: c l ass Atom puЬl i c : Atom( ) { } puЫ i c : / / Общие фун кuии всех прои з в одных классов vi rtua l -Atom ( ) { } vi rtua l l ong va l ue ( ) return О : } vi rtual Str i ng name ( ) return Stri ng ( " er ror" ) : v i rtu a l operator cha r ( ) { return О : } v i rtua l Atom *сору ( ) = О : }:

Обратите внимание: функция Atom::copy является чисто виртуш�ьной; за ее объ­ явлением следует спецификация О. Классы, производные непосредственно от Atom и не предоставившие тело Atom::copy, наследуют эту функцию как чисто виртуальную. А это означает, что функция сору должиа быть определена во всех классах, непосредственно производных от Atom, чтобы программа могла созда­ вать экземпляры этих IU""Iaccoв; это необходимо для правильной работы идиомы. Отсутствие тела у функции Ato m : :copy делает невозможным создание объекта Atom. Только производный класс, содержащий или унаследовавший функцию сору, может воплощаться в экземплярах. =

Теперь давайте посмотрим на классы писем, выполняющие большую часть • настоящей работы • . Эти классы автономны; чтобы понять логику их работы, не нужно разбираться в подробностя х устройс тва класса конверта. В листин­ ге 5.5 приведен класс числовых атомов N u mericAtom. Конструктор N umericAtom:: NumericAtom(const String&) выделяет целое число из своего параметра String, •по­ глощая:ь в процессе работы символы строки. За ним следует простой копирую­ щий конструктор. Функция vaLue возвращает целое значение. Листинг 5 . 5 . Класс лексического атома NumericAtom

c l ass Numeri cAtom : puЬl i c Atom puЬl i c : Numeri cAtom ( ) : s um ( O ) { } Numeri cAtom ( Stri ng &s ) sum = О :

продолжение..Р

1 52

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

Листинг 5.5 (продолжение)

for ( i nt i = О : s [ i ] >= ' О ' && s [ i ] sum = sum : return retva l : pri vate : l ong sum : }:

Поскольку класс N umericAtom предназначен для представления числовой вели­ чины, он переопределяет операцию value класса Atom, но сохраняет за функцией преобразования символов operator char стандартное поведение (функция возвра­ щает нуль-символ). Следовательно, пользователь этих кл асс ов по контексту дол­ жен знать, когда осмыслены вызовы таких функций, как operator char или value; определять их чисто виртуальными н е следует, поскольку нет смысла переопре­ делять их во многих производных классах. Функция сору создает физическую копию объекта и возвращает Ato m * . Так в нашем р аспоряжении появляется универсальная операция, дублирующая любой объект Atom. Благодаря функции сору отпадает необходимость в •поле типа� . о котором речь пойдет далее. Классы писем Name, Pun ct и Oper реализованы аналогично классу N u mericAtom (листинг 5.6). Листинг 5.6. Классы Name,

Punct и

Oper

c l ass Name puЬl i c Atom puЬl i c : Name ( ) : n C " " ) { } Name C Stri ng& s ) { for C i nt i =O ; s ( i ] >= · а · && s [ i ] n = n : retu rn retva l ; pri vate : St ri ng n : }: '

=

'

5.5. Классы конвертов и п и сем

1 53

c l a s s Punct : puЫ i c Atom { puЫ i c : Punct ( ) : с ( ' \ 0 ' ) { } { с = s [ O ] : s = s ( l . s . l ength ( ) - 1 ) : Punct ( Stri ng& s ) Punct ( const Punct& р ) { с = cha r ( p ) : operator cha r ( ) const { return с : } -Punct ( ) { } Punct *retva l = new Punct : Atom *сору ( ) retva l - >c = с : return retva l : pri vate : char с ; }: c l ass Oper : puЫ i c Atom { puЫ i c : Oper ( ) : с ( · \ О · ) { } Oper ( Stri ng& s ) { с = s [ O ] : s = s ( l . s . l ength ( ) - 1 ) ; Oper( Oper& о ) { с = cha r ( o ) : } -Oper O { } operator cha r ( ) const { return с : Atom *сору ( ) { Oper *retva l = new Oper : retva l - >c = с : return retva l : pri vate : char с : }:

Класс конверта GeneraLAtom предназначен для управления созданием всех объек­ тов производных классов Atom: c l a s s Genera l Atom : puЫ i c Atom { puЫ i c : Genera l Atom ( Stri ng& ) : Genera l Atom ( const Genera l Atom& а ) { rea l Atom=a . copy ( ) : } -Genera l Atom ( ) { de l ete rea l Atom : } Atom *t ransform ( ) { Atom *retval =rea l Atom : rea l Atom=O : return retva l : } puЫ i c : / / Объединение с и г натуры всех классов иерархии Atom . 1 1 Необходимо . ч тобы класс Genera l Atom мог 1 1 использоваться так . словно он я вл яетс я 1 1 экземпляром одного и з своих произ в одных 11 классов ( то есть без использования transform( ) ) . { return rea l Atom->va l ue ( ) ; } l ong v a l ue( ) { return rea l Atom- >name ( ) : } St ri ng name ( ) operator cha r ( ) { return rea l Atom- >operator cha r ( ) ; pri vate : Atom *rea l Atom : }:

1 54

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

Конверт GeneraLAtom - единственный класс, с которым пользователь работает напрямую. Объекты остальных классов рассматриваются как письма, хранящие­ ся в объектах класса конверта. Класс GeneraLAtom может получать в параметре конструктора GeneraLAtom::GeneraLAtom (String&) сим вольную строку; он анализи­ рует строку, создает и сохраняет указатель на объект соответствующего класса, производного от Atom: Genera l Atom : : Genera l Atom ( Stri ng &s ) i f ( ! s . l ength ( ) ) rea l Atom О : el se swi tch ( s [ O J ) { case ' О ' : case · 1 · : case 2 : case ' 3 ' : case ' 4 ' : case · 5 · : case ' 6 ' : case ' 7 ' : case · в · : case ' 9 ' : rea l Atom = new Numeri cAtom ( s ) : break : case ' . ' : case · : : case ' : ' : case . . rea l Atom = new Punct ( s ) : break : case ' * ' : case · 1 ' : case · + · : case ' ' rea l Atom = new Oper ( s ) : break : defau l t : i f ( s [ O J >= ' а ' && s [ O J . SEC2obj ( sec2 ) . Empl oyee < me > { } pri vate : Secretary SEClobj . SEC2obj ; }; c l a s s DeptHead : puЫ i c Empl oyee { puЫ i c : DeptHea d ( Name me . Name secy ) : SECobj ( secy ) . Empl oyee < me > { } pri vate : Secretary SECobj ; };

Рис. 6 . 2 . Отношения между типами

6.4. Отношения между объектами и кл ассами

21 7

Сэ м

о

Пзт

Терри

ДжинО

О Мао риан

Secret a ry Sam( " Sam" ) :

V i cePres i dent Pat ( "Pat " . " Terry " , "Jean · . " Мa ri on " ) :

Джо

DeptHead J o ( "Jo" . " Ch ri s " ) :

Рис. 6 . 3 . Отношения между сущностями

В С++ отношения •IS-A• представляются открытым наследованием. Каждый из следующих классов объявляется открыто производным от класса Emptoyee: Vi cePres i dent DeptHead Sec reta ry Admi nAsst

Это означает, что любые открытые (puьtic) характеристики класса Emptoyee так­ же являются характеристиками любого из производных классов.

О тношени я cc HAS - A» Отношения • HAS-A• определяют связи включения. Их синонимами являются выражения •является составной частью• или •использует в реализации•. В от­ личие от связей • IS-A•, выражающих исключительно отношения между класса­ ми, связи •HAS-A• могут определять отношения между двумя классами, между классом и объектом или между двумя объектами.

Начнем с уровня классов, а конкретно - с класса VicePresident (вице-президент). Предположим, вице-президенты договорились создать в компании должность помощника по административной работе, занимающегося исключительно обслу­ живанием вице-президентов. Помощники по административной работе подчиня­ ются только вице-президентам (в целях нашего примера будем считать, что они взаимодействуют только с начальниками, секретарями и администраторами из других компаний). Поскольку это относится ко всем вице-президентам, отношение

218

Глава 6 . Объектно-ориентированное проектирование

существует на уровне класса. Класс VicePresident полностью распоряжается клас­ сом Ad minAsst (помощник по административной работе), то есть фактически со­ держит (has а) его. Объявление второго класса размещается внутри объявления первого, отражая его коицептуш�ьную принадлежность. Хотя должность секретаря придумана не вице-президентами, вполне естест­ венно, что вице-президенты тоже пользуются услугами секретарей. На рис. 6.2 видно, что у начальников отделов тоже есть секретари, и это придумали не ви­ це-президенты (а у помощников по административной работе могут быть свои секретари). Мы говорим, что экземпляр Vi cePresident или DeptHead (начальник от­ дела) связан отношениями « HAS-A• с экземпляром Secretary (секретарь). Отно­ шения принадлежности существуют не на уровне класса, а на уровне экземпляра. Класс Secretary выражает самостоятельную концепцию; на концептуальном уровне он не «принадлежит• никакому другому классу. Отдельные экземпляры Secretary связываются со своими начальниками и объявляются как переменные классов, производных от M a nager. Отношение «HAS-A• на уровне экземпляра может быть реализовано как в форме полного включения объекта В в объект А, так и хранения указателя на В в объек­ те А независимо от того, создается ли объект В объектом А. Например, в иерар­ хии на рис. 6.2 класс DeptH ead может содержать только указатель на объект Secretary. Тем не менее, состояние объекта Secretary все равно рассматривается как часть состояния объекта D e pt H e a d . Отношение « H AS-A• между DeptHead и Secretary проявляется через указатель так же, как если бы объект Secretary был встроен в DeptHead. Но при использовании указателя отношение может быть причислено к категории « HAS-A• только в том случае, если указатель на этот экземпляр не хранится в других объектах (то есть если ссылка на данный экзем­ пляр Secretary хранится только в одном объекте DeptHead). А если класс С++ содержит ссылку? Ссылка представляет собой псевдоним для обращения к другому существующему объекту. Если имеется ссылка на объект того же класса, то отношение « HAS-A• существует независимо от ссылки, и факт ее присутствия этой связи не изменяет. В противном случае объявление ссылки означает, что объект может не находиться в исключительной принадлежности класса, содержащего объявление, и что доступ к нему может осуществляться по другому имени. В этом случае отношение «HAS-A• может и не существовать. Справедливо ли утверждение, что объект класса Admi nAsst (например, Terry) при­ надлежит объекту класса VicePresident (например, Pat)? Конечно, это так - в допол­ нение к отношению «концептуальной принадлежности•, существующей между этими классами на рис. 6.2. На практике редко встречаются отношения «HAS-A•, существующие только на уровне классов и не опускающиеся на уровень объектов (тем не менее. один такой пример будет далее рассмотрен - заметите ли вы его?) Давайте немного расширим пример и введем новый класс «обобщенного начальни­ ка• Manager (листинг 6. 1 ). Как нетрудно заметить, отношения «IS-A• существуют между класса.\Ш VicePresident и Manager, а также между DeptHead и Manager ( DeptHead, как и Vice President, является частным случаем Manager). Какое место в этой ие­ рархии занимает концепция секретаря? Можно сказать, что иа концептуш�ьном уровне секретарь «принадлежит• начальнику, поэтому объявление Secretary еле:

6.4. Отношения между объектами и классами

219

дует разместить внутри Manager. Н о если у разных начальников разное количе­ ство секретарей, было бы неправильно хранить экземпляр Secretary внутри экземп­ ляра Manager - вместо этого следует объявить один или несколько экземпляров Secretary в каждом специализированном классе, представляющем конкретную разновидность начальника. Листинг 6. 1 . Часть программной реализации корпоративной структуры

c l a s s Name { puЫ i c : Name ( const char * ) : /* . . . */ } : c l ass Empl oyee { puЬl i c : vi rtua l char *name ( ) : Empl oyee ( Name ) : pri vate : 11 . . . }: c l ass Manager : puЬl i c Empl oyee { puЬl i c : Manager< Name n ) : Empl oyee ( n ) { } protected : c l a s s Secretary : puЬl i c Empl oyee puЬl i c : Secretary ( Name n ) : Empl oyee ( n ) { } char *name < > : }: }:

c l a s s V i cePres i dent : puЫ i c Manager { puЬl i c : Vi cePres i dent ( Name me . Name Asst . Name sec l . Name sec2 ) : Manage r ( me ) . SEClobj ( sec2 > . SEC2obj ( sec2 ) . As stObj ( Asst ) { } pri vate : Sec reta ry SEClobj . SEC2obj : c l a s s Admi nAsst : puЬl i c Empl oyee 11 . . . } AsstObj : }:

c l a s s DeptHead : puЬl i c Manager { puЬl i c : DeptHead ( Name me . Name secy ) : Manager ( me ) . SECobj ( secy ) { } pri vate : Secretary SECobj : }: Manager : : Secretary Sam( " Sam" ) : Vi cePres i dent Pat ( " Pat" . "Terry " . "Jean " . " Mari on " ) : DeptHead Jo( "Jo" . " Chri s " ) :

220

Глава 6 . Объектно-ориентированное проектирование

О тно ш е ния cc USES-A» Отношение • USES-A• возникает в том случае, если функция класса или функция, дружественная по отношению к некоторому классу, получает в параметре экземп­ ляр другого класса, который она использует

(uses а). Также отношение

• U S ES-A•

существует, если логика функции класса опирается на услуги другого класса. На­ пример, можно говорить об отношениях 4USES-A• между вице-президентом и на­ чальником отдела; первый •использует• последнего для выполнения некоторых поручений. Данное отношение относится к классу •U SES-A•, а не 4HAS-A•, пото­ му что обращение к начальнику отдела со стороны вице-президента не подразуме­ вает, что объект DeptHead включается в состояние экземпляра VicePresident. Объект DeptHead не рассматривается

как

4строительный блок• для реализации VicePresident;

тем не менее, эти два класса взаимодействуют через связь • U S ES-A•.

От ношен ия cc CR EATES -A» В контексте упомянутых канонических форм отношение • CREATES-A• выгля­ дит необычно: оно связывает объект с классом. Экземпляр одного класса в процессе

выполнения одной из своих функций обращается к другому классу с запросом на создание

(creates а)

экземпляра; во втором классе вызывается оператор new

с последующим выполнением конструктора. Отношения этого вида похожи на

•U SES-A•,

объектами) .

но они формируются между объектом и классом ( а не двумя

В главе 8 м ы подробно рассмотрим прототипы и идиомы, расширяющие возмож­ ности отношений • CREATES-A•. Используя идиому отношения • C REATES-A• (которая традиционно не присуща С++), мы убедимся в том, что это отношение связывает два объекта, один из которых вызывает функцию другого без учета классов обоих объектов.

Ко н те кст н ое изучение от ношен и й между о бъ екта м и и кл ассами Четыре рассмотренных вида отношений отражают логические связи между сущ­ ностями и типами и особенности их преобразования в классы и объекты во вре­ мя реализации.

В зависимости от прикладной области, языка и используемых

идиом, другие отношения могут занимать более важные места в лексиконе раз­ работчика проекта. Представленные концепции отношений не являются формальными; читателю не стоит придавать им • слишком большого значения. Они должны интерпретиро­ ваться

в

своем контексте и разумно использоваться как средство передачи ин­

формации, а не как строго определенные правила проектирования. Например, для описания взаимодействий между Manager и Secreta ry в предыдущем примере

вместо связи • HAS-A• с таким же успехом можно использовать связи •USES-A•,

6.4. Отношения между объектами и кл ассами

22 1

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

Г ра ф ическое представление от но ш ений между о бъ е ктами и классами В [5] предложена специальная система обозначений для описания структуры объ­ ектно-ориентированных систем. Эта система состоит из четырех компонентов: +

дuаzраммы классов отражают структуру классов, их отношения друг с другом и общие отношения между их экземплярами;

+

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

+

диаграммы состояния представляют объекты как конечные автоматы

+

временные дuаzрам.мы отражают последовательность событий разных объектов.

и до­ кументируют переходы между состояниями в результате вызовов функций классов;

На практи ке чаще всего применяются диаграммы классов и объектные диаграм­ мы, представляющие основные отношения между классами и объектами в гра­ фическом виде. У обеих разновидностей диаграмм имеются собственные услов­ ные обозначения. На рис. 6.4 приведены условные обозначения для диаграмм классов, а на рис. 6.5 - для объектных диаграмм. , , - , _ _ _ _ _ , - .... ,

1

" - ....



'

1

\

'

.... ....

'

,

, - .... _ ,

Подпись Подпись



- - -

. . . . .IJ���!>. ... .

1·· · n�"!��- - •

Подпись Подпись Подпись Подпись JI> Jlll







-





-

'

1

,,

Обозначение класса Использование (для интерфейса) Использование (для реализации ) к ов (совместимый тип) Соо:щэдааниие экземмпплялярров ( овый н тип) н е э зе СНаследов ание (совместимый тип) Наследование (новый тип) Метакласс Неопределенные отношения

Рис. 6 . 4 . Условные обозначения отношений между классами

222

Глава 6. Объектно-ориентированное проектирование

� Та лексическая область 1) Та же лексическая область видимости (общая ) [eJ Пара метр 11 Параметр (об щи й) 1Е.1 Поле r.il Поле (общее) видимости

же

Простая связь инхронная связь С � Отказ поста новки в очередь Тайм-аут Асинхронная связь

-------

)(



от

С9



Рис. 6 . 5 . Условные обозначения отношений между объектами

На рис. 6.6 представлена диаграмма классов для описанного ранее примера EmpLoyee. На диаграмме отражены отношения наследования (классы EmpLoyee и Manager являются базовыми) и некоторые отношения, характеризуемые общностью на уровне классов. Так, у каждого начальника отдела ( DeptHead) имеется секре­ тарь (Secretary), а у вице-президентов (Vi cePresi dent) сразу два секретаря; каж­ дый объект класса Secreta ry имеет доступ к одному копировальному аппарату (CopyMachine). Обратите внимание: на этой диаграмме ие отражен факт объявле­ ния кцасса Secretary внутри Manager. ,

,

1

1

',

\

-

-

Manager ,

,_

_ _ _ _

,

.... , "-,

1 '

'

� VicePresident 1

1

, ' - ' .... - -

_ , - ... ,

" - ...

Рис. 6 . 6 . Диаграмма классов для примера Employee

223

6.4. Отношения между объектами и классами

Соответствующая объектная диаграмма изображена на рис. 6. 7.

Рис. 6 . 7. Объектная диаграмма для примера Employee

Из диаrраммы видно, что каждый объект, относящийся к классу Secretary, ис­ пользует объект Copier совместно с другими; Copier является общим полем всех объектов класса Secretary. На объектной диаграмме не показаны экземпляры классов Manager и EmpLoyee, поскольку эти классы не имеют самостоятельНЬiх эк­ земпляров. Секретарь Jean направляет телефонные звонки вице-президенту Pat; если некто, желающий поговорить с Pat, вместо этого говорит с Jean, возможно, диаграмму классов стоит изменить и обозначить на ней связь между VicePresident и Secretary иначе:

. :\.,�- :;::i., . _se��:_./_. . /"

""

"

"

__ " __

"

" " ... ... "

""" "

"

""

•" """"

2

(

.-.... .. . . . . . ...··· ·\ . ..

\ ..

.

:

"" _ _ _ " " "

За полным описанием подобной системы обозначений обращайтесь

к

(5).

224

Глава 6. Объектно-ориентир ованное проектирование

6 . 5 . Субтип ы , н аследова ни е и пе р е н аправле н ие Многое из того, что можно сказать о наследовании, справедливо и по отноше­ нию к типам - пары •тип/субтип• часто реализуются в виде пар •базовый/про­ изводный класс• . Но в некоторых случаях это соответствие нарушается. В этом разделе рассматриваются типичные ошибки, порождающие нежелательный раз­ лад между семантикой типов в предметной области и структурой наследования или делегирования в области решения. В первых двух подразделах (посвящен­ ных ошибкам, связанным с потерей субтипов и типами-омонимами) рассматри­ ваются отношения между типами и классами и ловушки, которые возникают на этой почве. Затем мы выясним, как наследование и делегирование связаны с ин­ капсуляцией, и как они могут привести к нарушению абстракции данных. Далее рассматриваются вопросы, относящиеся к •логической четкости• и инкапсуля­ ции функций классов. В конце раздела описываются некоторые специальные идиомы поддержки множественного наследования.

Н асл едо ва н и е ради н аследования - ош и б к а потери субти пов • Безумие передается п о наследству - оно проявляется в потомках•. Хороший инженер проектирует с учетом реализации. В процессе идентифика­ ции сущностей предметной области (или типов) в голову приходят естественные стратегии реализации, включая структуры наследования, которые можно видо­ изменить для конкретного приложения. Но такая специфическая •настройка• иногда нарушает архитектурные связи между классами и разрушает структуру системы. В этом нам поможет убедиться пример. Вернемся к приложению с обработкой чисел. для которого мы создали класс CompLex (см. с. 99). У него было несколько производных классов, в том числе классы Double и Biglnteger, а также класс Imaginary, который займет центральное место в следующем обсуждении. Класс CompLex обеспечивает эффективное выпол­ нение ряда операций: алгебраических функций, вычисления модуля, присваива­ ния, возможно, вывода и т. д. Класс lmaginary ведет себя как CompLex, но не имеет вещественной части. Из предметной области (математики) очевидно, что множество объектов Imaginary является подмножеством множества объектов Com pLex. Разумно предположить, что тип Imaginary должен быть субтипом Com pLex. Хотя все чисто мнимые числа являются частиым случаем комплексных чисел, не все комплексные числа явля­ ются чисто мнимыми. Отношение субтипизации в предметной области является признаком открытого наследования одного класса от другого в реализации С++. Можно предположить, что программа будет содержать следующий фрагмент:

6.5. Субтипы, наследование и перенаправление

225

c l ass Compl ex puЬl i c : }: c l ass I magi nary puЫ i c :

puЫ i c Compl ex {

}:

Такая запись выражает отношение •lmaginary IS-A Com pLex•, то есть Imagi nary является субтипом Com pLex, а множество объектов Imaginary является подмноже­ ством объектов CompLex. Наследование не обеспечивает принудительноzо приме­ нения этой семантики; вся ответственность за поддержание совместимой семан­ тики между классами иерархии возложена на программиста. Как правило, задача решается сохранением совместимой семантики между функциями классов с оди­ наковой сигнатурой. Оставшаяся часть главы содержит немало рекомендаций относительно того, как обеспечить эту семантику. Если программист будет следовать этим рекомендациям, компилятор сможет предоставить мощную защиту, вспомогательные средства и средства оптимиза­ ции через свою систему типов. Представление компилятора о совместимости ти­ пов тесно связано с архитектурной субтипизацией; отношения подмножеств ме­ жду абстракциями данных соответствуют классам и существующим между ними отношениям специализации. Именно эта связь позволяет языку программирова­ ния выражать архитектурные отношения и до определенной степени обеспечи­ вать соблюдение правил проектирования. Данные Imaginary находятся в базовом классе Com pLex так, чтобы вычислить модуль экземпляра производного класса, мы обращаемся к данным базового класса. Производный класс также содержит •лишний груз•: поле rpart заведомо равно О. -

Учитывать реализацию в процессе проектирования - правильная привычка, ес­ ли соображения реализации не выходят на первое место. В нашем примере они могут навести на мысль, что затраты на хранение лишнего слова в каждом объек­ те Imaginary неприемлемы. Ниже приведена альтернативная реализация К.ilacca Imaginary, лучше соответствующая этому представлению: c l ass Imagi nary { / / Не я вляется произ водным от Compl ex puЬl i c : Imag i nary ( douЫ e i = 0 ) : i pa rt ( i ) { } Imagi nary ( const Compl ex &с ) : i pa rt ( c . i pa rt ) { } Imagi nary& operator= ( const Compl ex &с ) { i pa rt = с . i pa rt : return *thi s : } Imagi nary operator+ ( const Imagi na ry &с ) const { return Imagi nary ( i pa rt + с . i pa rt ) :

226

Глава 6. Объектно-ориентированное проектирование

Imagi nary operator- ( const Imagi nary &с ) const { return Imag i na ry ( i pa rt + с . i pa rt ) : Imagi n a ry operator* ( const Imagi na ry &с > { . . . } I mag i na ry operator/ ( const Imag i na ry &с ) const { operator douЬl e ( ) { return i pa rt : } operator Compl ex ( ) { return Compl ex( O . i pa rt ) : } Imagi na ry operator - ( ) const { return Imagi na ry ( - i pa rt ) : pri vate : douЫ e i pa rt : }:

В этой реализации стоит обратить внимание на два обстоятельства. Во-первых, алгебраические операции она выполняет быстрее своего предшественника. Во-вто­ рых (что более важно), класс Imaginary перестал быть производным от CompLex. Вместо отношения 4IS-A• с CompLex он использует отношение 4USES-A• с DoubLe. Что же плохого в том, что для одних и тех же абстракций оптимизации области решения порождают две разные архитектурные структуры? Очевидно, две раз­ ные версии обладают одинаковой семантикой. Но в оптимизированной реализа­ ции теряется нечто очень важное: она перестает отражать связь между классами CompLex и Imaginary в предметной области. Класс Imaginary в приведенной реали­ зации продолжает отражать некоторые свойства CompLex и до определенной сте­ пени остается совместимым с ним; эта задача решается функциями: Imagi nary : : operator Compl ex Imagi nary : : Imagi nary ( const Compl ex& )

Однако из-за потери наследования мы уже не сможем использовать объекты Imaginary вместо CompLex там, где это имеет смысл. Например, оператор преобра­ зования не поможет в ситуации, когда вместо Com pLex* передается Imaginary*. Разные варианты реализации обладают своими достоинствами и недостатками, порой весьма нетривиальными. Приемлемое решение часто достигается только посредством компромисса. Возможно, в данном примере следовало бы объявить оба класса CompLex и Imaginary производными от абстрактного базового класса Number без вынесения общих данных в базовый класс. Тем не менее, в большинстве случаев злоупотребления наследованием происхо­ дят от недостатка опыта, легко обнаруживаются и имеют очевидные решения. Один программист, новичок в объектно-ориентированном программировании, объявил класс фильтра нижних частот FiLter производным от классов Resistor, Capacitor и Inductor. Однако фильтр ие является частным случаем резистора, кон­ денсатора или индуктора, а множество объектов lnductor не содержит множество объектов FiLter. •Правильное• решение состоит в том, чтобы включить объект ка­ ждого компонента в объект FiLter. По аналогии с тем, как фильтр содержит кон­ денсатор, индуктор и резистор в предметной области, программная абстракция FiLter должна содержать соответствующие вложенные объекты.

6.5. Субти пы , наследование и перенаправление

227

А вот еще один плохой пример:

cl ass Shape : puЬl i c Poi nt . puЬl i c Col or puЫ i c : vi rtua l voi d draw( ) : vi rtua l voi d rotate ( const Angl e& ) : };

1 1 Плохо

Данное объявление предполагает, что геометрическая фигура (Shape) является частным случаем цвета (CoLor), что неверно; фигура также не является и частным случаем точки ( Point). Верно другое - геометрические фигуры содержат цен­ тральную точку и обладают цветом (отношение • HAS-A• ). Конечно, реализация должна выглядеть так:

cl ass Shape { puЫ i c : Shape ( const Poi nt& ) : vi rtual voi d draw( ) ; vi rtua l voi d rotate ( const Angl e& ) : pri vate : Poi nt center : 1 1 Center и col or - атрибуты . Col or col or : 1 1 или свойства . класса Shape }; Напоследок рассмотрим классы String и Path Name, представленные в разделе 4.2. Если существующая библиотека содержит класс String, возникает искушение объявить класс Path N a m e производным от него. Открытое наследование по­ зволит передавать объекты Path Name там, где ожидается объект String. Например, существующая хеш-функция для String может быть использована для хеширова­ ния PathName. Но открытое наследование приведет к тому, что вызовы некото­ рых функций PathName нарушат логическую целостность объекта. Конструктор Path Name и операции класса могут предотвратить вхождение недопустимых сим­ волов во внутреннее представление. Но если Path N am e унаследует функцию operator[] (i nt) от класса Stri n g , то следующий фрагмент нарушит ограничения, которые PathName пытается соблюдать в своих операциях:

i nt ma i n O { PathName batFi l e '*' batFi l e [ O J

= ·

"AUTOEXEC . BAT" : 1 1 Запрещенный сим вол в имени файла

Как было показано в разделе 4.2, такие проблемы решаются за счет закрытого наследования. Сомнительные иерархии также возникают при опоре на изменчивость как осно­ ву для наследования, то есть когда один класс строится на базе другого через отношение •LIKE-A• вместо отношения • IS-A•. Близким родственником из­ менчивости является обратное наследование, имеющее своих сторонников как инструмент многократного использования. Например, графический пакет может

228

Глава 6 . Объектно-ориентированное проектирование

содержать класс Shape с производным от него классом CircLe. Если потребуется нарисовать эллипс, мы обобщаем CircLe посредством наследования и подменяем его операции для получения класса ELLi pse. Оба подхода достаточно опасны и бу­ дут рассмотрены далее.

Случайное н аследование - омон и м ы в м и ре т ипов До сих пор в нашем рассказе основное внимание уделялось зависимости структуры системы от семантического поведения абстракций предметной области и представ­ ляющих их классов. Поведение класса должно определять логически целостную абстракцию, а классы в иерархии наследования группируются по предоставляе­ мому ими сервису. Группировка по поведению сходна с группировкой классов по внешнему виду, но не эквивалентна ей. Различия между этими двумя пара­ дигмами (моделями структурирования) состоит в том, что первая требует глубо­ кого понимания семантики классов, а вторая . может базироваться на простом лексическом сравнении имен. Но существует третий способ группировки клас­ сов по внутренней структуре представления. Он напоминает второй способ, но вместо интерфейса класса анализируется его внутренняя структура. Класс Li ne в графической программе содержит два объекта Point; это же относится к клас­ су RectangLe. Из этого можно сделать вывод (ошибочный), что один из этих клас­ сов является специализацией другого. При проектировании интерфейсов класса или группировке классов посредством наследования очень важно учитывать свойства поведения. В [ 6] предлагает сле­ дующий критерий организации классов в иерархиях наследования: ... если для каждого объекта о1 типа S существует объект о2 типа Т такой, что для всех программ Р, определенных в контексте Т, поведение Р не изменяет­ ся при замене о1 на о2, то S является субтипом Т. Например, некоторая функция (из интересующих нас программ Р), написанная для работы с параметрами (о2) типа CompLex (тип Т), также должна быть спо­ собна принимать объекты (о 1 ) типа Imaginary (тип S). В практическом смысле все функции Т могут безопасно применяться к объектам S, поскольку поведение всех программ Р остается неизменным при замене объектов S ( о 1 ) объектами Т (о2). И это выглядит разумно, поскольку тип lmaginary может выполнить любую операцию, поддерживаемую типом CompLex, хотя обратное неверно: функция, на­ писанная для Imagi nary, в общем случае не работает с типом CompLex. Этот принцип, называемый прииципом подстановки Лисков, играет важную роль в объектно-ориентированном проектировании. Мы еще неоднократно вернемся к нему на страницах книги. Некоторые проектировщики выступают за применение наследования, не отве­ чающее принципу подстановки. Иногда это рекламируется как субтипизация, определяемая больше реализацией, нежели архитектурой. Но такие .применения порождают двусмысленности, являющиеся аналогом омонимов в языке програм-

6.5. Субтипы , наследование и перенаправление

229

мирования. В некоторых случаях подобные злоупотребления возникают из-за попыток применения объектной парадигмы 4Наоборот•, например, объявления класса ELLipse производным от CircLe и его 4Приукрашиванием•, обеспечивающим возможность обобщения. Поскольку такие случаи очевидно сомнительны по принципу подстановки Лисков, здесь они не рассматриваются. Большинство других злоупотреблений относится к категории изменчивости, то есть попытки 4Наследования в сторону• с применением отношений 4LIKE-A• вместо • IS-A•.

П оуч ител ь н ый пример Допустим, имеется готовый класс List, который служит контейнером для упоря­ доченных наборов объектов. Класс List не делает никаких предположений отно­ сительно содержащихся в нем объектов (то есть может быть реализован как спи­ сок void*). Интерфейс List выглядит примерно так: c l ass Li st { puЫ i c : voi d* head ( ) : voi d* tai 1 О : i nt count О : Bool has ( voi d* ) : voi d i nsert ( voi d* ) : }:

11 11 // 11 11

Воз в ращает начало списка Возвращает конец списка Воз вращает количест во элементов в списке Проверяет наличие элемента в списке Включает новый элемент в список

Пусть потребовалось создать новый класс множества Set со следующим интер­ фейсом:

c l ass Set { puЫ i c : / / Воз вращает количест во элементов в м ножестве i nt count ( ) : / / Проверяет наличие элемента в множестве Bool has ( voi d* ) : voi d i nsert ( vo i d* ) : / / Включает новый элемент в множество Гордясь собственной наблюдательностью, мы замечаем сходство между этими классами. Возможно, у нас даже имеются средства, позволяющие взять list за основу для реализации Set. Итак, мы пытаемся воспользоваться наследованием:

c l ass Set : puЫ i c Li st { puЬl i c : voi d i nsert ( voi d* m ) { i f ( ! ha s ( m ) ) L i st : : i nsert ( m ) : } pri vate : 1 1 Следующие две строки я вл яются недопустимыми конструкци я м и С++ / / Нач ало множества не определено L i st : : head : L i st : : ta i l : / / Конец м ножества не определен }: Итак, чтобы реализовать семантику множеств, запрещающую вставку нескольких экземпляров одного значения, мы должны переопределить функцию i nsert(void*).

230

Глава 6. Объектно-ориентированное проектирование

Операции count() и has(void*) наследуются без изменений. Операции head() и taiL() становятся недоступными - они не определены для множеств, хотя и имеют смысл для списков. Однако при этом возникает серьезная проблема. Применяя наследование, мы ут­ верждаем, что множество является частным случаем списка ( •IS-A• ) Другими словами, предполагается, что объекты List везде могут быть заменены объекта­ ми Set Но это просто неверно, как неверно и обратное. Для примера возьмем их поведение при повторном включении одинаковых значений; для класса Set опе­ рация insert будет пустой, а для List - нет. Более тоrо, класс Set не поддерживает семантики операций head() и taiL(). С++ приходит на помощь в последнем случае: приведенные выше объявления Set, в которых List::head и List: :taiL объявлены за­ крытыми, определены в языке как недопустимые. Если бы такие операции были разрешены, их семантика была бы сомнительной - особенно для виртуальных функций. .

Итак, типы List и Set почти являются типами-омонимами: они обладают сходны­ ми сигнатурами при существенно различающейся семантике (если бы тип List не содержал операций head() и taiL(), омонимия была бы полной). Именно из-за по­ добных проблем Лисков разработала свои рекомендации по сигнатурам абстрак­ ций в иерархиях наследования, приведенные в начале раздела. Рассмотрим другой пример, позаимствованный из [7]. Если у нас уже имеется готовый класс CircuLarlist, и мы хотим создать класс Queue, можно попробовать сделать следующее:

c l ass Ci rcu l a rli st puЫ i c : i nt empty ( ) : Ci rcu l a rl i st ( ) : voi d push ( i nt ) : / / Занесение с начала i nt рор ( ) : / / Занесение с конца voi d enter ( i nt ) : pri vate : cel l *rea r : }: c l a s s Queue : puЬl i c Ci rcul a rli st puЫ i c : Queue ( ) { } voi d enterq ( i nt х ) { enter ( x ) : } i nt l eaveq ( ) { return рор ( ) : }: Класс Queue автоматически получает операции push(i nt) и рор (); это побочный эффект наследования, не имеющий смысла для Queue. Корни этой проблемы уходят в механизм открытого наследования. Существует хорошее эмпирическое правило: открытое наследование должно применяться только для отношений суб­ типизации; если наследование применяется для удобства или по соображениям

6.5. Субти пы , наследование и перенап равление

231

многократного использования кода, следует применять закрытое наследование. Закрытое наследование (и лучшие альтернативы) рассматривается в главе 7 как средство многократного использования кода. Похожие (и наверное, еще худшие) проблемы возникают тогда, когда наследова­ ние применяется для многократного использования представления одного типа в другом. Например, класс Set объявляется производным от list, потому что внут­ ренние данные Set очень похожи на list: просто операции класса интерпретируют их несколько иначе. Естественно, такой подход не имеет отношения к типам и под­ типам, и его следует избегать. Мы вернемся к этой теме в главе 7. Решения без побочного наследован ия В ситуациях, когда наследование кажется хорошим решением, часто удается обой­ тись более простыми средствами . Например, вспомним пример с классами list и Set Пусть неудачная попытка вас не смущает; возможность использования некоторых операций list для реализации Set существует реально, просто открытое наследова­ ние было неправильным способом ее реализации. Вот как выглядит другой вариант: c l ass L i st { puЬl i c : voi d* hea d ( ) : voi d* ta i l ( ) : i nt count ( ) : Вооl has ( voi d* ) : voi d i nsert ( vo i d* ) :

}:

11 11 11 11 11

Воз вращает начало списка Воз вращает конец списка Воз вращает количест во элементов в списке Проверяет наличие элемента в списке Включает новый элемент в список

c l ass Set puЫ i c : i nt count ( ) { return l i st . count ( ) : } Bool has ( voi d* m) { return l i st . ha s ( m ) : } voi d i nsert ( vo i d* m > { i f ( ! ha s ( m ) ) l i st . i nsert ( m ) : pri vate : L i st l i st : }:

Три операции Set вручную перенаправляются внутреннему объекту list. При этом нам не нужно принимать специальные меры, чтобы запретить вызов опера­ ций head() или taiL() [6]. В этом варианте функциональность класса List заново используется для под­ держки семантики класса Set. Хотя между List и Set существует определенное сходство, мы отказываемся от попыток установить тесные связи между их типа­ ми. Если взглянуть на происходящее несколько иначе, такая реализация ука­ зывает на архитектуру, которая ничего не говорит о связи между Set и List. Она отдаленно напоминает методику делеzирования, в основу которой заложены не классы, а экземпляры (см. с. 1 66). С++ поддерживает перенаправление операций от одного класса к другому, которое может считаться слабой формой делегиро­ вания. Механизм делегирования описан в разделе 5.5.

232

Глава 6. Объектно-ориентированное проектирование

Наследован ие с р асш и рением и п одмено й Правильное применение наследования, обеспечивающее реализацию субтипизации по принципу подстановки Лисков, достигается путем наследования с расширени ­ ем . Интуиция подсказывает, что добавление новых функций в класс сокращает количество характеризуемых им объектов. Например, наделение простого телефо­ на функции ускоренного вызова создает новую абстракцию телефона с ООльшим количеством функций, чем у оригинала: множество телефонов с ускоренным вы­ зовом является подмножеством всех телефонов. Поскольку существующие опе­ рации остаются без изменений, объект производного класса по-прежнему всегда может использоваться вместо объекта базового класса - новые операции произ­ водного класса при этом просто игнорируются. Но это означает, что программа, работающая с такими объектами через интерфейс базового класса, не сможет ис­ пользоваться новыми функциями, добавленными на более низких уровнях. Иначе говоря, различия между Si mpLePhone и SpeedCaLLing Phone заставляют программи­ ста работать с каждым классом в его специфическом контексте. Функция, объяв­ ленная с параметром SimpLePhone, примет объект SpeedCaLLingPhone, но не сможет задействовать возможности ускоренного вызова. За счет наследования с расширением мы можем добавлять новые функции в про­ изводный класс, но не можем работать с ними через интерфейс базового класса. Данная модель наследования устанавливает жесткие ограничения и не позволяет организовать диспетчеризацию новых функций на стадии выполнения - одну из важнейших составляющих мощи объектной параднгмы. Если программисту прихо­ дится работать с каждым классом в иерархии наследования в его специфическом контексте, вся сила абстракции наследования теряется. Программист должен иметь возможность работать с большими наборами классов, интерпретируя их так, слов­ но они являются экземплярами одного класса, являющегося их общим предком. Другая крайность - использование абстрактного базового класса, определяюще­ го только сигнатуру и полностью лишенного прикладной семантики. Отсутствие семантики гарантирует отсутствие семантических конфликтов с производными классами. Для примера рассмотрим •универсальный базовый класс• с операция­ ми at, atPut, more, m oreNow и getNext. которые работают с объектом через поля или рассматривают его как составную структуру данных с последовательным хранени­ ем элементов. Операция at может получать строку или значение перечисляемого типа - ключ, однозначно определяющий возвращаемое значение ( ассоциатив­ ная выборка). Функция atPut может получать ключ и значение; ключ определяет поле, которому присваивается заданное значение (ассоциативное присваивание). Кроме того, некоторые операции atPut могут иметь побочные эффекты - при пе­ редаче специальных ключей инициируется выполнение внутренних функций класса. Рассмотрим простой пример ассоциативного массива:

cl ass Assoc i ati veArray { puЫ i c : voi d atPut ( voi d *el ement . Stri ng key ) : voi d *at ( const St ri ng& > : }:

6 . 5.

Субтипы, наследование и перенаправление

233

i nt ma i n ( ) { Assoc i ati veArray а : a . atPut ( ( voi d* ) 233 . "Assoc i ati veArray exampl e " ) : а . atPut ( ( voi d* ) 230 . " C i rcul a rl i st examp l e " ) : i nt c i rcl i stPage = ( i nt ) a . at ( "Ci rcul a rli st exampl e " ) : 1 1 Должно быт ь 230 / / Из менение ра з м ера массива a . atPut ( ( voi d* ) lO . " ! s i ze" ) : / / Вывод содержим о г о масс и в а a . atPut ( ( voi d* ) O . " ! pri nt " ) : i nt s i ze = ( i nt ) a . at ( " ! s i ze" ) : / / Получение раз мера массива

Ассоциативный массив используется для хранения номеров страниц, на которых находятся соответствующие разделы книrи. Выборка осуществляется по ключу. Но наряду с обычными ключами существуют специалf:.ные ключи с префиксом ! , используемые функциями a t и atPut ради побочных эффектов: вывода, получе­ ния и изменения размера массива и других служебных операций. Подобный стиль, часто называемый каркасным программированием, характерен для многих контекстов символических языков. Он приводит к появлению базо­ вых классов, представляющих •толстый интерфейс• , в котором объединяются функции множества несвязанных производных классов. Мы вернемся к этому стилю программирования в главе 8. Подобные интерфейсы обладают почти неограниченной гибкостью; с другой сто­ роны , они не способны выразить намерения проектировщика или обеспечить их соблюдение. Кроме того, их реализация будет недостаточно эффективной, потому что каждая операция требует поиска строки. Нам нужны полноценные классы, которые бы передавали общую семантику интерфейса, не ориентируясь на конкрет­ ную реализацию. Интерфейсы такого рода характеризуются базовыми классами, которые служат •заготовками • для реализаций, определяемых позднее в производ­ ных классах. Базовый класс, возглавляющий иерархию, может не использоваться для создания объектов, а лишь определять интерфейс к объектам производных классов на стадии компиляции. Базовый класс характеризует общее поведение всех классов, находящихся в иерархии под ним. Например, мы можем объявить базовый класс Wi ndow с операциями move( Poi nt), addChar(char), cLear(), deLeteli ne() и т. д., а также ряд производных классов: CursesWi ndow, XWindow, MSWi n d ow и SunViewWi ndow. Самостоятельные экземпляры Window никогда не создаются в про­ граммах; они просто не умеют ничего делать! Базовые классы, обладающие таким свойством, называются абстрактиыми базовыми классами. Абстрактные базовые классы С++ подробно описаны в разделе 5.4. Иерархия Window выглядит при­ мерно так:

c l ass Wi ndow { puЬl i c : vi rtua l voi d vi rtua l voi d vi rtua l voi d }:

addChar ( cha r ) c l ea r ( ) de 1 etel i ne( )

= О: = О: = О:

234

Глава 6. Объектно-ориентированное прое ктирование

c l ass CursesWi ndow : puЬl i c Wi ndow { puЬl i c : voi d addCha r ( char с ) { /* Подход ящий ал г оритм */ } voi d c l ea r ( ) { } voi d de 1 etel i ne( ) { ... } }: c l ass XWi ndow : puЫ i c Wi ndow puЬl i c : voi d addCh a r ( char с ) { /* Подходящий ал горитм */ } c l ea r ( ) voi d { } de l etel i ne( ) voi d { ... } }: 11

и

т.

Д.

Все функции класса Window объявлены чисто виртуальными, и класс не содер­ жит данных. Такие классы называются чисто абстрактиъши базовЪtми класса­ они характеризуют абстрактный тип данных, но ничего не говорят о его ми реализации. -

Замена функций базового класса в производном классе называется наследовани­ ем с подменой. Подмена функций базового класса в производном классе должно сохранять их семантику. Допустим, вам потребовалось написать виртуальную функцию с учетом контекста, в- котором производится вход в функцию и выход из нее [8]. Таким контекстом является состояние текущего объекта (возможно, дополненное некоторой внешней информацией о состоянии). При входе вирту­ альная функция базового класса должна предполагать о контексте не меньше, а при возврате - не больше, чем ее аналог в производном классе. Входной крите­ рий основан на изменении контекста при вызове функции базового класса для объекта производного класса, то есть в случае, когда вызов обрабатывается функцией производного класса. Так как функция производного класса должна быть способна обработать любые запросы, обрабатываемые версией базового класса, она не может вьщвигать более широкие предположения, чем версия базо­ вого класса. Для выходного критерия справедливо обратное утверждение: функ­ ция производного класса должна делать, по крайней мере, не меньше того, что ожидается от версии базового класса, хотя может делать и больше. Если эти критерии выполняются, функция производного класса называется се­ мантически совместимой с функцией базового класса. Они очевидны в тех пред­ метных областях, в которых отношения между типами имеют формальную осно­ ву, например, в иерархиях математических типов (вещественные, рациональные, неотрицательные рациональные, кардинальные и т. д.). Установление подобных критериев в областях графики, экономических систем, телекоммуникаций и боль ­ шинстве других областей, с которыми связана основная часть нашей работы, по­ требует несколько больших усилий. Обратите внимание: чисто виртуальные функции абстрактных базовых классов заведомо удовлетворяют этим критериям. Все они знают о контексте вызова

6.5. Субти n ы , наследование и nе ре наnравление

235

лишь то, что им передается заданное количество параметров некоторых типов. Эти функции не содержат кода, а следовательно не меняют свой контекст, а вся информация о выходном контексте сводится к типу возвращаемого значения. В распространенной ситуации, когда виртуальная функция •нечистого• базово­ класса подменяется в производном классе, эти две функции должны обладать, по крайней мере, синтаксическими различиями - если бы их не было, производ­ ный класс мог бы использовать версию базового класса. Тем не менее, эти две функции должны б ыть семантически совместимыми (см. выше). Сохранение имени функции не гарантирует совместимости семантики. Проектировщик дол­ жен проследить за тем, чтобы функция производного класса обладала по отно­ шению к производному классу такой же семантикой, как функция базового класса - по отношению к базовому классу.

го

Итак, в наследовании существуют две крайности. Первая - абстрактные базо­ вые классы, когда сиrnатуры всех классов в иерархии совпадают, но новые про­ изводные классы слегка изменяют семантику операций своих родителей. Другая крайность - наследование с расширением, при котором существующая реализация остается неизменной, а изменения интерфейса сохраняют полную совместимость снизу вверх. К сожалению, на практике большинство ситуаций применения на­ следования не относится ни к одному из этих крайних случаев. Абстрактные ба­ зовые классы, содержащие только виртуальные функции и не содержащие дан­ ных, встречаются редко. Хорошие абстрактные базовые классы проектируются для отражения общих аспектов поведения своих производных классов. Стремле­ ние к многократному использованию кода приводит к тому, что общие функции и данные реализации выделяются в базовый класс. Они образуют •поведение по умолчанию• , которое по необходимости подменяется в производных классах. Если позаботиться о сохранении семантики функций (поведения) при переходе от базового класса к производным, желаемая архитектурная абстракция иерар­ хий наследования будет обеспечена. С другой стороны, наследование с расширением тоже создает свои проблемы. Если анализ приложения показывает, что некоторая функция принадлежит толь­ ко производному классу, но не входит в его базовый класс, часть архитектурной абстракции утрачивается. Для примера возьмем библиотеку Shape с замкнутыми и незамкнутыми фигурами (рис. 6.8) . Если мы собираемся добавить функцию fitt для закраски внутренней области фигуры, куда ее следует включать? Если в класс Shape, то следует ли объявить ее чисто виртуальной функцией или наделить ка­ ким-нибудь стандартным поведением? Объявление функции чисто виртуальной приведет к тому, что она будет определена для линии (Line) и других незамкнутых фигур, для которых она не имеет смысла. Определить для функции Shape: :fitt не­ кое стандартное поведение? Но поведение, стандартное для окружности (Circte), скорее всего не имеет смысла для класса Li ne и его потомков, поэтому такой вариант тоже не годится. С семантической точки зрения правильнее всего бы­ ло бы определить fi tt чисто виртуальной функций в классе замкнутых фигур (CtosedShape), но это означает, что приложение, которому потребуется закраши­ вать фигуры, наряду с интерфейсом Shape должно видеть интерфейс CtosedShape.

236

Глава 6. Объектно-ориентированное проектирование

А это, в свою очередь, означает один лишний класс, о котором придется помнить программисту, одну лишнюю страницу документации и один лишний интерфейс, изменение которого способно нарушить работу приложения. Наконец, если бы классы замкнутых и разомкнутых фигур ( CLosedShape и OpenShape) не входили в исходную иерархию наследования (что вполне вероятно, если функция fiLL не учитывалась в первоначальной архитектуре), найти семантически обоснованное место для fiLL становится еще сложнее. Обычно в подобных ситуациях програм­ мисту приходится слегка лукавить насчет семантики и определять функцию fitt в классе Shape как виртуальную функцию с пустым телом. Другой, более строгий вариант - запускать исключение из вызова Shape::fitt.

Shape

Circle Рис. 6.8.

Line

Еще одна библиотека с геометрическим и фи гурами

Большинство реальных случаев лежит где-то посередине между �полюсами• насле­ дования с чистым расширением и абстрактными базовыми классами, поэтому про­ граммисты часто ошибаются с выбором отношений субтипизации. Можно сказать, что неудача в приведенном выше примере, в котором класс Set определялся произ­ водным от List с подменой функции insert(void*), произошла именно по этой при­ чине. Такое �примесное• наследование часто встречается в программах и создает массу сложностей с сопровождением и эволюцией, причем программист даже не понимает настоящей причины своих бед. Проблема описана в литературе и извест­ на на практике, но программирование, основанное на наследовании вместо суб­ типизации, рождает ее снова и снова. Положение усугубляется полиморфизмом. Учтите, что проблемы возникают только при наличии виртуальных функций и полиморфном использовании взаимосвязанных классов. Предположим, классы Set и List в виде, представленном на с. 229 (наследование с подменой), рассмат­ риваются как совершенно самостоятельные классы. Другими словами, объекты List всегда интерпретируются как List, а объекты Set всегда интерпретируются как Set, и программа никогда не делает попыток заменить один объект другим. В этом случае подмена не создает нежелательных последствий; оно превращает­ ся в артефакт решения, не связанный с архитектурой приложения. Вопрос суб­ типизации между этими двумя абстракциями (и обусловленная ею возможность использования одной абстракции вместо другой) никогда не возникает в области приложения. Но если функции List объявлены виртуальными и их виртуальность используется в программе, свойства подмены оказываются задействованными,

6.5. Субтипы, наследование и перенаправление

237

что может привести к проблемам. Глубинная связь между наследованием ( отра­ жающим сходство реализаций или отношения субтипизации) и полиморфизмом (конструкцией области решения, требуемой для поддержки субтипизации в пред­ метной области) делает этот вопрос настоящей проблемой. Давайте снова предположим, что классы List и Set рассматриваются как отдель­ ные объекты. Решение на с. 231 дает пример объединения классов Set и List без наследования. Вместо этого объект Set перенаправляет запросы своему внутрен­ нему объекту List. Два взаимодействующих типа, один из которых перенаправля­ ет часть своих операций другому типу, редко наводят на мысль о применении субтипизации. Впрочем, у этого правила имеются исключения, особенно в не­ тривиальных идиомах С++; они будут рассмотрены позднее. Но в общем случае перенаправление способно обеспечить полиморфизм (или видимость полиморфиз­ ма) без ненужного наследования. К недостаткам перенаправления следует отне­ сти �о, что по сравнению с наследованием оно обеспечивает более низкую безо­ пасность типов на стадии компиляции и хуже отражает логику проектирования.

Наследован ие с со кращени е м Некоторые среды объектно-ориентированного программирования позволяют со­ кращать производные классы, то есть исключать из них свойства базовых классов. С++ не поддерживает сокращения при открытом наследовании - такая поддерж­ ка на стадии компиляции противоречит возможности преобразования объекта производного класса в объект базового класса во время выполнения. Следую­ щий фрагмент кода поясняет сказанное: c l ass Base { puЬl i c : voi d functi on l ( ) ; voi d functi on2 ( ) ; ; }

c l ass Deri ved : puЫ i c Base { pri vate : 1 1 Сокращение ( з а прещено в С++ ) Ba se : : funct i on l : puЬl i c : voi d functi on3 ( ) : }; voi d f ( Base *ba seArg ) { ba seArg ->funct i onl ( ) ;

i nt ma i n ( ) { Deri ved *d f(d) : return О :

=

/ / Сюрпри з : сокращение и г норируется

new Deri ved ;

238

Глава 6. Объектно-ориентированное проектирование

Если бы сокращение было возможно, компилятору пришлось бы провести абсо­ лютно полный анализ потока данных и убедиться в том, что исключенные свой­ ства объекта нигде и никогда не потребуются. Проблема становится еще более серьезной при использовании виртуальНЬIХ функций с раздельной компиляцией. Один модуль, обладающий доступом к объявлению производного класса, создает объект производного класса. Этот объект передается через указатель на базовый класс (допустимое преобразование) функции другого модуля, которая не имеет доступа к объявлению производного класса и ничего не знает о сокращении. Второй модуль сможет вызывать функции базового класса для объектов, из которых эти функции были исключены! Подумайте, что произойдет в приве­ денном фрагменте, если функции functionl и fu nction2 объявить виртуальными, а функцию f компилировать в отдельном исходном файле, в котором видно толь­ ко объявление Base. Поддержка сокращения на стадии выполнения привела бы к чрезмерному ус­ ложнению языка. С каждым объектом класса пришлось бы передавать дополни­ тельные данные, указывающие, какие функции класса следует замаскировать как •сокращенные•. Данные базового класса можно сделать недоступными для производного класса и его клиентов, но их не удастся ликвидировать (скажем, для экономии памяти в объектах производного класса). А что касается виртуальных функций, вызов функции, прошедший проверку типов во время компиляции, заведомо вызовет что -то на стадии выполнения - вместо того, чтобы выдать сообщение ся вас не понял• , как в языках типа Smalltalk. Если производный класс не содержит функций с соответствующим именем, будет использована функция базового класса с подходящими именем и сигнатурой.

С неполиморфным наследованием дело обстоит несколько иначе. Как было пока­ зано ранее, сокращение на стадии компиляции посредством изменения видимо­ сти запрещено по правилам языка. Например, следующий фрагмент невозможен:

cl ass L i st { puЫ i c : voi d* hea d ( ) : voi d* ta i l O : i nt count ( ) : l ong ha s ( voi d* ) : voi d i nsert ( voi d* ) : }: cl ass Set : puЬl i c L i st { puЬl i c : voi d i nsert ( voi d* m ) : pri vate : L i st : : count : 1 1 Ошибка L i st : : ha s : 11 Ошибка

}:

6.5. Субти n ы , наследование и nеренаправление

239

Однако в случае закрытого нас:ледования (фактически исключающего все откры­ тые операции базового класса) доступ к операциям базового класса может пре­ доставляться по отдельности для каждой операции:

c l ass Set : pri vate L i st { puЬl i c : voi d i nsert ( vo i d* m ) : L i st : : count : L i st : : ha s : }:

Наследование с сокращением нередко является признаком неправильной струк­ туры наследования, например, обратного направления связей или перекосов в отношениях сродитель/потомок•. Вообще говоря, сокращение порождает мас­ су проблем, и его не рекомендуется применять как методику проектирования (даже разовые применения нежелательны).

П отр е б н ость в фун кция х классо в с сс и ст инн ой» с е ма нтикой В области объектно-ориентированных проектирования и программирования опе­ рации с объектами соответствуют некоторой семантике реального мира. Это утверждение более сильное и глубокое, чем простое заявление о том, что работа с объектами должна вестись через хорошо определенные интерфейсы. Сохране­ ние семантической тождественности является залогом успешного ра3ВИТИЯ про­ екта. Инкапсуляция прежде всего определяет не столько местонахождение дан­ ных или видимость операций, сколько организацию их поведения. Для пояснения сказанного рассмотрим пару примеров. Эти примеры закладыва­ ют основу для анализа связей между инкапсуляцией и наследованием в после­ дующих разделах.

П ример 1 . О перации get и set Программисты часто воспринимают рекомендации объектно-ориентированного проектирования в форме соткрытые данные вредны; данные всегда необходимо инкапсулировать, а доступ к ним должен производиться только через операции get и set•. Один из недостатков такого подхода - его выражение в контексте внутренних данных, относящихся исключительно к области решения. Другая проблема состоит в том, что этот подход ничего не говорит о целостном поведе­ нии объекта или представляемого им типа предметной области. Если функция ничего не говорит об объекте в целом или сигнатуре его поведения, она начинает выглядеть неестественно. Если она никак не связана с предметной областью, напрашивается предположение, что она создавалась на базе области решения. Последнее обстоятельство не так уж страшно, но наличие операций, не связанных с предметной областью, затрудняет понимание логики класса и его эволюцию.

240

Глава

б.

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

Имена функций должны быть четкими и семантически насыщенными; они долж­ ны иметь понятный смысл или интерпретацию в контексте своего класса. При­ сутствие функций get и set часто свидетельствует о том, что архитектура прило­ жения нуждается в пересмотре. Что делает объект, обращающийся к внутренним данным, и за что он отвечает? Чего добивается клиент, изменяющий состояние внутренних данных? Подобные вопросы помогают связать функции класса с пред­ метной областью и отделить то, что должен знать класс, от тех аспектов, которые видны его клиентам. Функции класса, предназначенные исключительно для дос­ тупа к внутренним данным, не обеспечивают инкапсуляцию за счет соблюдения правил видимости С++: наоборот, они работают по правилам видимости С++ для нарушения инкапсуляции.

П риме р 2 . Открыты е данн ы е Конечно, наличие открытых данных у класса нарушает инкапсуляцию. Присут­ ствие внутреннего объекта В в открытом интерфейсе другого объекта А загромо­ ждает интерфейс А поведением В. Маловероятно, чтобы поведение В выражалось для его клиентов в контексте А: работа с В должна осуществляться функциями класса А. В этом случае интерфейс А остается однородным и •гладким•, а поль­ зователю не приходится спускаться на один уровень вглубь для достижения нужного результата:

Неп рав ил ьно : c l ass Wi ndow { puЫ i c : Cursor cur :

Пра в ил ь но : c l ass Wi ndow { puЫ i c : voi d MoveCursor ( i nt . i nt ) :

}:

pri vate : Cursor cur : }: i nt ma i n ( ) { Wi ndow wi n : wi n . moveCursor ( 1 0 . 5 ) :

i nt ma i n ( ) { Wi ndow wi n : wi n . cur . move ( 10 . 5 ) :

Вызов win.moveCursor лучше выражает намерения программиста, чем win.cur.move, потому что он понимается на одном уровне абстракции вместо двух. При наследовании пример 2 становится еще интереснее. В [9] указывается, что в большинстве объектно-ориентированных языков производные классы могут обращаться к внутренним данным базовых классов, которые в остальных случаях недоступны извне. К С++ это не относится; таким образом, в С++ дело обстоит не так плохо, как в общем случае. Тем не менее, пользователи С++ могут реали­ зовать описанную в [9] популярную семантику, присвоив полям данных атрибут protected. О том. как это связано с нарушением инкапсуляции, рассказывается в следующем разделе.

6.5.

Субтипы , наследование и перенаправление

24 1

На следование и нез ави с и м о сть кл ассов Объектно-ориентированное проектирование базируется на абстракции данных. Абстракция данных как методика обобщения обычно тесно связана с инкапсуля­ цией, то есть механизмом сокры.тия информации. Хотя абстракция данных озна­ чает попытку скрыть от пользователя неприятные подробности реализации, при желании пользователь может получить доступ к •внутренностям класса•. С дру­ гой стороны, инкапсуляция означает яростную защиту от любых попыток пре­ одолеть интерфейс и ознакомиться с внутренним строением класса. Абстракция до некоторой степени относится к предметной области, а инкапсуляция (опять же до некоторой степени) - к области решения. Нельзя сказать, что абстракция данных и сокрытие информации необходимы или достаточны для объектно-ориентированного проектирования, но они являют­ ся важными компонентами хорошего объектно-ориентированного стиля и «по­ хорошему• независимы от объектно-ориентированного программирования. Кро­ ме того, на них интересно взглянуть в свете непрекращающихся споров о том, нарушает ли наследование принципы абстракции данных. Сокрытие данных обладает тремя свойствами, которые обязательно должны со­ храняться и которые служат слакмусовой бумажкой• для выявления нарушений инкапсуляции или сокрытия информации. Здесь они формулируются в контек­ сте изменений программы, поскольку их практическая ценность проявляется именно при эволюции программы. Под •изменениями• здесь подразумевается нечто большее, чем традиционные модели модификации внутреннего представ­ ления или функций абстрактного типа данных, хотя они составляют очевидное и наиболее распространенное направление эволюции программ. В изменениях также следует учитывать фактор наследования, когда новые классы строятся по­ средством модификации поведения существующих классов. + Приводит ли изменение в одном классе к необходимости пересмотра архи­

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

модификация, внешнее применение наследования или внешнее применение перенаправления) могли изменить семантику или поведение класса? (На этот критерий можно взглянуть иначе: спросите себя, какие компоненты системы придется тестировать заново при внесении изменений.) + К каким затратам (например, времени на перекомпиляцию) приведет измене­

ние класса? Пропорциональны ли эти затраты размеру класса; размеру тех классов, с которыми он взаимодействует; размеру программы, в которой этот класс используется; размеру всего проекта, содержащего класс? Здесь мы будем рассматривать абстракцию и сокрытие данных только с точки зрения наследования - а именно с точки зрения того, когда наследование созда­ ет особые проблемы или открывает особые возможности. С++ позволяет управлять доступностью членов базового класса (данных или функций) для производных классов. Следовательно, реализацией семантики

242

Глава 6. Объектно -ориентированное проектирование

сокрытия данных и инкапсуляции занимается программист С++. Стандартное открытое наследование обладает вполне разумной семантикой обеспечения ин­ капсуляции. + ЗакрЫТЪiе члены базовых классов недоступны (во всяком случае, напрямую)

для производных классов.

+ Открытые члены базовых классов доступны для производных классов не

в большей и не в меньшей степени, чем для любого другого кода. + Члены, объявленные открытыми в базовом классе, становятся частью сиrnа-

туры (открытого интерфейса) производного класса. Но при этом остается одна серьезная проблема. Хотя языковая поддержка аб­ стракции данных в С++ ограничивает семантическую видимость изменений закрытых данных, она по-прежнему серьезно отражается на перекомпиляции. В большинстве сред программирования на С++ приходится учитывать по­ следствия. + Изменение представления базового класса требует перекомпиляции кода

всех производных классов. + Включение виртуальных функций в базовый класс требует аналогичной пе­

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

компилировать производные классы не придется. Но если новая функция за­ мещает другую одноименную функцию (в своем базовом классе), то придется перекомпилировать весь код, в котором вызывается функция с этим именем.

В С++ существует и другая лазейка для нарушения инкапсуляции. Предполо­ жим, класс Shape объявляет свое поле center защищенным - иначе говоря, поле center доступно для •своих• (производных классов• ), но должно быть недоступ­ ным для •чужих•: cl ass Shape puЫ i c : vi rtual voi d draw ( ) О: vi rtual voi d move ( Poi nt ) protected : Poi nt center : =

=

О:

}:

Класс EUipse может наследовать от Shape и получить доступ к полю center. В дан­ ном случае налицо сознательное нарушение инкапсуляции, но придавать ему слишком большое значение было бы глупо, особенно если класс Shape является абстрактным базовым классом: c l ass E l l i pse : puЫ i c Shape { puЬl i c :

6.5. Субтипы, наследование и перенаправление

243

vi rtua l draw( ) : }; E l l i pse anEl l i pse : Теперь программа может работать с a n E Ш pse, но язык не позволит напрямую манипулировать с center. Тем не менее, существует обходной маневр: cl ass Cheat : puЬl i c E l l i pse { puЬl i c : voi d cheat ( Poi nt р ) { center }; Cheat anEl l i pse :

=

р:

Наследование от ELLi pse позволяет Cheat нарушить абстракцию не только ELLipse, но и Shape. Это противоречит здравому смыслу и нарушает принцип •наимень­ шей неожиданности•: добавление класса Cheat вроде бы никак не связано с клас­ сом Shape, но это изменение привело к нарушению инкапсуляции Shape. Следую­ щий пример еще коварнее: Poi nt center :

/ / Центр все г о и зображения

cl ass Ci rcl e : puЫ i c E l l i pse { puЫ i c : voi d i nc i denta l CenterReference ( ) . . . center . . . }; Ci rc l e aCi rcl e ; Допустим, создатель CircLe собирался обращаться к center для определения значе­ ния поля Shape::center объекта CircLe. Если в результате изменений класса Shape поле ce nter будет удалено, переименовано или объявлено закрытым, то обра­ щения окажутся связанными с глобальной переменной center; программа будет компилироваться без ошибок, но с неправильной семантикой. Наследование нарушает абстракцию, и это происходит только потому, что автор Shape предос­ тавил защищенный статус некоторым переменным класса. Это сознательный компромисс, часто применяемый для оптимизации быстродействия, эффектив­ ного расходования памяти или многократного использования кода. Мы вернем­ ся к теме защищенных полей и многократного использования в главе 7. В общем случае, хотя программист может обеспечить лингвистическую незави­ симость классов С++ при помощи конструкций ограничения доступа, семантика языка не позволяет избежать широкого распространения последствий внесе­ ния изменений. Для небольших программ, в которых полная перекомпиляция приемлема, это не проблема. В более крупных проектах, где перекомпиляция в идеальном случае должна ограничиваться измененным кодом, применяются специальные идиомы, снижающие последствия изменений. Такие идиомы опи­ саны в главах 5 и 8.

244

Глава 6. Объектно-ориентированное проектирование

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

между базовым и производным классами, когда подмена приводит к сужению подмножества объектов базового класса, представленных производным клас­ сом. Хорошим примером служит наследование CircLe от ЕШ рsе с подменой операции вращения rotate(AngLe). Впрочем, даже этот пример не идеален, по­ тому что с точки зрения типов операция rotate(AngLe), определенная для эл­ липсов, прекрасно работает с окружностями, и подменять ее незачем. Ничего не поделаешь - этот мир вообще не идеален. + Совместимость семантики базового класса с семантикой производного клас­

са. При входе виртуальная функция базового класса должна предполагать о контексте не меньше, а при возврате - не больше, чем ее аналог в производ­ ном классе. Чисто виртуальные функции абстрактных базовых классов заве­ домо удовлетворяют этому критерию. + Программы, в которых фактически выполняется наследование с расшире­

нием, а объявление функции для удобства включено в интерфейс базового класса. Субклассы дополняются собственными специфическими операциями. При условии безопасности и семантической осмысленности реализации этих операций в классах-соседях (классах, имеющих общий базовый класс), вклю­ чая пустую реализацию, такие операции можно включать в виде виртуаль­ ных функций в общий базовый класс с пустым телом. Например, класс Shape с производными классами CLosedShape и OpenShape может содержать операцию fiLL. Базовый класс реализует ее в виде пустой операции, и эта семантика наследуется незамкнутыми фигурами, для которых заполнение не имеет смыс­ ла. Операция подменяется только в классах, производных от CLosedShape. Гово­ рят, что такие базовые классы обладают толстым интерфейсом; злоупотреб­ лять этой методикой не рекомендуется. +

Отсутствие отношений cLIKE-A• между базовым и производным классом; классы должны быть связаны отношением clS-A•. Если классы связаны от­ ношением •LIKE-A•, найдите или создайте базовый класс, который бы они могли использовать в качестве общего родителя.

По поводу сокрытия данных в С++ тоже можно сформулировать некоторые ре­ комендации.

требует, чтобы предположения, сделанные на стадии проектирования, пересматривались в свете изменений класса только при изменении символиче­ ских характеристик, экспортируемых классом. Иначе говоря, тип характеризу­ ется своей сиmатурой (открытым интерфейсом), и компилятор С++ следит за согласованностью сигнатур между определением класса и его использованием.

+ С++

Уп раж нения

245

+ Даже при неизменности интерфейса трудно сказать, не приведут ли семанти­

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

+ Старайтесь обеспечить инкапсуляцию базовых классов, избегая использова­

ния защищенных данных. С точки зрения производных классов защищенные данные слишком легко превращаются в открытые. Тем не менее, всегда отда­ вайте предпочтение защищенным данным перед дружественными и открыты­ ми данными там, где это уместно. + В общем случае изменение реализации класса требует перекомпиляции кода

классов, производных от измененного или иначе зависящих от его интерфейса.

В системах, поддерживающих поэтапную загрузку кода С++ в функционирую­ щих программах (например, во встроенных системах, работающих в непрерыв­

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

Упр ажн е н и я 1 . Какие из отношений •IS-A•, •HAS-A•, •USES-A• и т. д. обладают свойствами: +

транзитивности;

+

ассоциативности;

+

коммутативности.

2. Рассмотрим следующий пример: c l ass CopyMachi ne puЬl i c :

246

Глава 6. Объектно-ориентированное проектирование

}: c l ass Secretary : puЬl i c Empl oyee { puЬl i c : Secretary ( const cha r *n ) : Empl oyee ( n ) { } pri vate : stat i c CopyМachi ne copyмach i ne : }: Secretary Conn i e ( " Conni e " ) . Terri ( " Terri " ) : +

+

Какими отношениями связаны классы Secretary и CopyMachine? А объекты Connie, Terri и CopyMachine? Если класс Manager связан отношением •HAS-A• с классом Secretary, а класс Secretary - отношением •HAS-A• с объектом CopyMachine, то в каком отно­ шении находятся классы Manager и CopyMachine?

3. Реализуйте пример Account, приведенный

в

этой главе.

4. Реализуйте ассоциативный массив, содержащий функции at и atPut, а также функции ортодоксальной канонической формы.

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

Л ите ратур а 1 . Whitehead, А. N., and Russell, В. А. W. • Principia Mathematica• . Cambridge, Mass.: Cambridge University Press , 1 9 1 0. 2. Ungar, David, and Randall В. Smith. •Self The Power of Simplicity•. SIGPLAN Notices 22, 1 2 ( DecemЬer 1987).

3. Beck, Kent, and Ward Cunningham. •А I..aЬoratory for Teaching Object-Oriented Thinking•. SIGPLAN Notices 24, 1 0 (OctoЬer 1989). 4. Brachman, Ronald ]. •What IS-A and Isn't: An Analysis of Taxonomic Links in Semantic Networks•. IEEE Computer 16, 1 0 (October 1983). 5. Booch, Grady. •Object-Oriented Design with Applications•. Redwood City, Calif.: Benjamin/Cummings, 199 1 . 6 . Liskov, Barbara. • Data Abstraction and Нierarchy• . S I GPLAN Notices 23,5 ( Мау 1 988). 7. Sethi, R. •Programming Languages• . Reading, Mass.: Addison-Wesley, 1 989. 8. Meyer, Bertrand. • Object-Oriented Software Construction•. Englewood Cliffs, N.J.: Prentice Hall, 1988, 257. 9. Snyder, Alan. •Encapsulation and Inheritance in Object-Oriented Programming Languages•. SIGPLAN Notices 2 1 , 1 1 (NovemЬer 1 986).

Гл ава 7 М ного кратное ис пол ьз ование п ро гра м м и о бъ е кт ы Под возможностью многократного использования понимается нечто, помогаю­ щее предотвратить повторное выполнение уже выполненной работы. Мы можем заново задействовать старые идеи, архитектурные решения и основные систем­ ные интерфейсы, исходные коды программ и объектов. В настоящей главе ос­ новное внимание уделяется созданию программ, пригодных для многократного использования в контексте объектной парадигмы вообще и С++ в частности. Многократное использование программ - обширная тема, и ее полное рассмот­ рение выходит за рамки темы книги. Хотя объектно-ориентированные проектиро­ вание и программирование способствуют многократному использованию, следует подчеркнуть, что многократное использование относится к области разработки; это отдельная область знаний с собственной методологией и правилами проек­ тирования. Не следует полагать, будто многократное использование достигается автоматически в результате качественного проектирования или реализации , а то и просто по счастливой случайности. На самом деле многократное использова­ ние продумывается заранее и обеспечивается только применением соответствую­ щих механизмов проектирования, документирования , управления и распростра­ нения. Не путайте многократное использование с вторwшым использованием практикой поиска и реконструи рован ия готового кода, подходящего для выпол­ нения некоторой задачи. Также многократное использование кода следует отли­ чать от консервации программного кода, то есть сохранен ия значительной части программного кода в нескольких версиях системы. Вторичное использование и консервация тоже приносят пользу, но не в таком объеме, как хорошо написан­ ная программа, рассчитанная на многократное использование. -

В этой главе основное внимание уделяется языковым конструкциям и идиомам многократного использования С++ с дополнительным акцентом на проектиро­ вание. Тема многократного использования затрагивается и в других частях кни­ ги. Так, раздел 6.3 посвящен анализу предметной области - важн ому аспекту проектирования, рассчитанного на многократное использование. В главе 1 1 мно­ гократное использование программ рассматривается с системной точки зрения. У многократного использования есть и другие аспекты, здесь не рассматриваемые, например, документирование, организация архивов мн огократно и спользуемого

248

Глава 7 . М ногократное использование программ и объекты

кода и средства просмотра содержимого таких архивов. За более подробной ин­ формацией о социологических и управленческих аспектах разработки, рассчи­ танной на многократное использование, обращайтесь к [ 1 ] . Многократное использование кода относится к области решения. Одним из средств многократного использования является наследование - производный класс по­ лучает доступ к функциональности базового класса, и ее не приходится програм­ мировать заново. В области объектно-ориентированных технологий многократ­ ное использование показало неплохой выигрыш в производительности .

Объектно-ориентированное проектирование иногда путают с проектированием, рассчитанным на многократное использование в объектной парадигме. Казалось бы, если деление на классы способствует многократному использованию, то луч­ шей объектно-ориентированной архитектурой будет та, в которой наиболее эф­ фективно применяется наследование. Также можно подумать, что соблюдение естественных отношений между типами, обусловленных предметной областью, автоматически сведет к минимуму дублирование кода. Оба предположения не­ верны, но самое опасное - не понять различий между ними. Чтобы обнаружить возможности многократного использования, следует про­ вести поиск сходных сущностей. В программах это сходство часто отражается посредством наследования. Но необходимо помнить, что наследование является концепцией области решения, применяемой как для многократного использова­ ния, так и для отражения отношений субтипизации. Чтобы выбрать способ отра­ жения сходства между сущностями в программе, необходимо понять суть отно­ шений между объектами: связаны ли они отношениями •IS-A• или •LIKE-A•? Если речь идет об отношениях •IS-A•, следует ориентироваться на многократное использование архитектуры, а не кода. В случае отношений •LIKE-A• сущест­ вует возможность многократного использования кода. Но при этом необходимо проявить осторожность и не пытаться организовать многократное использова­ ние посредством открытого наследования. В разделе 6.5 мы уже пытались реали­ зовать многократное использование за счет наследования класса Set от класса List. До некоторой степени это получилось, но при попытке применения объекта производного класса в контексте базового класса все развалилось. •Конструктив­ ная совместимость• объектов занимает центральное место в объектно-ориен­ тированном проектировании, а ее связь с многократным использованием чисто случайна. Обычно решение о применении открытого наследования должно при­ ниматься на базе соображений, изложенных в главе 6. Рассмотрим класс Set, сходный (like а) с классом List. Как в С++ выразить много­ кратное использование кода одного класса другим классом? Мы выяснили, что класс Set •похож• на класс List; оба класса обладают свойствами контейнеров, а их сигнатуры имеют много общего. Также выяснилось, что класс Set не являет­ ся частным случаем List, или наоборот. Важно заметить, что хотя сходство между реализациями двух классов может указывать на возможность многократного ис­ пользования, это еще не означает, что один класс должен быть производным от другого. Два класса моrут быть связаны так, словно существует третья абстрак­ ция, обладающая общими свойствами двух других. Множество (Set) является

7 . 1 . Об ограниченности аналогий

249

частным случаем коллекции (CoLLection}, и список (List} тоже является частным случаем коллекции, поэтому оба класса можно объявить производными от клас­ са CoLLection, содержащего код и данные, общие для Set и List.

Существует и другой способ совместного использования кода: включение экземп­ ляра одного класса в другой класс и перенаправление вызовов функций внешнего класса внутреннему классу. Этот способ позволяет реализовать класс Set с клас­ сом List без наследования. Иерархии обьектов (в отличие от иерархий классов} способны обеспечить многократное использование абстракций, даже не связан­ ных отношениями • LIKE-A• . Подобные случаи иногда называются иерархиями

реализации.

В этой главе мы изучим связь между субтипизацией и наследованием в контек­ сте многократного использования. Сначала будут проанализированы слабости от­ ношений •субтип/производный класс• с точки зрения создания универсальных библиотек, рассчитанных на многократное использование. Затем мы разберемся, что же именно может применяться многократно: архитектура, код или п амять. Все Перечисленные направления достаточно перспективны, но одни перспектив­ нее других; кроме того, одни хорошо сочетаются с объектно-ориентированным проектированием и С++, другие - нет. Многократное использование архитектур­ ных решений является важнейшей составляющей успешного многократного ис­ пользования в объектной парадигме. Многократное использование кода и па­ мяти тоже приносит пользу, поэтому мы поговорим и об этих направлениях. В частности, будут рассмотрены шаблоны (вероятно, наиболее перспективный механизм многократного использования кода С++} и оптимизации, разработан­ ные специально для шаблонов. Также будут затронуты темы отношений между перенаправлением, наследованием и многократным использованием, инкапсуля­ ции и абстрактных типов данных. Глава завершается некоторыми практически­ ми рекомендациями и обобщениями.

7 . 1 . О б ог раниченности аналоги й Если наследование применяется для отражения отношений •IS-A• между клас­ сами, многократное использование кода приносит лишь дополнительную (хотя и немаловажную! } пользу. Возможности многократного использования в иерар­ хиях классов чаще обусловлены соображениями проектирования , нежели стремле­ нием к многократному использованию как таковому - иначе говоря, эти возмож­ ности связаны с субтипизацией предметной области, о которой рассказывалось в главе 6. Но поскольку объектно-ориентированные методы программирования тесно связаны со структурами объектно-ориентированного проектирования, возможности мноrократного использования кода также обусловлены иерархиями субтипов. Хотя класс lmaginary объявлялся производным от Com pLex по архитек­ турным соображениям, в итоге класс Imaginary задействовал большую часть кода класса CompLex, что можно рассматривать как положительный побочный эффект. В этом смысле объектно-ориентированное проектирование является ценным

250

Глава 7. Многократное использование программ и объекты

средством многократного использования, а многократное использование свойств базового класса в производном классе - ценным средством проектирования. Тем не менее, многократным использованием часто оправдывают странные примене­ ния наследования, не имеющие ничего общего с субтипизацией. Хотя такой под­ ход может принести сиюминутную выгоду, он ухудшает инкапсуляцию измене­ ний в долгосрочной перспективе. Планирование с расчетом на многократное использование в объектной парадиг­ ме должно начинаться с анализа предметной области на стадии проектирования. Классы должны проектироваться как модули многократного использования, но при этом адекватно отображаться на предметную область. Между архитектурой классов, хорошо подходящих для конкретного приложения, и архитектурой, рас­ считанной на многократное использование вообще, возникают определенные трения. Одним из основных свойств объектной парадигмы является изоморфизм областей задачи и решения. Идентифицируя сущности в процессе системного анализа, мы обычно по своему усмотрению создаем для них аналоги в прИЛDже­ нии. В результате такой архитектурной ориентации создаются классы, хорошо подходящие для конкретных программ или систем, но не для многократного ис­ пользования вообще. Слишком буквальное восприятие изоморфизма областей задачи/решения ухудшает возможности многократного использования программ. Эффективное многократное использование требует расширения абстракции на всю предметную область (см. раздел 6.3). Для примера возьмем классы, задействованные в архитектуре программы-редак­ тора. Одним из таких классов может бытъ FiLe. Сигнатура FiLe формулируется на основании того, что этот класс должен знать для работы с Window, Editor, Keyboard, Session и другими классами. Один из подходов основан на •очеловечивании• классов; проектировщики берут на себя роль классов и разыгрывают различные сценарии взаимодействия или пытаются сформулировать видение своих потреб­ ностей в контексте приложения.

Архитектурные решения, принятые в процессе формирования класса (особенно редактора}, могут ухудшить потенциал его многократного использования в не­ скольких отношениях. Первая категория проблем относится к восприятию класса FiLe проектировщиком: для разных приложений FiLe может обозначать несколько различающиеся понятия . Один проектировщик выбирает для FiLe семантику •Файл UNIX•, а в другом, более общем представлении FiLe определяется как абстракт­ ный базовый класс с производным классом U nixFiLe. Обобщенное представление существенно расширяет потенциал многократного использования класса Fi Le, который может постепенно уточняться в производных классах вроде VSAM FiLe. Другая категория проблем относится к тому, что же именно должен делать класс FiLe. Для редактора Fi Le должен поддерживать чтение и запись. Современный редактор может реализовать в классе FiLe поддержку контрольных точек, чтобы обеспечить восстановление от системных сбоев во время сеансов редактирова­ ния. Но для расширения возможностей многократного использования класс FiLe

7.2. М ногократное испол ьзован ие архитектур ы

251

должен обладать средствами проверки логической целостности, сжатия и шиф­ рования, которые могут отсутствовать в версии редактора. Кроме того, версия

FiLe для редактора может содержать дополнительные операции, лишние для более

общих приложений, например, механизм контрольных точек.

На это можно возразить, что проектировщик должен выявить все основные опе­

рации для всех основных типов файлов. Если он не может этого сделать - значит,

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

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

UNIX, чтобы использоваться ре­

дактором, комп илятором (как исходные, так и объектные файлы) и операцион­ ной системой ( 1-узлы, операции с каталогами и т. д.). Во-вторых, справильная•

архитектура не может быть слишком столстой • (то есть не может содержать

слишком много кода, не относящегося к приложению). Таким образом, полная универсальность может оказатьс,я: недостижимой.

Некоторые из перечисленных проблем решаются переводом многократного ис­

пользования на более высокий уровень абстракции и реализацией этой абстрак­ ции в контексте ее применения. Например, определение типа содержимого

FiLe (ASCII, EBCDIC или FIELDATA) можно отложить до момента выполнения фактических операций с объектом FiLe, вместо его кодирования в поведении самого класса FiLe. Одним из средств достижения этой цели является пара.мет­ рический полиморфизм, поддерживаемый в С++ в виде шаблонов. Он расширяет

возможности многократного использования и в большей степени ориентирует разработчика на проблематику проектирования, нежели на проблематику реали­

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

ракции, предназначенной для многократного использования; эти функции реа­

лизуются пользователем класса в том контексте, в котором должна применяться эта абстракция. Абстрактные базовые классы и построение заготовок для много­ кратного использования рассматриваются в главе

1 1.

7 . 2 . М ногократное и спользование архитектуры В наши дни хорошо известно, что для многократного использования на спра­

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

252

Глава 7. Многократное использование п рограмм и объекты

с тщательно проработанной семантикой, поскольку в конечном счете многократ­ но используется именно семантика, а не реализация. В [2] так описана связь ме­ жду многократным использованием и объектно-ориентированным проектирова­ нием: Приспособляемость (и потенциал многократного использования) программных компонентов зависят от объема выполненного анализа предметной области и степени параметризации модуля. Кроме того, формы параметризации, под­ держиваемые языком программирования, не всегда обеспечивают нужную сте­ пень или форму приспособляемости. Настраиваемые параметры языка Ada поддерживают многократное использование, но не могут заменить насле­ дования. Впрочем, одних классов ·тоже недостаточно. Хотя некоторые язы­ ковые средства способствуют разработке программ, пригодных для много­ кратного использования, решить эту проблему одними лишь языковыми средствами не удастся. Анализ предметной области, а не конкретного приложения, позволяет создавать классы, пригодные для широкого круга приложений. Качественный анализ пр�д­ метной области является сложной и трудоемкой задачей; может оказаться , что такой анализ может быть проведен лишь для некоторого подмножества классов. Особое внимание следует уделить обобщению классов, возглавляющих иерар­ хии наследования, прежде всего абстрактных базовых классов. Производные классы могут быть более специализированными для конкретных приложений. Классы, выбираемые в качестве обобщенных абстракций для многократного использования , должны быть тщательно документированы и помещены в специ­ альное хранилище: если программисты не смогут найти класс, то его многократ­ ное использование окажется невозможным.

Многократное использование архитектуры может означать в этом контексте од­ но из двух: либо многократное использование четко определенных пакетов, либо многократное использование cumamypЬl класса (то есть совокупности аспектов его поведения, формирующей семантическую единицу для многократного исполь­ зования). Вернемся к классу Fi le, вроде бы подходящему для многократного использования. В операционной системе UNIX для файлов поддерживаются операции создания, удаления, последовательного чтения и последовательной записи. Также возможны операции произвольного чтения, проверки логической целостности на случай сбоя системы, переименования, сжатия и т. д. Вероятно, редактору понадобится небольшое подмножество этих операций: скорее всего, ему не потребуются операции проверки целостности, сжатия, произвОJiьного дос­ тупа и переименования. Сохранение всего •балласта• только ради многократно­ го использования усложняет программу и увеличивает ее объем. Строя иерар­ хию класса File в виде набора классов с разными степенями детализации, вы предоставляете пользователю свободу выбора. Выбирая нужный уровень иерар­ хии, программа находит абстракцию, подходящую для ее целей, и не обременяет­ ся лишним грузом. ПроrнозQрование применения абстракций типа File в разных приложениях и создание соответствующих уровней функциональности является

7

.

3 . Четы ре механизма многократного испол ьзования кода

253

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

А может,

будет лучше разработать реализацию

заново (полностью или частично}, чтобы она лучше подходила для потребностей приложения, создав •заглушки• для ненужных частей. Например, графический пакет может быть по-разному реализован для разных платформ в зависимости

от набора аппаратных графических примитивов. Графические приложения смогут без изменений работать на разных платформах, при этом одна и та же архитек­

тура будет многократно использоваться с разными реализациями. Впрочем, этот пример относится скорее не к многократному использованию, а к адаптации одuо­

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

7 . 3 . Ч етыре ме ханизма многократного испол ьзовани я кода Иногда нам удается разработать хорошую архитектуру, а возможности для много­

кратного использования, открывшиеся благодаря грамотному применению насле­

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

#incLude, то есть

как механизм автоматическо­

го включения избранных частей одного класса в другой. Главное преимущество этого механизма состоит в том, что он работает на уровне объектного кода и из­ бавляет от необходимости копировать исходные тексты.

Проблема многократного использования программного кода относится к облас­

ти решения. Как организовать многократное использование кода без нарушения иерархии классов, хорошо интегрированных по отношению к предметной области? В случае перенаправления строится новый объект для взаимодействия с сущест­ вующим объектом, причем система рассматривает совокупность двух объектов как один объект. Перенаправление особенно часто применяется к классам, связанных тесными отношениями

•LIKE-A•, но не дотягивающих до отношений •IS-A•. Set и List (см. раздел 6.5). Перенаправление

Снова вспомните пример с классами

254

Глава 7. Многократное использование программ и объекты

естественно сочетается с абстракцией данных; класс Set может пользоваться ус­ лугами встроенного в него объекта list. Другой пример - класс Мепu, использую­ щий код Window. Единицей многократного использования архитектуры является интерфейс клас­ са. Но какой должна быть единица многокраnюго использования кода? Многие полагают, что ее границы тоже должны совпадать с границами классов, а классы как сестественныеэ- абстракции также должны быть многократно используемы­ ми блоками в модели наследования. О наследовании речь пойдет позже, а пока следует сказать, что перенаправление обеспечивает более точную детализацию многократного использования на уровне отдельных функций класса. Помимо перенаправления механизмом многократного использования кода явля­ ется делегирование, которое не поддерживается напрямую в С++, но встречается в Actors, self и ряде других языков (см. также с. 1 66). При делегировании вызов функции для одного объекта автоматически перенаправляется другому объекту; аналогичным образом перенаправляются обращения к переменным класса. Два взаимодействующих экземпляра воспринимаются извне как один. Если один объект делегирует вызов функции другому, то функция второго объекта вы­ полняется в контексте пространства имен первого объекта. Делегирование ха­ рактерно для языков, в которых классы не поддерживаются: для таких языков оно играет ту же роль, что наследование для классических языков. В С++ для выполнения одного объекта в контексте другого применяется наследование (производный класс способен тесно взаимодействовать с членами базового клас­ са), но в С++ не существует аналога �классового• наследования на уровне эк­ земпляров. Если бы такой аналог существовал, язык имел бы прямую поддержку делегирования. Делегирование обладает большей гибкостью на стадии выполнения, чем на­ следование. В С++ для имитации делегирования применяется перенаправление: объект одного класса передает большую часть своей работы объекту другого класса. Такой подход обеспечивает гибкость делегирования, хотя при нем те­ ряется общность контекста, присутствующая в языках с сполноценным• деле­ гированием. Чтобы класс Menu мог многократно использовать класс Window, дос­ таточно включить в него объект Window. Многие функции Menu (move, refresh) напрямую реализуются в контексте функций Wi ndow. В простых случаях это решение хорошо работает в С++ и считается традиционным механизмом под­ держки многократного использования без нарушения абстракции. Но если фак­ тический класс потомка Window неизвестен во время компиляции (то есть если мы собираемся задействовать класс, производный от Window, размер и конкрет­ ные свойства которого могут изменяться), начинаются проблемы. Обычно они решаются сохранением указателя на нужный объект (например, включением указателя на Wi ndow в объект Menu) вместо экземпляра. Третий механизм многократного использования кода - закрытое наследование. При закрытом наследовании производный класс применяет код базового класса в своей реализации без включения интерфейса базового класса в свой интерфейс.

7

.

3 . Четы ре механизма многократного испол ьзования кода

255

С семантической точки зрения не существует принципиальных различий между закрытым наследованием и включением экземпляра в виде переменной другого класса. З акрытое наследование может потребоваться как условная запись, свиде­ тельствующая о том, что проектировщик стремится именно к многократному ис­ пользованию (в отличие от включения объекта только как характеристики внут­ реннего состояния). Пример: c l ass Res i stor { . . . } : c l ass I пductor { . . . } : c l ass Capaci tor { . . . } : 1 1 Фильтр н ижни х ч астот испол ьзует ( многократно) готовые 1 1 абстра кции Res i stor . I пductor и Capaci tor 11 в своем в нутреннем устройст ве : э тот фа кт 11 отражается посредством закрытого наследования . cl ass LowPassFi l ter : Res i stor . I пductor . Capaci tor puЬl i c : LowPassFi l ter ( Resi stor r . I пductor i . Capaci tor с ) : Res i stor ( r ) . I пductor ( l ) . Capaci tor ( c ) { } pri vate : 1 1 Закрытые данные не отражают стремлени я к 1 1 многократному испол ь зованию . а лишь хара ктери зуют 1 1 состояние объектов класса . F requeпcy wO : Qua l i ty q : douЫ e Hi ghЗdbPoi пt . LowЗdbPoi пt : }: Впрочем, такое решение обладает жесткими ограничениями. Следующий фраг­ мент недопустим, потому что класс не может быть дважды указан в качестве базового: c l ass Arm { . . . } : c l ass Leg { . . . } : 1 1 Недопустимо : c l ass Robot : Arm . Arm . Leg . Leg { / / Наследование для мно гократног о pri vate : 1 1 использова н и я Di recti oп di rect i oп : 1 1 Переменные состо я н и я Vel oci ty vel oci ty : puЫ i c : Robot O : }: Вероятно, последний механизм многократного использования параметриза ­ (параметрический полиморфизм) - обладает наибольшим потенциалом из всех перечисленных. В следующем разделе описывается поддержка параметри­ зации в С ++, а также рассматриваются программные идиомы для сокращения избыточного кода, генерируемого для параметризованных типов С++. -

ция

256

Глава 7. М ногократное использование программ и объе кты

7 . 4 . П а р аметризованные ти п ы , или шаблон ы

Шаблон (параметризованный тип) С++ определяет семейство типов и функций. Шаблоны являются альтернативой дл я плоских иерархи й насл едования, соз­ даваемых с целью многократного испол ьзования кода. Допустим , мы хотим применить алгоритмы реализации Stack к стекам с элементами л юбых типов (int, Window и т. д.). Если решать эту задачу путем насл едования, весь общий код сле­ дует поместить в базовый класс и создать отдельный производный кл асс для каждого создаваемого типа (intStack, WindowStack и т. д.). Но так как код Stack работает с элементами стека. было бы естественно выразить эти алгоритмы в кон­ тексте типов этих элементов. А сл едовател ьно, большая часть кода не будет общей для всех типов Stack, а должна генерироваться для каждого класса отдель­ но. Класс Stack даже не сможет быть абстрактным базовым классом, потому что объявления функций р о р и top зависят от типа стека. Другое возможное решение - реализация класса Stack для нетипизированных элементов void * : c l ass Stack { pri vate : struct Cel l { Cel l *next : voi d *rep : Cel l ( Cel l *с . voi d *r ) next ( c ) . rep ( r ) { } } *rep : puЬl i c : voi d *рор ( ) voi d *ret rep->rep : Cel l *с = rep : rep = rep ->next : del ete с : return ret : } voi d *top ( ) { return rep->rep : } voi d push ( vo i d *v ) { rep = new Cel l ( rep . v ) : } i nt empty ( ) { return rep ! = О : } Stack ( ) { rep = О : } }: =

Но подобная реализация Stack создает синтаксические неудобства - поскольку в ней хранятся тол ько у казател и voi d * , код приложения содержит множество лишних символов & и преобразований типов . Кроме того, теряется большая часть преимуществ проверки типов. Еще более серьезные проблемы возникают из-за ослабления типизации. Допустим , в класс list добавляется функция sort, реализованная с применением указателя voi d * . Ф ункции list: :sort не удастся сколько - нибудь удобно сравнить два эл емента, указатели на которые хранятся в списке; она не располагает информацией о том, на какие типы они ссылаются.

7.4. Параметризованные типы , или шаблоны

257

Высокоуровневые объектно-ориентированные языки вроде Smalltalk и CLOS ре­ шают эту проблему по -своему : большинство решений, связанных с типами, при­ нимается на стадии выполнения. Их полиморфизм в меньшей степени зависит от иерархии наследования, чем полиморфизм С++. К достоинствам такого под­ хода следует отнести ослабление связи между классам и, а к недостаткам - воз­ можность ошибок отсутствия метода на стадии выполнения. В С++ описанные проблемы решаются без потерь и в плане безопасности типов, и в плане эффективности выполнения. Все, что требуется сделать - параметри­ зовать класс Stack с другим типом. Реализация стека, приведенная в листинге 7. 1 , представляет собой шаблон, н а основе которого генерируются специализирован­ ные разновидности стека. Листинг 7. 1 . Шаблон Stack

templ ate c l ass Stack : templ ate c l ass Cel l fri end c l ass Stack : pri vate : Cel l *next : Т *rep : Cel l C T *r . Cel l *с ) : rep ( r ) . next ( c ) { } }: templ ate c l ass Stack puЫ i c : Т *рорС ) : Т *top ( ) { return rep ->rep : } voi d pus h C T *v ) { rep = new Cel l C v . rep ) : } i nt empty ( ) { return rep == О : } Stack ( ) { rep = О : } pri vate : Cel l *rep : }: templ ate Т *Stack : : pop ( ) Т *ret = rep ->rep : Cel l *с = rep : rep = rep ->next : del ete с : return ret : Шаблон определяется для некоторого обобщенного типа Т, который, как предпо­ лагается, обладает всеми свойствами , обязательными для элементов Stack . Когда программа требует создать объект Stack с элементами конкретного типа, компи­ лятор генерирует его и подставляет характеристики фактического типа на место

258

Глава 7. Многократное использование программ и объе кты

параметрического типа Т в теле шаблона. Объект Stack с элементами i nt создается на базе шаблона Stack с передачей параметра int: Stack a n i ntegerStack : Теперь в программе можно использовать конструкции вида: Stack wi ndowМanagerStack : i nt mai n ( ) { Stack a n i ntStack : a n i ntStack . push ( 5 ) : a n l ntStack . pop ( ) : i nt i Wi ndow w( 0 . 50 . 4 . 4 . 4 ) : wi ndowМanagerStack . push (w) : =

При применении шаблонов нужно придерживаться определенных правил. + Параметризованный класс не м ожет вкладываться в д ругой параметризован­ ный класс. + Статические переменные парам етр изованного класса пр инадлежат конкретной специ ализации шаблона. Параметризация пр имени ма не только к классам, но и к функциям. Самы м распространенным примером является функция sort. способная отсортировать вектор произвольного типа (листинг 7.2). Листинг 7 . 2 . Параметризованная версия функции sort

templ ate voi d sort ( S el ements [ J . const i nt nel ements ) О . s z = ne l ements - 1 : i nt fl i p do { О . fl i p О : j < sz : j++ ) fo r ( i nt j i f ( el ements [ j ] < el ements [ j +l J ) S t = el ements [ j +l J : el ements [ j + l ] = el ements [ j ] : t: el ements [ j ] fl i p++ : =

=

=

=

} } whi l e ( fl i p ) :

i nt ma i n ( ) { Comp l ex cvec [ l2] : for ( i nt i = О : i < 12 : i ++ ) ci n » cvec [ i ] : / / ca l l s sort ( Compl ex[ J . const i nt ) sort ( cvec . 1 2 ) : О : i < 12 : i ++ ) cout пext : del ete temp : return retva l : } i nt el ement ( const Т& ) const : / / Проверка принадлежности 11 Дру г ие операции i nt s i ze ( ) const : 1 1 Количество эленентов L i st ( ) : head ( O ) : { } 1 1 Конструктор по унолчанию pri vate : L i st ltem *head : 1 1 Начало списка }: =

=

=

=

В отдельном файле Set.h объявляется шаблон Set, использующий класс List в своей реализации. Чтобы избежать специализации класса List для каждой параметри­ зации Set, мы добавляем дополнител ьный уровень сокрытия информации в двух местах. Во-первых , каждая конкретная разновидность Set является производной от класса SetBase. И нтерфейс SetBase содержит все операции, которые, как ожида­ ет объект Set, поддерживаются его эле.ментами: c l ass SetBase { fri end E rsatzl i stEl ement : pri vate : vi rtual i nt compa rel t ( const voi d* . const voi d* ) const О: v i rtua l i nt compa reeq ( const voi d* . const voi d* ) const О: }: =

=

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

Глава 7. Многократное использование программ и объе кты

262

voi d * . В конечном счете эти операции определяются в •настоящем• классе, соз­ данном специализацией шаблона Set, в контексте типа элементов Set.

Вторым в ход идет другой фокус - инкапсуляция элементов списка в другом классе ErsatzlistELement, который обращается к ним только через указатели void*: cl ass E rsatzli stEl ement { 1 1 Класс не паранетри зуется : это означает . 1 1 что одна специал и з а ц и я шаблона L i st обслуживает 11 все специали з а ц и и Set . puЫ i c : voi d *rep : i nt operator< ( const E rsatzli stEl ement& 1 ) const { return theSet - >compa rel t ( th i s . &l ) ;

}

i nt operator== ( const E rsatzl i stEl ement& 1 ) const { return theSet - >compa reeq ( th i s . &l ) :

}

E rsatzLi stEl ement < const Ersatzli stEl ement ( const : theSet ( s ) . rep ( v ) pri vate : const SetBase *theSet :

}:

voi d* v = О > : rep ( v ) { } SetBase *s . voi d *v=O ) { } / / Требуется тол ь ко для вызова / / в и ртуальных функций сра внен и я 1 1 двух объектов и т . д .

Экземпляры этого класса-оболочки поддерживаются классом List, необходимым для реализации Set. Это означает, что все специализации List, создаваемые для Set, будут относиться к одному типу List, а следовательно, один тип списка будет совместно использоваться всеми специализациями Set. В завершение рассмотрим сам шаблон Set (листинг 7.5). Он параметризуется по типу содержащихся в нем объектов, rю не создает новых типов, кроме List . Шаблон содержит закрытые функции для сравнения двух объектов класса Т и операции, которые должны быть определены для Т по на­ стоянию Set. Эти •заготовки• функций перенаправляют свои запросы Т только после преобразования параметров void* к типу Т. Такое преобразование безопас­ но: в границах параметризованного типа можно гарантировать, что void* всегда указывает на объект типа Т. И конечно, весь код - заготовки и все прочее генерируется автоматически компилятором на базе шаблона благодаря поддерж­ ке параметризованных типов. Листинг 7 . 5 . Гибкая и эффективная версия Set

templ ate cl ass Set : pri vate SetBase 11 За крытое наследование для многокра т н о г о испол ь з о в а н и я кода puЬl i c : voi d add ( T t2 ) { i f ( ! exi sts ( t 2 ) ) { Ersatzl i stEl ement t ( th i s . new T ( t 2 ) ) : rep . put ( t ) :

7.4. Параметризованные типы , или шаблоны

263

} Т get O { Ersatzl i stEl ement l =rep . get C ) : return * ( ( T* ) ( l . rep ) ) : } voi d remove C const Т& ) : i nt exi sts C const Т& е ) { E rsatzl i stEl ement t ( th i s . С Т* ) &е ) : return rep . el ement ( t ) : } Set Uni on C const Set& ) const : Set I ntersect i on C const Set& ) const : i nt s i ze ( ) const { return rep . s i ze ( ) : } voi d sort ( ) { rep . sort ( ) ; } Set O : pri vate : Li st rep : i nt compa rel t C const voi d* v l . const voi d* v2 ) const { const Т* t l = ( const Т* ) v l : const Т* t2 = C const Т* ) v2 : return *t l { l . enter ( x ) : } { return 1 . рор( ) : i nt l eaveq ( ) l ong empty ( ) { return l . empty ( ) : pri vate : Ci rcu l a rl i st 1 : }: Перед нами разновидность перенаправления, подчеркивающая, что, во-первых, Queue содержит ( •HAS-A•) список, во-вторых, отношения •IS- A• не действуют, в -третьих, объект Circularlist не используется совместно с другими объектами (как при обращении к нему через указатель). Ин капсуляция CircularList при этом не нарушается. Второе решение - делегирование - возможно, но на С++ оно выглядит неесте­ ственно из-за отсутствия поддержки на языковом уровне (см. с. 254 ). Третье решение - закрытое наследование - может использоваться так: c l ass Queue : pri vate Ci rcu l a rl i st puЫ i c : Queue O { } voi d enterq ( i nt х ) { enter ( x ) : } { return рор ( ) : i nt l eaveq ( ) Ci rcu l arli st : : empty : }: С точки зрения эффективности закрытое наследование и иерархия реализации более или менее эквивале нтны. У каждого подхода есть свои преимущества. Если производный класс может напрямую применять функции базового класса, при наследовании программисту приходится писать меньший объем кода, чем при в ключении. П роизводный класс может подменить виртуальные функции, вызываемые функ­ циями закрытого базового класса, поэтому для двух классов, взаимодействующих

266

Глава 7. Многократное использование программ и объекты

с целью многократного использования кода, закрытое наследование подходит лучше включения. Базовый класс определяет общую структуру и характеризует обязанности производного класса в виде чисто виртуальных функций. Базовый класс зависит от реализации этих чисто виртуальных функций в производном классе, поскольку производный класс может вызывать функции базового класса. Хорошим примером служит библиотека Taxform в главе 1 1 . Однако многократное использование кода посредством наследования обладает также недостатками. Клиентс кий класс может обращаться к защищенным чле­ нам закрытого базового класса, нарушая его инкапсуляцию; при включении ин­ капсуляция сохраняется. Кроме того, наследование может внушить программи­ сту ложное предположение об отношениях субтипизации. Также возможно гибридное решени е, объединяющее закрытое наследование с пе­ ренаправлением: c l a s s Queue : pri vate Ci rcul a rl i st puЬl i c : Queue ( ) { } voi d enterq ( i nt х ) { enter ( x ) : } { return рор ( ) : } i nt l eaveq ( ) { return Ci rcul arli st : : empty ( ) : i nt empty ( ) }: Некоторым это решение кажется более •стилистически чистым• , чем другие варианты, однако оно требует дополнительных затрат на вызов функции empty (если только тело Queue::empty не расширяется посредством подстановки). Ис­ пользуйте эту конструкцию вместо решения со специ фи каторами доступа, если имеются перегруженные функции, и программист желает предоставить разный уровень доступа к одноименным функциям базового класса. Большинство компиляторов С++ сообщают о типичных ошибках, связанных с при­ менением закрытого наследования для многократного и спользования (вместо субтипизации). Это может быть важно с точки зрения архитектуры. Вернемся к предыдущему примеру с CircuLarlist, до полненному в иртуальными функциями: c l a s s C i rcul a r l i st { puЬl i c : v i rtua 1 i nt empty ( ) : C i rcul arli st ( ) : v i rtua l voi d push ( i nt ) : / / Занесение с начала v i rtua l i nt рор ( ) : / / Занесение с конца v i rtua l voi d enter ( i nt ) : pri vate : cel l *rea r : }: c l ass Queue : pri vate Ci rcul a rli st puЬl i c : Queue ( ) { }

7.6. Многократное использование памяти

267

vo1 d enterq ( i nt х ) { enter ( x ) : } i nt l eaveq ( ) { return рор О : с; rcu l a rl i st : : empty O : }:

i nt ma i n O { Ci rcul a rl i st *a l i st return О :

=

new Queue :

/ / Ошибка конnил я ц и и

Если бы это было возможно, то объекты Oueue могли бы находиться в любом контексте, в котором ожидается CircuLarlist. Тем не менее, компилятор выдает ошибку при попытке создания экземпляра он знает, что операции CircuLarlist не моrут перепоручаться Queue. -

Эта проблема возникает только при использовании виртуальных функций в кон­ тексте указателей или ссылок на объекты классов.

7 . 6 . М н огократ н ое использова н ие памяти Иногда требуется заимствовать поведение существующего класса другими класса­ ми или объектами, но без затрат на хранение лишних копий этого класса. Допус­ тим, несколько объектов Wi ndow желают совместно использовать один объект Keyboard для различных функций, связанных с вводом. Вместо того чтобы встраи­ вать отдельный объект Keyboard в каждый объект Wi n dow, все объекты Wi ndow моrут перенаправлять свои операции одному общему классу Keyboard. В результа­ те каждый объект Window наделяется поведением Keyboard, но все объекты совме­ стно используют общую память. Для отражения факта совместного использования можно применить идиому •манипулятор/тело• (см. главу 5): тело применяется совместно, а манипуляторы выступают в роли •посыльных• или •суррогатов• этого объекта. В некоторых случаях оптимальный способ совместного использо­ вания памяти между всеми объектами класса основан на статических перемен­ ных класса. Стремление к совместному использованию памяти также может быть обуслов­ лено более глубокими архитектурными соображениями, например, размеще­ нием объекта в блоке памяти, общем для нескольких процессоров. В С++ это делается напрямую, при условии, что класс о бьекта ие содержит указателей . Переменные-указатели класса необходимо либо удалить, либо организовать для них специальную обработку. Указатели иногда также встречаются в за­ маскированном виде: они моrут быть задействованы в реализациях ссылочных переменных класса и виртуальных функций. Содержимое указателя должно правильно интерпретироваться в каждом контексте, в котором он используется; обычно это условие выполняется только в том случае, если указатель ссылается на другой объект в общей памяти. Общие указатели создают меньше проблем, если каждый процессор (или процесс) отображает сегмент общей памяти на

268

Глава 7. Многократное использование про грамм и объекты

один и тот же логический адрес в своем адресном пространстве, но во многих операционных системах считается нежелател ьным, чтобы работа программы за­ висела от таких отображений . Исключив из общих данных указатели и выдел ив всю семантику указателей и виртуальных функций в класс-конверт, можно организовать нормальную рабо­ ту с общей памятью в приложениях С ++. Классы, спроектированные в расчете на относител ьные смещения вместо указателей и не содержащие виртуальных функций, тоже могут использоваться в сегментах общей памяти.

7 . 7 . М ногократное испол ьзова н ие интер ф е йс а Гибко спроектированная система может содержать нескол ько разных реализа­ ций одной архитектуры. Каждая реализация работает с тем, что считается одним и тем же типом, хотя внутренние алгоритмы изменяются в зависимости от кон­ текста. Рассмотрим ряд примеров. + Удаленный вызов процедур: фун кции класса-•заместителю� перенаправляют

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

Реализация для локального выполнения работы (то есть класс локального сервера). Например, если объект файл ового сервера знает, что диск под­ ключен к ло кальному контейнеру, он выполняет свою работу напрямую.



Реализаци я для перенаправлеиия запросов удаленному компьютеру. Если локальный объект файлового сервера знает , что ди ск подключен к друго­ му компьютеру , он перенаправляет запрос по сети на другой компьютер. Там этот запрос обрабатывается классом удаленного сервера.

+

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

+ Рыночные зависимости. Может потребоваться, чтобы класс имел разные

реализаци и для разных контекстов. Например, в программе может быть один кл асс ConsoLe с разны ми реал изациями для англ ийского и для араб­ ского языков.

7.7. Многократное использован ие интерфейса

269

+

Аппаратные зависимости. Класс Stack может быть написан в расчете на непо­ средственную аппаратную поддержку стека на одном компьютере, и на про­ граммную эмуляцию для других платформ. Если класс находится в библиотеке, то обе реализации могут иметь идентичный интерфейс класса и использоваться эквивалентно. То же относится и к другим аппаратно-зависимым примити­ вам (например, операциям с вещественными числами).

+

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

+

Оптимизация. Допустим, существует некий конкретный тип данных, не под­ держивающий подсчет ссылок; может потребоваться ею аналог с подсчетом ссылок для компонентов системы, критичных по затратам памяти и скорости выполнения. Пример {реализованный на базе простой абстракции данных, но который также может быть реализован с применением наследования) приве­ ден в разделе 3.5 {см. с. 8 1 ) .

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

Такие производные классы называются варианта.ми. Имена производных клас­ сов несущественны, а ссылки на них локализованы. Обращения к объектам этих классов производятся через указатели и ссылки с использованием виртуальных функций. Клиентский код пишется в контексте абстрактного базовою класса. Другое возможное решение - обработка альтернатив внутри родительскою класса {вместо создания на ею базе производных классов). Такое решение может рас­ сматриваться как форма инкапсулированного многократного использования; в этом контексте базовый класс называется прототипом {к сожалению, этим термином также обозначается другая, хотя и похожая идиома, описанная в гла­ ве 8). Получая запрос на выполнение некоторой операции, класс-прототип мо­ жет перенаправить запрос одНому из своих объектов, который обрабатывает его с помощью виртуальных функций. Преимущество прототипов перед наследо­ ванием состоит в инкапсуляции имен реализаций. Например, упоминавшийся ранее класс Stack может специализироваться одним из двух способов: в одном содержать объект, реализующий стек с применением особых аппаратных средств процессора, а в другом обрабатывать структуры данных обычным образом. Прототипы могут бытъ реализованы по тем же принципам, что и делегирован­ ные полиморфные классы {см. раздел 5.5). Иначе говоря, внутренний объект не обязан физически находиться внутри байтового представления внешнего объекта; его логическая принадлежность может моделироваться хранением во внешнем объекте указателя на вариант с нужной семантикой.

270

Глава 7. Многократное использование программ и объекты

7 . 8 . М н огократн ое испол ьзова н ие , н аследова н ие и пере н аправлен и е П роектировщики и программисты должны найти компромисс ме жду логической полнотой и эффе ктивностью многократного использования с одной стороны, и инкапсуляци ей архитектуры н езависимых кл ассов с другой. П роектировщики , работающие в бол ее гибко й среде , чем код С ++, м огут различными способами распред ел е ния функциональности и данных в ыд ерживат ь баланс м ежду воз ­ м о жностями многократного использования и чистотой архитектуры. П осле того как архитектура воплощена в про граммном коде, языковые ограничения создают противоречи е м ежду самодостаточностью класса и возм ожностью других клас­ сов использовать его код. Связь м ежду двумя классами может определить способ реал изации многократ­ ного использования: закрытое н аследовани е или внедрение экз емпляра одного класса в другой . Наследование приводит к частичному нарушению инкапсуля ­ ции базового класса, поскольку производный класс получ ает доступ к защищен­ ным чл енам. П опытки организовать совместное использовани е символич еских и м е н б ез закрыто го насл едования (наприм е р , с п рим енением друз е й ) оставля­ ет альтернативу стопроц ентной видимости одного класса для другого кл асса . Совместное использо вание символических имен без примен ения дружественных отношен и й и наследования приводит к разрастанию открытого интерфейса клас­ са. В [ 4 ] написано : Самый с е рьезный недостаток этого р ешения [им еется в виду п рим ен е ни е п одставляемых функций для эффективного обращен ия к пер еменным базо ­ в о го класса] состоит в том , что оно требует взаим одейств ия с проектиров­ щи ком класса-предка (а также проектировщиками в сех п ромежуточных пред­ к ов) , п оскольку доступ к унаследован ной перем енной - э кз емпляру возможен лиш ь при нал ичии соответствующих оп ераци й . Е сли вам (как проектиров­ щику класса) потребуется доступ к унасл едованн ой перем енной -экземпляру, а соответствующие операции не определены , следует обсудить с проектиров­ щико м клас са-предка возможность включ ен и я п оддержки этих оп ераци й. (Среда программирования может дать возможность обойти это огранич ени е, например , позволить вам определить эти операции временно . Но если вы на­ мерены серьезно использовать чужой код, взаи модействи е с проектировщи­ ком абсолютно н еобходимо.) Здесь важн о то , что речь идет не о простом 4заи мствовании• кода одного класса другим клас со м, а о расп р о ст р ане щш класс о м и нфо рмации о в озм о жности м но гократного использования среди заинтересованных сторон. В заимодействи е между 4 Поставщиком• и 4 Пользователем• кода, потенци ал ьно рассчитанного на м ногократное использовани е , отв ечает интер есам вс ех сторон для сохр ан е ния хорошей архитектуры с одноврем енным снижени ем объема кода и дублирования усилий .

7.9. Архитектурные альтернативы для испол ьзования исходных текстов

271

7 . 9 . Архитектурные альтернативы дл я многократного испол ьзования исходн ых текстов Существует множество способов управления многократным использованием исходных текстов или адаптации существующего кода для данного приложения. Наследование является лишь одним из средств. В зависимости от ситуации может применяться как наследование, так и более традиционные методы. Далее охарактеризованы плюсы и минусы разных решений. Простейшим среди всех решений является условны й выбор на стадии вьтол ­ Под этим хитроумным термином скрывается простой набор команд if и других условных конструкций. Проверка логического флага и выбор одной из двух ветвей программы хорошо подходит для небольших ветвей, часто при­ меняемых, но незначительных с архитектурной точки зрения (то есть не обла­ дающих существенным интерфейсом на уровне системной архитектуры). Орга­ низационной единицей в этом случае является команда С . Например, решение об активизации отладочного кода может приниматься в зависимости от состоя­ ния глобального флага.

нения.

хорошо знакомо программистам С Другое решение условная компиляция под видом директив #ifdef и других директив этого семейства. Условная ком­ пиляция хорошо подходит для изолированных небольших фрагментов кода, и только в тех ситуациях, когда решение о присутствии кода в программе мо­ жет быть принято на стадии компиляции. Это решение приносит максимальную пользу тогда, когда альтернативы связаны со значительными различиями в се­ манти ке или в синтаксисе, например, при выборе длины слова в регистрах вво­ да-вывода в зависимости от типа целевого процессора. Организационной едини­ цей в данном случае можно считать функцию, хотя несколько функций могут быть упакованы в один исходный файл для удобства сопровождения и на­ стройки. Условная компиляция также может потребоваться для активизации отладочного кода - с некоторой потерей гибкости, но с меньшим количеством лишних команд в окончательной версии программы (по сравнению с условным выбором). -

-

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

272

Глава 7. Многократное испол ьзование программ и объе кты

о них, вам придется вносить измене ния вручную. Э то приведет к н апрасн ой трате времени, особенно если автор изменил те функции, к которым вы не прикасались. Конечно, модификация создает риск внесения новых ошибок. Дру­ гой н едостаток - излишнее разрастание программного кода. А если кто-то за­ хочет 4Многократно использовать• вашу программу в той же системе, ситуация ТОЛЬКО усугубится. Как отмечалось выше, если вам понадобится новый класс, по функциональности сходный с существующим классом, но отличающийся от него реализацией, зада­ чу лучше решать путем наследования. Наследование позволяет создать новый класс на базе существующего класса, изменить то, что требуется изменить, и со ­ вместно использовать оставшуюся часть класса. Впрочем, у наследования как механизма многократного использования тоже есть недостатки. Допустим, имеется группа программ, построенная на базе объектов, а для адаптации поведения базовых классов к конкретным условиям (разнообразному составу оборудования и операционным системам) применено наследование. Продукт должен работать на новой платформе с минимальными изменениями в коде.

Для примера возьмем сеансовый интерфейс, приведенный в листинге 7 .6. Класс Seslnterface представляет функциональность клавиатуры и окна в некотором ин­ терактивном приложении. Он содержит функцию Sesinterface::saveTty(SGПY*), задающую характеристики устройства для данного интерфейса. Функция вызы­ вает примитивы той платформы, на которой она работает. Вызываемые прими­ тивы также изменяются в зависимости от операционной системы и платформы; в данном случае расхождения проявляются на уровне исходного текста. Так, для USG-систем вызывается функция ioctL с параметром ТСG ЕТА; для систем Sun пре­ дусмотрена особая обработка, а все прочие системы используют функцию gtty. Листинr 7 . 6 . Проrрамма, предназначенная для разных платформ

(добавляемый код выделен курсивом)

i nt Ses l nterface : : saveTty ( SGТТY *tty ) #i fndef USG i f sun # return i octl ( 2 . TIOCGETP . tty ) : # e 7 se return gtty ( 2 . tty ) : # endi f #el se return i octl ( 2 . TCGETA . tty ) : #endi f

}

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

7.9. Архитектурные ал ьтернативы для испол ьзования исходн ых тексто в

273

return gtty ( 2 . tty ) :

return i oct 1 ( 2 . TIOCGETP . tty ) ;

return i oct1 ( 2 . ТСGЕТА . tty ) ;

Рис. 7. 1 . Наследование как альтернатива условной проверке

При таком подходе модификация на уровне исходных текстов отсутствует. Мо­ жет показаться, что его преимуществом является возможность выбора ( соответ­ ствующих файлов с объектным кодом) на стадии компоновки вместо стадии компиляции. Тем не менее, здесь кроется ловушка. Допустим, где -то в системе скрывается код вида: voi d User : : code ( ) { Ses l nterface *s i

=

new Ses l nterface ( keyboa rd . wi ndow ) :

Невнимательный проектировщик может не осознать, что при добавлении произ­ водного класса изменения должны быть внесены во весь код, содержащий ссыл­ ки на команды создания экземпляров базового класса. Без замены базового клас­ са производным при создании экземпляров изменения будут проигнорированы. Так, мы можем взять приведенный выше код из User и заменить его следующим: voi d user : : code ( ) #i fndef USG # i f sun s i = new SunSes l nterface ( keyboa rd . wi ndow ) ; # el se s i = new Ses l nterface ( keyboa rd . wi ndow ) : # endi f #el se s i = new USGSes l nterface ( keyboa rd . wi ndow ) ; #endi f

Однако так мы снова возвращаемся к модификации исходных текстов, чего мы пытались избежать с помощью производных классов. Проблема решается созда­ нием трех новых классов, производных от User: c l ass USGUser : puЬl i c User puЬl i c : voi d code ( ) { s i = new USGSes l nterface ( keyboard . wi ndow ) ;

274

Глава 7. М ногократное использование про грам м и объекты

}: cl ass NonUSGUser : puЫ i c USGUser { puЬl i c : voi d code ( ) { si new Ses l nterface ( keyboa rd . wi ndow ) : =

}:

c l ass SunUser : puЬl i c NonUSGUser { puЬl i c : voi d code { ) { si new Ses l nterface { keyboa rd . wi ndow ) : =

}: Но тогда нам придется просмотреть весь код, в котором создаются объекты User, и сделать то же самое с классами, содержащими этот код. Таким образом, изме­ нения лавинообразно распространяются по системе. Как правило, в подобных случаях проще всего продолжать модификацию на уровне исходных текстов. Альтернативное решение - включение в класс допол­ нительного уровня абстракции (особенно для упрощения дальнейшей эволюции программы в области инициализации объектов класса). Эта идиома рассматри­ валась в главе 4.

7 . 10. Общие ре коме н да ции от н о с итель н о м н огокр атного ис пол ьзова н ия кода Следующие рекомендации, основанные на материале этой главы, помогут вам принять решение о применении наследования как механизма многократного ис­ пользования кода С++. + Анализ предметной области играет важную роль для многократного исполь­ зования классов. Проанализируйте предметную область в шшсках •критиче­ ских точек• параметризации, чтобы обеспечить максимально широкую при­ менимость класса с минимальными усилиями по ее достижению. Классы не должны быть рассчитаны •на все случаи жизни• - от этого они становятся слишком громоздкими, что затрудняет их понимание и использование (в том числе многократное). С другой стороны, классы не должны быть рассчитаны на одно конкретное приложение. + Вероятно, многократное использование архитектуры, определяемое интерфей­ сом класса, обладает максимальным потенциалом для повышения производи­ тельности работы в больших проектах. Многократное использование интер-

Упражнения

275

фейса может означать, что вам придется удалять или переписывать части реализации для каждого конкретного случая, поэтому возможностей для мно­

гократного использования кода может быть не так уж много. Тем не менее, ничто не мешает многократно использовать код, для которого это возможно,

и обеспечить доработку общих частей.

+ Настоящая мощь объектной парадигмы в области многократного использоЬа­ ния кроется в абстрактных базовых классах. Определяя иерархию классов,

представляющих совокупность более или менее разнородных типов в предмет­

ной области, вы создаете высокоуровневую абстракцию, которая может при­ меняться для обобщения в широком спектре приложений. Возможности мно­

гократного использования абстрактных классов обусловлены потенциальной широтой области их применения по сравнению с одиночной реализацией абстрактных типов данных. Впрочем, это не означает, что абстрактные базо­ вые классы способны- в одиночку решить проблему многократного использо­ вания; тщательный анализ предметной области и проработка потребностей приложения по-прежнему играют важную роль.

+ Если наследование требуется только для многократного использования, при­ меняйте закрытое наследование. Открытое наследование должно отражать отношения субтипизации, но не возможности многократного использования.

+ При проектировании с расчетом на многократное использование приходится искать компромисс между созданием множества мелких классов или относи­

тельно небольшого числа крупных классов. Слабая детализация, подсказан­ ная конкретным приложением, упрощает применение абстракции в этом при­ ложении, но может усложнить его в других приложениях той же предметной области. Детализированные классы меньше зависят от контекста, что расши­

ряет возможности их многократного использования. В традициях иерархий классов

Smalltalk

максимальный потенциал многократного использования

достигается при наибольшей детализации классов. Тем не менее, сильная детализация полезна лишь при наличии мощных средств просмотра кода в сре­

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

+ Применение аналогов делегирования дл я имитации поддержки типов н а ста­ дии выполнения позволяет обойти многие ограничения компилятора, мешаю­

щие многократному использованию. Идиомы, заложенные в основу таких моделей, описаны в главах

6

и 8.

Уп ражнения 1.

Обсудите следующую цитату: • Многократное использование - для тех, кто не хочет думать• .

2. Реализуйте пример

Stack и з

раздела

3.5

(см. с . 8 2 ) в виде варианта.

276

Глава 7. Многократное использование про грамм и объекты

3 . Составьте список многократно используемых классов, которые должны вхо­ дить в стандартную библиотеку любой системы программирования на С++. Какой процент этих классов должны составлять шаблоны? 4. Проанализируйте зависимости между классами в предыдущем примере (к чис­ лу таких зависимостей относятся параметризация одного класса по другому классу, объявление одного класса производным от другого или любой другой способ использования кода одного класса другим классом ) . Насколько глубо­ ка структура зависимости? Хорошо это (для многократного использования) или плохо (по эффективности работы прикладного программиста)? 5. Какие обобщенные функции, не связанные с библиотекой классов из двух предыдущих упражнений , должны входить в каждую систему программиро­ вания на С ++ ? Сколько из них должно оформляться в виде шаблонов? Сколь­ ко из этих функций потребует других функций библиотеки? 6. Н апишите шаблон класса CountedClassObject, создающий абстракцию класса с подсчетом ссылок для любого класса Т, не поддерживающего подсчет ссы­ лок (возьмите за образец класс Cou ntedStack из раздела 3 . 5).

Л итература 1 . Tracz, Will. •Software Reuse: Emerging Technology•. IEEE Computer Society Press, 1988. 2. Tracz, Will. •Software Reuse Myths•. Software Engineering Notes (АСМ SIG­ SOFГ) 1 3, 1 (January 1988) 17-20. 3 . Sethi, Ravi. •Programming Languages•, Вoston, Mass. : Addison-Wesley, 1989, 238.

4. Snyder, Allan. • Encapsulation and Inheritance in Object-Oriented Programming Languages•, SIGPLAN Notices 2 1 , 1 1 (NovemЬer 1986).

Гл ава 8 П р о тот и п ы Система контроля типов С++ в основном зависит от конструкций времени ком­ пиляции. Статическая проверка типов способствует раннему выявлению ошибок интерфейса и повышает эффективность кода по сравнению с проверкой типов на стадии выполнения (или полном отказе от нее). Тем не менее, именно поддержка типов на стадии выполнении в значительной мере определяет мощь и гибкость объектно-ориентированных языков. Программа, использующая виртуальные функции, частично откладывает проверку типов до стадии выполнения, что обеспечивает взаимозаменяемость объектов разных классов. За эту гибкость при­ ходится расплачиваться некоторой неопределенностью в работе программы до ее непосредственного выполнения. Если вызвать функцию ring для класса, входяще­ го в иерархию TeLephone, вы не знаете, что именно произойдет, известно лишь то, что во время выполнения будет выбрана некоторая разумная операция. Другие объектно-ориентированные языки (в частности, Smalltalk и семейство объектно-ориентированных языков, интегрированных с Lisp) идут на дополни­ тельные жертвы в проверке типов на стадии компиляции в интересах гибкости во время выполнения. Обычно это приводит к снижению эффективности кода и возможным сюрпризам на стадии выполнения (если у объекта требуют выпол­ нить операцию, которую он не поддерживает). Преимуществами такоrо подхода является более универсальная гибкость, обеспечиваемая полиморфизмом. Про­ граммы С++ с сильной объектной ориентацией обычно нуждаются в расширен­ ной проверке типов на стадии выполнения, не обеспечиваемой одними вирту­ альными функциями. Допустим, вы хотите загрузить объект Number из файла на диске. Объект может быть записан на диск в формате класса CompLex, Biglnteger, Imagi nary или любоrо друrого класса, производною от Number, однако программист, работающий с чис­ лами на уровне класса Number, этою не знает (а возможно, и знать не хочет). Тре­ буется, чтобы класс NumЬer •воссоздал • экземпляр самого себя по образу на диске. В процессе построения объект должен быть наделен характеристиками фактиче­ скою типа (CompLex, Imaginary и т. д.). В С++ существуют специальные идиомы, обеспечивающие подобную гибкость. К их числу относится идиома прототипов, которой посвящена эта rлава. Идиома прототипов также делает возможной эволюцию характеристик типов в процессе выполнения. Например, если объект N u m ber меняет значение с комплексной

278

Глава

8.

П рототип ы

величины (5 Зi) на 2, напрашивается предположение, что его тип должен из­ мениться с Com pLex на Integer. Сохраняя комплексную природу, объект будет использовать сложные комплексные алгоритмы для получения результатов, которые могут быть получены более простым способом. -

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

Один из критериев нечеткой классификации объектно-ориентированных языков программирования основан на том, являются ли в них классы базовыми и од1ю ­ значнЬ1Ми конструкциями. С++ принадлежит к числу таких языков - классы за­ нимают основополагающее место в языковой модели объектов и их поведения; невозможно говорить об объектах, не упоминая при этом классы. Языки, у кото­ рых классы занимают центральное место в объектной модели, называются клас ­

сическими. В С++ классы и объекты не тождественны. Классы ведут себя как типы (они реализуют абстрактные типы данных), а объекты представляют собой экземп­ ляры, созданные на базе этих (или встроеННЪIХ ) типов посредством объявлений или оператора new. Классы фиксируются на стадии компиляции, а объекты вообще не существуют до стадии выполнения. Новые объекты создаются на базе класса или встроенного типа; невозможно создать новый объект на основе только другого объекта. Программа, написанная на С++, содержит набор клас­ сов, обычно орrанизованных в иерархию (в соответствии с отношениями •базо­ вый/производный класс•), и отдельный набор объектов, которые также могут рассматриваться как элементы другой иерархии - иерархии объектов (орга­ низованной в соответствии с тем, как одни объекты создают другие). Под одно­ значностью классов понимается то. что класс не является •чем-то еще•: функ­ цией, объектом или командой. Языки, в которых различаются концепции классов и объектов, называются языками с двойной иерархией. С++ также входит в эту категорию.

Некоторые языки (такие, как self [ 1 ] ) относятся к категории языков с единой иерархией. Вместо использования классов как •заготовок• для построения объ­ ектов в таких языках некоторые объекты назначаются прототипами; другие объекты со сходными свойствами копируются с прототипов по принципу споле в поле• (этот процесс называется кл01�ированием). Объект-клон может прохо­ дить дальнейшую модификацию и настройку посредством изменения свойств, обычно ассоциируемых с классами (например, изменение поведения функций) или с экземплярами (изменение значений переменных экземпляра). Объек­ ты-прототипы занимают в языках с единой иерархией место, отведенное классам и типам в языках с двойной иерархией. Гибкость механизма прототипов приносит

Прототипы

279

пользу при развитии программы, при итеративной разработке, при обновлении систем, работающих в непрерывном режиме, а также в некоторых стилях програм­ мирования, для которых важна поддержка типов на стадии выполнения (напри­ мер, некоторых приложений из области искусственного интеллекта). В языках с единой иерархией поддерживаются механизмы создания исходных объектов-прототипов се нуля• - то есть для создания исходного пустого объек­ та (или находящегося в состоянии по умолчанию) с последующим добавлением в него полей и функций. В С++ такая возможность отсутствует, или, по крайней мере, она не поддерживается во время выполнения. Следовательно, прототи­ пы должны инициализироваться на базе классов, зафиксированных на стадии компиляции. Один класс может использоваться для создания нескольких объ­ ектов-прототипов, каждый из которых инициализируется отдельным набором данных для модификации его характеристик. Например, для разбора восьмерич­ ных или десятичных данных может создаваться свой экземпляр класса. Любой экземпляр может использоваться как прототип, а любой прототип - как эк­ земпляр, хотя по общепринятым правилам в программе обычно создается один специализированный прототип, играющий роль «типа• для набора объектов со сходной структурой. Для читателей, знакомых с принципами функционирования баз данных, можно провести аналогию между конструкциями языка программирования и концеп­ циями базы данных. Классы рассматриваются как компоненты схемы базы дан­ ных, то есть определения свойств, отношений, структуры записей и т. д. Схема указывает, каким образом новые записи создаются и заносятся в базу данных, определяя их общий формат и структуру. Схема является аналогом класса, а за­ писи - аналогом объектов. Наличие схемы позволяет легко создать новую за­ пись и включить ее в базу данных; точно так же при необходимости создается новый объект на базе класса. Изменение содержимого записи или объекта не отражается в схеме; так обеспечивается высокая степень гибкости при работе с информацией.

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

ей схемы.

Такая гибкость является важной составляющей объектной парадигмы, как было показано на примере класса Number; она имеет далеко идущие последствия для разработки больших систем. Допустим, поведение класса должно вырабаты­ ваться постепенно в течение жизненного цикла программы. Данная особен­ ность особенно ценна для систем, которые нельзя остановить, перекомпилиро­ вать и перезапустить при изменении функциональности со временем. К этой

280

Глава 8 . Прототипы

категории относятся большие встроенные системы: телекоммуникационные систе ­ мы, финансовые базы данных и т. д. Брокерская фирма может использовать в своей программе объектно-ориентированную архитектуру, в которой каждый тип счета представлен классом. Конечно, было бы неприемлемо останавливать все финансо ­ вые операции ради изменения поведения существующего типа счета (например, функции, ограничивающей количество одновременно приобретаемых акций). Гибкость, о которой идет речь, сводится к выбору между принятием решений на стадии компиляции и на стадии выполнения. В С++ семантика класса интер­ претируется на стадии компиляции: любые изменения характеристик класса вызывают необходимость перекомпиляции. С другой стороны, объекты облада­ ют гибкостью стадии выполнения. Именно 4 Компиляционная• природа классов (а также принципиальные различия между классами и объектами) затрудняет эволюцию программ. В одном из возможных решений проблемы классы наде­ ляются характеристиками объе ктов; д ругими словами, в роли класса может оказаться другой объект, предназначенный исключительно для представления общих аспектов 4Потомков• класса. Прототипы удовлетворяют эту потребность. В настоящей главе описана идиома моделирования прототипов в реализации С++. Она основана на использовании единой иерархии, объекты которой играют роль, традиционно отводимую классам; в свою очередь, классы становятся кон­ цептуальными аналогами метаклассов (как в языке Smalltalk).

8 . 1 . П р и мер с п рототипами класса Employee Предположим, мы строим систему, моделирующую работу некой организации. В эту иерархию входят классы EmpLoyee, Manager, VicePresident и т. д. П оскольку определен и я ролей могут изменяться со временем, разумно выбрать подход, основанный на применении экземпляров. В крупных проектах бывает полезно определять все классы как производные от единого общего базового класса. В частности, универсальный базовый класс мо­ жет использоваться для отладки и анализа быстродействия. Кроме того, модель с единым общим базовым классом упрощает реализацию модели прототипов: все классы в системе объявляются производными от общего базового класса Cl.ass. В листинге 8. 1 приведено объявление класса EmpLoyee, наследующего от Cl.ass. Объявление выглядит вполне обычно, если не считать дополнительных функций make, списки параметров которых совпадают со списками параметров конструкторов. Также обратите внимание на тот факт, что конструкторы выведе­ ны из открытого интерфейса класса (в нашем примере конструкторы находятся в секции protected для вызова в классах, производных от Em pLoyee). Функции make заменяют конструкторы при использовании класса в идиоме прототипа. Еще одно отличие от 4 Обычного• класса С++ - дополнительный конструктор, в параметре которого передается объект Exempl.ar; его смысл поясняется далее.

8. 1 . Пример с прототипами класса Employee

281

Класс Employee для идиомы прототипа c l ass Empl oyee : puЬl i c C l ass puЬl i c : Empl oyee ( Exempl a r ) : Empl oyee *make ( ) : Empl oyee *make< const char *name . Empl oyeel d i d ) : l ong pri ntPaycheck ( ) : voi d l ogTi meWorked ( Нours ) : protected : Emp 1 оуее ( ) : Empl oyee < const cha r *name . Empl oyeel d i d ) : pri vate : Dol l a rs sa l a ry : Days vacati onAl l otted . vacati onUsed : Stri ng name : Empl oyee l d i d : }:

Листинг 8 . 1 .

extern Empl oyee *empl oyee : Итак, у нас появился класс; но как уже отмечалось, главная цель этого класса слу�ить •типом для типов• , а его экземпляр использовать как фабрику для соз ­ дания новых экземпляров. Следовательно , мы должны со;щать сам экземпляр. Прототипы могут статически определяться как объекты, существующие в течение жизненного цикла программы и обладающие файловой статической видимостью , а доступ к ним м ожет осуществляться через гло бальный у казатель на класс, находящийся на более высоком уровне иерархии наследования : stat i c Empl oyee empl oyeeExempl a r < Exempl a r ( ) ) : extern Empl oyee *empl oyee = &empl oyeeExempl a r : Кром е того , можно создать ан онимный прототип в куче и обращаться к нему через глобальн ый указател ь: extern Empl oyee *empl oyee

=

new Empl oyee ( Exempl a r ( ) ) :

Эти определения могут находиться как в отдельном исходном файле, так и в одном файле с подробностями реализации класса прототипа. Объявления переменных объектов-прототипов могут публиковаться в глобальном загол овочном файле. При инициализации объекта-прототипа объект Exemplar передается в качестве па­ раметра. Тем сам ым выбирается конструктор , предназначенный для построения одного объекта на базе шаблона класса. Класс Exem plar не содержит информации и не имеет собственной семантики; он нужен только для устранения неоднознач­ ности при выборе конструктора (перегруженного) для создания прототипа. Таким образом, объект Exemplar может делать все, что угодно ; от него требуется лишь корректное поведение и наличие конструктора:

c l ass Exempl a r puЬl i c : Exemp l a r ( ) }:

/* Пусто */ }

282

Глава 8 . Прототипы

Функuия make объекта-прототипа вызывается для создания нового объекта. Таким образом, она берет на себя роль конструктора. В С++ конструкторы и деструкто ­ ры отличаются от •обь�чных• функций тем, что в действительности являются операциями самого класса, а не обьектов этого класса. Фун кция make похожа на остальные функции класса, не считая, что в представленной схеме она выполня­ ет запрос на создание Jювого объекта на базе прототипа. В обычном случае make возвращает указатель на копию прототипа. Как уже отмечалось, все новые объек­ ты создаются •клонированием• экземпляра. Функция make может быть перегруже­ на, а ее аргументы и спользоваться для параметризации содержимого полученного объекта ( • клонирование с мутациями•). Каждый вариант make может создавать новые э кземпляры простым вызовом конструктора с аналогичной с и гнатурой, об ычн о входящих в закрыты й или защищенный интерфей с класса). В нашем примере новый объект EmpLoyee создается вызовом Cl ass *smi th = empl oyee - >ma l ( ) const 1 1 Все операции 1 1 перенаправляются rep return rep : } { rep = l ette r - >make ( ) : Envel ope ( ) Envel ope ( Letter& ) : -Envel ope ( ) { i f < rep && rep - >deref( ) del ete rep : } Envel ope < Envel ope& х) { ( rep = x . rep ) ->ref( ) : } Envel ope& operator= ( Envel ope& х ) i f ( rep ! = х . rep ) { i f ( rep && rep- >deref( ) ref( ) : return *th i s : } Th i ng *type ( ) { return envel ope : pri vate : stat i c voi d *operator new ( s i ze t ) Sys_Erro r ( " heap Envel ope " ) ; } stat i c voi d operator del ete ( voi d * ) { } Letter *rep : }:

9.2. Символическая каноническая форма

31 3

Класс конверта ведет себя как абстракция с ослабленной типизацией, а его эк­ земпляры имитируют нетипизованные ярлыки для объектов, поддерживаемые во многих символических языках. Например, функции конверта не выражают подробную семантику содержащихся в нем объектов. С другой стороны, объект конверта перенимает поведение объекта класса письма, используя механизм operator-> (см. раздел 3.5) по аналогии с тем, как переменные в символических языках перенимают поведение тех объектов, с которыми они связываются. Тот факт. что интерфейс конверта передает некоторую информацию о своих классах писем, отражается в типе возвращаемого значения функции operator-> (а именно Letter* ). Это одно из немногих мест символической идиомы, в которой указатель становится видимым для прикладного программиста, и только как временное значение, которое обычно не сохраняется пользователем. -

Класс конверта содержит конструкторы, но в основном механизме инициализа­ ции объекта используются виртуальные функции make класса письма; как будет показано далее, это расширяет инкрементные возможности класса. Конструк­ торы конвертов являются «заготовками� для инициализации и преобразований, выполняемых компилятором. Два конструктора (конструктор по умолчанию и копирующий конструктор) входят в ортодоксальную каноническую форму. Наконец, также необходим конструктор для конструирования нового объекта конверта по экземплярам классов писем. Этот конструктор преобразует резуль­ таты вычислений, внутренних для классов писем, в объекты, которые могут ис­ пользоваться обычными клиентами составных объектов «конверт/письмо�. Копирующий конструктор, оператор присваивания и деструктор работают со счетчиком ссылок объекта (см. раздел 3.5). Деструктор проверяет, равен ли счет­ чик нулю, и освобождает память объекта при освобождении ссылки на него по­ следним конвертом. Но самая важная роль отводится оператору ->, автоматически перенаправ­ ляющему вызовы функций конверта объекту письма. Того же результата можно было бы добиться таким дублированием сигнатуры письма в конверте, при котором каждая функция конверта просто передает управление своему аналогу в письме. Однако подобное решение требует удвоения усилий и включения но­ вых функций в класс письма. Класс Letter определяет интерфейс ко всем классам, обслуживаемым интерфей­ сом E nvelope (листинги 9.3 и 9.4). Класс Letter сам по себе является базовым классом (обычно абстрактным) для группы классов, обслуживаемых интерфей­ сом Envelope. Один объект Envelope на протяжении своего жизненного цикла может предоставлять интерфейс к нескольким разным объектам писем. Напри­ мер, объект N u m ber изначально может предоставлять интерфейс к объекту Complex, но присваивание или иная операция может привести к замене исходного письма N u m ber другим объектом, возможно, относящимся к другому классу. Листинг 9 . 3 . Класс Letter

cl ass Letter : puЫ i c Th i ng puЬl i c : / * Здесь определ яются все пол ь зовател ьские опера ц и и . * Бла годаря испол ь зованию опера тора -> не обя з а тел ьно

продолжение&

314

Глава 9. Эмуляция символических языков н а С++

Листинг 9.3 (продолжение}

* воспрои з вод и т ь зту си г н а туру в Envel ope . Тем не менее . * в объ я влен и и поля rep класса Envel ope должен быт ь * указан соот ветствующий т и п . Опера торы прис в а и в а н и я * определ яются не з дес ь . а в Envel ope . * * Параметр возвращаеный_ тип должен быть л ибо * п ри м ит и в ным т и пом . л ибо Envel ope . л ибо Envel ope& . * либо конкретным типом данных . */ v i rtual voi d send ( Stri ng name . Stri ng address ) : v i rtua l douЫ e postage ( J : 1 1 v i rtual воз вращаеный_тип поль зова тель ская_функция 11 . . . 1 1 Конструктор v i rtual Envel ope mak e ( J : 11 Дру г ой конструктор v i rtua l Envel ope ma k e ( douЫ e J : v i rtual Envel ope ma ke ( i nt days . douЫ е wei ght ) : v i rtua l Th i ng *cutover ( Th i ng* ) : 1 1 Функция обновлен и я Lette r ( ) { } -Lette r ( ) { } Thi ng *type ( ) : protected : fri end c l a s s Envel ope : doub l e ounces : stat i c voi d *operator new{ s i ze_t l ) { return : : operator new( l ) : stat i c voi d operator del ete ( voi d *р ) : : operator del ete ( p ) : Stri ng name . addres s : 11 . . . pri vate : 11 . . .

}:

Листинг 9 . 4 . Подставляемые функции класса Letter

/* * * * * * * */

Здесь раз мещаются общие подста вляемые фун кции . Это сделано для выполнени я соглашен и й о выводе подставл яемых функций и з объ я влени я класса . Кроме то г о . ра з мещение подставл яемых функций после Envel ope и Lette r помогает изба в ит ьс я от некоторых ци клических за висимостей .

i n l i ne douЫ е Letter : : postage ( )

9.2. Символическая каноническая форма

i f ( ounces < 2 ) return 29 . 0 : el se return 29 . 0 + ( ( ounces i n l i ne Thi ng * Letter : : type ( ) { extern Thi ng *l etter : return l etter :

-

31 5

1 ) * 23 . 0 ) :

1 1 Протот ип

Объекты иерархии Letter •вкладываются• в объекты Envetope, а внешнему поль­ зователю виден только объект Envetope. Пользователь никогда не обращается к сигнатуре Letter напрямую. Тем не менее, Letter добавляет свои функции в ин­ терфейс Envetope посредством функции Envetope: : operator->. Класс Letter не обя­ зан быть конкретным типом данных, поскольку все 4Низкоуровневые аспекты• типа обрабатываются на уровне Envetope. Класс Letter служит базовым для взаимосвязанных классов приложения, находя­ щихся под управлением Envetope. Класс E nvetope содержит поле rep с указателем на экземпляр Letter. Вся •настоящая• работа выполняется в объектах классов, производных от Letter. Все функции приложения задаются в интерфейс«:: класса письма (обычно в виде чисто виртуальных функций). Некоторые функции, общие для всех классов, производных от класса письма, выделяются в класс письма, а их тела размеща­ ются в определении базового класса письма. Объявление остальных функций чисто виртуальными гарантирует, что они будут определены в производных классах. Тем не менее, как показано далее, наличие чисто виртуальных функций недопустимо в расширенной форме этой идиомы, реализующей свойства объек­ тов прототипов. Пользовательские функции должны возвращать объекты встроенных или кон­ кретных типов данных (иначе говоря - соответствующие ортодоксальной кано­ нической форме), типа Envetope или ссылки на Envetope. Все типы указателей, присутствующие в сигнатуре Envetope, должны быть константными; возврат не­ защищенного указателя на динамически выделенный блок памяти создает угро­ зу для схемы управления памяти. Конечно, возвращаемые значения могут быть объявлены с типом void. Функция make конструирует экземпляр класса, производного от Letter, и возвра­ щает Letter* (см. rлаву 8). Может существовать несколько перегруженных функций make, каждая из которых выполняет всю необходимую работу по инициализации нового объекта; никакие ее части не должны оставаться на долю конструкто­ ра. Например, функция Letter: :make может инициализировать объекты классов OverNight и FirstCLass следующим образом: Envel ope Letter : : make( i nt days . douЫ e wei ght ) Letter *retva l : i f ( days < 2 && wei ght ounces = wei ght : return Envel ope ( *retva l ) :

Если конструктор не содержит сколько-нибудь значительной логики инициали­ зации, его не придется изменять или перекомпилировать; данное обстоятельство полезно для инкрементной загрузки, поскольку виртуальные функции (такие, как make) могут подгружаться во время выполнения, а конструкторы - нет. В общем случае производные от Letter классы не обязаны следовать ортодоксаль­ ной канонической форме. Они должны содержать конструктор по умолчанию и деструктор, но ни копирующий конструктор, ни оператор присваивания не яв­ ляются обязательными.

Далее следуют классы, производные от Letter. Класс EnveLope может 4СОдержать• любые из них, и при правильном проектировании любой объект EnveLope может присваиваться любому другому, причем такое присваивание прозрачно для про­ граммиста. В листинге 9.5 приведены примеры простых классов, производных от Letter. Каждый из этих классов также содержит конструктор по умолчанию. Листинг 9. 5. Специфические классы приложения , производные от Letter

cl ass Fi rstCl a s s : puЬl i c Letter { puЬl i c : Fi rstCl ass ( ) : -Fi rstCl ass ( ) : Envel ope ma1 конверта. Прототип

9. 2. Символическая каноническая форма

31 7

письма может быть специальным экземпляром обобщенного базового класса письма (в данном случае Letter), если последний не является виртуальным базо­ вым классом. В противном случае можно определить специальный производный класс письма с простыми заглушками для чисто виртуальных функций, чтобы сконструировать синглетный прототип письма. Классы символической канонической формы используются по тому же принципу, что подсчитываемые указатели и объекты прототипов - то есть с применением оператора -> вместо оператора . (точка). Следующее простое приложение пред­ ставляет собой пример использования классов EnveLope и Letter: stat i c Envel ope envel opeExempl a r : / / Никогда не испол ьзуется напрямую Envel ope *envel ope - &envel opeExempl a r : i nt ma i n ( ) { Envel ope overni ghter ( *envel ope ) - >make ( l . 3 . 0 ) : overn i ghter->send ( " Addi son - Wes l ey " . " Readi ng . МА" > : Envel ope acrosstown = ( *envel ope ) - >make ( l . 0 ) : overni ghter = acrosstown : acrosstown - >send ( " Angwant i bo " . " Boston Соппюn " ) : return О : =

Так выглядят основные концепции символической канонической формы. Для более полного усвоения материала ниже приводится краткая сводка правил ис­ пользования этой идиомы. +

Все обращения к классу E nveLope должны производиться через оператор ->, а не через оператор . (точка). Это позволяет организовать автоматическое пе­ ренаправление операций классу Letter оператором ->.

+

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

+

Функция make выполняет всю работу конструктора; сам по себе конструктор почти ничего не делает. Это позволяет заменять код инициализации класса на стадии выполнения, что возможно только для виртуальных функций. Кон­ структоры присутствуют в классах, производных от Thing (они необходимы для работы механизма виртуальных функций С++), но они не содержат пользовательского кода. Программа должна содержать синглетный прототип для каждого класса; объ­ ект прототипа должен иметь возможность идентифицировать себя таковым (например, по факту его создания специальным конструктором). Обращения к прототипу никогда не производятся напрямую, а только через специальную переменную-указатель. Причины объясняются далее.

+

+

Функции cutover(Thing*) субклассов Thing обеспечивают динамическую загруз­ ку на стадии выполнения. Эти функции получают указатель на объект клас­ са, которому они принадлежат; они должны на месте преобразовать старый

31 8

Глава 9. Э муляция символических языков на С++

класс в соответствии с изменениями формата, структуры и типа нового клас­ са. Если функция cutover не может преобразовать объект на месте, возможно, ей удастся произвести замену приемами, связанными со спецификой среды (то есть имитировать односторонний механизм BECOMES в Smalltalk). До­ полнительные объяснения приводятся в следующем разделе. +

Помните, что объекты классов конвертов никогда не должны создаваться оператором new. Этот оператор объявляется закрытым, а любые· попытки ди­ намического создания объекта конверта приводят к ошибкам компиляции. Конверты должны объявляться только как автоматические переменные, члены других классов или глобальные переменные. В результате отказа от указателей программисту не придется помнить о необходимости освобождения памяти объектов; это позволяет использовать более гибкие полиморфные типы (та­ кие как N u m ber) в качестве конкретных типов данных вроде i nt.

Итак, в чем же эта идиома помогает программисту? Управление памятью в ос­ новном автоматизируется на уровне класса конверта, так как последний следит за появлением объектов, на которые не существует ни одной ссылки. А посколь­ ку доступ к функция�� 11 данным производится через дополнительный уровень адресации, клиент еще на один шаг изолируется от изменений, вносимых в класс. Следовательно, меньший объем программного кода зависит от подробностей реализации класса и нуждается в переко�шиляции: при изменении: класса. При наличии необходимых средств компоновки и загрузки эта методика даже позво­ ляет оперативно модифицировать классы в работающей программе. Программа останавливается лишь на вре�tя, необходимое для настройки нового класса. Описанный подход поднимает полиморфизм языка С++ на следующий уровень и создает иллюзию, будто характеристики типов могут изменяться во время вы­ полнения программы. Далее приводятся дополнительные пояснения с примерами.

9 . 3 . П ри мер обоб ще н н ого класса коллекции Предположим, вы хотите написать программу, в которой должно бьггь организо­ вано взаимозаменяемое использование трех контейнеров: массива с целочислен­ ными индексами, В-дерева и хеш-таблицы. Для этого мы определяем класс кон­ верта CoLLection, содержащий указатель на внутренний объект (см. выше). Чтобы класс CoLLection стал более гибюш, он будет определен на базе шаблона - это позво­ лит использовать его для хранения объектов произвольного класса. Так, следующее объявление создает коллекцию объектов Book, индексируемых по имени автора: Col l ecti on l i brary :

Объекты писем в этом примере создаются на базе классов, производных от CoLLectionRep. Эти классы характеризуют варианты (см. раздел 7.7) CoLLectionRep. В данном примере три варианта CoLLecti o n Rep (Array , Btree и HashTaЬLe) составля­ ют набор альтернатив, используемых классом CoLLection в объекте контейнера.

9. 3. Пример обобщенного класса коллекции

31 9

Основные функции класса CoUectionRep и его производных классов объявляются виртуальными, поэтому вызов функции CoUection через указатель на CoLLectionRep будет перенаправлен соответствующей функции Array, Btree или HashTaЫe. В ин­ терфейсе класса CoUectionRep должны быть объявлены операции для объединения всех функций этих классов. Из всех операций иерархии классов писем CoLLection знает лишь те, которые объявлены в самом классе CoLLection Rep. Поскольку не все производные классы писем подменяют эти функции, возможно, потребу­ ется организовать обработку исключений для защиты от некорректных вызовов функций. Например, обращение к элементу массива может производиться только по целочисленному индексу, а хеш-таблицы могут использовать либо целые числа для индексной выборки, либо строки для ассоциативной выборки. Если объект CoLLection в настоящее время содержит объект Array, то при вызове функции operator[] (S) для символьной строки произойдет исключение. Такова цена, которую приходится платить за гибкость символического стиля програм­ мирования. В листинге 9.6 приведен •скелет� объявления CoLLecti o n Rep - базового класса для классов писем в нашем примере. Листин г 9 . 6 . Базовый класс CollectionRep

#i ncl ude " k . h " #i ncl ude " col l ecti on . h " 1 1 Коллекция для хранен и я э кземпл я ров класса Т . 1 1 и ндексируемых п о з начен и я м класса S templ ate c l a s s Col l ecti onRep : puЬl i c Thi ng puЬl i c : v i rtua l Col l ecti on make( ) : v i rtua l Thi ng* cutove r ( Th i ng * ) : v i rtua l Т& operator[ ] ( 1 nt ) : v i rtua l Т& operator [ ] ( S ) : vi rtu a l voi d put ( const Т& ) : Col l ecti onRep ( ) { } -Col l ect i onRep ( ) { } Thi ng *type( ) : protected : fri end c l a s s Col l ect i on : stat i c voi d *operator new( s i ze- t l J { return : : operator new( l ) : stat i c voi d operator del ete ( voi d *р ) : : operator del ete ( p ) : pri vate : Col l ecti onRep *exempl a rPoi nter : }:

320

Глава 9 . Эмуляция символических я зыков на С++

Классы иерархии CoLLectionRep реализуют функцию type несколько необычным образом. Поскольку класс CoLLecti o n Rep является параметризованным, каждая специализация в новый класс требует собственного прототипа, поэтому функ­ ция type не может просто вернуть глобальный указатель. Вместо этого функция make прототипа сохраняет адрес прототипа в каждом создаваемом объекте (поле exemplarPointer) , а функция type просто возвращает это значение. Пример: c l ass Cl a ssDeri vedFromCol l ect i onRep puЫ i c Col l ecti onRep { Col l ecti on ma ke ( ) { Col l ect i on newObJect : newObject . exempl a rPoi nter = thi s : return newObj ect :

}:

Th i ng *Col l ecti onRep : : type ( ) { return exempl a rPoi nter : }

Класс CoLLecti o n Rep сам по себе является полезной абстракцией и может ис­ пользоваться в контексте другой идиомы. Логическая инкапсуляция иерархии CoLLectionRep внутри CoLLection имеет два преимущества. Во-первых, это позволяет классу CoUection изменять свое представление во время выполнения. Динамическая типизация позволяет присвоить коллекцию одного типа коллекции другого типа, поэтому все типы коллекций становятся практически взаимозаменяемыми. Боль­ шие коллекции при достижении предельного размера (или по другим критериям) могут оперативно менять свой тип для повышения быстродействия; например, переключаться с линейного массива на ассоциативную хеш-таблицу. Во-вторых, класс Тор, базовый для типа CoLLection, используя атрибуты класса Thing, реализует прозрачное управление памятью для идиомы прототипа. Эту конuепцию иллюстри­ рует листинг 9.6. Механизм подсчета ссылок наследуется от базового класса Thing.

Класс CoLLection представлен в листинге 9.7. Он выполняет основные операции по управлению памятью (в основном при присваивании), а остальные операции перенаправляются классу письма. Класс CoLLection может создавать письма из на­ бора классов по своему выбору, используя любые критерии. Для CoLLection мож­ но определить дополнительные конструкторы, чтобы пользователь мог сам вы­ брать базовую структуру данных для коллекции. Листинг 9. 7 . Класс Collection

#i nc l ude " k . h " templ ate c l ass Col l ecti onRep : templ ate c l a s s Col l ecti on : рuЫ 1 с То р puЬl i c : Col l ecti onRep *operator - > ( ) const { return rep : } Col l ect i on ( ) :

9.3. Пример обобщенного класса коллекции

32 1

Col l ect i on ( Col l ecti onRep& ) : -Col l ect i on ( ) : Col l ect i on ( Col l ecti on& ) : Col l ect i on& operator= ( Col l ecti on& ) : Т& operator [ J ( i nt i ) { return ( *rep ) [ i ] : Т& operator [ J ( S s ) { return ( *rep ) [ s J : } pri vate : stat i c voi d *operator new ( s i ze_t ) { retu rn О : stat i c voi d operator del ete ( voi d *р ) { : : operator del ete ( p ) : } Col l ect i onRep *rep : }:

В листинге 9.8 содержатся определения субклассов CoLLectionRep. Каждый объект CoLLection содержит письмо типа Array, Btree или HashTable. В каждом производ­ ном классе подменяются те функции, которые имеют смысл для его представ­ ления, а за остальными сохраняется стандартная реализация из CoLLection Rep. Поскольку производные классы избирательно подменяют небольшое подмно­ жество функций базового класса, эти функции не могут объявляться чисто виртуальными в CoLLection Rep. Листинг 9.8. Примеры классов реализации Collection

templ ate c l a s s Array : puЫ i c Col l ect i onRep { puЫ i c : Array ( ) : Array (Array& ) : -Array ( ) : c l a s s Col l ecti on make( ) : c l a s s Col l ecti on make ( i nt s i ze ) : Т& operator [ J ( i nt i ) : voi d put ( const Т& ) : pri vate : Т *vec : i nt s i ze : }: templ ate st ruct Has hTaЫ eEl ement { HashTa Ы eEl ement *next : Т *el ement : }:

templ ate c l ass HashTaЫ e : puЬl i c Col l ecti onRep { puЬl i c : HashTaЬl e O :

продолжение#

322

Глава 9. Э м ул яци я символ ич еских я зы ков н а С++

Листинг 9.8

(продолжение)

HashTaЬl e ( Ha s hTaЫ e& ) : -Ha shTa Ь l e ( ) : c l ass Col l ect i on mak e C ) : c l ass Co l l ect i on ma keC i nt ) : Т& operator [ ] C i nt i ) : Т& operator [ J ( S ) : voi d put ( const Т& ) : pri vate : i nt nbuckets : v i rtual i nt hash ( i nt l ) : HashTaЫ eEl ement *buckets : }:

Обычно управление памятью для таких объектов организуется на базе подсчета ссылок. Как и прежде, классы из 3.5 дают хороший пример применения идиомы «Конверт/письмо• для подсчета ссылок. В этом разделе показано, как оператор присваивания, конструктор и деструктор изменяют счетчик ссылок, хранящийся в переменной внутреннего объекта StringRep, и удаляют объект при уменьшении счетчика до нуля. То же самое происходит в классе CoLLection Rep. В разделе 9.5 будут рассмотрены другие способы уборки мусора. Описанный подход может эффективно применяться на более высоком уровне полиморфизма, например, для объединения двух коллекций с произвольны­ ми внутренними структурами данных. Однако при этом необходимо учитывать ряд потенциальных опасностей. В частности. если классы, используемые таким образом, поддерживают бинарные операции (такие, как объединение двух кол­ лекций оператором + ) , приходится учитывать возможность того, что передан­ ные объекты относятся к разным реальным типам (например, при слиянии Array с Btree). Архитектура таких классов существенно усложняется, а выбор пра­ вильной операции для переданных объектов потребует немалых затрат на ста­ дии выполнения (за аналогичными примерами обращайтесь к разделам 5.5 и 9.7). Теоретически необходимые преобразования можно было бы определить на уров­ не класса Т о р или Thing, добиваясь полноценного полиморфизма. Тем не менее, определения этих классов становятся очень сложными, и их придется изменять при каждом добавлении новых операций над объектами. Для определения клас­ сов, поддерживающих несколько внешних интерфейсов, можно воспользоваться множественным наследованием. Так, в примере, изображенном на рис. 9. 1 , класс List может проявлять свойства Ar ra y и Li n ked list. Но и этот вариант слишком быстро усложняется. Collection

CollectionRep

ListRep

//� / \

HashTaЫe

Btree

Array

List

Linkedlist

Рис. 9. 1 . Иерархия классов при м ножественном наследовании

9.4 . Код и идиом ы подцержки и нкре м ентн ой за грузки

323

9 . 4 . Код и идиомы подде ржки инкрементно й з а грузки Если в вашем распоряжении имеется системный компоновщик с возможностью дозагрузки, часто можно написать загрузчик для включения нового кода в рабо­ тающую программу. В листинге 9.9 приведена простая функция Load, которая получает имя откомпилированного объектного файла, загружает и запускает его. Допустим, объектный файл incr.o содержит единственную функцию с точкой вхо­ да, совпадающей с базовым адресом секции text объектного файла. Следующий фрагмент загружает и выполняет эту функцию во время работы программы: i nt ma i n ( ) { typedef voi d ( *P F ) ( . . . ) : / / Указ а тель на функцию PF anewfunc = ( PF ) l oad ( " i ncr . o " ) : ( *anewfunc ) ( ) : return О :

Программа работает на большинстве платформ Sun Micгosystems; при ее загруз­ ке должен использоваться флаг -n. Аналогичные программы можно написать для большинства современных операционных систем. Листинг 9.9. Функция для загрузки файла на платформах Sun

#i ncl ude #i ncl ude #i ncl ude caddr_t l oad( const char *fi l ename ) char buf[64J : ( caddr_t ) sbrk ( O ) : caddr_t oadx ( ( char* ) oadx ) + PAGS I Z caddr_t adx ( ( ( l ong ) oadx ) % PAGS I Z ) : spri ntf ( buf . " l d - N - Тtext %Х А a . out %s - о a . out . new" . adx . fi l ename ) : system ( buf ) : i nt fd open ( fi l ename . O_RDONLY ) : ехес Ехес : read ( fd . C char * ) &Ехес . s i zeof( exec ) ) : sbrk ( PAGS I Z - ( ( ( l ong ) oadx) % PAGS I Z ) ) : caddr_t l dadx = ( caddr_t ) sbrk ( Exec . a_text + Ехес . а data + Ехес . а bss ) : read ( fd . l dadx . Ехес . а-tёxt + Ехес . а -data ) : c l ose ( fd ) : return l dadx : =

=

-

=

Включение этого кода в интерактивную программу С++ позволяет организо­ вать загрузку новых функций на стадии выполнения. Пока программа пребывает

324

Глава 9. Э муляция символических языков

на С++

в состоянии ожидания, мы можем откомпилировать новый код и подгрузить его. Далее останется связать функцию с существующим кодом, после чего работу программы можно продолжить. ПРИМЕЧАНИЕ Программная реализация идиом поддержки инкрементной загрузки может б ыть написана вручную или полуавтоматически с генерирована средо й разработки . Идиомы применяются в ситуациях, в которых нео бходимо иметь возможность изменять программу во время вы­ полнения, в том числе при построении прототипов приложени й и для поддержки больших приложений , работающих в непрерывном режиме. В

следующих разделах описываются идиомы и структуры данных, поддержи­ вающие динамическую загрузку кода в работающую программу. Задача делится на две подзадачи: дозагрузку новых функций и преобразование форматов дан­ ных существующих объектов.

З агрузка ви ртуальных фун кци й Наиболее очевидным применением загрузчика является загрузка обновленных версий функций в работающую программу. Как было показано ранее, загрузить функцию несложно - необходимо лишь сгенерировать объектный файл, не со­ держащий ничего, кроме добавляемой функции. Впрочем, также придется свя­ зать вызовы существующей функции с новой реализацией. Именно этой теме посвящен настоящий раздел. В разделе 9.2 упоминалась таблица указателей на виртуальные функции, или для краткости таблица виртуальных функций, ассоциированная с каждым классом. Символическая каноническая форма гарантирует, что указатель на эту таблицу является первым элементом объекта-прототипа любого класса (впрочем, это об­ стоятельство зависит от реализации компилятора). Вот почему все объекты кон­ вертов объявляются производными от Тор: указатель на виртуальную функцию объекта может использоваться для адресации таблицы виртуальных функций 1 •

В среде С++ придется принять особые меры, гарантирующие, что программа со­ держит ровно одну копию таблицы виртуальных функций для каждого класса (кроме классов, созданных множественным наследованием). В одних системах компиляции С++ эта задача решается элементарно, в других требуется особая настройка. За дополнительной информацией обращайтесь к разработчикам ком­ пилятора. Впрочем, одного указателя на таблицу виртуальных функций недостаточно: нуж­ но знать, какой элемент таблицы соответствует обновляемой функции. Конечно, эта задача решалась бы очень просто, если бы таблица в памяти связывала име­ на функций с индексами элементов таблицы; в большинстве реализаций С++ 1

Решение подходит только для одиночноrо наследования; множественное наследование плохо обоб­ щается и здесь не рассматривается.

9.4. Код и идиомы n о,DДержки инкрементной загрузки

325

это не так. Из-за этого нам придется выполнить некоторую работу за предела.ми программы, чтобы передать загрузчику всю необходимую информацию для иден­ тификации нужного элемента таблицы. Для этой цели можно написать небольшую вспомогателъиую функцию, един­ ственная задача которой - вернуть составную характеристику функции, вклю­ чающую адрес и индекс функции в таблице виртуальных функций. Вспомога­ тельная функция кодируется вручную или генерируется автоматически при подготовке к обновлению функции. Процесс обновления делится на две фазы. В первой фазе вспомогательная функция загружается загрузчиком и вызывается для получения индекса элемента в таблице виртуальных функций. Во второй фазе происходит загрузка новой версии функции и связывание ее адреса с пра­ вильным элементом таблицы виртуальных функций. Прежде чем рассматривать функции загрузки, стоит познакомиться с некоторы­ ми вспомогательными структурами данных. Сначала определяется тип указате­ ля на функцию: typedef i nt ( *vptp ) ( ) :

Структура mptr определяет строение элемента таблицы виртуальных функций в большинстве существующих реализаций С++: struct mpt r { short d : short i : vpt r f : }:

Это объявление, используемое в системах С++ на базе компилятора С++ cfront, характерно для большинства других систем. В некоторых компиляторах могут использоваться другие структуры; в этом случае приведенный код придется соответствующим образом изменить. Два поля short определяют смещения, тре­ бующиеся в основном для множественного наследования и здесь не рассматри­ ваемые (при желании см. [ 1]). Для нас наибольший интерес представляет поле f, содержащее указатель на функцию для данного элемента таблицы. Вспомогательная функция загрузки пишется легко: она просто возвращает адрес существующей версии функции, которую мы собираемся заменить. Эта функция (назовем ее functionAddress) выглядит примерно так: extern vptp functi onAddress ( ) { 1 1 Код з а висит от пла тформы и комп и л я тора return ( vptp ) &Array : : put :

При каждой загрузке функции сначала загружается новая копия functionAddress, которая либо заменяет предыдущую версию (с освобождением памяти), либо ос­ тавляет ее в виде несобранного •мусора�. если в системе нет проблем, связанных с необходимостью максимально быстро освободить память.

326

Глава 9 . Эмуляция символических языков на С++

Функция functionAddress способна разрешить неодозначности при загрузке функ­ ции с перегруженным именем. Предположим, класс Array содержит несколько версий функции put: cl ass Array puЬl i c :

};

voi d put ( i nt . douЬl e ) : voi d put ( i пt . i пt ) :

Для выбора функции, имеющей второй параметр типа double, может применять­ ся присваивание с левосторонним значением нужного типа: exterп vptp functi onAddres s ( ) { 1 1 Код з а в и с и т от платформы и компиля тора typedef voi d < < Array : : *TYP E ) ( i nt douЬl e ) ) ТУРЕ retva l = &Array : : put : return ( vptp ) retva l : .

Следующая группа функций класса Тор обеспечивает инкрементную загрузку и замену функций. Первая функция comparefuncs проверяет, соответствуют ли две характеристики одной и той же функuии: i пt Тор : : compa refuncs ( i пt vtЫ . vptr vtЫ Fpt r . vptp fpt r ) 1 1 Код з а в и с и т о т пла тформы и компиля тора return vtЬl i ndex == ( i пt ) fpt r ;

Первая пара аргументов содержит информацию об элементе таблицы вирту­ альных функций: в i nt передается индекс, а в vptp - содержимое указателя на функцию ( m ptr: :f) для данного элемента. Второй параметр определяет адрес замещаемой функции. Если по первым двум параметрам функция comparefuncs определяет, что заданная функция совпадает с той, которая определяется по­ следним аргументом, она возвращает ненулевое значение. Вопрос о том, какие параметры реально требуются при сравнении, зависит от реализации. В нашем примере известно, что при получении адреса компилятор возвращает индекс функции в таблице, поэтому мы ограничиваемся сравнением индексов. Вторая функция fi ndVtЬLEntry определяется следующим образом: mpt r * Тор : : fi пdVtЫ Entry ( vptp f ) { 1 1 Код з а в и с и т от пла тформы и ком пиля тора mpt r **mpp = ( mpt r** ) thi s : reg i ster mpt r *vtЫ = *mpp : for ( i nt i = 1 : vtЫ [ i ] . f : ++i ) { i f ( compa reFuncs ( i . vtЬl [ i ] . f . f ) ) {

9.4. Код и идиомы подцержки инкрементной загрузки

return vtЫ +

327

i :

return О :

Функция просто ищет в таблице виртуальных функций текущего объекта (на которую указывает первое слово объекта) элемент, соответствующий обнов­ ляемой функции и передаваемый в качестве параметра. Если поиск оказывается успешным, функция возвращает указатель на соответствующий элемент табли­ цы виртуальных функций (mptr). Остается лишь загрузить новую виртуальную функцию и скомпоновать ее. Зада­ ча решается функцией Top: :update: extern " С " vptp l oad ( const char * ) : voi d Top : : update( const char *prepname . const cha r *l oadname ) l oad( prepname ) : vptp fi ndfunc mpt r *vtЫ = fi ndVtЫ Entry ( ( *fi ndfunc ) ( ) ) : l oad ( fi l ename ) : vtЫ Entry - >f =

=

При вызове функции указывается имя файла, содержащего вспомогательную функцию, и имя файла с загружаемой функцией. Используя определенные ранее функции (у функции Load изменен тип, чтобы она возвращала vptr вместо caddr_ t), она находит элемент таблицы виртуальных функций и заменяет текущее зна­ чение указателя указателем на новую загруженную функцию. Все последующие вызовы виртуальной функции адресуются новой версии. Если еще немного потрудиться, программу можно адаптировать для включения новых виртуальных функций в класс (см. упражнения в конце главы). Тем не менее, организовать полноценную поддержку такой возможности с сохранением семантической корректности гораздо сложнее. Например, добавление новой виртуальной функции не ограничивается расширением таблицы виртуальных функций с сохранением порядка предыдущих элементов. Подумайте, что про­ изойдет при добавлении новой виртуальной функции, имя которой замещает ранее существовавшую глобальную функцию: как определить, что именно было перекомпилировано и загружено?

О бн о вление структуры класса и функция cutover Расширение методов оперативного обновления, предназначенных для корректно­ го изменения структур данных, значительно повышает гибкость среды разработки и уровень поддержки програ.'\fмного продукта. Однако эта проблема сложнее, чем проблема обновления функций: механизм виртуальных функций содержит про­ межуточный уровень логической адресации, отсутствующий в данных. Тем не

328

Глава 9. Эмуляция символических языков на С++

менее, идиома «конверт/письмо• обеспечивает наличие такого уровня и может использоваться для организации корректной замены. В этом разделе описана мето­ дика поддержки оперативной замены данных в классах писем. Поскольку большая часть прикладного кода сосредоточена именно в классах писем, этот подход по­ зволяет решить большинство проблем, связанных с изменением данных классов . Обновление структуры данных класса (то есть повторная загрузка класса) факти­ чески означает повторную загрузку его функций; не сушествует кода, соответст­ вующего самой структуре класса, но информация об этой структуре распределе­ на среди его операций. В предыдущем разделе было показано, как организовать оперативную загрузку функций, однако перезагрузка класса не сводится к про­ стой перезагрузке всех его функций, поскольку существующие объекты также должны быть адаптированы к новой структуре класса. Чтобы обеспечить такую возможность, каждый прототип отслеживает все созда­ ваемые им объекты. При наличии списка созданных объектов он может пре­ образовать все представления данных своих классов писем при модификации их класса. Для хранения списка экземпляров удобно воспользоваться простым и универсальным классом List из библиотеки С++. Объект List объявляется ста­ тической пере!'.1енной в классе прототипа. Пользователь должен предоставить функцию cutover, которая умеет преобразовы­ вать существующие объекты в памяти в новые версии; изменяя формат данных, функция сохраняет семантику. Эта функция, как и любая другая, может загружать­ ся независимо. Момент вызова функции cutover после завершения определяется инфраструктурой самого приложения. Приложение может вызвать функцию cutover, когда система находится в известном, статическом состоянии, например, когда обновляемым функциям заведомо не может быть передано управление. Существует много способов построения функций cutover. Следующие примеры дают представление о том, что вам предстоит сделать. Добавление нового поля в класс Предположим, в класс HashTable включается новое поле со списком блоков, со­ держащих дополнительные данные: templ ate cl ass HashTaЬl e : puЫ i c Col l ecti onRep { puЫ i c : HashTabl e ( ) : HashTaЬl e ( HashTaЫ e& ) :

};

Thi ng *cutove r ( ) ; pri vate : i nt nbuclbuckets : 1 1 Инициал и з а ц и я новых полей ( возможно . по з начен и я м старых ) : retva l - >overfl ow О : 1 1 Удаление старых данных О: obj ect ->buckets del ete obj ect : 1 1 Воз в ращение нового объекта return retva l : =

=

=

=

Сущест венные изменения в п редставлении класса При значительном изменении внутреннеrn строения класса обновление сущест­ вующих экземпляров требует нетривиа:1 ы 1 ых мер. Новые экземпляры прихо­ дится строить на основе старых. Функция cutover должна иметь доступ к обеим версиям интерфейса класса, новой и старой. Нам потребуется копия объявления старого интерфейса, в которой все вхождения имени класса заменены некото­ рым временным именем. Функция cutover использует обе версии, поэтому она не ограничивается в способах построения нового объекта на базе старого.

Для примера возьмем класс Point: c l ass Poi nt : puЫ i c ShapeRep { puЬl i c : Shape mak e ( douЫ e х . douЫ e у ) ; voi d rotate ( Shape& р ) : / / Поворот вокру г заданной точ ки pri vate : douЫ e х . у : }:

Требуется изменить класс Point так, чтобы он работал в круговых координатах (например, чтобы мы могли воспользоваться новым графическим устройством).

330

Глава 9. Эмуляция символических языков на С ++

Интерфейс класса остается неизменным, поэтому преобразование существую­ щих объектов не нарушит работу системы. Новая версия класса выглядит так: c l ass Poi nt : puЬl i c ShapeRep { puЫ i c : Shape mak e ( douЫ e х . douЫ e у ) : voi d rotate C Shape& р ) : / / Поворот вокру г заданной точ к и Thi ng *cutover ( ) : pri vate : doubl e radi us : Ang l e theta : / / Л е г ко строятся и з douЫ e }:

Мы перерабатываем старый заголовочный файл и подставляем временное имя класса; получается приведенное ниже объявление. Обратите внимание: класс Point объявлен дружественным, чтобы функция cutover могла напрямую обра­ щатъся к его полям. В большинстве случаев функция cutover может получитъ все необходимое из открытой сигнатуры старого класса; тем не менее, после обнов­ ления старые функции могут стать недоступными ! cl ass OLDPoi nt : puЬl i c ShapeRep { fri end Poi nt : puЬl i c : Shape ma k e ( douЫ e х . douЫ e у > : voi d rotate ( Shape& р ) : / / Поворот вокру г заданной точ к и pri vate : douЬl e х . у ; : }

Далее мы пишем функцию cutover для нового класса: Thi ng * Poi nt : : cutove r ( ) { OLDPoi nt *ol d = C OLDPoi nt * ) thi s : Poi nt *newPoi nt new Poi nt : newPoi nt - >radi us = : : sqrt ( ol d ->x*ol d - > + o l d - >y*o l d - >y ) : i f ( : : abs ( ol d ->x) < . 000001 ) { newPoi nt - >theta : : atan ( l ) * 2 : el se { } newPoi nt - >theta : : atan2 ( ol d - >y . ol d - >x ) : } i f (у < 0 ) newPoi nt - >theta += : : atan ( l ) * 2 : return newPoi nt : =

=

=

Управление загрузкой функций и п реобразова нием объектов

Все управление загрузкой и преобразованием должно осуществляться самим при­ ложением. Например, приложение должно знать, в какой момент может быть вы­ полнено безопасное обновление (чтобы избежатъ попыток обновления в процессе

9.4 . Код и идиомы подцержки инкрем_е нтной загрузки

33 1

критических вычислений), или запросить данные у пользователя (скажем, имена файлов с объектным кодом, и т. д.). Во многих приложениях такой «управляю­ щий код• может быть сгенерирован автоматически. В этом разделе приводятся некоторые соображения относительно архитектуры такого кода. Мы рассмотрим процесс обновления, используя класс Point в качестве примера. После преобразования исходного текста все функции Poi nt необходимо заново откомпилировать для нового интерфейса. Вспомогательная функция пишется для каждой виртуальной функции Poi nt, после чего существующий прототип Point загружает новые функции в память, вызывая свою функцию Top::update. Функции загружаются по одной, причем каждая ассоциируется с парной вспо­ могательной функцией. Итак, все функции загружены в память. В их число входит функция cutover, преобразующая все существующие экземпляры в новое представление. Все про­ тотипы ведут список созданных экземпляров, поэтому задача преобразования существующих объектов классов, производных от класса письма, решается эле­ ментарно. Помните, что на концептуальном уровне классы писем находятся внутри соответствующих классов конвертов, поэтому мы знаем, что на любой объект иерархии писем может ссылаться только один класс конверта. Прототип этого класса отслеживает все свои экземпляры, он может последовательно пе­ ребрать их и проверить, какой тип письма в них содержится. Любое письмо, функция type которого возвращает указатель на прототип обновляемого письма, является кандидатом на обновление. А это означает, что конверт может последо­ вательно выполнить операцию cutover с каждым из объектов, обновляя поле rep указателем на преобразованный экземпляр. Предположим, мы заменяем класс Poi nt, производный от Shape Rep. Прототип Shape содержит список всех созданных объектов Shape; он знает, что поле rep ка­ ждого из них ссылается на некоторый объект в иерархии ShapeRep. Перебирая экземпляры класса Shape, алгоритм обновления выделяет из них те экземпляры s, для которых истинно условие s.rep->type()�point (где point - указатель на прото­ тип Point). Для каждого такого объекта s значение s.rep заменяется значением s.rep->cutover. В результате все объекты конвертов обновляются ссылками на объ­ екты нового класса, полученные в результате преобразования старых объектов. Остается внести еще одно изменение. Если одно письмо будет совместно исполь­ зоваться несколькими конвертами, возникнет путаница между старыми и новы­ ми версиями, и процесс преобразования нарушится. Каждое письмо, принадле­ жащее нескольким конвертам, должно обновляться только один раз. Для это­ го в каждый объект Thing включается дополнительный «теневой• счетчик. При переборе объектов мы проверяем значение этого счетчика; если оно равно нулю (значение инициализации), теневому счетчику присваивается текущее значение счетчика ссылок. Затем теневой счетчик последовательно уменьшается. Если он становится равным нулю, выполняется функция cutover; в противном случае объект пропускается. Таким образом, каждый общий объект письма преобразу­ ется только в последнем экземпляре в процессе перебора.

332

Глава 9. Эмуляция символических языков на С ++

Описанная логика размещается в новой функции Thi ng с именем docutover: i nt Thi ng : : docutove r ( ) { i f ( ! updateCountVa l ) updateCountV a l = refCountVa l : return ! - - updateCountVa l :

Функция Shape::dataUpdate управляет процессом обновления данных в классах, производных от базового класса писем ShapeRep (листинг 9. 1 0). Ее параметрами являются указатель на прототип обновляемого класса письма и указатель на прототип, который должен занять его место. После выполнения описанного ал­ горитма функция также обновляет объект прототипа. Полученная в результате программа отражает новую версию класса. Предполагается, что виртуальные функции новой версии класса уже загружены. Листинг 9. 1 О. Полный цикл обновления класса

typedef Th i ng *Th i ngp :

voi d Shape : : dataUpdate ( Th i ngp &ol dExempl a r . const Thi ngp newExempl a r ) { Th i ng *saveRep : Shape *sp : for ( L i sti ter р a l l Shapes : p . next ( sp ) : р++ ) { i f ( sp - >rep - >type ( ) == &ol dExempl a r ) i f ( p - >rep - >docutover ( ) ) { saveRep = sp- >rep : sp- >rep = ( ShapeRep* ) sp - >rep ->cutover ( ) ; del ete saveRep ; =

saveRep = ol dExempl a r : ol dExempl a r = newExempl a r : del ete saveRep :

И н крементн ая з агруз к а и а втоном н ы е обобще н н ы е конструкторы Инкрементная загрузка эффективно работает в сочетании с автономными обоб­ щенными конструкторами, описанными в разделе 8.3. Автономные обобщенные конструкторы позволяют специализированным прототипам (таким, как Number,

9.5. Уборка мусора

333

Name, Punctuation и т. д.) регистрироваться с применением более абстрактного обобщенного прототипа (Atom), называемого автономным обобщенным прототи­ пом. Регистрируемые прототипы обычно являются производными от класса автономного обобщенного прототипа. Автономный обобщенный прототип служит для них агентом; набор регистрируемых прототипов может изменяться на протя­ жении жизненного цикла программы. Методика инкрементной загрузки позволяет добавлять новые производные клас­ сы на протяжении жизненного цикла программы, создавать для них прототипы и регистрировать их в абстрактном базовом прототипе по мере появления. На­ пример, мы можем включить в анализатор работающей системы классы BinaryOp и UnaryOp, создать для них объекты прототипов с использованием соответствую­ щего конструктора и зарегистрировать в классе Atom. Далее система начинает принимать и правильно обрабатывать бинарные и унарные операторы в но­ вых строках, передаваемых из пользовательского интерфейса, входных каналов данных и любых источников, в которых прототип Atom применяется для синтак­ сического анализа.

9 . 5 . Уборка мусора Важной особенностью символических сред программирования является то, что они избавляют программиста от хлопот с управлением памятью: объекты, на которые не осталось ни одной ссылки, автоматически уничтожаются средой времени выполнения (с помощью операционной системы, оборудования или то­ го и другого). Этот процесс называется уборкой мусора. Уборка мусора создает впечатление, что объем памяти не ограничен, поэтому прикладные программи­ сты могут просто забыть о тех объектах, которые решили свою задачу и стали лишними. Если система определяет, что объект недоступен для любых других частей программы, занимаемая им память освобождается и становится доступ­ ной для последующих запросов на выделение памяти. Механика и время освобо­ ждения объекта остаются незаметными для конечного пользователя. Идиомы подсчета ссылок, представленные в разделе 3.5, являются упрощенной формой уборки мусора. В частности, идиома подсчета указателей (см. с. 78) обеспечивает уровень прозрачности управления памятью, присущий символиче­ ским средам. Однако алгоритмы, основанные на подсчете ссылок, не позволяют освобождать память циклических структур да.Itных без применения механизмов рекурсивного сканирования, требующих больших затрат ресурсов. В высокоуров­ невых объектно-ориентированных языках применяются особые схемы уборки мусора, избавленные от этого ограничения, но в них редко используется подсчет ссылок. Идиома уборки мусора, описанная в этом разделе, представляет собой альтернативу подсчету ссылок. Методы, не требующие подсчета ссылок, также лучше подходят для встроенных систем реального времени, в которых исключительные события (такие, как сбои па­ мяти или процессора) могут стать причиной каскадных операций восстановления,

334

Глава 9. Эмуляция символических языков на С++

когда освобождение памяти затруднено. Если процесс становится неуправляемым и нуждается в аварийном завершении, возможно, у него не будет возможности выполнить свои деструкторы. Если он использует объекты с подсчетом ссылок совместно с другими процессами, то счетчики ссьmок таких объектов никогда не уменьшатся до нуля, и эти объекты никогда не будут освобождены. В свете та­ ких сбоев аудит памяти отличается большей гибкостью в отношении возврата освободившихся ресурсов, чем подсчет ссылок. Во многих символических средах программирования детали уборки мусора скры­ ваются в реализации компилятора и среды времени выполнения. Хотя некото­ рые ранние среды Smalltalk использовали алгоритмы освобождения памяти на базе подсчета ссылок, прикладные программисты писали код, в котором подсчет ссылок не применялся. Сравните с подходом С++, описанным в разделе 3.5, где управление памятью реализовано не как алгоритм, скрытый внутри компилятора и среды времени выполнения, а как языковая идиома. Некоторые схемы уборки мусора при помощи специализированного оборудования определяют, содержит ли слово памяти активный указатель на объект или простой блок данных. Управ­ ление памятью в символических языках, на чем бы оно ни бьmо основано - на поддержке компилятора, средствах операционной системы или специализирован­ ном оборудовании - реализуется ниже уровня исходных текстов, написанных прикладными программистами. Термин «уборка мусора• обычно подразумевает именно такую прозрачность. Язык С++ настолько близок к оборудованию, что не существует сболее низкого• уровня, на котором можно было бы реализовать абсолютно прозрачный и всеобъемлющий механизм уборки мусора. Если при­ держиваться простых правил написания программ, формирующих среду уборки мусора. вы сможете реализовать достаточно прозрачную уборку мусора для от­ дельных классов в своих программах С++. Методика, описанная в этом разделе, прозрачна по отношению к механизму под­ счета ссылок из раздела 3.5; освобождение памяти объекта в ней отделяется от «разрыва• последней ссылки на него. С другой стороны. она не обеспечивает уничтожения структур с циклическими ссылками. По быстродействию она не­ сколько уступает методике подсчета ссылок, хотя количество сравнений зависит от особенностей использования и подробностей реализации. Разновидности пред­ ставленной методики позволяют выполнять поэтапную уборку мусора, поэтому нормальную работу программы не придется надолго приостанавливать для осво­ бождения памяти. Кроме того. в отличие от традиционной уборки мусора, этот механизм позволяет программисту выполнить некоторые действия при уничто­ жении объекта: уборщик мусора может вызвать деструкторы, чтобы правильно ликвидировать объект. Для уборки мусора было разработано несколько алгоритмов. Одним из первых появился алгоритм предварительной пометки [2]: он анализирует объекты в па­ мяти, проверяет, на какие объекты они ссылаются, и помечает объекты, на кото­ рые имеются ссылки. После того как все объекты обработаны, на следующем проходе непомеченные объекть1 уничтожаются.

9.5. Уборка мусора

335

Алгоритм полупространственноzо копирования гарантирует корректную ликви­ дацию объектов. на которые отсутствуют ссылки, а также ликвидацию групп объектов, не имеющих внешних ссылок. Прототип полупространственного ко­ пирования, алгоритм Бейкера [3], избавляется от лишних задержек, присущих алгоритму предварительной пометки, за счет дополнительных затрат памяти. В алгоритме Бейкера память делится на две половины, А и В. Половина А обыч­ но называется приемником, а половина В источником (причины вскоре станут ясны). Новые объекты создаются в половине А. В некоторый момент (например, при заполнении половины А или в период бездействия системы) все доступные объекты половины А перемещаются в один конец половины В. Указатели на объекты, хранящиеся в других объектах, изменяются в соответствии с изменив­ шимся положением объектов в памяти. После завершения половина А заведомо не содержит доступных объектов, а только •мусор•. Затем А и В меняются роля­ ми, и начинается новый цикл. Алгоритм Бейкера (как и большинство методов уборки мусора без подсчета ссы­ лок) зависит от возможности различать в памяти данные и ссылки на объекты (указатели). Но если взять обычное слово в памяти С++, вы не сможете опреде­ лить, что оно представляет - данные или указатель. Недавно появилось новое семейство алгоритмов уборки мусора, вьmолняющих почти полную уборку мусо­ ра и основанн!iх на алгоритме предварительной пометки. Иногда эти алгоритмы появления в памяти не могут уничтожить мусор из-за наложения указателей данных, содержимое которых совпадает с адресом объекта, хотя на самом деле значение представляет целочисленную величину или что-нибудь еще, совершен­ но не связанное с его интерпретацией в качестве адреса объекта. С другой сторо­ ны, при соблюдении программистом некоторых разумных правил эти алгоритмы не оставляют •виtячих• ссылок. Примеры таких алгоритмов приведены в [4] . -

-

И все же в ограниченном мире символической идиомы, в мире конвертов и писем

мы можем

идентифицировать все объекты заданного класса конверта (просмотром списка, хранящегося в прототипе), а, следовательно, можем найти все ссылки на любой класс письма. Если изменить алгоритм выделения памяти для класса пи­ сем, чтобы он использовал фиксированный пул (см. раздел 3.6), то типы всех объектов в пуле будут известны (потому что это один и тот же тип). Также мы знаем все возможные адреса, по которым могут находиться объекты классов писем, поэтому появляется возможность отметить используемые объекты и уничтожить все остальные. Поскольку все объекты в пуле имеют одинаковый размер, исчезают проблемы с фрагментацией памяти. С другой стороны, объем памяти, занятой объектами всех классов, участвующих в уборке мусора, должен быть известен заранее. ПРИМ ЕЧАНИЕ Используйте уборку мусора, что б ы из бавить пользователей от хлопот с управлением памя­ тью, например, при ускоренной разраб отке проб ных верси й приложени й . У борка мусора также является мощным средством аудита ресурсов в системах с исключениями реального времен и. О на позволяет гарантировать , что все р есурсы памяти будут осво божден ы даже в свете исключительн ых обстоятельств .

336

Глава 9. Эмуля ция символических языков на С++

Используя класс Triangle в качестве примера, рассмотрим одну из возможных реализаций уборки мусора в символической идиоме •конверт/письмо• . Для поддержки этой методики базовый класс письма Shape Rep необходимо допол­ нить новым битовым флаюм; сброс этого бита помечает объект как доступный для алюритма. Объектам также может понадобиться дополнительный битовый •Флаг занятости• , который показывает, свободна ли данная область памяти. Этот бит также может использоваться операторной функцией ShapeRep::operator new для поиска свободных областей памяти, которые можно было бы задейство­ вать, чтобы удовлетворить запросы на создание объектов. В исходном состоянии все биты занятости и пометки сброшены; при выделении памяти под объект также устанавливается ею бит занятости. Помимо этих битов каждый объект содержит биты А и В, определяющие ею принадлежность к одному из полупространств А и В алюритма Бейкера.

П ри м е р иерархи и гео м етрических фи гур с уборкой м усора В приложении Д представлен пример простого графического пакета, в котором

применяются символические идиомы, описанные в настоящей главе. В примере демонстрируются упоминавшиеся ранее средства параллельной дозагрузки и рас­ ширенная форма уборки мусора. Методика уборки мусора не требует подсчета ссылок, а работает в духе алгоритмов Бейкера и предварительной пометки. Алгоритм уборки мусора анализирует содержимое списка существующих объ­ ектов, хранящегося в прототипе Shape, просматривает поле rep каждою из них и проверяет, содержит ли оно указатель на объект Triangle (для чего возвращае­ мое значение функции type сравнивается с адресом прототипа Triangle). Для каж­ дого найденного объекта устанавливается бит пометки. После завершения этого прохода алгоритм перебирает элементы фиксированною вектора, в котором соз­ даются объекть1 Triangle, и ищет объекты со сброшенным битом пометки. После уничтожения объекта вызовом деструктора Triangle бит занятости сбрасывается. Напоследок бит пометки тоже сбрасывается для подготовки к следующему цик­ лу уборки мусора. Алгоритм в цикле обрабатывает все объекты Triangle, затем все объекты Li ne, все объекты Circle и т. д., пока не будут обработаны все классы, производные от ShapeRep, после чего все начинается заново. На более высоком уровне алгоритм последовательно применяется ко всем областям приложения (то есть к иерархии Shape, к иерархии CoLLection и т. д.). Планирование циклов уборки мусора должно производиться средой времени выполнения; возможно, его придется отдельно настраивать для каждою приложения. Алюритм Бейкера даже позволяет выпол­ нять уборку мусора поэтапно (см. упражнения в конце главы). За подробностями реализации алгоритма обращайтесь к приложению Д. Струк­ тура классов уже описывалась, а иерархия наследования изображена на рис. 9.2. Все специализированные реализации находятся в классах, производных от ShapeRep, а пользователь имеет дело лишь с экземплярами Shape.

9.5. Уборка мусора

337

Рис. 9 . 2 . Иерархия классов Shape в символической идиоме

Статическая функция Shape: :i nit инициализирует некоторые глобальные струк­ туры данных. Инициализация производится явно, без обращения к стандартным механизмам, предоставляемым средой С++, чтобы мы могли контролировать по­ рядок инициализации. Функция Shape::i nit сначала инициализирует два объек­ та-списка. В первом списке (aLLShapes) отслеживаются все созданные экземпляры Shape, а во втором (aLLSh a peExemptars) - все объекты прототипов для классов, производных от ShapeRep. Далее Shape: :init вызывает функцию i nit для каждого класса, производного от ShapeRep. В свою очередь, эти функции конструируют соответствующие объекты­ прототипы. Каждый прототип регистрируется в Shape при помощи функции Shape::register, сохраняющей указатель на все прототипы в списке aLLShapeExemptars. Теперь понятно, почему списки Shape должны инициализироваться раньше про­ тотипов. Недостаток такой схемы инициализации заключается в том, что класс Shape на стадии компиляции должен обладать информацией обо всех классах, производных от Shape Rep. После завершения инициализации пользователь выдает запрос на создание объ­ екта Shape, опираясь на идиому анонимного обобщенного конструктора:

Shape obj ect = ( *shape ) - >ma ke ( pl . р2 . рЗ ) :

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

shape - >operator - > ( ) - >ma ke ( pl . р2 . рЗ )

338

Глава 9. Эмуляция с и мволических языков на С ++

Прототип Shape возвращает указатель на экземпляр ShapeRep, который исполь­ зуется в качестве операнда для make. В идиоме ma ke ( ppl . рр2 . ррЗ ) : Наконец, для построения треугольника по трем вершинам вызывается функция TriangLe::make:

Shape Tri angl e : : make( Coordi nate ppl . Coordi nate рр2 . Coord i nate ррЗ ) { Tri angl e *retva l new Tri a ng l e : retva l - >pl ppl : retva l - >p2 рр2 : retva l ->pЗ ррЗ : retva l ->exemp l a rPoi nter thi s : return *retva l : =

= =

=

=

Функция make создает объект TriangLe вызовом оператора new и заполняет этот объект данными вершин треугольника. Поле exem pLarPoi nter (то есть поле типа) заполняется указателем на this, ссылающимся на прототип треугольника (значе­ ние this в этом контексте совпадает со значением указателя на глобальный про­ тотип triangLe ). Наконец, команда return передает новый объект вызывающей сто­ роне. Для преобразования объекта в правильный тип возвращаемого значения используется конструктор Shape(ShapeRep&).

9.5. Уборка мусора

339

При создании нового объекта TriangLe оператором new вызывается собственный оператор new класса TriangLe:

voi d *Tr i a ngl e : : operator new ( s i ze_t nbytes ) { i f ( pool l n i ti a l i zed - nbytes ) { gcCorтmon ( nbytes . pool l n i ti a l i zed . Pool Si ze . heap ) ; pool l n i ti a l i zed = nbytes : } Tri a ng l e *tp = ( Tri a ng l e* ) hea p : whi l e ( tp ->i nUs e ) { tp = ( Tri a ng l e* ) ( ( ( char* ) t p ) + Round ( nbytes ) ) ; } tp->gcma rl< = О : tp->i nUse = 1 : return < voi d* > tp : Оператор new класса TriangLe возвращает указатель на доступный блок памяти из пула объектов ТriangLe, размер которого соответствует размеру объекта. При первом вызове переменная pooUnitiaLized равна нулю; она инициализируется на стадии компиляции. Если при сравнении с переменной nbytes, равной sizeof(TriangLe), об­ наруживается несовпадение, вызывается функция ShapeRep::gcCommon. Она исполь­ зуется как уборщиком мусора, так и в особых случаях - для инициализации пулов памяти в начале работы программы или при замене классов. В частности, в фазе инициализации функция gcCommon присваивает начальные значения всем флаговым битам в пуле. После возврата из gcCommon оператор new перебирает блоки пула TriangLe (на который указывает TriangLe:: heap) в поисках блока с ус­ тановленным битом занятости i n Use, и возвращает первый найденный блок. Обработка возможного переполнения пула остается читателю для самостоятель­ ного упражнения. Все вновь созданные объекты Shape работают по одной схеме. При инициализа­ ции нового объекта Shape на базе существующего или присваивании одного объ­ екта другому вызывается соответствующий перегруженный конструктор или оператор присваивания:

Shape : : Shape ( Shape &х ) { Thi ng tp = thi s : a l l Shapes - >put ( tp ) : rep = x . rep : Shape& Shape : : operator= ( Shape &х) rep = x . rep : return *th i s : Таким образом, указатели на один объект класса, производного от Shape Rep, мо­ гут содержаться сразу в нескольких объектах Shape.

340

Глава 9. Эмуляция символических языков на

С++

Согласно идиоме подсчета указателей (см. с. 78), объекты Shape не создаются в динамической куче; тем самым предотвращаются все проблемы с их уничто­ жением. Любой объект Shape, созданный как автоматический, уничтожается при выходе из области видимости. Любой объект Shape, являющийся членом класса, уничтожается при уничтожении объекта этого класса. Глобальные объекты Shape уничтожаются при завершении программы. Впрочем, мы все равно должны по­ заботиться об освобождении памяти и корректной ликвидации объектов клас­ сов, производных от ShapeRep. В этом на.'1. поможет уборщик мусора. Уборка мусора может быть инициирована в любой момент. В приложении Д она инициируется вручную в разных частях программы. Более удобный, хотя и более затратный подход заключается в вызове уборщика мусора при каждом создании нового объекта. Возможны и другие стратегии, например, можно освобождать неиспользуемую память только тогда, когда оператор new не найдет ни одного свободного элемента в пуле. В средах реального времени процесс уборки можно даже распределить во времени для поэтапного выполнения, если уборка сопря­ жена с длительными паузами в работе системы (см. упражнения в конце главы). Уборкой мусора управляет функция Shape: : g c, которая проводит фазу пометки самостоятельно, а затем перепоручает уничтожение объектов конкретным объек­ там писем: voi d Shape : : gc ( ) { Li sti ter shapel ter = *a l l Shapes : for ( Торр tp = О : shapelter . next ( tp) : ) { ( ( Shape* ) t p ) ->rep- >ma rk ( ) : } L i sti ter shapeExempl a r lter = *a l l ShapeExempl ars : for ( Thi ngp anExempl a r О : shapeExempl a r l ter . next ( anExempl a r ) : ) { ShapeRep *th i sExemp l a r = ( ShapeRep* ) a nExempl a r : thi s Exempl a r - >gc ( O ) : =

В первом цикле перебираются все существующие объекты Shape и устанавлива­ ется их Еит пометки при помощи функции mark. Таким образом помечаются все объекты классов, производных от ShapeRep, на которые ссылаются существую­ щие объекты Shape. Во втором цикле перебираются прототипы классов писем (по одному для каждого класса, производного от ShapeRep), которые и выполня­ ют вторую фазу алгоритма, то есть уничтожение объектов. Поскольку прототип каждого класса, производного от ShapeRep, поддерживает собственный пул памяти, из которого выделяется память для всех динамических объектов, каждый такой класс может легко найти и перебрать все свои динами­ чески созданные экземпляры. В примере Shape пул представляет собой непре­ рывный блок памяти; возможны и другие реализации - важно лишь, чтобы про­ тотип мог отслеживать все свои экземпляры.

9. 5. Уборка мусора

34 1

Работа по уничтожению объектов выполняется статической общей функцией класса ShapeRep с именем gcCommon (листинг 9. 1 1 ). Функция получает размер объекта в байтах ( n bytes), размер объекта на момент последнего прохода убор­ ки мусора ( pooLinitiaLized), количество объектов в пуле ( PooLSize) и ссылку на указатель на кучу ( heap ). Эти параметры могут передаваться простой функцией gc производного класса, которая в свою очередь может вызываться из Shape без параметров. Листинг 9. 1 1 . Фаза уничтожения объектов на уровне типов

voi d ShapeRep : : gcCommon ( s i ze_t nbytes . const s i ze_t pool i n i t i a l i zed . const i nt Pool Si ze . Cha r_p &hea p ) { s i ze_t s = nbytes? nbytes : pool i n i ti a l i zed : s i ze_t S i zeof = Round ( s ) : ShapeRep *tp = ( ShapeRep * ) heap : for ( i nt i О : i < Pool Si ze : i ++ ) swi tch ( nbytes ) { case О : / / Обыч ная уборка мусора i f ( tp->i nUs e ) { i f ( t p - >gcma rk 1 1 tp->space ! = FromSpace ) { 1 1 Не унич тожа т ь tp- >space ToSpace : el se i f ( tp ! = tp- >type ( ) ) { 1 1 Объект необходимо ликвидировать tp->Sha peRep : : -ShapeRep ( ) : tp->i nUse О : pri ntf ( " ShapeRep : : gcCommon " ) : pri ntf ( " Recl a i med Tri ang l e object %c\ n " . · д · + ( ( ( char * ) tp - ( char * ) heap ) / Si zeof } } : =

=

=

brea k : defa ul t : / / и н и циализация области п а м я т и tp->i nUse = О : brea k : tp->gcma rk = О : tp = ( ShapeRep* ) ( Char_p ( t p ) + Si zeof ) :

Как уже отмечалось, функция ShapeRep::gcCom mon обеспечивает и инициализа­ цию пула, и освобождение памяти. Логика уборки мусора сосредоточена в нуле­ вой ветке команды switch внутри цикла. Она сохраняет объекты, которые были помечены или уже переведены в приемник ( из алгоритма Бейкера). В дан­ ной реализации проверка принадлежности к полупространству не особенно важ­ на, но если память будет освобождаться так, как показано выше, все меняется.

342

Глава 9. Эмуляция символичес ких я зы ков на

С++

Объекты, непомеченные в источнике, подлежат уничтожению; их бит занятости обнуляется , в результате чего память возвращаются в пул для дальнейшего использования оператором new. Обратите внимание: мы связали уничтожение объекта (вызов деструктора) с ос­ вобождением памяти (уборка мусора). Уничтожение объекта является семанти­ ческой операций, связанной с приложением, а уборка мусора освобождает кон­ кретный ресурс, с которым обычно возникает больше всего проблем во мноmх приложениях - память. Уничтожение объекта и освобождение памяти можно разделить на две операции (см. раздел 3.7 ), но тогда программисту придется вручную уничтожать объекты после того, как объект идентифицируется как неис­ пользуемый, но до того, как его память будет освобождена уборщиком мусора. Например, если объект содержит дескриптор открытого файла UNIX, одного лишь освобождения памяти будет недостаточно для возврата в систему ресур­ сов, связанных с этим файлом. Чтобы не перекладывать служебные операции на пользователя, мы объединили эти две функции. Представленный подход предполагает, что корректная ликвидация объекта мо­ жет быть отложена на неопределенный срок. Чтобы выполнить ее принудитель­ но, пользователь может вызвать функцию уборки мусора сразу же после освобо­ ждения последней ссылки на объект.

9 . 6 . И нкапсуля ц ия п р им и т и вны х т и пов В большинстве символических сред экземпляры всех типов данных представля­

ют собой объекты. С другой стороны, в С++ некоторые типы, особенно близкие к аппаратному уровню (int, char, Lon g , short и т. д.), примитивнее полноценных объектов. Было бы желательно работать с этими типами в виде объектов, как в символических языках - в частности, они должны нормально уничтожаться в ходе уборки мусора. Для каждого примитивного типа, который должен использоваться в символи­ ческой среде, можно создать новый класс-•оболочку� . Таким образом, мы по­ строим библиотеку классов, моделирующих свои примитивные аналоm. Задача довольно однообразная, но на концептуальном уровне не сложная. Примером служит иерархия классов N u m ber, описанная в разделе 5.5. После некоторых изменений в коде N u m ber (прежде всего объявления соответствующих классов производными от Тор и Thing) мы сможем применять полностью полиморфные объекты чисел, поддерживающие уборку мусора там, где обычно используются типы i nt, double, Long и т. д. Основные проблемы интеграции примитивных типов с символической идио­ мой возникают при выборе их места в иерархии наследования. Пример N u m ber должен воплотить функциональность всех числовых типов, но зато мы переста­ ем различать типы double и fl.oat; char, int и unsigned int, и т. д. Как поступить со строковым классом Strin g - выделить его в отдельный символический тип, как

343

9.7. Мультиметоды в символической идиоме

предполагалось в главе или

CoLLection,

3,

или отнести к

ArrayedCoLLection, SequenceaЫeCoLLection 6, эти решения

как в иерархии Smalltalk? Как отмечено в главе

должны приниматься на основе анализа предметной области и потребностей приложения.

9 . 7 . М ул ьтиметоды в с имвол ич еской идиоме Давайте попробуем представить, как программа должна интерпретировать сложе­ ние двух числовых объектов, относящихся к разным типам. При традиционном подходе система контроля типов компилятора располагает достаточной инфор­ мацией, чтобы выполнить необходимое преобразование. Но при динамической типизации этот способ не подходит, поэтому в символическом программирова­ нии нужно другое решение. Допустим, каждое число с точки зрения программи­ ста является экземпляром класса и

Biglnteger)

N u m ber,

Com p lex Number изменяет характери­

а классы писем (такие, как

остаются для него невидимыми. Объект

стики типа на стадии выполнения, поэтому компилятор не располагает достаточной

информацией, чтобы генерировать код преобразования.

И преобразование, и вы­

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

operator+

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

В

идеальном случае желательно выбирать алгоритм на основании типов

операндов. Виртуальные функции

С++

обоих

такой возможности не предоставляют,

хотя она поддерживается некоторыми символическими языками.

В

объектно­

ориентированных языках, интегрированных с Lisp, функция (или оператор), вы­ бираемая на стадии выполнения в зависимости от типа нескольких операндов, называется мульти.методом . Для имитации мультиметодов на

С++

используются идиомы, основанные на се­

лекторах типов (см. главу 4). Применять селекторы типов обычно нежелательно; предпочтительным механизмом выбора реализации среди нескольких производ­ ных классов считается механизм виртуальных функций. Далее показано, почему виртуальные функции в данной ситуации не подходят. На с.

1 78 в разделе 5.6 приводится определение оператора класса низкочастотно­ LPF:

го фильтра

Val ue* LPF : : operator ( ) ( Va l ue* f ) swi tch ( f - >type ( ) ) { case Т Data :

344

Глава 9 . Эмуляция символических языков на

С++

myType T_Data : cached l nput = f : return eva l uate ( ) : Т LPF : i f ( f ->fl ( ) > fl ( ) ) return thi s : el se return f : Т HPF : i f ( f- >fl ( ) > fl ( ) ) return new Notch ( fl ) ) . f->fl ( ) ) : el se return new BPF ( f- >fl ( ) . fl ( ) ) : т BPF : i f ( ( ( Fi l ter* ) f ) ->f2 ( ) < fl ( ) ) return f : el se return new BPF ( f->fl ( ) . fl ( ) ) : Т Notch : cached l nput = f : return th i s : =

case case case case

Там же показано, что анализ вариантов может быть преобразован в набор опре­ делений виртуальных функций. Но тогда увеличение числа типов фильтров приведет к размножению открытых виртуальных функций, что совершенно не способствует упрощению кода. Семантика Fi Lter: : o perator(} (Vatue* ) заключается в возврате значения одного из нескольких типов фильтров в зависимости от контекста. Контекст включает как тип фильтра, для которого бьmа вызвана функция класса, так и тип объекта, пе­ реданного в параметре. Итак, мы имеем входной и выходной фильтры, и типы обоих фильтров определяют, какой алгоритм нужно применить и какой резуль­ тат будет получен. В примере из 5.6 соответствующая логика распределена по .классам, производным от Fitter. В приведенном выше коде класса LPF она сосре­ доточена в выходных фильтрах. Если бы мы выбрали решение с виртуальными функциями, и вызов Fitter: :operator() (Vatue* input) просто обращался бы к функ­ ции входного фильтра для выполнения работы, то логика выполнения операции была бы сосредоточена во входных фильтрах. Ни один из этих подходов не идеален. Было бы желательно обрабатывать эти за­ просы как гибридные операции, не являющиеся монополией одного или другого класса. Для этого требуется нечто вроде дружественных функций с глобальной перегрузкой (см. раздел 3.3): С т а рое решен ие :

Н о в ое решение :

Compl ex operator+ ( Compl ex с . Imagi nary i ) { } c l ass Compl ex { fri end Compl ex operator+ ( Compl ex . I magi nary ) : puЫ i c : operator douЬl e ( ) : .

cl ass Compl ex { puЬl i c : Compl ex operator+ ( lmagi nary ) : operator douЬl e ( ) : }:

}:

.

.

9.7. Мультиметоды в с имволической идиоме

345

Однако здесь оператор выбирается во время компиляции на основании обьяв­ типов аргументов; не существует простого способа выбрать функцию во время выполнения в зависимости от фактических типов аргументов. Именно эта задача должна решаться мультиметодами.

ленных

В качестве простого примера возьмем класс Number; прежде всего нас интересует оператор +. Чтобы реализовать мультиметод operator+, мы воспользуемся комму­ тативностью сложения и сократим количество особых случаев вдвое. Для обра­ щения к полю типа будет использоваться виртуальная функция Top: :type. Не­ смотря на реализацию в виде виртуальной функции, она ведет себя как открытая переменная перечисляемого типа. Для начала добавим в класс Number и в каждый из его производных классов но­ вую функцию isA(const Top * const). Функция возвращает true при вызове для объ­ екта класса А с аргументом, указывающим на объект класса В, если истинно ус­ ловие 4В является частным случаем А• (отношение 4IS-A• ) . Проверка выявляет более общий из двух операндов, чтобы передать ему контроль над операцией. Кроме того, добавим единственную функцию promote, заменяющую операторы преобразования из предыдущих примеров. Функция promote преобразует аргу­ мент, относящийся к произвольной разновидности Number, в тип объекта, для ко­ торого она вызывается. Учитывая, что все преобразования теперь выполняются функцией promote, нам уже не придется перегружать сложение в сигнатуре каж­ дого класса конверта; каждый класс содержит одну функцию add (имя operator+ заменяется именем add для предотвращения неоднозначности при добавлении нового оператора +). Теперь выбор в зависимости от типа параметра становится лишним, поскольку функция add каждого конверта может считать, что она все­ гда получает параметр типа своего класса: c l ass Number puЫ i c : vi rtu a l Number *type ( ) const : vi rtual i nt i sA ( const Number *const ) const : vi rtua l Number promote ( const Number& ) const : v i rtu a l Number add ( const Number& ) const : fri end Number operator+ ( const Number& . const Number& ) const : }; cl ass Compl ex : puЫ i c Number { puЫ i c : i nt i sA( const Number *const n ) const { return n - >type ( ) compl exExempl a r : } Number promote ( const Number& n ) const 11 Все гда воз вращает Compl ex ==

346

Глава 9. Э муляция символи ческих языков на

С++

i f ( n . type ( ) i magi na ryExempl a r > { return Numbe r ( O . n . magni tude ( ) ) : } el se i f ( n . type ( ) � i ntegerExempl a r ) return Number ( n . magni tude ( ) . 0 ) : } el se �

11 Оператор работает толь ко с объекта м и Compl ex Number add ( const NumЬer& ) const : }: c l ass Imagi nary : puЫ i c Compl ex { puЫ i c : i nt i sA( const Number *const n ) const { return n - >type ( ) i magi naryExemp l a r 1 1 Compl ex : : i sA< n > : } 1 1 Функция promote отсутствует . поэ тому 1 1 п реобра зование в Imagi nary не выпол н яется . 1 1 Опера тор работает тол ь ко с объекта м и Imagi nary Number add ( const Number& ) const : ==

}:

Мультиметоды реализуются добавлением глобально перегруженного операто­ ра +, применяемого компилятором к любым двум объектам N u m ber: Number operator+ ( const Number &nl . const Number &n2 ) throw< NumberTypeError ) i f ( n l . i sA < &n2 ) ) { Number tempora ry n2 . promote ( n l ) : return n2 . add ( tempora ry ) : } el se i f ( n2 . i sA ( &n l ) ) { Number tempo ra ry nl . promote ( n2 ) : return nl . add ( tempo ra ry ) : } el se { th row NumberTypeError ( ) : =

=

Такое решение обладает рядом преимуществ по сравнению с представленным в разделе 5.5 на с. 1 48. Между функциями возникает более тесная связь: все пре­ образования типа локализуются, а бинарные математические операторы работают с операндами разных типов (необходимость в перегрузке операторов отпадает). Из определений функций классов исчезли команды switch. Представленное решение имитирует принципы работы символических языков. В некоторых реализациях Smalltalk конкретные числовые классы напрямую

Упражнения

347

выполняют математические операции только в том случае, если оба операнда от­ носятся к одному типу. Если типы не согласуются, класс N u mber должен выпол­ нить необходимые преобразования и в конечном счете вызвать метод нужного субкласса. Ранее был представлен глобально перегруженный оператор: operator+ ( const Number& . const Number& >

В схеме Smalltalk этот оператор выглядел бы так: NumЬer : : operator+ ( const NumЬer& ) В некоторых средах Usp мультиметоды реализуются в виде каскадных вызовов виртуальных функций, с динамическим определением метода по типам несколь­ ких аргументов.

У п ражнения 1 . Измените функцию Top::update и другие необходимые функции таким обра­ зом, чтобы в класс письма можно было добавлять 1ювые виртуальные функ­ ции. Учтите, что исходная таблица виртуальных функций для класса разме­ щается в памяти статически. Предположим, компилятор может обеспечить сортировку элементов новых функций в конце таблицы. Какие ограничения это накладывает на наследование от классов писем и как в них добавлять виртуальные функции? 2. Добавьте функцию Thing::backout(Thi ng*), которая возвращает класс к струк­ туре данных и набору виртуальных функций, действовавшим до последнего обновления.

З. Измените программу в приложении Д таким образом, чтобы уборка мусора выполнялась при каждом выделении памяти под новый объект. 4. Чтобы сократить простои приложения во время уборки мусора, из�ените ал­

горитм уборки мусора из приложения Д. Любой одиночный вызов Shape::gc должен инициировать уборку мусора только в одном классе, производном от ShapeRep. 5. Чтобы уборка мусора имела еще более распределенный характер, органи­ зуйте возврат управления из Shape: :gc в процессе пометки объектов. После­ дующие вызовы gc должны продолжать работу с того места, на котором она остановилась. 6. Повторите предыдущее упражнение так, чтобы в алгоритме Бейкера вместо фазы пометки прерывалась фаза уничтожения. 7. Объедините два предыдущих упражнения в единую стратегию. 8. Напишите систему управления памятью, в которой все классы одного разме­

ра используют один пул памяти и обрабатываются в одном цикле уборки му­ сора. Установления соответствия между классами и пулами должно происхо­ дить во время выполнения.

348

Глава 9. Эмуляция символических языков на

С++

9. Алmритм уборки мусора с разбивкой на поколения [5] поддерживает отдельные пулы памяти для объектов, классифицируемых по возрасту. Пулы с более но­ выми объектами обрабатываются чаще, чем пулы старых объектов, поскольку было замечено, что •молодые» объекты обычно уничтожаются с большей ве­ роятностью, чем старые. Напишите алгоритм уборки мусора с разбивкой на поколения, заменив единый пул памяти класса тремя пулами для разных по­ колений. Определите подходящий •возрасп объектов (измеряемый в коли­ честве циклов пометки/уничтожения) для каждого поколения. 10. Перепишите классы N u m ber из 5.5, используя конструкции символической идиомы. 1 1 . Напишите символическую версию класса String, основанную на версии String из 3.5.

Л и тература 1 . Ellis, Margaret А. , В . Stroustrup. addch ( x ) ; }

{ wi ndow- >addst r ( x ) ; } { wi ndow->cl ea r ( ) ; }

Edi tWi ndow( )

i f Сдля_среды_Х) wi ndow new XWi ndow : =

} el se { wi ndow

=

new CursesWi ndow :

} } pri vate : Wi ndow *Wi ndow : short topli ne . bottoml i ne : };

Данный прием хорошо известен как одно из обходных решений задачи динами­ ческого множественного наследования. Впрочем, у него есть свои тонкости: класс EditWi ndow остается производным от Window, поэтому функции редактора, знающие только о Window, смогут работать с EditWindow: c l ass Edi tWi ndow : puЫ i c Wi ndow { }; Wi ndow *screen new Edi tWi ndow : screen ->addch С · А · ) : =

Таким образом, мы имеем дело с простой разновидностью идиомы •конверт/ ПИСЬМО• .

Конечно, в этом случае структура иерархии определяется классом EditWi ndow. Добавление аргументов в конструктор EditWindow позволит создателю EditWi ndow передать дополнительную информацию об иерархии наследования: enum wType { Apol l o . Curses } ; Edi tWi ndow : : Edi tWi ndow (wType t ) swi tch ( t ) { case Apol l o :

1 0.2. П редостережение

353

wi ndow = new XWi ndow : brea k : case Curses : wi ndow = new CursesWi ndow : brea k :

Wi ndow *wi ndow = new Edi tWi ndow( Curses ) :

Остается последний штрих. Наследование должно быть реализовано так, чтобы конструкторы EditWindow фактически игнорировали конструктор базового клас­ са (Window). Если бы этот конструктор выполнялся, он мог бы создать окно на экране. Создание окна должно быть предоставлено классу EditWindow, который вызывает конструктор CursesWindow или XWindow и сохраняет полученное значе­ ние в указателе Window* в своих закрытых данных. Задача решается при помощи аргументов по умолчанию: Wi ndow : : Wi ndow ( bool doConstructor=true ) i f ( doConstructor ) {

Edi tWi ndow : : Edi tWi ndow( ) : Wi ndow( fa l se ) : Wi ndow Wi ndow

*ordi n a ryWi ndow = new Wi ndow : 1 1 То же . ч то " new Wi ndow(true ) " *edi torWi ndow = new Edi tWi ndow :

Если в иерархии имеются базовые классы выше уровня Window, возможно, в их конструкторы также придется добавить проверку особых случаев. Например, наследование всех классов от CLass при использовании идиомы прототипов (см. главу 8) потребует особых мер. Несмотря на команду if, конструктор CLass все равно будет вызван, но он, конечно, никак не помешает инициализации окон. Такие инициализации базовых классов обычно безвредны, хотя их стоит внима­ тельно проанализировать, чтобы избежать возможных сюрпризов.

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

354

Глава 1 0. Динамическое множественное наследование

Чтобы продемонстрировать ограниченность этой имитации, давайте снова вер­ немся к примеру Window. Мы выяснили, что цепочка наследования объекта мо­ жет задаваться при конструировании: Wi ndow *wi ndow

=

new Edi tWi ndow( Curses ) :

Нашу схему не удастся обобщить до такой степени, чтобы стала возможной сле­ дующая конструкция (если заранее не принять специальных мер): Wi ndow *wi ndow

=

new Curses ( Edi tWi ndowType ) ;

Возможно, такой порядок наследования окажется предпочтительным для обес­ печения другой иерархии замещения имен (например, чтобы функция Curses за­ мещала функцию EditWindow, а не наоборот). Но даже если эту схему удастся реализовать дополнительным программированием, со следующим примером де­ ло обстоит еще хуже: Wi ndow *wi ndow

=

new Wi ndow( Edi tWi ndowType ) :

Чтобы реализовать такую возможность, придется предоставить информацию о классе EditWindow клиенту, который собирался работать только с классом Window. Также стоит заметить, что программа может несколько усложниться, если класс участвует в нескольких отношениях наследования. К счастью, для пользовате­ лей классов эти сложности в большинстве остаются незаметными.

Глава 1 1

Си с т ем н ы е а с п е кт ы В предыдущих главах книги мы рассматривали механизмы, правила и идиомы применения объектов и классов как основных строительных блоков системы. Как было показано в главах 5 и 7, объектно-ориентированное программирование является полезным средством многократного использования кода и абстракции; оно естественным образом выражает принципы философии проектирования, опи­ санные в главе

6. Комбинация объектно-ориентированного программирования

с другими методами способствует созданию реализаций, отличающихся удобст­ вом управления, расширения и сопровождения. Вопрос смешанного использова­ ния парадигм на уровне реализации обсуждается в приложении А.

В этой главе тема объектно-ориентированного программирования рассматрива­ ется в более общей перспективе. Объектная парадигма хорошо согласуется со структурными аспектами больших систем, но в процессе конструирования сис­ темы приходятся учитывать множество других архитектурных аспектов, в том числе обработку исключений, планирование, пакетирование компонентов и управ­

ление сообществами объектов. Некоторые из этих тем будут рассмотрены позже. Поскольку настоящая глава даже вместе с главой

6 не в состоянии охватить весь

спектр проблем проектирования, основное внимание в ней уделяется тем облас­ тям, в которых язык программирования не подсказывает программисту, как

нужно действовать, или С++ предоставляет особые возможности для решения

проблем системного уровня. Глава не содержит особо глубокого или исчерпы­

вающего материала, а всего лишь служит оmравной точкой для описания конст­

рукций и приемов, специфических для конкретных проектов.

Системные конструкции, о которых идет речь, делятся на две категории: стати­ ческие и динамические. Статическая структура включает в себя отношения, представленные на диаграммах классов в главе

6, но к ней также причисляются

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

356

Глава 1 1 . Системные аспекты

Читатели, знакомые с реализациями операционных систем, могут рассматривать эту главу как •ответ объектной парадигмы операционным системам• в контексте проектирования встроенных систем. Во многих распространенных методах про­ ектирования за единицу абстракции принимается процесс; в объектно-ориен­ тированном проектировании такой единицей является объект. Процессы также являются единицей планирования; а что считать такой единицей в объектной парадигме? Операционная система предоставляет базовый сервис ввода-вывода, выделения ресурсов и обработки ошибок; кто должен предоставлять этот сервис в объектно-ориентированной программе? Некоторые из решений, описанных далее, могут использоваться в сочетании с традиционными системами или заме­ нять их в приложениях, предъявляющих особые требования к выделению ресур­ сов, синхронизации и сервису.

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

Однако при этом возникают проблемы с рекомендациями по проектированию, приведенными в главе 6. Допустим, мы проектируем программное обеспечение автопилота: в нем используются такие абстракции, как элероны, рули и двигате­ ли, а также операции с ними. Стоит ли объединить их в объект, представляющий самолет? Обратившись к стандартным правилам проектирования, мы спрашива­ ем себя, можно ли связать с таким объектом четкие, интуитивно понятные опе­ рации. Какими будут эти операции? Взлет, полет в крейсерском режиме, крен, повороты? С точки зрения системы управления полетом подобные операции не являются примитивными. Каждая из них требует десятков, сотен и тысяч взаимодействий между объектами системы, которые нелегко спроектировать как алгоритм или реализовать как функuию класса. Необходимы абстракции более высокого порядка. Из сказанного следует очень важный вывод - добавление одной функциональ­ ной возможности в систему редко соответствует добавлению одного программ­ ного компонента. Например, включение фигуры высшего пилотажа •бочка• в про­ грамму автопилота потребует внесения многочисленных изменений в реализацию текущей функциональности системы. Изменения функциональности способны нарушить любые парадигмы проектирования или реализации (исключения из этого правила встречаются в узкоспециализированных приложениях, исполь­ зующих особые языки и библиотеки, но в данном случае речь идет о больших, сложных системах). Если структурными единицами являются процедуры, боль­ шинство запросов конечного пользователя будет нарушать их границы. Объекты

1 1 . 1 . Статическая системная структура

357

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

Тр ан з а кц ионные диа граммы Хотя объектно-ориентированное проектирование уделяет основное внимание есте­ ственным •строительным блокам• систем, оно приучает нас рассматривать неко­ торые системы в контексте выполняемых ими функций, даже если эти функции не реализуются в виде примитивных операций. Для примера возьмем графиче­ ский редактор: в нем определены классы для представления прямоугольников, кругов и друrих фигур, а также линий, соединяющих фигуры. Чтобы перемещение линии приводило к перемещению соединяемых ей фигур, в редаКтор нужно вклю­ чить команду moveline. Другие линии следуют за фигурами. которые они соединяют.

Команда moveline должна поддерживаться соответствующими структурами данных и кодом функций объектов. Но где должен находиться код функuии? Перемеще­ ние линии требует активного взаимодействия между объектом Mouse, объектом Screen, объектами фигур и самим объектом линии Li ne. Таким образом, moveline является не столько четко определенной операuией над одним объектом, сколь­ ко результатом взаимодействия объектов, направленным на достижение общей цели. Такие сложные взаимодействия называются транзакция.ми. В [ 1 ] совокуп­ ность объектов, объединенных вместе для выполнения транзакции или набора взаимосвязанных транзакций, называется механизмом. Транзакция не является ответственностью одного конкретного объекта; она происходит в результате взаимодействия всех объектов механизма для выполнения системной функции. Многие операции в больших системах относятся к категории транзакций, и их удобнее рассматривать именно как транзакции, нежели как функции классов. Например, в области телефонии такие операции, как ожидание и перенаправ­ ление звонков и даже выписка счетов, должны оформляться не как объекты или их функции, а как транзакции. Рассмотрим другой пример из области авиации.

358

Глава 1 1 . Системные аспекты

Можно ли при анализе высокоуровневых транзакций ( таких как перенаправление звонка или приземление самолета) забыть об объектно-ориентированных мето­ дах? А если интерпретировать каждую транзакцию как процедуру с пошаговым уточнением (функциональной декомпозицией)? Объекты, характеризующие системные ресурсы, по-прежнему обеспечивают наилучшую инкапсуляцию в дол­ госрочной перспективе. Скорее всего, отказ от объектно-ориентированных методов и формирование системной структуры на базе транзакций приведет к ее разру­ шению со временем. Применение процедурного подхода распределяет информа­ цию о системе не там, где следовало бы; большинство процедур зависит от внут­ реннего строения структур данных, внешних по отношению к ним самим. Это усложняет понимание и эволюцию программы . При объектно-ориентированном подходе информация распределяется между транзакциями - но если проекти­ ровщик понимает, что должна делать транзакция, ему очевидно, какие объекты участвуют в ее выполнении, и где нужн о внести изменения. Для идентификации объектов и их взаимодействия в приложении применяются транзакционные диаграммы. Часто клиен т ы предпочи таю т работать с проек­ тировщиком на уровне деловых транзакций, слишком сложных для оформления в виде функций класса. Тем не менее, именно деловые транзакции в конечном счете реализуются системой при взаимодействии объектов через функции клас­ сов, и проектировщик должен воспроизвести это соответствие, отражая представ­ ления пользователя о функциональности системы. Транзакцион ные диаграммы описывают функциональность механизма в целом. Механизмом может быть как вся система, так и одна из подсистем. В данном случае термины •система• и •подсистема• обозначают части приложения, ав ­ тономные и доставляемые пользователю как независимое целое. Далее мы еще вернемся к подсистемам. Каждый столбец транзакционной диаграммы описывает транзакцию (или меха­ низм}, а КЮКJЩЯ строка соответствует объекту. Порядок столбцов и строк имеет зна­ чение: логически связанные функции и абстракции должны находиться в смежных позициях. Левый край может рассматриваться как логически прилегающий к пра­ вому краю; аналогичным образом связываются верх и н из. Перемещение слева направо часто соответствует перемещению по последовательным транзакциям в жизненном цикле системы; в частности, это относится к диаграмме на рис. 1 1 . 1 1 • Ячейки таблицы представляют функции, выполняемые объектом в ходе транзак­ ции. В таблице нас интересуют закономерности: группы сходных функций, при­ меняемых к взаимосвязанным объектам во взаимосвязанных транзакциях. Такие закономерности свидетельствуют о наличии сходных элементов в структуре объ­ ектов, которые могут бы ть вынесены в общие базовые классы . Например, можно заметить, что набор высоты сопровождается подъемом обоих рулей высоты; этот факт нав одит на мысль, что между ними имеется сходство. Классы рулей высоты можно объявить произв одными от одного базового класса Etevator и включить в них функцию ctimb для выполнения действий, необходимых для набора высоты. 1 Большое спасибо Нейлу Холлеру (Neil Haller) и Сьюзен Скоп (Suzan Scott), которые помогли при­ близить этот пример к реальности.

359

1 1 . 1 . Статическая системная структура

8зner



За1срыJuси Левый

зnерон Правый эперон

(Raise\ \_Raise)

Левый руль высоты

Правый

рупь высоты Руnь 1Щ1а1111е1111 Газ

набор нmы

Right

i�

Рис . 1 1 . 1 .



Креiiсерский режим

Поворот направо

Поворот наnево

Lower

Raise

Raise Raise Raise

LDNer

Right

l..efl

.... lrlaease ....--.......::.:

Снижение

Призем-

пение

CLDNer

� (LDNer\ � \_111М3у

1�

Decrease

Decrease

Транзакционная ди аграмма для п рограммы уп равл ения самолетом

Также нетрудно заметить, что при обоих поворотах (налево и направо) проис­ ходит прибавление газа . Можно предположить, что для обоих поворотов должна вызываться некоторая глобальная функция, логика выполнения которой являет­ ся общей для обеих транзакций. Эта функция не принадлежит одному конкрет­ ному объекту. Если взглянуть на вещи еще шире, можно найти еще более удачное решение. Поскольку левый и правый рули высоты всегда ведут себя одинаково, с точ­ ки зрения архитектуры их можно рассматривать как единое целое. Каждый руль высоты представлен отдельным объектом общего класса, а упоминавший­ ся ранее класс E Levator, выполнявший некоторые общие операции рулей высо­ ты, может выполнять их все. Две строки рулей высоты можно свернуть в одну строку. Некоторые функции (такие, как функция right руля направления, компенсирую­ щая вращение винта при взлете) группировке не подлежат. Их следует напря­ мую реализовать в виде функций классов, а не объединять с другими функция­ ми в транзакции. Следует подчеркнуть, что транзакционные диаграммы имеют неформальный ха­ рактер - это всего лишь инструмент для выделения закономерностей и сходных черт объектов. По принципу применения они напоминают карты КарнО - фор­ мальное средство, используемое в схемотехнике.

М одул и Структурной единицей программы является модуль. С++ поддерживает модули в той же степени, что и С - а именно за счет разделения кода на отдельные ис­ ходные файлы. Модуль может содержать как один класс, так и несколько клас­ сов; кроме того, он может экспортировать другие типы и константы.

360

Глава 1 1 . Системные аспекты

Модуль состоит из двух частей: символических имен, экспортируемых им для использования клиентами, и закрытой реализации. Экспортируемая часть модуля находится в заголовочном файле, обычно имеющем расширение . h или .hpp, а за­ крытая часть - в файле с исходным текстом программы, или просто в исходном файле (с расширением .с или . с р р ). Некоторые аспекты закрытой реализации также отражаются в заголовочном файле. Заголовочный файл состоит в основном из объявлений, обеспечивающих нормаль­ ную работу системы контроля типов и удобство записи. Как правило, в заголо­ вочные файлы выносятся объявления классов, используемых в модуле, и объяв­ ления глобальных переменных, определяемых внутри модуля, но доступных для его клиентов. В заголовочные файлы также выносятся другие сопутствующие определения типов, включая перечисляемые. Некоторые определения удобнее размещать в заголовочных файлах, особенно определения символических кон­ стант (как глобальных, так и действующих внутри класса). Исходный файл содержит определения функций классов модуля, глобальных функций и глобальных объектов. Содержимое исходного файла должно оставаться невидимым для клиентов мо­ дуля. Чтобы получить доступ к интерфейсам модуля, клиент включает заголо­ вочный файл директивой #i nctude. При этом возникает дилемма с подстановкой функций: подставляемые функции лучше всего размещать в отдельном исход­ ном файле с ключевым словом intine. Этот файл включается в конец заголовоч­ ного файла модуля, если функции должны подставляться, или компилируется отдельно, если подстановка не нужна. Например, для класса Stac k файл Stack.h может выглядеть так: #i fndef STACK Н #defi ne =STACK=H cl ass Stack { puЬl i c : i nt рор ( ) : }: #i fndef i nl i ne #i ncl ude " Stack l n l i nes . c " #endi f #endi f

Примерный вид файла StacklnLines.c: #i fndef -STACK I N L I NES-С #defi ne -STACK I N L I NES C #i ncl ude " Stack . h " i n l i ne i nt Stack : : рор( ) } #endi f

1 1 .1 .

Статическая системная структура

361

Наконец, файл Stack.c, содержащий не подставляемые функции Stack, выгля­ дит так: #i nc l ude " Stack . h " #i fdef i nl i ne #i nc l ude " Stack l n l i nes . c " #endi f Stack : : Stack О

Если теперь при компиляции системы определить макрос i n Line как пустую стро­ ку, каждая функция Stack будет сгенерирована в одном экземпляре (вместо мно­ жественных расширений на месте): СС -Di n l i ne='"' * . с

П одсистем ы Подсистемой называется набор модуi!еЙ, образующих независимый логический узел. Функциональность подсистемы должна быть достаточно широкой и в то же время достаточно общей, чтобы такая группировка приносила пользу. Напри­ мер, на проектирование и разработку подсистемы одна компания может заклю­ чить с другой компанией контракт.

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

ции класса; полученная абстракция ведет себя как подсистема. Такие функции обычно связаны друг с другом слабее, чем обычные (нестатические) функции, и не нуждаются в полиморфной интерпретации. Подсистема, как правило, не соответствует отдельному ресурсу, идентифицируемому как тип или сущность в объектно-ориентированном анализе. Скорее, она представляет синглетную сущ­ ность: в любую систему некоторая подсистема входит только один раз. Обыч­ но подсистема имеет более широкий концептуальный масштаб, обладает более

362

Глава 1 1 . Системные аспекты

разнородными внутренними состояниями и меньшей семантической связно­ стью, чем объект. Простым примером может послужить интерфейс операцион­ ной системы:

c l ass Operati ngSystem i nterface puЬl i c : stat i c i nt reboot ( ) : c l ass Proces s

}:

}:

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

Кар касы

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

сы-заготовки оставляют определение некоторых функций классов приложени­ ям. В качестве примеров объектно-ориентированных каркасов можно привести Х Toolkit, МасАрр и архитектуру MVC (Model-View-Controller модель, пред­ ставление, контроллер) в Smalltalk. -

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

1 1 . 1 . Статическая системная структура

363

которые определяют •контур• архитектуры и предоставляют пользователю воз­ можность подстроить семантику под конкретное приложение. При создании каркасов часто применяется одна полезная идиома: открытые функции абст­ ракций базовых классов почти не конкретизируются, а подробности реализации выносятся в виртуальные функции, подменяемые пользователем в производных классах. В результате каркас складывается из базовых классов, содержащих минимальное число открытых функций (листинг 1 1 . 1 ). Б ольшая часть откры­ тых операций выполняется на обобщенном уровне в базовом классе. Функции базовых классов обеспечивают логику вызова специфических функций, опреде­ ляемых в виде закрытых виртуальных функций в производных классах. Листинг 1 1 . 1

.

Каркас для форм

cl ass TaxFormF ramework { puЫ i c : TaxFormFramework ( i nt numЬerOfl i nes ) } voi d draw( ) { makeFi el d ( l oc l . taxpayer . name ( ) ) : makeFi e l d ( l oc2 . taxpayer . ss n ( ) ) : addFi el ds ( ) : pri vate : voi d ma keFi el d ( Poi nt . const char *const ) : 1 1 Произ водные классы должны содержа т ь 1 1 собст венную версию addFi el ds vi rtual voi d addFi e l ds ( ) О: Poi nt l oc l . l oc2 . . . . =

}:

c l a s s Schedu l eB : puЬl i c TaxFormFramework { puЫ i c : Schedu l eB ( ) : TaxFormFramework ( 14 ) { } pri vate : Poi nt l ocA . . . . voi d addFi el ds ( ) { 1 1 Доба вление строк для Schedul eB makeF i el d ( l ocA . пюrtgage . i ncome( ) ) : makeF i el d ( l ocB . other . i ncome ( ) ) :

}:

Каркасы часто пакетируются в библиотеки для последующеrо распространения.

364

Глава 1 1 . Системные аспекты

Библ и отеки

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

У каждой библиотеки имеется определенный набор правил или предположений, распространяющихся на все ее абстракции. Единые методы выделения памяти, стратегии обработки исключений и расширения классов (на базе шаблонов или производных классов) упрощают понимание и практическое использование биб­ лиотек. В идеальном случае все библиотеки, задействованные в проекте, прояв­ ляют подобную согласованность, но на практике этого добиться нелегко. Чтобы существующие библиотеки соответствовали локальным интерфейсным стандар­ там, для них можно построить набор простых интерфейсных классов. Согласова­ ние методов выделения памяти создает больше проблем, и многие библиотеки, не ориентированные на конкретные приложения, задействуют только средства выделения и освобождения памяти, описанные в спецификации языка. Если пользователь пожелает применить более экзотические средства ( например. ме­ тодику уборки мусора, описанную в главе 9), ему придется построить допол­ нительные классы на базе библиотечных классов. К счастью, большинство по­ добных преобразований выполняется без редактирования и перекомпиляции исходных файлов библиотек. Будьте особенно внимательны при создании зависимостей между библиотеками. Преобразования между взаимосвязанными библиотечными классами должны осу­ ществляться с учетом правил и рекомендаций, представленных в разделе 3.4 (см. с. 69). При создании библиотек шаблонов классов необходимо особо позабо­ титься о предотвращении дублирования кода (некоторые способы сокращения избыточности описаны в главе 7 на с. 261 ). В соответствии с этими рекомендациями библиотеки могут использоваться для пакетирования кода отдельных конкретных типов данных, наборов базовых классов или целых каркасов. Продолжая этот логический ряд, они также подхо­ дят для пакетирования и распространения кода подсистем. Однако подсистема

1 1 .2 . Динамическая системная структура

365

обычно включается в приложение в виде монолитного кода, поэтому библиотеки подсистем редко позволяют избирательно применять компоненты. Большинство рекомендаций по построению библиотек может рассматриваться как следствия принципов многократного использования кода, поэтому в этой области действуют все рекомендации из главы 7. Хороший стиль программиро­ вания и организации программного кода упрощает управление библиотеками; некоторые примеры приведены в ( 1 , 2) .

1 1 . 2 . Д инамическая системная стру ктур а Большинство методов проектирования ориентируется на статическую систем­ ную структуру. Динамические характеристики системы тоже учитываются, но полноценный анализ системной динамики требует обратной связи с прототипом программы или ранними вариантами реализации. Среди динамических аспектов следует выделить взаимодействие между отдельными экземплярами на стадии выполнения, соответствие дисциплин планирования системным ограничениям реального времени, а также распределенной обработки. Традиционно эти аспек­ ты относились к проблематике операционных систем, но их стоит рассмотреть заново в свете объектно-ориентированных принципов.

П л а н и рован ие Планирование является одним из важнейших факторов проектирования, посколь­ ку наш подход к модульности программ во многом определяется представления­ ми о процессах как единицах планирования. В языке С++ отсутствует прямая поддержка планирования. Впрочем, это вовсе не означает, что вопросы планиро­ вания чужды сообществу С++; изначально язык С++ создавался как язык с ими­ тацией событий по образцу Simula 67 и для поддержки диспетчеризации задей­ ствовал библиотеку задач. Во многих современных средах С++ продолжают использоваться потомки этой библиотеки. Прежде чем рассматривать тему планирования, необходимо сначала разобраться со смыслом некоторых терминов. Термины «процесс•, «мультиобработка• и «па­ раллельная обработка• встречаются часто, поэтому стоит разобраться, что они означают применительно к объектам. В этой главе мы будем понимать их сле­ дующим образом. Основная единица планирования, которая также может быть едини­ цей защиты, адресного пространства или пространства имен. Структурными единицами планирования обычно являются программы .

+ Процесс.

+ Мультиобработка.

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

366

Глава 1 1 . Системные аспе кты

называется средой с разделением времени. В таких средах полноценная асин­ хронность процессов отсутствует, хотя имитация асинхронности создает иллю­ зию параллелизма. +

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

Приложения существуют в невероятно широком спектре характеристик реаль­ ного времени и планирования. Не существует единственно правильного меха­ низма планирования, который подошел бы всем приложениям сразу. В языке было бы очень трудно реализовать поддержку мультиобработки, достаточно об­ щую для многих приложений, а узкоспециализированные модели планирования часто бывает трудно применить в конкретном приложении. Например, в одних приложениях может потребоваться мультиобработка без Параллелизма (скажем, при имитации автономных сущностей), а в других - передача управления с при­ оритетным прерыванием. Поддержка механизмов распространения тоже должна учитывать специфику конкретной сиrуации. В одних средах параллелизм реали­ зуется в низкоуровневых сетевых протоколах, в других он не поддерживается вообще, а в третьих используется реализация на базе общей памяти. Такое раз­ нообразие усложняет языковую поддержку и даже разработку идиоматических конструкций, которые бы обеспечивали единую, достаточно общую модель па­ раллелизма. В конкретных проектах либо усовершенствуется существующая схема, либо создаются совершенно новые подходы. Впрочем, языковая поддерж­ ка параллелизма все же существует; один из примеров приведен в [4]. В главе 6 было отмечено , что один из важнейших принципов проектирования, заложенных в основу объектной парадигмы, заключается в создании параллелей между струкrурой предметной области и струкrурой классов/объектов решения; мы назвали это свойство •принципом струкrурного изоморфизма•. • Струкrура• в обеих областях характеризуется набором взаимосвязанных аспектов поведения. Существует ли аналогичный базовый принцип для планирования? П рактический опьп (возможно, с долей самоанализа) подсказывает ответ - воз ­ можно. Если модель вычислений является неотъемлемой и четко идентифици­ руемой частью характеристик приложения, то в духе принципа струкrурного изоморфизма на вопрос можно ответить положительно. Для примера возьмем архитектуру объектно -ориентированной базы данных; объекты существуют в об ­ щей среде, где с ними работают несколько процессоров. Объекты являются еди­ ницами синхронизации, а в механизме планирования должны быть задействованы высокоуровневые средства, обеспечивающие целостность данных. Это хороший пример прямой связи параллелизма с решаемой задачей; параллелизм непосред­ ственно учитывается в постановке задачи.

1 1 .2. Динамическая системная структура

367

С другой стороны, в некоторых ситуациях задача может не быть параллельной по своей сути, но параллелизм логично вписывается в ее решение. Хорошим примером является пакет графического воспроизведения компьютерной анима­ ции или абстракция телефонного звонка в системе управления коммутируемой сетью: хотя •звонок• является единой концептуальной сущностью, одна его часть может обрабатываться одним процессором, находящимся вблизи от точки вызова, а другие части - другими процессорами, связанными с вызываемыми сторонами. В этом случае необходимость в распределении не только поднимает тему планирования, но и влияет на структуру решения. В большинстве приложений совершенно неважно, какая схема планирования ис­ пользована в их реализации, или на скольких процессорах они работают - од­ ном или нескольких. Планирование обычно относится к механизму реализации, а не к характеристикам самого приложения. Одни приложения параллельны по своей сущности; для других устанавливаются жесткие ограничения производи­ тельности, вынуждающие применять распределенную обработку. Взаимодейст­ вие с существующими программами или использование специализированных процессоров, обусловленное спецификой приложения (сигнальные и матема­ тические процессоры, графические ускорители), также могут ограничивать решение некоторыми формами планирования. Как правило, различия между спецификациями области решения и ограничениями предметной области имеют искусственный характер; ограничения предметной области неявно присутствуют в структуре задачи. Может ли каждый объект в объектно-ориентированной архитектуре рассмат­ риваться как процесс? Во многих средах мультиобработки затраты не пере­ ключение контекстов и передачу сообщений слишком велики, чтобы это стало возможным. Подумайте, чем обернется переключение контекста или отправка сообщения операционной системы объекту строки при поиске очередного сим­ вола (кстати говоря, то, что в Smalltalk называется сообщениями, имеет весьма отдаленное отношение к планированию процессов и не подразумевает никакой асинхронности). Более того, в большинстве приложений такое соответствие не оправдано с точки зрения семантики. Мультиобработка существует для того, чтобы создать иллюзию выполнения каждого процесса на отдельном процессоре; нет необходимости создавать такую иллюзию между строкой и ее клиентом.

С другой стороны , если в программе потребуется абстракция для процесса, мож­ но ли оформить ее в виде объекта Process? Все зависит от того, удастся ли спро­ ектировать правильные, семантически четкие функции для такой абстракции. Если вы собираетесь использовать •процессную• сторону этих объектов, то от­ вет будет положительным. Рассмотрим сильно распределенную среду с сотней процессов. Если процессы должны часто вступать в вычисления и выходить из них, тогда интерпретация единиц планирования как архитектурных компонен­ тов становится оправданной. Такие объекты могут быть производными от базо­ вого класса Process. Они называются актерами, потому что каждый из них, слов­ но артист в пьесе, играет свою роль независимо от других.

368

Глава 1 1 . Системные аспекты

Как упоминалось ранее, язык С++ не поддерживает актеров или других дисцип­ лин планирования; тем не менее, приложения С++ могут работать на базе сред, поддерживающих такие модели [5, 6]. Модель актеров близко воспроизводится в распространенной библиотеке задач С++, входящей в стандартную поставку многих сред С++. Далее приводится краткое описание, за более подробной ин­ формацией обращайтесь к [7]. Актеры объединяют в единую абстракцию единицы структурирования и плани­ рования. С одной стороны, их интерпретация 1J виде двух раздельных абстракций, не объединенных формальными связями, накладывает меньше оrраничений. Одним из недостатков архитектур, в основу которых закладываются процессы, является тесная связь единиц структурирования, адресного пространства и планирования. С другой стороны, структура планирования может не соответствовать структуре объектов; выполнение происходит в конечвых программн ых потоках (см. далее). Задачи С++

В популярной библиотеке С++ в качестве единиц планирования используются задачи, а общая модель достаточно близка к модели актеров. Объекты задач слу­ жат как единицами планирования , так и структурными единицами. Кроме того, они обеспечивают разбиение пространства имен - каждая задача имеет собст­ венную область видимости. Пакет задач С++ изначально проектировался для имитации дискретных событий, но он может применяться для любых схем, тре­ бующих невытесняющего планирования.

Ниже перечислен ы классы библиотеки, которые предоставляют поддержку мно­ гозадачности. tasl< Объекты классов, производньIХ от task, представляют активные ресурсы систе­ мы. Каждый объект ведет себя так, словно он обладает собственным счетчиком команд и работает на отдельном процессоре. Конструктор объекта task явля­ ется аналогом функции main. Как и процесс, объект задачи обладает собствен­ ным набором данных и отдельно планируется. Задача может временно приос­ тановить свое выполнение, заблокировав статус ресурса object (см. далее) или вызывая функцию deLay( n) для передачи управления процессору на n квантов. object Класс object служит базовым классом для представления пассивных ресурсов системы. Такие ресурсы находятся либо в состоянии готовности, либо в со­ стоянии ожидания, и объект task может заблокировать свое выполнение до изменения статуса одного или нескольких экземпляров object. Если задача ожидает изменения статуса нескольких ресурсов, она возобновляет выпол­ нение при переходе любого из ожидаемых ресурсов в состояние готовности. queue

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

1 1 .2. Динамическая системная структура

369

Хотя объекты task могут напрямую вызывать функции друг друга, такие вызовы осуществляются синхронно, что не соответствует семантике задач. В парадигме актеров взаимодействие между объектами обычно реализуется через интерфейсы сообщений, а получатель анализирует содержимое сообщения во время выполне­ ния, чтобы интерпретировать его семантику. Интерфейс одного объекта задачи недоступен напрямую для других объектов задач, поэтому сигнатуры классов не фигурируют в архитектуре системы, основанной на задачах. Виртуальные функ­ ции для объектов task практически бессмысленны: полиморфизм основан на ин­ терпретации (пользовательской) содержимого сообщения во время выполнения. Библиотека задач предоставляет в распоряжение программиста ограниченные средства синхронизации. Во-первых, полноценной синхронизации быть не мо­ жет, потому что все задачи работают на одном процессоре в одном программном потоке. Во-вторых (что более существенно), синхронизация операций пользова­ тельского уровня осуществляется при помощи механизмов блокировки в объек­ тах object и queue; подробности реализации скрываются от пользователя. В систему, использующую объекты задач, можно добавить схему приоритетной диспетчеризации. Например, запросы могут планироваться на основании при­ оритетов, ассоциируемых либо с запросами, либо с очередями, их содержащими. Приоритеты особенно полезны в системах с интенсивным вводом-выводом, где периферийным операциям назначается высокий приоритет для параллельного выполнения с текущими вычислениями.

ПРИМЕЧАНИ Е

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

П рограммные потоки

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

Проzраммн Ьtе потоки

На рис. 1 1 .2 показано, как осуществляется планирование потоков. Программный поток запускается по запросу. Предположим, запрос с пометкой 1 поступил от сообщения, сгенерированного где-то снаружи. Доставку всех запросов обеспечи­ вает объект-диспетчер, связывающий типы запросов с объектами, которые долж­ ны их получить, и вызываемыми функциями. Эта информация входит в боль­ шинство запросов и предоставляется объектом-отправителем; в этом смысле запросы напоминают сообщения или элементы очередей библиотеки task.

370

Глава

11.

Системные аспекты А

с

D

Е

Рис. 1 1 . 2 . Планирование п рограммных потоков для функций объектов

Диспетчер отправляет запрос 1 функции объекта А, которая начинает выпол­ няться. Эта функция вызывает функцию объекта В, которая в свою очередь вы­ зывает функцию объекта С. Функция объекта С выполняет свою работу и воз­ вращает управление. В конечном счете управление будет возвращено диспетчеру сразу же за точкой передачи управления А, и на этом работа потока завершается. Поток выполняется без прерЬtвания от начала до конца; его код не блокируется в ожидании ресурса и не уступает процессор вплоть до завершения. Чтобы гаран­ тировать завершение потока в течение заданного промежутка времени, можно воспользоваться системным таймером. Диспетчер запускает таймер в начале работы потока и останавливает его при завершении. Если таймер срабатывает в процессе выполнения потока, система решает, что текущий поток вышел из­ под контроля, и инициирует операцию восстановления.

В ходе выполнения поток может генерировать другие запросы. Когда при завер­ шении потока управление возвращается диспетчеру, последний запускает новый поток по запросу, сгенерированному предыдущим потоком. При отсутствии запросов диспетчер остается в пассивном состоянии до тех пор, пока поток не будет снова запущен по внешнему запросу. В ходе выполнения потока 1 может быть сгенерирован запрос 2, который запустит собственный программный поток. Благодаря отсутствию явной блокировки программные потоки существенно упрощают синхронизацию. Между потоками объект не находится в 4Частично обработанном• состоянии из-за того, что функция блокируется в ожидании ресурса. Планирование программных потоков естественно обобщается для распределен­ ных многопроцессорных сред. Любой поток работает на своем процессоре, а все межпроцессорные взаимодействия осуществляются при передаче запросов меж­ ду средами времени выполнения процессоров. Управление потоками может осуществляться на базе системы приоритетов - то­ гда диспетчер выбирает порядок обработки запросов на основании типа запроса и других подходящих критериев.

1 1 .2. Динамическая системная структура

37 1

ПРИМЕЧАНИ Е Используйте п рограммные потоки в приложениях реального времени, когда время отклика на происходящие соб ытия особ енно важно . Потоки та кже хорошо п одходят для распреде ­ ленных архитектур, в которых связ ывание объектов с процессорами производится на позд­ не й стади и п роектирования ( ил и является частью реализаци и ) .

Ко нте ксты Большинство нетривиальных проблем в программировании обычно сводится к именам и тому, что за ними стоит.

В

объектно-ориентированных архитекту­

рах имена многих классов и их функций являются глобальными, как и мани­ пуляторы многих системных объектов.

В

системах с десятками или сотнями

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

Контекст

описывает отно­

шения меЖду существующими, работающими объектами, что означает суще­

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

же причинам, по которым процессы считаются важными абстракциями во мно­ гих распространенных методах проектирования.

Сущности в большинстве приложений группируются в сообщества; другими словами, большинство объектов плотно взаимодействует лишь с небольшим подмножеством других объектов. Такие сообщества часто могут рассматривать­ ся как локальные группы, которые инициализируются и администрируются не­ зависимо от других групп. Одним из примеров такой группировки являются ие­ рархии объектов - инкапсуляция экземпляров классов внутри других объектов.

В

других случаях группировка объектов отражает абстракции, которые сами по

себе не могут быть нормально представленными в виде объектов, но и иерархии объектов не могут нормально отразить связи меЖду ними. Например, классы

Mouse, Menu, Keyboard, Screen

и

Window явно

связаны МеЖдУ собой, но их было бы

трудно объединить в рамках одной абстракции класса (хотя классы и

Mouse

можно сгруппировать в класс

Termi naL).

Screen, КеуЬоаrd

До определенной степени такие

сообщества являются подсистемами. Однако один объект может принадлежать нескольким сообществам; предположим, объект

Window

принадлежит сообществу

объектов, реализуюших графический редактор, но при этом он также может при­ надлежать диспетчеру окон. Объекты

TeLephone и CaLL могут представлять конкрет­

ную географическую точку; тем не менее, любой телефон может принадлежать одновременно двум таким сообществам, если он использует режим ожидания для задержки звонка на время разговора с другим абонентом. Ни 4большие объекты• , ни подсистемы не способны выразить такие отношения сколь-нибудь

372

Глава 1 1 . Системные аспекты

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

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

В заимодействие между пространствами и мен Распределенная среда должна обеспечивать общую поддержку взаимодействия между объектами в разных процессах и на разных процессорах. Если распреде­ ленные вычисления интегрируются с механизмами, заложенными в основу взаи­ модействий между объектами внутри процесса, то поддержка распределенности становится прозрачной для программиста. Затраты на переключение контекстов желательно свести к минимуму, для чего производится оптимизация удален­ НhlХ взаимодействий - вместо использования единого механизма диспетчери­ заuии с постоянными, хотя и высокими затратами. В этом разделе рассматрива­ ются идиомы С++, обеспечивающие прозрачную распределенность, а также ба.ианс между универсальностью и эффективностью. Снова о про цессах и объектах Н езависимо от того, что представляют главные абстракции архитектуры - про­ цессы или объекты - эти абстракции должны взаимодействовать друг с другом. Передача управления от одной абстракции другой (добровольная или принуди­ тельная) называется переключением контекста.

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

1 1 .2.

Ди намическая системная

структура

373

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

У обоих подходов есть свои достоинства и недостатки. Передача управления ме­ жду процессами может осуществляться в произвольном порядке, что упрощает реализапию приоритеmых схем диспетчеризации, не зависящих от порядка по­ ступления сообщений. Распределить процессы между процессорами проще, чем объекты; механизм вызова функций, используемый для взаимодействия между объектами, не расширяется на многопроцессорные конфигурации. За универ­ сальность приходится платить, и затраты на переключение контекстов и форма­ тирование сообщений могут оказаться весьма высокими. В объектно-ориентированном мире желательно вызывать функции напрямую, и нести затраты, связанные с использованием сообщений, лишь при необхо­ димости (для распределенной обработки). Кроме того, желательно, чтобы рас­ пределенный характер среды был как можно более прозрачным. О том, как это сделать, рассказано в следующем разделе. П розрачност ь По мере развития системы количество процессоров может увеличиваться, а объ­ екты будут переноситься с одного процессора на другой. Изменения в техноло­ гии или в требованиях задачи могут привести к необходимости перераспределе­ ния объектов между процессами или процессорами. Такие изменения возникают не только в ходе долгосрочного сопровождения, но и во время исходной реализа­ ции, когда проектировщик рассматривает и опробует различные варианты. Ис­ пользуя программные потоки и идиому •манипулятор/тело•, можно организо­ вать распределение объектов между процессорами с минимальными хлопотами.

Любое взаимодействие между парой объектов состоит из двух фаз: запроса и пе­ редачи. Каждый объект состоит из двух частей: маиипулятора и тела (см. раз­ дел 3.5). Чтобы обеспечить прозрачность распределенного характера среды, объ­ ект -манипулятор перехватывает сообщения и передает их своему телу. На одном процессоре манипулятор представляет собой простой класс конверта, а тело класс письма; эn1 два класса объединяются при помощи указателя (рис. 1 1 .3).

374

Гла ва 1 1 . Системные асп екты

Любой экземIUIЯр тела существует на одном процессоре, но он может иметь пред­ ставителей на нескольких процессорах, включая тот, на котором работает тело. Манипулятор, находящийся на одном процессоре с телом, обращается к телу напрямую как к классу письма (как в однопроцессорной системе). Удаленный доступ к объекту, как обычно, осуществляется через класс-манипулятор, но этот класс передает запросы не телу, а представителю.

Канал

связи

Рис . 1 1 . З . Классы-представители

Представитель пакетирует аргументы в сообщение и производит его прозрачную пересылку •настоящему• классу тела во внешнем пространстве имен. Например, представитель RemoteDiskRep пакетирует запросы read и write от объектов С и D и пересылает их объекту DiskRep на другом процессоре. Диспетчер операционной системы направляет сообщение в канал связи для передачи удаленному про­ цессору. Затем диспетчер запускает новый поток на передающем конце. На при­ емном конце операционная система доставляет сообщение объекту тела DiskRep. Доставка может осуществляться специальной функцией, которая декодирует сообщение и выполняет соответствующую операцию с DiskRep. Учтите, что организовать распределенное выполнение функций с возвращаемыми значениями нелегко, поскольку межпроцессорный вызов требует, чтобы между вызовом и возвратом проходила пауза в реальном времени. В среде планиро­ вания потоков не предусмотрен готовый механизм сохранения адреса возврата и других динамических данных потока (автоматических переменных) во время таких пауз. Важная особенность программных потоков состоит в том, что бОльшая часть информации о состоянии системы хранится не в стеке, а в данных объекта, что может упростить анализ ошибок и восстановление. Если возврат значений абсолютно необходим, его можно реализовать путем об­ ратного вызова. Генерируя запрос на обслуживание, инициатор запроса задает функцию обратного вызова в виде указателя на функцию, или функтора. Когда объект тела DiskRep завершает свою работу, он отправляет сообщение процессо­ ру, от которого исходил вызов. Сообщение доставляется объекту-представителю, который вызьшает функцию обратного вызова. Если объект класса манипулятора

1 1 .2. Ди намическая системная структура

375

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

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

В таких программах, как редакторы, компиляторы, игры и т. д., при возникнове­ нии ошибки может быть достаточно освободить ресурсы (закрыть открытые фай­ лы, освободить семафоры, изменить счетчики ссылок общих ресурсов и т. д.), вывести сообщение и завершить работу. Подобная •зачистка• обычно выполня­ ется автоматически в деструкторах объектов, поэтому она выполняется при стандартном уничтожении объектов в ходе обработки исключения. При выборе м одели обработки исключений необходимо ответить на важный во­ прос : что должно считаться единицей восстановления? В традиционных мини­ компьюте рных средах (и многих других) единицей восстановления является процесс, а исключения приводят к завершению или перезапуску процессов. По­ скольку для объектов более естественной дисциплиной планирования являются не процессы, а актеры или потоки, придется искать новую абстракцию восстанов­ ления. В одной из моделей обработки исключений единицами восстановления являются функции (функции классов, глобальные или статические функции). Такой механизм обработки исключений предлагается в качестве стандартного для языка С++ [8] . Так как обработка исключений еще не стала устоявшейся частью языка, а база для создания идиом еще не сформирована, в этой главе данная тема рассматри­ вается крайне поверхностно. Обычно механизмы и конструкции обработки ис­ ключений зависят от приложения, но общие познания в этой области подскажут вам некоторые полезные идеи, которые могут стать отправными точками для дальнейших исследований.

376

Глава 1 1 . Системные аспекты

При обработке исключений на базе функций основные проблемы возникают с уничтожением локальных переменных функций. Если исключение возникло при многократно вложенном вызове, необходимо уничтожить объекты для всех функций, для которых в стеке были созданы контексты вызова. Сначала нужно дать возможность восстановиться выполняемой функции, затем функции, кото­ рая вызвала ее, и т. д. - вплоть до верхнего уровня, чтобы ошибка не распростра­ нялась дальше. В схеме обработки исключений С++ все фрагменты кода, для которых должна активизироваться обработка исключений, выполняются в блоках try: voi d foo ( ) try { 1 1 Вызов ы . находящиес я в блоке t ry . 1 1 уч аствуют в обработке исключен и й . } 1 1 А в ызов ы . находящиеся вне блока . в 1 1 обработке исключений не участ вуют 1 1 ( если тол ь ко фун кция foo не была 11 выз вана из дру г о г о блока try ) .

Исключения генерируются средой времени выполнения (например, при делении на ноль) или в результате выполнения команды throw с выражением, опреде­ ляющuм тип инициируемого исключения. Исключения, сгенерированные в бло­ ке try или в функциях, вызванных из блока try, передают исключения обработчи­ ку. Обработчик исключения программируется в виде секции catch. В заголовке секции объявляется тип обрабатываемого исключения, а тело секции содержит команды, выполняемые при передаче управления обработчику. Простой пример: voi d foo ( ) { try { ba r ( ) : } catch ( const rhar *message ) { 1 1 Получ ает указа тел ь на параметр th row } catch ( const L i st &message ) { 1 1 Получ ает в ременную коп ию пара метра th row

voi d ba r ( ) throw ( L i st , const cha r* ) { Li st errorReport : th row errorReport :

/ / Вызывается первая секция catch

th row " Не 1 р ! :

1 1 Вызывается в тора я секция catch

"

1 1 .2.

Дина мическая системная структура

377

Обратите внимание: в интерфейсе функции bar указано, какие исключения она генерирует. Механизм обработки исключений не предусматривает продолжения выполнения с той точки, в которой произошло исключение. Описанная схема хорошо подходит для уничтожения локальных объектов функ­ ций, активных на момент возникновения исключения, а также для простых контейнерных классов, строк и других классов, входящих в библиотеки общего назначения. Но обработка исключений С++ не решает проблемы уничтожения объектов, созданных в куче. Кроме того, она не справляется с асинхронными или каскадными ошибками; эти проблемы решаются в области обработки сигналов. На практике сложные отказоустойчивые системы нуждаются в более глобальной стратегии восстановления. Во-первых, они должны справляться с событиями, которые обычно не ассоциируются с абстракциями языков программирования, например, ошибками контроля четности памяти и сбоями устройств. Такие ошибки асинхронны по отношению к •основному• программному потоку, а про­ цедура восстановления не связана с тем, что делает программа в момент их возникновения. Такая отказоустойчивость связана с событиями, происходящи­ ми �за кулисами•, и делится на субпарадиzматическое восстановление (для низкоуровневых прерываний вроде сбоев четности) и суперпарадиzматическое восстановление (восстановление на уровне контекста, процесса или процессора). Эмпирическое правило гласит, что восстановление в системах, работающих в не­ прерывном режиме, должно осуществляться на самом нижнем из доступных тех­ нологическом уровне. Во-вторых, если механизм try/throw/catch восстанавливает вложение вызовов функций (то есть смежные контексты вызовов в стеке), в некоторых ситуациях нужно, чтобы единицей восстановления был объект. Функции обработки ис­ ключений могут отложить повторную инициализацию на более высокие уровни. Поврежденный объект восстанавливается вызовом конструктора для сущест­ вующего объекта (см. 3. 7) или другим подходящим способом. В-третьих, обработка таких ошибок системного уровня, как нехватка памяти, должна производиться на общесистемном уровне. Схема восстановления мо­ жет освободить наименее критические ресурсы, чтобы обеспечить возможность функционирования системы в целом. Некоторые ресурсы можно вернуть, ис­ пользуя контрольные точки состояния системы. Для разных систем приходится задействовать разные методы восстановления - как и в случае с планированием, язык программирования не может предложить единого решения, подходящего для всех случаев.

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

378

Глава 1 1 . Системные аспекты

но это не единственный механизм, а ero эффективное применение в системах не­ прерывного режима работы требует глобальной стратегии восстановления. На момент написания книги обработка исключений поддерживается не всеми средами С++. Некоторые суррогатные решения описаны в [9] . Уборка мусора как средство повышения надежности Освобождение неиспользуемой памяти в сочетании с автоматизацией вызова деструкторов недоступных объектов является важной частью восстановления. Идиомы для реализации этих методов описаны в главе 9. Если система считает, что объект утратил логическую целостность, было бы сомнительно доверять де­ структору освобождение всех ero ресурсов. Обычно безопаснее просто возвра­ щать память таких объектов в пул; вызов деструкторов может вызвать каскад ошибок из-за появления недействительных или неопределенных указателей. Но в этом случае возникает опасность появления объектов, на которые не существу­ ет ни одной ссылки - именно эту проблему решают механизмы уборки мусора. Скорее всего, субпарадиrматическим обработчикам не удастся выполнить дейст­ вия, •положенные• для объектно-ориентированной парадигмы, поэтому они то­ же моrут просто освободить память объектов без их деинициализации. Скажем, процесс в системе с разделением времени, аварийно завершившийся из-за деле­ ния на ноль, может не закрывать все свои открытые файлы.

Механизм уборки мусора выполняет три операции, способствующие восстанов­ лению. + Освобождение памяти, связанной с недоступными объектами (то есть объек­

тами, на которые не существует ссылок). + Если механизм уборки мусора вызывает деструкторы для недоступных объ­

ектов, помимо памяти освобождаются другие ресурсы (файловые дескрипто­ ры, регистры виртуальной памяти, семафоры и т. д.).

+ Если механизм уборки мусора вызывает деструкторы, то объекты, ссылки на

которые присутствуют только в недоступных объектах, тоже освобождают­ ся, и т. д. - теоретически это может приводить к каскадному освобождению целых ветвей ресурсов. Если такие ресурсы не освобождаются, это может привести к постепенному ис­ тощению системных ресурсов и полному краху системы. Но, как и прежде, эту опасность следует сравнить с опасностью каскадных ошибок от вызова деструк­ торов для объектов, утративших логическую целостность. Уборка мусора обходится недешево, а ее реализация бывает утомительной и од­ нообразной. Тем не менее, она вполне подходит для кода С++, созданного гене­ раторами приложений, а также может использоваться при ручном кодировании в системах с повышенными требованиями к надежности. Контекст как единица восстановления При использовании программных потоков концепция процесса перестает суще­ ствовать, поэтому вы не сможете завершить и перезапустить процесс в рамках

Л и тер ату р а

379

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

Зо м би Даже в самых надежных схемах восстановления возможны непредвиденные со­ бытия. Что произойдет при попытке обращения к исчезнувшему объекту? Обыч­ но начинается полный хаос с каскадными ошибками, и отследить первопричину происходящего бывает нелегко. Проблему можно решить заменой уничтожен­ ных объектов обьектами-зомби с управляемым, отказоустойчивым поведением. Строение объекта-зомби зависит от среды С++, в которой вы работаете. В боль­ шинстве случаев зомби конструируются как списки указателей на таблицу вир­ туальных функций. Объект не содержит ничего, кроме этих указателей, причем все указатели ссылаются на одну и ту же таблицу. Эта таблица должна быть, по

крайней мере, не меньше самой большой таблицы виртуальных функций в сис­ теме, и в ней хранятся указатели на функцию восстановления. Объект-зомби мо­

жет находиться на месте любого другого объекта и направлять все вызовы своих

виртуальных функций на функцию восстановления. Объекты-зомби могут создаваться на месте удаляемых объектов системным опе­ ратором deLete или перегруженными операторами deLete отдельных классов. Функция восстановления, на которую ссылается таблица виртуальных функций, не делает ничего (пассивная операция), генерирует исключение (чтобы вызы­ вающая сторона знала о попытке разыменования • висячего• указателя) или предпринимает необходимые действия, чтобы система восстановления интер­ претировала текущий объект как недостоверный. Выбор зависит от системной политики восстановления. С вызовами невиртуальных функций классов через •висячие• указатели дело обстоит сложнее. В общем случае предотвратить их довольно сложно.

Л ите р атур а 1 . Booch, Grady. • Object-Oriented Design with Applications•. Redwood City, Calif.: Benjamin/Cummings, 1 99 1 .

М . and Gregory Bollela. •Managing С++ Libraries• . SIGPLAN 6 (June 1 989 ), 37.

2 . Coggins, James Notices 24,

3. Stroustrup, В . • The С++ Programming Language, 2nd ed . • Reading, M as s .: Addison-Wesley, 1 99 1 , ch. 1 2 . 4. Gehani, N . , and W. D. Roome. • Concurrent С Programming Language• , Summit, New Jersey: Silicon Press, 1 989.

380

Глава 1 1 . Системные аспекты

5. Agha, Gul А. •Actors : а model of concurrent computation in distributed systems• . Cambridge, Mass. : МIТ Press, 1 986. 6. Kafura, Dennis, and Keung Нае Lee. сАСТ++: Building а Concurrent С++ with Actors•, TR89- 1 8 , Blacksburg, Va .: Virginia Polytechnic Institute and State University, Department of Computer Science. 7. Shopiro, Jonathan Е. cExtending the С++ Task System for Real -Time Control• , Proceedings of the USENIX С++ Workshop, Santa Fe: USENIX Association PuЬlishers (November 1 987). 8. Ellis , Margaret А., and В. Stroustrup. •The Annotated С++ Reference Manual• , Reading, Mass.: Addison -Wesley, 1990, Chapter 15. 9. Miller, W. М . •Exception Handling Without Language Extensions•, Proceedings of the С++ Workshop, Denver: USENIX Association PuЬlishers (OctoЬer 1988).

П ри л ожение А С в с р еде С + + Язык С++ близок к С большая часть того, что мы видим в программах С++, позаимствовано непосредственно из С, а многие программы С требуют мини­ мальной доработки для превращения в программы С++ 1 • В этом приложении по­ казано, что нужно сделать для наделения программы С •стилем С + + • . Мы не пытаемся обучать читателя языку С. Если вам понадобится хороший учебник, обращайтесь к [2]. -

А. 1 . В ызовы функц ий В С++ появился новый уровень видимости, не поддерживавшийся в С. Он опре­ деляется новой языковой конструкцией, называемой классом и предназначенной для создания абстрактиы:х типов даииых, или пользовательских типов. Таким образом. если в С практически любое имя могло быть локальным или глобаль­ ным, в С++ имена могут быть локальными, глобальными или принадлежащими некоторому классу. Правда, в С переменные также могли находиться на разных уровнях вложенных фигурных скобок внутри функций, а имена в структурах могли повторять имена, встречающиеся в локальной и глобальной областях видимости. Тем не менее, в С этого нельзя было сказать о функциях: все функ­ ции были либо глобальными, либо статическими (то есть попросту •глобальны­ ми по отношению к исходному файлу•). В С++ функции еще могут объявляться внутри классов - тогда они называются фуикцuямu классов. Таким образом, если в программе существуют глобальная функция foo и класс F, содержащий функцию foo, эти две функции существуют независимо друг от друга. Говоря о них, для функции foo класса F мы используем обозначение F: :foo, а для гло­ бальной функции - обозначение : :foo (или просто foo). Допустим, в классе F присутствует конструкция вида foo ( ) : С первого взгляда трудно сказать, какую функцию хотел вызвать программист то ли функцию foo из класса F, то ли глобальную функцию foo. Возможно, для получения ответа на этот вопрос читателю программы придется проанализировать 1 Различия между С++ и С подробно обсуждаются

в

[ 1 ).

382

П р иложе н ие А. С в среде С++

код от точки вызова, выйти за пределы класса и в конечном итоге добраться до глобальной области видимости. Чтобы упростить чтение и понимание таких программ, можно воспользоваться синтаксисом, несколько проясняющим ситуа­ цию для программ С++ с классами. В этом синтаксисе глобальные функции вызываются с префиксом уточнения глобальной области видимости С++: : : foo ( ) :

Впрочем, это всего лишь условное обозначение, а не стандарт программирова­ ния. Оно не изменяет сгенерированный код (в данном примере) и не упрощает работу компилятора (при отсутствии неоднозначностей). Тем не менее, эта за­ пись помогает избежать недоразумений, поэтому она применяется в книге; чита­ телю также рекомендуется применять ее в своей работе.

А. 2 . П араметры функци й Стиль объявления параметров функций С++ несколько отличается от стиля классического языка С. Возможно. он покажется знакомым программистам, работающим на Паскале: Код С :

i nt ma i n ( a rgc . a rgv ) i nt a rgc : cha r *a rgv [ J : { } i nt func ( ) {

С++: i nt ma i n ( i nt a rgc . cha r *a rgv [ J ) {

Код

}

i nt func ( )

Объединенная синтаксическая форма, описьшающая интерфейс с функцией, слу­ жит двум целям. Во-первых, она начинает тело функции в ее определении. Во­ вторых, она используется как спецификация интерфейса функции. Эта специфика­ ция, называемая прототипом фун'/СЦии, должна предшествовать вызову функции.

А. З . П рототипы функци й Грамотно написанный исходный файл на С начинается с объявления функций, используемых в этом файле, даже если определения этих функций размеща ются в других файлах. Одни программисты включают эти объявления в каждую функцию, чтобы обозначить внешние функции, требующиеся данной функцией; другие собирают их в начале файла, третьи размещают все объявления в заголо­ вочном файле, включаемом в начало файла директивой #i ncLude.

А.З. П рототипы фун кций

383

В С++ объявления лучше всего размещать в заголовочных файлах. Если вы пре­ доставляете в распоряжение пользователя некоторую функцию или класс, обыч­ но при этом объявление функции или класса передается в заголовочном файле, имеющем расширение .h и находящемся в специальном каталоге, отведенном под заголовочные файлы. Пользователи вашей программы включают заголо­ вочный файл директивой #i ncLude и таким образом получают доступ к его функ­ циям с проверкой типов. Присутствие объявления в заголовочном файле не только помогает обеспечить безопасность типов, но и избавляет от необходимо­ сти заново объявлять функцию или класс при очередной ссылке. В большинстве сред программирования С++ все системные функции объяв­ ляются в заголовочном файле, поставляемом вместе с компилятором. Например, в операционной системе UNIX среда С++ содержит заголовочные файлы с объ­ явлениями функций ядра и библиотечными функциями, упоминаемыми в разде­ лах 2 и 3 руководства по UNIX. Чтобы получить доступ к объявлению функции, следует включить соответствующий заголовочный файл директивой #i ncLude. Если вы не знаете, какой именно файл должен включаться, обратитесь к адми­ нистратору или консультанту, или просмотрите файл самостоятельно. В •классическом» языке С фунюuш не объявляются перед использованием [2]; обычно компилятор не выдает предупреждения и генерирует код. Он предпола­ гает, что все необъявленные функции возвращают целые числа, а типы их пара­ метров точно соответствуют типам передаваемых аргументов. С другой стороны, С++ таких предположений не делает, а заставляет програм­ миста четко выразить свои намерения: все идентификаторы и функции должны объявляться перед использованием. Следующие примеры демонстрируют разли­ чия между классическим языком С и С++: Код С :

extern douЫ e myfunc ( ) : extern douЫ e cos < > :

Код С++ :

extern douЫ e myfunc ( i nt . char ) : extern '' С " douЫ e cos ( douЬl e ) :

В ANSI С используются те же правила. что и в коде С++ из приведенного при­ мера. но без указания спецификатора компоновки extern "С".

Обратите внимание: тип компоновки (то есть язык, на котором написана объяв­ ляемая функция - спецификатор extern "С" обозначает язык С) может явно ука­ зываться в объявлении. В общем случае он должен указываться для всех симво­ лических имен (как функций, так и данных) из кода, написанного на другом языке программирования, с которыми вы собираетесь работать в программе. В большинстве реализаций выбор компоновки С лля данных ни на что не влия­ ет, хотя в объектном коде имя, сгенерированное для функций с компоновкой С++, обычно отличается от имени той же функции с компоновкой С. Если тип компоновки не задан, предполагается компоновка С++ (как для myfunc). Явное указание типов параметров и возвращаемого значения полезно в двух от­ ношениях. Во-первых, оно гарантирует, что функция получит именно те типы аргументов, которые она ожидает получить, и не попытается обработать непред­ виденные данные. Во-вторых, компилятор получает информацию, достаточную для того, чтобы при необходимости преобразовать один тип объекта в дру.гой.

384

Приложение А. С в среде С++

Например. С++ умеет автоматически преобразовывать целые числа в веществен­ ные, что позволяет делать следующее: Код С :

extern douЫ e s i n ( ) : s i n ( l ) : /* Не определено */ si n ( ( douЬl e ) l ) : sin(2.0) : sin(З . 0 ) :

Код С++ :

extern douЫ e s i n ( douЬl e ) : s i n ( l ) : / / Автома тически s i n ( 2 ) : / / преобра зуется в douЫ e sin(З . 0 ) :

А. 4 . П ередача па раметров по с сыл ке Стандартная семантика передачи параметров в С (и С ++) требует создания копии каждого передаваемого параметра. Этот механизм называется передачей по зиа ­ чению. После выхода из функции временная копия уничтожается. Иногда в про­ граммах С в качестве параметра передается адрес переменной; естественно, что функция всегда сможет разыменоватъ параметр оператором *, чтобы прочитать или присвоить его значение. Таким образом, функция может изменять значение переменной, переданное вызывающей стороной. В С++ поддерживается механизм передачи по ссылке, снимающий необходимость в указателях и их явном разы­ меновании. Следующие функции incr возвращают старое значение своего аргу­ мента, увеличивая его текущее значение: Код С :

Код С++ :

i nt i nc r ( i ) i nt *i :

{

i nt i nc r ( i nt &i )

{

return (*i ) ++ :

i nt ma i n ( )

{

return i ++ :

i nt ma i n ( )

i nt i . j = 5 : i i nc r ( &j ) : /* j = 5 . j = 6 */ =

} {

i nt i . j 5: i i nc r ( j ) : 6 */ !* j = 5 . j =

=

=

В синтаксисе С++ int &i означает сСЬ1.Лку на переменную i. Формальный параметр функции (i) становится синонимом другого значения или объекта, переданного в качестве аргумента G). Имена i и j ссылаются на один объект; собственно, в этом и заключается смысл 4Передачи по ссылке•. В общем случае при создании ссы­ лочной переменной всегда можно указать уже существующий объект: i nt m i nt &n n 6: =

= =

5: m:

1 1 n означает m . а m означает n 1 1 И m . и n теперь ра вны 6

А.5. Перемен н ое количество параметров

385

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

А. 5 . Переменное количество параметро в Иногда требуется определить функцию, которая может получать любое количе­ ство аргументов любого типа. По контексту функция определяет, сколько пара­ метров ей было передано, и как их обрабатывать. Типичным примером служит функция pri ntf языка С. В этом разделе кратко показано, как объявить, вызвать и реализовать подобную фун кцию. Функция с неизвестным количеством аргументов неизвестных типов объяв­ ляется так: extern " С " i nt pri ntf ( . . . ) :

Многоточие ( . . . ) означает, что тип или количество параметров могут меняться, или они просто не известны на стадии компиляции. Эта конструкция обходит систему проверки типов, что позволяет при вызовах передавать произвольное число аргументов любых типов. В разновидности этой формы объявляются ти ­ пы любого фиксированного числа начальных параметров, за которыми следует неизвестное количество параметров неизвестных типов: extern " С " i nt pri ntf ( const cha r * . . . . )

:

Такие объявл ения обычно публикуются в заголовочных файлах, включаемых в приложения директивой #i nclude. Заголовочные файлы для примитивов опе­ рационной системы и библиотеки С входят в поставку большинства компиля­ торов С++. Функция с переменным количеством параметров вызывается в С ++ точно так же, как в С: #i nc l ude 1 1 В stdi o . h объ я вляется функция pri ntf i nt ma i n ( ) { pri ntf ( " hel l o worl d\ n " ) : 1: 1. j i nt i pri ntf( " %d pl us %d equa l s %d\ n " . i . j . i +j ) : return О : =

=

Функции с неизвестным количеством параметров неизвестных типов опреде­ ляются почти так же, как в С. Заголовочный файл stdarg.h, являющийся адапти ­ рованной версией файла varargs. h, позволяет писать программы с переменным количеством параметров независимым от платформы способом. В отличие от

386

Приложение А. С в среде С++

классического языка С, С++ требует, чтобы в объявление функции входил хотя бы один формальны й параметр с известным типом. Наличие параметра обеспе ­ чивает единственный гарантированно переносимый способ обращения к аргумен ­ там из тела вызываемой функции:

#i ncl ude #i ncl ude i nt pri ntf ( const cha r *pl , . . . ) { va_l i st ар : va_start ( ap . pl ) : swi tch ( fmt ) { case ' f ' : douЫ e d va_a rg ( ap . douЬl e ) : case ' s ' : char *stri ng va_a rg ( ap . cha r ) : case ' d ' : case ' с ' : i nt val va_a rg ( ap . i nt ) : =

=

=

} va_end ( a p ) :

1 1 Вызывается перед воз вратом

Применение стандартного пакета для работы с аргументами отличается от дру­

гих приемов, встречающихся в классических программах С и основанных на предположениях о структуре стека или физической смежности лексически смеж­

ных аргументов [2]. Единственно безопасный, заведомо переносимый путь рабо­

ты с фактическими аргументами, переданными функции с переменным числом

аргументов, основан на механизме stdarg.h.

Правила хорошего стиля программирования не рекомендуют использовать функции с переменным списком параметров, чтобы не ухудшать переносимость и удобочитаемость программы. Пакет stdargs обеспечивает максимальную пере­ носимость и удобство программирования, а объявление с многоточием удобнее записи, принятой в классическом языке С. Довольно часто stdargs удается заме­ нить перегрузкой функций и аргументами по умолчанию; программа упрощает­ ся и становится более безопасной по отношению к типам. За дополнительной информацией обращайтесь

к

описанию varargs или stdargs в электронной доку­

ментации вашей системы.

А. 6 . Указатели на функции Указатели н а функции в С++ работают практически так же, как в С . Един­

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

А. 6. Указатели на функции

Код С : voi d error ( p ) cha r *р : {

Код С++ : voi d error( const char *р ) {

} voi d < &efct ) ( ) : i nt ma i n O { efct &error : ( *efct ) ( " error" ) : efct ( " a l so cal l s erro r " ) : retu rn О :

} voi d ( *efct ) ( const char * ) : i nt ma i n ( ) { efct &error : ( *efct } ( ·· error" ) : efct ( " a l so ca l l s error" ) : return О :

=

387

=

Другой пример - вызов системной функции signaL В следующем фрагменте signaL объявляется как функция с двумя параметрами, int и SIG_PF (определяется в заголовочном файле). Сама функция просто возвращает указатель на функцию с параметром i nt: Код С : i nt efct2 ( ) :

Код С++ : i nt efct2 ( i nt ) :

typedef i nf ( *S I G_PF ) ( ) : S I G_PF fptr :

typedef i nf < *SIG_PF ) ( i nt ) : S I G_PF fpt r :

extern ( *s i gna l ( ) ) ( ) : fptr s i gna l ( 3 . &efct2 ) :

extern ( *s i gna l ( i nt . S I G_PF ) ) ( i nt ) : fptr s i gnal ( 3 . &efct2 ) :

=

=

Похожий пример, но с более простыми типами аргументов: Код С : extern ( *f( ) ) ( ) : ( *f( l . ' а ' . 123 ) ) ( 1 . 0 . 2 ) : ( *( f ( l . ' а ' . 123 ) ) ) ( 1 . 0 . 2 ) : fO . · а · . 123 ) ( 1 . 0 . 2 ) :

Код С++ : extern i nt ( *f ( i nt . cha r . l ong ) } ( douЫ e . i nt ) : ( *f( l . · а · . 123 ) ) ( 1 . О . 2 > : < *f( l . · а · . 1 23 ) ) ) ( 1 . О . 2 ) : fO . · а · . 123 ) 0 . 0 . 2 ) :

Указатели на функции часто объявляются в typedef. Вот как выглядит версия примера signaL с использованием определения типа: Код С : typedef i nt ( *S I G_PF ) ( ) : extern (*si gnal ( ) ) ( ) :

Код С++ : typedef i nt ( *S I G_P F ) ( i nt ) : extern S I G_PF si gna l ( i nt . S I G_PF ) :

Напоследок приведем более запутанный пример с typedef: Код С : typedef i nt ( *FP ) О : extern FP ( *f } ( ) ;

Код С++ : typedef i nt ( *F P ) ( d6uЫ e . i nt ) : extern FP f ( i nt . char . l ong ) :

f ( l . ' а ' . 123 ) ( 1 . 0 . 2 ) :

fO . ' а ' . 123 ) ( 1 0 . 2 ) : .

388

Приложение А . С в среде С++

А. 7 . М оди ф икатор const С++ позволяет преобразовать переменную в константу при помощи модификато­ ра типа const. Такие объявл ен ия заменяют препроцессорн ые директивы #define. М одификатор const дает компилятору возможность организовать проверку при­ сваиван ия указателям ( ил и тем объектам, на которые они указывают), или запи­ си в переменную после ее инициализации. Н иже приводятся три примера.

П ри м ер 1 . И спол ьз ован ие модифи катора const вместо ди рективы #define Директива С #define часто испол ьзуется ( или должна использоваться) для опре­ делен ия мнемонических, наглядных имен для констант. Обращение к констан­ там по имени помогает документировать их предполагаемую реализацию, а так­ же централ изовать определение и администрирование констант , неоднократн о встречающихся в программе. В С++ для определения констант вместо директи­ вы #define может указываться ключевое слово const. Вариант с const обеспечива­ ет более качественную проверку типов на стадии компиля ции, а в некоторых системах он также расширяет возможности символической отладки.

Код С : #defi ne CTRL Х ' \ 030 ' #defi ne msg -;;-a n error"

Код С++: const cha r CTRL_X ' \ 030 ' : const cha r *msg "an error " : =

В обоих вариантах :

i nt ma i n ( ) { char с :

i f (с CTRL Х ) { pri ntf( " % s \ n " --:- msg ) : return О : ==

.

.

.

}

Компилятор С++ гарантирует, что символические имена, определяемые подоб ­ ным образом, остаются неизмен ными; про грамма не сможет случайно изменить константу. Намеренное изменение кон стантной величи н ы потребует яв ного пре­ об разования (см. 2.9).

П ри м ер 2 . М оди ф и катор const и ука з ател и Указатели считаются •ан алогом команды goto в мире данных•. К сожален ию, указатели играют важную роль в программах С и С++; они позволяют эффек­ тивно органи зовать передачу бол ьших запи сей данных или массивов между функциями. И ногда программисту требуется предоставить пользователю доступ к дан ным через указател ь, но огран ичить его чтением. Ключ евое слово const и спользуется для управлен ия модификацией данных как непосредственно, так и через указатель. Возьмем следующий пример: char *const а = " exampl e 1 " :

А. 7. Модификатор const

389

Переменная-указател ь а объявляется константной; иначе говоря, ее содержимое не может б ыть изменено. Но в данном случае 4:Содержимое а • означает адрес не которо го бл ока памяти, в котором хранится последовательность символов, завершенная нулем. Таким образом, пр и веденное объявление означает, что мы не можем перевести указатель на другой блок памяти, но ничего не говорит об изменении тех данных, на которш он указывает: а = " exampl e 2 " : a[OJ = ' 2 ' :

1 1 Конnиля тор выдает сообщение об ошибке 11 Успешно создается строка " exampl e 2 "

В ыражение, которое может находиться в ле"вой части операции присваивания, называется /-значением. В данном случае а не является доп устимым 1-значением, тогда как а [8] им является.

Чтобы указ атель не мог использоваться как 1-значение для м одифи кац ии тех данных, на которые он ссылается, необходимо изменить объявление и объяви ть константными данные вместо указателя . Следующее объявление Ь делает содер­ жимое строки "example 2" неизменяемым, тогда как объявление с запрещает мо ­ дификацию как указателя, так и тех данных, на которые он ссылается. Символи­ ческое имя с может рассматриваться как неизменяемая константа: const char *Ь = " exampl e 2 " : const cha r *const с = " exampl e З " : Переменная а не является !-значением, но им является *а. Указатель Ь является ! -значением, а * Ь нет. Н аконец , с и * с не являются !-значениями. Обратите внимание: ключевое слово const ассоциируется с непосредственно следующим за ним объявлением: -

а = " ех4" : *а = ' Е ' : Ь = " ех4 " : *Ь = ' Е ' : с = " ех4" : *с = ' Е ' :

11 11 11 11 11 11

Нел ь з я Можно . а я вляется 1 - з начениен Можно . Ь я в ляетс я 1 - значениен Нел ь з я Нел ь з я Нел ь з я

П р и м е р 3 . Объя вл ен ие фун кций с ко н ста нтн ым и аргументам и

Чтобы константность объекта сохранялась при передаче управления между функ­ циями, можно добавить ключевое слово const в объявление формальных парамет­ ров. При поПЬIТКе передать константный арrумент функции, ожидающей получить неконстантный формальны й параметр, компилятор выдает сообщение об ошибке. Возьмем следующее объявление: extern char *strcpy ( char* . const char* ) ; Пусть программа содержит такие объявления: char a [20J : const char *Ь = " error message " : char *с = " user prompt> " ;

390

Приложение А. С в среде С++

Тоrда компилятор разрешит следующие вызовы:

: : st rcpy ( a . Ь ) : / / Копирование const в неконстантную п а н я т ь : : strcpy ( a . с ) : / / Параметр const char* может б ы т ь неконстантным : : strcpy ( c . Ы : / / Аналог : : strcpy C a . b l

Однако такой вызов невозможен:

: : strcpy ( b . с ) : / / Дл я неконстантного пара метра 11 нел ь з я переда в а т ь const cha r* Конечно, ограничение обходится явным преобразованием:

: : strcpy ( ( char * ) Ь . с ) :

/ / Теперь нормально

Но тоrда становится непонятно, зачем было объявлять Ь как const char*? Ключевое слово const играет важную роль в канонических формах, упоминав­ шихся в книrе.

А. В . Вз аим одейств и е с кодо м С Мноrие системы содержат низкоуровневые вставки или унаследованный код, кор­ ни которого теряются в истории проекта. Даже новые системы нередко содержат смесь кода С и С++, что объясняется историческими или социологическими при­ чинами. Поэтому в языке С++ была предусмотрена возможность взаимодействия с модулями, написанными на С. Настоящий раздел посвящен теме внешних связей с точки зрения С++: вы узнаете, как орrанизовать сосуществование С++ с други­ ми языками в одной программе. Сначала мы рассмотрим архитектурные аспек­ ты такоrо сосушествования, а затем синтаксис и проблемы администрирования.

А. 8 . 1 . Арх ите ктурн ы е а с пе кты Проблемы объединения кода С и С++ в одной программе не сводятся к синтак­ сису, механике общих объявлений и компоновки программ. Программист дол­ жен проанализировать С, С++ и друrие языки на пригодность для данной задачи и той роли, которую они сыграют в ее решении, а также на совместимость с инст­ рументарием проекта. Главной причиной для применения С++ является возможность выражения ар­ хитектурных концепций с помощью абстрактных типов данных и объектов. Код С редко отражает эти элементы стиля программирования; чаще он реализует функциональную декомпозицию или генерируется автоматически (например, в результате работы rенерирующеrо анализатора или САSЕ-системы). Таким образом, смесь кода С и С++ означает объединение минимум двух разных сти­ лей проектирования. Такая смесь часто кроется в истории развития продукта или в организационных недочетах. С++ может применяться для написания новоrо кода как в объектно-

А. 8 . Взаимодействие с кодом С

391

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

уасс). Проектировщик

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

+

Библиотеки обьектов в среде С. Допустим,

имеется система управления база-

ми данных (СУБД), написанная на С++ и ориентированная в основном на

приложения С++. Существующее приложение С требуется адаптировать для работы с этой СУБД. Программа С должна вызывать функuии С + + для объектов СУБД, причем код С и С++ использует общие структуры данных. • Основная» часть программы пишется на С, а объекты С++ применяются как пассивная библиотека серверных объектов.

+

Библиотеки С в среде С+ +.

На С++ написано великое множество программ -

как общедоступных, так и коммерческих. Построение программ

С++ на этой

базе значительно упрощается. Такие программы должны вызывать функции

С

и работать со структурами данных С (минимальный интерфейс между

С

и С++). Для примера можно привести программу С++, написанную на базе готовой графической библиотеки для рабочей станции или РС, или использо­ вание интерфейсов операционной системы из нового кода С++.

+

Инженерный анализ.

В некоторых ситуаuиях работающий код С переписыва­

ется на С++, чтобы задействовать преимущества проверки типов, структур­ ной организации программ и другие расширенные возможности. Фрагменты

готового кода С могут инкапсулироваться в классах С++ - либо с переводом кода С на С++, либо с предоставлением интерфейса для прозрачной работы с существующим кодом из абстракций С++. Многие существующие программы проектировались с использованием мето­ [4], анализа потока данных Й ордона [5] или других методов, по­

да Джексона

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

Далее описаны

две основные

категории методов перевода существующей программной базы на объектную парадигму.

+

Индуктивные методы. Начиная с конкретной информации о низкоуровневых компонентах существующей архитектуры (функциях, структурах данных и пе­

ременных), проектировщик выделяет комб:тшции этих компонентов, которые должны стать классами и объектами новой системы. Индуктивные методы делятся на управляемые данными и управляемые проце­ дурами. В методах, управляемых данными, структуры данных

(struct)

рассмат­

риваются как доминирующие архитектурные концепции, определяющие классы

(рис. А. 1 ) . Иерархии наследования формируются на основе сходства между

структурами данных. Функции, близко взаимодействующие с этими структура­

ми, становятся функциями соответствующих классов. Хотя этот подход часто

392

Приложение А. С в среде С++

приводит к разумной иерархии наследования, учтите, что выделение общих данных само по себе не определяет наследования. Гораздо более важную роль иrрают связи между семантиками базового и производного классов (глава 6). Часто бывает невозможно понять эти семантики, пока не будут идентифици­ рованы функции классов. Получение правильной структуры требует хороше­ го знания предметной области и последовательного уточнения посредством построения прототипов.

1---� ! - /i 1--.1 -� j( [ """"""" [

-- --- -

i

·

i

1 "7" 1 t

1 """"' 1

1 .,,.,..,, 1

Функция Функция Функция

+

Рис. А. 1 . Индукти вный метод формирования классов , управляемый данными

Дедуктивные методы. Дедуктивные методы начинают с абстракций и двига­

ются к конкретике, поэтому они больше подходят для реализации новых сис­ тем на основе спецификаций, нежели для эволюции существующих систем. Тем не менее, если система имеет четко определяемые абстракции высокого уровня, мы можем воспользоваться ими для построения структур классов и объектов, используемых в объектно-ориентированных системах (рис. А.2). Абстракции могут выявляться в процессе анализа ранней проектной докумен­ тации; вряд ли они обнаружатся в коде, написанном на языке, в котором ос­ новной абстракцией являются процедуры. Тем не менее, понимание архитек­ туры поможет проектировщику выявить фрагменты, обусловленные общей структурой. Возможно, готовые фрагменты удастся без изменений исполь­ зовать в новой структуре, которая сохранит и усилит исходные конструкции.

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

А.В. Взаимодействие с кодом С

393

Рис. А.2. Дедуктивный метод формирования классов с внутренним

пошаговым уточнением

О дн а парадигма может и с пользоватьс я для создан ия уточн енн ы х структур в рамках абстракци й другой парадигмы - скажем , фун кцион альн ая деком­ позиция может применяться в нутри кл ассов. В традиционной архитектуре встречается н езависимая группировка взаимосвязанных функций . Функции в верхн ей части таких иерархий могут оказаться хорошими кандидатам и на роль открЫТЬIХ функций класса или закрытых служебных функций , скрывае­ мых внутри класса для и спользования другими фун кциями того же класса. Н изкоуровн евые фун кции иерархии преоб р азуются в закрытые фу н кции класса или скрываются средствами структурн ого п рограммирован ия , опис ан ­ ными в приложен и и Е . У обоих вариантов имеется общи й недостаток: в н их делается попытка извлеч ь и нформацию проектирования из готово й реализации . Это примерн о то же самое, что пытать ся воссоздать трехмерное изоб ражен ие по ф отогр афии. Для реали­ заци й , изн ачально осн овывавшихся на объектн о-ориентированн ых методах или моделирован ии данн ых , подоб н ы й анализ с пособе н при н е сти плод ы , н о для реализаци й , о сн ованн ы х на процедур н о й декомпозиции , его польза в л учшем случае с ом н итель н а . В место того чтоб ы пытаться с троить архитектур ную мо ­ дель н а баз е кода, лучше попробуйте вернуться к исходн ым системным специ­ ф икациям и и с пользовать их для опред еления с и стемных кл асс ов. В полне возмож н о , что посл е этого существую щие компон е н ты реализации посл е не­ больших измен ени й удастся разместить в структуре классов . Н ет гарантий , что существующи й процедур ный код, получен ный в результате методики Йордона, удастся точн о ото бразить н а объектно-орие нтированны й код, а какая- то его ч асть может пережить тако й переход . М етодика с и стемного анализа Джек со­ н а неплохо подходит для ранних фаз объектн о - ориентированного п роекти рова­ н ия , а системы, построенные на базе этой модели , м огут уп ростить переход на объектную парадигму . И в се же методика Д жек сона не дает вразумитель н ых советов отн о с итель н о того , как ее аб стр акции могут и с пользовать ся в струк­ турах наследован ия.

394

П риложение А. С в с реде С++

Существующие фрагменты кода С могут инкапсулироваться в •модулях С++•. Такое решение не требует переделки архитектуры, это всего лишь способ упа­

ковки; тем не менее, оно предоставля ет удобный механизм для подключения ло­

гически сплоченного кода С к новому коду С++.

В общем случае переводить стабильно работающий код С на С++ не рекомен­ дуется: это не приносит сколько- нибудь существенной выгоды в долгосроч­ ной перспективе, хотя и требует немалых краткосрочных затрат. В крупных проектах одноразовое полное преобразование вообще неприемлемо, а за поша­ говое преобразование приходится расплачиваться наличием нескольких сред поддержки и усложнением интерфейса между преобразованным и непреобразо­ ванным кодом.

А. 8 . 2 . Я з ы ко ва я ко м п о нов ка С++ поддерживает перегрузку имен; иначе говоря, две функции могут иметь оди­ наковые имена, если нужная функция однозначно определяется по контексту вы­ зова (то есть по типу переданных параметров). Заголовочные файлы программы могут содержать несколько объявлений одноименных функций с разными пара­ метрами при условии согласования типов их возвращаемых значений. Для каждо­ го такого объявления в исходном файле С ++ определяется свое тело функции. В С++ 1 .2 иногда возникали неприятности из-за порядка следования директив #i ncLude в начале файла. Допустим, файлы cookie.h и cake.h содержат объявления функции print

cook i e . h : cake . h :

extern voi d pri nt ( Cook i e* ) : extern voi d pri nt ( Cake* ) :

Программист использует эти функции следующим образом:

/ / Констру кция пере г руз ки в С++ 1 . 2 overl oad pri nt : #i ncl ude " cooki e . h " #i ncl ude "cake . h " i nt fl ( ) { Cook i e с : Cake d : pri nt < &c ) : pri nt ( &d ) :

1 1 Вызывается функция с внутренн и м и менем pri nt 11 Вызывается функция с закодированным и менем

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

! / Конструкция пере г рузки в С++ 1 . 2 overl oad pri nt : #i ncl ude " cake . h " #i ncl ude " cook i e . h "

А.8. Взаи моде йствие с кодом С

395

i nt f2 0 { Cook i e с : Cake d : pri nt < &c ) : pri nt C &d ) :

/ / Вызывается фун кци я с закодированным и менем / / Вызывается функция с внутренн и м и менем pri nt

Порядок следования директив #incLude определяет вызываемую функцию! Одно из возможных решений - потребовать, чтобы все перегруженные объявления находились в одном файле #incLude. Для распространенных имен вроде pri nt, встречающихся во многих библиотеках, такое решение неразумно. Многие современные компиляторы С++ генерируют уникальные внутренние имена для всех функций, при этом типы аргументов шифруются в суффиксе, присоединяемом к имени функции. Ключевое слово overLoad стало пережитком прошлого. Но теперь возникает другая проблема: вызов read ассоциируется с внут­ ренним именем rеаd_нечто, где суффикс нечто определяется типом аргументов функции. Если функция read написана на С, все работает нормально. Но если программист просто хотел вызвать функцию read ( 2 ) , написанную на С, програм­ ма не пройдет компоновку. Для решения этой проблемы функции, написанные не на С++, должны поме­ чаться особым образом. Например, для вызова С-версии read следует использо­ вать объявление extern " С " i nt read( i nt . cha r* . i nt ) :

Объявление extern может относиться сразу extern " С " { i nt read ( i nt . cha r* . i nt ) : i nt open ( char* . i nt . i nt=O ) : i nt wri te ( i nt . char* . i nt ) :

I
push ( v ) : }

Интерфейсные функции формируют спроходы• между двумя разными парадиг­

мами. При их написании необходима осторожность, а наиболее успешно такие

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

А. 8 . 4 . С о вместно е исп ольз овани е за гол о в о ч н ых ф а йло в в С и С++ Объявления функций в заголовочных файлах сообщают информацию о типах аргументов и возвращаемого значения. Если разработчик и пользователь функции будут применять один заголовочный файл, включая его в программу директивой

1НncLude,

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

Было бы желательно, чтобы этот механизма работал с С-совместимыми функ­

циями как при вызове из других функций С, так и при вызове из кода С++. Рас­ смотрим объявления

ANSI

С:

extern cha r *foo ( short . l ong ) : extern voi d ba r ( cha r . const cha r* ) :

А. 8. Взаи модей ствие

с

397

кодом С

#defi ne s i ze 1 0 extern char *text [ S I ZE J : Эти объявления отличаются от классического синтаксиса С:

extern cha r *foo ( ) : extern voi d ba r ( ) : #defi ne s i ze 10 extern cha r *text [ S I ZE J :

В свою очередь, классический синтаксис С отличается от синтаксиса С++: extern "С" char *foo ( s hort . l ong ) : extern " С " voi d ba r ( char . const char* ) : const unsi gned char S I ZE 10 : extern cha r *text [ S I ZE J : =

Возможно ли использование одного общего заголовочного файла клиентами С и С++? В отдельных случаях удается выделить �наименьшее общее кратное• для всех трех языков. Например, спецификация конструкций SIZE и text С + + может использоваться в стиле С. С другой стороны, объявления в стиле С++ позволяют компилятору обеспечить более полную отладочную информацию или сгенерировать более эффективный код. Для объединения разных языко­ вых стилей в одном заголовочном файле лучше воспользоваться условной ком ­ пw�яцией , то есть препроцессорными макросами #if, #ifdef и другими директива­ ми этого семейства. Компиляторы ANSI С определяют препроцессорное символическое имя ST DC_; компиляторы С++ определяют имя cplusplus Если вы работаете только с ANSI С и С ++, решение может выглядеть примерно так: _

_

.

#i f _cpl uspl us extern "С" { #endi f extern char *foo ( s hort . l ong ) : extern voi d ba r ( cha r . const char* ) : #i f STDC #defi ne S I ZE 1 0 #endi f #i f _cp l uspl us const unsi gned char S I ZE 10 : #endi f extern cha r *text [ S I Z E J : =

#i f _cpl usp l us

}

#endi f

Результат не идеален: литерал 10 встречается в двух местах, что создает опасность десинхронизации кода С и С++ по мере эволюции программы. В следующем ре­ шении значение, представленное именем SIZE, изменяется только в одном месте:

398

Приложение А. С в среде С++

#i f _cpl uspl us extern ·· С '" { #endi f extern cha r *foo ( short . l ong ) : extern voi d ba r ( cha r . const char*) : #defi ne COММON_S I Z E 1 0 #i f _STDC_ #defi ne S I ZE C0tt10N -S I ZE #end i f #i f _cpl uspl us СОММОN-S I ZE : const unsi gned char S I ZE #endi f char *text [ S I ZE J : =

#i f _cpl uspl us } #endi f

Старайтесь выделять как можно большего общего текста, это способствует коор­ динации изменений. Другая распространенная ситуация - среда, в которой классический язык С и с­ пользуется совместно с С ++: #i f _cpl uspl us extern "С" { #endi f extern cha r *foo ( i f _cpl uspl us # s hort . l ong # end i f ): extern voi d ba r ( i f _cpl uspl us # cha r . const char* # endi f ): #defi ne COММON_S I Z E 1 0 #i f _cpl uspl us const uns i gned cha r S I ZE СОММОN-S I ZE : #el se # defi ne S I ZE СОММОN -S I ZE #endi f extern cha r *text [ S I ZE J : =

#i f _cpl uspl us } #endi f

А.В. Взаимодействие с кодом С

399

При сл иянии всех трех стилей файл усложняется: #i f _c p l uspl us extern С { #endi f extern char *foo ( i f _cpl uspl us l l _STDC_ # short . l ong # endi f ): extern voi d ba r ( # i f _cpl uspl us l l _STDC_ cha r . const cha r* # endi f ): "

"

#defi ne COMМON_S IZ E 10 #i f ! cpl uspl us # defi ne S I ZE СОММОN-S I ZE #el se const unsi gned char S I ZE #endi f

=

COMMON-S I ZE :

extern cha r *text[SIZEJ : #i f _cpl uspl us } #endi f

Как правило , чтение такого кода занимает на порядок больше времени , чем его написание. Возможно , чтобы избавиться от хлопот с параллельным сопровожде­ н ием, проще использовать одну директиву #ifdef для каждого языка и продубли­ ровать все содержимое. В отдель н ых случая х ед ины й общий синтаксис одинаково хоро шо подходит дл я ANSI С и С ++ . Н апример, сл едующая форма может использоваться в С++ и в ANSI С для объявления функций без параметров: тип_возврашаеного_значения

функция ( vоi d ) :

В программе ANSI С это объявление однозначно указывает на то , что функция вызывается без параметров. При отсутствии ключевого слова void компилятор ANSI С будет принимать вызовы функции с любым кол ичеством параметров. Хотя в С++ ключевое слово void н е обязательно, оно допустимо и обладает той же семантикой, что и в ANSI С. Другая форма, общая для обоих языков , может использоваться как основа для объявления символических констант: #i f _cpl uspl us extern С { "

"

400

П р иложе ние

#endi f stati c const тип иняl stati c const тип иня2 : #1 f _cpl uspl us } #endi f

А. С в с реде С ++

=

инициапиза тор :

Присутствие ключевых слов static ния синтаксиса двух языков.

и

extern играет ключевую роль для согласова­

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

А. 8 . 5 . И м порт ф ор матов дан н ых С в С++

Ранее рассказывалось, как организовать вызов функций С в С++ и наоборот. А как насчет данных? Если у вас имеется структура С, к которой нужно обра­ щаться из С++, нередко удается использовать один заголовочный файл с объяв­ лением структуры в обоих языках. Если объявление структуры соответствует синтаксису С, применение ее •экземпляров• в коде С и С++, объединенном в од­ ной программе, не создает никаких проблем.

А. 8 . 6 . И м порт ф ор матов дан н ых С++ в С Совместимость •снизу вверх• работает на нас при написании кода С++, исполь­ зующего существующие форматы данных С, но с обратным преобразованием дело обстоит сложнее. В общем случае нельзя добавить код С в существующее при­ ложение С++ и ожидать, что код С будет работать с форматами данных С++ понадобятся дополнительные преобразования. Если в программе С++ имеется простая конструкция struct языка С, не содержа­ функций, она может включаться в программы С и С++ и использоваться так, как описано выше. щая

В нетривиальных ситуациях применяются два основных решения. +

(Не рекомендуется ). Определите формат данных в классе С++ и продублируй­

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

те

А. 8 .

+

Взаимоде йств ие с кодом

С

40 1

Все обращения к данным производятся через специальные функции С++ с использованием приемов, упоминавшихся ранее.

В листинге А. 1 приведен пример кода С, который работает с полями класса EmpLoyeeRecord, написанного на С++. Листинr А. 1 . Работа с кодом С ++ из кода С

extern " С " const cha r *Empl oyeeRecordName < voi d* ) : extern " С " short Empl oyeeRecordYOS ( voi d*) : extern " С " douЫ e Empl oyeeRecordSa l a ry ( vo i d* ) : c l ass Empl oyeeRecord { fri end const cha r *Empl oyeeRecordName ( voi d* ) : fri end short Empl oyeeRecordYOS ( voi d* ) : fri end douЫ e Empl oyeeRecordSa l a ry ( voi d* ) : puЬl i c : Empl oyeeRecord ( const char* . short . douЬl e ) : Empl oyeeRecord ( ) : voi d gi veRa i s e ( douЬl e ) : vi rtual voi d di sbursePay ( ) : pri vate : char name [ NAМE S I ZE J : short yea rsOfSe rvi ce : douЫ e monthl ySa l a ry : }: extern " С " { voi d aCFunct i on ( voi d *RecordPtr ) : const cha r *Empl oyeeRecordName ( voi d *RecordPt r ) return ( ( Empl oyeeRecord * ) RecordPt r ) ->name : short Empl oyeeRecordYOS ( voi d *RecordPtr ) { return < < Empl oyeeRecord *) RecordPt r ) ->yea rsOfServi ce : douЫ e Empl oyeeRecordSa l a ry ( voi d *RecordPt r ) { return < < Empl oyeeRecord * ) RecordPt r ) - >month l ySa l a ry :

Empl oyeeRecord aRecord : aCFuncti on ( &aRecord ) : / / Вызов функции в п риложении С 1 1 с передачей указателя на данные С++

Код С получает указатель void * и использует ствляющих разыменование: extern char *Empl oyeeRecordName < voi d* ) : extern short *Empl oyeeRecordYOS ( voi d* ) :

ero

в параметрах функций, осуще ­

402

Приложение

А. С в

с реде

С+ +

extern douЫ e Empl oyeeRecordSa l a ry ( voi d* ) : voi d* aCFunct i on ( vo i d *RecordPtr ) { char *name Rmpl oyeeRecordName < RecordPt r ) : short YOS Empl oyeeRecordYOS ( RecordPt r ) : douЫ e sa l a ry Empl oyeeRecordSa l a ry ( RecordPt r ) : =

=

=

Упр ажн е н и я 1 . Напишите упрощенную версию функции printf, обрабатывающую только фор­ маты °lod и °loS. 2. Н апишите функцию быстрой сортировки qsort со следующим интерфейсом: voi d qsort ( vo1 d *base . i nt nel . i nt el ementS i ze . i nt ( *compa r ) ( voi d* . voi d* ) ) :

Здесь base - указатель на базовый элемент сортируемой таблицы; ne1 коли­ чество элементов в таблице; eLementSize - размер одного элемента; compar указатель на функцию, которая сравнивает два элемента и возвращает целое число, меньшее, равное или большее нуля, если перв ый арrумент соответст­ венно меньше, равен или больше второго арrумента. Можете воспользоваться исходным текстом любой существующей функции сортировки. 3. Напишите объявл ения функций UNI X pri ntf, maLLoc, read, write, pipe, open, c Lose и sig n a L. -

-

Л итератур а 1 . Koenig, А. and В. Stroustrup. 4С++: As Close as Possihle to С С++ Report 1 ,7 Quly/August 1989).

-

But No Closer•,

2. Kernighan, Brian W., and Dennis М. Ritchie. 4The С Programming Language•, 2nd ed. New York: Prentice-Hall, 1988.

3. DeMarco, Т. •Structured Analysis and System Specification•, New York: Yourdon Press, 1978. 4. jackson, М. А. • Principles of Program Design•. London: Academic Press, 1 975.

П ри л ожение Б П р о г ра м м а Shapes #i ncl ude " Shapes . h " /*- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -*/ */ /* Fi l e ma i n . c - - упра вляюща я nро г ранна для nринера Shapes

/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -*/

const i nt XSI ZE const i nt YS I ZE

= =

800 : 1024 :

voi d rotatel i st ( Sh ape *s . Ang l e а ) { for < Shape *f s : f : f f->rotate ( a ) : =

=

f - >next ( ) )

i nt

mai n ( )

{

Coordi nate

ori gi n ( 0 , 0 ) ;

/* * wi ndowВorder не тол ь ко оn редел яет г раницы * з а г оловком сnиска фи гур */

окн а , но

Rect wi ndowВorde r ( o ri gi n . XS I ZE . YS I ZE ) ; Shape * t new Tri a ng l e ( Coordi nate< 100 . 100 ) . Coord i nate ( 200 . 200 ) . Coordi nate ( ЗOO . 100 ) ); wi ndowВorder . append ( t ) ; =

Shape * с = new Ci rc l e ( Coordi nate ( 500 . 652 ) . 150 ) : t - >i nsert ( c ) :

и служит

404

П р ил ожение

Б.

П рограмм а Shapes

rotateli st ( &wi ndowBorder . 90 . 0 ) : return О : #i ncl ude #i ncl ude /* * Файл Shapes . h * Автор : Дж . Коплин * * За головоч ный фа йл содержит объ я влени я классов . испол ь зуемых * в демонстрационной библиотеке Shape . Мно г ие функции классов * определ яютс я подставляемыми . */ /*- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ */ /* Класс Ang l e представл яет угол в градусах или радианах . /* Он оформлен как класс . что поз вол яет ле г ко адаптировать */ */ /* е г о для систем . работающих как в градусах . так и */ /* в радианах . а та кже и з менить математическую точ ность */ /* предста влени я ( fl oat . douЫ e или i nt ) . /*- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ c l ass Angl e { fri end douЫ e cos ( Angl e > : fri end douЫ e s i n ( Angl e ) : puЬl i c : Angl e ( douЫ e d ) { radi ans d: } { radi ans = О : } Angl e О Ang l e ( const Ang l e& а ) { radi ans = a . radi ans : i nl i ne Angl e &operator=< const Angl e& а > radi ans a . radi ans : return *thi s : } Angl e operator+ ( const Angl e& > const : pri vate : radi ans : douЫ e }: =

=

/*- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ /* Класс Coordi nate описывает точ ку на декартовой плоскости . */ /* Значен и я координат мо гут задаваться в м икрометрах . */ /* п и кселах . дюй мах или в любых дру г их удобных еди н и цах . */ /*- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ cl ass Coordi nate { puЬl i c : i nl i ne l ong х ( ) const

{ return x_rep : }

П ро гр амм а Shapes

i nl i ne l ong у ( ) const { return y_rep : } Coord i nate &operator= ( const Coord i nate &с ) x_rep c . x_rep : y_rep c . y_rep : return *th i s : } Coord i nate ( l ong х . l ong у ) x_rep х : y_rep = у : } Coordi nate ( const Coordi nate &с > x_rep c . x_rep : y_rep c . y_rep : } Coordi nate ( ) { x_rep = y_rep О:} -Coord i nate ( ) { } =

=

=

=

=

=

voi d rotateAbout ( Coord i nate . Angl e ) : pri vate : l ong x_rep . y_rep : }; /*- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ /* Следующие функции перег ружают обыч ные три гонометрические */ */ /* фун кции таким образом . ч тобы они могли вызываться */ /* с аргументом Angl e . Поскол ь ку пере г руз ка не может /* прои з водиться тол ь ко на базе возвращаем о г о з начения */ */ /* ( об я з а тел ьно должен и з ме н я т ьс я тип аргументов ) . /* пере г рузка функци и atan . котора я должна умет ь воз вращать */ */ /* объект типа Angl e . невоз можна . Ее приходится определ я т ь */ /* как новую фун кцию . /*- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ { return : : cos ( a . radi ans ) : i n l i ne douЫ e cos ( Ang l e а ) i nl i ne douЫ e s i n ( Ang l e а ) { return : : s i n ( a . radi ans ) : i n l i ne Angl e Angl eata n ( douЫ e t ) { Angl e a ( : : atan ( t ) ) : return а : /*- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ */ !* Класс Col or и н капсулирует реализацию кодировки цветов */ /* для испол ь зуемо г о г рафическо г о па кета . Его в нутренние /* данные доступн ы толь ко для функций низ коуровневого */ */ !* и нтерфейса . то г да как высокоуровневые функции работают !* на более общем уровне . Такой подход делает воз можным */ */ !* адаптацию про гра м м ы для широко г о спектра пла тформ . */ !* F l a s h i ng ( ми г ание > - атрибут цвета . Часто испол ь зуемые */ /* константы Bl ack и Whi te объ я влены в виде глобал ьных */ / * констан тных объектов типа Col or . /*- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */

405

406

Приложение

Б.

Про грамма Shapes

enum Fl ash { Fl ashBl ack . Fl a s hWh i te . F l ashOff } ; c l ass Col or puЫ i c : i nl i ne i nl i ne i nl i ne

red ( ) { return red_rep : } douЫ e { return green_rep : } g reen ( ) douЫ e Ы uе ( ) { return Ьl ue_rep ; } douЫ e Col o r ( douЫ e r=O . O . douЫ е g=O . О . douЫ e Ь=О . О . F l ash f=F l ashOff ) { red_rep = r : green_rep = g : Ы ue_rep = Ь : fl a s h i ng = f :

} i nl i ne Col or& operator= C const Col or& с ) red_rep = c . red_rep ; g reen rep=c . green-rep : Ы uе rep = c . Ы ue rep : return *th i s : -

-Col o r ( ) pri vate : douЫ e Fl ash }:

{ }

red rep . g reen_rep . Ы ue_rep : fl a shi ng :

const Col or B l ack . Whi te = Col o r ( l . O . 1 . 0 . 1 . 0 ) : /*- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -*/ */ /* Shape - базовый класс для всех геометрических фи гур . !* Он определяет си г на туру ( совокупност ь выполняемых */ */ /* опера ци й ) . а та кже п редоставляет фа ктическую реали з а цию */ /* опера ци й . общих дл я всех фи гур ( например . операц и я /* перемещения move я вл яется общей с отдельными и сключени я ми ) . * / /*- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ c l ass Shape { puЫ i c : 1 1 Поворот относител ь но центра { } rotate (Angl е а ) voi d v i rtua 1 moveC Coord i nate ху ) voi d v i rtua l erase( ) : center_v a l =xy : draw( ) : } voi d 1 1 Прорисовка draw( ) = О : v i rtu a l voi d erase ( ) { vi rtua 1 col or = B l ack : draw( ) : v i rtua l voi d redraw( ) era s e ( ) : draw( ) ;

П ро гра мм а Shapes

}

Shape ( > : vi rtual -Shape ( ) : vi rtual Coordi nate ori gi n ( ) const { return center_va l : } vi rtual Coordi nate center ( ) const { return center_va l : } i nl i ne Shape *next ( ) const { return next poi nter : } i nl i ne Shape *prev ( ) const { return prev=poi nter : } i nl i ne voi d i nsert ( Shape* i > { i - >next_poi nter= thi s : i ->prev_poi nter=prev_poi nter : prev_poi nter- >next_poi nter=i : prev_poi nter = i :

}

; nl i ne

voi d

append ( Shape* i ) { i ->next_poi nter=next ( ) : i - >prev_poi nter = thi s : next_poi nter ->prev_poi nter=i : next_poi nter = i :

protected : /* * Следующие пол я объ я влены защищенными . а не за крытыми . * ч тобы они были доступны для фун кций прои з водных классов */ Coordi nate Col or

center_val : col or :

1 1 Ном и нальный центр 1 1 Цвет фи гуры

/* * Следующие статические переменные класса испол ь зуются * ба зовым па кетом окон ного интерфейса ( на пример , Х ) * для общей настройки г рафи к и . */ stat i c stat i c stati c

Di spl ay Wi ndow GContext

pri vate : Shape

*next_poi nter :

Shape

*prev_poi nter :

}:

di spl ay : wi ndow : gc : 11 11 11 11

Ука затель на следующую фи гуру в списке Указ ател ь на предыдущую фи гуру в списке

/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -*/ /* Li ne - - особа я ра з нов идность фи гур . котора я обычно */ /* испол ь зуетс я для прорисовки дру г и х фи гур . Из всех фи гур */ /* тол ь ко она содержит функцию rotateAЬout . испол ь зуемую */

407

408

Приложе ние

Б.

Программа Shapes

/* как при м и т и в дл я вращен и я дру г и х фи гур . Опера ция rotate */ /* я вляется вырожденной формой rotateAbout . Центр отрезка */ */ /* определ яется как сред R я я точ ка между е г о конца м и . */ /* началом счи тается перва я и з зада нных конеч ных точек . /*- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -*/ cl ass L i ne : puЬl i c Shape { puЬl i c : voi d rotate ( Angl e а ) { rotateAbout ( center ( ) . a ) : voi d rotateAbout ( Coordi nate . Angl e ) : voi d draw( ) : i nl i ne Li ne &operator= ( const Li ne &1 > { ( voi d ) thi s ->Shape : : operator= ( *thi s ) : p=Coordi nate ( l . р ) : q=Coordi nate ( l . q ) : return *th i s : } L i ne ( Coordi nate . Coordi nate ) : Li ne ( Li ne& 1 ) { р = l .p: q l .q: center_v a l = l . center_va l : } L i ne( ) { р = Coordi nate ( 0 . 0 ) : q = Coordi nate ( 0 . 0 ) : } -L i ne ( ) { erase( ) : } i nl i ne Coordi nate е О const { return q : i n l i ne Coordi nate ori gi n ( ) const { return р : pri vate : Coordi nate 1 1 Конеч ные точки отрезка р, q: }: =

/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ /* Прямоу г ол ь н и к ( Rect ) состоит и з четырех отрез ков . Началом */ /* прямоу г ол ь н и ка считается его левый верхни й у г ол . зада в аемый */ /* первым параметром конструктора . а его центром считается */ /* геометри ческий центр . Операция erase наследуется от Shape . */ /* Стороны объ я влены защищенным и . а не за крытыми . ч тобы они */ */ /* могли использоват ься прои з водным классом Squa re . /*- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -*/ c l ass Rect puЫ i c : voi d voi d voi d

puЫ i c Shape {

rotate ( Angl e ) : d raw( ) : move < Coordi nate ) : 1 1 Левый верхни й у гол . ширина . в ысота Rect ( Coordi nate . l ong , l ong ) : { erase ( ) : } -Rect ( ) Coordi nate i n l i ne ori gi n ( ) const { return 1 1 . or i g i n ( ) : protected : Li ne 1 1 . 12. 13. 14: }:

Программа Shapes

/*- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ /* Центрон эллипса ( E l l i ps e ) я вл я ется е г о г еонетрический */ */ /* центр : эта же точ ка считается началом фи гуры . */ /* Класс E l l i pse не реализован . /*- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ cl ass El l i pse рuЫ i с Shape { puЬl i c : voi d rotate ( Angl e ) : draw( ) : voi d 1 1 Центр . больша я ось . налая ось E l 1 i pse< Coord i nate . l ong . l ong ) : { erase ( ) : } -El l i ps e ( ) protected : l ong major . mi nor : }: / *- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ /* Треу г ол ь н и к ( Tri angl e ) состоит из трех отрезков . */ */ /* Е го центр выч исл яется как сред н я я точ ка нежду */ /* трен я верwинани . /*- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -*/ c l a s s Tri angl e : puЫ i c Shape { puЬl i c : voi d rotate(Angl е ) : d raw( ) : voi d voi d move ( Coordi nate ) : Tri angl e ( Coordi nate . Coordi nate . Coordi nate ) : { erase ( ) : } -Tr i angl e ( ) pri vate : L i ne 11. 12. 13: }: /*- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -*/ /* Квадрат ( Squa re ) представляет собой вырожденный */ /* пря ноу гол ь н и к . Все его функции наследуются от Rect : */ / * тол ь ко конструктор и неет собст венную реализацию . котора я */ */ /* ограничи вается простын вызовон конструктора Rect . /*- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ c l ass Squa re : puЬl i c Rect { puЬl i c : Square ( Coordi nate ctr . l ong х ) : Rect ( ct r . х . х ) { } }: /*- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ */ /* /* Файл Shapes . c содержит принерную реализацию Shape */ */ /* /*- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -*/

409

41 О

П риложение Б. П рограмма Shapes

extern С { extern voi d doX I n i t i a l i zati on ( Shape& > : extern voi d XOrawL i ne < Di sp 1 ау . Wi ndow . GContext . i nt . i nt . i nt . i nt ) : "

"

/* * Класс ANGLE */ Angl e Ang l e : : operator+ ( const Ang l e& angl e ) const { Angl e retval = angl e : retva l . radi ans += radi ans : return retva l : /* * Класс COORD I NATE */ voi d Coordi nate : : rotateAbout ( Coordi nate pi vot . Ang l e ang l e ) { l ong xdi stance = pi vot . x( ) - x( ) , ydi stance = pi vot . y ( ) -y ( ) : l ong xdi stsqua red = xdi stance * xdi stance . ydi stsqua red = ydi stance * ydi stance : douЫ e r = : : sqrt ( xdi stsquared + ydi stsqua red ) : Angl e newangl e = angl e + Angl eatan ( douЬl e (ydi stance ) /douЬl e ( xdi stance ) ) ; x_rep = pi vot . x ( ) + l ong ( r * : : cos C newang l e ) ) : y_rep = pi vot . y ( ) + l ong ( r * : : s i n ( newangl e ) ) ; /* * Класс SHAPE */ !* Фла г . испол ь зуемый базовым г рафическим пакетом */ stat i c i nt X_i ni ti a l i zed = О : Shape : : Shape ( ) { center_val = Coordi nate ( 0 , 0 ) : next_poi nter=prev_poi nter=O : col or = Whi te : i f( ! X_i n i t i a l i zed ) { doX I n i t i a l i zati on ( *thi s ) :

П р ограмма Shapes

X_i ni ti a l i zed

=

1:

/* * Класс L I NE */ voi d L i ne : : rotateAЬout ( Coordi nate с . Ang l e a ng l e ) erase ( ) : p . rotateAbout < c . a ngl e ) : q . rotateAЬout ( c . angl e ) : draw( ) : voi d L i ne : : draw( ) { i f ( p . x ( ) - q . x ( ) && p . y ( ) -q . y ( ) ) { XDrawl i ne ( d i spl ay . wi ndow . gc . р . х( ) . р .у( ) . q . xO . q .y( ) ) :

L i ne : : Li ne ( Coordi nate pl . Coordi nate р2 ) { р2 : р pl : q center_val = Coordi nate( ( p . x( ) +q . x( ) ) / 2 . ( p . y ( ) +q . y ( ) ) / 2 ) : col o r Col o r ( Wh i te ) : draw( ) : =

=

=

/* * Класс RECTANGLE */ voi d Rect : : rotate ( Ang l e angl e J e r aseO ; 1 1 . rotateAЬout ( cente r ( ) . 1 2 . rotateAЬout ( center ( ) . 1 3 . rotateAЬout ( center ( ) . 1 4 . rotateAЬout ( center ( ) . draw< ) : voi d Rect : : draw( ) { 1 1 . d raw( ) : 1 2 . draw( ) :

angl e ) : angl e ) : a ng l e ) : a ngl e ) :

41 1

41 2

П р ил оже ние Б. П ро гра мма Shapes

1 3 . draw( ) : 1 4 . d raw( ) : voi d Rect : : move ( Coordi nate с ) /* * Ар гумент определяет новое положение центра . * Функция определяет смещение центра и п рои з водит * соот ветствующее смещен и е остальных точек . */ erase( ) : l ong xmoved с . х ( ) - center ( ) . x ( ) : l ong ymoved = с . у ( ) - center ( ) . y ( ) ; center_v a l = с : 11 Li ne ( Coordi nate ( l l . or i gi n ( ) . x ( )+xmoved . 1 1 . or i gi n ( ) . у ( ) +ymoved ) . Coord i nate( l l . e ( ) . x{ ) +xmoved . l l . e ( ) . y { ) +ymoved ) 1 2 = Li ne ( Coordi nate( l 2 . ori gi n ( ) . x ( )+xmoved . 1 2 . or i gi n ( ) . y ( ) +ymoved ) . Coordi nate( l 2 . e ( ) . x{ )+xmoved . 1 2 . e ( ) . y { ) +ymoved ) 1 3 L i ne ( Coordi nate ( l 3 . or i gi n ( ) . x ( ) +xmoved . 1 3 . or i gi n ( ) . у ( ) +ymoved ) . Coordi nate( l 3 . e ( ) . x( ) +xmoved . 1 3 . e( ) . y ( ) +ymoved ) 1 4 Li ne ( Coordi nate ( l 4 . ori gi n ( ) . x( ) +xmoved . 1 4 . or i gi n ( ) . y ( ) +ymoved ) . Coord i nate ( l 4 . e ( ) . х ( )+xmoved . 1 4 . e( ) . у ( ) +ymoved ) draw( ) : =

=

): ):

=

):

=

Rect : : Rect ( Coordi nate topleft . l ong xs i ze . l ong ys i ze ) { a ( topLeft ) : Coordi nate b ( a . x( )+xs i ze . а . у ( ) ) ; Coordi nate с ( а . х ( ) . а . у ( ) +ys i ze ) : Coordi nate d ( b . x( ) . c .y( ) ) : Coordi nate 1 1 = L i ne ( а . Ы : 12

L i ne ( b . c ) : 1 3 L i ne ( c . d ) : 1 4 = L i ne ( d . a ) : center v a l = Coord i nate( ( l l . or i g i n ( ) . x ( ) +l 2 . e ( ) . x ( ) ) /2 . ( l 4 . ori gi n ( ) . y ( ) +l 4 . e ( ) . y ( ) ) / 2 ) : draw( ) : =

=

/* * Класс TRIANGLE */

):

Программа Shapes

voi d Tri angl e : : rotate ( Ang l e angl e ) erase O : 1 1 . rotateAbout ( center ( ) . angl e ) : 1 2 . rotateAbout ( center ( ) . angl e ) : 1 3 . rotateAЬout ( center ( ) . angl e ) : draw( ) : voi d Tri angl e : : пюve ( Coordi nate с ) { /* * Ар гумент определ яет новое положение центра фи гуры . * Фун кция определ яет смещение центра и прои з води т * соответствующее смещение остал ьных точек . */ eraseO : l ong xmoved с . х ( ) center( ) . x ( ) ; l ong упюvеd с . у ( ) - centerO . у ( ) : center val с: 11 Line ( Coordi nate ( l l . ori gi n ( ) . x( )+xmoved . 1 1 . ori gi n ( ) . y ( ) +yпюved ) . Coordi nate ( l l . e ( ) . x ( ) +xmoved . 1 1 . е ( ) . у ( ) +упюvеd ) ) : 1 2 Li ne< Coordi nate ( l 2 . ori gi n ( ) . x( ) +xmoved . 1 2 . or i gi n ( ) . y ( ) +yпюved ) . Coord i nate ( l 2 . e ( ) . x ( ) +xmoved . l 2 . e ( ) . y ( )+yпюved ) ) : 1 3 Li ne < Coordi nate ( l 3 . or i g i n ( ) . x ( )+xmoved . 1 3 . or i g i n ( ) . y ( ) +ymoved ) . Coordi nate ( l 3 . e 0 . х ( ) +xmoved . 1 3 . e ( ) . у О +упюvеd ) ) : draw( ) : �

-

=

=

=

с



voi d Tri angl e : : draw( ) { 1 1 . d raw( ) : 1 2 . draw( ) : 1 3 . draw ( ) :

Tri angl e : : Tri angl e < Coordi nate а . Coordi nate Ь . Coordi nate с ) { 1 1 = Li ne ( a . Ь > : 1 2 = Li ne ( b . c ) : 1 3 = Li ne ( c . a ) : center va l Coordi nate( ( l l . e ( ) . x( )+l 2 . e( ) . x ( ) +l 3 . e ( ) . x( } ) / 3 . ( l l . e ( ) . y ( )+l 2 . e ( ) . y ( )+l 3 . e ( ) . y ( ) ) / 3 ) : d raw( ) : =

41 3

При л ожение В С с ыло ч н ые возвращаем ы е з н а ч ен и я о п ерат оров Ссылочная переменная не является ни указателем, ни объектом или переменной

в традиционном понимании. Скорее, она определяет дополнительное имя для

объекта. При создании ссылки появляется возможность обращаться к уже суще­ ствующему объекту по новому имени. Почему же функции

operator=

и

operator[]

возвращают ссылочные значения? На

высоком уровне ответ звучит так: потому что ссылка позволяет использовать возвращаемое значение как

имя

переменной. Другими словами, всюду, где мо­

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

operator=

и

operator[]

означает, что такие выражения могут

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

Следующая идиома (из приложения

(см. 3.3).

А) демонстрирует простейшую передачу

параметров по ссылке:

Код С : i nt i nc r ( i ) i nt *i ; { retu rn ( *i ) ++ ;

Код С++ :

i nt ma i n ( ) { i nt j 5: i nc r ( &j ) :

i nt mai n ( ) { i nt j = 5 : i nc r ( j ) ;

=

i nt i nc r ( i nt &i )

{

return i ++ ·

Идиома передачи по ссылке распространяется на возвращаемые значения функ­ ций, в том числе и перегруженных операторных функций.

В

действительности

мноmе встроенные бинарные операторы возвращают ссылки для обеспечения ассоциативности. Рассмотрим следующий пример:

Ссылочные возвращаемые з начения операторов

Код С : struct Stri ng { char *rep : }; char i ndex ( t . i ) st ruct Stri ng *t : i nt i : { return t ->rep[ i ] : i nt ma i n ( ) { struct Stri ng х : char с :

}

с = i ndex ( x . 5 ) : i ndex ( x . 6 ) = ' а ' : return О :

41 5

Код С++ : 1 cl ass Stri ng 2 puЬl i c : З cha r operator [ ] ( i nt i ) 4 return t [ i ] : 5 6 pri vate : 7 char *t : 8 }: 9 10 1 1 i nt ma i n ( ) 12 { Stri ng х: char с : 13 14 с = х[5] : 15 'а' : х[6] 16 return О : 17 18 =

Для версии С компилятор выдает сообщение об ошибке: "t2 . c " . l i ne 16 : addressaЫ e express i on requi red Для верс ии С ++ сообщен ие выглядит иначе:

" t2 . c " . l i ne 16 : error : assi gnment to generated functi on cal l ( not an l va l ue )

Н о если доб авить в в ерсию С пром ежуточный уровень логическ ой адресации (и объявить ссылочн ое возвращаемое значение в версии, написанной на С++) , все работает нормально. Код С : struct Stri ng { char *rep : }; char *i ndex( t . i ) struct Stri ng *t : i nt i :

{

return t - >rep + i :

i nt ma i n ( ) { char х[80] . с : с = * ( i ndex ( x . 5 ) ) : * ( i ndex( x . 6 ) ) - ' а ' : return О :

Код С++ : c l ass Stri ng { puЫ i c : cha r &operator[ J ( i nt i ) return t [ i ] ;

}

pri vate : char *t : }:

i nt ma i n ( ) { Stri ng х : cha r с : с = х[5] : х[6] = ' а ' : return О :

П ри л ожение Г П ораз р я дно е ко п и ро в а н и е До выхода версии 1 .2 транслятора С++ присваивание структур (и классов) вы­ полнялось точно так же, как в языке С: содержимое источника просто копирова­ лось в приемник . Такой способ называется поразрядным копированием. Обычно программи ст при определении класса С++ использует каноническую форму ( см. 3. 1 ) чтобы предотвратить поразрядное копирование. Он -.перехваты­ вает• все операции, приводящие к копированию: присваивание ( operator= ), пере­ дачу арrументов и возвращаемых значений (Х::Х(Х&)). Затем пользователь может сделать то же, что прои с ходит при поразрядном копировании, или нечто б о­ лее интересное (например, обновить счетчик ссылок). Очень важно, чтобы эта специальная обработка выполнялась при хранении в объекте указателя на ди­ намически выделенную память; иначе возможны непредсказуемые последствия. ,

Вернемся к нашему примеру String: c l ass Stri ng { puЫ i c : Stri ng ( i nt n ) { rep = new cha r [ s i ze = n ] : -Stri ng ( ) { del ete rep : } i nl i ne i nt l ength ( ) const { return s i ze : } pri vate : char *rep : i nt s i ze : }:

Теперь посмотрим, что произойдет, если объявить в программе два о бъекта String, а затем присвоить один другому: i nt ma i n ( ) { St ri ng s l ( lO ) . s2 ( 20 ) : sl = s2 : return О :

Программа сначала вызывает конструктор String(int) для каждого из объектов sl и s2; в свою очередь, каждый объект выделяет память под с имвольный вектор и сохраняет указатель на него в своей переменной rep. Затем происходит при­ сваивание, и каждое поле sl становится в точности равным соответствующему

Поразрядное копирование

41 7

полю 52. В результате s l . rep и s2.rep ссылаются на один вектор из 20 элементов, а исходный вектор из 10 элементов, изначально связанный с sl, становится не­ доступным! Даже сам по себе этот факт выглядит странно. Но это не все: при вы­ ходе из программы автоматически вызываются деструкторы sl и s2 (их вызов обеспечивается компилятором). Каждый конструктор вызовет оператор detete для своего поля rep, а следовательно, один и тот же блок памяти будет освобо­ жден дважды. Обычно это приводит к нарушению логической целостности кучи и аварийному завершению программы. Чтобы решить эту проблему, следует перехватить присваивание и либо создать локальную копию вектора sl, либо обновить счетчик ссылок. Вот почему в любом нетривиальном классе определяются функция operator= и конструктор X::X(const Х&), как это сделано в ортодоксальной канонической форме. ВНИМАНИ Е Копирование на уровне членов

класса

-

не панацея.

С++ автоматически генерирует требуемый оператор = и конструктор X::X(const Х&) для тех классов, в которых они отсутствуют. Конструктор X: :X(const Х&) инициа­

лизирует текущий объект копированием соответствующих членов параметра; функция operator= последовательно присваивает их значения. Однако при этом не всегда используется поразрядное копирование: X::X(const Х&) инициализирует переменные объекта при помощи их конструктора X::X(const Х&) , а присваивание выполняется рекурсивно. Эти функции могут увеличивать счетчики ссылок на динамически выделенную память, принадлежащую внутренним объектам, или выполнять другие служебные операции для предотвращения упоминавшегося на примере String хаоса. Эта схема хорошо работает для объектов классов. Но указатели (точнее, указате­ ли, освобождаемые вызовом оператора detete в деструкторе) нарушают ее работу, и мы приходим к той же проблеме, с которой все началось: повторному освобо­ ждению динамически выделенной памяти. Поэтому во всех классах, в которых деструктор вызывает оператор detete для одного из членов, важно определять функцию operator= и конструктор X::X(const Х&). Одним из поводов для перегрузки функции operator-> (см. раздел 6.5) в С++ 2.0 стало стремление к использованию исключительно объектов; в этом случае авто­ матическое копирование на уровне членов класса всегда работает правильно, по­ скольку указатели уходят со сцены. Тем не менее, такой подход еще не получил достаточного распространения в сообществе программистов С++, и вместо него разрабатываются другие методики.

При л ожение Д Иерархия гео м етрических фи гур в с и м во л и чес ко й иди о ме 1 1 *** * *************** * **************************** 1tr описывает элемент таблицы виртуал ьных фун кций : ее фор11ат зависит от специфи ки построения объектно г о кода ваши11 компиля тором С++ .

typedef i nt ( *vptp ) ( ) : typedef vptp ( *vvptp ) ( ) : st ruct mptr { s hort d : short i : vptp f : } : / / ******************** ************************* ***************/ / 11 11 // 11 Ф А Й Л : К . Н 11 11 // 11 Объ я влен и я классов Тhi ng и Тор 11 11 / / ****************** * *************** ***************** ********* / /

Иерархия геометрических фиrур в символической иди оме

#i fndef -MPTR Н fi ncl ude ·mptr . h " fendi f fdefi ne -К Н fi ncl ude 1 1 ЗАВИСИТ ОТ ПЛАТФОРМЫ И КОМПИЛЯТОРА const unsi gned i nt WRDS IZE s i zeof ( voi d* ) : =

i nl i ne s i ze t Round ( si ze_t s ) { return ( si ze_t ) ( ( s + WRDS I ZE - l ) /WRDSI Z E ) *WRDS I ZE : 1 1 Часто испол ь зуемый тип указател я на символ typedef char *Cha r_p : 1 1 Фиктивный тип . используемый дл я устранения неоднозначностей 11 между конструктора ми классов . произ водных от ShapeRep enum Exempl a r { } : 11

ЗАВИСИТ О Т ПЛАТФОРМЫ И КОМПИЛЯТОРА

cl ass Тор { puЫ i c : 1 1 Объекты этого класса не содержат данных кроме поля _vtЫ . 1 1 с генерированного компиля тором . Наследование всех классов 11 от этого класса г арантирует . что в большинстве реализаций 1 1 _vtЫ все гда будет первым элементом любого объекта . 1 1 Если в ваwем компиля торе данное условие не выполняетс я . 1 1 _vtЫ придется искать дру г и м и средствами . Возможно . 1 1 это потребует и з менения реализаци и < а именно fi ndVtЫ Entry ) . 1 1 Поле _vtЫ резервируется для в нутреннего 11 использования системой . vi rtual -Тор ( ) { } mptr* fi ndVtЫ Entry( vptp ) : voi d update ( Stri ng . Stri ng . const char *const stati c voi d operator del ete ( voi d *р ) : : operator del ete ( p ) :

}

=

"

"):

1 1 Функция обще г о назначения doi t упрощает упра вление 11 процессом обновлен и я . vi rtua l Тор *doi t ( ) : protected : Тор ( ) { } stati c voi d *operator new( s i ze_t 1 )

41 9

420

Приложение Д. Иерархия геометрических фи гур в си мволической идиом е

return : : operator new( 1 ) : pri vate : 1 1 Сра внение д вух ука за телей на функuии . i nt compa reFuncs ( i nt . vptp . vptp ) : }: typedef Тор *Торр : c l ass Thi ng : puЬl i c Тор { 1 1 Все поля " rep" наследуются от Thi ng : э тот класс 11 определяет каноническую форку всех классов п исек . puЬl i c : vi rtual Thi ng *type ( ) { return thi s : } Thi ng ( ) { } 1 1 Функuия обновлени я пол я vi rtua l Thi пg *cutover ( ) : v i rtua l -Thi ng ( ) { } 1 1 Деструктор i nt docutover ( ) : }: typedef Thi ng *Th i ngp :

11 11 11 11 11

Ф А Й Л

К

С

Код класса Thi ng

11 11 11 11 11

/ ! * * * * * * * * * * * * * * * * * * * * * ********************************* * * * * * * / /

#i nc l ude " k . h " Тhi ng * Thi ng : : cutover ( ) 1 1 Фун кци я . п редоста вляекая пол ь зователек для упра влен и я 1 1 преобра зованиек старо г о форката данных класса 11 в новый форкат . Фун кuия вызывается для зкзекпл я ра 1 1 старо г о класса и возвращает зкзекпляр ново го класса . return thi s : i nt Thi ng : : docutover( ) 1 1 Воз кожно . пол ь зовател ь не захочет преобра зовывать 11 некоторые объекты в проuессе преобра зования данных 1 1 класса . Функция docutover воз вращает для отдельного 11 объекта логическую величину ( t rue или fa l se > . 1 1 указывающую . нужно ли п реобразовывать объект в новый

Иерархия геометрических фи гур в с имволической идиоме 1 1 форма т . Обыч но это делается с объекта ми . сов местно 11 испол ь зуемыми несколькими классам и кон вертов : 1 1 п реобра зование объекта должно быть выполнено ровно

11 один ра з . а НЕ для каждо го конверта . Фун кция docutover 11 определ яет . нужно л и п реобра зовывать общий объект 11 ( напри мер . анализом счетч и ка ссылок . ведением теневого 11 счетч и ка и т . д . ) . return 1 :

/ /*********************************************** *************/ / 11 11

//

11 11

Ф А Й Л

т о р

.

с

11 11 11

11 11 / /*************** *********** ************** ****************/ / Код класса Тор

#i ncl ude " k . h " #i ncl ude Тор * Тор : : doi t O { 1 1 Функция п редоставляет удобную точ ку входа . через 1 1 которую пол ь зователь может з а г рузить собственный 11 код для упра влен и я обновлением класса . return О : i nt Top : : compa reFuncs ( i nt vtЫ i ndex . vptp . vptp fpt r ) { 1 1 Функци я сравнивает два абстра ктных указател я на функции . 1 1 Первый указа тел ь на фун кцию хара ктеризуется индексом

11 в таблице виртуальных функций и адресом :

1 1 второй хара ктеризуется тол ь ко адресом . 1 1 Испол ьзование · параметров з а в исит от системы . 1 1 В данном случае адрес первого ука з а тел я не требуется . return vtЫ i ndex � ( i nt ) fptr ;

mpt r * Top : : fi ndVtЫ Entry ( vptp f ) { 1 1 Функция ищет в таблице в иртуал ьных фун кций " текуще г о " 1 1 объекта ука затель на функцию . ра вный параметру f . 1 1 и возвращает адрес е г о структуры mptr ( полное содержи мое 1 1 элемента таблицы в и ртуальных функций ) . 1 1 Ука зател ь mpp содержит а дрес указателя на таблицу 1 1 в иртуал ь ных фун кций : структура наследовани я гарантирует .

42 1

422

Приложе ние Д. Иерархия геометрических фигур в символической идиоме

1 1 что ука за тель на таблицу в иртуальных функций находится 11 в начале каждо го объекта . предста вляющего интерес 1 1 для нас ( все объекты являются nрои з водныни от Тор ) . mpt r ** mpp ( mptr**) thi s : =

1 1 Разыненование указателя для получени я адреса таблицы 11 виртуальных функций . находящейся в начале объекта . mptr * vtЫ = *mpp : pri ntf( "Top : : fi пdVtЫ Eпtry ( %d ) :

vtЫ = Ox%x\ n " . f . vtЬl ) :

1 1 Перебор таблицы в иртуальных функций текущег о 1 1 объекта и поиск в н е й заданной функци и . 1 1 Эленент с нулевын з начениен за вершает таблицу . 1 1 а эленент с нулевын и ндексон не исполь зуется . for ( i пt i 1 : vtЫ [ i ] . f : ++i ) { i f ( compa reFuncs ( i . vtЬl [ i ] . f . f ) ) { return vtЫ + i : =

return О : 1 1 Объ я вление в нешней функции С " l oad" extern "С" vptp l oad( const char *) : voi d Тор : : update (

Stri ng fi l ename . Stri ng fname . const cha r *const

11 11 11 11 11

11

TypedefSpec )

За г руз ка в п а н я т ь фун кции fname и з файла fi l ename . Последн и й необязател ьный паранетр задает тип указателя на функцию : он необходин в случае пере грузки функции . При з а г рузке происходит соответст вующее обновление таблицы в иртуал ьных функций . Обновляться но гут только

виртуальные функции .

const Stri ng temp

=

" /tmp " :

pri ntf( "Top : : update ( \ " % s\ " . \ " %s\ " . \ " %s\ " ) \ п " . ( const char * ) fi l ename . ( const char * ) fname . ( const char * ) TypedefSpec ) : Stri ng prepname = temp + " / " + "t . c " : "" : St ri ng Typedef . cast i f ( strl en ( TypedefSpec ) ) { Typedef = Stri ng ( " / / ТУРЕ used to di sambl guate\ overl oaded functi on\n\t\t\ttypedef " ) + TypedefSpec : =

Иерархия геометрических фигур в символической идиоме

} el se { Typedef = " typedef vptp ТУРЕ " : cast = " ( vptp ) " : } 1 1 Вспомог а тельная функция prepname воз вращает адрес 1 1 обновля емой функци и . F I LE *tempFi l e = fopen ( prepname . "w" ) : fpri ntf ( tempFi l e . "#\ i nc l ude \ " i nc l udes . h\ " \n\ extern vptp functi onAddress ( ) { \n\ %s : \n\ ТУРЕ retval = %s&%s : \ n\ return ( vptp ) retva l ; \ n\ }\n" . ( const char*)Typedef . ( const char * ) cast . ( const char* ) fname ) : fcl ose< tempFi l e ) : 1 1 Конnил я ц и я вспомог а тельной функции Stri ng coпmand = Stri ng ( " DIR= ' pwd ' ; cd " ) + temp + " : \ СС +еО - I SDIR - с -g " + prepname ; system( coпmand ) : unl i nk ( prepname ) : Stri ng objectname = prepname ( O . prepname . l engthO 2) + " . о" ; -

1 1 За грузка вспомог ательной функции . Всnонните . что 11 функция l oad возвращает адрес новой функции . vvptp fi ndfunc = ( vvptp ) l oad(objectname ) : un 1 i nk ( objectname ) : pri ntf( "Top : : update : ca l l i ng fi ndVtЫ Entry ( %d ) \ n " . ( *fi ndfunc ) ( ) ) : 1 1 Поиск пра вильного элемента в таблице виртуаль ных функций 11 данного класса . Вызов вспомо г ательной функции сообщает 11 fi ndVtЫ Entry . ка кую функцию нужно найти . mptr *vtЫ Entry = fi ndVtЫ Entry ( ( *fi ndfunc ) ( ) ) ; 1 1 За грузка новой версии функции и сохранение ее адреса 11 в таблице в иртуальных функций . pri ntf( "Top : : update : o l d vtЫ Entry - >f Ox%x\ n " . vtЫ Entry->f) : pri ntf( "Тор : : update : са 1 1 i ng l oad( \ " %s\ " ) \ n " . < const char *) fi l ename ) : vtЫ Entry->f l oad ( fi l ename ) ; pri ntf( "Top : : update : compl ete . new vtЫ Entry->f = Ox%x\ n " . vtЫ Entry - >f ) : =

=

423

424

Пр иложение

11 11 11 11 11

Ф А Й Л :

Д.

Иерархия гео метрических фи гур

в си м волической

C O O R D . H

Интерфейс структуры Coordi nate

11 11 11 11 11

#defi пe _COORDI NATE_H 1 1 Простой от крытый класс . представляющий декартовы 11 координаты на г рафическом э кране . st ruct Coord i пate { i пt х . у : Coord i пate ( i пt хх = О . i пt уу = 0 ) { х = хх : у = уу : } }: / / ****************** ************************************ ******/ / 11 11 11 Е S Н А Р Е н Ф А Й Л : 11 11 11 11 11 Интерфейс класса Shape 11 11 / / ***************************************************** *******/ / 1 1 Класс Shape обеспеч и вает пол ь зовательский интерфейс 1 1 ко всему г рафическому па кету - все остальные классы 11 просто испол ь зуются во в нутренней реализации #i fпdef -SНАРЕ-Н #defi пe -SНАРЕ-Н #i пcl ude " 1move\n " ) : object2->move ( pl ) : / / Уборка мусора shape - >gc ( ) : pri ntfC " exi ti ng\ n " ) : return О :

i nt ma i n ( ) i nt retval ma i n2 ( ) : shape->gc ( ) : return retva l : =

voi d doCl assUpdate ( ) { extern Shape *t ri angl e : const Stri ng i nc l ude " i nc l udes . h " : mk fi l e C i nc l ude . "#i ncl ude \ " ek . h\ " \n#i ncl ude \ " ev2tri . h\ " \ n " ) : =

compi 1 е ( " evЗtri а . с " ) : C *tri angl e ) . update C " evЗtri a . o " . " Tri ang l e : : ma ke " . " Shape C Tri ang l e : : *ТУРЕ ) ( ) " ) : compi l e C " evЗtri b . c " ) ; C *tri ang l e ) . update C " evЗtri b . o " . "Tri ang l e : : make" . " Shape ( Тri angl e : : *ТУРЕ ) \ C Coordi nate . Coordi nate . Coord i nate ) " ) : compi 1 е ( " evЗtri m . с " ) : C *tri angl е ) . update ( " evЗt ri m . о " . " Tri angl е : : move " ) : compi l e C " vЗtri c . c " ) : C *tri angl e ) . update C " vЗt ri c . o " . "Tri angl e : : cutover" ) : mk fi 1 е ( i nc 1 ude . "#i nc 1 ude \ "ek . h\ " \ n\ #i ncl ude \ " evЗtri . h\ " \ n " ) :

Иерархия геометрических фи гур в символической l(IДИоме

mkfi l e ( " evЗdoi t . c " . "fi ncl ude \ " ek . h\ " \n\ fi ncl ude \ " evЗtri . h\ " \ n\ Тор * Shape : : doi t ( ) { \ n\ pri ntf ( \ " vЗ Shape : : doi t ( new ) cal l ed\ \ n\ " ) : \ n\ Thi ngp Ttri angl e tri angl e : \n\ shape->dataUpdate ( Ttri angl e . \n\ new Tri angl e ( Exempl a r ( O ) ) ) : \n\ tri angl e < ShapeRep* ) Ttr1 angl e : \n\ pri ntf( \ " Shape : : doi t : di d data update\ \n\ " ) : \ return O : \n\ } \ n\n\ Tri ang l e : : Tri angl e ( Exempl a r е ) : ShapeRep ( e > { } \ n '' > : compi l e ( " evЗdoi t . c " ) : pri ntf ( " doCl a s sUpdate : \ ca l l i ng shape->update ( \ " evЗdoi t . o\ " . \ \ " Shape : : do i t\ " ) \ n " ) : shape- >update ( " evЗdoi t . o " . " Shape : : doi t " ) : shape - >doi t ( > : / / Периодическая уборка мусора shape - >gc ( ) : unl i nk ( " evЗdoi t . c " ) : unl i nk ( " evЗdoi t . o " ) : =

=

fi nc l ude i nt compi l e ( const Stri ng& fi l eName ) st ruct stat dotC . dotO : St ri ng fi l eNameDotO = fi 1 eName < О . fi 1 eName . 1 ength ( ) - 2 ) + " . о " : stat ( fi l eName . &dotC ) : stat ( fi l eNameDotO . &dotO ) : i f ( dotC . st_mti me < dotO . st_mt i me > { pri ntf( " \ " %s\ " i s up to date\ n " . ( const char* ) fi l eName ) : return О : } el se { St ri ng command = St ri ng ( " CC +еО -с - g " ) + fi l eName : pri ntf ( " compi l e : \n " . ( const char* ) command ) : return system( coпmand ) :

extern i nt mkfi l e ( const Stri ng &fi l eName . const St ri ng &contents ) F I L E *i nc = fopen ( fi l eName . "w" ) : pri ntf( "mkfi l e : c reati ng \ n " . ( const cha r * ) fi l eName ) : fpri ntf( i nc . ( const cha r* ) contents ) : return fc l ose ( i nc ) :

44 1

442

Приложени е д. Иерархия

геометрических

фи гур в си мволической идиоме

1 1 **********************************************************// 11 11 11 Т R 1 А N G L Е. Н Ф А Й Л : 11 11 11 11 Интерфейс класса Tri ang l e 11 11 11 * * *** * ****** * *** * * ***** * ** * * * * * ********** * * * * ** ** * *** * * * * * 1/ 1/ **********************************************************//

1 1 Интерфейс класса Tri angl e . реали зующе го семантику 11 геометрической фи гуры " треу гольни к " . #defi ne -TRIANGLE-Н #i fndef -SНAPEREP H #i ncl ude " ShapeRep . h " lendi f #i fndef -COORDINATE Н #i ncl ude ·coordi nate . h " lendi f cl ass Tri angl e : puЬl i c ShapeRep { puЬl i c : 1 1 Конструкторы Shape malce( > : Shape malce( Coordi nate . Coordi nate . Coordi nate ) : 1 1 Упра вление памятью voi d *operator new( s i ze_t ) : voi d operator del ete ( voi d * ) : О> : voi d gc ( s i ze_t -

1 1 Пользовательская семантика voi d drawO : voi d rotate ( douЬ l e ) : voi d nюve ( Coordi nate ) :

1 1 Фун кции класса Tri angl e < Exempl a r ) : Tri angl e ( ) : stati c voi d i ni t ( ) : pri vate : 1 1 Нико г да не должны вызыват ься Shape malce( Coordi nate ) : Shape malce( Coordi nate . Coordi nate ) : pri vate : 1 1 Переменные состоя н и я экземпляра Coordi nate pl . р2 . рЗ : pri vate : 1 1 Да нные упра вления па м я т ью

Иерархия геометрических фи гур в символической и;циоме stat i c char *heap : stat i c s i ze t pool l n i t i a l i zed : 10 } : enum { Pool S i ze =

}:

1 1 Объ я вление указа тел я на прототип Tri angl e extern ShapeRep *t ri angl e : 11 11 11 F I L Е : Т R I А N G L Е. С 11 11 11 11 11 Интерфейс класса Tri angl e 11 11 / / * * * ***** ****************************************************/ / #i ncl ude "Tri a ng l e . h " 1 1 Переменные . специфические для класса ShapeRep *tr i angl e О : char *Tri a ng l e : : heap О : О: si ze_t Tri angl e : : pool l n i ti a l i zed =

=

=

voi d Tri a ngl e : : i ni t ( ) { 1 1 Инициализация статических и г лобальных переменных . 1 1 специфических д л я класса Tri angl e . 1 1 Некоторые структуры данных инициализ ируются 11 при первом вызове ShapeRep : : gcCormюn tri angl e new Tri a ng l e ( Exempl a r ( O ) ) : =

Shape Tri angl e : : ma ke ( ) { 1 1 Построение вырожденного треу гол ь н и ка new Tri angl e : Tri ang l e *retval retv a l - >pЗ Coordi nate ( 0 . 0 ) : retval - >p2 retv a l ->pl retva l - >exempl a rPoi nter thi s : return *retva l : =

=

=

=

=

Shape Tri angl e : : make( Coordi nate ppl . Coordi nate рр2 . Coordi nate ррЗ ) { 1 1 Создание и возврат нового объекта Tri ang l e new Tri angl e : Tri a ng l e *retva l ppl : retva l - >pl =

=

443

444

Приложение д. Иерархия геометрических фиrур в си мволической идиоме

retva l - >p2 рр2 : retva l - >pЗ ррЗ : retva l ->exempl a rPoi nter return *retva l : =

=

=

thi s :

voi d Tri angl e : : gc ( s i ze_t nbytes ) { 1 1 Передача структур данных Tri angl e общей функции 1 1 уборки мусора ба зового класса . gcConmon ( nbytes . pool l n i t i a l i zed . Pool Si ze . heap ) : voi d Tri angl e : : d raw( ) { 1 1 В этой верс и и просто выводится и нформация о положении 1 1 треу г ольника в пуле . i nt Si zeof pool l n i ti a l i zed? pool l ni ti a l i zed : Round ( s i zeof( Tri angl e ) ) : pri ntf( ·"' . ' А ' + ( ( ( char * ) th i s - ( char * ) heap ) /Si zeof) ) : =

voi d Tri angl e : : move ( Coordi nate ) 11 . . . voi d Tri angl e : : rotate ( douЬl e ) 11 . . . voi d * Tri a ng l e : : operator new ( s i ze_t nbytes ) { 1 1 Если пул еще не инициализ ировался или класс Tri angl e 1 1 тол ь ко что обновился . управление передается 11 уборщи ку мусора . i f ( pool l n i t i a l i zed - nbytes ) { gcCoпmon ( nbytes . pool l n i t i a l i zed . Pool Si ze . heap ) ; pool l n i ti a l i zed nbytes : =

1 1 Поиск свободно г о элемента ( Tri angl e * ) heap : Tri angl e *tp 1 1 Нужно доба в и т ь проверку переполнения whi l e ( tp - >i nUse) { ( Tri angl e* ) ( ( ( char* ) t p ) + Round ( nbytes ) ) : tp =

=

Иерархия гео метри ческих фи rур в символической идиоме

11 Инициализация битов п а м я т и tp->gcma rk = О : tp->i nUse = 1 : return ( voi d* ) tp ; voi d Tri ang l e : : operator del ete ( voi d *) { 1 1 Оператор del ete никогда не должен вызываться . но С++ 1 1 настаи вает на е г о присутст в и и . если определен 11 оператор new . Tri angl e : : Tri angl e ( ) { }

/ / Информа ц и я о раз мере и vpt r

Tri angl e : : Tri angl e ( const Tri angl e& t ) 1 1 Коп ирующи й конструктор pl t . pl ; р2 t . p2 ; рЗ t рЗ : =

=

=

.

Tri angl e : : Tri angl e ( Exempl a r е ) : ShapeRep ( e ) 1 1 Построение протот ипа Tri angl e Shape Tri angl e : : make( Coordi nate ) 1 1 Фи ктивная функция - н и ко г да не должна вызываться . return *aShape : Shape Tri angl e : : make( Coordi nate . Coordi nate ) { 1 1 Фи ктивная функция - н и когда н е должна вызываться . return *aShape :

11 11 11 11 11

Ф А Й Л :

V 2 Т R 1 А N G L Е. С

Из мененный код класса Tri ang l e ( версия 2 )

11 11 11 11 11

/ / ** ********* * *** * * ** ********* ** * * **** * * * *********************/ /

#i ncl ude " v2Tri angl e . h " voi d Tri ang l e : : move ( Coordi nate )

445

446

Приложен ие д. Иерархия геометрических фигур в символической идиоме

pri ntf( " versi on 2 Tri angl e : : move o f s i ze %d\ n" . s i zeof(*thi s ) ) :

11 11 11 11 11

Ф А Й Л :

V 2 Т R I А N G L Е. Н

Измененный и нтерфейс класса Tri ang l e ( версия 2 )

11 11 11 11 11

/ / ****** *** **** **** ************************** ***************/ / #defi ne -TRIANGLE-Н #i fndef -SHAPEREP H #i nc l ude " ShapeRep . h " #endi f #i fndef _COORDINATE_H #i ncl ude " Coordi nate . h " #endi f c l ass Tri angl e : puЬl i c ShapeRep { puЫ i c : 1 1 Соот ветст вует Tri angl e . h . но с доба влением нового 11 определени я Tri a ngl e : : move Shape make( ) : Shape make( Coordi nate . Coordi nate . Coordi nate ) : Tri angl e O ; voi d draw( ) ; voi d move( Coordi nate ) : voi d rotate( douЬl e ) ; voi d *operator new( si ze_t ) ; voi d operator del ete ( voi d *) ; voi d gc ( s i ze_t = 0 ) ; Tri angl e ( Exempl a r ) ; stat i c voi d i ni t ( ) ; pri vate : stati c voi d pool l ni t ( s i ze_t ) : Shape make( Coordi nate ) { return *aShape : } Shape make( Coordi nate . Coordi nate ) { return *aShape ; Coordi nate pl . р2 . рЗ ; pri vate : stati c char *heap ; stati c s i ze_t pool i n i ti a l i zed : enum { Pool Si ze = 10 } ; }; extern ShapeRep *tri angl e :

Иерархия геометрических фигур

в

символическо й и-диоме

/ / ************** * ** * *** * **** * * ************�**** * ************/ / 11 11 11 V 3 Т R I А N G L Е. Н Ф А Й Л : 11 11 11 11 Структуры данных для версии 3 класса Tri ang l e 11 11 11 / / ******* * ******************************************** * *******/ / #defi ne _TRIANGLE_H #i fndef _SHAPEREP_H #i ncl ude " ShapeRep . h " #endi f #i fndef -COORDI NATE Н #i ncl ude " Coordi natё . h " Hendi f 1 1 Объ я вление НОВОЙ ( третьей ) версии класса Tri angl e 11 с доба влением атрибута col or cl ass Tri ang l e : puЫ i c ShapeRep puЬl i c : Shape rnake( ) : Shape make( Coordi nate . Coordi nate . Coordi nate ) : Tri angl e ( ) : voi d draw( ) ; voi d move < Coordi nate ) ; voi d rotate ( douЬl e ) ; voi d *operator new ( s i ze_t ) : voi d operator del ete ( voi d * ) : voi d gc ( s i ze_t 0) : Thi ng *cutover ( ) ; Tri ang l e ( Exempl a r ) ; stat i c voi d i ni t ( ) ; pri vate : stati c voi d pool l ni t ( s i ze_t ) ; Shape mak e ( Coordi nate ) { return *aShape ; } Shape make( Coordi nate . Coordi nate ) { return *aShape ; Coordi nate pl . р2 . рЗ ; enum Col or { Bl ack . Whi te } col or : pri vate : stati c char *heap : stati c s i ze_t pool l n i t i a l i zed : enum { Pool S i ze 10 } : }; =

=

extern ShapeRep *t ri angl e :

447

448

11 11 11 11 11

П риложение Д. Иерархия геометри ческих фи гур в символической идиоме

V 3 Т R I А N G L Е А. С

Ф А Й Л

Код версии 3 класса Tri a ng l e

11 11 11 11 11

#i ncl ude " vЗTri angl e . h " Shape Tri angl e : : ma lpЗ retva l - >p2 retva l ->pl retva l - >exempl a rPoi nter thi s : col or = B l acl< : return Shape ( *retva l ) : =

=

=

=

Coordi nate ( 0 . 0 ) :

=

1 1 Статически расширяется дл я использования в при веденной 11 выше фун кции malpl retva l ->p2 рр2 : retva l - >pЗ ррЗ : retva l ->exempl a rPoi nter thi s : return *retva l : =

=

=

=

=

Tri angl e : : Tr i angl e ( )

}

/ / Информация о ра з мере и vptr

Иерархия геометрических фи rур в символической идиоме f f ****************** AAAAAA Alr************************AAAAAAAA ** f f 11 11 11 Ф А Й Л : // V З T R I A N G L E C U T O V E R . C 11 11 11 Упра вление преобра зованием для версии 3 кода Tri aпg l e // 11 11 / / АААААААААААААААА АААААААААААААААААА***ААААААААААААААА А******* / /

#i nc l ude " vЗTri a ng l e . h " #i ncl ude "Map . h " 11 -------------------------------------------------------------c l ass v2Tri angl e : puЫ i c ShapeRep { puЫ i c : Shape make ( ) : Shape ma k e ( Coordi nate . Coordi nate . Coordi nate ) : v2Tri angl e ( ) : voi d move( Coordi nate ) : voi d *operator new ( s i ze t ) : voi d operator del ete ( void * ) : voi d gc ( s i ze_t 0) : voi d draw( ) : v2Tri a ng l e ( Exemp l a r ) : stati c voi d i n i t ( ) : pri vate : fri end Th i ng *Tri angl e : : cutover ( ) : / / Дл я преобра зования stat i c voi d pool l ni t ( s i ze t ) : Shape ma ke( Coordi nate ) { return *aShape : } Shape ma k e ( Coordi nate . Coordi nate ) { return *aShape : } Coordi nate pl . р2 . рЗ : }: =

11 -- - -- ---- -- - ---- ----------- ------ --- --- -- ------------- -- -----11 11 11 11 11

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

Map obj ectMap : Th i ng * Tri angl e : : cutover ( ) { 1 1 Функция вернет указа тел ь на преобра зованный треу гол ь н и к Tri ang l e * retval thi s : =

1 1 Переданный экземпляр в действител ь ност и представляет 1 1 старый объект Tri angl e . Объ я вление старой версии 11 сохран яется под и менем v2Tri angl e : класс Tri angl e

449

450

Приложение Д. Иерархия геометрических фи rур в символической идиоме

1 1 определ яется в в ерсии 3 . ( v2Tri aпg l e * ) thi s : v2Tri aпgl e *ol d Thi ngp ol dtp thi s : ( ShapeRep* )thi s : ShapeRep *ol d s r =

=

=

i f ( obj ectMap . el ement ( o l dtp ) ) { 1 1 Если объект уже был преобразован . не преобра зовывать 11 е г о заново - просто вернуть старое преобра зован ное 11 з начение (Tri ang l e* ) ( objectMap[ol dtp] ) : ret v a l } el se { 1 1 Создание треу гол ь н и ка новой < трет ьей ) верси и . 1 1 Сохра нение нескол ь ких ука за телей раз ных типов . new Tri angl e : retva l retva l : ShapeRep *news r retva l : Тhi ngp newtp =

=

=

=

1 1 Копирование подобъекта ба зово г о класса ( ShapeRep ) *ol dsr : *news r =

1 1 Инициал и з а ц и я полей ново го объекта retva l - >exempl a rPoi nter tri angl e : ol d - >pl : retva l - >pl retva l - >p2 ol d ->p2 : ol d ->pЗ : retva l ->pЗ Bl ack : retva l - >col or =

= =

=

=

1 1 Сохра н и т ь преобра зова нный объект на будущее objectMap[ol dtpJ = newtp : } return retva l : Tri angl e : : Tri angl e ( ) { } / / ************************************************************/ / 11 11 // V З T R I A N G L E M O V E. C Ф А Й Л : 11 11 11 // 11 Код реализации move верс и и З класса Tri ang l e 11 11 / / ******************* АААААААААААААААААААААААА ***************** / / #i ncl ude " vЗTri angl e . h " voi d Tri angl e : : move ( Coordi nate ) { pri ntf ( " vers i on З Tri angl e : : move of s i ze %d\ n " . s i zeof( *thi s ) ) :

П ри л ожение Е Б л о ч н о - ст руктур н ое

п рогр а м м и ров ан ие н а С++ Нисходящее проектирование - проверенная временем методика программиро­ вания, которая лежит в основе большинства методов объектно-ориентированного проектирования, использовавшихся за два последних десятилетия. Объектно-ори­ ентированное проектирование предлагалось как замена этой методики, особенно при моделировании больших, сложных систем. Тем не менее, методы нисходя­ щего проектирования (такие, как функциональная декомпозиция) по-прежнему моrут успешно применяться, если алгоритмы хорошо понятны, или же задача достаточно автономна, а структура ее решения известна заранее. На уровне языка поддержка нисходящего проектирования воплощена в блочно­ структурном программировании. В настоящем приложении показано, как при­ менение некоторых идиом С++ предоставляет в распоряжение программиста ин­ тересную разновидность блочно-структурного программирования. Эти идиомы требуют более точного указания областей видимости, чем в языке Паскаль или Modula-2, что объясняется спецификой ограничения доступа в С++. Возможно, кто-то из читателей не будет полностью удовлетворен результатом - все зависит от личного вкуса и наклонностей. Однако показанное решение можно настроить по своему желанию, и здесь будут представлены некоторые варианты. Макросы препроцессора С определяются как часть языка С++. В этой главе чита­ тель получит представление о том, как комбинирование макросов с конструкция­ ми С++ обеспечивает функциональность и выразительность, недоступные для •базового языка С++».

Е . 1 . Концеп ция блочно-структу рного п рограммировани я Вероятно, нисходящее проектирование является основным методом структури­ рования программ, используемым в наше время. А из всех приемов нисходящего проектирования на практике чаще всего встречается футщиональная декомпозиция, или пошаговое уточнеиие, идея которого была предложена Никласом Виртом в начале 1970-х годов. При функциональной декомпозиции система изначально

452

П риложение Е. Блочно - структурное программирование на С++

характеризуется некоторой высокоуровневой функцией. Затем эта функция раз­ бивается на составные части, каждая из которых проходит дальнейшую деком­ позицию, и т. д. Конечный результат представляет собой набор модулей с четко определенной семантикой, напрямую реализуемых программистом. При функциональной декомпозиции данным отводится безусловно вторичная роль. На каждом уровне уточнения для каждой процедурной единицы проекти­ руется структура данных. Она определяет архитектуру модулей следующего уровня. Процедуры рассматриваются как •владельцы• этих данных. Жизненный цикл блока данных совпадает с жизненным циклом стекового кадра той проце­ дуры, которой он принадл ежит. Область видимости блока данных также опреде­ ляется структурой соответствующей процедуры. За прошедшие годы во многих языках была реализована прямая поддержка нис­ ходящего проектирования. Одним из важнейших языковых средств поддержки нисходящего проектирования является блочно-структурное программирование . Языки с поддержкой блочно-структурного программирования содержат конст­ рукции изменения видимости, позволяющие вкладывать процедуры друг в дру­ га. Языки С и С++ называются блочно-структурными, но только в ограниченном смысле. Хотя они отделяют локальные переменные от глобальных и допускают вложение блоков объявлений в процедурах, их конструкции вложения блоков не распространяются на процедуры. По этому критерию ни С, ни С++ не являются полноценными языками блочно-структурного программирования. в отличие от таких языков, как Алгол 68, PL/ 1 , Modula-2, Ada и Паскаль. Но даже простые средства С++ при творческом применении создают основу для построения блочно-структурных программ. Элементы стиля и приемы, исполь­ зуемые для имитации блочно-структурного программирования, мы будем назы­ вать блочно-структурной идиомой. Сначала эта идиома описывается в ограни­ ченной форме, предоставляющей доступ к символическим именам соседних блоков, а затем расширяется до более универсальной формы. ПРИМЕЧА НИЕ

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

Е . 2 . О сновн ые строител ьн ые бло ки структурного п рограмм и рования на С ++ Чтобы строить на С + + иллюзию блочно-структурного программирования, необ­ ходимо уметь создавать для каждой функции область видимости (или простран­ ство имен), которая также может содержать новые функции. Структуры (struct) хорошо отвечают этим требованиям. В С++, в отличие от С, объявления структур могут содержать функции, причем функции, локальные по отношению к структуре (которая локальна по отношению к своей функции), обладают соответствующей

Е. 2. Основн ые строительные блоки структурного программировани я

453

видимостью. Кроме того, структуры могут использоваться для инкапсуляции ар­ хитектуры данных на каждом уровне функциональной декомпозиции. Вскоре вы убедитесь, что эта инкапсуляция упрощает анализ блочно-структурных программ С++ по сравнению с их аналогами из языка Алгол. Хотя структуры обеспечивают необходимую семантику для поддержки блочно­ структурного программирования, синтаксис программы загромождается и выгля­ дит неестественно по сравнению с процедурными блоками языков Алгол и Пас­ каль. Макросы препроцессора помогают донести намерения программиста до читателя программы. С их помощью программист обозначает границы новых областей видимости, создаваемых в каждом теле функции. Мы разместим этим макросы в заголовочном файле block.h: 1 1 Файл Ы ock . h #i ncl ude \ #defi ne Local Scope ( funct i on ) struct name2( functi on . ForLocal Scope ) { puЬl i c #defi ne EndLocal Scope } l ocal

Объявление locaLScope просто содержит открывающие элементы объявления struct. Макрос name2, определенный в g eneric.h, соединяет два своих аргумента в одно новое имя. Параметр макроса locaLScope должен определять имя функ­ ции, внутри которой он находится. Макрос EndlocaLScope завершает объявление struct и объявляет экземпляр структуры с именем Loca� которое используется для обращения к контексту, созданному этой структурой. В листинге Е. 1 приведен пример простой блочно-структурной реализации алгорит­ ма пузырьковой сортировки [ 1 ]. Алгоритм пузырьковой сортировки декомпозиру­ ется до тех пор, пока текущий модуль CompareExchan ge не будет выделен в само­ стоятельную функцию, абстрагированную внутри BubbleSort. Функция BubbleSort управляет вызовами CompareExchan ge и организует работу алгоритма сортировки. Листинr Е. 1 . Блочно-структурная реализация пузырьковой сортировки

#i nc l ude #i ncl ude #i ncl ude voi d BubЫ eSort ( i nt n . char * records [ J . cha r * keys [ J ) { 1 1 Ал горитм пуз ырь ковой сортиро в к и по [ 1 ] Loc a l Scope ( BubЬ l eSort ) : i nt bound t ; voi d Compa reExchange ( i nt j , cha r *records [ J . cha r *keys [ J ) { i f ( : : st rcmp ( keys [ j ] . keys [ j+l ) ) > 0 ) { char *temp = records [ j ] : records [ j +l J : records [ j ] records [ j + l J = temp : .

=

продолжение.Р

454

Приложение Е. Блочно-структурное программирование на С++

Листинr Е. 1 (продолжение)

keys [j ] : temp keys [ j ] = keys [ j +l ] : keys [ j + l J = temp ; t = j; =

} } EndLoca l Scope ;

l ocal . bound = n ; do { l ocal . t = - 1 : О ; j < l ocal . bound - 1 : j++ ) { for ( i nt j l oc a l . Compa reExchange ( j . records . keys ) ; } l oca l . bound = l ocal . t + 1 : } whi l e ( l oca l . t ! = - 1 ) : =

Обратите внимание: хотя функция BubbleSort вызывается как обычная функция, функция CompareExchange не видна за пределами BubbleSort. Блочное структури­ рование обеспечивает сокрытие информации на процедурном уровне, а алгорит­ мические подробности инкапсулируются в абстракциях более высокого порядка. Ниже приводится пример использования приведенной реализации пузырьковой сортировки: char *records [ J = { " Stroustrup . Bj a rne " . " Li ppman . Stan " , " Hansen . Топу " , " Koeni g , Andrew" }: char * keys [ J " bs " . " stan " . " hansen " . "ark" }:

=

{

i nt ma i n ( ) { for С i nt i = О : i < 4 : i ++ ) { cout l eftWa l l && bal l . ypos i t i on ba l l . ypos i t i on ; i nt &х = p2 ->ba l l . xpos i ti on : i f ( х l eftWa l l + l 1 1 х >= p2->ri ghtWa l l - 1 ) { p2 - >ba l l . xspeed = - p2 - >ba l l . xspeed : } i f (у ba l l . yspeed = - p2 - >ba l l . ys peed : } cha r с = mv i nc h ( y + p2 - >ba l l . yspeed . х + p2 - >ba l l . xs peed ) ; swi tch ( с ) { case · w · : mvaddc h ( y + p2 ->ba l l . yspeed . х + p2 - >ba l l . xspeed . · ) ; p2 - >score++ : case ' Ь ' : '

463

464

Приложение Е. Блочно- структурн ое программирование на С p2 - >ba l l . yspeed = - p2 - >ba l l . yspeed ; p2 - >ba l l . xspeed = p2- >bat . xspeed : break ;

voi d МoveBa l l ( ) { P l ayGameLoca l Scope *р = Pa rent ( Pl ayGame . thi s ) ; Bal l GameLocal Scope *р2 Pa rent ( Ba l l Game . р ) ; mvaddch ( p2 - >ba l l . ypos i ti on . p2 - >ba l l . xpos i t i on . ' ' ) ; p2 - >ba l l . xpos i t i on += p2 - >ba l l . xspeed ; p2 ->ba l l . ypos i ti on += p2 - >ba l l . yspeed : mvaddch ( p2 - >ba l l . ypos i t i on . p2 - >ba l l . xpos i ti on . ' О ' ) : =

voi d MoveBat ( char key ) { Local Scope ( MoveBat ) : voi d Moveleft ( Bat& Ьаt ) { P l ayABa l l Loca l Scope *р = Pa rent ( Pl ayABa l l . th i s ) : P l ayGameLocal Scope *р2 = Pa rent ( Pl ayGame . р ) : Pa rent ( Ba l l Game . p2 ) - >EraseBat ( ) : i f < Ьat . pos i ti on > Pa rent ( Ba l l Game . p2 ) - >l eftWa l l ) Ьat . posi t i on - - : } Pa rent ( Ba l l Game . p2 ) - >DrawBat ( ) : voi d MoveRi ght ( Bat& Ьаt ) { Pl ayABa l l Local Scope *р Pa rent ( P l ayABa l l . th i s ) : P l ayGameLocal Scope *р2 Pa rent ( Pl ayGame . р ) : Pa rent ( Ba l l Game . p2 ) - >EraseBat ( ) : i f C bat . posi t i on < Pa rent < Ba l l Game . p2 ) ->ri ghtWa l l bat . l engt h ) { bat . pos i ti on++ : } Pa rent ( Ba l l Game . p2 ) - >DrawBat ( ) : } EndLoca l Scope : �

=

Код блочно-структурно й видеои гр ы P l ayGameLoca l Scope *р = Pa rent ( Pl ayGame . th i s ) : swi tch ( key ) { case ' l ' : l ocal . Moveleft ( Pa rent ( Ba l l Game . p ) - >bat ) : Pa rent ( Ba l l Game . p ) ->bat . xspeed brea k : case ' r ' : . l ocal . MoveRi ght ( Pa rent ( Ba l l Game . p ) ->bat ) : Pa rent ( Ba l l Game . p ) ->bat . xspeed brea k : defau l t : brea k :

=

-1:

=

1:

} Endloc a l Scope :

whi l e C Pa rent ( Ba l l Game . thi s ) ->ba l l i s i nPl ay ( ) ) l oca l . CheckBa l l Pos i t i on ( ) : l ocal . MoveBa l l C ) : refresh ( ) : key = getch ( ) : 1 оса1 . MoveBat ( key ) : } Endloca l Scope : l ocal . ba l l s left 4 : wh i l e C l ocal . ba l l sleft > 0 ) refres h ( ) : l ocal . key getch ( ) : l ocal . Pl ayABa l l ( ) : - - l oca l . ba l l sleft : =

=

EndOuterScope : i nt bestScore

=

О:

i ni tsc r ( ) : cbreak ( ) : noecho ( ) : l ocal . DrawSi des ( ) : l ocal . DrawBat ( ) :

465

466

Приложение Е. Блочно-структурное программирован ие на С ++

for ( : : ) { l oca l . score = О : l oc a l . DrawWa l l ( ) ; l oc a l . Pl ayGame ( ) : i f ( l ocal . score > bestScore ) bestScore = l oca l . score : cout « " best score i s

"

«

bestScore

«

"\n" :

Л итература 1 . Knuth, Donald Е. •Sorting and Searching•. Reading, Mass.: Addison-Wesley, 1973.

2. Bell, Doug, Ian Morrey, and John Pugh. •Software Engineering: А Programming Approach• . Englewood Cliffs, N.J.: Prentice Hall, 1987, ff.43. 3. American Telephone and Telegraph Company, •UNIX System V Release 4 Prog­ rammer's Guide: ANSI С and Programming Support Tools•. Englewood Cliffs, N.J .: Prentice-Hall, 1990.

П ри л ожение Ж С писок тер м и н о в В этом п риложении перечислен ы основны е тер мины , и спользуем ые в книге. Термин , исnоnьзуемы й

в

книге

Оригинальный термин

Абстрактный базовый класс Абстрактный базовый прототип Абстрактный тип данных

Abstract base class

Абстракция

Abstraction

Автономный о боб щенный конструктор Автономны й обо б щенный прототип

Autoпomous generic constructor

Актер

Actor

Ассоциативная вы борка Ассоциативный масси в

Associative retrieval

Abstract base exemplar Abstract data type

Autonomous generic exemplar

Associative array

Базовый класс Б и блиотека

Ubrary

Вариант

Variant

Вектор

Vector

В иртуальная функция

Virtual function

В исячая ссылка

Dangling reference

Base class

Врем я вып олнения

Run time

Время компиляции

Compile time

Встроенный тип данных

Built-in type

Вы борка

Retrieval

Высокоуровневое управление каналом Глобальная перегрузка

Global overloading

Глуб о кое копирование

Deep сору

Делегирование

Delegation

Деструктор

Destructor

High-Level Data Unk Control ( HDLC)

Динамическая типизация

Dynamic typing

Динамичес кое множественное наследование Диспетчер памяти

Dynamic multiple inheritance Memory manager

З агрузка

Loading

Задача

Task

Закрытое наследование

Private derivation, или private inheritance

468

Приложение Ж. Список терминов

Термин, испоnьзуемыА в книrе

ОриrинаnьныА термин

Закрыты й член класса

Private member

З ащищенны й член класса Зом б и

Protected member

Идентификация операци й на стадии выполнения Ие рархия реализации

Ruп-time operator ideпtificatioп

Изоморфная структура

lsomorphic structure

Инициализация

l п itialization

Zomble lmplemeпtatioп hierarchy

Интерфейс

lnterface

Исключение

Exception

Итератор

lterator

Каркас

Framework

Каркасное проrраммирование

Frame-based programmi п g

Класс

Class

Класс конверта

Envelop class

Класс письма

Letter class

Класс-заместитель

Stand-in class

Компоновка Конверт

U пking

Конкретны й тип данных

Concrete data type

Конструктор

Constructor

Envelop

Конструктор по умолчанию

Default constructor

Контейне р

Container

Контей нерны й класс

Container class

Контекст

Context

Копирующи й конструктор Корневой базовый класс

Сору constructor Root base class

Косвенны й ша блон Курсор

Cursor

Куча

Неар

l п direct template

Левостороннее значение

Left-hand value ( 1 - value)

Л огическое копирование

Logical сору

Манипулятор

Handle

Массив

Array

Международная органи зация по стандарти зации Мета-возможности

lnternational Standards Organization (ISO)

Метакласс

Metaclass

Метод

Method

Meta-features

Метод близнецов

Buddy system

Механизм Множественное наследование Модель , представление, контроллер

Mechaпlsm

Модуль

Module

Мул ьтиметод

Multi-method

Multiple inheritance Model-View-Controller ( MVC)

Списо к терминов Термин , используемый

в

кн иrе

Ориrинальн ы й термин

Мультио б раб отка

Multiprocessi пg

М усор

Garbage

Наложение указателей

Pointer aliasing

Наследование

lnheritance

Наследование с переопределением

l nheritance with overriding

Наследо вание с расширением

lnheritance with addition

Наследо вание с сокращением

lnheritance with cancellation

Неоднозначность

Amblguities

Н епереносимы й код

NonportaЫe code

Н еполны й класс Область видимости

Partial class Scope

Обо б щенная о бъектная система Usp Обо б щенны й конструктор

Generic constructor

Обо б щенны й прототип Оболочка

Wrapper

Обработка исключений Обратный вызов

469

Common Lisp Object System (CLOS) Generic exemplar Exception handling са11ьасk

Объединение

Union

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

Object

Объявление Обязательство

Declaration

О ператор

Operator

О ператорная функция О ператорная функция класса О пережающая ссылка

Operator function

Object inversion OЬject-oriented programming Object-oriented design Responsibllity

Member operator function Forward reference

Открытое наследование Открыты й член класса

PuЫic. member

Пакетирование

Packaging

PuЫic derivation , или puЫic inheritance

Параллельная о б ра б отка

Parallel processing

Параметризованн ы й тип

Parameterized type

Параметрический полиморф изм Перегрузка

Over1oading

Parametric polymorphism

Перегрузка в классе

Member overloading

Переключение контекста

Context switch

Перенаправление

Forwarding

Подмена

Overriding

Переопределение П исьмо

Redefining

Планирование

Scheduling

Поверхностное копирование

Shallow сору

Letter

470

Приложение Ж . Список терминов

Термин , испол ьзуем ый

в

кн иге

Ориги нальный терм ин

Подм ножество

Subset

Подсисте ма

Subsystem

Подставл яе мая функция

l пliпe fu п ction

Подстановка

Substitution

Подсчет ссылок

Reference counti п g

Подсчитываемый указатель

Counted pointer

Полиморфизм

Polymorphism

Полиморф изм включения

l п clusioп polymorphism

Полупространственное коп ирование

Semispace copying

Пользовательский тип

User-defined type

Пометка

Tag

Помеченная структура

Tagged structure

Помеченны й класс

Tagged class

Предварительная пометка

Mark-and-sweep

Представитель Прео б разование типа

Ambassador

П рим ес ь

Mix-in

Туре conversioп

Принцип подстановки Лисков

Uskov substitution principle

Программный поток

Thread

Производны й класс

Derived class

Протокол доступа к каналу для данных

U пk Access Protocol for Data ( LAPD)

Прототип

Exemplar, или prototype

Распорядитель соо б щества прототипов

Commuпity manager

Селе ктор типа

Туре selector

Се мантическая сеть

Semantic network

С ильная типизация

Strong typing

С ильное связывание

Strong Ьinding

С инглетн ый класс письма С инглетны й о бъект

Singleton letter class

С истема контроля типов Сла бая типизация

Туре system

Слот

Slot

Создание экзе мпляра Соо б щество прототипов

lnstantiation

Singleton object Weak typing

Exemplar commu п ity Composite object

С оставной объект Состояни е о бъекта

State of object

Спе ци ф икатор доступа

Access specifier

Ссылка

Reterence

Статическая функ ция

Static function

Статическое множественное наследование Структура Суб класс

Static multiple inheritance

Суб парадигматическое восстановление

Subparadigmatic recovery

Structure Subclass

С пи сок терминов Термин, используемый

в

книrе

47 1

Ориrинальный термин

Суперкласс

Superclass

Суперпарадигматическое восстановление Сущность

Eпtity

Superparadigmatic recovery

Счетчик ссылок

Aefereпce couпt

Таблица виртуальных функци й Тело

Virtual functio п tаЫе

Ти п

Туре

Толстый интерфе й с

Fat iпterface

Тонкий класс

Skiппy class

Body

Традиционная простая телефонная услуга

Plaiп Old Telephoпe Service ( POTS)

Транзакци онная диаграмма

Tra п saction diagram

Транзакция

Transaction

Уб орка мусора

Garbage collection

Указатель

Pointer

Физическое копирование

Physical сору

Ф ре й м

Frame

Функтор

Functor

Функция класса

Member function

Ци фровая сеть с комплексными услугам и Чисто абстрактный базовы й класс

l п tegrated Services Digital Network (ISDN) Pure aьstract ьаsе class

Чисто виртуальная функция Ша блон

Template

Pure virtual functioп

Ша блонная функция

Fuпction template

Эволюция схем ы

Schema evolutioп

Экземпляр прототипа

Exemplar instance

Ал фа витны й указател ь А

u

Ada, язык программирования, 40, 252, 452 Algol 68, язык программирования, 20, 25

UNIX, система, 306

в Bliss, язык программирования, 25

с С, язык программирования, 20, 53, 381 , 385, 4 1 5 CREATES-A, отношение, 294

F Flavors, язык программирования, 20, 23, 184 FORTRAN, язык программирования, 205

н HAS-A, отношение. 294 HAS-METACLASS, отношение, 294

IS-A, отношение, 2 1 0. 294 ISO, организация по стандартизации, 1 56

L LAPD, протокол, 1 56 Lisp, система, 1 86, 303, 343 \-значение, 64

м Mesa, язык программирования, 25 Modula-2, язык программирования, 45 1 MVC, архитектура, 362

р PL/l, язык программирования, . 452

s Simula, язык программирования. 20, 25 Smalltalk, язык программирования, 19, 23, 206, 257, 275 Suп, система, 323

w Windows, система, 1 94

х Х Window. система, 1 85, 1 94

А абстрактный базовый класс, 1 04, 1 15, 140, 147. 233, 467 абстрактный базовый прототип. 289, 467 абстрактный тип данных, 25, 28, 52, 249, 381 , 467 абстракция, 1 26, 467 данные, 25 классы. 28 полиморфизм, 98 сложность программ, 203 автоматизация управления памятью, 3 1 , 303, 31 8 автономный обобщенный конструктор, 332, 467 автономный обобщенный прототип, 288, 333, 467 актер, 367, 467 алгол 68. язык программирования, 452 алгоритм Бейкера, 335 полупространственного копирования, 335 предварительной пометки, 334 анализ доменный, 2 1 2 объектно-ориентированный, 2 1 2 потока данных, 203 предметной области, 2 1 2 при проектировании. 2 1 1 анархический язык программирования, 23 аппликативное программирование, 175, 176 аргумент константный, 389 по умолчанию, 353 архитектура, 204 ассоциативная выборка, 467 ассоциативность присваивания, 57

473

Ал фавитный указатель

аудит памяти,

д

64, 232, 467

ассоциативный массив,

334

данные,

25

392 97, 1 66, 224, 23 1 , 467 дескриптор, 342 деструктор, 3 1 , 1 1 6, 322, 467 виртуальный , 135 вызов, 85 прямой вызов, 3 1

Б

дедуктивный метод, делегирование,

366 60, 467 абстрактный, 1 04, 1 15, 1 40, 233 виртуальный, 1 86 определение, 98 чисто абстрактный, 234 Бейкера алгоритм, 335, 34 1 , 347 библиотека, 2 1 4, 364, 467 бинарный оператор, 322 близнецов метод, 155

база данн ых,

базовый класс,

диаграмма

221 221 объектная, 2 2 1 состояния, 221

временная, классов,

блочно-структурное программирование,

45 1

2 1 0, 358

транзакционная,

диктаторский язык программирования,

в

динамическая системная структура, динамическая цепочка,

динамическое множественное наследование,

вертикальное управление доступом, видимость, класса,

1 84, 349, 467

60

1 05

директива условной компиляции, диспетчер памяти,

381

доменный анализ,

30

локальная,

467 212

доступ

381

49, 1 1 9, 1 2 1 , 191, 350, 467 1 96 виртуальный базовый класс, 186 виртуальный деструктор, 135 виртуальный конструктор, 23, 148, 1 50, 1 55, 286 виртуальный процессор, 365 висячая ссылка, 78, 335, 467

горизонтальный,

виртуальное наследование,

закрытый,

377 377

открытый,

28

27

доступность членов класса, дружественные отношения,

заголовочный файл,

суперпарадигматическое,

221

243 28

49, 360, 383

306. 467 368, 467

загрузка, задача,

время

1 26, 467 компиляции, 1 27, 467 встроенная система, 85, 245, 280, 305 встроенный тип данных, 1 1 7, 467 выборка, 467

1 1 1 , 1 1 2, 23 1 , 467 28 закрытый член, 28, 468 защищенный доступ, 28 защищенный член, 28, 468 зомби, 379, 468

закрытое наследование,

выполнения,

высокоуровневое управление каналом,

28

защищенный,

3

восстановление субпарадигматическое,

1 05 1 05

вертикальный,

виртуальная функция,

временная диаграмма,

32 50

динамическое управление памятью,

66

глобальная,

23, 467 457

динамическая типизация.

269, 467 ввод-вывод, 2 1 вектор, 33, 85, 9 1 , 4 14, 467 вариант,

вертикальны й доступ,

23

365

закрытый доступ,

467

г

и

304 глобальная перегрузка, 66, 467 глобальная функция, 38 1 глобальный указатель, 320 глубокое копирование, 2 1 , 467

идентификация операций на стадии

генератор приложений,

горизонтальное управление доступом, горизонтальный доступ,

1 05

выполнения,

1 30, 468

идиома абстрактный базовый прототип,

289

332 288 программирование, 452

автономный обобщенный конструктор,

60

автономный обобщенный прототип, блочно-структурное

474

Ал фавитный указатель

идиома (продолжение) виртуальный конструктор, 23, 1 50 динамическое множественное наследование, 350 итератор, 1 70 конверт/письмо, 1 4 1 косвенный шаблон, 260 манипулятор/тело, 2 1 , 1 4 1 объекты задач, 369 ортодоксальная каноническая форма, 52 поддержка инкрементной загрузки, 324 подсчет ссылок. 7 1 . 1 47 указателей, 78 представитель, 37 4 программные потоки, 37 1 прототипы, 269, 277 символическая каноническая форма, 307 синглетный класс письма, 83 сообщество прототипов, 287 уборка мусора, 335 фреймовый прототип, 292 функтор, 374 иерархия классов, 97, 278 объектов, 278 прототипов, 29 1 реализации, 249, 264, 468 функций, 455 изоморфная структура, 204, 468 индуктивный метод, 39 1 инициализация, 32, 9 1 , 1 17, 468 объектов классов, 34 порядок, 337 инкапсуляция, 60, 127, 239 деталей реализации, 305 примитивных типов, 342 экземпляров классов, 37 1 инкрементная разработка, 305 интерфейс, 468 толстый, 2 13, 233, 244, 29 1 топкий, 2 1 4 исключение, 375, 468 искусственный интеллект, 2 1 5 исходный файл, 360 итеративная разработка, 1 0 1 итератор, 1 70, 1 7 1 , 468

к каноническая форма, 53, 58 ортодоксальная, 52, 1 4 1 , 307 символическая, 307, 3 1 7 каркас, 362, 468 каркасное программирование, 233. 468

Карно карты, 359 класс, 468 абстрактный, 104, 1 1 5, 140, 233 базовый, 60, 98, 1 04, 186 вариант, 269 виртуальный, 186 заместитель, 468 конверта, 83, 1 4 1 , 1 5 1 , 468 контейнерный, 59 концепция, 205 метакласс, 294 неполный, 140 оболочка, 342 объявление, 49 письма, 83, 1 4 1 , 468 производный, 60, 98 прототип, 269 синглетный, 83 субкласс, 98 суперкласс, 98 тонкий, 2 1 4 чисто абстрактный, 234 комплексное число, 28 компоновка, 306, 383, 468 конверт, 83, 1 4 1 , 1 5 1 , 468 конкретный тип данных. 52, 468 константа, 40 константная функция класса, 43 константный объект, 42 конструктор, 3 1 , 1 1 6, 468 автономный, 332 виртуальный, 23, 1 48, 150, 1 55, 286 копирующий, 55, 3 1 3 обобщенный, 285, 332 по умолчанию, 54, 3 1 3, 468 контейнер, 59, 468 контейнерный класс, 59, 468 контекст, 37 1 , 468 копирование глубокое, 2 1 логическое, 55 поверхностное, 2 1 , 55 полупространственное, 335 физическое, 55 копирующий конструктор, 55, 3 13, 468 корневой базовый класс, 468 косвенный шаблон, 260, 468 курсор, 1 7 1 , 468 куча, 468

л левостороннее значение, 468 Лисков принцип подстановки, 228 логическое копирование, 55, 468 локальная переменная, 376

Ал фавитный м макрос, 453 манипулятор, 2 1 . 72. 1 4 1 . 373, 468 массив, 64, 232, 468 международная организация по стандартизации, 468 мета-возможности, 2 1 , 468 метакласс, 294. 468 метод, 29, 468 близнецов, 1 55, 468 дедуктивный, 392 индуктивный, 39 1 механизм, 357. 362. 468 многократное использование, 231, 269 множесmенное наследование, 1 83, 1 9 1 , 468 динамическое, 1 84, 349 статическое, 1 84, 349 модуль, 359, 365, 468 мультиметод, 1 80, 343, 468 мультиобработка, 365, 469 мусор, 333, 469

н наложение указателей, 335, 469 наследование, 266, 469 виртуальное, 1 96 динамическое, 1 84, 349 закрытое, 1 1 1 , 231 множесmенное, 1 83, 1 84, 1 9 1 , 349 открытое, 1 10, 231 с переопределением, 469 с подменой, 234 с расширением, 469 с сокращением, 237, 469 статическое, 1 84, 349 невытесняющее планирование, 368 неоднозначность, 469 непереносимый код, 469 неполный класс, 140, 469 нисходящее проектирование, 209, 451 нулевой указатель, 193 нуль-символ, 94 Ньютона метод. 1 98

о область видимости. 40, 469 предметная, 204 реализации, 204 решения, 204 обобшенная объектная система lisp. 469 обобщенный конструктор, 285, 469 обобщенный прототип, 469

указател ь

475

оболочка, 186, 469 обработка исключений, 293, 355, 375, 376, 469 обратный вызов, 37 4, 469 общая память, 267 объединение, 1 59, 469 объект, 469 автономный. 2 1 2 зомби, 379 и класс, 2 1 5 константный, 4 2 серверный, 372 синглетный, 85 составной, 83, 1 4 1 объектная диаграмма, 2 2 1 объектная инверсия, 469 объектная парадигма, 1 30 объектно-орие1пированное программирование, 19. 1 19. 130. 203. 469 объектно-ориентированное проектирование, 203, 209, 469 объектно-ориентироваtшый анализ. 2 1 2 объявление, 469 глобальной переменной. 360 класса, 49 оператора, 31 О обязательство, 469 оконная система, 350 омоним, 228 оператор, 469 бинарный, 32 2 класса, 29 операторная функция, 68, 78, 469 операционная система, 356 опережающая ссылка, 300, 469 оптимизация, 2 1 1 ориентированный ациклический граф, 1 86 ортодоксальная каноническая форма, 52, 58, 307 открытое наследование, 1 10, 23 1 , 469 открытый доступ, 27 открытый член. 27, 469 очередь, 368

п пакет, 40, 362 пакетирование, 355, 469 параллельная обработка, 366, 469 параметр функции. 382 параметризованный тип, 1 98, 256, 469 параметрический полиморфизм, 251, 290, 469 паскаль. язык программирования, 451 перегрузка, 469 в классе, 66 глобальная, 66

476

Алфавитный указатель

перегрузка (продолжение) операторов, 25, 52, 56, 143 функций, 33. 3 1 5 передача по значению, 384 по ссылке, 384 переключение контекста, 372, 469 перенаправление, 80, 147. 2 1 1 . 231, 469 переопределение, 469 перечисляемый тип, 360 письмо, 1 4 1 , 469 планирование, 355, 365, 368, 469 поверхностное копирование, 2 1 , 55, 469 подмена, 1 29, 469 подмножество, 205, 470 подсистема, 358, 36 1 , 470 подставляемая функция, 36. 49, 470 подстановка, 470 подстрока, 94 подсчет ссылок, 7 1 . 72, 78, 307, 322, 333, 470 строк, 72 указателей, 78, 3 1 7 полиморфизм, 9 8 , 1 19, 128, 470 включения, 290, 470 динамическое наследование. 352 определение, 1 1 9 параметрический, 25 1 . 290 полосовой фильтр, 176 полупространственное копирование, 335, 470 пользовательский тип данных, 28, 381 , 470 пометка, 470 помеченная структура, 470 помеченный класс, 470 поразрядное копирование, 4 1 6 порядок инициализации, 337 поток данных, 203 программный, 366 пошаговое уточнение, 45 1 предварительная пометка, 334, 470 предметная область, 204 представитель, 374, 470 преобразование типа, 193, 262, 470 прикладное программирование, 163 примесь, 1 84, 470 примитивный тип, 342 принцип изоморфной структуры, 204 подстановки Лисков, 228, 470 присваивание, 55, 85, 4 1 4 проверка типов наследование, 97 эффективность, 25 языки программирования, 20, 52, 143

программирование аппликативное, 1 75, 176 блочно-структурное, 451 каркасное, 233 объектно-ориентированное, 130, 203 прикладное, 1 63 функциональное, 175 программный поток, 366, 368. 369, 470 проектирование, 23, 2 1 2 нисходящее, 209, 451 объектно-ориентированное, 24. 203. 209 процедурное, 7 1 структурное. 209 производный класс, 60, 470 каноническая форма, 1 4 1 определение, 98 пространство имен, 372 протокол доступа к каналу для данных, 470 прототип, 207, 277, 278, 470 абстрактный, 289 автономный, 288. 333 базовый, 289 обобщенный, 288, 333 фреймовый, 292 функции, 382 процесс. 365 процессор виртуальный, 365 пузырьковая сортировка, 453

р разделение времени, 366 распорядитель сообщества прототипов, 287, 470 распределенная обработка, 366, 373 реализация, 2 1 1 рекурсивное сканирование, 333 Рунге-Кутта метод, 198

с селектор типа, 1 2 1 , 1 3 1 , 470 семантическая сеть, 2 1 5, 470 семантическая совместимость, 225 сервер имен, 29 1 серверный объект, 372 сеть семантическая, 2 1 5 цифровая, 1 04 сильная типизация, 23, 470 сильное связывание, 470 символическая каноническая форма, 307, 3 1 7 символическая константа, 4 0 символический язык, 303 синглетный класс, 83, 470 синглетный объект, 85, 470

Алфавитный указател ь

синхронизация, 370 система встроенная, 245, 305 контроля типов. 1 26, 470 оконная, 350 системная политика восстановления, 379 системная структура динамическая, 365 статическая, 356 слабая типизация, 23, 470 слот, 292, 470 создание экземпляра, 470 сокрытие данных, 2 4 1 сообщение, 367 сообшество прототипов. 287, 470 составной объект, 83, 1 4 1 , 470 состояние объекта, 470 специализация, 207 спецификатор доступа, 1 1 0, 470 список истории, 457 ссылка, 55, 87, 384, 4 1 4, 470 висячая, 335 опережающая, 300, 469 статическая системная структура, 356 статическая функция, 39, 381 , 470 статическое множественное наследование, 1 84, 349, 470 стек, 34 структура, 28, 470 изоморфная, 204 статическая, 356 структурное проектирование, 209 субкласс, 98, 470 субпарадигматическое восстановление, 377, 470 субтип, 205 субтипизация, 224 суперкласс, 98, 471 суперпарадигматическое восстановление, 377, 471 сушность, 23, 206, 2 1 0. 471 счетчик ссылок, 4 7 1

т таблица виртуальных функций, 324, 471 тело, 471 класса, 21, 141 объекта, 373 тестирование, 2 1 1 тип данных, 97, 471 абстрактный, 28, 52, 38 1 встроенный, 1 1 7 конкретный, 52 концепция, 204

477

тип данных (продолжение) параметризованный, 198, 256 перечисляемый, 360 пользовательский, 28, 38 1 примитивный, 342 типизация динамическая, 23 сильная, 23 слабая, 23 толстый интерфейс, 2 1 3, 233, 244, 29 1 , 471 тонкий интерфейс, 2 1 4 тонкий класс, 2 14, 47 1 традиционная простая телефонная услуга, 1 04, 4 7 1 транзакционная диаграмма, 2 1 0, 358, 471 транзакция, 2 14, 357, 47 1

у уборка мусора, 78, 1 4 1 , 322, 333, 335, 47 1 вызов деструкторов, 336 отказоустойчивость, 377 указатель, 55, 79, 4 7 1 глобальный, 320 на функцию, 386 нулевой, 1 93 преобразование, 1 19 управление доступом, 1 05 вертикальное, 60, 106 rоризоитальное, 60, 1 1 0 памятью, 32, 55, 80, 84, 1 43, 303 условная компиляция, 50

ф файл заголовочный, 49, 360, 383 исходный, 360 файловый дескриптор, 342 файловый сервер, 268 физическое копирование, 55, 4 7 1 фрагментация памяти, 158, 335 фрейм, 292, 47 1 фреймовый прототип, 292 функтор, 49, 1 7 1 , 1 76, 471 функциональная декомпозиция, 45 1 функциональное программирование, 175 функция виртуальная, 49, 1 19, 1 2 1 , 1 3 1 , 1 85, 1 9 1 , 350 глобальная, 38 1 класса, 26. 29, 39, 43, 381 , 471 константная, 43 обратного вызова, 374 операторная, 68, 78

478

Ал фавитный указатель

функция (продолжение) подмена, 1 29 подставляемая, 36, 49 статическая, 39. 38 1 чисто виртуальная, 140, 266 шаблонная, 256

ц

ш шаблон, 198. 256, 260, 47 1 шаблонная функция, 256, 47 1

э ЭВОЛЮЦИЯ

класса, 279 прототипа, 294, 4 7 1

цифровая сеть с комплексными услугами, 104, 471

ч чисто абстрактный базовый класс, 234, 47 1 чисто виртуальная функция, 140, 266, 47 1 член объекта закрытый, 28 защищенный, 28 открытый, 27

схемы, 279, 47 1

экземпляр

создание, 9 1

SI язык проrраммирования анархический, 23 диктаторский. 23 с двойной иерархией, 278 с единой иерархией, 278 символический, 303

Джеймс О. Коплиен П рогра м м и рование на С++. Классика Главный

Перевел с английского Е. Матвеев

редактор

Заведующий редакцией

проекта редактор

Руководитель Научный

CS

Е. Строганова А. Кривцов А . Жданов

Е. Матвеев

Литера�урный редактор Иллюстрации Художник

А. Жданов А. Санжаревский А. Моносов,

Корректоры Верстка

Л. Адуевская И. Смирнова Р. Гришанов

ИД № 05784 от 07.09.01 . 1 1 . 1 1 .04. Формаr 70XI00/16. Ус:л. п . л . 38,7. Тираж 3000 экз. Заказ .№ 1044. 000 «Питер ПрИНТ». 194044, Санкт-Петербург, пр. Б. Сампсониевский, дом 29а. НалоrоВВJ1 nьrота - общерос:сиiiский юша:ификатор проду1