Книга
Книга
Дж.Либерти
C++. ЭНЦИКЛОПЕДИЯ ПОЛЬЗОВАТЕЛЯ
Книга C++. Энциклопедия пользователя содержит обзор актуальных тем, связанных с языком программирования C++.
В книге рассматриваются вопросы объектно-ориентированного анализа и проектирования, универсального языка программирования UML и разработки объектных моделей. Достаточно подробно представлены вопросы реализации приложений, а также стандартная библиотека шаблонов STL.
Особое внимание уделено обработке данных, живучести объектов и шифрованию, архитектуре CORBA и модели СОМ.
Прилагаемый CD-ROM содержит исходные тексты примеров, рассмотренных в книге.
Книга рассчитана на читателей, которые имеют начальные сведения и знания
по программированию на языке C++.
Оглавление
Введение13
Часть I. Объектно-ориентированное программирование15
Глава 1 .Объектно-ориентированный анализ и проектирование16
Построение моделей17
Разработка программ: язык моделирования17
Разработка программ: процессы18
Замысел20
Анализ требований20
Примеры использования20
Анализ приложения27
Анализ систем27
Планирование выпуска документов27
Иллюстративный материал28
Артефакты28
Проектирование29
Что такое классы29
Преобразования30
Статическая модель31
Динамическая модель37
Резюме39
Глава 2. Проектирование классов в C++40
Перевод диаграмм классов в C++41
Стандартные классы41
Шаблонные классы42
Служебные классы42
Ассоциации43
Агрегации47
Обобщение49
Перевод диаграмм взаимодействия в C++50
Реализация диаграмм совместных работ и диаграмм последовательности 50 действий в C++
Перевод диаграмм состояний в C++55
Перевод диаграмм активности в C++57
Резюме59
ГлаваЗ.Наследование, полиморфизм и повторное использование60
программных кодов
Преимущества наследования61
Объектно-ориентированные связанные списки61
Разработка связанного списка62
Реализация связанного списка62
Абстрактные классы67
Перекрытие чисто виртуальных методов70
Виртуальные деструкторы71
Полиморфизм, реализованный путем перегрузки методов класса71
Управление памятью74
Проблемы, сопровождающие перегрузку других операторов77
Оператор присваивания78
Перегрузка операторов увеличения80
Виртуальный конструктор копий83
Множественное наследование83
Проблемы множественного наследования84
Множественное наследование и включение87
Резюме88
Часть II. Вопросы реализации89
Глава 4. Управление памятью90
Управление памятью и указатели91
Расход памяти94
Распределение массивов94
Паразитные, болтающиеся и дикие указатели95
Указатели const96
Указатели const и функции-члены const96
Передача по ссылке96
Передача указателя const97
Возврат ссылки на объект, который не находится в области97
видимости
Указатель, указатель, указатель99
Указатели и исключения100
Использование автоуказателей104
Подсчет ссылок108
Резюме118
Глава 5. Использование каркасов приложений119
Microsoft Foundation Classes120
Приступая к изучению120
Другие мастера122
В перспективе122
Архитектура приложения122
Многопоточность122
Кооперативная многопоточность против вытесняющей123
Проблемы вытесняющей многопоточности123
Пример для изучения124
Создание потоков125
Пример129
Служебные классы137
Классы манипулирования строками137
Классы времени137
Документы и представления138
Представления139
Резюме147
Глава 6. Контейнерные классы библиотеки STL148
Определение и реализация шаблонов149
Определение и реализация шаблонов функций149
Определение и реализация шаблонов классов149
Последовательные контейнеры150
Контейнер-вектор150
Контейнер-список163
Контейнер-дека172
Стеки173
Очереди175
Приоритетные очереди177
Связанные контейнеры178
Контейнер-запись178
Множественные записи187
Контейнер-набор189
Множественные наборы189
Вопросы производительности190
Использование стандартной библиотеки C++190
Конструирование типов элементов191
Резюме192
Глава 7. Итераторы и алгоритмы STL193
Классы итераторов194
Позиция внутри контейнера194
Типы итераторов контейнеров194
Базовый класс итераторов195
Вводные итераторы196
Выводные итераторы197
Пересылаемые итераторы197
Двунаправленные итераторы197
Итераторы произвольного доступа197
Итераторные операции197
Классы стандартных итераторов199
Объекты-функции200
Предикаты201
Арифметические функции202
Алгоритмы STL203
Операции немутирующих последовательностей203
Алгоритмы мутирующих последовательностей211
Операции сортировки и связности последовательностей221
Стандартные функции238
Резюме243
Глава 8. Исключение конфликтов имен244
Функции и классы, разрешаемые по именам245
Создание пространства имен248
Использование пространства имен250
Ключевое слово using252
Объявление using254
Псевдоним пространства имен255
Неименованное пространство имен255
Стандартное пространство имен256
Резюме257
Глава 9. Манипулирование типами объектов258
Оператор typeid()259
Класс type info259
Конструктор для класса type info260
Операторы сравнения260
Функция-член паше()261
Функция-член before()263
Оператор typeid() в конструкторах и деструкторах264
Неправильное использование typeid()264
Динамическое приведение объектов265
Оператор dynamic_cast()266
Операторы typeid() и dynamic cast269
Другие операторы приведения270
Оператор static_cast()270
Оператор reinterpret_cast()271
Когда использовать операторы dynamic_cast(), static_cast() или271
reinterpret_cast()
Оператор const_cast()272
Новые приведения против старых272
Резюме273
Глава 10. Настройка производительности приложения274
Функции inline вне определений классов275
Как избежать раскрытия программного кода реализации в279
распространяемых header-файлах
Анализ стоимости виртуальных функций и виртуальных базовых классов 280
Виртуальные функции280
Виртуальные базовые классы283
Компромиссы RTTI285
Управление памятью для временных объектов287
Резюме289
Часть III. Обработка данных291
Глава 11. Рекурсия и рекурсивные структуры данных292
Что такое рекурсия293
Числа Фибоначчи: рекурсивное определение293
Остановка рекурсии294
Рекурсивные структуры294
Обход рекурсивной структуры с помощью рекурсивной функции296
Цикл и хвостовая рекурсия299
Хвостовая рекурсия301
Непрямая рекурсия302
Рекурсия и стек302
Отладка рекурсивных функций303
Резюме304
Глава 12. Использование методов сортировки305
Анализ производительности алгоритмов306
Сравнение среднего, худшего и лучшего случаев306
Стабильностьсортировки307
Использование дополнительных способов хранения во время307
сортировки
Пузырьковая сортировка308
Анализ пузырьковой сортировки309
Сортировка вставками309
Анализ сортировки вставками311
Сортировка выбором311
Анализ сортировки выбором313
Быстрая сортировка313
Анализ быстрой сортировки315
Сортировка слиянием315
Анализ сортировки слиянием318
Сортировка по методу Шелла318
Анализ сортировки по методу Шелла320
Пирамидальная сортировка320
Анализ пирамидальной сортировки322
Выбор метода сортировки322
Генерирование тестовых данных323
Резюме325
Глава 13. Алгоритмы поиска данных326
Линейный поиск327
Анализ линейного поиска328
Поиск в отсортированном массиве328
Сопоставление с образцом330
Грубый алгоритм331
Представление образца331
Построение конечных автоматов332
Алгоритмы поиска на графе333
Поиск в глубину334
Поиск в ширину335
Сравнение поиска в глубину и поиска в ширину336
Поиск по первому наилучшему совпадению336
Реализация объектов графов337
Представление игры Tic-Tac-Toe340
Применение альфа-бета-отсечений340
Задача коммивояжера341
Внешний поиск342
Индексированный последовательный доступ342
Двоичные деревья342
Деревья 2-3-4343
Резюме344
Глава 14. Хеширование и синтаксический анализ345
Сравнение поиска и хеширования346
Функции хеширования346
Разрешение конфликтов347
Линейное повторное хеширование347
Нелинейное повторное хеширование347
Коэффициент загрузки(альфа)348
Связывание в цепочку349
Адресация областей памяти349
Синтаксический разбор358
Синтаксический разбор числовых выражений359
Синтаксический разбор строковых выражений360
Контекстно-свободная грамматика и синтаксический анализ361
Выполнение нисходящего синтаксического разбора для проверки361
правильности регулярных выражений
Резюме362
Часть IV. Живучесть объектов и шифрование363
Глава 15. Живучесть объектов364
Создание хранимых объектов365
Что такое В-дерево372
Запись В-дерева на диск374
Кэширование375
Определение размера страниц375
Определение количества страниц, которые могут одновременно375
находиться в памяти
Подкачка данных на жесткий диск375
Реализация В-дерева376
Как это работает401
Прогулка по программному коду403
Поиск414
Резюме416
Глава 16. Реляционные базы данных и живучесть417
Основные концепции реляционных баз данных418
Архитектура реляционной базы данных419
Ограничения и соображения420
Язык структурированных запросов SQL420
Нормирование420
Соединения421
Живучесть для реляционной базы данных421
Перемещения с идентификаторами объектов422
Использование пятен422
Скрытие деталей423
Непосредственное сохранение объектов423
Использование API БД423
Доступ к источникам данных ODBC423
Использование MFC425
Операторы SQL433
Установка характеристик БД434
Резюме434
Глава 17. Реализация живучести объектов с помощью реляционных 435 баз данных
Объекты в Oracle 8436
Типы объектов436
Ссылки на объекты437
Коллекции438
Использование внешних процедур, разработанных на языке C++439
Отображение UML-диаграмм на объектно-реляционную базу данных442
Проектирование базы данных442
Генерирование классов на C++443
Генерирование сервера445
Пример: Система заказа покупок448
Описание системы448
Глава 18. Объектно-ориентированные базы данных451
Обзор объектно-ориентированных баз данных452
Стандарт ODMG453
Приложение на C++ для ведения счетов453
Живучесть данных466
Схемы базы данных и средства захвата этой схемы468
Коллекции469
Итераторы471
Отношения471
Базы данных и транзакции475
Технические вопросы объектно-ориентированных баз данных487
Архитектура клиент/сервер487
Хранение данных и кластеризация объектов487
Передача данных488
Блокирование данных488
Резюме489
Глава 19. Защита приложений с помощью шифрования490
Краткая история шифрования491
Роль Национального бюро стандартов491
Понятие шифрования492
Коды492
Шифры493
Шифр Vernam495
Криптография по частному ключу497
Алгоритмы частного ключа497
Механизмы шифрования по секретному ключу498
Использование центров распространения ключей500
Криптография по общему ключу500
Метод головоломки Ральфа Меркле501
Многопользовательские криптографические методы Диффи-501
Хельмана
Метод RSA502
Использование Pretty Good Privacy502
Выбор простых чисел в PGP503
Использование случайных чисел в криптографии504
Шифрование файлов с помощью PGP504
Ограничения в криптографии506
Юридические ограничения на криптографию507
Криптографические атаки507
Атака грубой силы507
Криптоанализ508
Взлом файла, зашифрованного программой PGP508
Цифровые подписи509
Public Key Cryptography Standard(PKCS)509
Digital Signature Standard(DSS)509
Неотрицание510
Коммерческие криптографические продукты510
Безопасные Web-клиенты510
Безопасные почтовые клиенты511
Продукты для защиты рабочего стола512
Резюме514
Часть V. Распределенные вычисления515
Глава 20. CORBA516
Теория и обоснование517
Минимальная среда CORBА518
Каркас объектной технологии518
ПОР: объектное склеивание519
Компонентная модель520
IDL: соглашение связывания520
Сравнение IDL с определением класса C++521
Брокер объектных запросов522
Время жизни объектов522
Среды разработки523
Сравнение сред CORBA523
Способность ORB к взаимодействию523
Создание клиента С+524
Генерирование заглушки524
Связь с ORB525
Вызовы методов525
Завершенное клиентское приложение C++526
Создание сервера C++526
Генерирование скелета527
Реализация методов сервера527
Подсоединение класса сервера528
Загрузка ВОА в ORB528
Клиент Java530
Генерирование заглушки530
Запуск и программный код вызова метода530
Стратеги и тестирования531
Трассировка531
Службы мониторинга и регистрации531
Обработка исключений531
Удаленная отладка532
Служба имен и способность к взаимодействию532
Interoperable Object Reference (IOR)532
Именование контекстов533
Проблемы взаимодействия533
Производительность533
Перерасход памяти со стороны ORB534
Степень детализации интерфейса534
Ссылки на передаваемый объект534
Резюме534
Глава 21. COM535
Основы COM536
Архитектура COM537
Интерфейсы537
Интерфейс IUnknown541
COM-объекты543
Библиотеки типов545
Другие СОМ-технологии546
Использование COM-объектов в C++549
Использование интерфейсов Raw550
Использование интеллектуальных указателей551
Использование библиотек типов555
Создание COM-объектов в C++558
Множественное наследование558
Вложенные классы560
Использование классов tear off562
Резюме563
Дополнительная литература563
Глава 22. Java и C++564
Общие черты C++ и Java565
Комментарии565
Типы данных565
Операторы567
Операторы управления потоком568
Различия между C++ и Java568
Управление памятью568
Отсутствие указателей568
Отсутствие препроцессора568
Отсутствие деструктора568
Спецификаторы доступа569
Параметры метода569
Внешние функции570
Перечислители570
Строки571
Массивы571
Объектно-ориентированные возможности Java572
Классы572
Наследование576
Множественное наследование578
Обработка исключений579
Резюме579
Об авторах
Джесс Либерти (Jesse Liberty) является основателем и президентом компании Liberty Assotiates, Inc. (http://www. libertyassotiates.com), где он проводит обучение, консультации и практические занятия по вопросам создания объектно-ориентированного программного обеспечения. Либерти — автор многочисленных книг по объектно-ориентированному анализу и проектированию, а также по языку C++, пользующихся повышенным спросом. Он был ведущим инженером в области программного обеспечения компаний AT&T, Xerox и PBS, исполнял обязанности вице-президента компании Technology for Citibank. В настоящее время Либерти живет со своей семьей в пригороде Кембриджа (штат Массачусетс). С ним можно связаться по адресу: jliberty@libertyassotiates.com.
Вишваджит Аклеча (Vishwajit Aklecha) много лет работал в области разработки объектно-ориентированного программного обеспечения, ему присвоены степени бакалавра математики и магистра в области информационных технологий. Сейчас он работает в филиале International Software Operation компании Hewlett-Packard в Бангалоре Индия. Профессиональные интересы Вишваджита связаны с распределенными вычислительными системами, исследованием и разработкой больших ЭВМ и изучением объектной технологии. С ним можно связаться по адресу: vishwajit@technologist.com.
Стив Хайнс (Steve Haines) работает инженером по разработке программного обеспечения в среде Windows в компании ENGAGE games online, занимающейся разработкой видеоигр для Internet, где он отвечает за проектирование и реализацию критичных по времени ожидания технологий, построенных на базе Internet и других систем передачи данных. На протяжении своей профессиональной карьеры он занимается технологиями разработки программ компании Microsoft, и все свои знания и опыт посвятил построению изображений в играх в Internet. В настоящее время Стив является соискателем степени магистра в области информационных технологий по разделам формирующих технологий и мультимедиа в университете Южной Калифорнии. Он исполнял обязанности технического редактора и консультанта в издательствах Macmillan Computer Publishing и Addison-Wesley Longman. Писательская деятельность всегда была страстью Стива, и к ней он относится с особым энтузиазмом, унаследованным от своей бабушки, написавшей несколько пьес, которые в свое время были опубликованы.
Стивен Митчел (Steven Mitchell) выполняет работы в области связи и передачи данных. Он разработал значительное число приложений реляционных баз данных для внутреннего пользования, а в настоящее время участвует в проекте по разработке программ тестирования аппаратных и программных протоколов передачи данных. Стивен исполнял обязанности технического редактора нескольких книг по C++. Он проживает в городе Александрия (штат Виргиния).
Александр Николов (Alexander Nickolov) является системным программистом, специализирующимся на разработках моделей C0M/DC0M. На протяжении всей своей профессиональной деятельности он разрабатывал приложения на языке C++, работающие в Internet. В настоящее время Александр Николов работает консультантом по программному обеспечению в фирме GlobulCom Consulting и живет в городе Санта-Барбара (штат Калифорния). С ним можно связаться по адресу: agnickolov@geocities.com.
Чарльз Пейс (Charles Расе) — один из соавторов книги COBRA Unleashed. Он имеет 14-летний опыт разработки всевозможных видов современного программного обеспечения, написал множество компьютерных программ — от интерактивных обучающих игр до систем управления большими предприятиями. Чарльз мечтает предоставить разработчикам такие возможности, которые позволили бы им создавать приложения, основанные на новейших методиках разработки программных средств.
Магри Тхаккар (Meghraj Thakkar) — технический специалист компании Oracle Corp. Ему присвоена степень магистра информационных технологий и степень бакалавра электроники. Он имеет несколько свидетельств, удостоверяющих его высокие профессиональные качества, в том числе диплом системного инженера высшей квалификации Microsoft, Novell, Oracle, диплом консультанта высшей квалификации Lotus и другие. Магри прочитал несколько курсов в Калифорнийском университете в городе Ирвине. Он является соавтором нескольких книг, таких как Special Edition Using OracleS, Oracle Server и Unleashed Oracle Certified Professional — DBA, опубликованных издательством Macmillan Computer Publishing, а также подготовил и неоднократно проводил двухдневные занятия по теме "Поддержка Oracle в Windows NT" для служащих Oracle. В течение последних семи лет Магри приобрел опыт работы с различными программными продуктами фирмы Oracle.
Майкл Дж. Тоблер (Michael J. Tobler) — старший технический специалист компании BSI Consulting в Хьюстоне (штат Техас). Он накопил более чем 16-летний опыт работы над проектами разработки программ-
ного обеспечения, специализируясь на планировании, проектировании и разработке многоуровневых систем на языках C++ и Java. В настоящее время он является президентом Ассоциации пользователей Java в Хьюстоне. Майкл выступал соавтором книги The Waite Group's C++ How-To. Он также сделал немаловажное открытие: оказывается, что свободное падение в парашютном спорте подобно наркотической зависимости. Для переписки он предлагает свой адрес: ntobler@ibm.net.
Дональд Кси (Donald Xie) является старшим системным инженером и руководителем проекта электронных коммерческих приложений и развития Internet. Он также является преподавателем программирования на языке C++ в университете в Зифф-Девисе. Дональд живет со своей семьей в Перте (Австралия).
Стив Загибойло (Steve Zagieboylo) работает в сфере программного обеспечения с 1980 г., а разработками программного обеспечения на языке C++ занимается с 1989 года. Он сотрудничал с компаниями Lotus Development Corp., AT&T и некоторыми другими, а в настоящее время является президентом консалтинговой компании ZagNet (http://www.zag.net). Стив преподавал курс усовершенствованного объектно-ориентированного программирования на C++ в Гарвардской школе повышения квалификации, а в настоящее время продолжает читать курсы C++ при содействии университета в Зифф-Девисе. Не стесняйтесь побеспокоить его по электронной почте: Unleashed@ZAG.net.
Посвящение
Эта книга посвящается Робин, Рейчел и Стейси Либерти.
Благодарности
Как часто бывает в подобных случаях, появление этой книги стало возможными благодаря усилиям многих людей, фамилий и имен которых вы не найдете на обложке книги. Среди них следует отметить наиболее квалифицированного редактора Трейси Данкельбергер (Tracy Dunkelberger) из издательства Macmillan Computer Publishing. Она восстановила уверенность автора в том, что обязательства издательства Sams Publishing по созданию великолепных книг, которыми охотно пользуются программисты, будут выполнены.
Автор также хотел бы поблагодарить Шона Диксона (Sean Dixon) и Морина Мак-Дениэла (Maureen McDaniel) издательства Macmillan, старательно работавших над этой очень трудной книгой, которая стала значительно лучше, чем была, когда они ее отредактировали. За ошибки и путаницу несет ответственность только автор, в то время как высокое качество книги — это исключительно их заслуга, за что он приносит им свою благодарность. Автор хотел бы поблагодарить Бреда Джонса (Brad Jones) и Криса Денни (Chris Denny), которые благословили его на написание книги Sams Teach Yourself C++ in 21 days.
Автор также благодарит и выражает свою признательность Джону Франклину (John Franklin), Девиду и Адаму Маклинам (David and Adam Maclean) из издательства Wrox Publishing, Роберту Мартину (Robert Martin) и редакторам книги C++ Report — все они лелеяли труд автора и позволяли ему свободно заимствовать фрагменты из их публикаций.
Жена автора Стейси и дочери Робин и Рейчел поддерживают и стимулируют его работу, в частности, написание этой книги, в надежде увидеть ее логическое завершение.
Введение
Книга C++. Энциклопедия пользователя представляет собой обзор актуальных тем, связанных с языком программирования C++. Целью книги является подробное изложение каждой из этих тем, что позволит читателю в полной мере изучить язык C++.
Многие из представленных в книге тем могли бы составить отдельную книгу. Но из-за ограниченности объема рассмотрены только вопросы, необходимые для понимания основных технологий. Во многих случаях вы сможете убедиться в том, что предлагаемой информации достаточно для достижения конкретных целей.
Вопросы, рассматриваемые в книге
Часть I. Объектно-ориентированное программирование
Эта часть начинается с введения в объектно-ориентированный анализ и проектирование. Язык C++ дает гораздо больший эффект при наличии хорошо продуманной объектно-ориентированной модели, чем при разработке программного кода без всякого плана со всеми вытекающими из этого проблемами и ошибками. Существенное преимущество объектно-ориентированного программирования (ООП) вы сможете оценить после того, как проделаете необходимый анализ и затратите время на проектирование тщательно продуманного программного продукта. Глава 1 послужит отправной точкой на этом трудном, но заманчивом пути объектного моделирования. Наряду с этим вы ознакомитесь с основными понятиями универсального языка моделирования (UML — Unified Modelling Language), который постепенно становится промышленным стандартом.
Из главы 2 вы узнаете, как реализовать объектную модель в языке C++. Такое отображение проектной модели на программный код имеет важное значение в том случае, если вы хотите использовать весь потенциал языка C++ как языка объектно-ориентированного программирования.
В главе 3 эта тема продолжена с акцентом на то, как C++ поддерживает наследование и полиморфизм. Такое детальное исследование всех особенностей полиморфизма заложит основание для создания высококачественных коммерческих приложений на языке C++.
Часть II. Вопросы реализации
В главе 4 представлены усовершенствованные методы управления памятью. Здесь рассмотрены сложные проблемы, касающиеся обычных указателей и ссылок, автоуказателей и интеллектуальных указателей. В главе 5 рассматриваются каркасы приложений, и в этом контексте представлены такие актуальные проблемы, как многопоточность.
В этой части также предлагается подробная вводная информация о стандартной библиотеке шаблонов (Standard Template Library — STL). В главе 6 рассматриваются контейнерные классы библиотеки STL, а в главе 7 — итераторы и алгоритмы STL. В главе 8 читатели узнают о новых средствах C++ в стандарте ANSI — пространстве имен, с помощью которых можно избежать конфликтов имен при использовании библиотек независимых поставщиков.
В главе 9 в фокус вашего внимания попадают идентификация типов времени выполнения и новые операторы приведения типов, соответствующие требованиям ANSI. И наконец, в главе 10 вы узнаете, как настроить приложение таким образом, чтобы его производительность при заданном быстродействии и размере программного кода была оптимальной.
Часть III. Обработка данных
Часть III открывает глава 11, посвященная обсуждению более совершенных методов рекурсии. В главе 12 описываются алгоритмы сортировки, а в главе 13 рассказывается об объектно-ориентированном поиске. Итоги обсуждений подводятся в главе 14 с учетом методов хеширования и синтаксического разбора.
Часть IV. Живучесть объектов и шифрование
В главе 15 рассматривается устойчивость объектов и показано, как записывать объекты на жесткий диск и как управлять памятью при работе с ними. В главе 16 авторы возвращаются к анализу каркасов приложений и соединений баз данных ODBC (Object Data Base Connection — Соединения объектной базы данных),
а также библиотеки MFC (Microsoft Foundation Classes — Базовые классы Microsoft). В главе 17 этот анализ распространяется на живучесть объектов при использовании реляционных баз данных, а в главе 18 рассматриваются объектно-ориентированные базы данных. И наконец, в главе 19 обсуждаются методы шифрования, включая коды Диффи (Diffie), Хеллермана (Hellerman), Хоффмана (Hoffman) и Цезаря (Caesar); популярные методы шифрования и методы шифрования с ключом общего пользования, такие как PGP (Pretty Good Privacy), а также метод DES (DataEncryption Standard) и Clipper.
Часть V. Распределенные вычисления
В главе 20 рассматривается архитектура CORBA (Common Object Request Broker Architecture), глава 21 представляет собой введение в модель COM (Component Object Model). И наконец, в главе 22 проводятся исследования различий между языками Java и C++ и делается вывод, являются ли эти различия существенными или нет.
Что необходимо знать перед тем, как приступить к чтению этой книги
Предполагается, что, перед тем как приступать к чтению книги C++. Энциклопедия пользователя, вы, по меньшей мере, ознакомились с одним из учебных пособий по C++ и/или программировали на языке C++ не менее полугода. Опытные программисты найдут в этой книге детали по различным вопросам, с которыми они ранее не сталкивались; менее опытные почерпнут в ней множество новых идей, получат новую информацию и параллельно пройдут хорошую практику.
Необходимое программное обеспечение
Все программы, о которых идет речь в этой книге, могут быть созданы и выполнены в Microsoft Visual C++ или в любом 32-разрядном компиляторе, совместимом с ANSI. В то время как учебные программы, представленные в главах, посвященных библиотеке MFC, могут быть откомпилированы только на компьютерах с Windows (Windows 95/98 или Windows NT), практически все другие программы, приводимые в качестве примеров в книге, могут быть откомпилированы в любой операционной системе.
Никакое другое программное обеспечение не понадобится — только редактор, компилятор и компоновщик. Если вы работаете в интегрированной среде разработки, такой как, например, Visual C++, значит, вы уже обеспечены всем необходимым. Авторы попытались отладить все программы, приведенные в книге, на различных компиляторах. Эти программы работают в Microsoft Visual C++, и в силу этого авторы рекомендуют вам воспользоваться именно этим компилятором.
Как читать книгу
Воспринимайте эту книгу как набор "белых листов", посвященных актуальным вопросам языка C++. Вы можете читать главы в произвольном порядке, углубляясь в те области, которые представляют для вас наибольший интерес. При этом имейте в виду, что авторы не выступают с претензиями дать всеобъемлющие сведения по каждой теме, они хотели предоставить лишь вводную информацию по каждой из этих актуальных тем, на основе которой читатели смогут проводить свои собственные исследования.
Читать эту книгу надо так, как советовал Хампти Дампти: от начала до конца. Альтернативный способ состоит в том, что вы читаете три первые главы, а затем переходите к чтению тех глав, которые представляют для вас наибольший интерес.
В любом случае надеемся, что вы с удовольствием прочитаете эту книгу. С Джесси Либерти вы можете связаться по адресу: jliberty@libertyassociates.com. Поддержка данной книги организована на Web-узле Sams (http://samapublishing.coni), а также на собственном Web-узле автора (http://www.libertyassociates.com).
ЧАСТЬЧАСТЬОбъектноориентированное программирование
ЧАСТЬ
ЧАСТЬ
Объектно-ориентированный анализ и проектирование
В ЭТОЙ ГЛАВЕ
Построение моделей
Разработка программ: язык моделирования
Разработка программ: процессы
Замысел
Анализ требований
Проектирование
Глава 1
Язык C++ создавался как мост между объектно-ориентированным программированием (ООП) и языком С, наиболее популярным в мире языком программирования, предназначенным для разработки коммерческого программного обеспечения (ПО). ~
Язык С разрабатывался как промежуточное звено между языками программирования высокого уровня, предназначенными для разработки бизнес-приложений, таких как COBOL, и высокопроизводительным, обеспечивающим больший контроль над компьютером, но неудобным в использовании языком ассемблера. Назначение С состоит также и в том, чтобы внедрить в практику идеи структурного программирования, смысл которых состоит в декомпозиции проблем на составные части, представляющие собой часто повторяющиеся совокупности операций, именуемые процедурами.
Программы, какие пишут в конце 1990-х годов, намного сложнее, чем те, какие писали в начале текущего десятилетия. Программами, написанными в процедурных языках, намного труднее управлять, они неудобны в эксплуатации и не допускают расширений. Графические пользовательские интерфейсы, Internet, цифровые телефонные линии связи и множество новых технологий значительно повысили сложность проектов как раз в то время, когда требования к качеству пользовательского интерфейса существенно ужесточились.
Перед лицом все увеличивающейся сложности разработчики провели критический анализ состояния всей отрасли ПО. То, что они обнаружили, было, мягко говоря, обескураживающим — устаревшее ПО, отрывочное, со множеством недостатков, пораженное невыявленными программными ошибками, ненадежное и дорогостоящее. Реализация проектов, как правило, выходит за пределы сметной стоимости, сами проекты попадают на рынок с большим опозданием. Стоимость эксплуатации и использования такого ПО становится запредельной и влечет за собой непроизводительные затраты огромных средств.
Объектно-ориентированное программное обеспечение указывает путь из этой пропасти. Объектно-ориентированные языки программирования устанавливают устойчивую связь между структурами данных и методами манипулирования ними. Что еще более важно, в рамках ООП снимаются заботы о структурах данных и функциях, манипулирующих этими данными, вместо этого необходимо думать об объектах.
Окружающий мир насыщен различными объектами: автомобилями, собаками, деревьями, облаками, цветами. Вещи и предметы. Каждая вещь, каждый предмет характеризуется признаками (быстрый, дружественный, коричневый, красивый). Для большинства из них характерен собственный алгоритм поведения (двигается, лает, растет, порождает дождь, увлажняется). Мы не думаем о данных, описывающих собаку, и о том, как мы должны ими пользоваться — мы думаем о собаке как об объекте из окружающего мира, о том, на что она похожа и что делает.
Построение моделей
Если мы хотим овладеть сложностью, мы должны построить модель Вселенной. Цель модели заключается в том, чтобы построить содержательное абстрактное представление реального мира. Такое абстрактное представление должно быть проще, чем реальный мир, но оно должно отображать реальный мир с такой точностью, чтобы мы могли воспользоваться этой моделью в целях предсказания поведения предметов реального мира.
Классическим примером может служить детский глобус. Модель — это еще не реальная вещь, мы никогда не перепутаем детский глобус с Землей, однако первый достаточно хорошо отображает вторую, так что мы можем многое узнать о Земле, изучая глобус.
Разумеется, при этом имеют место существенные упрощения. На глобусе никогда не бывает дождей, наводнений, землетрясений и т.п., однако можно воспользоваться глобусом, чтобы вычислить, сколько времени понадобится, чтобы проделать путь от дома до Индианополиса на самолете, если автору придется ехать и объясняться с руководством фирмы Sams, почему он не успевает сдать в срок рукопись.
От модели, которая по сложности не уступает моделируемому предмету, мало проку. По этому поводу у Стивена Райта (Steven Wright) есть превосходная шутка: "У меня есть карта в масштабе один дюйм в одном дюйме. Я живу в квадрате Е5".
Целью этапа проектирования объектно-ориентированного ПО является построение хороших моделей. При этом необходимо помнить о двух важных компонентах: языке моделирования и процессе.
Разработка программ: язык моделирования
Язык моделирования — это не самый важный аспект объектно-ориентированного анализа и проектирования, но, к сожалению, ему обычно уделяется наибольшее внимание. Язык моделирования — всего лишь соглашение о том, как мы рисуем нашу модель на бумаге. Мы легко можем согласиться изображать классы
Часть I
в виде треугольников, а отношение наследования — в виде точечной линии. Если мы пришли к такому соглашению, то модель герани должна иметь вид, показанный на рис. 1.1.
Из рис. 1.1 видно, что Герань — это специальный вид Цветов. Если мы с вами согласимся рисовать наши диаграммы наследования (обобщение/конкретизация) подобным образом, то прекрасно будем понимать друг друга. Впоследствии мы, по-видимому, захотим смоделировать множество различных сложных отношений, тем самым разработаем собственный более сложный набор правил и соглашений по составлению диаграмм.
Разумеется, нужно будет давать пояснения к нашим соглашениям всем тем, с кем мы работаем, и каждый новый служащий или сотрудник должен изучить эти соглашения. Мы можем взаимодействовать с другими компаниями, которые разработали свои собственные соглашения, и нам потребуется время, чтобы найти общие соглашения с ними и устранить неизбежные в этих случаях недоразумения.
Будет удобнее, если каждый работающий в этой области программирования согласится признать некоторый общий язык моделирования. (Например, если каждый разработчик согласится на обычную устную речь, но при условии, что за один раз рассматривается не более одного предмета.) Универсальным языком (lingua franca) для разработки программного обеспечения является UML (Unified Modeling Language). В задачу языка UML входит отвечать на такие вопросы: "Как изобразить на бумаге отношение наследования?" Рис. 1.1, изображающий герань, в UML принимает вид, показанный на рис. 1.2.
РИСУНОК 1.1. Обобщение/конкретизация.РИСУНОК 1.1. Обобщение/конкретизация.РИСУНОК 1.2. Изображение специализации в UML.РИСУНОК 1.2. Изображение специализации в UML.
РИСУНОК 1.1. Обобщение/конкретизация.
РИСУНОК 1.1. Обобщение/конкретизация.
РИСУНОК 1.2. Изображение специализации в UML.
РИСУНОК 1.2. Изображение специализации в UML.
В языке UML классы изображаются в виде прямоугольников, наследование — в виде линии со стрелкой. Интересно отметить, что на такой диаграмме стрелка направлена от специализированного класса в сторону более общего класса. Направление стрелки противоречит интуитивному представлению большинства разработчиков, однако это не имеет никакого значения; если имеется соответствующее соглашение, система работает превосходно.
Детали UML достаточно просты. Диаграммы нетрудно использовать или понять, автор будет давать соответствующие пояснения по мере изучения этой главы и книги, так что мы не будем изучать язык вне контекста. И хотя об языке UML можно написать целую книгу, все дело в том, что в основном вы будете пользоваться лишь небольшим подмножеством обозначений UML, и это подмножество нетрудно освоить по ходу изложения материала.
Разработка программ: процессы
Процесс объектно-ориентированного анализа и проектирования — намного более сложное и ответственное дело, чем язык моделирования. Тем не менее, о нем вы слышите гораздо реже. Это можно объяснить тем, что вопросы, касающиеся языков моделирования, более или менее успешно решены, выбрана также и область применения UML. В то же время дискуссии по поводу процесса в самом разгаре.
Методолог — это специалист, который разрабатывает или изучает один или большее число методов. Обычно методологи разрабатывают и публикуют свои собственные методы. Метод представляет собой моделирующий язык и процесс. Тремя ведущими методологами являются Грейди Буч (Grady Booch), автор метода Буча, Айвер Джекобсон (Ivar Jacobson), который разработал метод проектирования объектно-ориентированного программного обеспечения, и Джеймс Рамбоф (James Rumbaugh), который разработал технологию объектного моделирования (ОМТ — Object Modeling Technology). Эти три специалиста объединили свои усилия в целях создания Objectory — метода и коммерческого продукта компании Rational Software, Inc. Все три специалиста являются служащими компании Rational Software, где их коллеги любовно называют их тремя мушкетерами.
Глава 1
В настоящей главе кратко описывается Objectory. Автор не приводит его точного описания, поскольку он не является сторонником слепой преданности умозрительной теории — он больше заинтересован в поставках готового продукта, чем в приверженности к какому-либо методу. Другие методы также имеют свои достоинства, а автор старается придерживаться эклектической точки зрения и собирать фрагменты по мере продвижения и объединять их в единую работоспособную конструкцию.
Процесс проектирования программного обеспечения носит итеративный характер. Это означает, что по мере продвижения разработки ПО мы многократно проходим через весь процесс, чтобы проникнуться лучшим пониманием предъявляемых требований. Проект определяет направление реализации, но детали, обнаруженные во время реализации, вносят коррективы в проект в виде обратной связи. Самое главное то, что мы даже не пытаемся разрабатывать важный проект как единую упорядоченную последовательность действий; вместо этого мы неоднократно возвращаемся к конкретным фрагментам проекта, постоянно
совершенствуя проект и улучшая его реализацию.
РИСУНОК 1.3. Каскадный метод.РИСУНОК 1.3. Каскадный метод.Итеративная разработка отличается от каскадного проектирования. В условиях каскадного проектирования выход одной ступени становится входом для следующей, при этом возврат на предыдущие ступени исключается (рис. 1.3), требования передаются разработчику, после чего разработчик создает проект (и следит за воплощением своего замысла) и передает его программисту, который и реализует его. В свою очередь, программист передает программные коды QA-персоналу (QuestionAnswer), который производит его тестирование и передает заказчику. Гладко в теории, стихийное бедствие на практике.
РИСУНОК 1.3. Каскадный метод.
РИСУНОК 1.3. Каскадный метод.
В условиях итеративного проектирования некий мечтатель предлагает
свою концепцию, после чего мы приступаем к воплощению замысла. По мере того как мы углубляемся в детали, концепция расширяется и углубляется. Если требования четко сформулированы, мы начинаем проектные работы, отчетливо сознавая, что все вопросы, которые возникнут в процессе проектных работ, могут потребовать уточнения. В процессе работы над проектом мы создаем прототипы, а затем воплощаем их в программный продукт. Проблемы, возникающие в процессе проектирования, приводят к внесению изменений в проект, они могут оказать влияние на наше понимание требований. Самое главное заключается в том, что мы каждый раз разрабатываем и реализуем только конкретные фрагменты полного программного продукта, многократно возвращаясь к фазам проектирования и реализации.
Несмотря на то что этапы процесса многократно повторяются, их почти невозможно описать таким циклическим образом. Поэтому они могут быть описаны в такой последовательности: замысел, анализ, проект, реализация, тестирование, откат. На практике в процессе создания единого продукта мы многократно проходим через каждый из этих этапов. Итеративный процесс трудно представить и понять, если
зацикливаться на каждом этапе; поэтому будем описывать их один за другим. В итеративном процессе разработки можно выделить следующие этапы:
Постановка задачи
Анализ
Проектирование
Реализация
Тестирование
Распространение
Постановка задачи представляет собой формулировку замысла. В одном предложении излагается грандиозная идея. Анализ — это процесс понимания требований. Проектирование — процесс создания модели классов, из которых вы будете генерировать программный код. Реализация представляет собой написание программных кодов на языке C++; тестирование предназначено для того, чтобы убедиться в том, что вы все сделали правильно, а распространение — это доставка программного продукта заказчикам. Здесь указано самое главное. Все остальное — это несущественные подробности.
СПОРНЫЕ МОМЕНТЫ
Бесконечные споры ведутся относительно того, что происходит на каждом этапе итеративного процесса проектирования, спорят даже о том, как назвать эти этапы. На практике это не имеет никакого значения. Фактически основные этапы любого процесса заключаются в том, чтобы выяснить, что вы хотите построить, найти решение поставленной задачи и реализовать проект.’
Часть I
Если группы новостей и списки рассылки объектной технологии мало чем отличаются друг от друга, то существенные особенности объектно-ориентированного анализа и проектирования ярко выражены. В этой главе автор предлагает практический подход к процессу, который послужит краеугольным камнем для построения архитектуры вашего приложения. В последующих главах мы сосредоточимся на обсуждении деталей реализации проекта на языке C++.
Цель всей этой работы состоит в том, чтобы написать такой программный код, который решает поставленную задачу, надежен, допускает расширения и удобен в эксплуатации. Но самое главное — это задача создания высококачественного программного кода в условиях ограничений по времени и по финансированию.
Замысел
Все крупные программные проекты начинаются с замысла. Конкретный индивидуум проникается мыслью, что неплохо было бы создать программный продукт, наделенный теми или иными свойствами. Редко, когда грандиозные замыслы становятся плодами коллективной мысли. Самая первая стадия объектно-ориентированного анализа заключается в том, чтобы сформулировать этот замысел в виде отдельного предложения (или максимум, в виде короткого абзаца). Замысел становится руководящим принципом разработки, и коллектив, который формируется для реализации этого замысла, может обращаться к нему — и при необходимости вносить в него изменения — по мере продвижения этих разработок.
Даже если авторство формулировки замысла принадлежит отделу маркетинга, все равно должно быть назначено ответственное лицо, выполняющее роль постановщика. Именно в его обязанности входит хранение священного огня. По мере продвижения к цели постановка задачи совершенствуется. Сроки разработки и выхода программного продукта на рынок могут повлиять на то, что вы намерены завершить на первой итерации, но постановщик должен следить за воплощением главной идеи, чтобы то, что создается, отражало основной замысел с высокой точностью. Только твердая приверженность замыслу, неукоснительное выполнение обязательств приводят к успешному завершению проекта. Если вы измените замыслу, ваш проект обречен на неудачу.
Анализ требований
Фаза постановки задачи, в процессе которой формулируется замысел, весьма кратка. Это может быть всего лишь секундное озарение, плюс время, необходимое для записи на бумаге того, что имел в виду постановщик. Часто бывает так, что вы, будучи специалистом по ООП, подключаетесь к проекту уже после того, как замысел сформулирован.
Некоторые компании путают понятия замысла и постановку задачи с требованиями. Четкий замысел необходим, но недостаточен. Прежде чем перейти к анализу, вы должны четко представлять, как будет использован программный продукт и как он будет функционировать. Назначение фазы анализа состоит в том, чтобы определить и сформулировать соответствующие требования. Результатом фазы анализа является документ, содержащий требования. В первом разделе требований должен содержаться анализ примеров использования.
Примеры использования
Движущей силой анализа, проектирования и реализации являются примеры использования. Пример использования есть не что иное, как высококачественное описание того, как будет использован программный продукт. Примеры использования приводят в движение не только анализ, но и проектирование, они помогают определить классы, играют исключительно важную роль при тестировании программного продукта.
Создание четко очерченного и исчерпывающего набора примеров использования может оказаться единственной важной задачей анализа. Именно в этой части вы в наибольшей степени зависите от экспертов в области применения; именно они обладают самой полной информацией о требованиях, предъявляемых коммерческой областью применения, которые вы пытаетесь собрать.
Примеры использования практически не учитывают особенностей пользовательского интерфейса и совсем не учитывают внутренней структуры системы, которую вы создаете. Любая система или лицо, взаимодействующее с проектируемой системой, называется действующим субъектом.
В качестве обобщения приведем некоторые определения:
Пример применения: описание того, как будет использоваться ПО.
Специалисты в предметной области: специалисты, имеющие опыт работы в конкретной области коммерческой деятельности, для которой создается программный продукт.
Действующий субъект: любое лицо или система, взаимодействующая с системой, которую вы разрабатываете.
Глава 1
Пример использования — это описание взаимодействия между действующим объектом и самой системой. Для целей анализа примеров применения система рассматривается как "черный ящик". Действующий субъект "посылает сообщение" системе, и непременно что-то случается: возвращается информация, меняется состояние системы, космический корабль меняет курс или нечто подобное.
Идентификация действующего субъекта
Важно отметить, что не все действующие субъекты — люди. Системы, которые взаимодействуют с системой, которую вы строите, — также действующие субъекты. Следовательно, если мы создавали автоматизированный кассовый аппарат, действующими субъектами могут быть как клиенты, так и банковские служащие — равно как и система контроля за ипотеками. Наиболее важными характеристиками действующих субъектов являются следующие:
Являются внешними по отношению к проектируемой системе
Взаимодействуют с проектируемой системой
Инициация часто бывает наиболее трудной фазой анализа примеров применения. Часто бывает так, что наилучшим способом начать анализ является "мозговой штурм". Просто начните составлять список людей и систем, которые будут взаимодействовать с вашей новой системой. Помните о том, что когда мы обсуждаем людей, то имеем в виду роли — банковского служащего, руководителя, клиента и др. Одно лицо может исполнять несколько ролей.
Что касается примера ATM-машины (Automated Teller Machine — автоматизированный кассовый аппарат), приведенного чуть выше, считаем, что такой список будет содержать следующие роли:
Клиент
Банковский персонал
Учрежденческая система
Лицо, которое наполняет ATM-машину деньгами и материалами
На первых порах нет необходимости расширять этот очевидный список. Для того чтобы вы освоили операцию генерирования примеров применения, достаточно того, чтобы вы выполнили генерирование трех или четырех действующих субъектов. Каждый из этих субъектов взаимодействует с системой по-своему. Мы хотим интегрировать эти взаимодействия в наши примеры применения.
Определение первых примеров применения
Начнем с роли клиента. Для клиента можно применить процедуру мозгового штурма к следующим примерам использования:
Клиент проверяет состояние собственного баланса
Клиент кладет деньги на свой счет
Клиент снимает деньги со своего счета
Клиент переводит деньги с одного счета на другой
Клиент открывает счет
Клиент закрывает счет
Должны ли мы проводить различие между "Клиент депонирует деньги на свой текущий счет" и "Клиент депонирует деньги на свой сберегательный счет" или мы должны объединить эти действия (что мы и сделали в предшествующем списке) в операцию "Клиент вносит денежную сумму на свой счет"? Ответ на этот вопрос зависит от того, имеет ли значение такое различие в рассматриваемой области.
Чтобы определить, являются ли эти действия одним или двумя примерами применения, вы должны получить ответ на такие вопросы: являются ли механизмы различными (существенно ли отличаются операции клиента над счетами) и являются ли результаты этих операции различными (реагирует ли система на эти действия по-разному). Ответ на оба вопроса, касающихся вкладов, — "нет". Клиент откладывает денежные суммы на оба счета практически одинаково, и результаты его действий практически аналогичны; ATM-машина отреагирует на них тем, что увеличит остаток на соответствующем счете.
При условии, что поведение и реакция действующего субъекта и системы в большей или меньшей степени идентичны, независимо от того, сделан вклад на текущий или сберегательный счет, оба эти примера использования фактически являются одним и тем же примером. Позднее, когда мы будем создавать сцена-
Часть I
рии примера применения, мы можем выполнить два изменения и посмотреть, сможем ли мы вообще обнаружить какое-либо различие.
В процессе изучения каждого действующего субъекта вы сможете обнаружить дополнительные примеры использования, пытаясь найти ответы на следующие вопросы:
■ Почему этот действующий субъект пользуется этой системой?
Клиент использует эту систему для получения наличных денег, чтобы положить деньги на хранение или проверить остаток на счете.
■ Какой результат действующий субъект ожидает получить от каждого требования?
Положить деньги на счет или получить наличные деньги, чтобы сделать покупку.
■ Что заставило действующий субъект воспользоваться услугами этой системы в данный момент?
Он получил денежные поступления либо намеревается сделать покупку.
■ Что должен сделать действующий субъект, чтобы воспользоваться этой системой?
Вставить карту ATM в приемное устройство машины.
Нам нужен пример применения для регистрации клиента в системе.
■ Какую информацию должен действующий субъект предоставить системе?
Он должен ввести персональный идентификационный номер.
Мы должны воспользоваться примерами применения для получения и корректировки данных персонального идентификационного номера.
■ Какую информация надеется получить действующий субъект от системы?
Состояние счета и прочее.
Вы часто можете найти дополнительные примеры применения, сосредоточиваясь на атрибутах объектов области применения. У клиента есть имя — PIN (Personal Identification Number — Персональный идентификационный номер) — и номер счета, должны ли мы создавать примеры применения, чтобы иметь возможность манипулировать этими объектами? Счет характеризуется номером, состоянием, данными о состоявшихся транзакциях, должны ли мы вводить эти элементы в примеры применения?
После того как мы выполним детальный анализ примеров применения клиента, следующим шагом при воплощении списка примеров применения состоит в создании примеров использования для каждого из остальных действующих субъектов. Представленный ниже список представляет собой набор примеров применения для случая АТМ-машины:
■ Клиент проверяет состояние собственного счета
■ Клиент кладет деньги на свой счет
■ Клиент снимает деньги со своего счета
■ Клиент переводит деньги с одного счета на другой
Клиент открывает счет
Клиент закрывает счет
Клиент вносит запись в свой счет
■ Клиент просматривает последние транзакции
■ Банковский служащий вносит запись в специальный доверительный счет
■ Банковский служащий выполняет корректировку счета клиента
■ Учрежденческая система выполняет обновление счета пользователя, отражающего внешнюю деятельность
■ Изменения в счете пользователя отображаются в учрежденческой системе
■ Машина ATM сигнализирует о том, что наличность для раздачи закончилась
Банковский технический персонал загружает машину ATM наличностью и принадлежностями
Создание модели области применения
Как только вы получите в первом приближении набор примеров использования, можете начинать разработку требований с использованием детализированной модели области применения. Модель области применения — это документ, в котором содержатся все имеющиеся у вас сведения об области применения. В рамках модели области применения вы создаете объекты области применения, описывающие все объекты,
Глава 1
ссылки на которые имеются в примерах использования. На этой стадии пример ATM включает такие объекты, как клиент, банковский персонал, учрежденческие системы, текущий счет, сберегательный счет и др.
Для каждого из этих объектов области применения мы хотим собрать такие важные данные как имя объекта (например, имя клиента, счета и т.п.), независимо от того, является ли этот объект действующим субъектом или нет, главные атрибуты объекта и его поведение и т.д. Многие моделирующие средства поддерживают сбор этой информации в описаниях "класса". На рис. 1.4 показано, как производится сбор этой информации программой Rational Rose.
Очень важно сознавать, что описываемые категории не являются объектами проекта. Это скорее описание того, как устроен мир, но не описание работы системы.
Мы можем отобразить схематично взаимоотношения между объектами в области применения примера с машиной ATM, воспользовавшись языком UML. Мы можем пользоваться одними и теми же средствами на любой стадии проекта.
Например, можно зафиксировать тот факт, что текущий и сберегательный счет — суть специализации более общего понятия банковского счета на языке UML, касающегося классов и отношений обобщения (рис. 1.5).
РИСУНОК 1.4. Сбор информацииРИСУНОК 1.4. Сбор информациипрограммой Rational Rose.программой Rational Rose.РИСУНОК 1.5. Специализация.РИСУНОК 1.5. Специализация.
РИСУНОК 1.4. Сбор информации
РИСУНОК 1.4. Сбор информации
программой Rational Rose.
программой Rational Rose.
РИСУНОК 1.5. Специализация.
РИСУНОК 1.5. Специализация.
На этом риунке прямоугольники представляют собой различные объекты области применения, а линии со стрелкой обозначают обобщения. Язык UML определяет, что эти линии чертятся в направлении от специализированного класса к более общему "базовому" классу. Таким образом, как Текущий счет, так и Сберегательный счет указывают на Банковский счет, что означает, что каждый из них являются специализацией Банковского счета.
ПРИМЕЧАНИЕ
Нелишне подчеркнуть то, что мы показываем на данной стадии, — это отношения между объектами в области применения. Позднее вы, возможно, решите ввести объект CheckingAccount в свой проект, равно как и объект Bankaccount, и сможете реализовать это отношение с помощью наследования, но это уже будут решения, относящиеся к стадии разработки. При анализе все, что от нас требуется, — это документирование восприятия этих объектов в области ! » применения.
UML — это язык моделирования с богатыми возможностями, среди которых имеется множество отношений. Однако основные отношения, которые используется в анализе, — это обобщение (или специализация), ограничение и сопоставление.
Обобщение
Обобщение часто отождествляют с наследованием, но при этом имеет место четко выраженное и существенное различие между этими понятиями. Обобщение описывает отношение, наследование — это программная реализация обобщения, т.е. как мы представляем обобщение в программных кодах.
Из определения обобщения следует, что производный объект есть подтип базового типа. Таким образом, текущий счет есть банковский счет. Отношение обладает симметрией: банковский счет объединяет в себе одинаковые особенности поведения и общие атрибуты текущего и сберегательного счетов.
Часть I
Во время анализа области применения мы стремимся отобразить такого рода отношения в том виде, в каком они существуют в реальном мире.
Включение
Часто конкретный объект состоит из множества подобъектов. Например, в конструкцию автомобиля входят двигатель, руль управления, шины, двери, радиоприемник и др. Текущий счет состоит из сальдо, данных об операциях, идентификатора клиента и др. Мы утверждаем, что текущий счет содержит эти элементы; в моделях включения имеется это отношение. Язык UML отображает отношение включения с помощью линии с ромбом от содержащего объекта к включаемому объекту, как показано на рис. 1.6.
Из рисунка видно, что текущий счет "имеет" сальдо. Вы можете использовать сочетание этих диаграмм, чтобы отобразить сложные наборы отношений (рис. 1.7).
РИСУНОК 1.6. Отношение включения.РИСУНОК 1.6. Отношение включения.РИСУНОК 1.7. Отношения между объектами.РИСУНОК 1.7. Отношения между объектами.
РИСУНОК 1.6. Отношение включения.
РИСУНОК 1.6. Отношение включения.
РИСУНОК 1.7. Отношения между объектами.
РИСУНОК 1.7. Отношения между объектами.
РИСУНОК 1.8. Связь.РИСУНОК 1.8. Связь.степени с Объектом В.степени с Объектом В.
РИСУНОК 1.8. Связь.
РИСУНОК 1.8. Связь.
степени с Объектом В.
степени с Объектом В.
На рисунке показано, что и текущий счет и сберегательный счет являются банковскими счетами и что у всех банковских счетов имеется сальдо и данные об операциях.
Связи
Третье отношение, которое обычно фиксируется на стадии анализа области применения, — простейшая связь (ассоциация). Связь предполагает, что два объекта знают друг о друге и тем или иным способом взаимодействуют. Это определение становится намного более точным на стадии проектирования, но для целей анализа предполагаем, что только Объект А взаимодействует с Объектом В, но ни один из них не содержит другой и не является специализацией другого. В UML показывают эту связь в виде прямой линии между объектами (рис. 1.8).
На этом рисунке показано, что Объект А связан в той или иной
Разработка сценариев
Теперь, когда у нас есть предварительный набор примеров использования и средств для отображения на диаграммах отношений между объектами области применения, мы готовы формализовать примеры использования и приступить к более глубокому их изучению.
Каждый пример использования можно разбить на последовательности сценариев. Сценарий — это описание специфического набора обстоятельств, которые выделяют его из различных взаимозависимых элементов примера использования. Например, "Клиент снимает деньги со своего счета" может иметь такие сценарии:
Клиент обращается с требованием снять со счета $300, получает эту сумму, затем помещает наличность в приемное устройство, после чего система печатает квитанцию о получении.
Клиент обращается с требованием снять со счета $300, однако остаток на его счету составляет всего лишь $200. Клиент получает информацию о том, что на его текущем счете недостаточно наличности, чтобы совершить эту операцию.
Глава 1
Клиент обращается с требованием снять $300 с текущего счета, однако он уже снял сегодня со счета $100, а разрешается снять не более $300 в течение дня. Клиент получает информацию о возникшей проблеме, после чего принимает решение.
Клиент обращается с требованием снять со счета $300, но в аппарате закончилась бумага, на которой печатаются квитанции. Клиент получает информацию о возникшей проблеме, после чего принимает решение, получать или не получать деньги без квитанции.
И так далее. Каждый сценарий исследует различные варианты в рамках исходного примера использования. Часто такими вариантами являются исключительные ситуации (не счете нет требуемой суммы, в машине нет нужной суммы и т.д.). Иногда исследуются нюансы принятия решения в самом примере использования (например, хочет ли клиент перевести деньги прежде, чем их снимать со счета). Не каждый возможный сценарий нужно исследовать. Мы ищем те сценарии, которые затрагивают требования системы или детали взаимодействия с действующим субъектом.
Выработка руководящих принципов
Для документирования каждого сценария вам придется вырабатывать руководящие принципы. Вы включаете эти руководящие принципы в постановку задачи. Позаботьтесь о том, чтобы в каждом сценарии были отражены следующие моменты:
Предварительные условия — какие условия должны быть выполнены, прежде чем сценарий начнет работать
Триггеры — механизм запуска сценария
Какие действия выполняют действующие субъекты
Какие результаты и изменения вызваны системой
Какую обратную связь получают действующие субъекты
Имеются ли повторяющиеся действия и что вызывает их завершение
Описание логического течения сценария
Что вызывает окончание сценария
Последующие условия — какие условия должны быть выполнены по завершении сценария
В дополнение к этому у вас появится необходимость присвоить имена каждому примеру использования и каждому сценарию. Таким образом, может возникнуть такая ситуация:
Пример использования:
Клиент снимает наличность со счета
Сценарий:
Успешное получение со счета наличных денег
Предварительные условия:
Клиент уже зарегистрирован в системе
Триггер:
Клиент запрашивает "снятие со счета"
Описание:
Клиент предпочитает снять сумму наличными с текущего счета. На счете достаточно денег, машина ATM заправлена бумагой, а сеть исправна и функционирует. Машина ATM просит указать сумму, снимаемую со счета, а клиент называет $300 — допустимую на текущий момент сумму. Машина выдает $300 и печатает квитанцию, а клиент получает деньги и квитанцию.
Последующие условия:
На дебет счета клиента относится сумма $300, а клиент получает наличными $300.
Этот пример использования может быть представлен с помощью простой диаграммы, показанной на рис. 1.9.
РИСУНОК 1.9.
Диаграмма примера использования.
Часть I
На этой диаграмме практически нет никакой другой информации, кроме описания взаимодействия между действующим субъектом (клиентом) и системой, которое выполнено на высоком уровне абстракции. Польза от этой диаграммы несколько увеличится, если вы покажете взаимодействие между примерами использования. Автор говорит "польза несколько увеличится", поскольку в рассматриваемом случае возможны только два взаимодействия: "использует" и "расширяет". Стереотип "использует" указывает на то, что один пример использования это подмножество другого. Например, нельзя снять сумму со счета, не зарегистрировавшись в системе. Мы можем показать это отношение с помощью диаграммы, представленной на рис. 1.10.
На этом рисунке показано, что пример использования Снимаемая со счета сумма "использует" пример использования Регистрация и тем самым полностью реализует Регистрацию как часть Снимаемой со счета суммы.
Пример использования "расширяет" предназначен для того, чтобы указать условное отношение и нечто такое, что напоминает наследование, однако что касается примеров использования "использует" и "расширяет", то в объектном моделировании еще так много путаницы, что некоторые разработчики просто отказались от примера "расширяет", чувствуя, что его смысл недостаточно хорошо им понятен. Автор пользуется стереотипом "использует" в тех случаях, когда без него ему пришлось бы копировать и вставлять в нужное место весь пример использования, а стереотипом "расширяет" — только когда он пользуется примером использования при определенных условиях.
Диаграммы взаимодействия
Хотя диаграмма примера использования сама по себе может иметь всего лишь ограниченную ценность, тем не менее, существуют диаграммы, которые можно ассоциировать с этим примером использования, что самым кардинальным образом может улучшить документирование и понимание взаимодействий. Например, мы знаем, что сценарий Снимаемая со счета сумма представляет взаимодействия между следующими объектами области применения: клиент, текущий счет и пользовательский интерфейс. Можно документально зафиксировать это взаимодействие с помощью диаграммы взаимодействий, представленной на рис. 1.11.
Диаграмма взаимодействий, показанная на этом рисунке, отображает подробности сценария, которые могут оказаться далеко не очевидными при чтении текста. Объекты, которые взаимодействуют, — это объекты области применения, а вся конструкция АТМ/UI (Automated Teller Machine/User interface) рассматривается как единый объект с одним конкретным банковским счетом, вызываемым с любой степенью детализации.
Этот сравнительно простой пример с машиной ATM показывает всего лишь воображаемую совокупность взаимодействий, однако выявление специфики этих взаимодействий может оказать существенную помощь для понимания как самой проблемы, так и требований вашей новой системы.
КлиентКлиентПользовательскийинтерфейсПользовательскийинтерфейсТекущийсчетТекущийсчет
Клиент
Клиент
Пользовательскийинтерфейс
Пользовательскийинтерфейс
Текущийсчет
Текущийсчет
РИСУНОК 1.10. Стереотип "использует ".РИСУНОК 1.10. Стереотип "использует ".Контрольный баланс,финансовое положение и пр.>Разрешение наобратную операциюДебит $300Контрольный баланс,финансовое положение и пр.>Разрешение наобратную операциюДебит $300
РИСУНОК 1.10. Стереотип "использует ".
РИСУНОК 1.10. Стереотип "использует ".
Контрольный баланс,финансовое положение и пр.>
Разрешение наобратную операцию
Дебит $300
Контрольный баланс,финансовое положение и пр.>
Разрешение наобратную операцию
Дебит $300
Отмена запроса
Указать возможные варианты-<
Указать сумму и счет
Наличность дляраспределения через автомат<
Квитанция о
получении запроса>
Печать квитанции
РИСУНОК 1.11. Диаграмма взаимодействий в языке UML.
Глава 1
Создание пакета
Поскольку вы генерируете множество примеров использования для любой достаточно сложной проблемы, язык UML позволяет сгруппировать ваши примеры использования в пакеты.
Пакет подобен каталогу или папке — он является совокупностью моделирующих объектов (классов, действующих субъектов и т.д.). Чтобы правильно ориентироваться в хитросплетениях примеров использования, их можно группировать по любой характеристике, лишь бы это имело смысл для решения вашей проблемы. Таким образом, можно группировать примеры использования по типу счета (любой признак, характеризующий текущие и сберегательные счета), по кредиту или дебету, по типу клиента или любой другой характеристике, которая имеет для вас смысл. Что еще важнее, один и тот же пример использования может фигурировать в нескольких различных пакетах, обеспечивая большую гибкость на стадии проектирования.
Анализ приложения
В дополнение к примерам использования документ с требованиями будет содержать предположения и ограничения вашего клиента, его требования к аппаратным средствам и операционным системам. Утверждение требований приложения является прерогативой вашего заказчика — обычно вы формулируете их на стадии проектирования и реализации, но решающее слово в их принятии остается за заказчиком.
Требования приложения чаще всего обусловливаются необходимостью стыковки с существующей системой. В такого рода случаях понимание того, что делают существующие системы и как они работают, является немаловажной составляющей вашего анализа.
В идеальном случае вы анализируете проблему, находите ее решение, а затем решаете, какая платформа и операционная система наилучшим образом подходят для реализации проекта. Такой сценарий настолько идеален, насколько и редок. Гораздо чаще заказчик должен нести определенные затраты на изготовление специальной операционной системы или аппаратной платформы. Коммерческие планы заказчика зависят от того, насколько хорошо работает ваше ПО в рамках существующей системы, вы должны выявить соответствующие требования на достаточно ранней стадии проектирования и проводить дальнейшую разработку с учетом принятых решений.
Анализ систем
Некоторые программные продукты написаны из расчета работы в автономном режиме и обеспечивают взаимодействие только с конечным пользователем. Однако часто возникает необходимость интерфейса с существующей системой. Системный анализ — это процесс сбора всех подробностей о системах, с которыми вы взаимодействуете. Будет ли ваша новая система сервером, предоставляющим услуги существующей системе, или клиентом? Будет ли у вас возможность согласовывать интерфейс между системами или вам необходимо приноравливаться к существующему стандарту? Будет ли другие системы работать стабильно или вам все время придется стрелять по движущейся мишени?
На эти и на другие связанные с ними вопросы необходимо найти ответы на стадии анализа, прежде чем вы начнете проектировать новую систему. Кроме того, вы, возможно, захотите неявно выявить конкретные ограничения, которые, по предположению, имеют место при взаимодействии с другими системами. Не увеличат ли они времени ответа вашей системы? Не приведет ли это к повышению требований к новой системе, к увеличению расхода ресурсов и машинного времени?
Планирование выпуска документов
Как только вы поймете, что должна делать ваша система и каким должно быть ее поведение, можете приступать к созданию документа, определяющего сроки выполнения и сметную стоимость проекта. Часто сроки исполнения в директивном порядке определяются заказчиком: "Вы должны закончить работы за 18 месяцев". В идеальном случае вы изучаете эти требования и определяете, какое время потребуется на разработку проектора и реализацию проектных решений. Но это идеальный случай, на практике большая часть систем все-таки выпускается в установленные сроки и без превышения сметной стоимости, однако весь вопрос в том, какие функциональные средства реализованы в ней за указанные сроки и при выделенном уровне финансирования.
При подсчете сметной стоимости проекта и разработке временного графика необходимо придерживаться следующих принципов:
Если устанавливается диапазон какого-либо показателя, то чаще всего его верхний предел выбирается с большой долей оптимизма.
Часть I
Закон Либерти (Liberty Law) утверждает, что все длится дольше, чем вы ожидаете, даже если вы делаете поправку на Закон Либерти.
В условиях реальной жизни возникает необходимость ввести приоритеты в вашей работе, иначе вы ее не закончите — это же так просто. Очень важно, чтобы, когда истекают сроки, то, что у вас есть, работало и чтобы оно "тянуло" на первую версию продукта. Если вы строите мост и не успеваете сдать его в эксплуатацию в срок, то, если у вас нет шансов построить на нем дорожку для велосипедистов, это, конечно, очень плохо, но, тем не менее, вы все же можете открыть движение по мосту и начать взимать плату за проезд. Но если все сроки вышли, а мост построен только наполовину, это еще хуже.
При подготовке плановых документов очень важно сознавать, что они никуда не годятся. На такой ранней стадии фактически невозможно дать надежную оценку длительности процесса разработки проекта. Как только будет готов документ с требованиями, вы получаете в свое распоряжение удобный рычаг управления длительностью разработки проекта, достаточно точный метод оценки длительности реализации и сможете прикинуть, как долго будет длиться тестирование системы. Затем вы должны предусмотреть резерв времени, достигающий, по меньшей мере, от 20 до 25% от всех временных затрат, этот резерв можно сокращать по мере продвижения работ и накопления необходимых данных.
ПРИМЕЧАНИЕ
Включение резерва времени в плановый документ не является предлогом для отказа от плановых документов. Это скорее . предостережение не слишком полагаться на них на достаточно ранней стадии. По мере реализации проекта ваше представление о том, как работает система будет становиться все более полным, а ваши оценки — все более точными.
Иллюстративный материал
Завершающим фрагментом постановки задачи является выбор иллюстративного материала. Под иллюстративным материалом мы подразумеваем диаграммы, картинки, фототипы и любые другие визуальные отображения, призванные помочь облегчить понимание и разработку графического пользовательского интерфейса вашего продукта.
Для многих больших проектов можно разработать полный прототип, чтобы помочь представить себе (а также и заказчикам), каким будет поведение системы. Для некоторых коллективов разработчиков прототип становится "живой" постановкой задачи; "реальная" система разрабатывается в целях реализации функциональных средств, продемонстрированных в прототипе.
Артефакты
В конце каждой фазы анализа и разработки вы будете подготавливать целый ряд документов или "артефактов". В табл. 1.1 представлены документы, подготавливаемые на стадии анализа. Они нужны заказчику для того, чтобы быть уверенным в том, что вы правильно понимаете, что от вас требуется, конечным пользователям — чтобы поддерживать обратную связь с проектом и устанавливать руководящие принципы проектирования, и коллективам разработчиков — для правильной разработки и реализации программных кодов. Многие из этих документов содержат материал, исключительно важный как для коллектива, осуществляющего документирование системы, так и для Службы обеспечения качества (Quality Assurance), который позволяет им предвидеть поведение системы.
Таблица 1.1. Документы, созданные на стадии анализа хода разработки проекта
Документ
Описание
Сообщение о примере использования
Документ, содержащий подробную информацию о примерах использования, сценариях, стереотипах, предварительных условиях и иллюстративных материалах
Анализ области применения
Документ и диаграммы, описывающие отношения между объектами области применения
Диаграммы анализа совместной работы
Диаграммы совместной работы описывают взаимодействия между объектами проблемной области
Диаграмма анализа действий
Диаграмма действий, описывающая взаимодействия между объектами проблемной области
Анализ систем
Отчет и диаграммы, описывающие системы низкого уровня и аппаратные системы, на основе которых строится проект
Глава 1
Документ
Описание
Документ с анализом приложения
Отчет и диаграммы, описывающие требования пользователя, характерные для данного конкретного проекта
Отчет об ограничениях на рабочие
Отчет, описывающий рабочие характеристики и ограничения, накладываемые
характеристики
на них заказчиком
Стоимостной и плановый документы
Отчет, в котором используются диаграммы Ганта (Gantt) и Перта (Pert) и который отражает плановые сроки, этапы и соответствующие стоимости
Проектирование
Анализ базируется на понимании проблемной области, в то время как целью проектирования является получение решения. Проектирование — это процесс воплощения нашего понимания задачи в модель, которая может быть реализована в виде программного продукта. Результат этого процесса материализуется в виде проектной документации.
Проектная документация содержит два раздела: Проектирование классов и Архитектурные механизмы. В свою очередь, раздел Проектирование классов состоит из подразделов статического (в котором детализируются различные классы, их отношения и характеристики) и динамического проектирования (в котором рассматриваются подробности взаимодействия классов).
В разделе Архитектурные механизмы проектной документации подробно определяется, как достигается устойчивость объектов, их параллельное функционирование, система распределенных объектов и др. В оставшейся части этой главы рассматриваются вопросы отображения в проектной документации аспектов проектирования классов; в остальных главах книги приводятся пояснения того, как реализовать различные архитектурные механизмы.
Что такое классы
Будучи программистом, работающим на языке программирования C++, вы должны иметь определенный опыт построения классов. Формальная методология проектирования требует, чтобы вы отделяли классы C++ от классов, используемых на стадии проектирования, хотя между ними имеется тесная связь. Класс C++, который создается с помощью программных кодов, — это реализация класса, который вы спроектировали. Оба эти виды классов изоморфны: каждый класс проекта соответствует классу в программном коде, но не следует их путать. Естественно, классы проекта можно реализовать и в другом языке, однако синтаксис определений классов при этом может быть изменен.
Сделав это замечание, в дальнейшем в большинстве случаев мы будем говорить об этих классах, не делая между ними различий, поскольку эти различия проявляются на уровне высокой абстракции. Когда вы говорите, что в вашей модели класс Cat содержит метод Meow(), это следует понимать так, что вы поместите в свой класс на языке C++ также и метод Meow().
Вы отображаете классы этой модели в диаграммах UML и в то же время включаете классы C++ в программный код, который можно компилировать. Различие носит смысловой характер, хотя оно не очень заметно.
В любом случае самым большим камнем преткновения для многих начинающих проектировщиков является поиск исходного набора классов и понимание того, как функционирует правильно спроектированный класс. Один из простейших методов — выписать все сценарии примера использования, после чего создать свой класс для каждого существительного.
Рассмотрим следующий сценарий примера использования:
Клиент намеревается снять наличность с текущего счета. На счете имеется нужная сумма, достаточно наличности и квитанций в машине ATM, а сеть исправна и функционирует. Машина ATM запрашивает клиента указать сумму, снимаемую со счета, а клиент просит выдать ему $300, сумму, допустимую для получения на текущий момент. Машина выделяет $300 и печатает квитанцию, а заказчик получает деньги и квитанцию.
Вы можете разложить этот сценарий на следующие классы:
Customer (Клиент)
Cash (Наличность)
Checking (Текущий счет)
Account (Счет)
Объектно-ориентированное программирование Часть I
Receipts (Квитанция)
ATM (Машина ATM)
Network (Сеть)
Amount (Сумма)
Withdrawal (Снятие со счета )
Machine (Машина)
Money (Деньги)
Вы можете объединить синонимы и получить следующий список, а затем создать классы для каждого из этих существительных:
Customer (Клиент)
Cash (Наличность (деньги, сумма, снятие со счета))
Checking (Текущий счет)
Account (Счет)
Receipts (Квитанция)
ATM (Машина ATM)
Network (Сеть)
Как бы там ни было, но для начала это вполне приемлемый подход. Затем можно перейти к построению диаграмм очевидных отношений между объектами этих классов, как показано на рис. 1.12.
РИСУНОК 1.12.
Предварительные классы.
Преобразования
То, что вы начали делать в предыдущем разделе, недостаточно, чтобы выделить существительные из сценария и начать преобразования объектов, пригодных для анализа области применения, в объекты, используемые на стадии проектирования. Но это лишь первый шаг в правильном направлении. Часто многие объекты области применения на стадии проектирования имеют заменители. Объект называют заменителем, чтобы отличать физическую сумму, фактически выдаваемую машиной ATM, от объекта в вашем проекте, который есть не что иное, как просто умозрительная абстракция, представленная в программном коде.
Вы, по всей вероятности, обнаружите, что большая часть объектов области применения в проекте получила изоморфное представление, т.е. существует взаимно однозначное соответствие между объектом области применения и объектом проекта. Но иногда, однако, один объект области применения представлен на стадии проектирования целой серией объектов. Другой раз последовательность объектов области применения может быть представлена одним объектом на стадии проектирования.
Из рис. 1.12 можно легко убедиться в том, что мы уже учли тот факт, что CheckingAccount — это специализация Account. Мы не ставим задачу найти обобщающее отношение, это очевидно само по себе, поэтому мы его фиксируем с самого начала. Аналогично из анализа области применения мы знаем, что машина ATM выдает Cash и Receipts, так что мы включили эту информацию непосредственно на стадии проекта-
Глава 1
рования. Отношение между Customer и CheckingAccount менее очевидно, так что мы пока воздержимся от его определения.
Другие преобразования
Как только вы выполните преобразование объектов области применения, можете приступать к поиску других объектов, фигурирующих на стадии проектирования. Удобной отправной точкой могут служить интерфейсы. Каждый интерфейс между вашей новой системой и существующими системами должен быть инкапсулирован в класс интерфейсов. Взаимодействие с базой данных какого-либо типа весьма неплохо вписывается в класс интерфейсов.
Такие интерфейсные классы обеспечивают инкапсуляцию протоколов передачи данных и тем самым защищают программный код от изменений в другой системе. Интерфейсные классы предоставляют возможность вносить изменения в собственный проект или в проект других систем, не разрушая при этом остальных программных кодов. Пока две системы продолжают поддерживать согласованный интерфейс, они могут функционировать независимо друг от друга.
Манипулирование данными
Аналогичным образом вы создаете классы для манипулирование данными. Если нужно преобразовать данные из одного формата в другой (например, градусы по Фаренгейту в градусы по Цельсию или из британской системы мер в метрическую), вы, возможно, захотите инкапсулировать эти манипуляции в классе манипулирования данными. Можете пользоваться этим методом при обмене сообщениями в формате, требуемом другими системами, или при передаче данных через сеть Internet — короче говоря, всякий раз, когда возникает необходимость манипулирования данными в специальном формате, вы должны инкапсулировать соответствующий протокол в классе манипулирования данными.
Представления
Каждое "представление" или "отчет", которые генерирует ваша система (или, если вы генерируете большое число отчетов, каждый набор отчетов), является кандидатом для включения в соответствующий класс. Правила, которым должен подчиняться отчет — как для него производится сбор информации и как она отображаются, — могут быть эффективно инкапсулированы в пределах класса представлений.
Устройства
Если ваша система взаимодействует с устройствами или управляет их работой (такими, как принтеры, модемы, сканнеры и др.), специфика протокола взаимодействия с устройством должна быть инкапсулирована в соответствующий класс. Опять-таки, создавая классы для интерфейса с конкретным устройством, вы можете подключать новые устройства с новыми протоколами и оставлять без изменений остальные программные коды; вы всего лишь создаете новый интересный класс, который поддерживает тот же интерфейс (или производный интерфейс), и этого вполне достаточно.
Статическая модель
Как только вы установили предварительный набор классов, можно приступать к моделированию их отношений и взаимодействий. В целях сохранения ясности изложения материала сначала дадим пояснения к статической модели, а затем рассмотрим динамическую модель. В процессе практического проектирования вы будете свободно переходить от статической модели к динамической и наоборот, добавляя соответствующие детали в каждую из них — и фактически добавляя новые классы и наделяя их необходимыми свойствами.
Статическая модель охватывает три области, представляющих интерес для проектировщиков: обязанности, атрибуты и отношения. Самый важный фактор из указанных выше — и мы сосредоточимся на его изучении прежде всего — это набор задач, решение которых входит в обязанности конкретного класса. Наиболее важным руководящим принципом является следующий: В обязанность каждого класса входит решение одной задачи.
Это не означает, что каждому классу определен только один метод. Это далеко не так; у многих классов имеется несколько методов. Однако все эти методы должны быть взаимно согласованы и непротиворечивы, т.е. все они должны быть родственными друг другу и способствовать повышению возможностей класса выполнять возложенные на него задачи.
В хорошо спроектированной системе каждый объект — это экземпляр четко определенного и легко воспринимаемого класса, который обеспечивает решение задач, принадлежащих одной области интереса. Классы
Часть I
обычно персдаюг несвойственные им обязанности другим, родственным классам. Создавая классы, для которых имеется только одна область интереса, вы облегчаете построение удобного в эксплуатации программного кода.
Чтобы иметь возможность управлять обязанностями вашего класса, вам, возможно, будет удобно начать выполнять проектные работы с использования карточек CRC.
Карточки CRC
CRC означает Class (Класс), Responsibility (Обязанности) и Collaboration (Совместная работа). Сама карточка CRC представляет собой карту размером 4x6. Это простое и незамысловатое устройство позволит сотрудничать с другими исполнителями при изучении первичных обязанностей вашего исходного набора классов. Вы можете собрать стопку пустых индексных карт размером 4x6, сесть за круглый стол и провести серию сеансов по заполнению карточек CRC.
Как проводить сеансы по заполнению карточек CRC
В идеальном случае в каждом сеансе по заполнению карточек CRC должна принимать участие группа от трех до шести человек; при участии большего числа специалистов продуктивность их труда снижается. В их числе должен быть распорядитель, в обязанности которого входит следить за тем, чтобы дискуссия не теряла нужного направления и чтобы участники зафиксировали все обнаруженные факты. Среди участников должен быть, по меньшей мере, один старший специалист по системной архитектуре, желательно с большим опытом в области объектно-ориентированного анализа и проектирования. Кроме того, вы, наверное, захотите включить в эту группу, но меньшей мере, одного или двух "специалистов по области применения", которые могут сформулировать требования, предъявляемые системой, и дать квалифицированную консультацию о гом, как должна работать система.
Непременным условием успеха таких сеансов является бросающееся в глаза отсутствие всяких начальников. Такое собрание должно иметь непринужденный творческий характер, в нем не должно быть места желанию произвести благоприятное впечатление на начальника. Его назначение, прежде всего, состоит в том, чтобы выполнить исследования, принять решения с определенной долей риска, выявить обязанности классов и понять, как они могут взаимодействовать друг с другом.
Сеанс по заполнению карточек CRC начинается с того, что вся группа собирается за круглым столом, имея небольшую стопку карточек размером 4x6. В верхней части каждой карточки вы пишете имя конкретного класса. Далее проводите линию в центр карточки и слева пишете Обязанности, а справа — Совместно выполняемые работы.
Начните заполнение этих карточек с наиболее важных из выявленных вами классов. На обратной стороне карточки запишите определение класса из одного, максимум из двух предложений. Вы можете также указать, какие классы являются специализациями данного класса, если это очевидно во время работы с карточкой CRC Напишите Суперкласс (Superclass): ниже имени класса и укажите имя класса, производным которого является данный класс.
Обязанности классов
Главной задачей сеансов по заполнению карточек является выявление обязанностей каждого класса. Не затрачивайте ваших усилий на выявление всех атрибутов, обращайте внимание в процессе работы только на самые существенные и очевидные атрибуты. Самое главное — это выявление обязанностей классов. Если, выполняя свои обязанности, какой-либо класс должен передать работу другому классу, вы получаете соответствующую информацию из описания совместно выполняемых работ.
По мере продвижения работ следите за списком обязанностей. Если на карточке размером 4x6 больше не остается места, есть смысл задать себе вопрос: а не много ли я хочу от этого класса? Помните, что каждый класс должен быть ответственным за одну широкую область деятельности, а различные обязанности, указанные в списке, должны быть согласованными и непротиворечивыми, т.е. они должны функционировать совместно, чтобы осуществлять обязанность класса, имеющую более общий характер. На этом этапе вы не должны сосредоточивать свои усилия на отношениях, не следует проявлять нездоровый интерес к интерфейсам классов или к тому, какие методы должны быть общедоступными, а какие — приватными. Основное внимание следует уделять пониманию функций каждого класса.
Антропоморфные классы
Основное назначение карточек CRC заключается в том, чтобы сделать классы антропоморфными, т.е. наделить каждый класс качествами, свойственными человеку. Вот как это делается: после того как вы получили предвари гельный набор классов, обратитесь снова к сценариям CRC. Разложите карточки в про-
Глава 1
извольном порядке и пройдитесь по сценариям все вместе. Например, возвратимся к следующему сценарию:
Клиент намеревается снять определенную сумму с текущего счета. На счете имеется достаточная сумма, достаточно денег в машине ATM, машина заправлена бумагой для квитанций. Машина ATM просит клиента указать сумму, какую он желает снять со счета, клиент запрашивает $300, эта сумма является допустимой на текущий момент времени. Машина выдает $300, а клиент получает эти деньги и квитанцию, фиксирующую выдачу этой суммы. Допустим, что в сеансе по заполнению карточек CRC принимают участие пять человек: Эми — распорядитель и специалист по объектно-ориентированному проектированию, Барри — ведущий программист, Чарли — клиент, Доррис — специалист в области применения и Эд — программист.
Эми поднимает карточку CRC, представляющую текущий счет (CheckingAccount) и говорит: "Я сообщаю клиенту, сколько денег у него на счете. Он просит выдать ему $300. Я посылаю сообщение кассовому автомату выдать $300 наличными". Барри поднимает свою карточку и говорит: "Я — кассовый аппарат, я отсчитываю $300 и посылаю Эми сообщение, извещающее его о том, что остаток на его счете уменьшился на $300. Кому я должен сообщить, что в машине стало на $300 меньше? Должен ли я следить за этим?" Чарли говорит: "Я думаю, что необходим объект, чтобы следить за наличностью в машине". Эд возражает: "Нет, кассовый аппарат сам должен знать, сколько в нем наличных денег, это одна из его обязанностей". Эми не соглашается: "Нет, кто-то другой должен координировать выдачу наличных денег. Кассовый аппарат должен знать, что имеется достаточно наличности, и, если у клиента на счете достаточно денег, он должен отсчитать нужную сумму и знать, когда нужно закрыть ящик. Он должен делегировать обязанность по контролю за наличностью некоторому внутреннему счету. Тот объект, который отслеживает сумму наличных денег в машине, должен сообщить в контору, когда его следует пополнить. Если возложить эту задачу на машину, она окажется перегруженной обязанностями".
Обсуждение продолжается. Поднимая карточки и взаимодействуя друг с другом, участники сеанса выявляют, какие требования и возможности целесообразно делегировать другим классам. Каждый класс обретает плоть, их обязанности уточняются. Если группа увязнет в обсуждении вопросов проектирования, распорядитель может вмешаться и направить работу группы по правильному пути.
Ограничения, накладываемые на карточки CRC
Несмотря на то что карточки CRC могут оказаться мощным инструментом, обеспечивающим переход к проектированию, для них характерны ограничения. Первое — они не всегда хорошо отображают проблему. В случае достаточно сложного проекта вы окажетесь заваленными этими карточками, отслеживание всех этих карточек становится трудным делом.
Карточки CRC не отражают взаимоотношений между классами. Хотя совместно выполняемые работы фиксируются, тем не менее, природа совместных работ моделируется недостаточно хорошо. Просматривая карточки CRC, вы не можете сказать, пополняют ли классы один другого, кто кого из них создает и т.д. Карточки CRC "не отлавливают" атрибуты, так что трудно переходить от карточек CRC к программным кодам. И что более важно, карточки CRC статичны; вы, конечно, можете воспроизвести взаимодействие между классами, однако сами по себе карточки CRC не улавливают эту информацию.
Короче говоря, карточки CRC хороши для начала, однако, чтобы построить устойчивую и полную модель вашего проекта, необходимо перенести классы в язык UML. И хотя такой перенос в UML не является исключительно трудной задачей, тем не менее, это улица с односторонним движением. Как только вы отобразите классы в диаграммы языка UML, возврата на предыдущий этап не будет; вы убираете карточки CRC и больше к ним не возвращаетесь. Очень трудно добиться взаимной синхронизации этих двух моделей.
Преобразование карточек CRC в диаграммы UML
Каждая карточка CRC может быть преобразована в класс, смоделированный средствами языка UML. Обязанности преобразуются в методы классов, и все атрибуты, которые вам удалось выявить, также добавляются в классы. Определение класса, зафиксированное на обратной стороне карточки, помещается в документацию по классу. На рис. 1.13 показано отношение между карточкой CheckingAccount и классом UML, построенном на базе этой карточки.
Класс: CheckingAccount
Суперкласс: Account
Обязанности:
Отслеживание состояния текущего счета
Часть I
РИСУНОК 1.13. Карточка CRC.РИСУНОК 1.13. Карточка CRC.Прием и перевод вкладов
РИСУНОК 1.13. Карточка CRC.
РИСУНОК 1.13. Карточка CRC.
Фиксация операции
Выдача наличности
Отслеживание остатка наличности машины ATM на текущий день
Совместно выполняемые работы:
Другие счета
Учрежденческие системы
Аппарат выдачи наличных денег
Отношения между классами
После того как классы будут представлены в UML, можно постепенно переходить к изучению отношений между различными классами. Основными отношениями, которые вы должны смоделировать, являются следующие:
Обобщение
Связь
Агрегирование
Композиция
Отношение обобщения реализовано в C++ через общее наследование. Однако в аспекте проектирования мы больше внимания посвятим семантике в ущерб механизму обобщения: что из чего следует.
Мы изучали отношение обобщения на стадии анализа, однако сейчас обратим внимание не только на объекты области применения, но и на объекты в нашем проекте. Попытаемся выявить общие функции в родственных классах и снабдить ими базовые классы, которые смогут инкапсулировать общие обязанности.
Как только вы выявите общие функции, переносите их из специализированных классов в обобщающий их класс. Таким образом, как только текущему и сберегательному счетам требуется метод для перевода денег с одного счета на другой, автор перемещает метод TransferFunds() в базовый класс счета. Чем больше информации о производных классах вы получаете, тем более полиморфным будет ваш проект.
Одна из возможностей, которая реализована в языке C++ и недоступна в Java, — множественное наследование (хотя в языке Java имеется похожая в первом приближении, хотя и ограниченная возможность со множественными интерфейсами). Множественное наследование позволяет классу наследовать свойства более чем одного базисного класса, накапливая в себе члены и методы двух и большего числа классов.
Опыт показывает, что вы должны разумно использовать множественное наследование, поскольку это может существенно усложнить как проект, так и его реализацию. Многие из проблем, которые ранее решались с помощью множественного наследования, сегодня решаются путем агрегирования. Короче говоря, множественное наследование — это мощное средство, но может случиться так, что ваш проект потребует создания отдельного класса, специализацией которого станет поведение двух или большего числа других классов.
Множественное наследование против включения
Является ли объект суммой своих частей? Есть ли смысл моделировать объект Саг (Автомобиль) как специализацию SteeringWheel (Руль), Door (Дверца) и Tire (Шина), как показано на рис. 1.14?
РИСУНОК 1.14.
Ложное наследование.
Важно вернуться к основным понятиям. Общее наследование всегда должно моделировать обобщение. В общем виде эту мысль можно выразить следующим образом. Наследование должно моделировать отноше-
Глава 1
ние есть. Если вы захотите смоделировать отношение иметь (например, автомобиль имеет руль), то для этой цели необходимо использовать агрегирование, как показано на рис. 1.15.
На этом рисунке показано, что у автомобиля имеется руль, четыре колеса и 2-5 дверей. Это одна из сравнительно точных моделей отношения между автомобилем и его составными частями. Обратите внимание на тот факт, что ромбы на диаграмме не закрашены. Это означает, что мы моделируем отношение как агрегирование (группирование), но не как композицию. Композиция предусматривает управление на протяжении всего времени существования объекта. И хотя у автомобиля имеются шины и дверцы, и шины и дверцы могут существовать и до того, как они станут частями автомобиля, и после того, как они перестанут быть его частями.
РИСУНОК 1.16. Композиция.РИСУНОК 1.16. Композиция.На рис. 1.16 показано моделирование композиции. Модель отражает не только тот факт, что человеческое тело есть группирование головы, двух рук и двух ног, но и тот факт, что эти объекты (голова, руки, ноги) созданы тогда же, когда создано само тело, и исчезнут вместе с исчезновением тела. Иначе говоря, для них невозможно независимое существование; тело состоит из этих частей.
РИСУНОК 1.16. Композиция.
РИСУНОК 1.16. Композиция.
РИСУНОК 1.15. Агрегирование.
Дискриминаторы
Как спроектировать классы, необходимые для отображения различных модельных линий типичного производителя автомобилей? Предположим, что вам поручили спроектировать систему для компании Acme Motors, которая одновременно выпускает пять моделей автомобилей: Плутон (Pluto — низкоскоростной миниатюрный автомобиль с двигателем малых размеров), Венера (Venus — седан с четырьмя дверями и двигателем средних размеров), Марс (Mars — спортивная модель с самым большим двигателем, выпускаемым компанией и развивающим максимальную мощность), Юпитер (Jupiter — мини-фургон с таким же двигателем, что и спортивная модель, но рассчитанным на переключения на низких оборотах и на использование его мощности для компенсации большего веса машины) и Земля (Earth — фургон с неболь
шим высокооборотным двигателем.
Вы можете начать с создания подтипов класса Саг, которые отображают различные модели, а затем создать экземпляры каждой модели по мере того, как они сходят с конвейерной лини, как показано на рис 1.17.
Чем отличаются между собой эти модели? Как нетрудно видеть, они отличаются друг от друга размерами двигателя, типом корпуса и рабочими характеристиками. Эти отличающиеся между собой характеристики можно комбинировать и подбирать таким образом, что будут получаться различные мол
средством стереотипа дискриминатора, как показано на рис 1.18.
На этом рисунке показано, что классы могут быть порождены классом Саг путем комбинирования и подбора трех отличительных атрибутов. Размер двигателя соответствует мощности автомобиля, технические характеристики указывают на спортивную принадлежность автомобиля. Таким образом, можно иметь мощ
ный спортивный фургон, семейный седан малой мощности и т.д.
Каждый атрибут может быть реализован с помощью простого перечислителя. Таким образом, тип корпуса может быть реализован следующим оператором, представленным в программных кодах:
enum BodyType = ( sedan, coupe, minivan, stationwagon } ;
Однако может получиться так, что простого значения недостаточно, чтобы смоделировать конкретный дискриминатор. Например, технические характеристики могут быть довольно сложными. В этом случае мо-
Часть I
РИСУНОК 1.18. Моделирование дискриминатора.РИСУНОК 1.18. Моделирование дискриминатора.делью дискриминатора может быть класс, а сам дискриминатор может быть инкапсулирован в экземпляре этого типа.
РИСУНОК 1.18. Моделирование дискриминатора.
РИСУНОК 1.18. Моделирование дискриминатора.
Performance — это тип мощности для автомобиля. Когда вы инициализируете экземпляр класса Саг, тоPerformance — это тип мощности для автомобиля. Когда вы инициализируете экземпляр класса Саг, тоСледовательно, класс Саг может моделировать технические характеристики в типе performance, который содержит информацию о том, где происходит переключение двигателя и сколько оборотов в минуту он может развивать. Стереотип UML для класса, который инкапсулирует дискриминатор и который может быть использован для создания экземпляров класса (Саг), имеющих различные логические типы (например, SportsCar или LuxuryCar), — это тип мощности. В этом случае класс
Performance — это тип мощности для автомобиля. Когда вы инициализируете экземпляр класса Саг, то
Performance — это тип мощности для автомобиля. Когда вы инициализируете экземпляр класса Саг, то
конкретным объектомконкретным объектомодновременно генерируете экземпляр объекта Performance и сопоставляете с Performance заданный класс Саг, как показано на рис. 1.19.
конкретным объектом
конкретным объектом
РИСУНОК 1.19.
Дискриминатор как тип мощности.
Типы мощности позволяют создавать множество логических типов без использования свойства наследо
вания. Таким образом, можно работать с большими и сложными наборами типов, не опасаясь "демографического" взрыва, являющегося следствием применения средств комбинаторики, с которым вы можете
столкнуться при использовании свойства наследования.
РИСУНОК 1.20. Отношение между объектом Саг и его типом мощности.РИСУНОК 1.20. Отношение между объектом Саг и его типом мощности.В C++ обычный путь реализации типов мощности состоит в использовании указателей. В этом случае класс Саг содержит указатель на экземпляр класса PerformanceCharacteristics (рис. 1.20). Честолюбивый читатель может в качестве упражнения выполнить преобразование дискриминаторов корпуса и двигателя в типы мощностей.
РИСУНОК 1.20. Отношение между объектом Саг и его типом мощности.
РИСУНОК 1.20. Отношение между объектом Саг и его типом мощности.
Class Car : public Vehicle
(
public:
Car () ;
~Car() ;
// Другие общедоступные методы не рассматриваются
private:
Performancecharacteristics * pPerformance;
В качестве заключительного замечания отметим, что типы мощности позволяют создавать новые типы (не только экземпляры) во время выполнения программы. Поскольку каждый логический тип отличается только атрибутами сопоставленного ему типа мощности, эти атрибуты могут служить параметрами для конструктора типа мощности. Это означает, что вы можете создавать новые типы во время выполнения программ практически мгновенно, т.е. передавая типу мощности различные данные о размерах двигателя и точках переключения, можно эффективно создавать новые технические характеристики. Назначая такие характеристики различным автомобилям, вы можете эффективно расширять наборы типов во время выполнения.
Глава 1
Динамическая модель
Дополнительно к моделированию отношений между классами очень важно построить правильную модель их взаимодействия. Например, классы CheckingAccount, ATM и Receipt могут взаимодействовать с классом Customer при выполнении примера использования "Снятие суммы со счета". С этой целью возвращаемся к диаграммам последовательности действий, которые впервые были использованы на стадии анализа, однако сейчас они пополняются деталями на базе методов, которые мы разработали ранее для соответствующих классов, как показано на рис. 1.21.
РИСУНОК 1.21.
Диаграмма последовательности действий.
Эта простая диаграмма взаимодействий отображает взаимодействия между некоторыми проектными классами во времени. Предполагается, что класс ATM делегирует классу CheckingAccount все обязанности для работы с остатком, в то время как класс CheckingAccount передает классу ATM обязанность управлять отображением данных для пользователей.
Диаграммы совместного выполнения работ существуют в двух разновидностях. Одна из этих разновидностей, представленная на рис. 1.20, именуется диаграммой последовательности действий. Другое представление этой же информации предлагает диаграмма совместно выполняемых работ. Диаграмма последовательности действий показывает, в какие моменты времени происходят события; а диаграмма совместно выполняемых работ показывает, как взаимодействует между собой классы. Диаграмму совместно выполняемых работ можно построить на базе диаграммы последовательности действий; инструментальные средства, подобные Rational Rose, доводят автоматизацию этих работ до такого уровня, что они могут выполняться по щелчку кнопки мыши (рис. 1.22).
РИСУНОК 1.22.
Диаграмма совместно выполняемых работ.
Сплошной маркерСплошной маркерРИСУНОК 1.23. Состояния счета клиента.РИСУНОК 1.23. Состояния счета клиента.1.24).1.24).РИСУНОК 1.24. Пользователь может отменить транзакцию.РИСУНОК 1.24. Пользователь может отменить транзакцию.РИСУНОК 1.25. Суперсостояние.РИСУНОК 1.25. Суперсостояние.
Сплошной маркер
Сплошной маркер
РИСУНОК 1.23. Состояния счета клиента.
РИСУНОК 1.23. Состояния счета клиента.
1.24).
1.24).
РИСУНОК 1.24. Пользователь может отменить транзакцию.
РИСУНОК 1.24. Пользователь может отменить транзакцию.
РИСУНОК 1.25. Суперсостояние.
РИСУНОК 1.25. Суперсостояние.
Объектно-ориентированное программирование Часть I
Диаграммы состояний
Чтобы понять, как осуществляется взаимодействие между объектами, мы должны иметь правильное представление о возможных состояниях конкретных объектов. Мы можем моделировать переходы из одного состояния в другое с помощью диаграмм состояний (или диаграмм перехода). На рис. 1.23 показаны различные состояния класса CustomerAccount в момент регистрации клиента в системе.
Каждая диаграмма состояний начинается с единственного состояния пуска и заканчивается одним из нескольких состояний окончания или вообще без такового. Каждое состояние имеет имя, переходы могут быть помечены. Защитная блокировка указывает условие, которое должно быть выполнено, чтобы объект мог перейти из одного состояния в другое.
Суперсостояния
Клиент в любой момент может раздумать и отказаться от регистрации. Он может сделать это, когда вставит свою карту для идентификации своего счета или после того, как введет свой пароль. В любом случае система должна принять его требование отмены сеанса и возвратиться в состояние "Клиент не зарегистрирован" (рис.
Вы видите, что в более сложной диаграмме состояние Отказ от операции быстро становится источником путаницы. Это создает большие неудобства, так как отказ от операции представляет собой исключительную ситуацию, которая не должна занимать на диаграмме центрального места. Можно упростить эту диаграмму, вводя суперсостояние (рис. 1.25).
Диаграмма на рис. 1.24 содержит ту же информацию, которая представлена на рис. 1.23, однако она значительно легче воспринимается. С момента начала регистрации в системе до завершения системой процесса регистрации вы можете отказаться от операции. Если вы отменяете операцию, вы возвращаетесь в состояние "клиент на зарегистрирован".
Глава 1
Резюме
В этой главе в общих чертах описаны проблемы, сопровождающие объектно-ориентированный анализ и проектирование. Суть такого подхода заключается в анализе того, как ваша система будет использована и как она должна функционировать, и в последующем проектировании классов и моделировании их отношения и взаимодействия.
В старые добрые времена мы в общих чертах строили картину того, что мы намереваемся получить, и немедленно приступали к написанию программных кодов. Проблема заключалась в том, что сложные проекты никогда не завершались, а если и завершались, то полученный программный продукт оказывался ненадежным и хрупким. Выдвигая на передний план необходимость понимания требований и моделируя стадию проектирования, мы тем самым гарантируем успешное завершение создания программного продукта, который будет соответствовать предъявляемым требованиям.
Большая часть последующего материала этой книги будет посвящена подробностям реализации.
Проблемы, имеющие отношение к тестированию и распространению, выходят за рамки книги, поэтому они не рассматриваются.
Глава 2
Прочитав главу 1, вы научились формулировать свои идеи, проблемы и решения в хорошо организованной и понимаемой форме, воспользовавшись для этой пели универсальным языком моделирования UML. Теперь вы можете показывать проекты объектов любому заинтересованному липу, имеющему отношение к отрасли программирования, не сомневаясь, что он поймет ваш замысел именно так, как вы себе его пред ставляли. Следующий шаг состоит в воплощении ваших проектов в рабочие решения. Основным содержанием настоящей главы является преобразование различных моделей, полученных вами на стадии анализа, в соответствующие программные коды на языке C++. Если быть точнее, то в этой главе описывается преобразование в коды C++ следующих диаграмм UML:
Классов
Взаимодействия (диаграммы совместно выполняемых работ)
Перехода из одного состояния в другое
Активности
Перевод диаграмм классов в C++
Диаграммы классов служат двум целям: представлению статических состояний классов и использованию отношений между ними. На ранних стадиях жизненного цикла разработки ПО важно отметить, что в модели диаграммы класса делается попытка определить не только интерфейс общего пользования каждого класса, но и связи, агрегирование и обобщения, устанавливаемые между конкретными классами. Хотя определение конкретного набора классов имеет важное значение при построении основных инструментатьных средств, предназначенных для использования в законченном приложении, чтобы успешно продвигаться в направлении полного решения задачи не менее важно понимать, как каждый класс взаимодействует с другими классами. Далее в этой главе вы узнаете о том, как преобразовывать наиболее часто используемые элементы диаграмм классов в программные коды на языке C++.
Стандартные классы
CheckingAccount^ Oalance: int^ DaysA TMWithdrawal: int,\>GetBalance(): int: int^Deposit (amount: int): void ^Transferln (amount: int): bool^TransferOutl): int■\> WriteChecks (amount: int): bool I jРИСУНОК 2.1. Диаграммакласса текущего счета.CheckingAccount^ Oalance: int^ DaysA TMWithdrawal: int,\>GetBalance(): int: int^Deposit (amount: int): void ^Transferln (amount: int): bool^TransferOutl): int■\> WriteChecks (amount: int): bool I jРИСУНОК 2.1. Диаграммакласса текущего счета.Как уже было отмечено в главе 1, классы представляются в виде прямоу-CheckingAccount
CheckingAccount
^ Oalance: int
^ DaysA TMWithdrawal: int
,\>GetBalance(): int: int
^Deposit (amount: int): void ^Transferln (amount: int): bool
^TransferOutl): int
■\> WriteChecks (amount: int): bool I j
РИСУНОК 2.1. Диаграмма
класса текущего счета.
CheckingAccount
^ Oalance: int
^ DaysA TMWithdrawal: int
,\>GetBalance(): int: int
^Deposit (amount: int): void ^Transferln (amount: int): bool
^TransferOutl): int
■\> WriteChecks (amount: int): bool I j
РИСУНОК 2.1. Диаграмма
класса текущего счета.
гольников, разделенных на три части, в верхней части прямоугольника указано !—1
имя класса, в средней части — атрибуты, а в нижней части — операции. На % Qaiance: int’
рис. 2.1 показана типичная диаграмма класса.^ DaysA TMWithdrawai: int
Преобразование диаграммы класса, представленной на рис. 2.1, в класс на <i>GetBaiance()- int- int
C++ является процессом, состоящим из трех этапов:О Deposit (amount: int): void
Назначение классу имени, основой которого служит имя, указанное в <^>Transferin (amount: int): bool верхнем прямоугольнике диаграммы класса: CheckingAccount. Не забудь-<<>TransferOut(): inti
те ВКЛЮЧИТЬ В ЭТОТ класс конструктор И деструктор.^ WriteChecks (amount: int): bool J
Включение в класс атрибутов (из среднего прямоугольника диаграммы) РИСУНОК 2.1. Диаграмма в качестве приватных переменных-членов. И хотя такие переменные не класса текущего счета. обязательно должны быть приватными, однако, если они будут приватными, это существенно облегчит инкапсуляцию. В случае, представленном на рис. 2.1, такими атрибутами являются Balance и DaysATMWithdrawal.
Включение операций (нижний прямоугольник диаграммы) в общедоступные функции-члены. Эти функции являются интерфейсами общего пользования такого класса. На рис. 2.1 такими операциями являются GetBalance(), Deposit(), Transferln(), TransferOut() и WriteChecks().
В листинге 2.1 представлено законченное объявление класса CheckingAccount.
Листинг 2.1. Представление класса UML в C++ class CheckingAccount 1 public: // Создание / Разрушение CheckingAccount () ; -CheckingAccount () ;
// Операции общего пользования int GetBalance () ;
void Deposit (int amount);
Часть I
BOOL Transferin (int amount); int TransferOut () ;
BOOL WriteChecks (int amount); private:
// Приватные атрибуты int Balance;
int DaysATMWithdrawal;
Шаблонные классы
StackTypeStack —"\>Pop(): <StackType>■\>Push (item: <StackType>): void StackTypeStack —"\>Pop(): <StackType>■\>Push (item: <StackType>): voidШаблонные классы, которые также называют параметризованными, — это обобщающие классы, которыми нельзя пользоваться до тех пор, пока они не инициализированы. Поскольку типы данных, используемые в шаблонном классе, не определены до тех пор, пока этот класс не инициализирован, вы можете создавать обобщающий класс Stack только один раз; элементы, проталкиваемые в стек и выталкиваемые из него, могут отличаться от одного экземпляра класса к другому. Этот единственный в своем роде класс Stack может быть использован для хранения целых, значений с плавающей точкой или любых других предопределенных или заданных пользователем типов. На диаграммах классов шаблонные классы представлены так же, как и обычные классы, однако в их верхнем правом углу имеется прямоугольник, обозначенный точками, в котором определены аргументы, предназначенные для использования в классе (рис. 2.2).
StackTypeStack —
"\>Pop(): <StackType>
■\>Push (item: <StackType>): void
StackTypeStack —
"\>Pop(): <StackType>
■\>Push (item: <StackType>): void
РИСУНОК 2.2. Шаблонная диаграмма класса Stack.РИСУНОК 2.2. Шаблонная диаграмма класса Stack.Реализация шаблонного класса в языке C++ соответствует типу шаблонного класса в C++. Чтобы преобразовать класс C++ для стандартного класса, выполните следующие действия: 1. Перед объявлением класса поместите шаблон из ключевых слов и определите тип подстановки для класса: <класс StackTypeX
РИСУНОК 2.2. Шаблонная диаграмма класса Stack.
РИСУНОК 2.2. Шаблонная диаграмма класса Stack.
2 Для каждого экземпляра, в котором вы используете конкретный тип переменной, замените этот тип переменной на имя класса, за которым следует подставляемый тип, заключенный в угловые скобки: Stack< StackType >.
В листинге 2.2 представлена реализация в C++ диаграммы класса, изображенной на рис. 2.2.
Листинг 2.2. Шаблонный класс в C++ template <class StackType> class Stack { public: // Создание / Разрушение Stack () ; Stack (const Stack<StackType>S right); -Stack () ; // Оператор присваивания const Stack<StackType>s operator = (const Stack<StackType>S right); // Операторы сравнения int operator==(const Stack<StackType> Sright) const; int operator!=(const Stack<StackType> Sright) const; // Интерфейс общего пользования Stack<StackType>S Pop () ; void Push (const Stack<StackType> sright); };
Служебные классы
Служебный класс представляет собой класс, в котором содержатся сгруппированные функциональные средства, но в то же время отсутствуют постоянные данные-члены (атрибуты). Несмотря на то что служебные классы не являются частью стандартного языка C++, некоторые программисты предпочитают именно их; в силу этого обстоятельства в UML предусматривается их поддержка. Назначение служебного класса заключается в том, чтобы собрать воедино некоторые функциональные средства (такие, как тригономет-
Глава 2
«Utility»TrigMathFunctions<^> Cosine (angle: float): float^>Sine (angle: float): float^Tangent (angle: float): floatРИСУНОК 2.3. Диаграмма служебного класса TrigMathFunctions.«Utility»TrigMathFunctions<^> Cosine (angle: float): float^>Sine (angle: float): float^Tangent (angle: float): floatРИСУНОК 2.3. Диаграмма служебного класса TrigMathFunctions.рические математические функции) и поместить их в отдельный класс. При этом отпадает необходимость создавать экземпляры этого класса, поскольку каждая функция этого класса объявляется как static: вы просто используете функции, содержащиеся в этом классе, называя точное имя нужной функции. Например, класс тригонометрических функций может содержать функции Cosine(), Sine() и Tangent(), которые могут быть вызваны в любой момент любым классом. В системе обозначений, используемой в диаграммах классов, именам служебных классов предшествует конструкция <<Utility>>. Диаграмма класса TrigMathFunctions представлена на рис. 2.3.
«Utility»TrigMathFunctions
<^> Cosine (angle: float): float
^>Sine (angle: float): float
^Tangent (angle: float): float
РИСУНОК 2.3. Диаграмма служебного класса TrigMathFunctions.
«Utility»TrigMathFunctions
<^> Cosine (angle: float): float
^>Sine (angle: float): float
^Tangent (angle: float): float
РИСУНОК 2.3. Диаграмма служебного класса TrigMathFunctions.
Чтобы представить служебный класс в C++, каждая операция должна быть объявлена как static — существует только один экземпляр каждой функции для всех объектов этого класса. Более того, служебные классы не имеют конструкторов и деструкторов, поскольку отсутствуют данные, которые нужно инициализировать или хранить. В листинге 2.3 представлена реализация класса TrigMathFunctions в C++.
Листинг 2.3. Объявление класса в служебном классе class TrigMathFunctions.
<
static float Cosine (float angle);
static float Sine (float angle);
static float Tangent (float angle);
Ассоциации
Ассоциации (связи) представляют собой отношения между классами. Ассоциация может быть простой — например, ассоциация 1:1, отражающая отношение между двумя классами, — и сложной, такой как, например, ассоциация N:N, отражающая отношения между тремя или большим числом классов. В последующих разделах рассматривается каждый тип ассоциаций и приводятся примеры, в которых их определения уточняются.
Ассоциации 1:1
Неименованная ассоциация 1:1 — это отношение между двумя классами (рис. 2.4).
Неименованная ассоциация говорит нам о том, что существует отношение между двумя классами, но ничего не сообщает о природе этого отношения. Чтобы дать определение этому отношению, мы можем поместить именную метку над линией (рис. 2.5).
РИСУНОК 2.4. Неименованная ассоциация классов.РИСУНОК 2.4. Неименованная ассоциация классов.РИСУНОК 2.5. Именованная ассоциация классов.РИСУНОК 2.5. Именованная ассоциация классов.
РИСУНОК 2.4. Неименованная ассоциация классов.
РИСУНОК 2.4. Неименованная ассоциация классов.
РИСУНОК 2.5. Именованная ассоциация классов.
РИСУНОК 2.5. Именованная ассоциация классов.
Вы отображаете ассоциацию 1:1 в коды C++ путем определения экземпляра ассоциированного класса внутри другого класса и предоставления интерфейса для доступа к этой информации. Листинг 2.4 содержит фрагменты из определения класса Person. Листинг 2.5 содержит фрагменты определения класса Book.
Листинг 2.4. Объявление класса Person ♦include "Book.h" class Person ( public:
Person () ;
-Person();
//## Ассоциация: Reads
//## Роль: Person::<the_Book> const Book get_the_Book() const; void set_the_Book(const Book value);
private:
Часть I
Book the_Book; };
inline const Book Person::get_the_Book() const (
return the_Book;
}
inline void Person::set_the_Book(const Book value) {
the_Book = value;
Листинг 2.5. Объявление класса Book
#include "Person.h"
class Book { public:
Book () ;
-Book () ;
//## Ассоциация: Reads
//## Роль: Book::<the_Person>
const Person get_the_Person() const;
void set_the_Person(const Person value);
private:
Person the_Person;
1;
inline const Person Book::get_the_Person() const {
return the_Person;
}
inline void Book::set_the_Person(const Person value) (
the_Person = value;
}
Нетрудно видеть, что класс Person объявил экземпляр класса Book с именем the_Book и зарезервировал функции-члены get_the_Book() и set_the_Book() для доступа к объектам. Аналогично класс Book объявляет экземпляр класса Person с именем the_Person и предусматривает функции-члены get_the_Person() и set_the_Person() для доступа к объекту к Person. Обратите внимание на тот факт, что именованная ассоциация Reads используется только в диаграмме класса и не определена в программном коде C++, разве что в комментарии.
Другой тип ассоциации 1:1 предусматривает именование ролей ассоциации на каждом конце. В подобных случаях соглашение по именованию классов C++ может быть легко использовано для идентификации отношения между двумя классами ассоциации. Рассмотрим пример ассоциации классов Renter (Квартиросъемщик) и Apartment (Квартира): Renter рассматривает Apartment как свое жилище, a Apartment считает Renter своим нанимателем. Следовательно, именованными ролями этой ассоциации будут Tenant (Наниматель) и Dwelling (Жилище) (рис. 2.6).
РИСУНОК 2.6.
Ассоциация классов с именованными ролями.
Чтобы создать класс C++ для ассоциации 1:1 именованных ролей, в обоих классах этой ассоциации должны быть переменные типов другого класса. В листинге 2.6 в классе Renter имеется переменная-член Dwelling типа Apartment. Эта переменная представляет для квартиросъемщика квартиру, в которой он живет. Более того, класс Renter обеспечивает интерфейс общего пользования для модификации своей переменной Dwelling посредством двух общедоступных функции get_Dwelling() и set_Dwelling(). Аналогично в
Глава 2
классе Apartment имеется приватная переменная-член Tenant типа Renter и две функции-члены, которые имеют доступ к переменной типа Renter класса Apartment: get_Renter() и set_Renter(). В листинге 2.6 представлено объявление класса Renter, а в листинге 2.7 — объявление класса Apartment.
Листинг 2.6. Объявление класса Renter
#include "Apartment.h”
class Renter ( public:
Renter () ;
-Renter () ;
//## Ассоциация: Unnamed
//## Роль: Renter::Dwelling
const Apartment get_Dwelling() const;
void set_Dwelling(const Apartment value);
private:
Apartment Dwelling;
1;
Листинг 2.7. Объявление класса Apartment
#include "Renter.h"
class Apartment { public:
Apartment () ;
-Apartment () ;
//## Ассоциация: Unnamed
//## Роль: Apartment::Tenant
const Renter get_Tenant() const;
void set_Tenant(const Renter value);
private:
Renter Tenant;
1;
Ассоциации N:1 и 1:N
Ассоциации N:1 или kN, которые еще называются как отношение один-ко-многим, имеют место, когда один объект может существовать для многих экземпляров другого объекта, однако другой объект может существовать только для исходного объекта, и только один раз. Пример такого рода ассоциации имеет место между нанимателем и многоквартирным зданием, в котором находится квартира: для нанимателя существует только одно многоквартирное здание, в то же время для одного многоквартирного здания может быть много нанимателей. Для обозначений на диаграмме классов ассоциации один-ко-многим характерно то, что имя роли ассоциации помещается над линией ассоциации, а список типов ассоциаций помещается ниже этой линии. В табл. 2.1 указано, какими способами обозначается множественность ассоциации.
Таблица 2.1. Множественности ассоциации
ОбозначениеАссоциация
1
Только одна
0..1
Либо одна, либо ни одной
0..*
Ни одной или большее число
1..*
Одна или большее число
*
Ни одной или большее число
M..N
Многие-ко-многим
Часть I
На рис 2.7 представлена диаграммы ассоциации классов Tenant и ApartmentBuilding.
РИСУНОК 2.7.Ассоциация классов один-ко-многим.РИСУНОК 2.7.Ассоциация классов один-ко-многим.TenantHousesLives atApartmentBuilding0..*1TenantHousesLives atApartmentBuilding0..*1
РИСУНОК 2.7.
Ассоциация классов один-ко-многим.
РИСУНОК 2.7.
Ассоциация классов один-ко-многим.
Tenant
HousesLives at
ApartmentBuilding
0..*1
Tenant
HousesLives at
ApartmentBuilding
0..*1
Ассоциация классов Tenant и ApartmentBuilding в C++ представлена так же, как и ассоциация 1:1. Класс Tenant имеет приватный атрибут Lives_at типа ApartmentBuilding. Однако отношение, связывающее ApartmentBuilding с Tenant, более сложное. Вспомните, что в ApartmentBuilding может поселиться нуль и большее число Tenants. Чтобы облегчить поддержание такого рода ассоциации, в классе ApartmentBuilding поддерживается коллекция Tenants. (Для объявления этого класса, представленного в листинге 2.9, используется шаблонный класс UnboundedSetByValue, предназначенный для хранения в нем данных о всех проживающих.) В листинге 2.8 представлено объявление класса ApartmentBuilding.
Листинг 2.8. Объявление класса Tenant
// класс ApartmentBuilding #include "AprtmntB.h"
class Tenant
{
public:
Tenant () ;
-Tenant () ;
//## Ассоциация: Unnamed
//## Роль: Tenant::Lives at
const ApartmentBuilding get_Lives_at() const;
void set_Lives_at(const ApartmentBuilding value);
private:
ApartmentBuilding Lives_at;
Листинг 2.9. Объявление класса ApartmentBuilding // класс Tenant #include "Tenant.h"
class ApartmentBuilding (
public:
ApartmentBuilding();
-ApartmentBuilding();
//## Ассоциация: Unnamed
//## Роль: ApartmentBuilding::Houses
const UnboundedSetByValue<Tenant> get_Houses() const;
void set_Houses(const UnboundedSetByValue<Tenant> value);
private:
UnboundedSetByValue<Tenant> Houses;
Ассоциации N:N
Ассоциация N:N — это отношение, которое существует между двумя классами, при котором нуль или большее число экземпляров одного класса связано конкретным числом экземпляров другого класса. Примером такого типа отношений может служить отношение, сложившееся у подрядчика с компанией. Компания может нанять множество подрядчиков, подрядчик может одновременно работать на несколько различных компаний (рис. 2.8).
РИСУНОК 2.8.
Ассоциация классов многие-ко-многим.
Глава 2
Реализация такой ассоциации в C++ подобна примеру с многоквартирным домом из предыдущего раздела. Company содержит приватный набор подрядчиков Contractors под именем Employees, а каждый Contractor ведет перечень компаний Company под именем Works_for. Представленные ниже листинги работают с этими коллекциями, используя для этой цели шаблонный класс UnboundedSetByValue. В листинге 2.10 содержится объявление класса Contractor. В листинге 2.11 представлено объявление класса Company.
Листинг 2.10. Объявление класса Contractor
// класс Company
#include "Company.h"
class Contractor (
public:
Contractor () ;
-Contractor () ;
//## Ассоциация: Unnamed
//## Роль: Contractor::Works for
const UnboundedSetByValue<Company> get_Works_for () const;
void set_Works_for(const UnboundedSetByValue<Company> value);
private:
UnboundedSetByValue<Company> Works_for; 1;
Листинг 2.11. Объявление класса Company
11 класс Contractor #include "Cntrctor.h"
class Company {
public:
Company () ;
-Company () ;
//## Ассоциация: Unnamed
//## Роль: Company::Employees
const UnboundedSetByValue<Contractor> get_Employees() const;
void set_Employees(const UnboundedSetByValue<Contractor> value);
private:
UnboundedSetByValue<Contractor> Employees; 1;
Агрегации
Агрегация представляет собой такие отношения между двумя классами, когда один класс играет в отношении более важную роль, чем другой. Наиболее часто используемой формой агрегации является композиция. При таком отношении, связывающем два класса, один из них фактически содержит другой. Примером такого отношения может служить отношение, представленное в главе 1, в рамках которого класс BankAccount
содержит классы Balance и TransactionHistory.
РИСУНОК 2.9. Диаграмма композиции классов.РИСУНОК 2.9. Диаграмма композиции классов.Отношение поглощения изображается на диаграмме классов прямой линией, проведенной между двумя классами, с закрашенным ромбом со стороны класса, который содержит другой класс (рис. 2.9).
РИСУНОК 2.9. Диаграмма композиции классов.
РИСУНОК 2.9. Диаграмма композиции классов.
Классы Balance и TransactionHistory хранят ссылки на свои классы BankAccounts, но сами содержатся в классе BankAccount; класс BankAccount создает приватные экземпляры классов Balance и TransactionHistory. Листинг 2.12 содержит объявление класса BankAccount. Класс BankAccount содержит приватные переменную-член the_Balance типа Balance и переменную-член the_TransactionHistory типа TransactionHistory. Листинг 2.13 содержит объявление класса Balance, а в листинге 2.14 представлено объявление класса TransactionHistory.
Объектно-ориентированное программирование Часть I
Листинг 2.12. Объявление класса BankAccount
Н Ю1лсс TransactionHistory #include "TrnsctnH.h" // Balance #include "Balance.h" class BankAccount { public:
BankAccount () ; -BankAccount () ;
//## Ассоциация: Unnamed
//## Роль: BankAccount::<the_Balance> const Balance get_the_Balance() const; void set_the_Balance(const Balance value);
//## Ассоциация: Unnamed
//## Роль: BankAccount::<the_TransactionHistory> const TransactionHistory get_the_TransactionHistory() const; void set_the_TransactionHistory(const TransactionHistory value);
private:
Balance the_Balance ;
TransactionHistory the_TransactionHistory; );
Листинг 2.13. Объявление класса Balance
// класс BankAccount
#include "Bnkccunt.h"
class Balance ( public:
Balance () ;
-Balance () ;
//## Ассоциация: Unnamed
//## Роль: Balance::<the_BankAccount>
const BankAccount get_the_BankAccount() const;
void set_the_BankAccount(const BankAccount value);
private: BankAccount the_BankAccount;
);
Листинг 2.14 Объявление класса TransactionHistory // класс BankAccount #include "Bnkccunt.h"
class TransactionHistory (
public: TransactionHistory () ; -TransactionHistory () ;
//## Ассоциация: Unnamed
//## Роль: TransactionHistory::<the_BankAccount> const BankAccount get_the_BankAccount() const; void set_the_BankAccount(const BankAccount value);
private: BankAccount the_BankAccount;
);
Глава 2Глава 2
Глава 2
Глава 2
Обобщение
РИСУНОК 2.10. Диаграмма обобщения классов.РИСУНОК 2.10. Диаграмма обобщения классов.Обобщение используется для представления отношения от-общего-к-конк- ретному, связывающего два класса. В терминах C++ такое отношение называется наследованием. Например, текущий счет и сберегательный счет — это конкретные типы банковских счетов. Обобщение обозначено на диаграмме классов стрелкой, направленной от специального класса к обобщающему классу (рис. 2.10).
РИСУНОК 2.10. Диаграмма обобщения классов.
РИСУНОК 2.10. Диаграмма обобщения классов.
В C++ обобщение обозначается добавлением в объявление конкретного класса суффикса, состоящего из двоеточия, за которым следует имя обобщающего класса В листинге 2.10 класс Checking наследуется от класса Account в операторе class Checking: public Account. Класс Savings наследуется от класса
Account с помощью оператора class Savings: public Account. Поскольку класс Account не подозревает о существовании классов Checking или Savings, его объявление опущено. В листинге 2.15 показано объявление класса Checking, а в листинге 2 16 — объявление класса Savings.
Листинг 2.15. Объявление класса Checking // Account
#include "Account.h”
class Checking : public Account (
1;
Листинг 2.16. Объявление класса Savings // Account #include "Account.h"
class Savings : public Account 1
РИСУНОК 2.11. Диаграмма множественного обобщающегоРИСУНОК 2.11. Диаграмма множественного обобщающегокласса.класса.Диаграммы классов также поддерживают принцип множественного обобщения, или множественного наследования (по терминологии C++). Множественное наследование представлено на диаграмме класса стрелкой, направленной от специализированного класса к каждому его обобщающему классу. Рассмотрим класс Derived, который пользуется атрибутами как класса BaseA, так и класса BaseB (рис. 2.11).
РИСУНОК 2.11. Диаграмма множественного обобщающего
РИСУНОК 2.11. Диаграмма множественного обобщающего
класса.
класса.
На рис. 2.11 обобщающие стрелки проведены в направлении обоих базовых классов. Чтобы представить множественное обобщение, или множественное наследование, в языке C++, оба базовых класса в операторе объявления класса разделены запятой, они расположены сразу после двоеточия, следующего за объявлением порожденого класса. Класс Derived наследует как от класса
BaseA, так и от класса BaseB в операторе class Derived: public BaseA, public BaseB. В листинге 2.17 представ
лен заголовок объявления класса Derived. Поскольку классы BaseA и BaseB не учитывают факт существования класса Derived, их листинги здесь не приводятся.
Листинг 2.17. Объявление класса Derived
// BaseB #include// BaseA #include// BaseB #include// BaseA #include"BaseB.h""BaseA.h""BaseB.h""BaseA.h"
// BaseB #include
// BaseA #include
// BaseB #include
// BaseA #include
"BaseB.h"
"BaseA.h"
"BaseB.h"
"BaseA.h"
class Derived : public BaseA, public BaseB {
Часть I
Перевод диаграмм взаимодействия в C++
Существует два вида диаграмм взаимодействия: диаграммы совместных работ и диаграммы последовательности действий. Диаграммы совместных работ иллюстрируют взаимодействие объектов в организационном аспекте — они показывают, как упорядочены объекты по отношению друг к другу. Диаграммы последовательности действий отображают только взаимодействие объектов, не учитывая их упорядоченность.
Как диаграммы совместных работ, так и диаграммы последовательности действий имеют свои достоинства и недостатки. Диаграмма совместных работ позволяет наглядно представлять классификацию объектов в их взаимодействии, но по мере увеличения числа взаимодействий она становится все более сложной и неудобной для чтения. Более того, такая модель теряет способность отображать естественным образом порядок взаимодействий, в силу чего взаимодействия должны быть пронумерованы.
Диаграммы последовательности действий показывают взаимодействия объектов в упорядоченном виде и четко фиксируют относительный порядок взаимодействий, но при этом затрудняется восприятие протекающего процесса, характерное для наглядного представления.
В главе 1 описывается процесс построения диаграмм взаимодействия на основе анализа системы. После того как построение диаграмм завершено и объекты, указанные на диаграмме взаимодействия, ассоциированы с классами, определенными на диаграммах классов, их реализация в C++ не представляет особых трудностей. Построение диаграмм совместной работы преследует четыре основные цели:
■ Идентификация системных пользовательских интерфейсов
и Совершенствование интерфейса общего пользования для каждого класса
и Определение, с какими объектами взаимодействует каждый из объектов
и Понимание и упорядочивание событий, которые происходят при возникновении соответствующих благоприятных условий.
Во-первых, вы должны предусмотреть в пользовательском интерфейсе системы все допустимые взаимодействия пользователя. Взаимодействия пользователя представляют действующие субъекты. Сообщения, которые действующие субъекты посылают системным объектам, должны быть взяты из пользовательского интерфейса и отправлены системным объектам; действующие субъекты обычно не взаимодействуют непосредственно с системными объектами.
Во-вторых, следует сделать так, чтобы каждое сообщение, порожденное объектом, включенным в диаграмму взаимодействия, соответствовало вытекающему из нее интерфейсу общего назначения этого объекта. Если объект А запрашивает услугу X от объекта В, то сервисная операция X объекта В должна быть доступной и для объекта А.
В-третьих, вы должны сделать так, чтобы объекты, взаимодействующие друг с другом, имели доступ друг к другу. Если объект С посылает сообщение объекту D, то объект С должен видеть объект D. Это особенно важно при выборе пути реализации системы.
И наконец, вы должны сделать так, чтобы каждая последовательность сообщений, которые появляются на объекте и передаются этим объектом, представляла событие, которое происходит в этом объекте. Если объект Е получает сообщение I и передает сообщение J, то служба объекта Е, которая получает сообщение 1, должна передать сообщение J до того, как она окончит работу.
Реализация диаграмм совместных работ и
диаграмм последовательности действий в C++
Диаграммы совместных работ и диаграммы последовательности действий взаимозаменяемы. В качестве справки следует отметить, что такое инструментальное средство моделирования, как Rational Rose, позволяет выполнять прямое преобразование одной диаграммы в другую. Вспомните пример операции снятия наличных средств с текущего счета с помощью машины ATM. Подробности разработки как диаграмм совместных работ, так и диаграмм последовательности действий уже рассматривались нами ранее, тем не менее, они показаны соответственно на рисунках 2.12 и 2.13.
Прежде чем мы начнем построение классов для этих диаграмм, определим объекты, представленные на этих диаграммах. Ниже приводится список этих объектов:
и Машина (ATM)
Текущий счет (CheckingAccount)
Глава 2Глава 2Квитанция (Receipt)
Глава 2
Глава 2
Пользовательский интерфейс (Userinterface)
РИСУНОК 2.12.
Диаграмма совместных работ для операции снятия со счета наличных денег через машину А ТМ.
РИСУНОК 2.13.
Диаграмма последовательности действий для операции снятия со счета наличных денег через машину ATM.
6. Print6. PrintPersonPersonATM: ATMATM: ATMChecking Account:Checking AccountChecking Account:Checking AccountReceipt: ReceiptReceipt: ReceiptОбъект Userinterface предназначен для представления действий объекта Person, поскольку объект Person никогда непосредственно не взаимодействует с другими объектами.
6. Print
6. Print
Person
Person
ATM: ATM
ATM: ATM
Checking Account:
Checking Account
Checking Account:
Checking Account
Receipt: Receipt
Receipt: Receipt
Прежде всего рассмотрим системный пользовательский интерфейс. Объект Person на рисунках 2.12 и 2.13 осуществляет взаимодействие с системными объектами:
Выполняет действие CheckBalances (Проверить текущий остаток) с объектом ATM.
Выполняет действие WithdrawCash (Снять со счета наличные деньги) с объектом CheckingAccount.
Получает наличность, в то время как объект CheckingAccount выполняет действие Dispense cash (Выдача наличности) с объектом Person.
Объект Userinterface должен предоставлять объекту Person опции CheckBalance и WithdrawCash, а также обеспечить выдачу объекту Person наличности из объекта CheckingAccount. Во всех случаях мы прибегали к услугам ATM, так что пользовательский интерфейс, обеспечивающий получение наличных денег, нам известен.
Часть I
Объект Userinterface может быть введен в действие через объявление класса, подобного представленному в листинге 2.18. Этот класс будет расширен при последующем рассмотрении, а сейчас введем интерфейс общего доступа.
Листинг 2.18. Объявление класса Userinterface
class Userlnterfасе {
public:
// Общедоступный интерфейс
char * CheckBalance ();
BOOL WithdrawCash () ;
} ;
ПРИМЕЧАНИЕ
Все остатки представлены в этом примере в виде символьных строк. Это делается в целях удобства реализации во избежание ограничений на целые значения и округления значений с плавающей точкой.
В целях совершенствования общедоступного интерфейса каждого класса соответствующие методы должны быть включены в те классы, которые получает сообщение согласно диаграмме взаимодействий. В рассматриваемом примере класс ATM получает сообщения CheckBalance и Display Balance, класс CheckingAccount — сообщения GetBalance и WithdrawCash, а класс Receipt — сообщение Print.
В листинге 2.19 представлен общедоступный интерфейс для класса ATM, в листинге 2.20 — общедоступный интерфейс для класса CheckingAccount, а в листинге 2.21 — общедоступный интерфейс для класса Receipt.
Листинг 2.19. Общедоступный интерфейс для класса ATM
class ATM {
public:
// Общедоступный интерфейс
char * CheckBalance () ;
char * DisplayBalance ();
1;
Листинг 2.20. Общедоступный интерфейс для класса CheckingAccount class CheckingAccount { public: char * GetBalance () ; BOOL WithdrawCash ();
Листинг 2.21. Общедоступный интерфейс для класса Receipt class Receipt { public:
BOOL Print ();
Теперь, когда общедоступный интерфейс извлечен из диаграмм взаимодействия и включен в объявления классов, следующий шаг состоит в том, чтобы установить область видимости для конкретных классов. В нашем примере класс Userinterface непосредственно сообщается с классами ATM и CheckingAccount. На этой стадии реализация зависит от приложения, а вам предоставляется следующий выбор:
Экземпляры классов ATM и CheckingAccount могут быть инициализированы с классом Userinterface. Выбирайте этот вариант, если классу Userinterface нужен непосредственный доступ как к классу ATM, так и к классу CheckingAccount.
Глава 2
Экземпляры каждого класса могут быть порождены в пространстве глобальной системы, а указатели на каждый из них могут быть переданы в класс Userinterface. Выбирайте этот вариант, если классы ATM и CheckingAccount требуют большей устойчивости, чем класс Userinterface.
■ Учитывая интересы классов, которые должны быть видимы из класса ATM, экземпляр объекта класса CheckingAccount может быть порожден с классом ATM и снабжен интерфейсом с классом CheckingAccount.
На этой стадии нет однозначного решения по реализации, все зависит от других диаграмм взаимодействия и других ограничений на стадии проектирования, таких как механизм хранения класса CheckingAccount (например, класс CheckingAccounts может храниться в базе данных, а в пределах объекта ATM может быть видимым и доступным только один его экземпляр).
В этом конкретном примере CheckingAccount — это объект, экземпляр которого будет порожден в объе.,1 ATM, а экземпляр объекта ATM будет порожден в объекте Userinterface. В листинге 2.22 представлено объ,п,- ление класса Userinterface с атрибутом ATM, включенным в качестве приватного элемента данных the_Alivi.
Листинг 2.22. Объявление класса Userinterface со включенным объектом ATM
class Userinterface (
public:
// Общедоступный интерфейс
char * CheckBalance () ;
BOOL WithdrawCash ();
private:
// Приватные атрибуты
ATM the_ATM;
);
Далее, объект ATM взаимодействует только с объектом CheckingAccount и, как уже отмечалось выше, будет содержать только экземпляр объекта CheckingAccount. Поскольку он содержит объект CheckingAccount, в его обязанность входит поддержание связи с CheckingAccount в интересах класса Userinterface. В листинге 2.23 представлено объявление класса ATM с включенным в него объектом: the_CheckingAccount. Оно также содержит метод WithdrawCashFromCheckingAccount() (снятие суммы с текущего счета), который взаимодействует с объектом CheckingAccount класса Userinterface.
Листинг 2.23. Объявление класса ATM, содержащего объект CheckingAccount
class ATM <
public:
// Общедоступный интерфейс
char * CheckBalance ();
char * DisplayBalance ();
// Интерфейс класса CheckingAccount
BOOL WithdrawCashFromCheckingAccount ();
private:
H Приватные атрибуты
CheckingAccount the_CheckingAccount;
Класс CheckingAccount не содержит собственных классов. Класс Receipt может содержаться в классе CheckingAccount, поскольку с ним установлена связь, тем не менее, это не имеет смысла, ибо создание объекта Receipt в CheckingAccount логически не обосновано. Это еще одно решение, которое принимается на стадии реализации, оно оправдано тем, что объект Receipt не характерен для объекта CheckingAccount, в силу чего он не может стать частью объекта CheckingAccount.
Класс Receipt не должен устанавливать связь с каким-либо классом, представленным на диаграмме взаимодействия, так что в область его видимости никакие классы не попадают.
Завершающий шаг состоит в том, чтобы выявить все операции, посылающие сообщения, получающие сообщения и выдающие наличность. Наиболее простой путь решения данной задачи — проследовать в направлении стрелок на диаграмме взаимодействия. Первое из сообщений, CheckBalance, посылается с объекта Userinterface на объект ATM. Следующее сообщение, GetBalance, посылается с объекта ATM на объект CheckingAccount. Тем самым накладывается следующее ограничение: функция ATM::CheckBalace() должна
Объектно-ориентированное программирование Часть I
вызвать функцию ChcckingAccount::GetBalance(), прежде чем завершит свою работу. Реализация функции ATM::CheckBalance() представлена в листинге 2.24.
Листинг 2.24. Реализация функции CheckBalanceQ в классе ATM char * ATM::CheckBalance {
// Выполнение других функций, отслеживающих состояние счета
Н Вызов функции CheckingAccount::GetBalance szBalance = the_CheckingAccount.GetBalance () ;
// Возврат данных о состоянии счета return szBalance;
}
Следующее сообщение, DisplayBalance, направлено от объекта CheckingAccount к объекту ATM. В силу этого объект CheckingAccount должен вызвать метод ATM::DisplayBalance() в свой метод GetBalance(). В листинге 2.25 представлена реализация метода CheckingAccount::GetBalance().
Листинг 2.25 Реализация функции GetBalanceQ в классе CheckingAccount
char * CheckingAccount::GetBalance() {
// Просмотр состояния Balance в базе данных
// Вызов функции ATM:-.DisplayBalance () для передачи следующего сообщения
// GetParentO возвращает указатель на родительский класс ATM
GetParent()->DisplayBalance ();
// Возврат данных об остатке заинтересованному лицу return szBalance;
)
Функция UserInterface::WithdrawCash() получена аналогичным образом, ее реализация представлена в листинге 2.26.
Листинг 2.26. Функция Userlnterface::WithdrawCash() Function
BOOL Userinterface::WithdrawCash() {
// Выдать значение, которое возвращает ATM:iWithdrawCashFromCheckingAccount return the_ATM.WithdrawCashFrcmCheckingAccount();
}
В листинге 2.27 представлена реализация функции CheckingAccount::WithdrawCash(). Изучение диаграмм взаимодействия показывает, что эта функция генерирует два события: UserInterface::Dispense() и Receipt::Print(). Функция UserInterface::Dispense() выдает клиенту наличность из машины ATM, а функция Receipt::Print() печатает квитанцию для клиента. В этом листинге предполагается, что GetParentO — это функция, которая существует в каждом классе, и что она возвращает указатель своему родителю. Более того, предполагается, что имеется также функция GetReceipt(), существующая в объекте Userinterface, которая возвращает системный объект Receipt. На реализацию этих функциональных средств ограничения не накладываются, с этой целью разработчик может выбрать любой приемлемый (однако функциональный) способ.
Листинг 2.27. Функция CheckingAccount::WithdrawCash()
BOOL CheckingAccount::WithdrawCash() {
// Выполнить функции, реализующие снятие со счета наличных денег
Н Послать сообщение Dispense объекту Userinterface
GetParent()->GetParent()->Dispense
// Послать сообщение Print объекту Receipt
GetParentO ->GetParent() ->GetReceiptО ->PrintO ;
// Возвратить результат
if (fSuccess)
Глава 2Глава 2return TRUE; else
Глава 2
Глава 2
return FALSE; )
Перевод диаграмм состояний в С++
Диаграммы взаимодействий отображают взаимодействие между переменными объектами, в то время как диаграммы состояний отображают те изменения, которые происходят с отдельным объектом. Диаграммы состояний показывают все состояния внутри объекта и переходы, которые существуют между состояниями. Диаграммы перехода должны быть созданы для всех объектов, имеющих следующие характеристики:
Множество состояний, в которых может находиться объект
Конкретные действия, которые должны быть осуществлены при переходе из одного состояния в другое
Если при переходе объекта из одного состояния в другое должно произойти какое-либо событие, диаг
раммы состояний могут оказать существенную помощь для правильной реализации проектных решений. В диаграммах состояний состояние Start означает начало перехода, обычно представленное началом функции-члена, в которой содержится переход. Состояние End означает завершение перехода из одного состояния в другое и может соответствовать завершению этой функции-члена. Состояния в объекте представлены переменными, характерными для конкретных приложений. Например, объект светофор принимает три
состояния (зеленый, желтый и красный), которые могут быть представленные целым значением состояния. И хотя реализация фактического перехода из одного состояния в другое зависит от конкретного приложения, как только инициировано состояние Start, действия, которые должны быть выполнены, должны
быть четко определены, а их реализация должна быть простой. Возвратимся к примеру класса CustomerAccount из главы 1.
На рис. 2.14 показана диаграмма состояний для этого класса.
Эта диаграмма дает представление о том, что объект (или в случае с языком C++ — класс) должен обеспечить интерфейс, позволяющий пользователю зарегистрироваться в системе. Конкретнее, класс должен каким-то образом запросить у пользователя информацию о его счете (GettingAccountlnfoO), пригласить ввести пароль (GettingPassword()) и изменить состояние объекта с NotLoggedln на Loggedln — и все это с сохранением возможности отмены пользователем операции в любой момент (Canceled). Эти шаги могут быть реализованы в одном методе или в наборе функций, эта реализация зависит от приложения и от разработчика. Для достижения этой цели в данном примере используется метод Login.
Класс CustomerAccount поддерживает булевскую приватную переменную-член m_fLoggedIn, которая содержит информацию о состоянии клиента в момент регистрации в системе. Эта переменная проверяется всеми методами, которые выполняют какие- либо действия, требующие связи пользователя с системой (например, GetBalanceO или WithdrawCash()). Объявление класса CustomerAccount представлено в листинге 2.28.
РИСУНОК 2.14. Диаграмма перехода объекта CustomerAccount.РИСУНОК 2.14. Диаграмма перехода объекта CustomerAccount.Листинг 2.28. Объявление класса CustomerAccount
РИСУНОК 2.14. Диаграмма перехода объекта CustomerAccount.
РИСУНОК 2.14. Диаграмма перехода объекта CustomerAccount.
class CustomerAccount {
public:
// Создание / Разрушение CustomerAccount () ;
-CustomerAccount();
// Общедоступный интерфейс BOOL Login();
BOOL Logout();
char *GetBalance () ;
private:
5656Часть I
56
56
// Приватные данные о состоянии BOOL m_fLogged!n=FALSE;
// Приватные данные char *m_szBalance;
};
Конструктор класса CustomerAccount инициализирует переменную-член m_fLoggedIn со значением false, так что ни одна из функций не попытается выполнить неавтономные действия, будучи в автономном режиме. Деструктор проверяет состояние регистрации и при необходимости выводит клиента из системы, перед тем как завершить работу. Функция LogOut() просто присваивает переменной m_fLoggedIn значение false.
Функция Login выполняет действия, уточненные на диаграмме состояний: сначала она запрашивает у пользователя номер счета и сохраняет его в переменной szAccountNumber. На практике реализация этого объекта требует использования более совершенных методологий отказа, однако, чтобы привлечь ваше внимание к тому факту, что это действие может быть прекращено, номер счета сравнивается со значением Cancel, прежде чем работа будет продолжена. Затем клиент получает просьбу указать свой пароль. И снова ответ клиента сравнивается со значением Cancel. Если клиент ввел и номер счета, и пароль, проверка этих значений выполняется другой функцией, а именно функцией ValidPassword(), которая, по всей видимости, выполняет несколько просмотров базы данных и операций сравнения. После того как все эти действия будут выполнены, значение переменной m_fLoggedIn меняется на true. Если какое-либо событие отменено, значение переменной m_fLoggedIn остается false и переход не происходит. В листинге 2.29 показана реализация класса CustomerAccount.
Листинг 2.29. Реализация класса CustomerAccount
#include "CustomerAccount.h"
CustomerAccount::CustomerAccount() {
m_fLoggedln = FALSE;
}
CustomerAccount::-CustomerAccount() {
if (m_fLoggedin) Logout () ;
)
BOOL CustomerAccount::Login()
char szAccountNumber[32]; char szPassword[32];
// Получение данных о банковском счете printf ("Enter Account Number:");
scanf ("%s",szAccountNumber);
H Событие может быть отменено в любой момент if (Istrcmp (szAccountNumber, "Cancel"))
return FALSE;
I/ Получение пароля printf ("Enter Password:"); scanf ("%s", szPassword);
// Событие может быть отменено в любой момент if (!strcmp (szPassword, "Cancel"))
return FALSE;
H Некоторые вспомогательные функции, выполнявшие проверку пароля if (IValidPassword (szAccountNumber, szPassword))
return FALSE;
else
// Пароль действительный — Зарегистрировать пользователя в системе m_fLogged!n = TRUE ;
// Проверка закончилась успешно return TRUE;
)
Глава 2Глава 2BOOL CustomerAccount::LogOut() (
Глава 2
Глава 2
m_fLoggedln = FALSE;
)
char *CustomerAccount::GetBalance() {
// Проверка состояния регистрации if(!m_fLoggedln)
return NULL;
II Возвратить данные об остатке return m_szBalance;
}
Перевод диаграмм активности в C++
Диаграмма активности — это специальный тип диаграммы состояний, в которой представлены состояния действия и транзакции, имеющие место по завершении операций. Диаграммы активности могут быть использованы для представления диаграмм перехода в синхронные состояния, на которых все или большая часть событий на диаграмме представляют собой завершение операций внутреннего происхождения. Когда возникают асинхронные события, используйте диаграммы переходов для описания модели.
Реализация диаграмм активности во многом напоминает реализацию диаграмм перехода: состояние Start представляет собой начало операции, а состояние End — ее завершение. Поскольку диаграммы активности — синхронные диаграммы, то в момент, когда операции запускаются на выполнение, все действия однозначно определены.
Рассмотрим диаграмму активности, представленную на рис. 2.15.
РИСУНОК 2.15.
Диаграмма активности для операции снятия со счета денег с помощью машины ATM.
Диаграмма, изображенная на рис. 2.15, представляет три объекта, взаимодействующих друг с другом: Customer, ATM и CheckingAccount. Диаграмма запускается в момент, когда Customer предпринимает попытку выполнить WithdrawCash (Снять сумму со счета). Активность WithdrawCash запускает две другие одновременные активности: AuthorizeTransaction и ProcessRequest. ProcessRequest осуществляет переход к активности FundsAvailable объекта CheckingAccount. Оба перехода — AuthorizeTransaction и FundsAvailable —
Часть I
ведут к активности DebitAccount объекта ATM — активность DebitAccount не может иметь места до тех пор, пока обе предшествующие активности на будут завершены. Когда завершится активность DebitAccount, происходит переход к активности ReceiveCash, а данная диаграмма активности завершается.
При создании или изучении диаграммы активности сконцентрируйте внимание на реализации общей процедуры. И хотя диаграмма активности, изображенная на рис. 2.15, содержит информацию, которая может быть реализована как взаимодействия объектов, область применения диаграммы активности ограничивается моделированием соответствующей процедуры. Процедура, которая моделируется в рамках этого примера, представляет собой транзакцию снятия со счета наличности; фактическая реализация операций с объектами излишня для случая реализации процедуры снятия со счета денег.
В листингах 2.30—2.32 представлены соответственно объявления классов Customer, ATM и CheckingAccount. Ядро диаграммы активности реализовано в рамках двух функций — WithdrawCashTransaction() и ProcessRequest(), представленных в листинге 2.33. Функция WithdrawCashTransaction() сначала вызывает операцию Customer::WithdrawCash(). После того как эта активность завершится, вызывается операция Customer::AuthorizeTransaction() и функция ProcessRequest(). Функция ProcessRequest() вызывает операцию ATM::ProcessRequest(), за которой следует операция CheckingAccount::FundsAvailable(). Необходимость поместить вызовы этих операций в отдельные функции обосновывается двойной зависимостью от операции ATM::DebitAccount(). Функция ATM::DebitAccount() не может быть запущена до тех пор, пока не будет завершено выполнение функций Customer::AuthorizeTransaction() и CheckingAccount::FundsAvailable(). На этой стадии остаются невыполненными такие действия, как вызов функции Customer: :ReceiveCash() и ее завершение.
Листинг 2.30. Объявление класса Customer
class Customer ( public:
// Создание / Разрушение Customer () ;
-Customer () ;
// Общедоступный интерфейс BOOL WithdrawCash () ;
BOOL AuthorizeTransaction() ; void ReceiveCash();
Листинг 2.31. Объявление класса ATM
class ATM ( public:
// Создание / Разрушение ATM() ;
-ATM();
// Общедоступный интерфейс BOOL ProcessRequest() ;
void DebitAccount() ;
Листинг 2.32. Объявление класса CheckingAccount class CheckingAccount { public:
// Создание / Разрушение CheckingAccount();
-CheckingAccount();
11 Общедоступный интерфейс BOOL FundsAvailable() ;
Глава 2
Листинг 2.33. Реализация функций WithdrawCashTransactionQ и ProcessRequestQ void WithdrawCashTransaction ( // Активные классы Customer myCustomer;
ATM my ATM ; CheckingAccount myCheckingAccount;
// * Состояние пуска
// Запуск первой активности myCustomer.WithdrawCash();
// Теперь, когда первая транзакция завершена, запускайте две следующие if (myCustomer.AuthorizeTransaction() 44 ProcessRequest()) {
И Теперь, когда оба действия завершены, продолжайте обработку myATM.DebitAccountO; myCustomer.ReceiveCash();
}
Н * Состояние завершения }
BOOL ProcessRequest () {
myATM.ProcessRequest(); return myCheckingAccount.FundsAvailable(); )
Резюме
В этой главе рассматривалась реализация в C++ некоторых наиболее популярных моделей UML, полученных на стадии проектирования. Вы узнали о диаграммах классов, диаграммах взаимодействий (о диаграммах совместных работ и диаграммах последовательности действий), диаграммах состояний и диаграммах активности. Универсальный язык моделирования UML обладает достаточно мощными инструментальными средствами, которые оказывают специалистам по программному обеспечению неоценимую помощь как на стадии анализа, так и на стадии проектирования. В одних случаях перевод таких проектных решений в программные коды C++ производится по формальным правилам, в других случаях следует учитывать особенности приложения, однако, так или иначе, успех проекта возможен только при наличии мощной базы методов реализации проектных решений. Существуют целый ряд инструментальных средств, которые об- легчающт задачу разработчиков, моделирующих соответствующий процесс. Инструментальные средства, разработанные компанией Rational Rose, могут генерировать для некоторых диаграмм программные коды C++ и даже переводить диаграммы из одной формы в другую. Завершив изучение этой главы, вы должны поупражняться с некоторыми из этих автоматических инструментальных средств, чтобы убедиться в том, что они генерируют те программные коды C++, на которые вы рассчитываете.
К
1i
if
Наследование, полиморфизм
и повторное использование
программных кодов
В ЭТОЙ ГЛАВЕ
■ Преимущества наследования
■ Объектно-ориентированные связанные списки
■ Абстрактные классы
■ Виртуальные деструкторы
■ Полиморфизм, реализованный путем перегрузки методов класса
■ Управление памятью
■ Проблемы при перегрузке других операторов
■ Множественное наследование
Глава 3
Способность обнаруживать шаблоны и отношения между предметами в нашей жизни — это именно то, что нам приходится делать. Мы унаследовали эту способность от наших далеких предков, которые пользовались ею для предсказания поведения опасных животных. Если они обнаруживали, что животные с короткой шерстью, длинными клыками и крупными зубами пожирают своих соседей, они остерегались их. Одним из основных отношений, которое мы открываем в возрасте примерно трех лет является отношение это есть: "Смотри, мама, это (есть) фрукт. Это (есть) машина". Мы делаем такие открытия задолго до того, как овладеваем языком, чтобы сформулировать его; трехлетний мальчик, увидев пожарную машину, объявил: "Большая красная машина".
Величайшей заслугой объектно-ориентированного анализа является то, что благодаря ему стало возможным практическое использование феноменологии. Подобно Гегелю и Канту, мы может спросить: "Что такое автомобиль? Что отличает его от грузовой машины, от человека, от скалы?" С одной точки зрения, автомобиль представляет собой совокупность определенных частей: рулевого колеса, тормозов, сидений, фар. С другой точки зрения, также имеющей право на жизнь, Автомобиль — не роскошь, а Средство передвижения. Утверждая, что автомобиль средство передвижения, мы используем богатое смыслом сокращение.
Поскольку автомобиль есть средство передвижения, он перемещается и перевозит вещи. В этом и состоит предназначение Средства передвижения. Автомобили наследуют характеристики "перемещается" и "перевозит вещи" от своего родительского типа: Средства передвижения. Мы также знаем, что Автомобили сугь специализация Средств передвижения. Они представляют собой специальный вид Средств передвижения, а именно тот, который соответствует техническим характеристикам Автомобилей.
Моделируя это отношение, мы пользуемся приемом обобщения, а при реализации в языке C++ — наследованием.
Преимущества наследования
Использование свойства наследования влечет за собой два преимущества. Во-первых, мы можем конкретизировать существующие классы (типы) и писать только те программные коды, которые претерпели изменения. Таким образом, мы можем повторно воспользоваться существующими классами (в качестве базовых классов) и извлечь для себя выгоду из работы, которая когда-то уже была проделана. Теперь нам не обязательно "копировать и вставлять" программные коды. Копирование и вставка особо проблематичны, поскольку изменения, внесенные в один раздел программного кода, влекут за собой соотвествующие изменения в другом разделе — это весьма опасная, чреватая отказами практика. Используя вместо этого специализацию, мы можем подвергать изменениям базовый класс, который автоматически вносит нужные изменения во все производные классы.
Вторым существенным преимуществом наследования является то, что оно позволяет подходить к производным объектам с позиций полиморфизма. Поли означает много, морф означает форму, полиморфизм означает способность принимать многие формы.
Пожалуй, наиболее широкое применение полиморфизм находит в приложениях, интенсивно использующих окна. В этом отношении библиотека Microsoft Foundation Classes могут служить прекрасным примером. Фактически все "экранные окна" происходят непосредственно или косвенно от CWindow, который инкапсулирует общедоступный интерфейс функциональных средства для всех объектов на экране. CWindow предлагает такие методы, как UpdateWindow(), BeginPaint(), GetWindowText(), и многие другие.
Каждый из этих методов может быть перекрыт в любом из классов, которые происходят от CWindow. В число таких производных классов входят CButton, CRadioButton, CListBox и др. Когда вы вызываете UpdateWindow() на CButton, она воспроизводит себя. Когда вы вызываете UpdateWindow() на CListBox, окно списка также воспроизводит себя. Подробности того, что делают CButton и CListBox, когда они воспроизводят себя, существенно отличаются, однако эти подробности инкапсулированы интерфейсом UpdateWindow(), представленным CWindow.
Объектно-ориентированные связанные списки
Чтобы продемонстрировать могущество полиморфизма, подробно ознакомимся с объектно-ориентированным связанным списком. Как вам уже, возможно, известно, связанный список — это структура данных, разработанная с таким расчетом, чтобы хранить неопределенное число объектов. Вы можете, естественно, хранить объекты только в массивах, но массивы имеют фиксированные размеры. Если вы заранее не знаете, какое число потребуется, то массив — не самый лучший выбор. Если вы сделаете массив слишком большим, это чревато излишней тратой памяти, если он будет слишком мал, быстро возникнет не-
Часть I
хватка памяти. Все, что нужно в подобного рода случаях, — это массив, способный расширяться; связанный список — это подходящая отправная точка.
Разработка связанного списка
Связанный список обычно реализуется в виде последовательности узлов. Каждый узел указывает на один объект (данные), а также, возможно, на следующий узел в списке. Если следующий узел отсутствует в списке, узел указывает на NULL (рис. 3.1).
РИСУНОК 3.1.
Связанный список.
Мы хотим, чтобы каждый узел выполнял специальные функции, таким образом, мы создаем три специализированных типа: LinkedList (связанный список), TaiiNode (хвостовой узел) и InternalNode (внутренний узел). LinkedList предоставляет клиенту точку входа в список. TaiiNode действует как часовой, он отмечает конец списка. И наконец, узлы InternalNode содержат фактические данные.
Разложим обычное поведение этих трех типов в базовый класс Node, который поддерживает два метода: Insert() и Show(). Метод Insert() берет объект и помещает его в список, а метод Show() отображает значения данных в списке. Отношения между этими объектами показано на рис. 3.2.
РИСУНОК 3.2.
Иерархия наследования узлов.
В узлах LinkedList и InternalNode имеется переменная-член Node * под именем myNext. Этот указатель используется для поиска следующего узла в списке. Это, как легко видеть, односвязный список, т.е. каждый узел указывает на следующий узел в списке, но не указывает на предыдущий.
Чтобы что-то поместить в список, создадим класс Data. Все, что потребуется от класса Data, — это иметь некоторое значение (мы воспользуемся целыми значениями) и метод, обеспечивающий двум объектам Data возможность сравниваться и решать, какой из них "больше", что позволило бы их упорядочивать. Мы бы также предпочли, чтобы объект Data был способен отображать себя, так чтобы мы могли знать его значение.
ПРИМЕЧАНИЕ
В рассматриваемом случае утверждаем, что LinkedList — это фактически особый тип узла, обеспечивающий предоставление интерфейса клиентам связанного списка. Не нарушает ли это мандат, согласно которому наследование представляет отношение это есть? Вовсе нет; LinkedList — специальный узел, который обозначает начало списка. Сам список — это виртуальное построение, абстракция. Объект LinkedList, по существу, представляет собой дескриптор списка.
Реализация связанного списка
Когда создается узел LinkedList, он немедленно строит узел TaiiNode:
LinkedList::LinkedList()
{
Глава 3
nextNode = new TaiiNode;
}
Таким образом, пустой список состоит из двух таких объектов: LinkedList и TaiiNode, а узлы InternalNodes, содержащие Data, отсутствуют (рис. 3.3).
Когда вы вводите данные в LinkedList, узел LinkedList передает данные туда, куда показывает его собственный указатель nextNode. Изначально это TaiiNode:
Node * LinkedList::Insert(Data * theData <
nextNode = nextNode->Insert(theData); return this;
)
Когда в узел TaiiNode вызывается Insert(), TaiiNode знает, что передаваемый в него объект — это наименьший объект в списке. Таким образом, TaiiNode вставляет новый узел в список непосредственно перед собой.
Если новый объект — первый объект, включаемый в список, то он по определению является наименьшим объектом в списке, однако любой объект, появляющийся в хвосте, должен быть наименьшим, в противном случае он уже должен быть включен в список другим узлом, находящимся ближе к началу списка.
Как узел TaiiNode вводит новый объект данных? Он порождает новый экземпляр узла InternalNode. Для построения нового InternalNode конструктору требуется два аргумента: указатель на данные и указатель на узел, который его создал (в рассматриваемом случае — TaiiNode). Затем новый узел присваивает свой указатель данным, а также свой указатель nextNode узлу, которому он был дан, а именно:
Node * TaiiNode::Insert(Data * theData) (
InternalNode * dataNode = new InternalNode(theData, this); return dataNode;
)
Новый экземпляр InternalNode указывает на данные, а также на TaiiNode. Узел TaiiNode возвращает вызывающему объекту указатель на новый, созданный им узел. В первом случае TaiiNode возвращает указатель на новый InternalNode узлу LinkedList.
Узел LinkedList назначает собственный указатель nextNode на возвращаемое значение из nextNode->Insert(). Таким образом, теперь LinkedList указывает на новый InternalNode, как показано на рис. 3.4.
РИСУНОК 3.3. Пустой список.РИСУНОК 3.4. Список после включения в него узла.
В следующий раз, однако, LinkedList будет передавать данные тому объекту, на который указывает его указатель nextNode, но в этом случае указатель указывает не на TaiiNode, а на InternalNode.
Когда InternalNode получает объект Data, он сравнивает собственные данные с данными из нового узла, например, таким образом:
Node * InternalNode::Insert(Data * theData) {
int result = myData->Compare(*theData);
Новый объект либо больше существующего узла, либо меньше. Если он меньше, то InternalNode передает объект Data туда, куда указывает его собственный элемент данных NextNode, т.е. новый объект Data передается следующему узлу в списке: case klsSmaller: nextNode = nextNode->Insert(theData); return this;
В свою очередь, каждый InternalNode анализирует данные. Если новые данные являются наименьшими в списке, они в конечном итоге переместятся в TaiiNode и будут включены в список в качестве последнего
Часть I
узла. С другой стороны, если новый объект Data попадает в узел InternalNode, содержащий данные, которые меньше,.чем новые данные, то этот InternalNode включает новые данные в список.
В этом случае InternalNode делает то, что делал TaiiNode: он создает новый узел InternalNode и дает новому InternalNode инструкции указывать на него (т.е. новый узел InternalNode указывает на InternalNode, который он создал). Таким образом, он фактически включает новый объект в список непосредственно перед текущим InternalNode. Затем текущий узел возвращает указатель на новый узел, так что, кто бы ни вызвал Insert(), он теперь может установить связь с новым InternalNode.
case klsSame:// неудача
case klsLarger: // новые данные помещаются передо мной {
InternalNode * dataNode = new InternalNode(theData, this);
return dataNode;
}
ПРИМЕЧАНИЕ
В этом примере программного кода реализуется проектное правило, которое заключается в том, что объекты того же размера, что и текущий объект, рассматриваются как объекты большего размера. Вы легко можете изменить этот код, чтобы исключить дубликаты или обращаться с ними особым способом.
После того как данные оказались в списке, мы запрашиваем у пользователя новый объект. Программа продолжается до тех пор, пока пользователь не укажет, что он закончил ввод данных. На этой стадии потребуем, чтобы LinkedList отобразил эти данные. Узел LinkedList пересылает эту команду туда, куда указывает его элемент данных NextNode: virtual void Show() { nextNode->Show(); )
Если список пуст, LinkedList указывает на TaiiNode. Метод Show() узла TaiiNode никаких действий не совершает, соответственно и отображать нечего. Более обычным является ситуация, когда LinkedList указывает на InternalNode. Для метода Show() узла InternalNode характерно действие
virtual void Show() { myData->Show(); nextNode~>Show(); }
Каждый InternalNode требует от своего объекта данных печатать его значение, а затем посылает команду тому узлу, который стоит следующим в связанном списке. Таким образом, метод Show() вызывается по очереди для каждого объекта данных Data. В конечном итоге доходит очередь и до метода Show() узла TaiiNode, на чем и заканчивается отображение.
Когда возникает необходимость удалить список, клиенту достаточно удалить только узел LinkedList. В деструкторе LinkedList мы удаляем следующий узел списка таким образом:
-LinkedList() { delete nextNode; }
Каждый узел, в свою очередь, удаляет следующий за ним в списке узел, пока не дойдет очередь до узла TaiiNode, на этом, собственно говоря, удаления и заканчиваются. Обратите внимание на тот факт, что совсем необязательно знать, сколько узлов содержится в списке Удаление узла LinkedList подобно падению первой кости домино: каждая предыдущая, падая, сбрасывает следующую, пока не будут использованы все кости.
В листинге 3.1 показано, как выглядит вся программа. Здесь содержатся только те программные коды, которые имеют непосредственное отношение к рассматриваемой процедуре.
Листинг 3.1. Объектно-ориентированный связанный список #include <iostream.h> enum { kSmaller, kLarger, kSame); class Data ( public: Data(int val):dataValue(val){} virtual ~Data(){) virtual int Compare(const Data S) ; virtual void Show() { cout « dataValue « endl; } private: int dataValue;
Глава 3
);
int Data::Compare(const Data S theOtherData) { if (dataValue < theOtherData.dataValue) return kSmaller;
if (dataValue > theOtherData.dataValue) return kLarger;
else
return kSame;
)
class Node / / абстрактный тип данных {
public:
Node()(}
virtual -Node(){}
virtual Node * Insert (Data * theData) = 0;
virtual void Show() = 0;
private:
class InternalNode: public Node { public: InternalNode(Data * theData, Node * next); -InternalNode(){ delete nextNode; delete myData; } virtual Node * Insert(Data * theData); virtual void Show() { myData->Show() ; nextNode->Show(); }
private:
Data * myData;
Node * nextNode ;
};
InternalNode::InternalNode(Data * theData, Node * next): myData(theData),nextNode(next) { )
Node * InternalNode::Insert(Data * theData) {
int result = myData->Compare(*theData);
switch(result)
<
case kSame: // выпадание
case kLarger: H отображаются новые данные (
InternalNode * dataNode = new InternalNode(theData, this); return dataNode;
case kSmaller:
nextNode = nextNode->Insert(theData) ; return this;
) return this;
class TaiiNode : public Node (
public:
TaiiNode () {)
-TaiiNode () {)
virtual Node * Insert(Data * theData);
virtual void Show() ( }
3 Зак.53
Объектно-ориентированное программирование Часть I private: );
Node * TaiiNode::Insert(Data * theData) <
InternalNode * dataNode = new InternalNode(theData, this); return dataNode;
}
class LinkedList : public Node ( public:
LinkedList () ;
-LinkedList() ( delete nextNode; } virtual Node * Insert(Data * theData); virtual void Show() ( nextNode->Show() ; } private: Node * nextNode;
);
LinkedList::LinkedList() ( nextNode = new TaiiNode; }
Node * LinkedList::Insert(Data * theData) { nextNode = nextNode->Insert(theData); return this;
}
int main() {
Data * pData; int val ;
LinkedList 11;
for (;;) (
cout « "What value do you want to add to the list? (0 when done) : "; cin » val ; if (!val) break;
pData = new Data(val);
11.Insert(pData);
} cout « "\n\n"; 11.Show(); cout « "\n\n"; return 0 ; )
У каждого объекта имеется собственная область ответственности, и каждый объект рассматривает все узлы полиморфно. Однако мы можем использовать специфику узлов LinkedList, InternalNodes и TaiiNode, чтобы строить в высшей степени децентрализованные приложения. Это позволит повысить уровень инкапсуляции, благодаря чему с программными кодами легче будет работать.
Наиболее важной частью этого кода с точки зрения понимания полиморфизма» является точка, в которой каждый Node вызывает nextNode->Insert(). Каждый узел, отличный от TaiiNode, знает только, что у него имеется указатель на следующий узел списка, но не знает, является ли этот узел InternalNode или TaiiNode, однако различие между ними определяет статический тип. Проанализируем функции lnternalNode::Insert() и TailNode::Insert() — они существенно отличаются друг от друга. Объект, вызывающий их, не знает, какую из них он вызывает, однако все идет как надо.
Аналогично, когда вы требуете от узлов проявить себя, все происходит должным образом, т.е. LinkedList требует от следующего узла проявить себя, каждый узел InternalNode отображает содержащиеся в нем дан-
Глава 3
ные, а затем посылает эту команду следующему за ним узлу; узел TaiiNode, завершая цепь, действует как часовой.
Абстрактные классы
Класс Data, с которым мы работали в первой части этой главы, достаточно прост. Может случиться так, что приложение потребует несколько различных, но в то же время связанных между собой классов данных. При этом может оказаться полезным иметь возможность полиморфного подхода к этим объектам, позволяя связанному списку манипулировать классами Data без учета требований специфики отдельных производных типов.
С этой целью вы можете принять решение присвоить классу данных Data тип абстрактных данных (ADT — Abstract Data Туре). Чтобы сделать это, вы должны добавить этому классу одну или большее число чисто виртуальных функций, как показано в листинге 3.2.
Листинг 3.2. Класс Data как тип абстрактных данных class Data { public:
Data(int val):myVal(val)( }
virtual ~Data(){ )
int Compare(const Data &) ;
virtual void Show() = 0;
protected:
int myVal;
};
Семантика этого объявления устанавливает, что класс Data имеет тип ADT, и, чтобы создать конкретный класс Data, необходимо перекрыть метод Show() и предложить метод, отличный от чисто виртуального. Наиболее важная идея заключается в том, что разработчику класса Data требуются производные классы для перекрытия метода Show() и конкретной реализации. Вы можете сделать это для усиления семантики этого объекта с таким расчетом, чтобы он был абстрактным и чтобы все производные от него конкретные объекты проявляли себя соответствующим образом. По существу, вы утверждаете: "Каждый объект класса Data может показать себя, однако общего метода реализации этого свойства не существует".
На этой стадии вы можете создать классы, которые отличаются друг от друга способами отображения значений. В рамках такого искусственного примера построим два подобных класса: IntegerData, который отображает данные в виде целых значений, и Graphic Data, который отображает значение в виде простейшей диаграммы. Эти классы показаны в листинге 3.3.
Листинг 3.3. Конкретные типы данных
class IntegerData : public Data {
public:
IntegerData(int val) : Data(val) { )
virtual -IntegerData () { )
virtual void Show();
private:
class GraphicData : public Data <
public:
GraphicData(int val) : Data(val) ( )
virtual -GraphicData() { }
virtual void Show() ;
private:
};
Каждый из этих классов перекрывает метод Show(). Вот как это делает класс IntegerData: void IntegerData::Show() (
Часть I
cout « "The value of the integer is " « myVal « endl; }
А так класс GraphicData перекрывает метод Show();
void GraphicData::Show() { cout « "(" « myVal « ") : ";; for ( int i = 0; i < myVal; i++) cout « "*"; cout « endl ;
}
Теперь можно воспользоваться этими программными кодами в своей исходной программе и решать во время выполнения программы, каким порожденным типом воспользоваться. Вызывающая программа не обязательно должна знать, какой из методов Show() будет вызван, благодаря свойствам реализованного полиморфизма производится вызов правильного метода, в этом вы можете убедиться, просмотрев листинг 3.4.
Листинг 3.4. Типы полимерных данных ♦include <iostream.h> enum ( kSmaller, kLarger, kSame); class Data ( public: Data(int val):myVal(val)(} virtual -Data () ( ) int Compare(const Data 6) ; virtual void Show() = 0; protected: int myVal;
);
class IntegerData : public Data ( public: IntegerData(int val) : Data(val) (} virtual -IntegerData() () virtual void Show() ;
private:
void IntegerData: :Show() (
cout « "The value of the integer is " « myVal « endl; }
class GraphicData : public Data ( public: GraphicData(int val) : Data(val) {) virtual -GraphicData() ( ) virtual void Show() ;
private: };
void GraphicData::Show() { cout « " (" « myVal « ") : " ; ; for ( int i = 0; i < myVal; i++) cout « "*"; cout « endl;
}
int Data::Compare(const Data & theOtherData) ( if (myVal < theOtherData.myVal)
Глава 3
return kSmaller;
if (myVal > theOtherData.myVal) return kLarger;
else return kSame;
)
class Node / / тип абстрактных данных
(
public:
Node(){}
virtual -Node () { }
virtual Node * Insert (Data * theData) = 0;
virtual void Show() = 0;
private:
};
class InternalNode: public Node {
public:
InternalNode(Data * theData, Node * next);
-InternalNode()( delete nextNode; delete myData; } virtual Node * Insert(Data * theData); virtual void Show() ( myData->Show() ; nextNode->Show(); }
private:
Data * myData ;
Node * nextNode ;
};
InternalNode::InternalNode(Data * theData, Node * next): myData(theData),nextNode(next) ( )
Node * InternalNode::Insert(Data * theData) {
int result = myData->Compare(*theData);
switch(result)
(
case kSame: // выпадание
case kLarger: H отображаются новые данные (
InternalNode * dataNode = new InternalNode(theData, this); return dataNode;
} case kSmaller: nextNode = nextNode->Insert(theData); return this;
) return this;
}
class TaiiNode : public Node
(
public:
TaiiNode () (}
-TaiiNode () {}
virtual Node * Insert(Data * theData);
virtual void Show() ( }
private:
);
Node * TaiiNode::Insert(Data * theData) {
InternalNode * dataNode = new InternalNode(theData, this);
Часть I return dataNode;
class LinkedList : public Node
public:
LinkedList () ;
-LinkedList () { delete nextNode; } virtual Node * Insert(Data * theData); virtual void Show() { nextNode->Show() ; } private: Node * nextNode;
LinkedList,: :LinkedList() { nextNode = new TaiiNode; )
Node * LinkedList::Insert(Data * theData) (
nextNode — nextNode-Mnsert (theData) ; return this;
}
int main() {
Data * pData; int val; int whichData; LinkedList 11, for (; ;) {
cout « "[1] Integer, [2] Written, [0] Quit: "; cin » whichData; if ('whichData) break;
cout « "What value do you want to add to the list? " ; cin » val; if (whichData == 1)
pData = new IntegerData(val); else
pData = new GraphicData(val);
11.Insert(pData);
cout « "\n\n";
11.Show();
cout « "\n\n";
return 0;
}
Связанный список не претерпел изменений по сравнению с ранней версией программы, однако на этот раз функция main() просит пользователя назвать тип объекта, которым он хочет воспользоваться, чтобы хранить значение. Как только пользователь введет данные, обращение к Show() повлечет вызов правильного метода. Узел знает только то, что в нем содержится некоторый вид объекта Data; он не знает и не желает знать, является ли это объект IntegerData или объект GraphicData. В каждом узле содержится другой вид объекта, тем не менее, вызывается правильный метод.
Перекрытие чисто виртуальных методов
Несмотря на то что классы должны обеспечить реализацию для всех чисто виртуальных функций, абстрактный тип данных тоже может предложить свою реализацию. Класс Data имеет право предлагать свою реализацию метода Show(), и если это имеет место, то производные классы могут вызывать ее, используя для этой цели оператор видимости.
Глава 3
Способность обеспечить реализацию чисто виртуальных методов позволяют абстрактному типу данных обеспечить базовые функциональные средства и при этом продолжать требовать от производных (порожденных) классов конкретной реализации. Обратите внимание на то, что класс остается абстрактным, если функция объявляется как чисто виртуальная, даже если вы предложили собственную реализацию. Следовательно, класс Data может принимать такой вид;
class Data {
public:
Data(int val):myVal(val)( )
virtual ~Data(){ }
int Compare(const Data &) ;
virtual void Show() = 0;
protected: int myVal;
void Data:: Show () <
cout « "\ nThis is your data. Any questions?\ n";
}
Даже если класс Data предоставляет реализацию метода Show(), он все еще остается чисто виртуальным, а сам класс — все еще абстрактным. Производные классы, которые стремятся иметь экземпляры объектов, все еще должны перекрывать метод Show() и представить свою собственную реализацию. Если производные классы намерены воспользоваться методом Show() класса Data, они должны непосредственно обратиться к нему через оператор видимости (:;)
void GraphicData::Show() (
Data::Show();// вызов с помощью оператора видимости
cout « " (" « myVal « ") : " ; ;
for ( int i = 0; i < myVal; i+ + ) cout « "*"; cout « endl;
}
Виртуальные деструкторы
Деструкторам не только свойственно быть виртуальными, это просто необходимо в том случае, если вы намереваетесь использовать класс полиморфно. Если бы в листинге 3.4 мы не сделали деструктор виртуальным, то при разрушении объектов в связанном списке, в объектах Data расходовалась память. Не забывайте, что узлы не знают, с какими объектами данных они имеют дело; они удаляют эти объекты как часть их собственного деструктора:
-InternalNode () { delete nextNode; delete myData; }
Обратите внимание на то, что myData объявлен как класс InternalNode следующим образом:
Data * myData;
Если же деструктор объекта данных не виртуальный, то информационная часть производного объекта IntegerData удаляется, а все остальное сохраняется. Поскольку IntegerData в этом упрощенном примере не добавляет никаких новых переменных-членов, это не имеет особого значения; однако если в объекте IntegerData имеются переменные-члены, то потребуется дополнительная память.
Полиморфизм, реализованный путем
перегрузки методов класса
В основе второй формы полиморфизма наследование не заложено: она реализуется путем перегрузки методов класса. Самым обычным методом перегрузки, естественно, является конструктор. Этот конструктор, подобно любому другому методу, отличному от деструктора, может быть перегружен в результате изменения либо числа параметров, либо их типов.
В целях изучения различных методов, которые вы, возможно, захотите перегрузить, рассмотрим простой класс. В листинге 3.5 представлен простой набор классов.
1111Объектно-ориентированное программирование Часть I
11
11
Листинг 3.5. Перегрузка членов
#include <iostream.h>
class MyPoint { public:
MyPoint (int x, int y):myX(x), myY(y) { )
-MyPoint () ( ) int GetX() const { return myX; } void SetX(int x) ( myX = x; } int GetY() const { return myY; } void SetY(int y) { myY = y; } private:
int myX;
int myY;
};
class MyRectangle ( public:
MyRectangle(MyPoint upperLeft, MyPoint lowerRight): myUpperLeft(upperLeft), myLowerRight(lowerRight) { } -MyRectangle () { }
int GetWidth() ( return myLowerRight.GetX() — myUpperLeft.GetX(); } int GetHeightO { return myLowerRight.GetY() — myUpperLeft. GetY (); }
private:
MyPoint myUpperLeft;
MyPoint myLowerRight; };
int main() {
MyPoint ul(0,0);
MyPoint lw(20,30);
MyRectangle myRect(ul,Iw); cout « "This rectangle measures "; cout « myRect. GetWidth() ; cout « " by " ; cout « myRect.GetHeightO « endl; return 0; )
Как видно, класс Rectangle состоит из пары точек, каждая из которых обозначает противоположный угол прямоугольника. Точка, в свою очередь, представлена координатами х и у, как если бы это была решетка. Вы создаете Rectangle, задавая пару точек. Перегрузка конструктора осуществляется так, чтобы построить Rectangle, не задавая пары точек, а просто указывая координаты х и у верхнего левого и нижнего правого углов прямоугольника (листинг 3.6).
Листинг 3.6. Перегруженный конструктор
#include <iostream.h>
class Point
public:
Point (int x, int y) :myX(x) , myY(y) ( }
-Point ()( } int GetX() const ( return myX; ) void SetX(int x) ( myX = x; } int GetY() const ( return myY; } void SetY(int y) { myY = y; } private:
int myX;
Глава 3
int myY; };
class Rectangle {
public:
Rectangle(Point upperLeft, Point lowerRight): myUpperLeft(upperLeft), myLowerRight(lowerRight) {}
Rectangle(int upperLeftX, int upperLeftY, int lowerRightX, int lowerRightY): myUpperLeft(upperLeftX,upperLeftY), myLowerRight(lowerRightX,lowerRightY) {}
-Rectangle () {) int GetWidth () { return my LowerRight. GetX() — myUpperLeft.GetX(); } int GetHeightO ( return myLowerRight. GetY() — myUpperLeft.GetY() ; }
private:
Point myUpperLeft;
Point myLowerRight;
1;
int main() (
Point ul(0,0);
Point lw(20,30);
Rectangle myRect(ul,Iw);
Rectangle otherRect(0,5,20,30) ;
cout « "myRect measures "; cout « myRect.GetWidth(); cout « " by " « myRect.GetHeightO « endl;
cout « "otherRect measures ";
cout « otherRect.GetWidth(); cout « " by " « otherRect.GetHeightO « endl;
return 0;
Следует отметить, что конструктор по умолчанию — это не конструктор, назначаемый по умолчанию, это скорее конструктор, для которого не нужны параметры. Оказывается, что компилятор предоставляет конструктор по умолчанию, если вы не объявили никакого конструктора. Поскольку мы фактически явно объявили конструкторы для этого класса, на текущий момент у нас нет конструктора по умолчанию и мы должны позаботиться, чтобы такой конструктор был.
Существует еще три метода, которые предоставляет вам компилятор, если мы сами его не объявили: деструктор, конструктор копий и оператор присваивания. Эти три метода вместе с конструктором по умолчанию, называются каноническими методами любого класса. В практике программирования считается хорошим тоном явно объявлять эти методы для каждого нетривиального класса. Необходимость в этих методах становится еще более очевидной, когда класс управляет памятью. Перепишем класс Rectangle с таким расчетом, чтобы он хранил свои определяющие точки в куче, как показано в листинге 3.7.
Листинг 3.7. Члены в куче
class Rectangle (
public:
Rectangle(Point upperLeft, Point lowerRight): myUpperLeft ( new Point(upperLeft)) , myLowerRight(new Point(lowerRight))
{)•
Rectangle(int upperLeftX, int upperLeftY, int lowerRightX, int lowerRightY): myUpperLeft(new Point(upperLeftX,upperLeftY)) ,
Часть I
myLowerRight(new Point(lowerRightX,lowerRightY) ) {}
-Rectangle(){ delete myUpperLeft; delete myLowerRight;}
int GetWidthQ { return myLowerRight->GetX() — myUpperLeft->GetX(); }
int GetHeightf) { return myLowerRight->GetY() — myUpperLeft->GetY(); } private:
Point * myUpperLeft;
Point * myLowerRight;
} ;
Больше ничего в программе менять не надо, механизм хранения элемента данных Point полностью инкапсулирован в классе Rectangle. Обратите внимание, что деструктор должен удалить элементы данных Point, а конструктор должен инициализировать их в куче. Кроме того, хотя автор обычно устанавливает указатели в значение NULL после их удаления, он не делает этого в деструкторе, поскольку маловероятно, что эти указатели будут когда-либо использованы снова (объект на грани исчезновения).
ПРИМЕЧАНИЕ
Автор обычно избегает встроенных методов, особенно встроенных методов, не укладывающихся в одну строку инструкций, однако для целей этой книги используются встроенные инструкции, представляющие собой компактные, легко воспринимаемые примеры.
Управление памятью
Если класс управляет памятью, то следует позаботиться о том, чтобы конструктор копий и оператор присваивания делали глубокие, не поверхностные копии. Конструктор копий и оператор присваивания, которые предоставляются компилятором, делают побитовые, т.е. поверхностные копии. Это означает, что они копируют указатели, но не объекты, на которые направлены эти указатели.
Если вы передаете объект по значению либо как функцию, либо как значение, возвращаемое какой- либо функцией, то делается копия этого объекта. Если таким объектом является объект, определенный пользователем (такой, как объект Rectangle из последнего примера), конструктор копии этого класса вызывается для создания временного объекта.
Всем конструкторам копий требуется один параметр: постоянная ссылка на объект того же класса. Конструктор копий, предоставляемый компилятором по умолчанию, копирует каждый элемент данных поразрядно. Таким образом, если вы передаете объект Rectangle в функцию по значению, создается копия, указатели которой указывают на ту область памяти, что и оригинал. В листинге 3.8 представлен конструктор копий, написанный программистом и функционирующий так же, как и конструктор, предоставляемый компилятором. Не пытайтесь запускать эту программу в работу — она может привести к аварийному завершению. Смысл этого упражнения состоит в том, чтобы показать, какими проблемами сопровождается использование конструктора, генерирующего поверхностные копии.
Листинг 3.8. Наглядный пример конструктора, с помощью которого создаются поверхностные копии
♦include <iostream.h>
class Point {
public:
Point (int x, int y):myX(x), myY(y) (}
-Point () (}
int GetX() const ( return myX; }
void SetX(int x) ( myX = x; )
int GetY() const ( return myY; }
void SetY(int y) ( myY = y; ) private:
int myX;
int myY;
};
class Rectangle {
public:
Rectangle(Point upperLeft, Point lowerRight):
Глава 3
myUpperLeft ( new Point(upperLeft)), myLowerRight(new Point(lowerRight)) {)
Rectangle(int upperLeftX, int upperLeftY, int lowerRightX, int lowerRightY): myUpperLeft(new Point(upperLeftX,upperLeftY)), myLowerRight(new Point(lowerRightX,lowerRightY)) (}
Rectangle( const Rectangle S rhs ) : myUpperLeft(rhs.myUpperLeft), myLowerRight(rhs.myLowerRight) ( cout « "In Rectangle's copy constructor...\n"; }
-Rectangle() { cout « "\nln destructor..." « endl; delete myUpperLeft; delete myLowerRight: )
int GetWidth () { return myLowerRight->GetX() - myUpperLeft->GetX() ; ) int GetHeightQ ( return myLowerRight->GetY() - myUpperLeft->GetY(); } // приватные:
Point * myUpperLeft; Point * myLowerRight;
void SomeFunction( Rectangle ) ; int mainQ { Point ul(0,0); Point lw(20,30); Rectangle myRect(ul,Iw);
cout « "myRect measures "; cout « myRect.GetWidthQ; cout « " by " « myRect.GetHeightQ « endl; cout « "myRect address: " « SmyRect « endl; cout « "myRect->myUpperLeft: " « myRect.myUpperLeft « endl; cout « "SmyRect.myUpperLeft: " « SmyRect.myUpperLeft « endl;
SomeFunction(myRect); cout « "Back from SomeFunction"; return 0; } void SomeFunction ( Rectangle r ) { cout « "r measures "; cout « r. GetWidth () ; cout « " by " « r.GetHeightQ « endl; cout « "r address: " « Sr « endl; cout « "r->myUpperLeft: " « r.myUpperLeft « endl; cout « "sr.myUpperLeft: " « Sr.myUpperLeft « endl; cout « "Returning from SomeFunction!";
Выходные данные программы, представленной в листинге 3.8:
myRect measures 20 by 30
myRect address: 0x0012FF6C
myRect->myUpperLeft: 0x00421180
Объектно-ориентированное программирование Часть I
SmyRect.myUpperLeft: 0x0012FF6C In Rectangle's copy constructor... r measures 20 by 30 r address: 0x0012FF48 r->myUpperLeft: 0x00421180 &r.myUpperLeft: 0x0012FF48 Returning from SomeFunction! In destructor.. . Back from SomeFunction In destructor. . .
Эта программа представляет собой конструктор копий, который имитирует работу конструктора простых поразрядных (поверхностных) копий. Обратите внимание на тот факт, что переменные-члены сделаны общедоступными, так что вы можете видеть их из функции main(). Вот как она работает: в main() объявляем прямоугольник под именем myRect. Мы инициализируем этот прямоугольник и сообщаем о его размерах. Затем сообщаем адрес самого прямоугольника, за которым следует адрес объекта, содержащегося в переменной myUpperLeft, а также адрес этого указателя.
Далее передаем прямоугольник по значению функции SomeFunction(). Обратите внимание на печать сообщения, указывающего, что в соответствии с ожиданиями мы передаем конструктор копий. Затем производится печать той же информации о прямоугольнике из SomeFunction(). Очень важно сознавать, что адрес myUpperLeft Point, являющейся членом г (прямоугольник в SomeFunction()) тот же, что и адрес myUpperLeft Point, являющейся членом myRect.
После того как мы возвратимся из SomeFunction(), деструктор будет вызываться в соответствии с нашими ожиданиями. После того как мы выйдем из main(), также вызывается деструктор для myRect. К сожалению, поскольку указатель myUpperLeft объекта myRect указывает на тот же объект, что и указатель объекта г, у нас появляется проблема — г был разрушен, a Point удалена.
Свободная памятьСвободная памятьНа рис. 3.5 иллюстрируется возникшая проблема.
Свободная память
Свободная память
РИСУНОК 3.5.
Проблема использования двумя объектами одной области памяти, возникшая в результате поверхностного копирования.
Когда удаляется копия, память помечается как свободная. Когда удаляется исходный объект, вы удаляете уже удаленный указатель. Если вам повезет, программа сразу же завершится аварийно. Это не очень хорошо.
Выход из этой ситуации следует искать в построении конструктора копий, который не только копирует указатель, но и объект, на который он указывает. На рис. 3.6 представлено то, что мы надеемся получить, а в листинге 3.9 представлена одна из реализаций нового конструктора копий.
Листинг 3.9. Результат глубокого копирования
class Rectangle (
public:
Rectangle(Point upperLeft, Point lowerRight): myUpperLeft ( new Point(upperLeft)), myLowerRight(new Point(lowerRight)) {}
Rectangle(int upperLeftX, int upperLeftY, int lowerRightX, int lowerRightY): myUpperLeft(new Point(upperLeftX,upperLeftY)), myLowerRight(new Point(lowerRightX,lowerRightY)) ()
Rectangle( const Rectangle 6 rhs ) : myUpperLeft(new Point(*myUpperLeft)) , myLowerRight(new Point(*myLowerRight)) (
cout « "\ nln Rectangle's copy constructor...\ n";
Глава 3
}
-Rectangle() (
cout « "\ nln destructor..." « endl;
delete myUpperLeft; delete myLowerRight; }
int GetWidth() ( return myLowerRight->GetX() — myUpperLeft->GetX(); } int GetHeight() { return myLowerRight->GetY() — myUpperLeft->GetY(); }
11 приватные:
Point * myUpperLeft;
Свободная памятьСвободная памятьPoint * myLowerRight;
Свободная память
Свободная память
РИСУНОК 3.6.
Глубокая копия.
Ниже приведены выходные данные программы, представленной в листинге 3.9.
In Point's copy constructor In Point's copy constructor In Point's copy constructor In Point's copy constructor myRect measures 20 by 30 myRect address: 00x0012FF6C myRect->myUpperLeft: 0x00421180 SmyRect.myUpperLeft: 0x0012FF6C In Point's copy constructor In Point's copy constructor In Rectangle's copy constructor. . . r measures 1962613862 by 881745716 r address: 0x0012FF34 r->myUpperLeft: 0x004211E0 &r.myUpperLeft: 0x0012FF34 Returning from SomeFunction! In destructor...
Back from SomeFunction! In destructor...
Мы внесли исправления в конструктор копий класса Rectangle в целях создания нового объекта Point. Эти изменения предусматривают выделение памяти для новой копии, и выходные данные отражают это обстоятельство. Адрес myRect->myUpperLeft теперь отличается от адреса r->myUpperLeft, и этот программный код не вызывает сбоев.
Проблемы, сопровождающие перегрузку других операторов
Хотя вы можете перегрузить любой метод, операторы, осуществляющие перегрузку, вызывают наибольшую путаницу — их не могут избежать даже опытные программисты. Чтобы понимать, как производится перегрузка с помощью операторов, нужно иметь в виду, что компилятор транслирует оператор (например, такой как +, =, ++) в метод (такой, как myClass::operator=()).
Имеется три разновидности операторов: унарные (++, —), бинарные (+, -) и тернарные (?). Местность операторов определяет, сколько вызывается термов или выражений. В унарном операторе используется только один терм (например, х+ + , у—). В случае с бинарным оператором используются два терма (например, а = Ь, х+у). Существует только один тернарный оператор: условный оператор ? (например, х ? true : false).
Объектно-ориентированное программирование Часть I
Вы можете фактически перегружать любой из встроенных операторов, но не можете создавать собственных операторов. В силу этого, хотя вы и можете включить в свой класс оператор увеличения, однако вы не можете создать оператор возведения в квадрат. И хотя вы можете перегрузить эти операторы, чтобы они делали все, что нужно, тем не менее, хороший тон в программировании требует, чтобы в этом был какой-либо смысл. Вы можете сделать так, чтобы оператор ++ уменьшал значения на единицу, но это ничего, кроме дополнительной головной боли, вам не принесет.
Оператор присваивания
Единственный, и самый главный оператор, обеспечивающий перегрузку, — это оператор присваивания. Та же проблема выбора глубокой или поверхностной копии характерна для оператора присваивания, как и для конструктора копий. Оператор присваивания — это бинарный оператор —. Слева и справа используются два терма. Предположим, что вы написали следующий оператор: myRect = otherRect;
Известно, что компилятор преобразует этот код в следующую конструкцию: myRect.operator=(otherRect);
Таким образом, компилятор преобразует присваивание в метод вызова объекта, указанного в операторе присваивания слева, и передает его в объект справа в качестве параметра.
Обратите внимание на то, что эта операция присваивания вызовет оператор присваивания, созданный компилятором по умолчанию, если вы не предусмотрели собственный оператор присваивания. Опять-таки, оператор присваивания, предлагаемый по умолчанию, строит простую поразрядную копию, в результате получаем сбой, когда копия разрушается. Вы можете решить эту проблему, написав свой оператор присваивания, обеспечивающий получение глубокой копии, однако будьте осторожны. Это сопряжено с определенной долей риска!
Это приемлемая первая попытка создать оператор присваивания. Если при этом допущена какая-либо ошибка, попробуйте обнаружить ее, прежде чем ознакомиться с анализом:
Rectangle & Rectangle::operator=(const Rectangle S rhs) ( delete myUpperLeft; delete myLowerRight; myUpperLeft = new Point(*rhs.myUpperLeft); myLowerRight" new Point(*rhs.myLowerRight); return *this; )
Обратите внимание на то, что этот оператор присваивания тщательно удаляет исходные переменные- члены, прежде чем создавать новые значения, тем самым устраняя очевидную возможность расхода памяти. Однако имеется еще одна проблема. Если программный код вашего оператора присваивания написан таким образом, что обьект присвоен сам себе, возникнут определенные осложнения.
Если вы передаете ссылки, может случиться, что этот программный код вы закончите примерно такой строкой: myFirstRect = myOtherRect;
Фактически при определенных условиях оба имени, myFirstRect и myOtherRect, будут указывать на один и тот же объект. Когда такое случается, переменные-члены myUpperLeft и myLowerRight разрушаются, если нужно сделать копию, то достоверных данных для присваивания не будет!
В листинге 3.10 представлена рабочая модель. И снова автор временно обеспечил общий доступ к переменным-членам, так что функция main() может убедиться, что адреса этих двух объектов различны.
Листинг 3.10. Проверка присваивания
#include <iostream.h>
class Point < public:
Point (int x, int y) :myX(x) , myY (y) {} Point (const Point S rhs) : myX(rhs.myX), myY(rhs.myY) {
Глава 3
cout « "In Point's copy constructor\n"; )
-Point () {}
int GetX() const ( return myX; ) void SetX(int x) { myX = x; } int GetY() const { return myY; } void SetY(int y) { myY = y; } private:
int myX; int myY;
class Rectangle ( public: Rectangle(Point upperLeft, Point lowerRight):
myUpperLeft ( new Point(upperLeft)) , myLowerRight(new Point(lowerRight)) (}
Rectangle(int upperLeftX, int upperLeftY, int lowerRightX, int lowerRightY): myUpperLeft(new Point(upperLeftX,upperLeftY)), myLowerRight(new Point(lowerRightX,lowerRightY)) О
Rectangle( const Rectangle S rhs ):
myUpperLeft(new Point(*myUpperLeft)), myLowerRight(new Point(‘myLowerRight)) {
cout « "\nln Rectangle's copy constructor...\n";
}
-Rectangle () {
cout « "\nln destructor..." « endl; delete myUpperLeft; delete myLowerRight; }
Rectangle & operator=(const Rectangle S rhs);
int GetWidthO { return myLowerRight->GetX() - myUpperLeft->GetX(); } int GetHeightO ( return myLowerRight->GetY() - myUpperLef t->GetY () ; } //приватные:
Point * myUpperLeft;
Point * myLowerRight; ) ;
Rectangle & Rectangle::operator=(const Rectangle S rhs) { if ( this == &rhs )// защита от случая a = a
return *this;
delete myUpperLeft;
delete myLowerRight;
myUpperLeft = new Point(*rhs.myUpperLeft);
myLowerRight= new Point(*rhs.myLowerRight); return *this;
) int main() { Point ul(0,0); Point lw(20,30); Rectangle myRect (ul, Iw) ; Rectangle otherRect(0,30,50,50); cout « "\nmyRect measures "; cout « myRect.GetWidthO ; cout « " by " « myRect.GetHeightO « endl;
Часть I
cout « "myRect address: " « SmyRect « endl;
cout « "myRect->myUpperLeft: " « myRect.myUpperLeft « endl;
cout « "SmyRect.myUpperLeft: " « SmyRect.myUpperLeft « endl;
cout « "\notherRect measures ";
cout « otherRect.GetWidth();
cout « " by " « otherRect. GetHeight () « endl ;
cout « "otherRect address: " « SotherRect « endl;
cout « notherRect->myUpperLeft: " « otherRect.myUpperLeft « endl;
cout « "SotherRect.myUpperLeft: " « SotherRect.myUpperLeft « endl;
cout « "\nAssigning myRect = otherRect...\n";
myRect = otherRect;
cout « "\notherRect measures ";
cout « otherRect.GetWidth();
cout « " by " « otherRect.GetHeight() « endl;
cout « "otherRect address: " « SotherRect « endl;
cout « "otherRect->myUpperLeft: " « otherRect.myUpperLeft « endl;
cout « "SotherRect.myUpperLeft: " « SotherRect.myUpperLeft « endl;
return 0;
}
Перегрузка операторов увеличения
Префиксные и суффиксные операторы увеличения в течение многих лет были причиной многочисленных путаниц, несмотря на то, что их реализация достаточно проста. Семантика префиксного оператора означает "сначала увеличить, затем выбрать", в то время как семантика суффиксного оператора означает "сначала выбрать, затем увеличивать".
Рассмотрим следующие операторы:
int а=0, Ь=5; а = Ь++;
После того как эти операторы будут выполнены, а будет 5, а b — 6, т.е. а будет присвоено значение b (5) и после этого значение b будет увеличено на 1. Чтобы обеспечить это, оператор увеличения должен быть готов возвратить исходное значение, которое имела переменная Ь, и в то же время установить значение b на 1 больше.
Это легко проследить на примере. Во-первых, мы должны решить, имеет ли смысл оператор увеличения для нашего класса. Что значит увеличить Rectangle на Г? Конечно, мы можем присвоить любое значение, какое пожелаем, — можем использовать оператор увеличения, чтобы возвратить положение координат верхнего левого угла:
Point upperLeft = myRect++; // возвратить текущие координаты верхнего левого угла с / / помощью оператора ++
Это вполне допустимо в C++. Если бы вы работали у автора, то это была бы последняя строка C++, какую вы написали бы в его фирме. Для этого языка справедлива старая истина: в C++ трудно выстрелить в ступню, но, если вы умудрились сделать это, вам оторвет всю ногу.
Естественно, оператор увеличения (++) следует использовать для увеличения значений на 1. Тем не менее, далеко не очевидно, что значит "увеличить прямоугольник на 1", и автор не намерен использовать оператор увеличения таким образом. Какой смысл имеет выражение "увеличить точку на 1"? Это граничное условие; вы можете решить, что увеличение точки на 1 означает, что увеличивают на 1 обе координаты х и у, но это означает растянуть Point. В листинге 3.11 показано, как вы можете построить такой оператор увеличения.
Листинг 3.11. Оператор увеличения
♦include <iostream.h>
Глава 3
public: Point (int х, int у) :myX(x) , myY(у) (} Point (const Point & rhs): myX (rhs.myX), myY(rhs.myY) < cout « "In Point's copy constructor\n";
} -Point () {}
int GetX() const { return myX; } void SetX(int x) ( myX = x; } int GetY() const ( return myY; } void SetY(int y) ( myY = y; } const Point & operator++(); Pointoperator++(int);
const Point 6 operator—() ; Pointoperator--(int);
private: int myX ; int myY ; );
const Point & Point::operator++()
++myX; ++myY; return *this;
Point Point::operator++(int) ( Point temp(*this); // содержит текущее значение ++myX; ++myY; return temp;
}
const Point & Point::operator--() {
—myX;
—myY ; return *this;
}
Point Point::operator--(int) { Point temp(*this); // содержит текущее значение --myX;
--myY; return temp ;
)
class Shape ( public: Shape () {} -Shape () {} virtual Shape * Clone() const ( return new Shape(*this); ) };
class Rectangle : public Shape { public: Rectangle(Point upperLeft, Point lowerRight):
Часть I myUpperLeft ( new Point(upperLeft)), myLowerRight(new Point(lowerRight)) {}
Rectangle(int upperLeftX, int upperLeftY, int lowerRightX, int lowerRightY): myUpperLeft(new Point(upperLeftX,upperLeftY)), myLowerRight(new Point(lowerRightX,lowerRightY)) {)
Rectangle( const Rectangle & rhs ) : myUpperLeft(new Point(‘myUpperLeft)), myLowerRight(new Point(*myLowerRight)) ( cout « "\nln Rectangle's copy constructor...\n"; }
Shape * Clone() const { return new Rectangle(*this) ; }
-Rectangle () {
cout « "\nln destructor..." « endl;
dele te myUpperLeft; delete myLowerRight; }
Rectangle & operator”(const Rectangle & rhs); void Expand() ( — (*myUpperLeft) ; ++(*myLowerRight) ; ) int GetWidth() ( return myLowerRight->GetX() - myUpperLef t->GetX () ; } int GetHeight() ( return myLowerRight->GetY() - myUpperLeft->GetY(); }
private:
Point * myUpperLeft;
Point * my LowerRight ;
};
Rectangle & Rectangle::operator”(const Rectangle & rhs) { if ( this == &rhs )// защита от случая a = a
return *this;
delete myUpperLeft;
delete myLowerRight;
myUpperLeft = new Point(*rhs.myUpperLeft);
myLowerRight” new Point(*rhs.myLowerRight); return *this;
} int main() {
Point ul(10,10) ;
Point lw(20,30);
Rectangle myRect(ul,Iw);
cout « "\nmyRect measures ";
cout « myRect.GetWidth();
cout « " by " « myRect.GetHeight() « endl;
myRect.Expand() ;
cout « "\nmyRect measures ";
cout « myRect.GetWidth();
cout « " by " « myRect. GetHeight () « endl;
return 0;
}
Обратите внимание на то, что суффиксные операторы фиксируют текущее состояние объекта во временных переменных, перед тем как обновить их внутренние переменные-члены. Благодаря этому они способны возвратить исходные значения, несмотря на производимые обновления.
Глава 3
Операторы, которые создают временные переменные, должны, естественно, возвращать значения, поскольку вы не можете возвратить ссылку на объект, который выходит из области видимости. Однако даже если мы возвращаем ссылку на *this, мы возвращаем постоянную ссылку. Это объясняется тем, что вы не можете писать подобные программные коды:
Point а=7; а++++;
Поскольку такой синтаксис запрещен во встроенных классах, мы здесь его не употребляем.
Виртуальный конструктор копий
Наряду с обычными конструкторами, деструкторами, конструкторами копий и операторами присваивания рассмотрим конструкторы виртуальных копий для вашего класса. И хотя язык C++ не поддерживает идею виртуального конструктора, всегда возникает необходимость возвратить копию объекта, которая представляет собой точный дубликат, даже если рассматривать объект с позиций полиморфизма.
Обычный ответ на подобного рода ситуацию состоит в построении дублирующего метода, который просто возвращает указатель на объект того же типа. Представьте себе, что объект Rectangle порожден от класса Shape. Класс Shape может включать следующее объявление:
virtual Shape * Shape::Clone() const { return new Shape(*this); }
В свою очередь, Rectangle может перекрыть этот метод следующим образом:
virtual Shape * Rectangle::Clone() const ( return new Rectangle(*this); }
Обратите внимание на то, автор возвращает Shape * в обоих классах. Новый стандарт ANSI фактически позволяет возвратить различные типы при каждом перекрытии. Rectangle получает возможность возвратить указатель Rectangle. К сожалению, только немногие компиляторы отвечают требованиям этого стандарта.
Множественное наследование
Одна из возможностей, доступная в C++, которой нет в языке Java, — это множественное наследование (хотя в языке Java имеется подобная, хотя и несколько ограниченная возможность со многими интерфейсами). Множественное наследование позволяет классу наследовать более чем один базовый класс, включая элементы данных и методы двух или большего числа классов.
С наследованием нужно обращаться осторожно. Достаточно часто проблему, которая решается с применением множественного наследования, лучше решать методом агрегирования или с использованием шаблонов. Для многих система разработки и отладка методов в условиях многочисленных унаследованных объектов сопряжена с серьезными трудностями, а вся программа становится значительно более сложной по мере переплетения классов.
Несмотря на вышесказанное, множественное наследование остается мощным инструментальным средством, и нет никаких оснований отказываться от него. Важно только отметить, что им следует пользовать-
РИСУНОК 3.7. Простое множественное наследование.РИСУНОК 3.7. Простое множественное наследование.ся только тогда, когда это необходимо. С точки зрения разработчика, важно понимать, что же моделирует множественное наследование: а именно класс, пользующийся характеристиками и поведением двух других, возможно, не связанных между собой классов.
РИСУНОК 3.7. Простое множественное наследование.
РИСУНОК 3.7. Простое множественное наследование.
При простом множественном наследовании два базовых класса никак не связаны между собой (рис. 3.7).
В рамках этой простой иллюстрации класс Griffin (Гриф) наследует характеристики и поведение от Lion (Лев) и от Eagle (Орел). Следовательно, Griffin может EatMeat() (Есть мясо), Roar() (Рычать), Squawk() (Клекотать) и Fly() (Летать). Это реализуется на языке C++ путем формирования списков базовых классов, отделенных друг от друга запятыми. В листинге 3.12 демонстрируется реализация классов, модели которых представлены на рис. .3.7.
Листинг 3.12. Пример реализации множественного наследования
#include <iostream.h>
class Lion {
public:
РИСУНОК 3.8. Два класса совместно используют общий базовый класс.РИСУНОК 3.8. Два класса совместно используют общий базовый класс.
РИСУНОК 3.8. Два класса совместно используют общий базовый класс.
РИСУНОК 3.8. Два класса совместно используют общий базовый класс.
Часть I
void EatMeatO;
void Roar();
protected:
private: );
class Eagle (
public:
void Squawk();
void Fly() ; };
class Griffin : public Lion, public Eagle { 1;
Этот программный код сделан намеренно кратким — из него исключены конструкторы, деструкторы и прочее для упрощения и наглядности. Класс Griffin является производным от классов Lion и Eagle, поэтому он конкретизирует оба эти класса и реализует нашу модель, согласно которой Griffin есть Lion и в то же время Eagle.
Проблемы множественного наследования
Проблемы возникают, когда Lion и Eagle разделяют общий базовый класс, например, Animal. Предположим, что в классе Animal имеется переменная- член age типа int и функция GetAge(), которая возвращает значение возраста Animal. На рис. 3.8 показано, как эта модель выглядит в UML.
В листинге 3.13 показано, как эта модель реализуется в C++. И снова из программного кода исключено все, кроме основных методов, чтобы проиллюстрировать основные вопросы, рассматриваемые в этой главе. В реальную программу, вам, естественно, придется включить конструкторы, деструкторы и другие средства.
Листинг 3.13. Два класса с общим базовым классом
#include <iostream.h>
class Animal <
public:
Animal () : age (1) { }
void Sleep(){}
int GetAgeО const ( return age; } private:
int age;
);
class Lion public Animal {
public:
void EatMeatO{} void Roar()(} protected:
private: );
class Eagle : public Animal
public:
void Squawk(){} void Fly () { }
};
class Griffin : public Lion, public Eagle (
Наследование, полиморфизм и повторное использование программных кодов Глава 3
);
int main () { Griffin g; cout « g.GetAge(); // Неоднозначность! He следует выполнять компоновку! return 0 ;
}
Этот общий базовый класс Animal содержит переменную-член, которую класс Griffin наследует дважды. Когда вы запрашиваете переменную-член age (возраст), компилятор не обязательно будет знать, возраст какого объекта вы имеете в виду, поэтому он сообщит об ошибке в этих строках:
'Griffin::GetAge' is ambiguous, could be the 'GetAge' in base 'Animal' of base 'Lion'of class 'Griffin' or the 'GetAge' in base 'Animal' of base 'Eagle' of class 'Griffin'.
Компилятор делает попытку сообщить вам, что он не знает, возраст какого класса Animal указать: возраст, который Griffin наследует от Lion, или возраст, который Griffin наследует от Eagle. Будучи разработчиком класса Griffin, вы должны учитывать эту взаимосвязь и быть готовым к решению проблем неоднозначности, которые при этом возникают. Язык C++ облегчает задачу, предоставляя пользователю такое средство, как виртуальное наследование, продемонстрированное в листинге 3.14.
Листинг 3.14. Виртуальное наследование
#include <iostream.h>
class Animal
public:
Animal():age (1)( }
void Sleep () ( }
int GetAge() const { return age; } private:
int age;
};
class Lion : virtual public Animal {
public:
void EatMeatO ( } void Roar(){ ) protected:
private:
class Eagle : virtual public Animal {
public:
void Squawk(){ } void Fly () { )
1;
class Griffin : public Lion, public Eagle {
};
int main() {
Griffin g;
cout « g. GetAge ();
return 0;
}
Отображенные в этом листинге изменения заключаются в том, что, когда базовые классы класса Griffin (т.е. Lion и Eagle) происходят от класса Animal, они помечаются словом virtual. Эта новая модель показана на рис. 3.9.
Объектно-ориентированное программирование Часть I
При наличии виртуального наследования Griffin наследует только одну копию переменных-членов Animal, а неоднозначность устраняется. Проблема, возникающая при таком решении, заключается в том, что как Lion, так и Eagle должны знать, что они могут вступить в отношения множественного наследования; виртуальное ключевое слово должно быть установлено при объявлении наследования, но не при объявлении Griffin.
Обратите внимание на то, что если Animal требует инициализации (например, переменная age передается в качестве параметра), то Lion и Eagle должны его инициализировать (как правило), но Griffin инициализирует также Animal. Это несколько необычно, однако это единственный путь устранить неоднозначность, когда Lion инициализируется к одному, a Eagle — к другому значению.
В листинге 3.15 иллюстрируется, как можно инициализировать базовый класс в каждом из виртуально порожденных классов.
Листинг 3.15. Инициализация базовых классов с виртуальным наследованием #include <iostream.h> class Animal { public: Animal(int theAge):age(theAge)(} void Sleep ()() int GetAgeO const { return age; } private: int age; V. class Lion : virtual public Animal ( public: Lion(int theAge, int howManyCubs):Animal(theAge), numCubs(howManyCubs)(} void EatMeat()(} void Roar()(} protected: private: int numCubs;
);
class Eagle : virtual public Animal ( public: Eagle (int theAge, int theWeight)-.Animal (theAge) , weight(theWeight) ( ) void Squawk(){) void Fly()() private: int weight;
); class Griffin : public Lion, public Eagle { public: Griffin(int theAge, int theWeight, int howManyCubs): Lion(theAge, theWeight),//инициализациябазового класса Lion
Eagle(theAge, howManyCubs),//инициализациябазового класса Eagle
Animal(theAge){}//инициализациявиртуального базовогокласса Animal!
РИСУНОК 3.9. Моделирование виртуального наследования.РИСУНОК 3.9. Моделирование виртуального наследования.); int main() { int hisAge = 5 ; int hisWeight = 7; int litterSize = 4; Griffin g(hisAge, hisWeight, litterSize);
РИСУНОК 3.9. Моделирование виртуального наследования.
РИСУНОК 3.9. Моделирование виртуального наследования.
Глава 3
cout « g.GetAge(); return 0;
Множественное наследование и включение
Как узнать, когда использовать множественное наследование, а когда его следует избегать? Должен чи автомобиль наследовать что-либо от руля или шины?
Вы должны реализовать класс Саг, как показано ниже:
class Steeringwheel { 1;
class Door { };
class Tire { 1;
class Car : public SteeringWheel, public Door, public Tire ( 1;
РИСУНОК 3.10. Неудачная попытка построения модели с наследованием.РИСУНОК 3.10. Неудачная попытка построения модели с наследованием.Хотя такой программный код поддается компилированию, тем не менее, модель, которую он реализует, плохо структурирована, что следует из рис. 3.10.
РИСУНОК 3.10. Неудачная попытка построения модели с наследованием.
РИСУНОК 3.10. Неудачная попытка построения модели с наследованием.
Полезно возвратиться к базовым понятиям: общедоступное наследование всегда должно моделировать конкретные предметы. Такое положение можно выразить следующей формулой: наследование должно моделировать отношение есть. Если вы хотите моделировать отношение имеет (например, автомобиль имеет руль), то для этой цели можно использовать агрегирование. В C++ агрегирование реализуется с помощью переменных-членов, т.е. вы придаете классу Саг переменную-член SteeringWheel:
class SteeringWheel
{
};
class Door { );
class Tire {
class Car
{
public:
private:
SteeringWheel s;
Door d [2];
Tire t[4];
Вопрос заключается в том, является ли автомобиль рулем или он имеет руль? Вы можете возрази п>. что автомобиль есть совокупность руля и шин, однако наследование не моделирует этого обстоятсльс ва. Тем не менее, автомобиль не является конкретизацией этих предметов — это агрегация этих предмеюв Автомобиль имеет руль, дверцы и шины. Вы изображаете эти отношения на диаграмме на языке UML используя символ агрегации, показанный на рис. 3.11.
Незаштрихованный ромб класса Саг указывает на то, что объект Саг имеет то, что показано на другом конце соединяющей линии. В рассматриваемом случае Саг имеет, по меньшей мере, один руль, ды р
Часть I
цы и шины. Можно уточнить эту диаграмму, добавив свойство множественности, т.е. указать, сколько объектов различного типа может иметь объект Саг (рис. 3.12).
РИСУНОК 3.11. Моделирование с агрегацией.РИСУНОК 3.11. Моделирование с агрегацией.РИСУНОК 3.12. Обозначение множественности в языке UML.РИСУНОК 3.12. Обозначение множественности в языке UML.
РИСУНОК 3.11. Моделирование с агрегацией.
РИСУНОК 3.11. Моделирование с агрегацией.
РИСУНОК 3.12. Обозначение множественности в языке UML.
РИСУНОК 3.12. Обозначение множественности в языке UML.
На рис. 3.12 показано, что Саг имеет один SteeringWheel (Руль управления), четыре шины (Vehicle (Транспортное средство) с другим числом шин не являются Саг (Автомобилем)) и от двух до пяти дверец.
Резюме
Полиморфизм — это мощное инструментальное средство, предоставленное в распоряжение программистов, работающих на языке C++. Оно может быть реализовано путем перегрузки методов и операторов, что предоставляет клиенту большую гибкость в его взаимодействии с серверным объектом. Еще более мощная реализация полиморфизма может быть достигнута путем применения виртуальных методов перекрытия в порожденных классах. Может быть построена полная иерархия классов, каждый из которых специализируется на реализации виртуальных методов.
Множественная реализация и включение представляют собой инструментальные средства, предназначенные для построения мощных полиморфных моделей, однако важно сосредоточить внимание не столько на реализации, сколько на проектировании и семантике своей модели. Если вы понимаете суть проекта своего класса, реализация вытекает из этого естественным путем.
Вопросы реализации
ЧАСТЬ
Управление памятью
В ЭТОЙ ГЛАВЕ
Управление памятью и указатели
Указатели и исключения
Глава 4
Опыт преподавания автором языка C++ показывает, что управление памятью является труднейшей темой. Даже опытные программисты, работающие с C++, путаются в понимании указателей и ссылок. Одной из основных причин такой путаницы является частое механическое преподавание темы указателей ("вот как их использовать"), а не концептуальное ("вот что это такое").
После того как вы поймете, что указатель представляет собой не что иное, как переменную, которая содержит адрес, все остальное уже не покажется для вас очень сложным. Основы такого понимания перечислены ниже:
Указатель является переменной и потому имеет собственный адрес.
Указатель содержит адрес объекта, в котором вы заинтересованы.
Вы получаете интересующий вас объект, разадресовывая указатель.
Оператор new возвращает адрес. Указатель представляет собой место для хранения адреса.
Все сказанное несколько усложняется, когда добавляются ссылки, поскольку они, по сути, являются автоматически разадресованными указателями. Трудность со ссылками состоит в том, что они всегда указывают на объект и никогда не указывают на самих себя. Ниже приведен краткий тест на то, понимаете ли вы ссылки:
♦include <iostream.h>
int main ()
{ int x = 7; int & ref = x; ref = 8; int у = 10; ref = у; cout « "x = " « x; cout « " у = " « у; cout « " ref = " « ref « endl ; return 0;
}
Вопрос заключается в том, что будет напечатано? Прекратите чтение, пока не укажете ответ. Ответили ли вы так:
X = 8, у = 10, ref = 10
Если да, то это самая широко распространенная ошибка (не огорчайтесь, вопрос трудный). Обратите внимание на оператор: ref = у;
Данный оператор не заставлял ref указывать на у, как было бы, если бы было записано:
X = у;
Таким образом, правильный ответ таков:
х = 10, у = 10, ref = 10
Наконец, указатели и ссылки представляют особый и сложный вызов для исключений. Когда возбуждается исключение, стек раскручивается и локальные переменные разрушаются, но указатели сохраняются. В этой главе рассказывается о том, как управлять указателями и ссылками, чтобы избежать отсутствие в программе расхода памяти — даже в случае непредсказуемых исключений.
Управление памятью и указатели
В C++ распределение памяти для переменных осуществляется в стеке и в куче. Когда переменные распределяются в стеке, то при распределении вызывается их конструктор и при выходе их из области видимости вызывается их деструктор. Наибольшее распространение для объекта получил способ выхода из области видимости через возврат функции, как показано в листинге 4.1.
Листинг 4,1. Распределение памяти в стеке
♦include <iostream.h>
class myClass {
Часть II
public: myClass(int val=O):myValue(val) (
cout « "In myClass constructor\n"; )
myClass(const myClass & rhs):myValue(rhs.myValue) <
cout « "In myClass copy constructor\n"; }
-myClass() { cout « "In myClass Destructor\n"; ) int GetValue () const { return myValue; }
private:
int myValue;
);
int someFunction(myClass c) ;
int main()
cout « "In main, ready to create object (3) ... \n"« endl;
myClass object(3);
cout « "In main, object's value is: " « object. GetValue () « endl;
cout « "In main, passing object (by value) to someFunction...\n" « endl; someFunction(object);
cout « "In main, returned from someFunction. Exiting... \n" « endl; return 0;
}
int someFunction(myClass c) (
cout « "In someFunction, c's value is: " « c.GetValue() « endl;
cout « "Exiting someFunction...\n" « endl; return c.GetValue();
)
Ниже приведен вывод программы в листинге 4.1:
In main, ready to create object(3)...
In myClass constructor In main, object's value is: 3 In main, passing object (by value) to someFunction...
In myClass copy constructor In someFunction, c's value is: 3 Exiting someFunction...
In myClass Destructor In main, returned from someFunction. Exiting...
In myClass Destructor
В этом простом примере в main() создается объект в стеке и затем передается по значению someFunction(). В результате передачи по значению в области видимости функции someFunction() создается копия. Когда копия покидает область видимости, вызывается деструктор. Когда объект в main() покидает область видимости, то вызывается деструктор исходного объекта.
Второй способ создания объекта заключается в распределении его в куче с использованием оператора new, как показано в листинге 4.2. Оператор new распределяет объект из кучи (непоименованный) и возвращает адрес этого объекта. Затем адрес можно сохранить в указателе.
Листинг 4.2. Распределение памяти из кучи
#include <iostream.h>
class myClass (
public: myClass(int val=0):myValue(val) ( cout « "In myClass constructor\n"; } myClass(const myClass & rhs):myValue(rhs.myValue)
Глава 4
cout « "In myClass copy constructor\n"; )
-myClass() ( cout « "In myClass Destructor\n"; }
int GetValue() const ( return myValue; ) private:
int myValue;
};
int someFunction(myClass c) ;
int someFunction(myClass *pc) ;
int main() {
cout « "In main, ready to create object(3) . . . "« endl;
myClass * pObject = new myClass(3);
cout « "In main, pObject's value is: " « pObject->GetValue () « endl;
cout « "In main, passing pObject (by value) to someFunction..." « endl; someFunction(*pObject);
cout « "In main, returned from someFunction(*object).";
cout « "Calling someFunction(object)..." « endl;
someFunction(pObject);
cout « "In main, returned from someFunction(object). "
cout « "Deleting pObject..." « endl;
delete pObject; return 0;
}
int someFunction(myClass c) (
cout « "In someFunction, c's value is: " « c.GetValue() « endl;
cout « "Exiting someFunction..." « endl;
return c.GetValue();
}
int someFunction(myClass * c) {
cout « "In someFunction(myClass *) , c's value is: ";
cout « c->GetValue () « endl;
cout « "Exiting someFunction..." « endl;
return c->GetValue () ;
)
Ниже приведен вывод программы в листинге 4.2:
In main, ready to create object(3)...
In myClass constructor
In main, pObject's value is: 3
In main, passing pObject (by value) to someFunction...
In myClass copy constructor
In someFunction, c's value is: 3
Exiting someFunction...
In myClass Destructor
In main, returned from someFunction(*object) . Calling someFunction(object)...
In someFunction (myClass *) , c's value is: 3
Exiting someFunction...
In main, returned from someFunction(object) . Deleting pObject...
In myClass Destructor
На этот раз объект распределяется из кучи обращением:
myClass * pObject = new myClass(3) ;
Как результат вызывается конструктор. Мы передаем объект по значению (разадресовывая указатель) функции SomeFunction() с помощью оператора:
someFunction(*pObject);
Вопросы реализации Часть II
Затем вызывается конструктор копии. Теперь объект создается из стека и разрушается, если функция возвращает управление (обратите внимание на вызов деструктора).
Затем мы передаем объект перегруженной функции SomeFunction() по ссылке, как показано: someFunction(pObject);
В этот раз конструктор копии не вызывается потому, что копия не создается. Объект, на который происходит ссылка в данной функции, — это тот же самый объект, на который имеется ссылка в main(). Когда метод возвращает управление, то деструктор не вызывается. В данный момент мы готовы покинуть main(), но обязаны явно вызвать оператор delete. Каждое обращение к оператору new должно сопровождаться вызовом оператора delete, в противном случае потребуется расход памяти.
Расход памяти
Термин расход (утечка) памяти распространен в индустрии программного обеспечения, но объяснение ему дается не часто. Вот идея: если память распределяется оператором new и не востребуется обратно оператором delete, то она безвозвратно теряется до самого конца программы. Дело обстоит так, будто память "утекает" из программы.
Если такую ошибку сделать в функции, которая распределяет много памяти (несколько больших объектов или множество маленьких), и вы повторно вызовете данную функцию, то вполне возможно разрушить программу (или замедлить ее работу), поскольку появится недостаток в доступной памяти. В C++, в отличие от Java, нет автоматического возврата памяти ("сборки мусора"). Вы должны востребовать память самостоятельно.
Распределение массивов
Если распределяется массив памяти, то следует использовать специальный оператор delete[], Если его не использовать, а использовать просто delete (без квадратных скобок), то высвободится только первый объект, а остаток памяти будет утерян. В листинге 4.3 демонстрируется применение оператора deleted.
Листинг 4.3. Использование delete[] #include <iostream.h> class myClass { public:
myClass(int val=O):myValue(val) { cout « "In myClass constructor\n"; } myClass(const myClass & rhs):myValue(rhs.myValue) ( cout « "In myClass copy constructor\n";
} -myClass() { cout « "In myClass Destructor\n"; } int GetValue() const { return myValue; } void SetValue(int theVal) { myValue = theVal; ) private: int myValue;
};
void someFunction();
int main() { cout « "In main, ready to call someFunction..." « endl; someFunction(); cout « "In main, returned from someFunction." « endl; return 0;"
) void someFunction() { const int arraysize = 5; myClass * pArray = new myClass[arraySize]; for (int i = 0; i < arraySize; i++) pArray[i].SetValue(i);
Глава 4
for (int j = 0; j < arraySize; j++)
cout « "pArray[" « j « "]: " « pArray [ j] .GetValue () « endl;
delete [] pArray;
Ниже показан вывод программы:
In main, ready to call someFunction...
In myClass constructor In myClass constructor In myClass constructor In myClass constructor In myClass constructor pArray[0] : 0 pArray[1 ] : 1 pArray[ 2 ] : 2 pArray[ 3 ] : 3 pArray[4] : 4 In myClass Destructor In myClass Destructor In myClass Destructor In myClass Destructor In myClass Destructor In main, returned from someFunction.
Это еще один простой пример, который показывает, как распределяется память для массива объектов из кучи. При обращении к оператору new распределяются пять объектов из кучи. Затем им присваиваются значения. В свою очередь, обращение, показанное ниже, приводит к разрушению каждого из объектов:
delete [] pArray
Оператор delete (без квадратных скобок) предполагает, что вы хотите удалить только один объект. Оператор delete (и квадратных скобках) указывает компилятору использовать счетчик объектов, записанный при распределении массива. Затем разрушается корректное число объектов, что предотвращает расход памяти.
Паразитные, болтающиеся и дикие указатели
Если вы удаляете указатель, который уже был удален, то рискуете разрушить всю программу. Для того чтобы гарантировать, что этого не произойдет, возьмите за правило устанавливать все удаленные указатели в значение NULL. Удалять пустой указатель безопасно и законно, что показано в листинге 4.4.
Листинг 4.4. Удаление указателя NULL #include Ciostream.h> class myClass ( public:
myClass(int val=0):myValue(val) ( cout « "In myClass constructor\n"; } myClass(const myClass & rhs) rmyValue(rhs.myValue) (
cout « "In myClass copy constructor\n"; }
-myClass() { cout « "In myClass Destructor\n"; } int GetValue() const ( return myValue; } void SetValue(int theVal) { myValue = theVal; } private: int myValue;
int main() {
myClass * pc = new myClass(5);
cout << "The value of the object is " « pc->GetValue() « endl;
Часть II delete рс; рс = 0 ; cout « "Here is other work, passing pointers around willy nilly.\n"; cout « "Now ready to delete again..." « endl; delete pc; cout « "No harm done" « endl; return 0;
}
Ниже приведен вывод из программы листинга 4.4:
In myClass constructor
The value of the object is 5
In myClass Destructor
Here is other work, passing pointers around willy nilly.
Now ready to delete again...
No harm done
Конечно, данный пример абсурден, поскольку вы бы никогда не удалили один и тот же указатель в одном методе. Проблема состоит в том, что указатели часто передаются в метод и из метода. При этом создаются их копии. В сложной программе легко потерять их след и случайно удалить уже удаленный указатель. Установка в NULL уже удаленных указателей защитит вас от ошибки. Такой прием также гарантирует, что если вы попытаетесь использовать указатель, установленный в NULL, то катастрофа произойдет немедленно, а не превратится в тонкую и трудно обнаруживаемую ошибку.
Указатели const
Ключевое слово const для указателей можно использовать перед типом, после типа или в обоих местах, в зависимости от того, что вы пытаетесь выполнить.
В следующем примере рОпе представляет собой указатель на постоянное целое. Указываемое значение изменить нельзя:
const int * pOne; // указатель на постоянное целое
В этом примере pTwo представляет собой постоянный указатель на целое. Целое можно изменить, но pTwo не может указывать на что-либо еще:
int * const pTwo; // постоянный указатель на целое
И наконец, в последнем примере рТЪгее представляет собой постоянный указатель на постоянное целое. Указываемое значение нельзя изменить, и рТЪгее не может указывать на что-либо иное:
const int * const pThree; // постоянный указатель на постоянное целое
Хитрость в том, чтобы смотреть на то, что находится справа от ключевого слова const, чтобы отыскать, что же объявляется константой. Если тип находится справа от ключевого слова, то значение является постоянным:
const int * pl;// указываемая переменная int является константой
Если справа от ключевого слова const находится переменная, то постоянным является сам указатель:
int * const р2;// р2 является константой и не может указывать на что-либо еще
Указатели const и функции-члены const
Если типом const объявляется функция-член, то компилятор отмечает как ошибку любую попытку этой функции изменить данные в объекте, которому функция принадлежит.
При объявлении объекта типа const вы, по сути, объявляете, что указатель this является указателем на объект const. Указатель const this может использоваться только с функцией-членом const.
Передача по ссылке
При каждой передаче объекта в функцию по значению создается копия этого объекта. Каждый раз, когда объект возвращается из функции по значению, создается другая копия.
Эти объекты создаются в стеке. Их копирование занимает время и память. Для маленьких объектов, подобных встроенным целым значениям, это незначительные затраты.
Глава 4
Однако для больших объектов, созданных пользователем, затраты возрастают. Размер созданного пользователем в стеке объекта — это сумма всех его переменных-членов. Они, в свою очередь, могут оказаться созданными пользователем объектами и передача такой огромной структуры путем копирования ее в стеке может оказаться очень дорогостоящим удовольствием в смысле производительности и расхода памяти.
Если временный объект разрушается, а это происходит тогда, когда функция возвращает управление, то вызывается деструктор объекта. Если объект возвращается по значению, то будет создаваться и разрушаться и копия данного объекта.
Передача указателя const
Хотя передача указателя более эффективна по сравнению с передачей объекта по значению, но она и более опасна, поскольку позволяет изменить объект и обесценивает защиту, которая обеспечивается при передаче по значению.
Передача по значению похожа на то, как если бы вы дали музею фотографию произведения искусства, а не реальную вещь. Если вандалы повредят фотографию, то с оригиналом ничего не произойдет. Передача по ссылке похожа на то, как если бы вы передали музею свой домашний адрес и предложили бы гостям прийти и посмотреть на реальную вещь.
Решение заключается в передаче указателя const. Такой прием предотвратит вызов какого-либо метода, отличного от const, и тем самым обеспечит защиту, предлагаемую передачей по значению. В то же время будет сохранена эффективность передачи по ссылке. Гости опять приглашаются в дом, но произведение искусства защищено пуленепробиваемым стеклом.
Передача постоянной ссылки предоставляет такую же эффективность, не заставляя вас использовать довольно неуклюжий синтаксис разадресации указателей. Постоянная ссылка на самом деле является ссылкой на постоянный объект (сами по себе ссылки всегда постоянны). Передавая ссылку на постоянный объект, его нельзя изменить, но при этом конструктор копии не вызывается.
Вы можете передать постоянную ссылку только тогда, когда знаете, что объект не является NULL. Помните, что, хотя возможно создать ссылку на объект NULL, однако это сделает программу некорректной. Она может разрушиться, может работать вполне хорошо, но она незаконна с точки зрения C++, и результаты ее непредсказуемы.
Возврат ссылки на объект, который не находится в области видимости
После того как программисты, работающие с C++, обучатся передавать параметры по ссылке, будет не исключена тенденция увеличения расхода памяти. Однако такую тенденцию можно преодолеть. Помните о том, что ссылка — это всегда псевдоним какого-то другого объекта. Если вы передаете ссылку в функцию или из нее, то обязательно спросите себя: "Какому объекту дать псевдоним и будет ли он существовать всякий раз, когда его используют?"
Листинг 4.5 иллюстрирует возвращение ссылки на временный объект. Обратите внимание, что некоторые компиляторы этого не допустят. В любом случае данная программа почти наверняка разрушится при выполнении. Она некорректна.
Листинг 4.5. Возвращение ссылки на временный объект
♦include Ciostream.h>
class myClass (
public:
myClass(int val=0):myValue(val) ( cout « "In myClass constructor\n"; } myClass(const myClass & rhs):myValue(rhs.myValue)
{
cout « "In myClass copy constructor\n";
}
-myClass() { cout « "In myClass destructor\n"; } int GetValue() const ( return myValue; }
void SetValue(int theVal) { myValue = theVal; ) private:
int myValue;
};
void SomeFunction() ; myClass SWorkFunction();
4 Зак. 53
Часть II
int main () {
SomeFunction () ;
return 0;
}
myClass S WorkFunction() (
myClass me(5); return me ;
}
void SomeFunction() {
myClass srC = WorkFunction() ;
int value = rC.GetValue();
cout « "rC's value: " « value « endl; )
Ниже показан вывод из программы листинга 4.5:
In myClass constructor In myClass destructor ■ rC's value: 1245036
Функция WorkFunction() создает объект в стеке и возвращает ссылку на него, которая присваивается локальной ссылке (гс) в функции SomeFunction(). К сожалению, теперь гс ссылается на объект, который покинул область видимости. Таким образом, ссылка недействительна, и вся программа некорректна.
Может появиться искушение решить проблему, заставив WorkFunction() создать объект из кучи. Когда функция возвратит управление, объект все еще будет существовать:
♦include <iostream.h> class myClass ( public:
myClass(int val=0):myValue(val) ( cout « "In myClass constructor\n"; } myClass(const myClass S rhs):myValue(rhs.myValue) { cout « "In myClass copy constructor\n";
}
-myClass() { cout « "In myClass Destructor\n"; } int GetValue() const { return myValue; } void SetValue(int theVal) ( myValue = theVal; } private: int myValue;
};
void SomeFunction () ; myClass SWorkFunction();
int main() (
SomeFunction () ; return 0;
} myClass S WorkFunction() <
myClass * pC = new myClass (5); return *pC;
} void SomeFunction() {
myClass SrC = WorkFunction () ; int value = rC.GetValue () ; cout « "rC's value: " « value « endl; )
Глава 4
Ниже следует вывод:
In myClass constructor rC's value: 5
Этот пересмотренный код решает проблему, представленную в листинге 4.5, но за счет расхода памяти. Как восстановить память, распределенную в функции WorkFunction()? Конечно, можно удалить ее в SomeFunction(), как показано в следующем примере:
♦include <iostream.h>
class myClass ( public: myClass(int val=0):myValue(val) { cout « "In myClass constructor\n"; B} myClass(const myClass & rhs):myValue(rhs.myValue) ( cout « "In myClass copy constructor\n";
} -myClass() ( cout « "In myClass Destructor\n"; } int GetValueO const ( return myValue; } void SetValue(int theVal) ( myValue = theVal; } private: int myValue;
);
void SomeFunction(); myClass tWorkFunction() ; int main() { SomeFunction () ; return 0;
}
myClass sWorkFunction() { myClass * pC = new myClass(5); return *pC;
).
void SomeFunction() ( myClass SrC = WorkFunction(); int value = rC. GetValue () ; cout « "rC's value: " « value « endl; myClass * pC = SrC; // получить указатель на память delete pC; // теперь rC является ссылкой на пустой объект!
)
Вывод будет таким:
In myClass constructor rC' s value: 5 In myClass Destructor
В данном примере создается указатель на память в куче (путем получения адреса ссылки) и затем объект удаляется. Проблема в том, что теперь гс будет ссылкой на объект NULL, а это незаконно. На многих компиляторах программа будет компилироваться, связываться и работать. Но не обманывайтесь: ваша программа некорректна и определенно непереносима.
Указатель, указатель, указатель
Когда программа распределяет память из свободного хранилища (из кучи), то возвращается указатель. Хранить указатель обязательно. Если его потерять, то память нельзя будет удалить и появится ее утечка.
Когда данный блок памяти передается между функциями, кто-то будет владеть указателем. Обычно значение в блоке передается с использованием ссылок, и функция, которая создала память, будет ее и удалять. Но это общее правило.
Часть II
Тем не менее, опасно, когда одна функция создает память, а другая ее освобождает. Неоднозначность в том, кто владеет указателем, может привести к одной из двух проблем: забывают удалить указатель, или он удаляется дважды. Любой вариант может привести к серьезным ошибкам в программе. Безопаснее создавать функции так, чтобы они удаляли ту память, которую создают.
СОВЕТ
Если вы создаете функцию, которая должна создать память и затем передать ее обратно в вызывающую функцию, то рассмотрите возможность изменения интерфейса. Заставьте вызывающую функцию распределять память и затем передавать ее в функцию по ссылке. В результате такого подхода все управление памятью перемещается из вашей программы обратно в функцию, которая подготовлена для ее удаления.
Указатели и исключения
Управление памятью объектов в куче становится особенно проблематичным при наличии исключений. Рассмотрим пример, показанный в листинге 4.6.
Листинг 4,6. Объекты в куче и исключения
#include <iostream.h>
class myException { public: char * errorMsgO { return "Oops."; ) ); class Point ( public: Point (int x, int y):myX(x), myY (y) (.
cout « "Point constructor called"« endl; ) Point (const Point & rhs): myX(rhs.myX), myY(rhs.myY){ cout « "Point copy constructor called" « endl;) ~Point(){ cout « "Point destructor called" « endl;) int GetX() const { return myX; ) void SetX(int x) { myX = x; ) int GetY() const ( return myY; )’
void SetY(int y) ( myY = y; ) private: int myX; int myY; ); class Rectangle public: Rectangle(Point upperLeft, Point lowerRight): myUpperLeft ( new Point(upperLeft)), myLowerRight(new Point(lowerRight)) (} Rectangle(Point * pUpperLeft, Point * pLowerRight): myUpperLeft ( new Point(*pUpperLeft)), myLowerRight(new Point(*pLowerRight)) ()
Rectangle(int upperLeftX, int upperLeftY, int lowerRightX, int lowerRightY): myUpperLeft(new Point(upperLeftX,upperLeftY)), myLowerRight(new Point(lowerRightX,lowerRightY))
Глава 4
Rectangle( const Rectangle 6 rhs ) : myUpperLeft(new Point(*myUpperLeft)) , myLowerRight(new Point(*myLowerRight)) (}
-Rectangle() ( cout « "In Rectangle's destructor" « endl; delete myUpperLeft; delete myLowerRight;
}
int GetWidth () ( return myLowerRight->GetX() - myUpperLeft->GetX() ; } int GetHeight () { return myLowerRight->GetY() - myUpperLeft->GetY(); )
void DangerousMethod() { throw myException() ; } private: Point * myUpperLeft; Point * myLowerRight;
};
int main() {
try ( cout « "Begin round 1..." « endl; Point * pUL = new Point (0,0) ; Point * pLR = new Point(20,30); Rectangle myRectangle(pUL, pLR) ; int w = myRectangle .GetWidth () ; int h = myRectangle.GetHeight(); cout « "the Rectangle is " « w « " by " « h « endl; delete pUL; delete pLR;
} catch ( myException 6 e ) (
cout « "caught exception: " « e.errorMsgO « "\n\n" « endl; )
try ( cout « "Begin round 2..." « endl; Point * pUL = new Point (0,0) ; Point * pLR = new Point(20,30); Rectangle myRectangle(pUL, pLR); int w = myRectangle.GetWidth(); int h = myRectangle.GetHeight(); cout « "the Rectangle is " « w « " by " « h « endl; myRectangle.DangerousMethodO; delete pUL; delete pLR;
) catch ( myException 6 e ) {
cout « "caught exception: " « e.errorMsgO « "\n\n" « endl; } return 0;
Ниже представлен вывод, порожденный этим программным кодом:
Вопросы реализации Часть II Begin round 1... Point constructor called Point constructor called Point copy constructor called Point copy constructor called the Rectangle is 20 by 30 Point destructor called Point destructor called In Rectangle's destructor Point destructor called Point destructor called Begin round 2... Point constructor called Point constructor called Point copy constructor called Point copy constructor called the Rectangle is 20 by 30 In Rectangle's destructor Point destructor called Point destructor called caught exception: Oops.
В данном простом примере создается пара объектов Point, которые затем используются (путем создания копии) для получения прямоугольника. После этого объекты Point можно удалить. При втором проходе вызывается метод DangerousMethod(), который возбуждает исключение.
Подсчитайте конструкторы и деструкторы. В первом проходе имеет место смещение деструктора по отношению к каждому вызову конструктора. Поскольку во втором проходе возбуждается исключение, то операторы delete никогда не выполняются и неизбежен расход памяти.
Обратите внимание на то, что в обоих проходах вызывается деструктор Rectangle. Локальные объекты (в стеке) разрушаются при выходе из исключения, но указатели не удаляются.
Эту ошибочную программу можно исправить, добавив оператор delete в блок catch. Обратите внимание, что необходимо вытянуть указатели за пределы области видимости блока try, так чтобы блок catch мог их видеть (листинг 4.7).
Листинг 4.7. Удаление указателей в блоках catch int main () <
Point * pUL = 0;
Point * pLR = 0 ; try { cout « "Begin round 1. . ." « endl ; pUL ж now Point(0,0); pLR ■ now Point(20,30);
Rectangle myRectanglo(pUL, pLR) ;
int w = myRectanglo. Ge tWidth() ; int h = myRectanglo.GetHoight(); cout « "the Rectangle is " « w « " by " « h « endl; delete pUL; delete pLR;
catch ( myException 6 о ) { cout « "caught exception: " « o.orrorMsgO « "\n\n" « endl; ) try ( cout « "Begin round 2..." « endl; pUL = now Point(0,0); pLR = now Point(20,30);
Глава 4
Rectangle myRectangle(pUL, pLR) ;
int w = myRectangle.GetWidth () ;
int h = myRectangle.GetHeight();
cout « "the Rectangle is " « w « " by " « h « endl;
myRectangle.DangerousMethod();
delete pUL;
delete pLR; ) catch ( myException 6 e ) {
cout « "caught exception: " « e.errorMsgO « "\n\n" « endl; delete pUL;
delete pLR; }
return 0;
int main() (
Point * pUL = 0;
Point * pLR = 0 ; try {
cout « "Begin round 1..." « endl;
pUL = new Point(0,0) ;
pLR = new Point(20,30) ;
Rectangle myRectangle(pUL, pLR) ;
int w = myRectangle.GetWidth();
int h = myRectangle.GetHeight();
cout « " the Rectangle is " « w « " by " « h « endl ; delete pUL;
delete pLR; } catch ( myException 6 e ) {
cout « "caught exception: " « e.errorMsgO « "\n\n" « endl; }
try (
cout « "Begin round 2..." « endl;
pUL = new Point(0,0);
pLR = new Point(20,30) ;
Rectangle myRectangle(pUL, pLR) ;
int w = myRectangle .GetWidth () ;
int h = myRectangle. GetHeight () ;
cout « "the Rectangle is " « w « " by " « h « endl; myRectangle.DangerousMethod();
delete pUL; delete pLR; ) catch ( myException 6 e ) (
cout « "caught exception: " « e.errorMsgO « "\n\n" « endl; delete pUL;
delete pLR; )
return 0;
Вопросы реализации Часть II
Ниже представлен вывод из пересмотренной программы:
Begin round 1... Point constructor called Point constructor called Point copy constructor called Point copy constructor called the Rectangle is 20 by 30 Point destructor called Point destructor called In Rectangle's destructor Point destructor called Point destructor called Begin round 2... Point constructor called Point constructor called Point copy constructor called Point copy constructor called the Rectangle is 20 by 30 In Rectangle's destructor Point destructor called Point destructor called caught exception: Oops.
Point destructor called Point destructor called
Хотя пересмотренный код и работает, но он плохо масштабируется. Копирование операторов delete во все блоки catch — не очень хорошее решение. Оно делает код трудным для поддержания и склонным к ошибкам. Для реального решения этой проблемы требуется объект, который находится в стеке, но который действует как указатель. Нужен интеллектуальный указатель, и в библиотеке Standard Template Library предлагается именно то, что надо: auto_ptr.
Использование автоуказателей
Библиотека Standard Template Library (STL) предлагает класс auto_ptr. Объект этого класса действует как указатель, но находится в стеке, чтобы быть разрушенным при возбуждении исключения. Данный класс работает, припрятывая реальный указатель и удаляя его в собственном деструкторе.
Листинг 4.8 показывает, как переписать пример из листинга 4.7, используя auto_ptr.
Листинг 4.8'. Использование auto ptr #include <iostream> #include <memory> using namespace std; class myException { public: char * errorMsgO { return "Oops."; } );
class Point { public: Point (int x, int y) :myX(x) , myY(y) < cout « "Point constructor called"« endl;
1 Point (const Point 6 rhs) : myX(rhs.myX), myY(rhs.myY)( cout « "Point copy constructor called" « endl;} -Point()( cout « "Point destructor called" « endl;} int GetX() const ( return myX; } void SetX(int x) ( myX = x; } int GetY() const { return myY; }
Глава 4
void SetY(int у) { myY = у; ) private: int myX; int myY;
);
class Rectangle { public: Rectangle( Point upperLoft, Point lowerRight ): myUpperLeft(new Point(upporLeft)), myLowerRight(now Point(lowerRight)) О
Rectangle( auto_ptr<Point> pUpperLeft, auto_ptr<Point> pLowerRight ): myUpperLeft (new Point(*pUpporLoft)), myLowerRight(new Point(*pLowerRight)) (}
Rectangle( int upperLeftX, int upperLoftY, int lowerRightX, int lowerRightY ): myUpperLeft(now Point(upperLeftX,upperLeftY)), myLowerRight(new Point(lowerRightX,lowerRightY)) ()
Rectangle( const Rectangle & rhs ) : myUpperLeft(new Point(*myUpporLoft)), myLowerRight(now Point(*myLoworRight)) О
-Rectangle()( cout « "In Rectangle's destructor" « ondl; )
int GetWidth() ( return myLowerRight->GetX() - myUpperLeft->GetX() ; ) int GetHoight() { return myLowerRight->GetY() - myUpperLeft->GetY(); )
void DangerousMethodO ( throw myExcoption(); } private: auto_ptr<Point> myUpperLeft; auto_ptr<Point> myLowerRight;
);
int main() (
try ( cout « "Begin round 1..." « endl; auto_ptr<Point> pUL(new Point(0,0)); auto_ptr<Point> pLR(now Point(20,30)); Rectangle myRectangle(pUL, pLR); int w = myRectangle. Getwidth () ; int h = myRectangle.GetHeight(); cout « "the Rectangle is " « w « " by " « h « endl;
catch ( myException 6 о ) (
cout « "caught exception: " « o.orrorMsgO « "\n\n" « endl; )
try (
cout « "Begin round 2. . ." « ondl ; auto_ptr<Point> pUL(new Point(0,0));
Часть II
auto_ptr<Point> pLR(new Point (20,30)) ; Rectangle myRectangle(pUL, pLR) ;
int w = myRectangle . GetWidth () ; int h = myRectangle.GetHeight();
cout « "the Rectangle is " « w « " by " « h « endl; myRectangle.DangerousMethod();
) catch ( myException 6 e ) (
cout « "caught exception: " « e.errorMsgO « "\n\n" « endl; }
return 0;
Ниже представлен вывод программы в листинге 4.8:
Begin round 1... Point constructor called Point constructor called Point copy constructor called Point copy constructor called Point destructor called Point destructor called the Rectangle is 20 by 30 In Rectangle's destructor Point destructor called Point destructor called Begin round 2... Point constructor called Point constructor called Point copy constructor called Point copy constructor called Point destructor called Point destructor called the Rectangle is 20 by 30 In Rectangle's destructor Point destructor called Point destructor calledcaught exception: Oops.
На этот раз деструкторы вызываются правильно, не вынуждая вас вызывать их из main(), независимо, в операторе catch или нет! Указатель auto_ptr управляет памятью вместо вас. Вы просто передаете указатель на объект, и auto_ptr делает все остальное.
Обратите внимание, что в программе имеется один нюанс. Вскоре мы обсудим его.
Как видно, auto_ptr упрощает управление памятью, и его можно использовать подобно обычным указателям. Рассмотрим следующий пример: int GetWidth () ( return myLowerRight->GetX() — myapperLeft->GetX () ; }
Нет зависимости от того, как вызывается метод GetX(): используется оператор указания на auto_ptr, как если бы это был обычный указатель.
Копирование auto_ptr
Что произойдет, если делать копию auto_ptr?
Существует два способа копирования указателя. Первый заключается в вызове конструктора копии:
Point * ptrOne = ptrTwo;
Второй состоит вызове оператора присваивания:
Point * ptrOne; ptrOne = ptrTwo;
В любом случае автор шаблонного класса auto_ptr имеет три выбора: ■ Сделать тонкую копию
Глава 4
Сделать глубокую копию
Передать владение
Если делается тонкая копия, возникает обычная проблема: если копия выходит за пределы области видимости, то она удаляет указываемый объект и другие указатели будут дикими. Если делается глубокая копия, возникают накладные расходы на конструирование и разрушение каждой копии — довольно дорогостоящая операция и неподходящая для шаблонного класса универсальной библиотеки. Хотя такое ограничение вынуждает авторов Standard Template Library выбирать третий путь, это не означает, что создание глубокой копии является плохим решением. Можно создать собственный класс auto_ptr, который делает глубокую копию.
В любом случае Standard Template Library при реализации auto_ptr передает владение при присваивании указателя. Предположим, что вы написали следующий оператор:
auto_ptr<Point> newAutoPtr = pUL;
В этом случае pUL больше не владеет объектом Point и деструктор объекта Point не будет вызываться, если pUL выходит из области видимости. Фактически следует обращаться с pUL как с указателем на NULL. По этой причине вы должны быть очень внимательны при передаче auto_ptr в функцию по значению.
Это обсуждение возвращает нас к программе в листинге 4.8 и ее ошибке. Добавим программный код для исследования объектов Point, созданных после создания Rectangle:
int main ()
{
cout « "Begin round 1. . . " « endl ; auto_ptr<Point> pUL(new Point(0,0)); auto_ptr<Point> pLR(new Point(20,30)); Rectangle myRectangle(pUL, pLR) ; int w = myRectangle.GetWidth(); int h = myRectangle.GetHeight(); cout « "the Rectangle is " « w « " by " « h « endl; cout « "the upper left point x is " « pUL->GetX() « endl; cout « "the lower right point x is " « pLR->GetX() « endl;
} catch ( myException 6 e ) ( cout « "caught exception: " « e.errorMsgO « "\n\n" « endl; }
return 0; )
Вывод из данной пересмотренной части программы представлен ниже:
Begin round 1... Point constructor called Point constructor called Point copy constructor called Point copy constructor called Point destructor called Point destructor called the Rectangle is 20 by 30 the upper left point x is -572662307 the lower right point x is -572662307 In Rectangle's destructor Point destructor called Point destructor called
Мы упростили main(), удалив второй блок try/catch, и добавили две строки для печати содержимого Point. Появился мусор: -572662307 (ваше значение может быть иным). Дело в том, что мы передали auto_ptr в конструктор Rectangle по значению. Владение было предано внутренней копии, сделанной конструктором. Когда мы продолжаем попытки использовать исходные auto_ptr, то в них находится мусор, который был в памяти.
Часть II
Исправление ошибки достаточно просто. Передайте auto_ptr по ссылке. Нужно изменить единственный метод-член Rectangle:
Rectangle( auto_ptr<Point>6 pUpperLeft, auto_ptr<Point>6 pLowerRight ): myUpperLeft (new Point(*pUpperLeft)), myLowerRight(new Point(*pLowerRight)) О
Такое маленькое изменение — добавление двух амперсандов — приводит к тому, что копия auto_ptr не создается и владение не передается. На этот раз объекты Point имеют правильные значения:
Begin round 1...
Point constructor called Point constructor called Point copy constructor called Point copy constructor called the Rectangle is 20 by 30 the upper left point x is 0 the lower right point x is 20 In Rectangle's destructor Point destructor called Point destructor called Point destructor called Point destructor called
Но это выглядит так, будто конструктор копии вызывается в следующем операторе:
myUpperLeft (new Point(*pUpperLeft)),
He явный ли это вызов конструктора копии auto_ptr? Нет. Вот что происходит: auto_ptr передается по ссылке. При инициализации он разадресуется, возвращая указываемый объект (Point), который затем используется для инициализации нового auto_ptr, вызывая не конструктор копии, а конструктор, который принимает указатель на объект Point.
Подсчет ссылок
В главе 3 мы рассматривали, почему важно писать собственный конструктор копии и оператор присваивания. Цель заключается в том, чтобы гарантировать создание глубоких, а не поразрядных или тонких копий. Создание глубоких копий защитит вас от появления диких указателей, но будет иметь свою цену.
После того как выполнена глубокая копия, появляются два объекта, занимающие память для хранения одной и той же информации. Если вы пишете класс String и передаете строки между функциями, то может получиться много копий одной и той же строки. Результат — расход памяти и вероятность появления ошибок. Так легко потерять след того, кто владеет памятью, и соответственно того, кто должен удалять каждую копию.
Объекты с подсчетом ссылок позволяют преодолеть данные ограничения. Класс, подсчитывающий ссылки, хранит только один объект в памяти, но зато следит, сколько указателей ссылаются на объект. Когда ссылок не остается, то объект удаляет себя. Такой подход элегантен и эффективен.
Написание объекта, подсчитывающего ссылки, не слишком трудно, но необходимость обобщения класса, для того чтобы он стал базовым для всех объектов, которым может потребоваться подсчет ссылок, заставляет задуматься над целесообразностью такого подхода. Вместо того чтобы заставлять класс String наследовать базовый класс ReferenceCounted, заставим его содержать объявление для приватного класса CountedString. Таким образом, интерфейс String не обнаруживает подсчета ссылок. В конце концов, клиентам все равно, как String управляет своей памятью.
Внутренний класс CountedString порожден от ReferenceCounted и, следовательно, наследует все детали управления подсчетом ссылок. Класс CountedString уточняет процесс, добавляя способность управлять (скрытой) строкой С-стиля, которая является реальной памятью и ссылки на которую подсчитываются.
Строку создает следующий оператор:
String si("hello world");
При создании строки ее конструктор инициализирует внутренний указатель CountedString, одновременно инициализируя в куче новый объект CountedString. Внутренний указатель CountedString представляет собой интеллектуальный указатель. Таким образом, строка не должна ничего "знать" об увеличении или уменьшении числа ссылок.
Глава 4
Следовательно, строка несет ответственность за строкоподобные методы, такие как определение индекса в строке. Класс countedString несет ответственность за управление памятью строки. Он наследует способность к подсчету ссылок от своего базового класса ReferenceCounted.
String хранит интеллектуальный указатель на countedString, делегируя ответственность за вызов AddRef() и ReleaseRef() интеллектуальному указателю:
class String {
public:
String (const char * cString = "") ; char operator[](int index) const; chars operator[](int index);
//
private:
struct countedString : public ReferenceCounted (
char * cString;
countedString( const char * initialCString);
countedString( const countedString 6 rhs);
-countedString();
void initialize(const char * initialCString);
//..
friend class RCSmartPointer<countedString>;
};
RCSmartPointer<countedString> rcString ;
);
Базовый класс для countedString — это ReferenceCounted, который несет ответственность за управление подсчетом ссылок и удалением памяти, когда на нее больше не ссылаются:
class ReferenceCounted
{
public:
void addRef(); void removeRef () ;
void markNotShareable();
bool isShareable();
bool isShared() ;
protected:
ReferenceCounted();
ReferenceCounted(const ReferenceCountedS rhs);
ReferenceCountedS operators(const ReferenceCountedS rhs);
virtual -ReferenceCounted();
private: int referencecount; bool shareable;
};
Класс хранит счетчик ссылок и двоичное значение, указывающее на то, можно ли использовать память совместно. Протокол говорит о том, что объект начинает жизнь как совместно используемый и может быть установлен в монопольный, владеющий классом в соответствии со своими собственными правилами.
String помечает свою память как неразделяемую всякий раз, когда возвращает непостоянную ссылку с помощью оператора index. Этот процесс известен как "копия на запись", в котором мы делаем копию строки сразу же, если появляется вероятность того, что память будет переписана. Клиент, очевидно, забудет тот факт, что мы используем память совместно, и захочет записать только одну из копий, а не все. Поскольку мы не можем сказать, когда память перезаписывается, то просто делаем копию всякий раз, когда запись возможна:
char S String::operator[](int index)
(
if ( rcString->isShared())
Часть II ( rcString->removeRef() ; rcString = new countedString(rcString->cString);
}
rcString->markNotShareable(); return rcString->cString[index];
}
Если строка помечена как монопольная, то она никогда уже не будет помечена как используемая совместно. Общеиспользуемого метода MarkSh*reable() не существует. Мы никогда не будем знать, что данную память безопасно использовать совместно, поэтому не будем использовать ее совместно.
Ключевыми методами являются AddRef() и RemoveRef(). Кто-то должен вызывать эти методы каждый раз при создании копии. Мы предпочитаем не беспокоить String такой обязанностью и потому создаем интеллектуальный указатель для объектов с подсчетом ссылок (RCSmartPointer), которому принадлежит эта ответственность:
template<class Т>
class RCSmartPointer <
public:
RCSmartPointer(Т* ptrToRC = 0) ;
RCSmartPointer(const RCSmartPointerS rhs);
-RCSmartPointer();
RCSmartPointerS operator»(const RCSmartPointerS rhs);
T* operator->() const;
Ts operator*() const;
private:
T * pRC; void initialize();
);
Интеллектуальный указатель инициализируется указателем на любой объект с подсчетом ссылок — в нашем случае это String. Дело интеллектуального указателя, наращивать счетчик ссылок, когда добавляется новая ссылка, и уменьшать счетчик ссылок, когда указатель удаляется. Поскольку интеллектуальный указатель должен наращивать указатель для обоих своих конструкторов, то он делает это с помощью метода initialize():
templatecclass Т>
void RCSmartPointer<T>::initialize() {
if ( pRC = 0 ) return;
if ( pRC->isShareable() = false ) {
pRC = now T(*pRC);
} pRC->addRef () ;
)
Здесь представлена суть объектно-ориентированной конструкции: делегирование ответственности и повторное использование. В листинге 4.9 представлена вся программа.
Листинг 4.9, Делегирование ответственности
♦include <iostream.h>
♦include <string.h>
templatecclass T> class RCSmartPointer
public:
RCSmartPointer(T* ptrToRC = 0);
RCSmartPointer(const RCSmartPointerS rhs);
-RCSmartPointer () ;
Глава 4
RCSmartPointerS operator=(const RCSmartPointerS rhs);
T* operator->() const; T6 operator* () const;
private:
T * pRC; void initialize() ;
};
template<class T> void RCSmartPointer<T>::initialize() ( if ( pRC = 0 ) return;
if ( pRC->isShareable() — false ) ( pRC = new T(*pRC);
} pRC->addRef () ; )
templatecclass T>
RCSmartPointer<T>::RCSmartPointer(T* ptrToRC): pRC (ptrToRC) { initialize () ;
}
templatecclass T> RCSmartPointer<T>::RCSmartPointer(const RCSmartPointerS rhs): pRC(rhs.pRC) (
initialize () ; )
templatecclass T> RCSmartPointerCT>::-RCSmartPointer() ( if ( PRC )
pRC->removeRef();
}
templatecclass T>
RCSmartPointerCT>S RCSmartPointer<T>::operator=(const RCSmartPointerS rhs)
if ( pRC == rhs.pRC ) return *this;
if ( PRC ) pRC->removeRef();
pRC = rhs.pRC; return *this;
) template<class T> T* RCSmartPointer<T>::operator-»() const
return pRC ; }
template<class T> TS RCSmartPointer<T>::operator*() const ( return *pRC;
}
class ReferenceCounted
Часть II
public: void addRef() ( ++referenceCount; } void removeRef() { if ( --referencecount — 0 ) delete this;
)
void markNotShareable() ( shareable ■ false; ) bool isShareable() ( return shareable; } bool isShared() { return referenceCount > 1; )
protected: ReferenceCountedf):referenceCount(0), shareable(true) {} ReferenceCounted(const ReferenceCountedt rhs) : referenceCount(0) , shareable(true) <)
ReferenceCountedS operator=(const ReferenceCountedS rhs) ( return *this; } virtual -ReferenceCounted(){}
private: int referenceCount; bool shareable;
};
class String { public:
String(const char * cString = ""):rcString(now countedstring(cString))(}
char operator!](int index) const ( return rcString->cString[index]; ) chars operator!](int index); operator char*() const ( return rcString->cString; ) void ShowCountedStringAddress() const { rcString->ShowAddress(); )
private: struct countedstring : public RoferoncoCountod ( char * cString;
countedstring( const char * initialCString) ( initialize(initialCString);
) countedstring( const countedstring & rhs) ( initialize(rhs.cString); ) -countedstring() (
delete [] cString; ) void initialize(const char * initialCString) (
cString = new char[strIon(initialCString) +1]; strcpy(cString,initialCString);
}
void ShowAddress () const ( cout « ScString; )
friend class RCSmartPointor<countedString>; );
RCSmartPointer<countedString> rcString;
char 6 String::operator[](int index) (
if ( rcString->isShared()) {
Глава 4
rcString->removeRef();
rcString = new countedString(rcString->cString); } rcString->markNotShareable();
return rcString->cString[index];
int main() (
String si("hello world");
String s2 = si; // конструктор копии
String s3("bye");
s3 = s2;// присваивание
cout « "si: " « si « " ";
si.ShowCountedStringAddress();
cout « "\ns2: " « s2 « " ";
s2.ShowCountedStringAddress();
cout « "\ns3: " « s3 « " ";
s3.ShowCountedStringAddress();
cout « endl ; return 0 ;
Автор добавил метод ShowAddress(), чтобы доказать работу подсчета ссылок. Ниже приведен вывод программы из листинга 4.9:
si: hello world 0х0042118С
s2: hello world 0x0042118C
s3: hello world 0x0042118C
Как видно, все три строки имеют в памяти один и тот же адрес.
Вот как все это работает. В первой строке main() создается объект String. Поместив код в свой отладчик, вы обнаружите, что эта безобидная строка переносит вас в сердце всех обсуждаемых классов. Самое интересное заключается в том, что вы увидите, как счетчики ссылок вызываются автоматически. Вот этапы работы:
String si("hello world");
В стеке создается объект String. Дальнейший шаг переносит нас в конструктор String:
String (const char * cString = "")-.rcString(new countedString (cString)) { )
При инициализации переменной-члена rcString мы входим в оператор new и затем в конструктор для countedString:
countedString( const char * initialCString) ( initialize(initialCString); }
Прежде чем войти в тело конструктора, мы входим в инициализацию базового класса (неявную), в которой переменная-член ReferenceCounted устанавливается в 0:
ReferenceCounted():referenceCount(0), shareable(true) ()
Почему в 0, а не в 1? Выясняется, что проще и безопаснее разрешить производному классу устанавливать переменную в 1. Здесь мы назначаем объект ReferenceCounted совместно используемым — по умолчанию. Затем входим в тело конструктора countedString, в результате чего переносимся в метод-член initialize (с передачей исходной cString как параметра):
void initialize(const char * initialCString)
В теле данной инициализации в куче создается новый массив символов, и его адрес запоминается в переменной-члене cString:
cString = new char[strlen(initialCString) +1] ;
strcpy(cString,initialCString);
Помните, мы все еще инициализируем член rcString класса String, который является RCSmartPointer, в котором внутренний указатель устанавливается на указание объекта ReferenceCounted, распределенный в куче, и вызывается метод initialize() RCSmartPointer:
Вопросы реализации Часть II
tenrplatecclass Т>
RCSmartPointer<T>::RCSmartPointer(Т* ptrToRC): pRC(ptrToRC) { initialize () ;
}
Метод initialize() проверяет, установлен ли указатель в значение NULL. Если да, то никаких действий не требуется. В противном случае метод проверяет, является ли объект ReferenceCounted совместно используемым. Если нет, то распределяется новая память. В противном случае увеличивается счетчик ссылок:
if ( pRC->isShareable() == false ) ( pRC = new T(*pRC);
} pRC->addRef () ;
Это приводит к вызову метода addRef() базового класса: void addRef() { ++referenceCount; }
В завершение описанной последовательности событий распределяется память в куче и эта память управляется объектом с подсчетом ссылок, после того как инициализируется счетчик ссылок. То, что выглядит как стек-базированный объект, на самом деле управляет памятью для подсчета ссылок в куче.
А ГДЕ КОНСТРУКТОР КОПИИ И ОПЕРАТОР ПРИСВАИВАНИЯ?
Итак, только что мы занимались копированием объектов String, которые имеют указатели. Нуждаемся ли мы в конст- • рукторе копии и операторе присваивания? Хорошо, у нас есть один — сгенерированный компилятором по умолчанию.
Хотя конструктор копии по умолчанию порождает поразрядную копию, это хорошо. Когда он копирует член rcString, то вызывается конструктор копии RCSmartPointer или оператор присваивания и это гарантирует, что будет сделана глубокая копия — та, что использует подсчет ссылок. Объект String полностью делегирует ответственность своему члену rcString.
Подсчитываемые прямоугольники
После того как появился базовый класс ReferenceCounted и при наличии работающего интеллектуального указателя, вы можете везде воплощать данную конструкцию. Мы можем легко трансформировать класс Rectangle в класс с подсчетом ссылок — и интерфейс вообще не изменится, как показано в листинге 4.10.
Листинг 4.10. Класс Rectangle с подсчетом ссылок Counted Rectangles #include <iostream> #include <memory> #include <string> using namespace std; class myException { public: char * errorMsgO ( return "Oops."; } ); templatecclass T> class RCSmartPointer ( public: RCSmartPointer(T* ptrToRC = 0) ;
RCSmartPointer(const RCSmartPointer& rhs) ; -RCSmartPointer();
RCSmartPointere operator=(const RCSmartPointerS rhs) ;
T* operator->() const; Tt operator*() const;
private:
Глава 4
Т * pRC; void initialized;
};
templateCclass T> void RCSmartPointer<T>: :initialize () { if ( pRC = 0 ) return;
if ( pRC->isShareable() ~ false ) ( pRC = new T(*pRC);
} pRC->addRef () ; }
template<class T>
RCSmartPointer<T>:: RCSmartPointer(T* ptrToRC): pRC (ptrToRC) ( initialize () ;
}
templatecclass T>
RCSmartPointer<T>::RCSmartPointer(const RCSmartPointerS rhs) : pRC(rhs.pRC) ( initialize () ;
}
templatecclass T>
RCSmartPointer<T>::-RCSmartPointer() ( if ( pRC )'
pRC->removeRef();
)
templatecclass T>
RCSmartPointerCT>6 RCSmartPointerCT>::operator=(const RCSmartPointerS rhs) { if ( pRC == rhs.pRC ) return *this;
if ( pRC ) pRC->removeRef () ;
pRC = rhs.pRC;
return *this; }
templatecclass T> T* RCSmartPointerCT>::operator->() const ( return pRC;
}
templatecclass T> T6 RCSmartPointer<T>::operator*() const ( return *pRC;
}
class ReferenceCounted ( public: void addRef() ( ++referenceCount; } void removeRefd ( if ( —referenceCount = 0 )
Часть II
delete this; )
void markNotShareable() { shareable = false; } bool isShareable() ( return shareable; }
bool isShared() { return referencecount > 1; }
protected:
ReferenceCounted():referencecount(0), shareable(true) {}
ReferenceCounted(const ReferenceCountedS rhs): referencecount(0), shareable(true) (}
ReferenceCountedS operator=(const ReferenceCountedS rhs) ( return *this; ) virtual -ReferenceCounted()(}
private:
int referencecount; bool shareable;
);
class Point ( public:
Point (int x, int y) :myX(x) , myY(y) ( cout « "Point constructor called"« endl;
Point (const Point 6 rhs) : myX(rhs.myX), myY(rhs.myY)( cout « "Point copy constructor called" « endl;) -Point()( cout « "Point destructor called" « endl;} int GetX() const ( return myX; } void SetX(int x) ( myX = x; } int GetY() const ( return myY; } void SetY(int y) ( myY = y; ) private:
int myX;
int myY; };
class Rectangle ( public: Rectangle( Point upperLeft, Point lowerRight ): rcRect(new countedRect(upperLeft, lowerRight)) ()
Rectangle( Point * pUpperLeft, Point * pLowerRight ): rcRect(new countedRect(*pUpperLeft, *pLowerRight)) (}
Rectangle( int upperLeftX, int upperLeftY, int lowerRightX, int lowerRightY ): rcRect(new countedRect(new Point(upperLeftX,upperLeftY), new Point(lowerRightX,lowerRightY)))
(}
-Rectangle()( cout « "In Rectangle's destructor" « endl; }
int GetWidthQ (
return rcRect->myLowerRight->GetX() - rcRect->myUpperLeft->GetX();
Глава 4
int Ge tHeight () {
return rcRect->myLowerRight->GetY() - rcRect->myUpperLeft->GetY(); )
void DangerousMethodQ { throw myException(); } private:
struct countedRect : public ReferenceCounted (
char * cString;
Point * myUpperLeft; Point * myLowerRight;
countedRect( const Point * ul, const Point * Ir) { initialize(ul,Ir); } countedRect( Point ul, Point Ir ) ( initialize(Sul, Sir); } countedRect( const countedRect S rhs) ( initialize(rhs.myUpperLeft, rhs.myLowerRight);
)
-countedRect () (
delete myUpperLeft; delete myLowerRight;
} void initialize(const Point * ul, const Point * Ir) ( myUpperLeft = new Point(*ul);
myLowerRight = new Point(*lr) ; }
friend class RCSmartPointer<countedRect>; 1;
RCSmartPointer<countedRect> rcRect;
);
int main() (
try ( cout « "Begin round 1..." « endl; Point pUL(O,O);
Point pLR(20,30);
Rectangle myRectangle(pUL, pLR) ; int w = myRectangle.GetWidth(); int h = myRectangle.GetHeight(); cout « "the Rectangle is " « w « " by " « h « endl; myRectangle.DangerousMethod(); cout « "You never get here.\n" « endl;
) catch ( myException s e ) {
cout « "caught exception: " « e.errorMsgO « "\n\n" « endl;
return 0;
Ниже приведен вывод программы из листинга 4.10:
Begin round 1...
Point constructor called Point constructor called
Point copy constructor called
Часть II
Point copy constructor called Point copy constructor called Point copy constructor called Point copy constructor called Point copy constructor called Point destructor called Point destructor called Point destructor called Point destructor called the Rectangle is 20 by 30 In Rectangle's destructor Point destructor called Point destructor called Point destructor called Point destructor called caught exception: Oops.
Это переносимое решение, которое можно использовать при любых обстоятельствах. Оно обеспечивает двойную выгоду использования меньшего объема памяти и предотвращения ее расхода. Обратите внимание на то, что даже тогда, когда DangerousMethod() возбуждает исключение, стек раскручивается, но память не теряется — интеллектуальные указатели уменьшаются и распределенная память разрушается.
Резюме
Хитрость в управлении памятью заключается в отчетливом понимании того, какую память вы распределяете и где находится указатель на эту память. Необходимо либо следить за указателями на память и обеспечивать возврат памяти по окончании использования, либо обеспечивать сборку мусора в форме интеллектуальных указателей.
Хотя интеллектуальные указатели имеют свои ограничения, они помогают защитить код от расхода памяти и особенно полезны из-за исключений. Чтобы быть действенной, программа должна хорошо управлять ресурсами и не допускать утечки памяти.
Использование каркасовИспользование каркасов55
Использование каркасов
Использование каркасов
5
5
приложении
В ЭТОЙ ГЛАВЕ
Microsoft Foundation Classes
В перспективе
Проблемы вытесняющей многопоточности
Пример для изучения
Служебные классы
Документы и представления
Часть II
Прекрасной, но не реализованной возможностью ООП является повторное использование программного кода. Идея ООП состояла в создании классов, которые могли бы служить в качестве фундамента построения новых программ. Компоненты можно было бы добавлять в новые архитектуры подобно тому, как электронные чипы размещаются на платах. Однако до сих пор это не реализуется — за исключением библиотек каркасов приложений.
Множество библиотек каркасов приложений сопровождают создание приложений в оконных средах, таких как Windows NT или X Window. До сих пор наиболее распространенной библиотекой остается Microsoft Foundation Classes (MFC). Эта глава посвящена MFC как примеру того, что могут дать библиотеки каркасов приложений.
ПРИМЕЧАНИЕ
Библиотека Microsoft Foundation Classes является большой, мощной и сложной конструкцией. Ни в одном исчерпывающем учебном пособии нельзя полностью представить ее функциональность, не говоря уже об отдельной главе. Цель этой главы заключается не в том, чтобы научить вас использовать MFC, а скорее в том, чтобы дать вам как можно, более полное представление о ней и исследовать некоторые универсальные вопросы, общие для всех подобных библиотек каркасов приложений. Для изучения детальной вводной информации об MFC обратитесь к книге Professional MFC Programming Майка Бласкзака (Mike Blaszczak), изданной Wrox Press.
Microsoft Foundation Classes
Библиотека Microsoft Foundation Classes является каркасом приложения, представляющим структуру и множество классов, из которых можно строить приложения для Windows. MFC обеспечивает стандартные интерфейсные реализации оконных классов, а также множество служебных классов, помогающих манипулировать определенными видами объектов, таких как String и Time.
Приступая к изучению
MFC выходит за рамки обеспечения множества классов уровня приложения и на самом деле предлагает каркас, внутри которого можно использовать данные классы. Мощь подобного подхода можно почувствовать при работе с мастерами, которые автоматизируют создание приложений Windows. На рис. 5.1 показано, как создается новый проект с помощью Application Wizard.
После запуска мастера приложений (Application Wizard) он задает вопросы, которые помогают конструировать приложение внутри архитектуры Document/View. На рис. 5.2 показан первый вопрос: строите ли вы приложение Single Document Interface (SDI) (однодокументный интерфейс) или Multi-Document Interface (MDI) (многодокументный интерфейс) либо просто нуждаетесь в диалоговом интерфейсе?
•He'.hie* I Wakipao» I OteDocunsia |•He'.hie* I Wakipao» I OteDocunsia |ДАП CL’M Apf-Weed 3 duster Resource I ype Wizard 8 Custom AppWizatdDatabase Project ^DevStudoAdchn Wizard Ф Extended Stored Proc Wizard BlSAR Extension Wizard SaMakafiteGMFC ActiveX ControWizard flMFCAppWoardjdfiДАП CL’M Apf-Weed 3 duster Resource I ype Wizard 8 Custom AppWizatdDatabase Project ^DevStudoAdchn Wizard Ф Extended Stored Proc Wizard BlSAR Extension Wizard SaMakafiteGMFC ActiveX ControWizard flMFCAppWoardjdfi)М Win32 Stake Lb ary)М Win32 Stake Lb aryjuNLEASHEDjuNLEASHEDSWew Database Wizard fiUtity Project 51 Win32 ApoicabonWin32 Console Appication Wri32 Dynamic-Link Lb«ySWew Database Wizard fiUtity Project 51 Win32 ApoicabonWin32 Console Appication Wri32 Dynamic-Link Lb«yРИСУНОК 5.1. Application Wizard.РИСУНОК 5.1. Application Wizard.РИСУНОК 5.2. Выбор типа интерфейса.РИСУНОК 5.2. Выбор типа интерфейса.
•He'.hie* I Wakipao» I OteDocunsia |
•He'.hie* I Wakipao» I OteDocunsia |
ДАП CL’M Apf-Weed 3 duster Resource I ype Wizard 8 Custom AppWizatd
Database Project ^DevStudoAdchn Wizard Ф Extended Stored Proc Wizard BlSAR Extension Wizard SaMakafite
GMFC ActiveX ControWizard flMFCAppWoardjdfi
ДАП CL’M Apf-Weed 3 duster Resource I ype Wizard 8 Custom AppWizatd
Database Project ^DevStudoAdchn Wizard Ф Extended Stored Proc Wizard BlSAR Extension Wizard SaMakafite
GMFC ActiveX ControWizard flMFCAppWoardjdfi
)М Win32 Stake Lb ary
)М Win32 Stake Lb ary
juNLEASHED
juNLEASHED
SWew Database Wizard fiUtity Project 51 Win32 Apoicabon
Win32 Console Appication Wri32 Dynamic-Link Lb«y
SWew Database Wizard fiUtity Project 51 Win32 Apoicabon
Win32 Console Appication Wri32 Dynamic-Link Lb«y
РИСУНОК 5.1. Application Wizard.
РИСУНОК 5.1. Application Wizard.
РИСУНОК 5.2. Выбор типа интерфейса.
РИСУНОК 5.2. Выбор типа интерфейса.
В последующих окнах мастера приложений можно выбирать среди различных опций: поддержку базы данных, поддержку СОМ, поддержку доступа к Internet и т.п. После запуска Application Wizard создается несколько классов, которые составляют начальный каркас приложения (рис. 5.3).
#ifdef _AFXDLLEnable3dControls();#ifdef _AFXDLLEnable3dControls();#elseEnable3dControlsStatic();#endif#elseEnable3dControlsStatic();#endif0*1.” CAboutDIg§ CAboutDIg!)- - 9^ DoDataExchange(CDataExhange*p g.*V*CChildFramer-- §AssertValid()r-- ^CChildFramer- "CChildFramer - - § DumpiCDumpContext & de) :$ PreCreateWiridowfCREATESTRUCTg CMainFramet— § AssertValid()I--- § CMainFrame r-- $ "CMainFrame r - - § DumpiCDumpContext & de) i - ^ OnCreate(LPCREATESTRUCT ipCrea H- § PreCreateWindow(CREATESTRUCT t - m_wndStatusB3r m_wndToolBarg. “T^* CUnleashedApp§ CUnleashedApp r-- $ Initlnstancef)^OnAppAbout()g *1^” CUnleashedDoc t-- ^AssertValidf) r - 9^ CUnleashedDoc)) I--- $"CUnleashedDoc!) I- - - § DumpiCDumpContext & do) I--- §OnNewDocument!)• § Senalize(CArchive & ar) g.*!^” CUnleashedViewr-- ^AssertValid!) r - -^ CUnleashedView!) r-- $"CUnleashedView!) г - - § DumpiCDumpContext & de)§ GetDocument!)г" 9^ OnBeginPrinting(CDC*pDC, CPrintlnfo §OnDraw(CDC‘pDC)t--2^OnEndPrinting(CDC*pDC, CPrintlnfoI"" V§ OnPreparePrinting(CPrintlnfo*plnfo) §PreCreateWindow(CREATESTRUCTРИСУНОК 5.3. Классы, созданные мастером.файла INI (включая MRU)0*1.” CAboutDIg§ CAboutDIg!)- - 9^ DoDataExchange(CDataExhange*p g.*V*CChildFramer-- §AssertValid()r-- ^CChildFramer- "CChildFramer - - § DumpiCDumpContext & de) :$ PreCreateWiridowfCREATESTRUCTg CMainFramet— § AssertValid()I--- § CMainFrame r-- $ "CMainFrame r - - § DumpiCDumpContext & de) i - ^ OnCreate(LPCREATESTRUCT ipCrea H- § PreCreateWindow(CREATESTRUCT t - m_wndStatusB3r m_wndToolBarg. “T^* CUnleashedApp§ CUnleashedApp r-- $ Initlnstancef)^OnAppAbout()g *1^” CUnleashedDoc t-- ^AssertValidf) r - 9^ CUnleashedDoc)) I--- $"CUnleashedDoc!) I- - - § DumpiCDumpContext & do) I--- §OnNewDocument!)• § Senalize(CArchive & ar) g.*!^” CUnleashedViewr-- ^AssertValid!) r - -^ CUnleashedView!) r-- $"CUnleashedView!) г - - § DumpiCDumpContext & de)§ GetDocument!)г" 9^ OnBeginPrinting(CDC*pDC, CPrintlnfo §OnDraw(CDC‘pDC)t--2^OnEndPrinting(CDC*pDC, CPrintlnfoI"" V§ OnPreparePrinting(CPrintlnfo*plnfo) §PreCreateWindow(CREATESTRUCTРИСУНОК 5.3. Классы, созданные мастером.файла INI (включая MRU)
#ifdef _AFXDLL
Enable3dControls();
#ifdef _AFXDLL
Enable3dControls();
#else
Enable3dControlsStatic();
#endif
#else
Enable3dControlsStatic();
#endif
0*1.” CAboutDIg
§ CAboutDIg!)
- - 9^ DoDataExchange(CDataExhange*p g.*V*CChildFrame
r-- §AssertValid()
r-- ^CChildFrame
r- "CChildFrame
r - - § DumpiCDumpContext & de) :$ PreCreateWiridowfCREATESTRUCT
g CMainFrame
t— § AssertValid()
I--- § CMainFrame r-- $ "CMainFrame r - - § DumpiCDumpContext & de) i - ^ OnCreate(LPCREATESTRUCT ipCrea H- § PreCreateWindow(CREATESTRUCT t - m_wndStatusB3r m_wndToolBar
g. “T^* CUnleashedApp
§ CUnleashedApp r-- $ Initlnstancef)
^OnAppAbout()
g *1^” CUnleashedDoc t-- ^AssertValidf) r - 9^ CUnleashedDoc)) I--- $"CUnleashedDoc!) I- - - § DumpiCDumpContext & do) I--- §OnNewDocument!)
• § Senalize(CArchive & ar) g.*!^” CUnleashedView
r-- ^AssertValid!) r - -^ CUnleashedView!) r-- $"CUnleashedView!) г - - § DumpiCDumpContext & de)
§ GetDocument!)
г" 9^ OnBeginPrinting(CDC*pDC, CPrintlnfo §OnDraw(CDC‘pDC)
t--2^OnEndPrinting(CDC*pDC, CPrintlnfo
I"" V§ OnPreparePrinting(CPrintlnfo*plnfo) §PreCreateWindow(CREATESTRUCT
РИСУНОК 5.3. Классы, созданные мастером.
файла INI (включая MRU)
0*1.” CAboutDIg
§ CAboutDIg!)
- - 9^ DoDataExchange(CDataExhange*p g.*V*CChildFrame
r-- §AssertValid()
r-- ^CChildFrame
r- "CChildFrame
r - - § DumpiCDumpContext & de) :$ PreCreateWiridowfCREATESTRUCT
g CMainFrame
t— § AssertValid()
I--- § CMainFrame r-- $ "CMainFrame r - - § DumpiCDumpContext & de) i - ^ OnCreate(LPCREATESTRUCT ipCrea H- § PreCreateWindow(CREATESTRUCT t - m_wndStatusB3r m_wndToolBar
g. “T^* CUnleashedApp
§ CUnleashedApp r-- $ Initlnstancef)
^OnAppAbout()
g *1^” CUnleashedDoc t-- ^AssertValidf) r - 9^ CUnleashedDoc)) I--- $"CUnleashedDoc!) I- - - § DumpiCDumpContext & do) I--- §OnNewDocument!)
• § Senalize(CArchive & ar) g.*!^” CUnleashedView
r-- ^AssertValid!) r - -^ CUnleashedView!) r-- $"CUnleashedView!) г - - § DumpiCDumpContext & de)
§ GetDocument!)
г" 9^ OnBeginPrinting(CDC*pDC, CPrintlnfo §OnDraw(CDC‘pDC)
t--2^OnEndPrinting(CDC*pDC, CPrintlnfo
I"" V§ OnPreparePrinting(CPrintlnfo*plnfo) §PreCreateWindow(CREATESTRUCT
РИСУНОК 5.3. Классы, созданные мастером.
файла INI (включая MRU)
Глава 5
Обратите внимание на то, что каждый класс содержит несколько ^(unleashed classes : стандартных методов. Часто эти методы предлагают просто актуальный программный код и затем указывают, куда добавить пользовательский код, как показано в этом простом примере вывода из Application Wizard:
BOOL CUnleashedDoc::OnNewDocument() ( if (ICDocument::OnNewDocument()) return FALSE;
II TODO: здесь добавить программный код повторной // инициализации // (SDI-документы будут повторно использовать данный // документ) return TRUE;
)
Иногда программный код, предлагаемый мастером, расширен. Он может сэкономить много времени при разработке и упростить взаимодействие с более сложными элементами программирования Windows, такими как настройка СОМ для межпроцессного взаимодействия. В листинге 5.1 представлен метод Initlnstance, сгенерированный мастером.
Листинг 5.1. Код, сгенерированный мастером.
BOOL CUnleashedApp::Initlnstance() (
AfxEnableControlContainer();
// Стандартная инициализация
// Если вы не используете эти возможности и хотите // уменьшить размер окончательного исполняемого модуля, то // удалите из следующего текста подпрограммы специальной / / инициализации, которые не нужны.
// Вызывайте данный модуль, // когда используете MFC в // совместно используемой // библиотеке DLL
// Вызывайте данный модуль, // когда связываетесь с MFC // статически
// Измените ключ реестра под которым запомнены наши // параметры.
// Вы должны изменить данную строку во что-то подходящее, И например, имя своей компании или организации.
SetRegistryKey(_Т("Local AppWizard-Generated Applications"));
LoadStdProfileSettings();// Загрузить опции стандартного
И Зарегистрируйте шаблоны документов приложения. Шаблоны документов / / служат как соединение между документами, окнами и представлениями.
CMultiDocTemplate* pDocTemplate;
pDocTemplate = new CMultiDocTemplate(
IDR_UNLEASTYPE,
RUNTIME—CLASS(CUnleashedDoc),
RUNTIME_CLASS(CChildFrame), // пользовательский интерфейс MDI RUNTIME-CLASS(CUnleashedView));
AddDocTemplate(pDocTemplate);
// создать главное MDI окно-рамку
CMainFrame* pMainFrame = new CMainFrame; if (!pMainFrame->LoadFrame(IDR_MAINFRAME))
Часть II
return FALSE;
m_pMainWnd = pMainFrame;
// Синтаксический разбор командной строки для стандартных команд оболочки, DDE, // открытия файла
CCommandLinelnfo cmdlnfo;
ParseCommandLine(cmdlnfo);
// Диспетчерские команды, указанные в командной строке
if (• ProcessShellCommand (cmdlnfo))
return FALSE;
H Главное окно инициализировано, поэтому покажите и обновите его.
pMainFrame->ShowWindow(m_nCmdShow);
pMainFrame->UpdateWindow();
return TRUE;
}
Программа в листинге 5.1 готова к выполнению даже без единой строки пользовательского кода. Она компилируется, компонуется и выполняется. Программный код, сгенерированный Application Wizard, позволяет создавать и закрывать окна, а также создавать стандартную панель кнопок и меню. Такой каркас становится в дальнейшем базой, к которой вы добавляете свои функции и пользовательский интерфейс.
Другие мастера
Кроме Application Wizard, MFC предлагает мастера, которые создают и манипулируют классами, методами и членами классов, а также мастера, которые предоставляют методы для ответа на действия пользователя, например, на щелчок на кнопке. MFC также содержит мастера, которые взаимодействуют с базами данных и используют СОМ.
В перспективе
Лучший подход к MFC заключается в разграничении архитектуры приложений (т.е. приложений, потоков, управляющих команд и т.п.) и архитектуры документ/представление (например, CWnd, CView и т.п.).
Архитектура приложения
Приложения MFC состоят из одного класса CWinApp и одного или нескольких CWinThread. Как можно догадаться, CWinApp представляет приложение (или процесс), а различные CWinThread реализуют многопоточность. Когда приложение запускается, то существует только один поток: первичный. Затем создаются и разрушаются новые потоки, но когда приложение завершается, то завершается его первичный поток.
Обработка сообщений — т.е. ответ на события — управляется классами CCmdTarget, самый важный из которых — CWnd. Классы Windows являются и должны быть производными CCmdTarget, поскольку окна представляют собой основные объекты; которые отвечают на действия пользователя, такие как щелчки кнопкой мыши и выбор команд и опций меню. Потоки также порождаются от класса CCmdTarget, поэтому сообщения можно посылать потокам и потоки могут отвечать на запросы асинхронно.
Архитектура MFC велика и сложна, и в этой главе не делается попытки подробно объяснить ее. Вместо этого внимание сосредоточено на нескольких механизмах, которые играют особую роль, поскольку их особенности выходят за рамки специфики MFC. Например, мы подробно рассмотрим, как MFC реализует многопоточность и безопасность потоков, поскольку эти вопросы важны при написании кода C++ в любой среде. Детали могут варьироваться, но задачи и требования остаются неизменными.
Многопоточность
MFC содержит множество классов для управления потоками. Хотя специфика того, как MFC реализует эти классы, может отличаться от того, как обрабатывают многопоточность, например библиотеки ХОреп, основные задачи и цели остаются универсальными.
Под термином поток подразумевается поток выполнения. Операционная система Windows (и большинство современных операционных систем) позволяют приложению иметь более чем один поток выполнения в одной программе. Кажется, что программа выполняет две или более задачи одновременно, хотя чаще всего компьютер переключается между потоками, обеспечивая иллюзию одновременности. Фактически именно так достигается иллюзия мультипроцессирования. Например, при выполнении программы тексто-
Глава 5
вого процессора, броузера Internet и программы электронных таблиц процессор просто переключается между ними.
Программа представляет процесс, а каждый процесс может иметь несколько потоков. Принципиальная разница между процессом и потоком заключается в том, что переключение между процессами требует больше времени, чем переключение между потоками. Вот почему на потоки иногда ссылаются как на легковесные процессы.
Контекст потока — это информация, в которой нуждается компьютер для переключения между потоками. Контекст потока состоит из стека (для временных переменных и для адресов возврата подпрограмм), стека ядра (для хранения адресов возвратов подпрограмм обслуживания прерываний) и множества регистров. В регистрах также хранятся указатель команд и указатель стека. Указатель команд сообщает процессору, где найти следующую команду, а указатель стека — где он может сохранить или получить следующую локальную переменную.
Процессор просто держит свой путь по коду. Операционная система (в данном случае Windows) сообщает процессору, когда переключать поток или процесс. Говорят, что операционная система управляет квантованием времени, позволяя каждому потоку выполняться в свой небольшой квант времени.
ПРИМЕЧАНИЕ
Многозадачность указывает на выполнение более одного вида работы одновременно. Можно сказать, что операционная система является многозадачной, если она управляет более чем одним процессом (выполняющейся программой) одновременно. Одиночный процесс является многопоточным, когда внутри одного и того же процесса есть несколько потоков управления.
Кооперативная многопоточность против вытесняющей
Программисты, знакомые с Windows 3.x или с Мас, возможно, сталкивались с формой многопоточности под названием кооперативная. Эта примитивная форма многопоточности позволяет выполнять более одного потока, но каждый поток должен произвольно периодически передавать управление процессору, для того чтобы предоставить возможность выполняться другим потокам.
Проблема с кооперативной многопоточностью заключается в том, что один поток может поглотить весь процессор. Тогда другие потоки будут выполняться очень медленно. Хуже того, поток может повиснуть, и тогда остановится все приложение.
Что касается вытесняющей многопоточности, то здесь учитывается способность процессора сигнализировать о том, когда пора переключаться между потоками. Этот более разумный механизм связан со взаимодействием между операционной системой (например, Windows NT) и процессором (например, Pentium), чтобы гарантировать предоставление каждому потоку предварительно установленного времени (обычно измеряемого в миллисекундах). Операционная система быстро переключается между потоками, распределяя'кванты времени на основе приоритетов потоков.
Достоинство вытесняющей многопоточности заключается в том, что ни один поток не может потребить непропорциональную долю времени системы, а если поток повиснет, то оставшиеся потоки могут продолжить работу.
Проблемы вытесняющей многопоточности
Создание программ, которые выполняются в многопроцессной среде, таких как Windows, очень просто и целенаправленно: программа пишется так, будто она обладает неоспоримым доступом к процессору, жесткому диску, базе данных и т.д. Обо всем остальном должна позаботиться операционная система — вы просто идете вперед так, будто ваша программа выполняется в гордом одиночестве.
Написание многопоточного приложения более сложно. На самом деле это гораздо более сложно, поскольку ваш код неожиданно может прерываться в любое время (что всегда и происходит). Еще важнее то, что более одного потока может войти в ваш программный код так, что один и тот же код может выполняться в двух разных потоках. Такое может произойти, если каждая функция в двух разных потоках вызывает ваш код.
Многопоточные приложения могут быть обманчивыми. Если ваш код непотокобезопасен, то один поток может тщательно устанавливать значения переменных для базы данных, а другой может перекрыть те же самые значения, делая переменные бессмысленными. Это плохо и может оказаться одной из самых трудноуловимых ошибок.
Для примера предположим, что есть функция, которая получает запись из базы данных, обновляет ее и записывает обратно. Ниже приведен предполагаемый программный код:
int Updater ()
{
getARecord() ;
updatelt () ;
writeltBack () ;
}
Предположим, что есть функция fund в потоке 1 и другая функция, func2, в потоке 2 и обе вызывают Updater().
Выполняется fund() и вызывает Updater(). Updater() начинает выполняться в контексте потока 1 и получает запись. Но прежде чем запись успевает обновиться, поток прерывается и запускается поток 2.
Теперь func2() вызывает функцию Updater(), которая выполняется снова, на этот раз в контексте потока 2. Она получает ту же самую запись (случайно), обновляет ее, прежде чем прерывается.
Затем возобновляется поток 1, и функция Updater() продолжает выполнение с того места, где она была прервана, с записью, которая была изначально считана из базы данных. Запись будет обновлена и записана обратно. При этом будут искажены обновления, произведенные в потоке 2.
Это один пример из многих, когда многопоточность может исказить данные, если применяемые методы не являются потокобезопасными.
Краткий совет по поводу многопоточности: если можно избежать написания многопоточного кода, то избегайте его. Не используйте многопоточность до тех пор, пока есть иной выбор. Многопоточность делает код сложным и гораздо более трудным для поддержки. Она существенно увеличивает вероятность ошибок и чаще всего снижает производительность. В конце концов, она занимает время на обработку контекста потока и переключение потоков. Добавьте достаточно потоков, и процессор будет тратить значительную часть времени, просто переключаясь между ними, и пропорционально этому меньше времени будет тратиться на выполнение полезной работы!
Предположим, что имеется программа, которая принимает много данных, высчитывает сумму и затем использует эту сумму во втором уравнении. "Ага! — скажете вы. — Две задачи! Я создам два потока". Проблема в том, что второй поток не может начинаться до тех пор, пока не закончит работу первый. Даже если вы можете вычислять их в одно и то же время и если нельзя использовать данные до тех пор, пока оба не завершатся, то почему бы не выполнять их последовательно, а не параллельно? Выполнение задач вместе не даст ничего, а цена переключения между задачами может быть слишком высока.
Итак, когда же использовать многопоточность? Классический сценарий — это когда одна задача занимает много времени, а вторая задача не зависит от первой. Предположим, что необходимо сохранить данные на жестком диске и также отобразить их на экране. Вам не нравится ждать, пока данные будут сохраняться на жестком диске и лишь потом появятся на экране, — запись на диск может продлиться долго. Почему бы не поместить задачу "запись на диск" в отдельный поток и не запустить более важную задачу взаимодействия с пользователем? Действительно, почему бы нет?
Пример для изучения
Автор написал приложение, которое "звонит" по телефону. Это напоминает те раздражающие телемар- кетинговые звонки, которые звучат, когда вы сидите за ужином. Разница заключается в том, что эта программа "звонит" только тем, кто желает принимать звонки. Программа полностью автоматизирована и "не звонит" во время ужина.
Компьютер может звонить 72 разным людям одновременно. После каждого звонка программа сохраняет результаты вызова и периодически обновляет базу данных полученными результатами. Одновременно она хранит кэш номеров для вызова, так чтобы никогда не простаивать. Данный проект связан с многопоточностью.
Конструкция проекта довольно сложна, но наша цель — прийти к следующему: автор запускает один поток для каждой линии вызова (CCaller) для 72 потоков, если все 72 линии активны. Автор также создал один поток для управления отчетом о функционировании (Creporter) и еще один поток для управления набором звонков, ожидающих освобождения линии (CLocalCallQueue). Наконец, есть главный поток, который управляет всеми другими потоками и который владеет пользовательским интерфейсом (панелью управления наблюдением за звонками). Таким образом, приложение выполняет 75 потоков!
Это большое число потоков, но при наличии мощного компьютера (Windows NT Workstation, выполняющейся на компьютере с процессором Pentium PC, и 128 Мб оперативной памяти) они работают дос-
Глава 5
таточно быстро, создавая иллюзию одновременности работы. Кроме того, приложение само не только многопоточное, но и многопроцессное, поскольку существуют дополнительные вызывающие устройства и дополнительные компоненты в других процессах (и на других устройствах), ответственные за планирование заданий и управление базой данных.
Теперь рассмотрим проблемы, которые возникают в подобной среде.
Создание потоков
Первая задача состоит в создании потока. Фактически существует два разных способа создания и управления потоками. Первый подходит для использования с элементами (окнами), которые должны выполняться как поток. Представьте объект, который содержит методы, но оперирует ими независимо от всех других потоков. Это первый тип потока, и он порождается от класса CWinThread.
Второй тип потока более подходит для использования в том случае, когда необходима функция, выполняющаяся в собственном потоке. На такой тип потока ссылаются как на рабочий.
Использование объектов CWinThread
Объект CWinThread создается точно так же, как любой другой объект: вы порождаете его от самого CWinThread или от какого-то класса, который порожден от CWinThread. Когда реализуется CWinThread- порождённый объект, то поток не создается. Для создания потока необходимо вызвать метод CreateThread() данного объекта.
Создание объекта CWinThread вполне целенаправленно. Объявляется класс, порожденный (прямо или косвенно) от CWinThread, и реализуется объект данного типа в куче. Затем вызывается CreateThread() данного объекта и с помощью метода обеспечивается возврат значения true (что означает успешное создание потока). Код может выглядеть следующим образом:
CMyClass * pThread;
pThread = new CMyClass;
if ( ! pThread->CreateThread() ) { MessageBox("Thread Creation Failed!\n") ; Delete pThread;
)
ПРИМЕЧАНИЕ
Существует определенная путаница в терминологии. Является ли pThread (или, точнее, объект, указываемый pThread) потоком или он выполняется в потоке? Технически это объект CWinThread, выполняющийся в собственном потоке. Правильно говорить, что это объект CWinThread, который выполняется в собственном потоке и который управляет данным потоком, но для краткости мы вместо такого длинного определения применяем термин "поток".
Управление сообщениями
Существенным достоинством использования сообщений является то, что новый объект (указываемый pThread) не только действует в собственном потоке, но и принимает сообщения, используя ::PostThreadMessage(). Фактически поток может принимать сообщения даже в том случае, если он не связан ни с каким экранным элементом или окном!
Давая указатель объекту CWinThread в куче, вы можете послать данному потоку сообщение из любого другого потока, как показано ниже:
pThread-PostThreadMessage(MY_MESSAGE_CONSTANT, wParam, IParam);
wParam и IParam являются устаревшим наследием, оставшимся от 16-разрядного программирования. В 16-разрядном Windows IParam является 32-разрядным, но wParam — только 16-разрядным (w — это обозначение 16 разрядов в 16-разрядной операционной системе). Сегодня Windows — это настоящая 32-раз- рядная система, и оба параметра являются 32-разрядными (как и int).
Первый параметр является либо системно-определенной, либо определенной пользователем константой. Отображение между идентификатором данного сообщения и вызываемым методом обрабатывается функцией OnThreadMessage(), которая, в свою очередь, определяется в маршруте сообщения для объекта CWinThread.
В многопоточном приложении, обсуждавшемся ранее, каждый вызывающий поток порожден от CWinThread:
Часть II
class CCaller : public CWinThread {
public:
bool CallCompleted(bool isConnected = true); bool RestoreRoute () ;
//
И
манипулирование int
void
П ...
управлением
Call
HangUp
0; 0;
и
время жизни virtual BOOL virtual BOOL
//..
Exitinstance Initlnstance
0 ;
0;
void void protected: DECLARE declare" declare^ IVoice* //...
MakeCalls ReceiveCalls
_MESSAGE_MAP ()
"dispatch_mapo
"lNTERFACE_MAP () m_pVoicel;
0
0;
private:
//...
};
Здесь пропущена большая часть объявления данного класса, с тем чтобы показать только наиболее относящиеся к делу методы. Макрос DECLARE_MESSAGE_MAP устанавливает маршрут для диспетчеризации сообщения. В файле реализации мы используем еще несколько макросов для привязки сообщений к методам:
BEGIK_MESSAGE_MAP(CCaller, CWinThread)
//( (AFX_MSG_MAP(CCallClientApp)
// Примечание — Classwizard будет здесь добавлять и удалять отображение макросов.
И Не изменяйте то, что видите в этих блоках сгенерированного программного хода!
//})AFX_MSG_MAP
// Стандартные команды настройки принтера
ON_THREAD_MESSAGE(WM_PLACE_CALLS,CCaller::MakeCalls)
ON_THREAD_MESSAGE(WM_RECEIVE_CALLS,CCaller::ReceiveCalls) //...
END_MESSAGE_MAP ()
Обратите внимание та то, что CCaller показан как класс, непосредственно порожденный от CWinThread в первой строке, и что пересылаются два сообщения. WM_PLACE_CALLS отображается на метод MakeCalls(), a WM_RECEIVE_CALLS отображается на ReceiveCalls().
Объявление этих констант находится в глобальном файле заголовка:
const int WM_PLACE_CALLS = WM_USER + 1;
const int WM_RECEIVE_CALLS = WM_USER + 2;
Детали макросов отображения сообщений выходят за рамки настоящего обсуждения, но вы видите, что создать объект CWinThread, настроить его в собственном потоке и затем использовать его методы прямо (из его собственного потока) или косвенно (посылая ему сообщения) достаточно просто.
Теперь другие объекты, имеющие указатель на ваш объект, могут посылать ему сообщения и получать эти сообщения обработанными в фоновом режиме.
Управление окнами
Объект CWinThread, кроме всего прочего, предназначен и для управления окнами (и другими производными элементами), которые требуют собственных потоков. Связывание окна с объектом CWinThread достигается просто: заставьте initlnstance() создать окно (или элемент) и сохраните указатель на него. Затем объект CWinThread становится оберткой для окна.
Глава 5
Для того чтобы поддержать процесс, MFC предоставляет возможность присваивать окно переменной- члену mjpMainWnd объекта CWinThread. Это приводит к тому, что код диспетчеризации сообщения в CWinThread управляет исключительно данным окном.
Обратите внимание, что теперь имеется три объекта, за которыми нужно следить:
Объект CWinThread
Сам поток
Окно, которым управляет CWinThread
Помните, что при создании объекта CWinThread поток не создается. Поток создается путем обращения к CreateThread(). Подобно этому, когда поток разрушается, то объект CWinThread продолжает жить. Такое положение вещей можно изменить, установив переменную-член m_bAutoDelete в значение true (по умолчанию — false). Такая установка указывает Windows разрушить объект CWinThread при разрушении его потока.
Альтернатива: рабочие потоки
Если вы просто хотите, чтобы метод одного из классов работал в фоновом режиме, т.е. в собственном потоке, то можете создать рабочий поток. Для этого используйте функцию AfxBeginThread(), которая на самом деле перегружена так, чтобы ее можно было использовать для этой цели. Кроме того, ее можно использовать для управления потоками с классами, порожденными от CWinThread. В этом случае нас интересует только первая перегрузка AfxBeginThread():
CWinThread * AfxBeginThread()
AFX_THREADPROC LPVOID intUINT DWORDAFX_THREADPROC LPVOID intUINT DWORDpSomeThreadFunction,
AFX_THREADPROC LPVOID int
UINT DWORD
AFX_THREADPROC LPVOID int
UINT DWORD
pParamter,
LPSECURITY_ATTRIBUTES IpSecurityAttributes = NULLLPSECURITY_ATTRIBUTES IpSecurityAttributes = NULLnPriority = THREAD_PRIORITY_NORMAL, nStackSize = 0, dwCreateFlags = 0,
LPSECURITY_ATTRIBUTES IpSecurityAttributes = NULL
LPSECURITY_ATTRIBUTES IpSecurityAttributes = NULL
Поскольку наибольшее значение имеют первые два параметра, то они будут рассматриваться в последнюю очередь. nPriority является целым значением, которое определяет начальный приоритет потока и которое можно установить в THREAD_PRIORITY_HIGHEST или THREAD_PRIORITY_BELOW_NORMAL для управления относительным приоритетом каждого из потоков. Как показано, приоритетом, установленным по умолчанию, является THREAD PRIORITY NORMAL.
Четвертый параметр nStackSize является UINT (беззнаковое целое). Этот параметр устанавливает начальный размер стека потока (каждый поток имеет собственный стек). Значение, принятое по умолчанию, 0, указывает Windows распределить стек для нового потока такого же размера, как и для порождающего потока.
Пятый параметр dwCreateFlags — это DWORD (двойное слово). Он указывает 32-разрядное беззнаковое целое. Флаги создания можно установить в 0 (по умолчанию) или в CREATE_SUSPENDED. Данные флаги позволяют создавать поток, но не выполнять его до вызова ::ResumeThread().
Шестой параметр является структурой атрибутов безопасности. Его можно без проблем игнорировать и оставить значение, принятое по умолчанию, — NULL. Если нужна безопасность Windows NT, то можно создать и инициализировать свою структуру безопасности и затем вставить ее, используя данный параметр.
Вернемся к двум первым параметрам. Первый параметр представляет собой функцию, которую вы хотите выполнить в собственном потоке (это функция управления потоком). Второй параметр является структурой, содержащей все параметры, которые нужно передать управляющей функции.
Вот что можно узнать из файла справки MFC о функции управления потоком:
Управляющая функция определяет поток. Когда происходит вход в данную функцию, то поток запускается, а когда функция теряет управление, поток прекращается. Эта функция должна иметь следующий прототип:
UINT MyControllingFunction( LPVOID pParam );
Уточним терминологию. Новая управляющая функция будет вызываться какой-то другой функцией, на которую мы будем ссылаться как на вызывающую. Вызывающая функция взаимодействует с управляющей функцией через единственный параметр LPVOID. Что бы ни требовалось передать управляющей функции, это должно упаковываться в один параметр. Это не так тяжело, как звучит: просто создайте структуру, загрузите ее значениями, которые хотите передать, приведите ее к LPVOID и передайте всю структуру в управляющую функцию, используя AfxBeginThread().
Часть II
В некоторых книгах советуют сделать управляющую функцию глобальной. Автор не согласен с этим. Вы достигнете лучшей инкапсуляции (и тем самым более просто поддерживаемого кода), если сделаете данную функцию статической функцией-членом конкретного класса.
Обратите внимание, что в статической функции-члене нет указателя this, поэтому необходимо передавать указатель this как параметр. Например, можно объявить такой метод-член:
class myClass {
public:
static UINT myThreadFunc(LPVOID pMe);
H ...
};
И написать реализацию, подобную следующей:
UINT myClass: : myThreadFunc (LPVOID ptheThisPointer) (
myClass * pmyThis = (CClientManager *) pMyThisPointer;
ASSERT (pmyThis);
pmyThis->SomeClassMethod(); return 0;
)
Затем используется AfxBeginThread(), а указатель класса this передается в качестве второго параметра:
AfxBeginThread(myClass:: myThreadFunc,(LPVOID) this );
Альтернативно функцию можно сделать дружественной классу.
Синхронизация
Теперь, когда поток "проснулся" и выполняется, возникает проблема синхронизации потоков. MFC предоставляет несколько объектов, помогающих решить эту задачу: критические разделы, семафоры, мю- тексы и события.
Поток может проверять каждый из этих объектов (кроме критических разделов), для того чтобы установить, сигналит ли объект или нет (часто говорят о том, чист объект или нет). Когда объект синхронизации сигналит, это указывает на то, что поток может иметь доступ. До тех пор пока объект сигналит, поток ждет. Когда поток ждет, он ничего не делает и блокирован.
Критические разделы
Критические разделы представляют собой самые простые, наиболее легкие в использовании и наиболее ограниченные синхронизирующие объекты. Поток подчиняется простому правилу: когда он хочет коснуться области синхронизируемого кода, то прежде просит блокировать объект CriticalSection. Затем выполнение потока блокируется до тех пор, пока объект CriticalSection не сможет быть блокирован. Если другой поток уже блокировал объект, то поток ждет, пока другой поток разблокирует CriticalSection. Таким образом CriticalSection синхронизирует доступ к указанной области программного кода. Ниже показано, как можно использовать критический раздел для изоляции кода, который изменяет значение:
m_CS.Lock();
s.Format("Current value: %d" , m_Value); m_Value++;
m_CS. Unlock» ;
Объект имеет две переменные: m_Value (целую) и m_CS (CriticalSection). Критический раздел блокируется, значение возрастает, и критический раздел разблокируется.
Мютексы
Для многих целей критические разделы представляют собой именно то, что надо. Они быстры, легковесны и просты в применении. Но, к сожалению, они ограничены. Самое существенное ограничение состоит в том, что они видимы только одному процессу, и в том, что не могут ждать указанное время до разблокирования.
Более интеллектуальным синхронизирующим устройством является мютекс. Слово mutex происходит от слов /им/иа11у exclusive (взаимно исключающий) с намеком на то, что если два (или более) объекта разделяют мютекс, то они обладают взаимно исключающим доступом к тому, чем управляет мютекс.
Глава 5
Мютекс очень похож на критический раздел: либо вы владеете им (и можете получить доступ к области защищаемого им программного кода), либо вы им не владеете (и должны ждать). Однако, в отличие от критического раздела, мютекс может совместно использоваться несколькими процессами. Программный код также может указывать, пока мютекс станет доступным, прежде чем наступит "тайм-аут". Вот как можно использовать мютекс для управления доступом к коду, который меняет значение:
CSingleLock theLock ( 6m_myMutex ); if ( theLock.Lock(WAIT_TIME) ) (
s.Format("Current value: %d", m_Value);
m_Value++;
theLock.Unlock() ;
}
else
AfxMessageBox("Lock on Mutex failed. No increment!");
} break;
Мютекс работает так. Вы начинаете с инициализации объекта Lock. Если вы ожидаете только один объект (например, мютекс), то можно использовать CSingleLock. В ожидании доступности более чем одного объекта используется CMultiLock.
Когда вы готовы получить доступ к защищаемому коду, то блокируете объект Lock и передаете, как долго собираетесь ждать, указывая время в миллисекундах. Значение, принятое по умолчанию, INFINITE, означает, что вы будете ждать до скончания века.
События
Объекты событий полезны в ожидании какого-либо события. Объекты событий не сигналят до тех пор, пока какой-то другой объект не заставит их делать это. Потоки могут ждать, когда событие засигналит, предоставляя вам возможность сказать потоку: "Начинай выполняться, когда произойдет данное событие".
Обычно событие создается в несигналящем состоянии и затем указывается одному или нескольким потокам ждать, когда данное событие засигналит. Вы можете установить такое событие для уведомления потоков о завершении печати, о разгрузке файла, о сообщении в очереди и т.п.
События имеют переменную-член bManualReset, которая определяет, что произойдет, когда будет вызвана функция SetEvent() для объекта Event. Если bManualReset имеет значение false, то все ожидающие потоки при вызове SetEvent() будут освобождены. Если вы хотите, чтобы событие было сброшено (перестало сигналить), то должны вызвать функцию ResetEvent().
Если bManualReset имеет значение true, то освобождается только один ожидающий поток и Event еще раз автоматически устанавливается в несигналящее состояние.
Для события (bManualReset установлено в true), очищаемого вручную, все ожидающие потоки можно освободить, вызвав PulseEvent(). Такое действие также автоматически очистит событие. Следовательно, PulseEvent() позволяет обрабатывать вручную очищаемое событие, как если бы оно было автоочищаемым событием.
Пример
Программа в листинге 5.1 использует несколько синхронизирующих устройств, описанных в предыдущих разделах. Эта программа преднамеренно упрощена, чтобы сконцентрироваться на том, как взаимодействуют объекты синхронизации. Это приложение MFC рисует простое диалоговое окно, показанное на рис. 5.4.
При щелчке на кнопке Start Thread 1 порождается рабочий поток, который увеличивает и отображает значение, хранимое в диалоговом окне. (Обычно такие значения хранятся в документе, но в диалоговом приложении MFC нет объекта Document.) Затем данный поток ждет некоторое время (до одной секунды) и повторяется. Документы и представления детально рассматриваются далее в этой главе.
При щелчке на кнопке Start Thread 2 в диалоговом окне порождается второй рабочий поток, который также увеличивает и отображает то же самое значение, что сохранено в диалоговом окне. Если позволить этим потокам какое-то время выполняться, то станет видно, что числа беспорядочно добавляются к списку в окне, поскольку потоки "воюют" за один и тот же ресурс.
При щелчке на кнопке Start Thread 3 в диалоговом окне создается новый CWinHiread-производный поток. Затем диалоговое окно посылает этому новому объекту сообщение с указанием о начале работы, и поток также начинает увеличивать значение, хранящееся в диалоговом окне. Теперь все три потока увеличивают одно и то же значение (рис. 5.5).
Часть IIЧасть IIРИСУНОК 5.4. Простое диалоговое окно.РИСУНОК 5.4. Простое диалоговое окно.РИСУНОК 5.5. Добавление значения без синхронизации.РИСУНОК 5.5. Добавление значения без синхронизации.
Часть II
Часть II
РИСУНОК 5.4. Простое диалоговое окно.
РИСУНОК 5.4. Простое диалоговое окно.
РИСУНОК 5.5. Добавление значения без синхронизации.
РИСУНОК 5.5. Добавление значения без синхронизации.
Обратите внимание, что значения имеют случайный характер:
Current value: 10 Current value: 12 Current value: 11 T3 Current value: 9 Current value: 14 Current value: 13
Это прямой результат соревнования трех потоков за одно значение, без синхронного управления.
Если в диалоговом окне щелкнуть на переключателе Critical Section, то приложение получит доступ к значению под управлением CCriticalSection. Как и следует ожидать, критический раздел является причиной того, что числа будут перечислены по порядку. Щелчок на переключателе Mutex приведет к тому, что значения будут доступны под управлением объекта CMutex и будет установлено время тайм-аута для доступа к значению. И в этом случае значения будут перечисляться по порядку.
Кнопка Pause применяется для того, чтобы можно было исследовать отображаемые значения.
Автор добавил событие CEvent, которое является несигналящим. При инициализации диалогового окна этот объект создает поток, который отображает сообщение для пользователя, но поток ждет, когда событие засигналит. По мере создания каждого из трех управляемых пользователем потоков значение счетчика увеличивается. Когда выполняются все три потока, событие сигналит и сообщение отображается, как показано на рис. 5.6.
Это неудачный пример использования CEvent, но он иллюстрирует то, как использовать событие. Рабочий поток, который отображает это сообщение, прекращается при щелчке на кнопке ОК.
Подробное исследование программного кода показывает, как используются данные объекты синхронизации. При создании приложения было указано MFC (с помощью мастеров), что требуется диалог-базиро- ванное приложение, а не одно- и не многодокументный интерфейс (рис. 5.7). Полное приложение включено в CD-ROM, сопровождающий книгу.
РИСУНОК 5.6. Событие сигналит.РИСУНОК 5.6. Событие сигналит.РИСУНОК 5.7. Диалоговое окно мастера приложений.РИСУНОК 5.7. Диалоговое окно мастера приложений.
РИСУНОК 5.6. Событие сигналит.
РИСУНОК 5.6. Событие сигналит.
РИСУНОК 5.7. Диалоговое окно мастера приложений.
РИСУНОК 5.7. Диалоговое окно мастера приложений.
Использование каркасов приложений Глава 5
Построение диалог-базированного приложения означает, что имеется более простое приложение, но отсутствует класс CDocument. Конструктор для CDocument предназначен инициализировать переменные состояния:
CThreadsUnleashedDlg::CThreadsUnleashedDlg(CWnd* pParent /*=NULL*/) ; m_Value(O), m_Paused(false), m_MTC(O), m_numThreads (0), m_AllRunningEvent(false,true,NULL,NULL), CDialog(CThreadsUnleashedDlg::IDD, pParent)
{ //{ (AFX_DATA_INIT (CThreadsUnleashedDlg) m_RadioButtonValue = 0;
//) )AFX_DATA_INIT // Заметьте, что Loadicon не требует в Win32 последующей Destroylcon m_hlcon = AfxGetApp () ->LoadIcon (IDR_MAINFRAME) ;
)
В данном коде m_Va!ue представляет значение, которое будет возрастать, m_Paused является флагом, указывающим, что пользователь щелкнул на кнопке Pause, ш_МТС является указателем на объект MyThreadClass, который создается, когда пользователь щелкает на кнопке Start Thread 3. m_NumThreads хранит счетчик того, как много затребованных пользователем потоков выполняется. m_AHRunningEvent является объектом Event, который сигналит, когда выполняются все три затребованных пользователем потока.
ПРИМЕЧАНИЕ
Часть программного кода выделена полужирным шрифтом, чтобы привлечь внимание к особенно интересным аспектам или показать, что добавлено или изменилось.
Оставшаяся часть метода создана Application Wizard.
Автор создает новый поток в OnInitDialog(), как показано ниже:
BOOL CThreadsUnleashedDlg::OnInitDialog() {
CDialog::OnInitDialog();
II . . . неважные детали опущены . . .
AfxBeginThread (ThreadsNotification, (LPVOID) this) ;
return TRUE; II возвратить значение TRUE, если только фокус не установлен на элеиент // управления
}
Вызов в AfxBeginThread(ThreadsNotification, (LPVOID)this) порождает рабочий поток, который реализуется в методе ThreadsNotification(). Остаток кода Application Wizard опущен, поскольку очевиден.
Метод ThreadsNotificationO выглядит следующим образом:
UINT CThreadsUnleashedDlg::ThreadsNotification(LPVOID pParam) (
CThreadsUnleashedDlg * pThis = (CThreadsUnleashedDlg*) pParam;
WaitForSingleObject (pThis->m_AllRunningEvent, INFINITE) ;
AfxMessageBox("All three threads are now running");
return 0;
}
С помощью этого очень простого метода распаковывается параметр и преобразуется в указатель на объект CThreadsUnleasedDIg, который вызывает данный метод. Такой подход необходим, поскольку функция рабочего потока должна быть статической. Она не может иметь указатель this. Затем мы используем указатель this для доступа к объекту CEvent, передаваемого WaitForSingleObject(), и указываем коду ждать вечно. Такая инструкция блокирует поток и освобождает его кванты времени. Если событие засигналит, данный поток освободится и отобразит окно сообщения. Последняя строка return 0 требует от функции возвратить управление и покинуть поток.
Часть II
Теперь диалоговое окно отображается и "засыпает", пока не будет выполнен щелчок на кнопке. Когда пользователь щелкает на кнопке Start Thread 1, диалоговому окну посылается сообщение, которое перехватывается в карте сообщения:
ON_BN_CLICKED(IDC_BUTTON_START_T1, OnButtonStartTl)
Здесь вызывается метод OnButtonStartTl():
void CThreadsUnleashedDlg::OnButtonStartTl() {
CButton * pB = (CButton*)GetDlgItem(IDC_BUTTON_START_Tl);
pB->EnableWindow(false);
m_Threadl = AfxBeginThread(ThreadFunction,(LPVOID)this);
CStatic * pStatic = ( (CStatic *) (GetDlgItem(IDC_STATIC_STATUS)));
pStatic->SetWindowText("Thread 1 running");
CountThreads () ;
Мы создаем указатель на кнопку, возвращаемый из GetDlgItem(), и используем данный указатель отключения кнопки (визуализация серым цветом, показывающим, что ее нельзя нажать).
Поток создается путем вызова AfxBeginThread(), передачи ему адреса статической функции, которая возвращает UINT, и одного параметра. Это требуемый синтаксис AfxBeginThread(). Поскольку мы хотим передать указатель this, то просто приводим его к LPVOID (который транслируется в void **).
Это действие порождает новый поток и немедленно возвращает управление. Затем мы получаем указатель на объект CStatic в диалоговом окне и обновляем его для отображения текста Thread 1 running. В конце вызывается CountThreads().
Вскоре мы исследуем, что происходит в теле нового потока. Но прежде рассмотрим CountThreads():
void CThreadsUnleashedDlg::CountThreads() {
if ( Interlockedlncrement(6m_numThreads) >= 3) m_AHRunningEvent. SetEvent () ;
}
Цель данного метода состоит в увеличении счетчика выполняющихся потоков. Когда счетчик достигает 3, то устанавливается событие AllRunningEvent, так что поток уведомления может быть освобожден. Поскольку мы хотим убедиться, что переменная m_numThreads находится под синхронизирующим управлением, то можем воспользоваться преимуществом метода Interlockedlncrement(), который принимает указатель на длинное целое (4 байта) и увеличивает его под управлением потока.
ПРИМЕЧАНИЕ
Под управлением Windows NT данный подход надежно возвращает вновь увеличенное значение. Однако под управлением Windows 95 этого не происходит. Таким образом, представленный программный код специфичен для Windows NT. Если вы должны выполнять данный код в Windows 95, то увеличьте значение и протестируйте код отдельно.
Рабочий поток выполняет простую работу по увеличению члена m_Value диалогового окна:
UINT CThreadsUnleashedDlg::ThreadFunction(LPVOID pParam) {
CListBox * pLB = ((CListBox*)(pThis->GetDlgItem(IDC_LIST_OUTPUT)));
CString s; int offset;
srand( (unsigned)time( NULL ) );
for ( ;; ) (
if ( pThis->m_Paused ) {
Sleep(500); continue;
)
switch ( pThis->m_RadioButtonValue )
Глава 5
{
case sNONE: // Нет s.Format("Current value: %d",pThis->m_Value); Sleep(rand() % 1000); pThis->m_Value++; break;
case sCRITICAL_SECTION: // критический раздел pThis->m_CS.Lock();// ожидание невозможно
s.Format("Current value: %d",pThis->m_Value); Sleep (rand() % 1000); pThis->m_Value++; pThis->m_CS.Unlock(); break;
case sMUTEX: // Мьютекс CSingleLock theLock ( 6(pThis->m_myMutex) ); if ( theLock.Lock(WAIT_TIME) ) ( s.Format("Current value: %d",pThis->m_Value); Sleep (rand () % 1000); pThis->m_Value++; theLock.Unlock();
) else ( AfxMessageBox("Lock on Mutex failed. No increment!");
} break; ) offset = pLB->AddString(s); pLB->SetTop!ndex( offset );
) return 0;
}
Первая работа данного раздела кода заключается в приведении указателя this:
CThreadsUnleashedDlg * pThis = (CThreadsUnleashedDlg*) pParam;
Поскольку поток реализован в статической функции-члене, то вы должны передать копию указателя this, который должен использоваться (явно) для доступа к членам. Однако поскольку это метод-член, то он имеет доступ к приватным переменным-членам класса.
Мы немедленно входим в бесконечный цикл, берем значение и увеличиваем его до тех пор, пока поток не разрушится. Каждый раз в цикле проверяем значение переменной-члена m_Paused — должны ли мы выдержать паузу, чтобы пользователь рассмотрел, что происходит:
if ( pThis->m_Paused ) { Sleep(500); continue;
}
Теперь рассмотрим кнопки-переключатели. Обратите внимание, что мы исследуем не элемент управления, а переменную-член m_RadioButtonValue:
switch ( pThis->m_RadioButtonValue )
Эта переменная-член создана Class Wizard (мастером классов) и связана с переключателями. Все переключатели содержатся в группе и распознаются как один набор взаимно исключающих вариантов. Для создания последовательности переключателей первая кнопка помечается как имеющая стиль "группа" (не помечайте так больше ничего). Затем данный переключатель можно найти во вкладке Class Wizard's Member Variable и создать целое значение переменной-члена, связанное с кнопкой. Целое устанавливается в нуль- базированное смещение от выбранной кнопки. В данном примере None имеет значение 0, Critical Section — значение 1 и Mutex — значение 2. Код для отображения кнопок на значения генерируется мастером:
Часть II
DDX_Radio(pDX, IDC_RADIO_NONE, m_RadioButtonValue);
Пока выполняется мастер, автор также отобразил каждую кнопку на метод, так чтобы, когда пользователь выберет один из переключателей, вызвался бы связанный метод:
ON_BN_CLICKED(IDC_RADIO_NONE, OnRadioNone)
ON~BN~CLICKED(IDC~RADIO_MUTEX, OnRadioMutex)
ON_BN~CLICKED(IDC~RADIO_CRITICAL_SECTION, OnRadioCriticalSection)
Например, когда пользователь щелкает на кнопке Mutex (IDC_RADIO_MUTEX), вызывается метод OnRadioMutex(), как показано ниже:
void CThreadsUnleashedDlg::OnRadioMutex() (
UpdateDataO ;
CStatic * pStatic = ( (CStatic *) (GetDlgItem(IDC_STATIC_STATUS))); pStatic->SetWindowText("Using Mutex");
}
Первое, что делает данный метод, — обновляет связанную переменную (m_RadioButtonNone) и печатает сообщение состояния на строке состояния.
Вернемся к ThreadFunctionQ. После того как мы определили, что не выдерживается пауза, переключаемся на значение m_RadioButtonNone, которое, как вы теперь знаете, соответствует переключателю, выбранному пользователем. Для проверки оператора switch используем перечисляемую константу:
enum synch { SHONE, sCRITICAL_SECTION, sMUTEX, sEVENT) ;
Это позволяет легче расшифровать назначение программного кода. Если значение sNone (нуль), то это означает, что выбрана первая кнопка None, печатается значение m_Value и увеличивается. Если выбрана кнопка Critical Section либо Mutex, то автор ограничивает вызов функции приращения кодом для управления синхронизирующим устройством.
В случае, когда выбран переключатель Critical Section, автор блокирует переменную m_CS критического раздела диалогового окна, обновляет значение и затем разблокирует его:
case sCRITICAL_SECTION: // критический раздел pThis->m_CS.Lock(); Н ожидание невозможно s.Format("Current value: %d",pThis->m_Value);
Sleep (rand() % 1000);
pThis->m_Value++;
pThis->m_CS.Unlock(); break;
В случае, когда выбран переключатель Mutex, имеется возможность большего контроля. Автор создает объект CSingleLock, передавая адрес мютекса, а затем требует замок, указывая время тайм-аута. Если мютекс блокирован в пределах отведенного времени, то автор двигается вперед, печатает и обновляет значение. Если мютекс недоступен в пределах отведенного времени, то работа прекращается и отображается предупреждающее сообщение. Это предотвращает поток от "зависания", когда ожидается мютекс, который никогда не освободится.'
Если пользователь щелкает на кнопке Start Thread 3, инициализируется несколько иная последовательность событий:
void CThreadsUnleashedDlg::OnButtonStartT3()
CButton * pB = (CButton*)GetDlgItem(IDC_BUTTON_START_T3) ; pB->EnableWindow (false) ;
if ( ! m_MTC ) (
m_MTC = new CMyThreadClass(this);
BOOL bRc = m MTC->CreateThread() ;
if ( ! bRc )” 1
CString msg("Unable to start thread!\n") ;
AfxMessageBox(msg);
// JLTODO: послать завершающее сообщение ) m_MTC->PostThreadMessage( WM_START_WORK, 0, 0 ) ;
Глава 5
CountThreads () ; }
}
И вновь мы обесцвечиваем кнопку, но на этот раз проверяем, имеет ли значение NULL переменная- член, которая указывает на порожденный от CWinThread объект. Предполагая, что да, мы готовы создать новый экземпляр класса потока.
Обычно действительно проверяется, имеет ли указатель значение NULL, однако в данном программном коде мы знаем, что это не так, и потому оператор if можно переписать, используя ASSERT.
ASSERT ( ! m_MTC ) ;
Поскольку мы обесцветили кнопку, данный поток существовать уже не может. Автор предоставил этот путь, чтобы вы могли воспользоваться им в будущем, когда будете создавать или разрушать потоки для объектов.
Цель заключается в реализации объекта типа CMyThreadClass. Данный класс порожден от CWinThread и, таким образом, способен управлять потоком Windows:
class CMyThreadClass : public CWinThread
Создание потока представляет собой двухэтапный процесс. На первом этапе реализуется объект потока:
т_МТС = new CMyThreadClass(this);
На втором этапе создается сам поток:
BOOL bRc = m_MTC->CreateThread() ;
Обратите внимание на разницу. Когда мы создаем экземпляр, используя оператор new, то создаем объект, но не создаем поток Windows. Поток создается путем обращения к CreateThread(). Мы говорим об объекте и о потоке так, будто это одно и то же, но это разные вещи.
После того как поток запущен, мы хотим, чтобы он начал работать, но не хотим вызывать его метод StartWork() прямо. Если так сделать, то поток зависнет вплоть до возврата методом управления (что никогда не произойдет). Вместо этого мы хотим асинхронно послать сообщение данному потоку и указать ему начать работу. Такое указание посылается методом PostThreadMessage(), который возвращается немедленно:
m_MTC->PostThreadMessage( WM_START_WORK, 0, 0 );
Этот оператор посыпает потоку определенное пользователем сообщение со значением WM_START_WORK. Данное значение также определяется в файле constants.h, как показано ниже:
const int WM_START_WORK = WM_USER + 1;
Когда поток обнаруживает это сообщение, то отображает его на метод-член класса диспетчерской картой:
BEGIN_MESSAGE_MAP(CMyThreadClass, CWinThread)
И(( AFX_MSG_MAP(CMyThreadClass)
H Примечание — будет здесь добавлять и удалять отображение макросов.
//)) AFX_MSG_MAP
ON_THREAD_MESSAGE (WM_START_WORK, OnStartWork
END_MESSAGE_MAP ()
Обратите внимание, что автор добавил свою строку ON_THREAD_MESSAGE после защищенного кода AFX_MSG_MAP, но перед макросом END_MESSAGE_MAP(). Все, что находится между этими двумя строками, может быть переписано мастерами, но код, который добавил автор, находится в безопасности:
// (( AFX_MSG_MAP(CMyThreadClass)
//}} AFX_MSG_MAP
Это отображение просто диктует, чтобы при приеме сообщения WM_START_WORK вызывался метод OnStartWork():
void CMyThreadClass::OnStartWork() {
CStatic * pStatic = ( (CStatic *) (m_pDlg->GetDlgIteni(IDC_STATIC_STATUS))) ; pStatic->SetWindowText("Thread 3 running");
CListBox * pLB = ((CListBox*) (m_pDlg->GetDlgIteni(IDC_LIST_OUTPUT))) ;
Часть II
srand( (unsigned)time( NULL ) );
CString s;
CString status; for ( ;; ) ( if ( m_pDlg->IsPaused() ) (
Sleep(500) ; continue; ) int val;
switch ( m_pDlg->m_RadioButtonValue ) (
case sNONE: // Нет val = m_pDlg-XSetValue () ; s.Format("T3 Current value: %d",val); Sleep (rand() % 1000);
m_pDlg->SetValue(++val); break;
case sCRITICAL_SECTION: H критический раздел m_pDlg->m_CS.Lock();// ожидание невозможно
val = m_pDlg->GetValue();
s.Format("T3 Current value: %d",val);
Sleep (rand () % 1000); m_pDlg->SetValue(++val); m_pDlg->m_CS.Unlock();
break; case sMUTEX: // Мьютекс CSingleLock theLock ( & (m_pDlg->m_myMutex) ); if ( theLock.Lock(WAIT_TIME) ) (
val = m_pDlg->GetValue();
s.Format("T3 Current value: %d",val); Sleep (rand () % 1000);
m_pDlg->SetValue(++val); theLock.Unlock() ;
) else (
AfxMessageBoxfLock on Mutex failed. No increment!"); ) break;
int offset = pLB->AddString(s); pLB->SetTop!ndex( offset ) ;
)
Неудивительно, что данный метод очень похож на ThreadFunction(), который мы только что исследовали. Значительная разница заключается в том, что это не статический метод класса диалогов, а простой метод-член класса потоков. Класс потоков может поддерживать свое состояние и отвечать на сообщения потоков.
Для того чтобы гарантировать правильную синхронизацию, этот поток должен использовать тот же мютекс, который использует поток класса диалогов. Помните, что CMutex можно именовать. Для этого мы инициализируем мютекс в конструкторе диалога:
CThreadsUnleashedDlg::CThreadsUnleashedDlg(CWnd* pParent /*=NULL*/) : m_value(0), m_Paused(false), m_MTC(0),
Глава 5
m_numThreads(0) , m_AllRunningEvent(false,true,NULL,NULL), m_myMutex(false,"ml"), CDialog(CThreadsUnleashedDlg::IDD, pParent)
Затем мы создаем другой мютекс как член потока:
class CMyThreadClass : public CWinThread (
private: CThreadsUnleashedDlg * m_pDlg; CMutex m_Mutex;
1;
Затем инициализируем данный мютекс тем же самым именем:
CMyThreadClass::CMyThreadClass(CThreadsUnleashedDlg * pDlg): m_pDlg(pDlg) , m_Mutex(false,"ml") ( }
Сейчас можно изменить программный код в потоке для ссылки на собственный мютекс, который является ссылкой на объект-мютекс, хранимый диалогом:
CSingleLock theLock ( 6m_Mutex ); if ( theLock.Lock(WMT_TIME) ) {
H... )
Служебные классы
Библиотека MFC предоставляет набор служебных классов, часть из которых мы уже видели в действии. Служебные классы добавлены компанией Microsoft к базовым классам для того, чтобы упростить программирование, — они освобождают вас от написания части кода.
Наиболее важными и полезными из служебных классов являются CString (для манипулирования символьными строками) и CTime (для манипулирования датами, временем и промежутками времени).
Классы манипулирования строками
Возможно, тем служебным классом, который используется наиболее часто, является CString. Этот простой строковый класс помогает использовать текстовые строки: создание, форматирование, копирование, передачу в качестве параметров и т.п.
Конструктор CString перегружен так, чтобы принимать параметры (конструктор по умолчанию) либо существующий объект CString (конструктор копии), либо несколько альтернатив, возможно, самыми полезными из которых являются строки С-стиля и NULL-определенные строки.
После того как объект CString создан, его можно объединять с другими CString. Следующий код инициализирует $3 строкой "Hello world":
CString si("Hello");
CString s2 (" world");
CString s3 = sl+s2;
Класс CString можно использовать для осмысленных манипуляций строками, например, поиском подстрок. Одним из самых мощных методов является Format(), который принимает строку, подобную той, что принимает printf(), и который использует те же спецификации, что и printf(). Вы увидите CString.Format() в действии в этой и других главах книги.
Классы времени
Другой популярный набор служебных классов MFC помогает манипулировать временем. Стандарт ANSI обеспечивает тип данных time_t, который MFC оборачивает в класс CTime. Данный класс представляет абсолютное время и дату в диапазоне между 1 января 1970 года и 18 января 2038 года. Класс CTime вклю-
Вопросы реализации Часть П
чает служебные функции для преобразования дат Грегорианского календаря. Дополнительные методы помогают вычитать год, месяц, день, часы, минуты из заданного значения времени.
Класс CTimeSpan обеспечивает относительные значения времени, т.е. диапазоны времени. Когда один объект CTime вычитается из другого, результатом будет объект CTimeSpan. Вы можете добавлять и вычитать объекты CTimeSpan из объектов CTime для создания новых объектов CTimeSpan. Таким образом можно манипулировать диапазоном дат в 68 лет.
Этот класс можно использовать для измерения интервалов между двумя событиями. Изменим класс CMyThreadClass, показанный ранее, так, чтобы тогда, когда он не находился под синхронизирующим управлением, он приостанавливался на случайные интервалы времени — от 1 до 5 тыс. миллисекунд (5 секунд). Мы можем измерять длительность (в секундах), учитывая текущее время начала работы и время конца работы и измеряя разницу между ними:
CTime start, end; CTimeSpan len; start = CTime::GetCurrentTime(); rightNow = start.Format("T3 Start: %b %d %Y %I:%M%p"); offset = pLB->AddString(rightNow); pLB->SetTop!ndex( offset ); val = m_pDlg->GetValue(); s.Format("T3 Current value: %d",val); Sleep (rand() % 5000); m_pDlg->SetValue(++val); end = CTime::GetCurrentTime(); rightNow = end.Format("T3 End: %b %d %Y %I:%M%p"); offset = pLB->AddString(rightNow); pLB->SetTop!ndex( offset ); len = end - start; duration.Format("T3 - Duration %d seconds",len.GetSeconds() ) ; offset = pLB->AddString(duration); pLB->SetTop!ndex( offset ) ;
Переменная start устанавливается в текущее время перед началом работы. Переменная end устанавливается в текущее время по окончании работы. Промежуток времени len устанавливается в разницу.
Мы можем извлечь и напечатать текущее время, вызвав Format() объектов CTime. Мы можем напечатать длительность, используя метод-член GetSeconds() объекта CTimeSpan.
Вывод из предыдущего программного кода выглядит следующим образом:
T3Start Jul 28 1998 05:02:39PM T3End Jul 28 1998 05:02:44PM T3 Duration 5 seconds T3Current value: 10 T3Start Jul 28 1998 05:02:44PM T3End Jul 28 1998 05:02:48PM T3 Duration 4 seconds T3Current value: 11 T3Start:Jul 28 1998 05:02:48PM T3End Jul 28 1998 05:02:51 PM T3 Duration 3 seconds T3Current value: 12 T3Start: Jul 28 1998 05:02:51PM T3End: Jul 28 1998 05:02:55 PM T3 Duration 4 seconds
Документы и представления
Конечно, диалоговое окна и потоки не являются сердцем и душой MFC. Реальная причина существования MFC заключается в помощи управлением по созданию и манипулированию окнами — экранными представлениями данных. Для этого MFC использует документы (для управления данными) и представления (для управления окнами и элементами управления).
Глава 5
Конструктивный шаблон MFC документ/представление является упрощением конструктивного шаблона Model/View/Controller (MVC), исходно использовавшегося для построения пользовательских интерфейсов в Smalltalk-80. В MVC создается объект, который представляет ваши данные. Такой объект называется моделью. Затем вы присваиваете ответственность за просмотр данных различным представлениям, определяете просмотр и ответственность за управление или манипулирование данными, которые присваиваются контроллерам.
Вариант Microsoft конструктивного шаблона MVC является чем-то более простым чем шаблон документ/представление. В подходе Microsoft данные (документ) по-прежнему отделены от представления, но представление и контроллер слиты. Шаблон документ/представление столь характерен для Microsoft Foundation Classes, что иногда трудно написать программу, которая игнорирует данный шаблон.
Важно заметить, что и в MVC, и в шаблонах документ/представление моделью или документом являются любые используемые программой данные. Документы не обязательно должны быть текстами — они могут быть коллекциями записей, таблиц или аморфными структурами данных.
Представления
Представления в MFC выступают как окна и элементы управления. Класс CWnd является базовым для всех представлений, включая диалоговые окна, класс CFrameWnd управляет рамкой окна и класс CMDIChildWnd управляет многодокументным оконным интерфейсом. Класс CWnd также является базовым классом для CView, а равно и для всех элементов управления, таких как CButton, CListbox и т.д.
Классы CDocument и CView инкапсулируют шаблон конструктора документ/представление и работают с CWnd-производными классами над отображением содержимого CDocument. Класс CView можно расширить многими способами, и MFC обеспечивает вас семейством порожденных классов, включающих CScrollView (для прокрутки больших документов), CEditView (для построения текстовых документов), CRichEditView (для создания документов с расширенным текстом с форматированием полужирным, курсивным и другими шрифтами). CFonnView (для создания экранных форм), CRecordView (для работы с базами данных), CTreeView (для создания структурированных представлений иерархических данных).
CView является родительским классом всех перечисленных порожденных представлений и обеспечивает несколько виртуальных функций для перекрытия производными классами. Вероятно, наиболее важные функции CView — это OnInitialUpdate(), OnUpdate() и OnDraw().
OnInitialUpdate() вызывается, когда представление первый раз обновляет себя. Каждый раз, когда документ изменяется, он уведомляет все текущие прикрепленные (вложенные) документы. MFC вызывает OnInitialUpdate() первый раз и OnUpdate() все последующие разы.
Поведение, предусмотренное по умолчанию, для методов обновления заключается в лишении окна законной силы и вызове OnDraw(). Такой подход обеспечивает гибкость в том, как визуализировать представление, но, кроме того, предоставляет актуальный программный код, который можно использовать для реализации функций рисования по умолчанию.
Представление форм
Одним очень распространенным примером использования MFC является создание сложных форм для сбора информации от пользователя. Существует два общих метода для реализации подобных приложений: как простое диалоговое окно и с новым классом CFonnView, который объединяет архитектуру документ/ представление с простотой использования диалогового окна. Здесь мы исследуем представление форм достаточно глубоко для того, чтобы рассмотреть общий подход построения подобных приложений с использованием фактически любой библиотеки каркасов приложений.
Пример программы, которую мы будем реализовывать, выглядит так, как показано на рис. 5.8.
Это приложение представляет форму для использования в кредитном агентстве. Оно собирает информацию от пользователя с помощью нескольких элементов управления. Среди них мы исследуем поля для редактируемых данных (например, Last Name), переключатели (например, Spending), поля со списком (например, Salary Range), ползунок (например, Credit Limit) и счетчик (например, Member Since). Кроме того, исследуем вкладки свойств, которые вызываются здесь с помощью кнопки Profile, но которые в приложениях часто применяются как устройства для сбора пользовательских настроек. В качестве примера вкладок свойств на рис. 5.9 показана страница свойств текстового редактора Microsoft Word.
Часть И
РИСУНОК 5.8. Пример приложения с представлением формы.
РИСУНОК 5.9. Вкладки свойств Microsoft Word.
Как видно, вкладка свойств является способом управления последовательностью диалог-базированных форм, предоставляющим пользователю свободу перемещения между вкладками по мере необходимости. Та вкладка свойств, которую мы реализуем в примере, выглядит так, как та, которая изображена на рис. 5.10. Она несколько проще, чем вкладка свойств Microsoft Word, но предоставляет возможность в деталях исследовать, как реализуются вкладки свойств.
Приступая к изучению представлений форм
Для начала запустим новый проект SDI и вызовем мастер обычным способом. Как раз перед подтверждением имен файлов выделим представление (CControlsUnleashedView) и изменим его тип (базовый класс) с CView на CFormView, как показано на рис. 5.11.
РИСУНОК 5.10. Пример вкладки свойств.РИСУНОК 5.10. Пример вкладки свойств.
РИСУНОК 5.10. Пример вкладки свойств.
РИСУНОК 5.10. Пример вкладки свойств.
После того как приложение будет построено, откроем редактор ресурсов и приступим к построению начальной формы. Каждому элементу управления присваивается имя, и вызывается мастер Class Wizard для присваивания переменных, соответствующих значению каждого элемента управления, как показано на рис. 5.12.
Для некоторых элементов управления, таких как IDC_COMBOSalaryRange, создаются две переменные: одна содержит значение (m_SalaryRangeValue), а другая предоставляет сам элемент управления (m_ComboSalaryRange).
Имея переменную, которая содержит сам элемент управления, можно написать такой простой оператор:
m_ComboSalaryRange.GetCount();
Глава 5
Если бы такой переменной не было, то пришлось бы писать:
CComboBox * рВох = (CComboBox *) GetDlgItem(IDC_COMBOSalaryRange); pBox->GetCount() ;
РИСУНОК 5.12. Присваивание переменных каждомуРИСУНОК 5.12. Присваивание переменных каждомуэлементу управления.элементу управления.Когда вы ожидаете получения доступа к самому элементу управления, то использование сгенерированной мастером Class Wizard переменной проще и легче поддерживается.
РИСУНОК 5.12. Присваивание переменных каждому
РИСУНОК 5.12. Присваивание переменных каждому
элементу управления.
элементу управления.
После того как все элементы управления формы получили свои значения, Class Wizard генерирует код, гарантирующий, что эти переменные будут обновляться вместе со значениями в элементах управления. Исследуем, как работает такой механизм. При этом обратите внимание на то, что эти переменные являются членами CFonnView, а не документа. Если вы хотите обеспечить, чтобы данные были помещены в централизованное хранилище (а именно это и требуется, когда данные будет отображать более чем одно представление), то необходимо создать соответствующие переменные в документе и управлять передачей данных между представлением и документом вручную.
Управление данными
Для приема данных в переменные и последующего извлечения их из переменных вызывается UpdateData(). Если вы передаете параметр false, то инициализируете элементы управления данными в представлении. Если вы не передаете параметр, то значение true, принятое по умолчанию, указывает, что необходимо обновить переменные содержимым элементов управления.
Метод UpdateData() в конце концов вызовет DoDataExchange() (не вызывайте данный метод прямо!), который был создан Class Wizard (хотя можно при желании создать его самостоятельно). Метод DoDataExchange() выглядит следующим образом:
void CControlsUnleashedView::DoDataExchange(CDataExchange* pDX)
CFonnView: :DoDataExchange(pDX) ;
//((AFX_DATA_MAP(CControlsUnleashedView)
DDX_Control(pDX, IDC_COMBOSalaryRange, m_ComboSalaryRange);
DDX_Control(pDX, IDc2sPIN_MEMBER_SINCE, m_MemberSinceControl) ;
DDX_Control(pDX, IDC_STATICVISAPic, m_VisaPic);
DDX_Control(pDX, IDC_STATICMCPic, m_MasterCardPic);
DDX_Control(pDX, IDC_SLIDERCredit, m_CreditSlider);
DDX_Control(pDX, IDC_RADIOVisa, m_VisaButton);
DDX_Control(pDX, IDC_BUTTONProfile, m_ButtonProfile);
DDX_Control(pDX, IDC_EDITCreditLimit, m_CreditLimitControl);
DDX_Control(pDX, IDC_COMBOOptions, m_CCOptions);
DDX~Control(pDX, IDC~CHECKVisaClub, m_CheckVisaClub);
DDX_CBIndex(pDX, IDC_COMBOOptions, m_CCOptionValue);
DDX_CBString(pDX, IDC_COMBOState, m_State);
DDX_Text(pDX, IDC_EDITAddress, m_Address);
DDX_Text(pDX, IDC_EDITCity, m_City);
DDX_Text(pDX, IDC_EDITFirstName, m_FirstName);
DDX_Text(pDX, IDC_EDITLastName, m_LastName);
DDX_Radio(pDX, IDC_RADIONoProfile, m_RadioProfile);
DDX_Radio(pDX, IDC_RADIOVisa, m_VisaButtonValue);
DDX_Text(pDX, IDC_Zip, m_Zip);
DDX_Check(pDX, IDC_CHECKAdv, m_CheckAdWalue) ;
DDX_Check(pDX, IDC_CHECKCheckWriting, m_CheckWritingValue);
DDX_Text(pDX, IDC_EDIT_MEMBER_SINCE, m_EditSince);
DDV_MinMaxInt(pDX, m_EditSince, 1990, 2005);
DDX_Text(pDX, IDC_EDITCreditLimit, m_EditCreditLimitValue);
DDX_CBIndex(pDX, IDC_COMBOSalaryRange, m_SalaryRangeValue);
//))AFX_DATA_MAP"
)
Часть II
Каждый из макросов DDX_ гарантирует, что данные будут взяты из элементов управления и вставлены в переменные в правильном формате (например, преобразованы из строк в целые там, где это необходимо). Кроме того, макросы предоставляют возможность автоматической проверки диапазона. Например, переменная m_EditSince устанавливается для проверки целого значения между 1990 и 2005. Когда вы исследуете данную переменную в Class Wizard, то получите возможность установить указанные ограничения (рис. 5.13).
Если пользователь делает попытку покинуть диалоговое окно, а выбранное значение не находится в заданных пределах, то выдается информация об ошибке и происходит возврат в соответствующее поле для выполнения исправления (рис. 5.14).
Проверка выполняется успешно тогда, когда пользо-
РИСУНОК 5.13. Мастер Class Wizard.РИСУНОК 5.13. Мастер Class Wizard.РИСУНОК 5.14. Проверка значений.РИСУНОК 5.14. Проверка значений.РИСУНОК 5.15. Проверка значений при покидании поля.РИСУНОК 5.15. Проверка значений при покидании поля.ватель щелкает на кнопке ОК, но автор модифицировал программный код так, чтобы обеспечить выполнение проверки после того, как пользователь покинет поле. Он сделал это, указав Class Wizard создать метод для сообщения EN_KILL_FOCUS, который вызывается, когда вы покидаете поле (рис. 5.15).
РИСУНОК 5.13. Мастер Class Wizard.
РИСУНОК 5.13. Мастер Class Wizard.
РИСУНОК 5.14. Проверка значений.
РИСУНОК 5.14. Проверка значений.
РИСУНОК 5.15. Проверка значений при покидании поля.
РИСУНОК 5.15. Проверка значений при покидании поля.
В реализации данного метода автор просто вызывает метод UpdateData(), который выполняет проверку данных:
void CControlsUnleashedView::OnKillfocusEditMemberSince()
{
UpdateDataO ;
}
Программирование, управляемое событиями
Конечно, наиболее часто приходится отвечать на сообщения о щелчках на переключателях, которые управляются в основном так же, как и сообщения, сгенерированные другими элементами управления. Например, если щелкнуть на переключателе Visa или Master Card, то будет изменяться отображаемый рисунок и флажки доступных возможностей. Мы создаем метод OnRADIOVisa(), который будет вызываться при нажатии кнопки:
void CControlsUnleashedView::OnRADIOVisa() (
UpdateData() ;
if ( m_VisaButtonValue=0 ) ( m_CCOptiona.ResetContent(); m_CCOptions.AddString("Visa Card");
Использование каркасов приложений
Глава 5
m_CCOptions.AddString("Visa Card Gold");
m__CCOptions .AddString ("Visa Card Platinum");
m_CCOptions.SetCurSel(0) ;
m_CheckVisaClub.EnableWindow(true);
m_VisaPic.ShowWindow(SW_SHOW);
m_MasterCardPic.ShowWindow(SW_HIDE);
} else <
m_CCOptions.ResetContent() ;
m_CCOptions.AddString("Master Card Gold");
m_CCOptions.AddString("Master Card Platinum");
m_CCOptions.AddString("Master Card Premium");
m_CCOptions.SetCurSel(0);
m_CheckVisaClub.EnableWindow(false);
m_VisaPic.ShowWindow(SW_HIDE);
m_MasterCardPic.ShowWindow(SW_SHOW); }
}
Как видно, этот метод используется для проверки значения выбранного переключателя и затем соответственно для установки содержимое поля со списком. Метод также скрывает рисунок Master Card и отображает рисунок Visa (или наоборот).
Метод OnRADIOMasterCard() вызывает OnRADIOVisa:
void CControlsUnleashedView::OnRADIOMasterCard()
{
OnRADIOVisa();
}
На пользователей производит впечатление, когда они щелкают на переключателе, а другие части формы реагируют на это, хотя реализация может быть совершенно простои. Подобным образом можно связать и другие элементы управления. Например, поле Credit Limit связывается с ползунком, расположенным ниже этого поля. При перемещении ползунка обновляется поле, ограничивающее кредит, и наоборот. Положение ползунка обновляется путем перехвата сообщения об уничтожении фокуса и установки позиции ползунка на основе текущего содержимого поля:
void CControlsUnleashedView::OnKillfocusEDITCreditLimit()
(
UpdateDataO ;
m_CreditSlider.SetPos( m_EditCreditLimitValue / 1000 ) ;
UpdateData(false);
)
Когда бегунок остановлен, вы можете захватить сообщение г. горизонтальной прокрутке и обновить текстовое поле:
void CControlsUnleashedView::OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar)
{
CSliderCtrl* p_Slider= dynamic_cast<CSliderCtrl*> (pScrollBar);
if (!p_Slider)
TRACE("Not really a CSliderCtrlXn");
else
{
int value=p_Slider->GetPos();
CString temp;
temp.Format("%ld",value * 1000);
(GetDlgItem(IDC_EDITCreditLimit))->SetWindowText(temp);
}
CFormView::OnHScroll(nSBCode, nPos, pScrollBar);
}
Часть II
Обратите внимание на то, что прежде всего необходимо гарантировать прием представлением уведомления о горизонтальной прокрутке от самого ползунка. Внутрь передается указатель на класс CScrollBar. Поскольку класс CSliderCtrl происходит от CScrollBar, можно использовать новый оператор ANSI dynamic_cast для гарантии, что у нас имеется действительный объект CSliderCtrl, до того, как реализовывать обновление.
Наконец, мы связываем поле со списком Salary Range. Изменение информации о зарплате должно установить в поле Credit Limit по умолчанию и это, в свою очередь, позволит установить положение ползунка:
void CControlsUnleashedView::OnCloseupCOMBOSalaryRange() {
UpdateDataО;
m_EditCreditLimitValue = m_ComboSalaryRange.GetItemData(m_SalaryRangeValue);
m_CreditSlider.SetPos( m_EditCreditLimitValue / 1000 ) ;
UpdateData(false);
Этот программный код имеет небольшую хитрость. Мы устанавливаем m_EditCreditLimitValue в элемент данных, связанный с полем, в котором находится список. Windows не обеспечивает данной связи. Мы делаем это, инициализируя диалоговое окно:
// свяэать начальное ограничение кредита с каждый диапазоном зарплаты for(int offset-0; offset<=m_ComboSalaryRange.GetCount(); offset++) (
switch (offset)
crLimit=5000;
break;
crLimit=15000; break; case 2:
crLimit=40000;
break;
crLimit=80000;
break;
crLimit=l00000;
break;
default:
crLimit=0;
)
m_ComboSalaryRange.SetltemData(offset,crLimit);
)
Данный код связывает значение 5000 с первым входом в окне, значение 15000 — со вторым входом и т.д. Возвращаясь к OnCloseupCOMBOSalaryRange(), когда пользователь выбирает Salary Range, мы осуществляем выбор, используем его для извлечения связанного элемента данных и затем помещаем это значение в поле для редактирования. Наконец, мы используем значение поля для установки ползунка. Пользовательский интерфейс становится динамичным и интегрированным: размер зарплаты устанавливает ограничение кредита по умолчанию.
Переключатели
Переключатели в группе распознаются Windows как связанная группа. Вы должны помочь Windows, гарантировав, что порядок табуляции правилен. Суть в том, что кнопки располагаются в последовательном порядке, и первая кнопка та, для которой установлен стиль Group. Кроме того, первый элемент управления, не принадлежащий группе, должен иметь стиль Group, установленный так, чтобы дать Windows сигнал о том, что достигнут конец группы переключателей (рис 5.16).
Глава 5Глава 5ПРИМЕЧАНИЕ
Глава 5
Глава 5
Порядок табуляции управляет последовательностью, в которой пользователь будет перемещаться от одного элемента управления к другому. Порядок табуляции устанавливается в редакторе ресурсов нажатием клавиш Ctrl+D и затем щелчком на каждом элементе управления в том порядке, в котором они должны вызываться.
Обратите внимание, что кнопки в группе Credit Card Туре располагаются последовательно. У Visa установлен атрибут Group и рисунок Visa (IDC_STATICMCPic) — следующий в порядке табуляции — имеет свой атрибут, который установлен так, чтобы сообщать об окончании группы переключателей.
РИСУНОК 5.16.
Установка стиля Group.
Member since: |1997^РИСУНОК 5.17. Счетчик.Member since: |1997^РИСУНОК 5.17. Счетчик.Счетчик
Member since: |1997^
РИСУНОК 5.17. Счетчик.
Member since: |1997^
РИСУНОК 5.17. Счетчик.
MFC предоставляет возможность объединить поле для редактирования со счетчиком, как видно из Member Since на рис. 5.17.
Если исследовать элементы управления в редакторе ресурсов, то выяснится, что они достаточно своеобразны (рис. 5.18).
в последовательностив последовательностиУстановкой стиля Autobuddy вы связываете счетчик с полем сразу перед ним
в последовательности
в последовательности
РИСУНОК 5.18. Редактор ресурсов.РИСУНОК 5.18. Редактор ресурсов.РИСУНОК 5.19. Установка нужных флажков.РИСУНОК 5.19. Установка нужных флажков.переходов. (Еще одна причина проверить порядок переходов!) Если вы хотите, чтобы поле управлялось счетчиком (как показано на рис. 5.19), то следует также установить флажок Setbuddyinteger.
РИСУНОК 5.18. Редактор ресурсов.
РИСУНОК 5.18. Редактор ресурсов.
РИСУНОК 5.19. Установка нужных флажков.
РИСУНОК 5.19. Установка нужных флажков.
Наконец, необходимо установить диапазон счетчика, что делается в методе OnInitialUpdate():
m_MemberSinceControl.SetRange(1990,2005);
Вкладки свойств
К этому моменту почти все элементы управления для примера приложения выбраны. Последний шаг заключается в создании вкладок свойств, которые появляются, когда пользователь щелкает на кнопке Profile. Вкладки свойств — это, по сути, связанные диалоговые окна, каждое из которых представляет tabSheet (рис. 5.20).
Простая вклада свойств состоит из контейнера (класс CProfile : public CPropertySheet), который содержит три CPropertyPage производных класса: CWorkPage, CIncomePage и CCreditHistoryPage. Для создания этих вкладок вы прежде всего создаете три отдельных диалоговых окна.
Вкладки свойств будут принимать размеры большей из них. Идеально, когда они будут довольно близки по размеру друг к другу. Редактор ресурсов помогает решить проблему размеров, предлагая формы шаблонов.
Часть II
Если вы выберете в меню Resource пункт Insert Resource и откроете ветвь Dialog, то сможете выбрать один из трех шаблонов: IDD_PROPAGE_LARGE, IDD_PROPAGE_MEDIUM и IDD_PROPAGE_SMALL. Автор три раза выбирал IDD_PROPAGE_MEDIUM для создания каждой из вкладок, как показано на рис. 5.21.
РИСУНОК 5.20. Вкладки свойств.РИСУНОК 5.20. Вкладки свойств.РИСУНОК 5.21. Выбор шаблона.РИСУНОК 5.21. Выбор шаблона.
РИСУНОК 5.20. Вкладки свойств.
РИСУНОК 5.20. Вкладки свойств.
РИСУНОК 5.21. Выбор шаблона.
РИСУНОК 5.21. Выбор шаблона.
РИСУНОК 5.22. Добавление элементов управления во вкладку свойств.РИСУНОК 5.22. Добавление элементов управления во вкладку свойств.
РИСУНОК 5.22. Добавление элементов управления во вкладку свойств.
РИСУНОК 5.22. Добавление элементов управления во вкладку свойств.
Следующий шаг заключается в добавлении всех элементов управления к каждой вкладки свойств, как показано на рис. 5.22. Это делается обычным способом с помощью Resource Editor.
После того как элементы управления будут спозиционированы, вызовите мастер Class Wizard, который подскажет, как создать класс для нового диалогового окна. Автор рекомендует изменить имя файла для этих классов, чтобы поместить их в один файл. (Автор использовал CProperty.h и Cproperty.cpp.)
Когда все вкладки завершены, пришло время создать класс CPropertySheet. Для этого необходимо щелкнуть правой кнопкой на проекте и выбрать команду New Class из контекстного меню. Далее присвойте классу имя и породите его от CPropertySheet.
Вкладка свойств создается динамически по требованию. Когда пользователь щелкает на кнопке Profile, вызывается метод OnBUTTONProfile
(который отображается с помощью Class Wizard).
Прежде всего мы создаем объект CProfile (помните, что CProfile происходит от CPropertySheet). Затем создаются локальные экземпляры CCreditPage, ClncomePage и CWorkPage и передаются как параметры методу AddPage() класса CPropertySheet. Как раз перед добавлением этих вкладок мы инициализируем элемент управления m_SalaryComboValue в ClncomePage для соответствия полю со списком для информации о зар
плате в диалоговом окне:
void CControlsUnleashedView:."OnBUTTONProfile () (
CProfile theProfile(_T("Profile"),this) ;
UpdateData();
CCreditPage creditHistoryPage;
ClncomePage incomePage;
CWorkPage workPage;
incomePage.m_SalaryComboValue * m_SalaryRangeValue;
theProfile.AddPage(SworkPage);
theProfile.AddPage((incomePage);
theProfile.AddPage((creditHistoryPage);
// здесь начальные значения диалога
if ( theProfile.DoModal() — IDOK ) < m_SalaryRangeValue = incomePage.m_SalaryComboValue; m_EditCreditLimitValue = m_ComboSalaryRange.GetItemData(m_SalaryRangeValue); m_CreditSlider.SetPos( m_EditCreditbimitValue / 1000 );
Глава 5
}
UpdateData(false);
}
Затем вкладка свойств вызывается как окно модального диалога через обращение к методу DoModal(). Если мы вернемся к IDOK (пользователь щелкнул на кнопке ОК), то сможем предпринять любое соответствующее действие. То, что показано в программном коде, — это обновление единичного элемента управления (Salary Range). В коммерческом приложении вы, вероятно, обновили бы документ новыми данными, а затем обновили бы все представления так, чтобы они отражали новые данные.
Резюме
Вы увидели, что библиотека Microsoft Foundation Classes предлагает мощный набор служебных и вспомогательных классов, необходимых при создании приложений Windows. Каждый поставщик может предложить свои каркасы приложений. Поставщики усиленно работают над построением актуального базового программного кода, позволяя вам сконцентрироваться на семантике конкретной проблемы, а не на деталях операционной системы.
Поскольку библиотеки каркасов приложений отличаются друг от друга, у программистов будет довольно широкий выбор. Следовательно, делать его нужно осмысленно. Идеальный каркас приложения — это тот, который хорошо написан, инкапсулирован, объектно-ориентирован, действен, расширяем и надежен. Он также должен поддерживаться и эволюционировать вместе с базовой операционной системы.
Контейнерные классы библиотеки STL
В ЭТОЙ ГЛАВЕ
Определение и реализация шаблонов
Последовательные контейнеры
Стеки
Очереди
Связанные контейнеры
Вопросы производительности
Использование стандартной библиотеки С++
Конструирование типов элементов
Глава 6
Контейнер является объектом, который содержит другие объекты. Стандартная библиотека C++ (Standard C++ Library) содержит несколько контейнерных классов, которые представляют собой мощные инструменты, помогающие разработчикам справиться с распространенными проблемами программирования. Существует два типа контейнерных классов Standard Template Library (STL): последовательные и связанные. Последовательные контейнеры сконструированы для обеспечения последовательного и случайного доступа к их членам, или элементам. Связанные (ассоциативные) контейнеры оптимизированы для доступа к своим элементам по ключу. Как и другие компоненты Standard C++ Library, библиотека STL переносима между различными операционными системами. Все контейнерные классы STL определены в namespace std.
Определение и реализация шаблонов
Прежде чем углубиться в обсуждение контейнерных классов Standard Template Library, кратко рассмотрим концепцию шаблонов. Шаблоны C++ позволяют типам данных передаваться в качестве параметров в функции и определения классов. Одна и та же функция или класс может использоваться для множества объектных типов.
Определение и реализация шаблонов функций
Шаблон функций предоставляет возможность конструировать функцию, которую затем можно использовать для обработки различных типов объектов. Например, можно указать, что аргумент будет объектом разных типов. Типы возвращаемого значения также могут изменяться в зависимости от параметров шаблона.
Следующий фрагмент кода показывает определение шаблона функций:
template<class ClassX, class ClassY>void Square2(Classxe x, ClassYS y) (
1
Функция Square2() может использоваться для обработки разных типов объектов, если эти объекты реализуют оператор перегрузки *=. Например, можно вызвать следующую функцию для вычисления квадратов alnt и aFloat, двух объектов разных типов:
Square2(int alnt, float aFloat);
Шаблон функций реализуется путем генерирования функции из функции и параметров template. Например, для шаблона функций Square2() рассмотрим данный оператор:
Square2(int alnt, float aFloat);
Приведенный оператор реализует следующую функцию:
Square2(int, float);
ИЗ
template<class ClassX, class ClassY>void Square2(ClassXe x, ClassYS y) и параметров template ClassX и ClassY.
Действительная реализация выполняется компилятором на основе определенного шаблона.
Определение и реализация шаблонов классов
Шаблон классов обеспечивает основу для конструирования и реализации классов с различными типами их членов. Вы определяете шаблон классов способом, похожим на тот, которым конструируется обычный, нешаблонный класс. Разница заключается в том, что члены шаблона классов можно параметризовать. Ниже приведен пример определения шаблона классов:
template<class Т> class MyClass ( public: И конструкторы и деструктор MyClass();П конструктор по умолчанию
MyClass(те newVal); II конструктор копии -MyClass();П деструктор
// функции доступа
Часть II void SetVar(T£ newVal); Те GetVar () const; И дружественные функции ostreame operator«(ostreame os, const MyCla«s<T>6 c) ; private: ’ T mVar;
) ;
templateCclass T>MyClass<T>::MyClass() {}
templateCclass T>MyClass<T>:.MyClass(T6 newVal): mVar(newVal) (}-
templateCclass T>MyClass<T>::-MyClass() (}
' templateCclass T> void MyClassCT>::SetVar(T6 newVal) ■ ( mVar = newVal;
' } templateCclass T> те MyClassCT>::GetVal() const ( return mVar;
} templateCclass T> ostreame MyClassCT>::operatorCC(ostreame os, const MyClassCT>e c)
■ os « "member variable mVar = " « c.mVar « "\n"; return os;
)
Сравните это с обычным определением класса. Существенная разница заключается в том, что теперь члены класса имеют параметризованный тип. Когда член определяется вне определения класса, то вы должны добавлять к определению функции следующий префикс:
templateCclass Т>
Вы также должны параметризовать MyClass следующим образом: MyClassCT>
Реализация шаблона классов происходит путем генерирования определения класса из шаблона классов и параметров. Следующий пример показывает реализацию двух классов, MyClass<int> и MyClass<string>: int main () . ( ' MyClass<int> intObj ;
Sg^Zjintobj . SetVar (5) ;
' " cout « "intObj .GetVar = " « intObj .GetVar () « "\n";
cout « intObj;
MyClassCstring> strObj("This is a string"); cout « "strObj .GetVar = " « strObj .GetVar () « "\n"; cout « strObj ;
■' return 0 ;
Последовательные контейнеры
' ■ Последовательные контейнеры библиотеки STL обеспечивают эффективный последовательный доступ к списку объектов. Библиотека Standard C++ Library предлагает три последовательных контейнера: vector, list и deque.
Контейнер-вектор
' Для хранения и доступа к нескольким элементам часто используются массивы. Элементы в массиве имеют один и тот же тип и получают доступ по индексу. Библиотека STL обеспечивает контейнерный класс vector,
который ведет себя подобно массиву, но более силен и безопасен в использовании по сравнению со стандартным массивом C++.
Vector представляет собой контейнер, оптимизированный для обеспечения быстрого доступа к своим элементам по индексу. Контейнерный класс vector определен в файле заголовка <vector> в namespace sM (см. главу 8). При необходимости класс vector может наращивать себя. Предположим, есть контейнер-вектор для хранения десяти элементов. После заполнения контейнера-вектора десяти объектами он полон. Если добавить к нему еще один объект, то контейнер-вектор автоматически увеличит свою емкость так, чтобы принять 11-й объект. Так определяется класс vector:
template *class I, class А = allocator*!» class vector // члены класса 1;
Первый аргумент (class Т) — это тип элементов в контейнере-векторе. Второй аргумент (class А) — это класс распределителя. Распределители представляют собой диспетчеры памяти, отвечающие за распределение и высвобождение памяти для элементов в контейнере. По умолчанию элементы создаются оператором new() и освобождаются оператором delete(), т.е. конструктор по умолчанию класса Т вызывается для создания нового элемента. Векторы для хранения целых и плавающих можно определить следующим образом: vector*int>vlnts;Н вектор содержит целые элементы
vector<float>vFloats;Н вектор содержит элементы с плажаххцей точкой
Конструкторы
Для использования вектора его необходимо прежде всего создать. При создании объекта вектора от операционной системы получается блок памяти. Размер блока, по крайней мере, достаточно велик, чтобы вместить весь объект vector. Точный размер (называемый емкостью) блока памяти может быть больше, чем тот, что требуется для хранения вектора. При реализации важно решить, исходя из требуемой производительности, какая стратегия распределения памяти лучше подходит для вектора.
Как показано ниже, контейнер vector предлагает несколько конструкторов:
template «class Т, class А = allocator*!» class vector { public: // типы typedef typename A::size_type size_type;II смотри Примечание
(J конструкторы explicit vector(const AS = A());
explicit vector(size_type n, const !S val = !(), const AS = A()); vector(const vectors v) ;
1;
ПРИМЕЧАНИЕ
Ключевое слово typename используется для утверждения, что за ним следует тип. Хотя А является параметром во время компиляции компилятор не знает, что такое А Точное значение A::slze_type не прояснится до тех пор, ПОЙ"!® не реализуется класс шаблонов. Следовательно, компилятор не знает, является ли A::size_type типом или переменуЯ ной-членом. Квалифицируя имя типа ключевым словом typename, вы указываете, что это имя является типом". S7J
A::size_type представляет собой просто имя для беззнакового целого. Оно используется для индексирования элементов контейнера. Вектор можно создать различными способами. Например, этот оператор создает вектор целых без элементов:
vector*int> vlntl;
Следующий оператор создает вектор из 100 целых, каждое из которых инициализируется с помощью МО: vector*int> v!nt2(100);
Можно также инициализировать вектор содержимым другого вектора, например:
vector*int> vlnt3(vlnt2);
Конструктор копии vector копирует все элементы из vlntl в vlnt3. Показанный оператор может пагубно отразиться на производительности, если работа выполняется с очень большими по размеру векторами.
Часть II
Размер и емкость
Вектор вмещает ограниченное количество элементов. Класс vector содержит функции-члены, которые заботятся о получении памяти так, чтобы векторы могли расти, когда это нужно. Такие функции получения памяти объявляются следующим образом:
template <class T, class A = allocator<T» class vector
( public:
// размер size_type
и емкость
max_siz.e () const;
H
максимальное число элементов
size_type
size() const;
П
число элементов в векторе
bool
empty () const ;
П
size = 0;
size_type
capacityO const;
П
размер распределенной памяти
void
reserve(size_type n);
H
резервирование пространства памяти для
П
п элементов
// увеличить размер вектора и добавить элемент voidresize (size_type n, T element = T());
}
Функция-член max_size() возвращает количество элементов наибольшего возможного вектора. Такая информация полезна, если следует убедиться, что можно создать большой вектор.
Вспомните, что под вектор распределяется блок памяти, который может быть больше действительного размера, требуемого создаваемому вектору. Функция capacityO указывает общее количество элементов, которые могут содержаться в распределенной памяти. Если capacityO больше size(), то к вектору можно добавить capacityO—size() элементов без получения дополнительной памяти.
Функцию-член reserve(n) можно вызвать для запроса памяти для п элементов. Операция reserve(n) распределяет блок памяти, достаточно большой, чтобы вместить п элементов. Если затребованный размер памяти меньше или равен текущей емкости вектора, то перераспределения памяти не происходит и емкость вектора не изменяется. В противном случае распределяется новый блок памяти, способный принять возросший размер вектора, и емкость вектора устанавливается в п.
Когда создается вектор, можно указать количество элементов. Это количество называется размером вектора. Размер вектора можно выяснить с помощью функции-члена size(). Значение размера для вектора можно изменить в течение жизни вектора. Функция resize(n) изменяет размер вектора к п элементам. Когда п больше чем capacityO, то компилятор должен распределить больше памяти, чтобы принять вектор. В листинге 6.1 показано, как создаются векторы и как меняются их размеры.
Листинг 6.1. Создание и изменение размеров векторов
#include <iostream>
#include <vector> using namespace std;
typedef vector<int> intVector;
template<class T, class A> void ShowVector(const vector<T, A>& v) ;// отобразить свойства вектора
int main () { intVector vlntl;// определить вектор целых без элементов
cout « "vlntl" « "\n";
ShowVector(vlntl);
intVector vlnt2(3);// определить вектор целых с тремя элементами
cout « "vlnt2(3)" « "\n";
ShowVector(vlnt2);
vlnt2.resize(5, 100);// увеличить размер vlnt2 до 5
И и добавить значение 100 в конец
cout « "vlnt2 after resize(5, 100)\n";
ShowVector(vlnt2);
vlnt2.reserve(10) ;// зарезервировать память для 10 элементов
cout « "vlnt2 after reserve(10)\n";
ShowVector(vlnt2);
Глава 6
return 0; }
И
// Отобразить свойства вектора
И templateCclass Т, class А> void ShowVector(const vector*!, A>6 v) < cout « "max_size() = " « v.max_size(); cout « "\tsize() = " « v.size(); cout « "\t" « (v.empty ()? "empty": "not empty"); cout « "\tcapacity () = " « v.capacity () ; cout « "\n\n";
)
Вывод из листинга 6.1 приведен ниже:
vlntl max_size () vlnt (3) max_size ()
= 1073741823 size() = 0empty capacity() = 0
= 1073741823 size() = 3not emptycapacityO = 3
vlnt after max_size ()
resize(5, 100) = 1073741823 size() = 5not emptycapacityO = 6
vlnt after max_size ()
reserve(10) = 1073741823 sized = 5not emptycapacityO = 10
При определении пустого вектора vlntl память для его потенциальных элементов конструктором по умолчанию не резервируется. Когда определяется вектор vlntl для хранения трех целых, то конструктор распределяет память, как раз достаточную для этих трех элементов. Затем размер vlntl меняется для хранения пяти элементов. Посмотрите на вывод и обратите внимание, что компилятор увеличил размер вектора до шести элементов. У вас значение размера может оказаться другим, если у компилятора другая стратегия распределения памяти. Позже вызывается функция reserve(), чтобы распределить для vlntl больше памяти. В результате этого вызова увеличивается размер vlntl до десяти элементов, но размер сохраняется равным пяти. Дополнительное пространство для пяти элементов буквально зарезервировано для будущего исполь
зования.
ПРИМЕЧАНИЕ
Функция ShowVectorQ определена как функция шаблона, чтобы ее можно было использовать для отображения любого типа вектора. Вообще, если функция предназначена для использования различных типов объектов, то ее следует определять как функцию шаблона.
При вызове функции reserve(n) или resize(n) (где п > capacity()) в целях получения большого объема памяти для вектора должен распределиться новый блок памяти. Тот факт, что память, распределенная для вектора, должна быть непрерывной, подразумевает, что вновь полученный блок должен идти сразу же после предыдущего распределенного блока. Такое бывает, возможно, не всегда, поскольку память, следующая за первым распределенным блоком, уже может оказаться распределенной под другие объекты. В такой ситуации где-то должен распределиться новый блок, который по размеру, по меньшей мере, равен объединенному размеру исходного и дополнительного блоков. Элементы вектора, включая новый элемент, копируются в новую память. В конце всей операции освобождается исходный блок.
Функцию resize() можно также использовать для сокращения размера вектора. Однако функция resize() не влияет на емкость вектора.
Доступ к элементам
От контейнера будет мало проку, если не обеспечить доступ к его элементам. Контейнеры vector сконструированы для обеспечения быстрого доступа к элементам, выполняемого с помощью индекса. Следующий программный код показывает функции доступа к элементам, доступные в классе vector:
template <class Т, class А = allocator*!» class vector
Часть II
public:
/ / типы typedef typename A::referencereference;
typedef typename A::const_referenceconst_reference;
// доступ к элементу referenceoperator[](size_type n) ;
conat_referenceoperator[](size_type n) const;
// n-й элемент
reference const_reference
at(size_type n) ; at(size_type n) const;
H
/ / n-й элемент c проверкой диапазона
I
reference const_reference reference
const_reference
front();H первый элемент
front () const;
back();// последний элемент
back()const;
Обратите внимание, что первые строки создают два typedef: reference и const_reference. По умолчанию inference является ссылкой на объект класса Т, a const_reference является const Т&.
Перегруженный оператор индексации [] обеспечивает доступ к элементам по индексам — точно так же, как получается доступ к элементам в массивах. Оператор не проверяет граничных значений, поэтому за правильность индекса несут ответственность программисты. Попытки получить доступ к элементам с использованием индекса, выходящего за границы допустимого, приведут к непредсказуемым результатам. Однако функцию at() можно использовать для проверки того, находится ли индекс внутри допустимого диапазона, и для возбуждения исключения out_of_range, чтобы использовать блок try/catch для перехвата исключения. Исключение out_of_range определено в <stdexcept>, как included в <vector>. Листинг 6.2 демонстрирует применение оператора индексирования и функции-члена at() для доступа к элементам вектора.
Листинг 6.2. Доступ к членам вектора •include <iostream> •include <vector> using namespace std;
typedef vector<int> intVector;
*Mplate<class T, class A>
Void ShowVector(const vector<T, A>4 v) ;
int main»
IntVector vlnt(3);// определить вектор целых с тремя элементами
Cout « "vlnt(3)" « "\n";
// присвоить значение элементу, используя индексы for (vector<int>: :size_type i = 0; i < vlnt. size (); ++i) vlnt[i] » 5 * i;
// присвоить значение элементу, используя функцию at() try
(
vlnt.at((intVector::size_type)4) = 50; H вне диапазона!
J catch(out_of_range) <
cout « "Index out of range" « endl;
}
ShowVector(vlnt);
vlnt.resize(5, 100);// увеличить размер vlnt2 до 5
И и добавить значение 100 в конец cout « "vlnt after resize(5, 100)\n";
// теперь попробовать доступ к 4-му элементу вновь, используя функцию at() try
(
vlnt.at((intVector::size_type)4) = 50; H теперь в диапазоне
)
Контейнерные классы библиотеки STL Глава 6 catch(out of range) (’
cout « "Index out of range" « endl;}ShowVector(Vint);return 0;}H11 Отобразить свойства вектора//templateCclass T, class A>void ShowVector(const vector<T, A>4 v){// переместиться по вектору, используя индексыcout « "\nelements:\n";for (vector<T, A>: : size_type i = 0; i < v.sizeO; ++i),
cout « v[i] « ", ";" ^
cout « "\nfront() = " « v.frontf); cout « "\tback() = " « v.back();.
cout « "\n\n";■ •
Вывод из листинга показан ниже:.
vlnt(3)'
Index out of range'
elements:'
0, 5, 10, front () = 0 back() = 10,
vlnt after resize (5, 100)/.
elements: 0, 5, 10, 100, 50,„
front() = 0 back» =50 Прежде всего создается вектор vlnt с тремя целыми элементами. Значения элементам можно присвоить, используя оператор индексирования. При попытке использовать функцию at() для присваивания значения несуществующему элементу блок try выполняет проверку диапазона и возбуждает исключение out_of_rang£, если позиция недействительна. Блок try/catch используется для перехвата этого исключения дважды. Исключение появляется в момент незаконного доступа к несуществующему элементу. После изменения размера вектора до пяти элементов второе обращение к функции at() будет успешным. Функция шаблона ShowVector() изменена так, чтобы отображать все элементы в векторе, используя оператор индексирования [].
Итераторы
В предыдущем разделе доступ к элементам вектора выполнялся с использованием оператора индексирования. Однако оператор индексирования может не подходить для определенных контейнеров, таких как list, когда доступ к элементам по индексу неэффективен. STL предоставляет другой метод доступа: итераторы. Итераторы обеспечивают стандартную модель доступа к данным так, чтобы эти контейнеры не должны были прибегать к расширенным операциям доступа к элементам. Каждый контейнер STL использует итераторный класс, который наиболее удобен для оптимизированных операций доступа к элементам. Например, контейнерный класс <vector> определяет несколько функций-членов для поддержки применения итераторов:
template <class Т, class А = allocator<T>> class vector ( public: // типы typedef (implementation defined)iterator;
typedef (implementation defined)conat_iterator;
Часть II
iterator const_iterator iterator
const_iterator
begin();II указывает на первый элемент
begin () const;
end();// указывает на (последний + 1) элемент
end () const;
/ / указывает на reverse_iterator
последний элемент реверсированной последовательности rbegin ();
typedef std::reverse_iterator<iterator> typedef atd::reverse_iterator<const_iterator>
reverse_iterator;
const reverse iterator;
// итераторы
const_reverse_iterator rbegin() const;
II указывает на (последний + 1) элемент реверсированной последовательности reverse__iteratorrend() ;
const reverse iterator rend() const;
Вообще, итераторы — это не указатели. Они реализованы, чтобы действовать как указатели. Итератор работает как указатель на элемент (например, Т) в векторе. Как и другие указатели, итераторы можно увеличивать, уменьшать и разадресовывать. Стандарт C++ требует, чтобы итераторы снабжались операторами приращения и/или убывания (оба префиксных или постфиксных), которые можно было бы использовать для пересечения контейнеров. Когда итератор наращивается, он указывает на следующий элемент в векторе. Когда итератор уменьшается, он указывает на предыдущий элемент в векторе. Итератор можно также разадресовывать для возврата указываемого элемента. Оператор разадресации всегда возвращает элемент, указываемый итератором.
Обычно итератор предоставляет возможность пересекать элементы в векторе от начала до конца. Можно также получать доступ к элементам в обратном порядке, используя оператор reverse_iterator. Элементы, указываемые итератором, можно изменять. Если элементы в векторе не подлежат изменению, то следует использовать const_iterator или const_reverse_iterator. Все четыре варианта определены для класса vector в STL. Говорят, что два итератора равны, если они указывают на один и тот же элемент в одном и том же векторе. Более подробно итераторы рассматриваются в главе 7.
В листинге 6.3 итераторы используются для присваивания значений элементам вектора. Функция ShowVector(), использованная в листинге 6.2, переписана в листинге 6.3 так, чтобы отображать элементы вектора с помощью итераторов.
Листинг 6.3. Доступ к элементам с помощью итераторов
#include <iostream>
#include <vector> using namespace std;
typedef vector<int> intVector;
templateCclass T, class A> void ShowVector(const vector<T, A>S v) ;
int main() (
intVector vlnt(3);// определить вектор целых с тремя элементами
cout « ”vlnt(3)" « "\n";
И присвоить значение элементу в vlnt, используя итератор int i = 0;
for (intVector: :iterator itor = vlnt.begin() ; itor != vlnt.end() ; ++itor)
Глава 6
И перемещаться по вектору, используя итератор cout « "elements displayed using an iterator:\n"; for (vector<T, A>: :const_iterator itor = v.begin() ; itor •= v.end() ; ++itor) cout « *itor « ", " ;
cout « "\n";
II перемещаться по вектору, используя реверсированный итератор cout « "elements displayed using a reverse iterator:\n"; for (vector<T, A>::const_reverse_iterator r_itor = v.rbegin() ;
r_itor < v.rend() ; ++r_itor) cout « *r_itor « ", ";
cout « "\n\n";
Ниже приведен вывод из листинга 6.3:
vlnt(3)
elements displayed using an iterator:
0, 5, 10,
elements displayed using a reverse iterator:
10, 5, 0,
Этот листинг демонстрирует использование итераторов для доступа к элементам. Для присваивания значения каждому элементу в векторе vlnt используется неконстантный итератор. Итератор разадресовывается с помощью оператора разадресации *. Функция ShowVector() отображает элементы вектора, используя и пересылаемый, и реверсный итераторы. Поскольку вы не намереваетесь изменять какие-то элементы в ShowVector(), то в обоих случаях используете итератор const. Данный итератор определен как vector<T, A>::const_iterator (или const_reverse_iterator), чтобы его можно было использовать для любого типа вектора.
Вы, вероятно, заметили довольно непоследовательные стили, используемые для пересечения вектора пересылаемыми и реверсными итераторами. Пересылаемый оператор проверяется на векторе vector::end() с помощью оператора !=
for (vector<T, А>::const_iterator itor = v.begin(); itor != v.end(); ++itor)
Однако для реверсных итераторов используется более простой оператор (<):
for (vector<T, А>::const_reverse_iterator r_itor = v.rbegin();
r_itor < v.rend(); ++r_itor)
Такая непоследовательность указывает на разницу между пересылаемым и реверсным итераторами. Согласно стандарту C++, реализация оператора < не требуется для пересылаемых итераторов, но требуется для реверсных итераторов, хотя поставщики STL могут реализовывать оператор для обоих типов итераторов. Хотя ваш компилятор может поддерживать этот оператор для пересылаемого итератора, его использование сделает программный код менее переносимым, поскольку оператор не является частью стандарта языка C++.
Модификаторы
Элементы можно вставлять или удалять из любой позиции вектора. Функции вставки и удаления называются модификаторами, поскольку они изменяют содержимое вектора. Контейнерным классом <vector> обеспечиваются следующие модификаторы: template <class Т, class А = allocator<T>> class vector { public: // вставка и удаление
И ■вс.театаъ t перед роа и возвратить итератор, указывающий на // вновь вставленный элеиент iterator insert(iterator pos, const Те t) ;
I / вставить n копий t перед pos voidinsert(iterator pos, size_type n, const Те t)
// вставить диапазон элементов перед pos voidinsert(iterator pos, const_iterator i, const-iterator j) ;
voidpush_back (const T6 t);// добавить t в конец
voidpop_back();// удалить последний элемент
158
IJ удалить элемент в позиции роз iteratorerase(iterator pos) ;
// удалить элементы от первого до предпоследнего iteratorerase(iterator first, iterator last);
IJ удалить все элементы voidclear();
}
Когда в конец вектора добавляется новый элемент, может произойти одно из двух. Если емкость вектора больше чем его размер, то распределения памяти не происходит. Новый элемент просто добавляется в конец вектора. Если емкость вектора такая же, как и его размер, то распределяется новый блок памяти, принимающий новый элемент. Как уже говорилось при рассмотрении функции reserve(), получение нового блока памяти может вызвать три затратных с точки зрения времени процесса:
Распределение нового блока памяти, достаточно большого, чтобы принять весь вектор, включая вновь добавленный элемент
Копирование всех элементов в новые позиции памяти
Освобождение старого блока памяти
Для большого вектора эти операции могут занять много процессорного времени и памяти — потенциально дорогостоящее упражнение.
Другая дорогостоящая операция контейнера vector — это вставка и удаление из середины вектора. Когда новый элемент вставляется в середину вектора, все элементы, следующие за этим элементом, должны переместиться, предоставляя ему место. Точно так же, когда элемент удаляется из середины вектора, то все последующие элементы должны подвинуться, чтобы заполнить возникший промежуток. Если ожидается частая вставка или удаление из середины, то рассмотрите возможность использования другого класса, такого как list (описанного в этой главе далее). Листинг 6.4 показывает вставку и удаление элементов векторов.
Листинг 6.4. Добавление и удаление элементов
♦include <iostream>
♦include <vector> using namespace std;
typedef vector<int>intVector;
typedef vector<int>::iterator ivltor;
templateCclass T, class A> void ShowVector(const vector<T, A>S v) ;
int main() (
intVector vlnt (5); H определить вектор целых с пятью элементами cout « "vlnt(5)" « "\n";
/ присвоить значение каждому элементу, используя индексы for (vector<int>: :aize_type i = 0; i < vlnt.size(); ++i) vlnt[i] = 5 * i;
ShowVector(vlnt);
// вставить элемент
cout « "vlnt after insert(vlnt.begin() + 1, 50)\n";
ivltor itor = vlnt. insert (vlnt.begin () + 1, 50);-
ShowVector(vlnt); cout « "Current element is " « *itor « "\n\n";
II вставить 5 элементов cout « "vlnt after insert(vlnt.end(), 5, 30) \n"; vlnt. insert (vlnt.end() , 5, 30);
ShowVector (vlnt) ;
// удалить один элемент из vlnt cout « "vlnt after erase one element\n";
vlnt.erase (vlnt.begin () + 3) ;
ShowVector(vlnt);
Глава 6
И удалить три элемента иэ vlnt cout « "vlnt after erase three elementXn"; vlnt. erase (vlnt .begin () + 3, vlnt.begin() + 6); ShowVector(vlnt);
// вставить несколько элементов из другого вектора intVector vlnt2(2, 0) ;
cout « "vlnt2" « "\n";
ShowVector(vlnt2);
cout « "vlnt2 after insert from vlnt\n";
vlnt2.insert(vlnt2.begin() + 1, vlnt.begin() + 1, vlnt.begin() + 3); ShowVector(vlnt2);
II добавить элемент в конец vlnt2 cout « "vlnt2 after push_back()\n"; vlnt2,push_back(100);
ShowVector(vlnt2);
// удалить элемент иэ конца vlnt2 cout « "vlnt2 after pop_back()\n"; vlnt2-pop_back();
ShowVector(vlnt2);
// очистить vlnt2 cout « "vlnt2 cleared\n"; vlnt2 . clear () ;
ShowVector(vlnt2);
return 0; )
П 11 Отобразить свойства вектора // template<class T, class A> void ShowVector(const vector<T, A>4 v) (
cout « "sized = " « v.sized « "Xtcapacity() “ " « v.capacity() « "\n";
// пройтись по вектору используя индексы cout « "elements:\t";
for (vector<T, A>::size_type i « 0; i < v. sized; ++i) cout « v[i] « ", " ;
cout « "\n\n";
)
Вывод из программы показан ниже:
vlnt(5)
sized = 5 elements:
capacity () = 5 0, 5, 10, 15, 20,
vlnt after insert(vlnt.begin() + 1, 50)
sized = 6capacity () = 10
elements:0, 50, 5, 10, 15, 20,
Current element is 50
vlnt after insert(vlnt.end() , 5, 30)
sized = 11capacity () = 12
elements:0, 50, 5, 10, 15, 20, 30, 30, 30, 30, 30
vlnt after erase sized = 10 elements:
one element capacity () = 12 0, 50, 5, 15, 20, 30, 30, 30, 30, 30,
vlnt after erase sized = 7 elements:
three element capacity () = 12
0, 50, 5, 30, 30, 30, 30,
■■■ Вопросы реализации
ШЫ1 Часть II
vlnt2 sized = 2 elements:
capacity ()
0, 0,
= 2
vlnt2 after
insert from vlnt
sized = 4
capacity ()
= 4
elements:
vlnt2 after
0, 50, 5, push back()
0,
sized = 5
capacity 0
= 8
elements:
vlnt2 after
0, 50, 5, pop back()
o, 100,
sized = 4
capacity ()
= 8
elements:0, 50, 5,
vlnt2 cleared
0,
sized = 0 elements:
capacity ()
= 8
Прежде всего вставляется элемент перед вторым элементом в векторе, что является потенциально дорогим удовольствием. Функция insert() возвращает итератор, указывающий на вновь добавленный элемент. Этот результирующий итератор можно разадресовать для отображения нового элемента. Затем в конец вектора добавляются пять элементов, каждый из которых имеет значение 30. Функция end() возвращает последний элемент плюс 1 так, чтобы вставка элемента перед end() эффективно добавляла его после последнего элемента. Затем дважды вызывается функция erase(). При первом вызове из вектора удаляется один элемент (четвертый элемент со значением 10), при втором — три элемента (с четвертого по шестой). Затем определяется другой вектор с двумя целыми элементами vlnt2. Два элемента из vlnt вставляются перед вторым элементом vlnt2. Элементы можно также добавлять или удалять, используя функции push_back() и pop_back(), как это делается с vlnt2. Когда очищен весь vlnt2, размер вектора сокращается. Однако это не уменьшает вектор, его размер остается неизменным.
Векторные операции
Библиотека STL определяет перегруженные операторы сравнения таким образом, чтобы можно было сравнить друг с другом два вектора одного и того же типа. Перегруженные операторы сравнения часто используются в стандартных алгоритмах. Операторы сравнения также используются в дополнительной векторной операции Swap(), которая применяется для обмена значениями элементов в двух векторах. В контейнерном классе <vector> обеспечены следующие операторы сравнения:
template Cclass Т, class А = allocatorCT» class vector public:
// операции над векторами
void swap(vectors v) ;// обмен элементами между текущим вектором и V
template Cclass Т, class А>
void swap(const vectorcT, A>&vl, const vector CT, A>tv2) ( vl.swap(v2);
}
// операторы сравнения templatecclass T, class A> bool operator==(const vectorcT, A>6 vl, const vectorCT, A>4 v2) ;
templatecclass T, class A>
bool operator! = (const vectorcT, A>6 vl, const vectorCT, A>4 v2) ;
templatecclass T class A>
bool operatorc( const vectorcT, A>6 vl, const vectorCT, A>6 v2) ;
templatecclass T, class A>
bool operatorC=(const vectorcT, A>6 vl, const vectorCT, A>4 v2) ;
templatecclass T, class A>
bool operator>( const vectorcT, A>6 vl, const vectorcT, A>4 v2) ;
templatecclass T, class A>
bool operator>=(const vectorcT, A>6 vl, const vectorCT, A>4 v2) ;
Контейнерные классы библиотеки STL Глава 6
Два вектора, vl и v2, равны (==), если vl.sizeO == v2.size() и vl[n] == v2[n], где п имеет значение от О до vl.size()-l. Псевдо реализация оператора == показана ниже:
template<class Т, class А> bool operator==(const vector<T, A>4 vl, const vector<T, A>S v2) { bool isEqual = false;
if (vl.sizeO ~ v2.size()) { isEqual = true ;
for (vector<T, A>::size_type n = 0; n < vl.sizeO; ++n){
if (vl[n] != v2 [n]){
isEqual = false; break;
} } }
return isEqual; }
Вектор vl меньше вектора v2, если vl лексикографически меньше чем v2. Фраза "лексикографически меньше чем v2" означает, что правильно одно из следующих утверждений:
■ Первый vl[n], который не равен v2[n], меньше чем v2[n]
■ Все vl[n] = v2[n], где и = 0, 1, ..., vl.size-1, но vl.sizeO < v2.size()
Оператор < можно продемонстрировать в следующем фрагменте программного кода: template<class Т, class А> bool operator<(const vector<T, A>S vl, const vector<T, A>S v2) < bool isLess = false; for (vector<T, A>: :size_type n = 0; n < v2.size(); ++n){
if ((n > vl.sizeO) || (vl[n] < v2 [n]))(
isLess = true ; break; ) } return isLess; )
Другие операторы реализуются, базируясь на == и <, как показано ниже: template<class Т, class А> bool operator!=(const vector<T, A>S vl, const vector<T, A>S v2) < return ! (vl = v2) ; } template<class T, class A> bool operator<=(const vector<T, A>S vl, const vector<T, A>6 v2) { return ((vl < v2) || (vl = v2)) ; }
template<class T, class A> bool operator>(const vector<T, A>S vl, const vector<T, A>S v2) { return ! (vl <= v2) ;
} template<class T, class A> bool operator>=(const vector<T, A>6 vl, const vector<T, A>S v2) { return ! (vl < v2) ;
}
А теперь пришло время увидеть перегруженные операторы сравнения в действии. В листинге 6.5 используется несколько подходов к сравнению векторов.
6 Зак. 53
Вопросы реализации Часть II Листинг 6.5. Сравнение векторов ♦include <iostream> ♦include <vector> using namespace std; typedef vector<int>intVector;
template<class T, class A> void Showvector(const vector<T, A>6 v) ; template<class T, class A> void compareVectors(const vectorCT, A>4 vl, const vector<T, A>6 v2); int main() (
intVector vlntl (5); И определить вектор целых с пятью элементами cout « "vlntl(5)" « "\n";
// присвоить значение каждому элементу в vlnt, используя индексы for (vector<int>::size_type i = 0; i < vlntl .size () ; ++i){
vlntl[i] = 5 * i;
) Showvector(vlntl) ;
intVector vlnt2 = vlntl;// определить вектор vlnt2 и
// скопировать элементы из vlntl cout « "vlnt2 (5) " « "\n";
Showvector(vlnt2);
// сравнить vlnt и vlnt2 compareVectors(vlntl, vlnt2);
// добавить элемент в конец vlnt2 cout « "vlnt2 after push_back()\n"; vlnt2.push_back(100); ShowVector(vlnt2);
// вновь сравнить vlnt и vlnt2 compareVectors(vlntl, vlnt2);
// сейчас подкачиваются в vlnt и vlnt2 vlntl.swap(vlnt2); cout « "vlntl after swap\n"; ShowVector(vlntl); cout « "vlnt2 after swap\n"; ShowVector(vlnt2) ; compareVectors(vlntl, vlnt2); return 0;
}
H // Отобразить свойства вектора*
И template<class Т, class А> void ShowVector(const vector<T, A>6 v) { cout « "size() = " « v.sized « "\tcapacity() = " « v.capacityO « "\n"; // отобразить элементы вектора, используя индексы cout « "elements:\t" ; for (vector-cT, A>: :size_type i = 0; i < v.sized; ++i) cout « v[i] « ", "; cout « "\n\n";
}
H 11 сравнить два вектора И template<class Т, class А> void compareVectors(const vector<T, A>4 vl, const vector<T, A>4 v2) {
Глава 6
if (vl = v2){
cout « "vl == v2" ;
} else if (vl < v2)
( cout « "vl < v2" ;
} else <
cout « "vl > v2";
} cout « "\n\n";
Ниже представлен вывод из программы:
vlntl (5) size () = 5capacity () = 5
elements:О, 5, 10, 15, 20,
vlnt (5) size () = 5capacity () = 5
elements:0, 5, 10, 15, 20,
vl == v2
vlnt2 after push_back() sized = 6capacity () = 10
elements:0, 5, 10, 15, 20, 100,
vl < v2
vlntl after swap sized = 6capacity () = 10
elements:0, 5, 10, 15, 20, 100,
vlnt2 after swap sized = 5capacity () = 5
elements:0, 5, 10, 15, 20,
vl > v2
Прежде всего создаются два целых вектора, vlntl и vlnt2, с идентичными элементами, скопированными из одного вектора в другой. При их сравнении обнаруживается, что они эквивалентны — как и ожидалось. Добавление нового элемента в vlnt2 делает vlnt2 больше vlntl. В конце vlntl и vlnt2 обмениваются элементами и vlntl становится больше vlnt2.
Одно из ограничений контейнерного класса vector заключается в стоимости добавления и удаления элементов из середины вектора. Этот недостаток преимущественно конструктивный, поскольку векторы занимают непрерывно распределенные блоки памяти, с тем чтобы обеспечить быстрый последовательный доступ к их элементам. Когда ожидается частое удаление и добавление, то лучшим выбором является контейнерный класс list.
Контейнер-список
list представляет собой контейнер, сконструированный в целях обеспечения оптимальности частых вставок и удалений элементов.
Контейнерный класс list в библиотеке STL определяется в файле <list> в namespace std. Класс list обычно реализуется как двухсвязный список, где каждый узел имеет связь и с предыдущим, и с последующим узлами в списке. Класс list обеспечивает все типы членов и операции класса vector, за исключением индексируемых сарасйуО и reserve().
В следующем листинге перечислены контейнерные члены, предоставляемые классом list:
template <class Т, class А = allocator<T>> class list (
public:
// типы
typedef typename A::size_typesize_type; H размер типа
# / типь итераторов f ссылок typeaef ' rmp-.emerxarxoi aef xnecl r.ypeaef -mp-.enien'LaL-.oi aefxned. typedef stc. reverse j wrator*.- tenxor/ ttypeaef six reverste^..t;erator<conrt ; teii’tor/ r.ypeaef r.ypeuanie i. reference typeaef г.уренапхе i const reference
. ten loj
const_- teratoi reve?'tit - teratoT
conn reverst : вегаЬот reference
court reference
/* конструктора expire-1 -«st const la = 1•
erpxxcxt ^.xst 6 sxxe_ type r const 74 va. = 7 vector const —sx.$ v
const lu =• /
t , ^унг.од» размере.
t!Xfcfe_r.ype S*.Xfc_r^rp€ 000-
mar_s^.xe ., const, sixxe ; const етртл ' const
максимальное числе элементов
числе элементе! 1 списке нхяе == (
// увеличить размер списка у добавить элемент
vox©resXае\sxze_type г 7 element = 7
. достуг 1 элемента* referenceat .ssxze type r.
const_referenceat .S-.^e^type r, const
r-y элемент cпровержои диапазона
reference
cone^^reference reference
const reference
front
front constback ’
backconst
первый элемент
последний элемент
указываем на первые элементуказываем на кпоследний 4 ^; элементуказываем на первые элементуказываем на кпоследний 4 ^; элемент
указываем на первые элемент
указываем на кпоследний 4 ^; элемент
указываем на первые элемент
указываем на кпоследний 4 ^; элемент
/’ итератор*
-terator const. - teracor
- вегавот
couat . teratior
beg„i ;
begxx* / const eno :
enc ’ const.
указываем на первый
reverse _ terawi
const_reverse_ - teraxoi
элемент реверсирюванноу последовательностк rbegxr '.
rbegxi const
. i указывает на .последниу •* L. ■ элемент реверсирюванмоу последовательности reverse - %;егачо?гене -
const^reverse .xeratoi renc ; const
8 вставка у удаохение
/ ' вставить t пере; pos f возвратить итератор ' ‘ указывахкикиу на вновь вставленныр элемент
i teratio?
..nsert ...tera^or p>os const 74 t.
г вставит*
i копит t пере; pos
VO-.C
insert ;; teratoi pos s;-.re_ type i const 74 t.
f / вставит*.
диапазон элементов переел pos
VO-.C
-usert. .L-teratoi pos const . ter&toj . const_ - teratoi j) •
/ ' операции
с о$;онча).ние» массива
xro„.C
риз!_Ьас> const 7.4 t;добавит* t ь конег
VU^C
pop^back ‘удашит* последние элемент
/z удалить
элемент иь позицир pos
-uerator
erase?.: tera^to? pos
/ ' удашит*
элемент* с первого по предпоследнего
-сегаво?
erasie. .itesratoi f..rst _ tera.toi ..ast
// удалит*
все элемент*
vo-.c
c-.eaz '
' ' спуюковые операции
vo_c swap .-.-.st t .vo_c swap .-.-.st t .поменять элемент* межд? текущие списков и S
vo_c swap .-.-.st t .
vo_c swap .-.-.st t .
const ....st <7const ....st <7bool opera tor= ^ const -.„st<? kk>t —.st.;
const ....st <7
const ....st <7
Глава 6
bool
operator•=(const
list*!
A>&
listl.
const
list*!,
A>6
list2);
bool
operator*( const
list*!,
A>4
listl,
■ const
list*!,
A>6
list?);
bool
operator<=(const
list*!.
A>6
list!,
const
list*!,
A>6
list?);
bool
operator» const
list*!
A>&
listl.
const
list*!,
A>6
list?);
bool
operator>=(const
list*!,
A>t
listl,
const
list*!,
A>6
list?);
Распределение памяти для контейнерного класса list более динамично, чем в случае с контейнерным классом vector. Помните что вектор использует непрерывный блок памяти для приема своих элементов. Если элемент удаляется или вставляется в середину вектора, то это оказывает влияние на всю занимаемую память. В контейнере list эта проблеме смягчена.
Благодаря своей расширенной гибкости по управлению памятью, класс list может обеспечить несколько дополнительных операций: вклейки, операции с начальными элементами, сортировку и слияние. В следующих нескольких раздела? эти операции рассматриваются более подробно.
Операции вклейки
Операции вклейки перераспределяю^ элементы из одного списка в другой. Контейнерный класс list обеспечивает три операции вклейте
template<class Т, class h = allocator*!» class list public.
// вставить все элемента x перед *pos в текущем списке void splice(iterator pos list» x) :
I/ вставить все элемента *i ив списка х перед *pos в текущем списке void splice (iterator ров. list» х, iterator i) ;
H вставить элементе от ’first до ’(last-1) из списка х перед *pos // е текущем списке void splice (iterator pos. list» x, iterator first, iterator last); }
Функция splice() удаляет указанные элементы из списка х и вставляет их в текущий список в заданную позицию. Этот процесс просто выполняет операции над указателями, а элементы не копируются. Рассмотрим, что происходит "за кулисами' .
РИСУНОК 6.1Два списка Перес операцией вклейки.РИСУНОК 6.1Два списка Перес операцией вклейки.Предположим, что созданы два списка, ListA и ListB, как показано на рис. 6.1. После выполнения операции c.splice(pos, х. first, last) два списка распределяются, как показано на рис. 6.2.
РИСУНОК 6.1
Два списка Перес операцией вклейки.
РИСУНОК 6.1
Два списка Перес операцией вклейки.
РИСУНОК 6.2.
Эти же списки после операции вклейки.
tf
Порядок существующие элементов гарантируется операцией вклейки неизменным, а распределения (или перераспределения) памяти ь результате вклейки не происходит. В листинге 6.6 показано использование операции вклейки.
Вопросы реализации Часть II Листинг 6.6. Пример операции вклейки #include <iostream> #include <list> using namespace std; typedef list<int>intList;
typedef list<int>::iteratorintListltor;
template<class T, class A> void showList(const list<T, A>6 aList); int main() { // определить список целых с пятью элементами intList ListA(5); intj = 0;
for (intListltor ia = ListA.begin() ; ia != ListA. end (); ++ia) *ia = 5 * j++; cout « "ListA" « "\n"; showList(ListA) ; 11 определить список целых с шестью элементами intList ListB(6); j = 0; for (intListltor ib = ListB.begin() ; ib !- ListB.endO; ++ib) *ib = 100 * j++; cout « "ListB" « "\n"; showList(ListB); 11 Объединить данные! cout « "Splice:\n"; ListA. splice(++ListA.begin() , ListB, ++(++ListB.begin()) , —ListB.endO); cout « "ListA" « "\n"; showList(ListA) ; cout « "ListB" « "\n" ; showList(ListB); return 0; ) H IJ отобразить список элементов И template<class Т, class А> void showList(const list<T, A>6 aList) cout « "size() = " « aList. size () « ":\t"; for (list<T, A>::const_iterator i = aList.begin(); i != aList.end() ; ++i) cout « *i « ", " ; cout « "\n\n"; }
Ниже :
показан
вывод
этого
кода:
ListA size ()
= 5:
0,
5, 10, 15,
. 20,
ListB size ()
= 6:
0,
100,
200,
300,
400, 500,
Splice ListA size ()
= 7:
0,
200,
300,
400,
5, 10, 15,
20
ListB size ()
= 4:
0,
100,
500,
Элементам в списках ListA и ListB значения присваиваются с помощью итератора. Затем третий и четвертый элементы ListB перемещаются в ListA.
Глава 6
Функция splice() проста, но есть один нюанс, который достоин упоминания. Исследуем следующий оператор:
ListA.splice (++ListA. begin () , ListB, ++ (++ListB. begin ()) , —ListB.end()) ;
Этот оператор говорит, что вы будете вклеивать в ListA часть ListB, начиная с третьего элемента в ListB и заканчивая предпоследним элементом ListB. Вы используете два оператора приращения для ListB.begin(), чтобы достигнуть третий элемент ListB. Почему нельзя вместо этого просто использовать ListB.begin + 2? К сожалению, потому, что стандарт C++ не требует реализации оператора + классами итераторов.
Операции с начальными элементами
В отличие от векторов, списки эффективно выполняют вставку и удаление первого элемента. Операции с начальными элементами предоставляют возможность добавлять элементы в список более эффективно, поскольку список не надо пересекать. Данный факт ведет к включению двух операций с начальными элементами — одна добавляет первый элемент, а другая удаляет его:
template(class Т, class А = allocator<T» class list (
public:
void push_front(const T4 t) ;// добавить t в начало
void pop_front();// удалить первый элемент
);
Использование обеих операций с начальными элементами показано в листинге 6.7.
Листинг 6.7. Операции с начальными элементами списка
#include <iostream>
#include <list> using namespace std;
typedef list<int>intList;
typedef list<int>::iteratorintListltor;
template<class T, class A> void showList(const list<T, A>6 aList);
int main() { // определить список целых с пятью элементами intList ListA(5); intj = 0;
for (intListltor ia = ListA.begin () ; ia ! = ListA.end(); ++ia)
cout « "\n\n";
}
Вывод из листинга 6.7 представлен ниже:
ListA sized = 5:0, 5, 10, 15, 20,
First element removed: sized =4:5, 10, 15, 20,
Insert 100 at the beginning: sized = 5:100, 5, 10, 15, 20,
Первый элемент ListA удаляется функцией pop_front(). После того как новый элемент вставляется в начало функцией push_front(), новый элемент становится первым в списке.
Операции с начальными элементами стабильны, т.е. порядок и позиции существующих элементов в списке не изменяются. Операция push_front() должна распределить память для нового элемента, операция pop_front() должна освободить память, занимаемую удаляемым элементом.
Операции sort() и mergeQ
Бывают случаи, когда список требуется отсортировать. Контейнерный класс list обеспечивает следующие две операции sort():
templateCclass Т, class А = allocator<T» class list { public: 11 алгоритмы сортировки void sortd ", template<class Compare> void sort(Compare);
11 реверсировать порядок элементов void reverse();
I/ слить два отсортированных списка void merge(lists x) ; template<class Compare> void merge(lists x, Compare);
);
Нормальная функция sort() использует для сортировки элементов механизм сравнения класса Т. Функция шаблона sort() использует функции сравнения класса Compare.
Функция reverse() реверсирует порядок элементов в списке, но не пытается каким-либо образом отсортировать список.
Функция merge() работает подобно операции splice():
void splice(iterator pos, lists x, iterator x.begin(), iterator x.end());
Однако функция merge() сортирует результирующий список, используя либо встроенный, либо внешний механизм сравнения. Два исходных списка должны сами быть отсортированы, прежде чем вызывать функцию merge(). В противном случае результат может быть неотсортированным. В листинге 6.8 показано, как можно отсортировать списки. Этот листинг также демонстрирует разные результаты слияния сортированных и несортированных списков.
Глава 6
// определить список целых с пятью элементами intList ListA(5) ; intj = 0;
for (intListltor ia = ListA.begin() ; ia ! = ListA.end(); ++ia) *ia = 5 * j++; cout « "ListA" « "\n"; showList(ListA);
11 определить список целых с шестью элементами intList ListB(6); j = 10;
for (intListltor ib = ListB.begin() ; ib != ListB.endd ; ++ib) ‘ib = 2 ‘ j—; cout « "ListB" « "\n"; showList(ListB);
intList ListC = ListA; intList ListD = ListB;
11 слить первый раз без сортировки cout « "Merge unsorted lists:\n"; ListA.merge(ListB);
cout « "ListA" « "\n"; showList(ListA) ; cout « "ListB" « "\n"; showList(ListB);
// реверсировать элементы в ListA cout « "Reverse ListA\n";
ListA.reverse() ; showList(ListA); 11 сортировать и слить cout « "Sort and Merge:\n"; ListC.sort(); ListD. sort () ;
ListC.merge(ListB);
cout « "ListC" « "\n"; showList(ListC); cout « "ListB" « "\n"; showList(ListB);
return 0; }
H 11 Отобразить список элементов И templateCclass Т, class А> void showList(const list<T, A>& aList) { cout « "sized = " « aList.sized « ":\t"; for (list<T, A>::const_iterator i = aList.begin() ; i != aList.end() ; ++i) cout « *i « ", " ; cout « "\n\n";
)
Вопросы реализации Часть II
ListB size () = 0:
Reverse ListA.
sized = 11:10, 12, 14, 16, 18, 20, 20, 15, 10, 5, 0,
Sort and Merge: ListC sized = 11:0, 5, 10, 10, 12, 14, 15, 16, 18, 20, 20,
ListD sized = 0:
Когда сливаются два несортированных списка — ListA и ListB, то элементы из ListB просто добавляются в конец ListA и результирующий ListA получается неотсортированным. Когда список ListA реверсируется, то результат все равно остается неотсортированным. Затем вы сортируете ListC и ListD. При слиянии двух сортированных списков — ListC и ListD — результат получается также отсортированным.
Операции по удалению элементов
Элементы из списка можно удалять. Например, приложение по обслуживанию новостей в Internet может отображать списки новостей на основе профилей пользователей. Когда пользователь меняет свой профиль, то определенные элементы должны из списка удаляться. Контейнерный класс list обеспечивает следующие четыре операции для удаления элементов из списков: template<class Т, class А = allocator<T*> class list { public: // удалить элемент из списка void remove(const Тб t) ; template<class Predicate* void remove_if(Predicate p); // удалить дублирующиеся элементы void unique(); template<class Predicate* void unique(Predicate p); }; Обычная функция removeQ удаляет элементы, которые равны t. Для этого используется перегруженный T::operator==. Функция шаблона remove_if() удаляет элементы, у которых предикат р оценивается как true. Обычная функция unique() удаляет из списка все дублирующиеся элементы. Элементы ет, ет+1, .„, еп, где е( обозначает i-й элемент в списке, считаются дублирующимися, если они идут в списке последовательной группой и если еш == ет+1 ==...==еп. Функция шаблона unique() делает то же самое, за исключением использования предиката р вместо оператора ==. В листинге 6.9 показаны различные операции удаления из списка.
Листинг 6.9. Удаление элементов из списков ♦include <iostream* ♦include <list* using namespace std; typedef list<int*intList;
typedef list<int>::iterator intListltor; template<class T, class A* void showList(const list<T, A>6 aList); int main() ( IJ определить список целых с пятью элементами intList ListA(5); intj = 0;
for (intListltor ia = ListA.begin () ; ia != ListA.end(); ++ia) *ia = 5 * j++; cout « "ListA" « "\n"; showList(ListA);
Глава 6
// удалить элемент из списка ListA ListA.remove(5); cout « "5 removed from ListA:\n"; ShowList(ListA);
11 определить список целых с шестью элементами intList ListB(6); j = 10; for (intListltor ib = ListB.begin() ; ib != ListB.end(); ++ib) *ib = 2 * j-; cout « "ListB" « "\n"; ShowList(ListB);
// слить ListA & ListB и добавить элемент 10 в начало ListA cout « "splice ListA & ListB and add 10 to the beginning:\n"; ListA.splice(++(++ListA. begin ()), ListB); ListA.push_front(10); cout « "ListA" « "\n"; showList(ListA);
// сначала отсортировать ListA ListA.sort();// так, чтобы элементы с одинаковыми значениями были сгруппированы вместе,
cout « "ListA sorted:\n"; showList(ListA);
// удалить дубликаты из ListA ListA.unique () ;
cout « "Duplicates in ListA removed:\n"; showList(ListA); return 0;
)
H 11 Отобразить список элементов И templatecdass Т, class А> void showList(const list<T, A>& aList) ( cout « "sized = " « aList.sized « ":\t"; for (list<T, A>: :const_iterator i = aList.begin() ; i != aList.end() ; ++i) cout « *i « ", "; cout « "\n\n";
}
Вывод из листинга 6.9 показан ниже:
ListA sized = 5:0, 5, 10, 15, 20,
5 removed from ListA: sized = 4:0, 10, 15, 20,
ListB sized = 6:20, 18, 16, 14, 12, 10,
splice ListA 6 ListB and add 10 to the beginning: ListA sized=11:10, 0,10,20,18,16,14,12,10,15,20,
ListA sorted: sized=11:0, 10,10,10,12,14,15,16,18,20,20,
Duplicates in ListA removed: sized=8:0, 10,12,14,15,16,18,20,
При попытке удалить любой элемент со значением 5 из целого списка ListA будет удален второй элемент. Затем вы вставляете все элементы из ListB перед вторым элементом в ListA. Функция splice() не сортирует результирующий список. После того как ListA отсортирован, видно, что есть три элемента со значением 10 и два элемента со значением 20. Дублирующиеся значения удаляются функцией unique().
Часть II
Контейнер-дека
Дека подобна вектору с двумя концами и наследует эффективность контейнерного класса vector в операциях последовательного чтения и записи. Но дополнительно контейнерный класс deque обеспечивает оптимизацию операций с начальными и конечными элементами. Данные операции реализуются подобно тому, как они реализованы в контейнерном классе list, где память распределяется только для новых элементов. Такая особенность класса deque устраняет необходимость перераспределять весь контейнер в новую память, как это необходимо делать с классом vector. Таким образом, deque идеально подходит для приложений, в которых вставка и удаление элементов имеют место как с одного, гак и с двух концов и для которых важен последовательный доступ к элементам. Примером такого приложения является имитатор сборки поездов, где вагоны могут присоединяться к поезду с обеих сторон.
Контейнерный класс deque определяется в файле <deque> в namespace std. Класс deque имеет все векторные функции-члены и операции с начальными членами, как показано з следующем листинге:
template <class Т, class А = allocator<T» class deque (
public:
// типы typedef
typename A::size_type
size_type;
// размер типа
// типы typedef typedef typedef typedef typedef typedef
итераторов и ссылок (implementation defined, may be T*) (implementation defined, may be const std::reverse_iterator<iterator> std::reverse_iterator<const_iterator> typename A::reference typename A::const_reference
i terator ;
T*)const_iterator ;
reverse_iterator;
const_reverse_iterator; reference;
const_reference
// конструкторы
explicit deque (const As = A());
explicit deque(size_type n, const TS val = T() , const AS = AC; vector(const deques dq);
И функции размера
size_typemax_size() const;П максимальное число элементов
size_typesized const;// число элементов в деке
boolempty() const; // sized = 0;
// увеличить размер деки и добавить элемент voidresize(size_type n, T element = TO);
/ / доступ к элементам
reference const_reference
at(size type n) ;
// а-й элемент c
at(size_type n)
const
;// проверкой диапазона
reference const_reference
front(); front () const;
H
первый элемент
reference const_reference
back(); back()const
H
последний элемент
// итераторы
iterator const_iterator
begin(); begin() const;
H
указывает на первый элемент
iterator const_iterator
end(> ; end() const;
//
указывает на (последний + 1) элемент
// указывает на первый элемент реверсированной последовательности reverse_i teratorrbegin();
const_reverse_iteratorrbegind const;
// указывает на (последний + 1) элемент реверсированной последовательности reverse_iteratorrend();
const_reverse_iteratorrend() const;
// вставка и удаление
// вставить t перед роз и возвратить итератор,
// указывапций на вновь вставленный элемент iterator insert(iterator pos, const Ts t) ;
Глава 6
/ / вставить п копий t перед роз voidinsert(iterator pos, size_type n, const IS t>
// вставить диапазон элементов перед pos
voidinsert(iterator pos, const_iterator i, const_iterator j) ;
11 операции с началом и скончанием
void
push_front(const TS t, :
// добавить t з начало
void
pop_front();
// удалить первый элемент
void
push_back(const Is t; .
П добавить t з конец
void
pop_back(),
// удалить последний элемент
// удалить элемент из позиции pos iterator erase(iterator pos) :
// удалить элементы с первого по предпоследний iterator erase(iterator first, iterator last),
// удалить все элементы
voidclear ();
// операции деки
void swap(deque S dq) :// поменять элементы между текущей декой и dq
bool
opera tor= (const
deque-CT,
A>6
dql.
const
deque<T.
A>4
dq2)
bool
operator!=(const
deque<T,
A>&
dql.
const
deque<T,
A>4
dq2)
bool
operator<( const
deque<T,
A>&
dql,
const
deque<T,
A>4
dq2)
bool
operator<=(const
deque<T.
A>6
dql,
const
deque<T,
A>4
dq2)
bool
operator>( const
dequeCT,
A>4
dql,
const
deque<T,
A>&
dq2)
bool
operator>=(const
deque<T.
A>4
dql,
const
deque<T.
A>4
dq2)
Стеки
Одной из наиболее распространенных :труктур данных в программировании является стек. Однако стек не реализован как независимый контейнерный класс. Вместо этого эн реализован как обертка контейнера. Шаблонный класс stack определен в файле <stack> з namespace std.
Стек — это непрерывно распределенный блок, который увеличивается или уменьшается с конца. Элементы в стеке доступны или удаляются только с конца. Вы видели подобные характеристики в последовательных контейнерах, векторах и деках. Фактически любой последовательный контейнер, который поддерживает операции back(), pushbackf) л pop_back(). иожет использоваться для реализации стека. Большинство других контейнерных методов для стека не требуются и, следовательно, стеком не выставляются.
Шаблонный класс библиотеки STL stack сконструирован для хранения объектов любого типа. Единственное ограничение заключается в том. что все объекты должны быть одного и того же типа.
Стек является структурой LIFO (last in, first out — последним вошел — первым вышел). Это как переполненный лифт: первый вошедший человек продвигается к стене, а последний вошедший стоит рядом с дверью. Когда лифт достигает назначенного этажа, то последний вошедший выходит первым. Если кто-то хочет покинуть лифт ранее, то все люди между ним и дверью должны уступить ему дорог/, вероятно, выйдя из лифта и затем вернувшись обратно.
По соглашению открытый конец стека часто называют вершиной стека, а операции, проводимые над стеком, часто называют проталкиванием и выталкиванием. Класс stack наследует эти распространенные термины.
ПРИМЕЧАНИЕ
Йг-Класс библиотеки STL stack — это не то же самое, что механизм стека, используемый компиляторами И Ц$6ми системами, в которых стеки могут содержать объекты разных типов. Однако базовая
Интерфейс стека очень прост:
tenplate <class Т, class Container = deque<T>> class stack {
public:
Вопросы реализации Часть II
typedef typename Container::value_type value_type; typedef typename Container::size_type size_type; typedef typename Container container_type; protected: Container c; public: explicit stack(const Containers = Container()) ; bool empty () const { return c.empty () ; ) size_type size() const ( return c.sizeO; } value_type6 top() { return c.back(); } const value_typeS top() const { return c.back(); } void push(const value_type6 x) { c.push_back(x); } void pop() { c.pop_back () ; } };
T — это тип элементов, содержащихся в стеке. Контейнерный класс может быть любым контейнером, который поддерживает операции с начальными элементами. По умолчанию используется контейнерный класс STL deque. Термин "операции с начальными элементами" проходит по всем контейнерным классам.
Как видно, функция top() является просто интерфейсом к функции контейнера back(). Точно так же функции push() и рор() — это на самом деле функции push_back() и pop_back() контейнера. Функция empty() указывает, содержит ли стек элементы вообще. Функция size() возвращает количество элементов, содержащихся в стеке. В листинге 6.10 показано несколько базовых стековых операций.
Листинг 6.10. Стековые операции
#include <iostream> #include <stack> using namespace std;
template-Cclass T, class C> void ShowStack(stack<T, 06 aStack);
int main() ( // создать стек целых stack<int> slnt; cout « "Stack slnt created:\n"; ShowStack (slnt) ; 11 протолкнуть элемент в стек for (unsigned int i = 0; i < 5; ++i) slnt.push(i * 2) ; cout « "slnt:\n"; ShowStack (slnt) ; 11 изменить верхний элемент slnt. top () = 100; cout « "top element modified:\n"; ShowStack(slnt); // получить все элементы cout « "Show all elements:\n"; while (!slnt.empty()) cout « slnt. top () « ", "; slnt.pop() ;
) cout « "\n\n"; return 0; }
H 11 Отобразить свойства стека И template-Cclass Т, class О void ShowStack (stack-CT, 06 aStack) (
cout « "size = " « aStack.size() ;
Глава 6
if (laStack.empty ())
cout « "\ttop = " « aStack. top() ; cout « "\n\n";
Вывод из листинга 6.10 показан ниже:
Stack slnt created: size = 0
slnt: size = 5slnt: size = 5top = 8top = 8
slnt: size = 5
slnt: size = 5
top = 8
top = 8
top element modified: size = 5top = 100
Show all elements: 100, 6, 4, 2, 0, Создается пустой стек slnt, и в него проталкиваются пять элементов. Стек slnt автоматически получает больше памяти при увеличении своего размера. Доступ к последнему элементу можно получить, используя функцию top(). Изменять можно только последний элемент в стеке. Для доступа к непоследнему элементу необходимо удалить все элементы с вершины вплоть до данного, используя функцию рор(), — приблизительно то же самое, что происходит в переполненном лифте. Прежде чем получать доступ к элементам, необходимо также убедиться, что стек не пуст, в противном случае могут возникнуть проблемы с незаполненным стеком. Например, одни компиляторы могут в таких случаях возбуждать исключение, тогда как другие компиляторы молча игнорируют ситуацию.
Все мы видели печально знаменитую ошибку "переполнение стека". Что она означает? В теории стек переполниться не может, поскольку он просто принимает столько элементов, сколько необходимо. Однако на практике доступная память всегда ограничена. Когда вы ее исчерпываете, то получаете переполнение стека или другую связанную с памятью ошибку.
Возможно, вы заметили довольно странное объявление конструктора stack: он может принимать контейнер как параметр. Например, вы можете определить и инициализировать стек, используя следующий оператор:
stack<int> intStack(intVector) ;// где intVector определен как
// vector<int> intVector;
Этот оператор инициализирует intStack из intVector, т.е. код создает intStack и копирует в него все элементы из intVector.
Как обычно, для класса stack определено несколько перегруженных шаблонных операторов сравнения. Данные операторы в общем случае являются обертками для соответствующих операторов сравнения контейнеров:'
template <class Т, class Container>
bool operator==(const stacker, Containers x, const stacker, Containers y) ; template <class T, class Container>
bool operator< (const stacker, Containers x, const stacker, Containers y) ; template <class T, class Container
bool operator! = (const stacker, Containers x, const stacker, Containers y) ; template <class T, class Container
bool operator (const stacker, Containers x, const stacker, Containers y) ; template <class T, class Container
bool operator>= (const stacker, Containers x, const stacker, Containers y) ; template <class T, class Container
bool operator<= (const stacker, Containers x, const stacker, Containers y) ;
Очереди
Очередь — это еще одна широко используемая структура в программировании. Элементы добавляются в очередь с одной стороны, а извлекаются с другой. Классическая аналогия такова: стек — это как стопка подносов с салатом в буфете. Вы добавляете в стек, помещая поднос на вершину (проталкивая стек вниз) и берете из стека, выталкивая поднос сверху (тот, что был добавлен совсем недавно).
Очередь — это как очередь в театр. Вы становитесь в конец очереди и уходите из очереди в ее начале. Такая структура известна под названием FIFO (first in, first out — первым вошел — первым вышел). Стек
Часть II
же — это структура LIFO (last in, first out — последним вошел — первым вышел). Конечно, иногда бывает, что вы второй в длинной очереди в супермаркете, когда кто-то начинает новую очередь и берет последнего человека в очереди — превращая то, что должно быть очередью FIFO, в стек L1FO и заставляя вас скрипеть зубами от раздражения.
Как stack, queue реализуется в виде обертки к контейнеру. Контейнер должен поддерживать операции front(), back(), push_back() и pop_front(). Обратите внимание, что характеристики очереди не соответствуют классу vector, поскольку для этого класса нет операции pop_front(). Шаблонный класс queue определен в файле <queue> в namespace std. Интерфейс шаблонного класса queue выглядит довольно знакомо, как показывает следующий программный код:
template <class Т, class Container = deque<T> > class queue { public:
typedef typename Container::value_type value_type;
typedef typename Container::size_type size_type; typedef typename Container container_type;
protected:
Container c;
public: explicit queue(const Containers = Container()); bool empty() const { return c.empty () ; } size_type sized const { return c. sized; } value_typeS front О { return c.front О; } const value_type6 frontd const ( return c.front(); } value_types back О { return c.back О; ) const value_type6 back() const ( return c.back(); } void push(const value_type6 x) ( c.push_back(x); } void pop О ( c ,pop_f ront() ; }
);
Этот код показывает сходство между шаблонными классами queue и stack. Обычные операции push() и рор() используются для обертки контейнерных операций push_back() и pop_front(). В листинге 6.11 демонстрируются базовые операции очереди.
Листинг 6.11. Базовые операции очереди
♦include <iostream>
♦include <queue> using namespace std;
template<class T, class C> void ShowQueue(const queue<T, 06 aQueue);
int main() (
// создать очередь целых queue<int> qlnt; cout « "Queue qlnt created:\n”; ShowQueue(qlnt);
// протолкнуть элементы в очередь for (unsigned int i = 1; i < 5; ++i) qlnt.push(i * 2) ;
cout « "qlnt:\n"; ShowQueue(qlnt);
// изменить первый и последний элементы qlnt. front () = 20;
qlnt. back () = 30; cout « "The first and last elements modified:\n"; ShowQueue(qlnt);
II удалить из очереди первый элемент qlnt.pop () ; cout « "After one pop() operation\n"; ShowQueue(qlnt);
Глава 6
return 0; }
И // Отобразить элементы очереди И template<class Т, class О void ShowQueue (const queued, CS aQueue) { '
cout « "sized = " « aQueue. size () ;
if (!aQueue.empty()) {
cout « "\tfrontd = " « aQueue.front() ; cout « "\tback() = " « aQueue . back () ;
} cout « "\n\n";
}
Вывод из листинга 6.11 показан ниже:
Queue qlnt created: size () = О
qlnt: size () = 4front () = 2 back() = 8
The first and last elements modified:
sized = 4front () = 20back() = 30
After one pop() operation sized = 3front () = 4back() = 30
Элементы проталкиваются в очередь. Как и стек, очередь может получать память при увеличении своего размера. Вы можете получать доступ к первому и последнему элементам в очереди, используя операции frontQ и back() После того как первый (верхний) элемент из очереди удален с помощью функции рор(), следующий элемент становится новым первым элементом.
И вновь для сравнения двух очередей определяются перегруженные операторы сравнения:
template <class Т, class Container
bool operator==(const queued, Containers x, const queued, Container>s y) ; template <class T, class Container
bool operator< (const queued, Containers x, const queued, Containers y) ; template <class T, class Container
bool operator! = (const queued, Containers x, const queued, Containers y) ; template <class T, class Container
bool operator (const queued, Containers x, const queued, Containers y) ; template <class T, class Container>
bool operator>= (const queued, Containers x, const queued, Containers y) ; template <class T, class Container
bool operator<= (const queued, Containers x, const queued, Containers y) ;
ПРИМЕЧАНИЕ
Как и класс стека библиотеки STL, класс очереди — это не то же самое, что механизм очереди, используемый компиляторами и операционными системами, где очереди могут содержать различные типы объектов. Однако базовая функциональность очень похожа.
Приоритетные очереди
Приоритетная очередь — это очередь, элементам которой присвоен приоритет. Шаблонный класс priority_queue также определен в файле <queue> в namespace std. Элемент с высшим приоритетом находится в начале очереди. Интерфейс для шаблонного класса priority_queue выглядит следующим образом:
template <class Т, class Container = vectord>, class Compare = less-CContainer: :value_type> >
class priority_queue
(
Часть II
public:
typedef typename Container::value_type value_type;
typedef typename Container::size_type size_type;
typedef typename Container container_type;
protected:
Container c;
Compare comp;
public:
explicit priority_queue(const Compares x = Compare(),
const Containers = Container())
: c(Container), comp(Compare)();
template <class Inputlterator>
priority_queue(Inputiterator first, Inputiterator last,
const Compares x = Compare(), const Containers = Container());
bool empty () const ( return c. empty () ; }
size_type size() const ( return c.size(); }
const value_typeS top() const { return c.front(); }
void push(const value_typeS x) ;
void pop();
};
Элементы приоритетной очереди проталкиваются и выталкиваются с одного конца. Любая последовательность, которая поддерживает операции front(), push_back() и pop_back(), может использоваться как контейнер для приоритетных очередей. По умолчанию используется vector.
Приоритетная очередь должна обеспечивать способ сравнения приоритетов своих элементов. По умолчанию используется оператор <. Операция рор() возвращает элемент, который имеет высший приоритет. Элементы, которые имеют одинаковый приоритет, выстраиваются на вершине в том порядке, в котором они изначально вставлялись — поэтому и очередь. Предположим, что в приоритетной очереди есть два элемента — А и В, причем А был вставлен перед В. Если оба элемента имеют приоритет пять, то А будет вверху, когда не будет элементов с более высоким приоритетом. Когда А вытолкнется из очереди, то В станет первым элементом на вершине.
Когда элемент проталкивается в приоритетную очередь, он помещается после всех элементов с высшим или равным приоритетом и перед всеми элементами с более низким приоритетом. То, как воплощается данный процесс размещения, определяется реализацией.
Связанные контейнеры
Тогда как последовательные контейнеры сконструированы для последовательного и произвольного доступа к элементам с помощью индекса или итератора, связанные контейнеры применяются для быстрого произвольного доступа к элементам с помощью ключей. Библиотека Standard C++ Library обеспечивает четыре ассоциативных контейнера: запись, множественную запись, набор и множественный набор.
Вы видели, что вектор подобен расширенной версии массива. Он имеет все характеристики массива, а также еще несколько дополнительных механизмов. К сожалению, вектор имеет один существенный недостаток, характерный для массивов: не обеспечивается произвольный доступ к элементам через ключевые значения, отличные от индекса или итератора. С другой стороны, связанные контейнеры обеспечивают быстрый произвольный доступ, базируясь на ключевых значениях.
Контейнер-запись
Первый связанный контейнерный класс — это тар. Он определен в файле <тар> в namespace std. Перед тем как разбираться в деталях записей, вспомним, как организован массив и как происходит доступ к нему. В листинге 6.12 показан массив объектов Product.
Листинг 6.12. Листинг Product
#include <iostream>
#include <string> using namespace std;
class Product
(
public:
Product(string newName = "", double newPrice = 0, int newStockLevel = 0) :
Глава 6
mName(newName), mPrice(newPrice), mStockLevel(newStockLevel)
(}
void
SetName(string newName)(mName= newName;
}
void
SetName(int newStockLevel)(mStockLevel = newStockLevel;}
void
SetPrice(double newPrice)(mPrice= newPrice;}
string
GetName()const(return mName;}
int
GetStockLevel()const(return mStockLevel;}
double
GetPrice()const(return mPrice;
}
friend ostreamS operator«(ostreamS os, Products p) ( os « "Product: " « p.mName « "\tPrice: " « p.mPrice « "\tStock on hand: " « p.mStockLevel; return os;
}
private:
stringmName;
intmStockLevel;
doublemPrice;
}; int main() { Product Pen("Pen", 5.99, 58); Product TableLamp("Table Lamp", 28.49, 24); Product Speaker("Speaker", 24.95, 40);
Product productArray[3] = (Pen, TableLamp, Speaker};
cout « "Price list:\n"; for (int i = 0; i < 3; ++i) cout « productArray[i] « "\n";
for (int j = 0; j < 3; ++j) { if (productArray[j].GetName() == "Speaker")
cout « "\nSpeaker's price is " « productArray [j ] . GetPrice () « "\n"; } return 0;
Вывод из листинга 6.12 показан ниже:
Price list:
Product: PenPrice: 5.99Stock on hand:
58
Product: Table LampPrice: 28.49Stock on hand:
Product: SpeakerPrice: 24.95Stock on hand:
Speaker's price is 24.95
24
40
Используя эту программу, вы можете напечатать прайс-лист продукции для всего склада. Но что, если нужно знать только цену динамиков? Для получения такой информации необходимо написать код, подобный следующему:
for (int j = 0; j < 3; ++j) {
if (productArray[j].GetName() = "Speaker")
cout « "Speaker's price is ” « productArray [j ] .GetPrice () « "\ n"; }
Такой процесс довольно неэффективен и запутан. Не лучше ли сделать так?
cout « "Speaker's price is " « productArray ["Speaker"] .GetPrice () « "\ n";
Очевидно, что с обычными массивами такого сделать нельзя. К счастью, это можно сделать с контейнерным классом тар библиотеки STL. Изменим листинг 6.12 так, чтобы он хранил списки продуктов в тар и с его помощью можно было получить цену динамиков. Такой цели достигает программа в листинге 6.13.
180
Листинг 5.13. Использование map для хранения перечня продуктов
#include <iostream>
#include <strinq> tinclude <map> using namespace std
class Product ( public:
Product”. mName.'"New Product"), mStockLevel (0) , mPrice(O) (}
Product(string newName, double newPrice = 0, int newStockLevel = 0) :
mName(newName), mPrice(newPrice), mStockLevel(newStockLevel)
(}
void void
void
string int
SetName(string newName)(mName= newName;}
SetName(int newStockLevel)(mStockLevel = newStockLevel;}
SetPrice(double newPrice)(mPrice= newPrice;}
GetNamef)const(return mName;}
SetStockLevel()const(return mStockLevel;}
double
friend
GetPrice0const(return mPrice;
^streams operator«(ostreamS os. Products p)
}
f
os « "Product: " « p.mName « "\tPrice: " « p.mPrice « "\tStock on hand: " « p.mStockLevel; return os, private. stringTName-
intmStockLevel;
doublemPrice:
} : int main(; { Product Pen{"Pen". 5 99, 58), Product VableLamp("Table Lamp", 28.49, 24); Product Speaker("Speaker", 24 95, 40); map<string, Product» productMap; productMap'Pen GetName ()] = Pen; productMap[TableLamp.GetName()] = TableLamp; productMap[Speaker.GetName()1 = Speaker; cout « 'Speaker s price is " « productMap["Speaker"].GetPrice() « '\n"; return ), )
Вывод из листинга 6 13 показан ниже:
Speaker's price is 24.95
В листинге 6.13 показана важная характеристика контейнерного класса тар: доступ к элементам записи можно получить по ключевым значениям. Это подразумевает, что контейнер тар должен иметь класс Key. Контейнерный класс тар должен также уметь сравнивать ключевые значения, используя либо встроенные операторы сравнения класса Key, либо внешний объект сравнения. Как и ожидается, контейнер тар действительно имеет класс Key, который обеспечивает данный механизм:
template<class Key, class Т, class Compare = less<Key>, class A = Allocator<T>> class map
(
/ / типы
typedef Comparekey_compare;
typedef Aallocator_type;
difference_type; reference;difference_type; reference;typedef typename A: :deference_type typedef typename A::reference
difference_type; reference;
difference_type; reference;
Глава 6
cypedet cypename А::const_referenceconst_reference.
Первый аргумент Key является гипом ключей для записи, второй — типом значения элемента. В примере записи Product ключом является идентификатор продукта типа string, а значением элемента — тип Product. Третий аргумент является классом Compare, который можно использовать для сравнения значений двух ключей. Четвертый аргумент А — это распределитель, который заботится об управлении памятью контейнеров. В большинстве случаев он должен быть адекватным Allocator<T> по умолчанию. Мар содержит последовательность лар ключ/значение, где каждый ключ уникален. Это позволяет осуществить точное распределение злементов в записи. Кроме того, вы должны уметь отсортировать элементы в записи согласно их ключевым значениям. Если не указывать класс Compare, го будет использоваться функция сравнения <. Оператор < можно перегрузить для сравнения двух объектов вашего класса. В примере Product для сравнения идентификаторов продуктов используется operator<(string, string). Если вы решите упорядочить список продуктов по наименованиям, то можете определить класс Compare и обеспечить его записью Product. Более подробно определение класса сравнения описано в главе 7
Конструкторы и деструктор
Следующий фрагмент программного кода показывает конструкторы и деструктор для контейнерного класса тар:
templa^e<class Key; class Т, class Compare = less<Key>, class A = Allocator<T» class map {
// конструкторы и деструктор j/ создать пустую запись explicit mapyconst Compared cmp = Compare(), const A# = A ).
; ' создать мар и копировать с первого по предпоследний элементы из Inputiterator templare<class Inputlterator>
map ='Input Iterator first, Inputiterator Last, const Compares cmp = Compare (), const AS = Ain
// конструктор копии map(const maps m) ;
11 деструктор
-map(:
' / оператор присваивания maps operator=(const maps;,
Конструктор-член создает объект map с исходным классом Compare и, или классом распределителя. (Сейчас мы не будем затрагивать шаблонный конструктор, поскольку он вовлекает вводной итераторный класс. Итераторы рассматриваются в этой главе далее.) Конструктор копии, деструктор и оператор присваивания являются стандартными.
Размеры
Как и последовательные контейнеры, класс тар обеспечивает несколько функций-членов, касающихся определения размеров записи:
template<class Key, class Т, class Compare = less<Key> class A = Allocator<T» class map (
public:
H типы
typedef typename A::size_type
SJ-ze_type;
H размеры
size_typesizedconst;
/j количество элементов
size_type max_size()const;
// размер наибольшего
boolempty()const
{ return (size() == 0) ; )
Все эти операции аналогичны операциям в классе vector
Часть II
Итераторы
Как и другие стандартные контейнеры, записи можно пересекать, используя итераторы:
template<class Key, class Т, class Compare = less<Key>, class A = AllocatorCT» class map
(
public:
// типы
typedef Keykey_type;
typedef Tmapped_type;
iterator;const_iterator;reverse_iterator;const reverse iterator;iterator;const_iterator;reverse_iterator;const reverse iterator;typedef (implementation defined)
iterator;
const_iterator;
reverse_iterator;
const reverse iterator;
iterator;
const_iterator;
reverse_iterator;
const reverse iterator;
typedef (implementation defined) typedef std::reverse_iterator<iterator> typedef std::reverse_iterator<const_iterator>
// итераторы iterator const_iterator iteratorconst iterator// итераторы iterator const_iterator iteratorconst iteratorbegin();begin () const;end() ;end() const ;begin();begin () const;end() ;end() const ;// указывает на первый элемент11 указывает на (последний + 1) элемент// указывает на первый элемент11 указывает на (последний + 1) элемент
// итераторы iterator const_iterator iterator
const iterator
// итераторы iterator const_iterator iterator
const iterator
begin();
begin () const;end() ;
end() const ;
begin();
begin () const;end() ;
end() const ;
// указывает на первый элемент
11 указывает на (последний + 1) элемент
// указывает на первый элемент
11 указывает на (последний + 1) элемент
// указывает на первый элемент реверсированной последовательности
rbegin () ;rbegin() const;rbegin () ;rbegin() const;reverse_iterator const_reverse_iterator
rbegin () ;
rbegin() const;
rbegin () ;
rbegin() const;
// указывает на (последний + 1) элемент реверсированной последовательности reverse_iteratorrend();
const_reverse_iterator rend() const; }; Each element in a map is a struct pair defined in namespace std as follows: template<class First, class Second> struct pair ( // другие члены struct Firstfirst;
Second second;
В примере записи Product можно изменить цену любого продукта и напечатать прайс-лист, как показано в листинге 6.14.
Листинг 6.14. Доступ к элементам записи #include <iostream> #include <string> #include <map> using namespace std; class Product { public: Product():mName("New Product"), mStockLevel(0), mPrice(O) () Product(string newName, double newPrice = 0, int newStockLevel = 0) : mName(newName), mPrice(newPrice), mStockLevel(newStockLevel) ()
void SetName(string newName){ mName= newName;}
void SetName(int newStockLevel) { mStockLevel = newStockLevel; } void SetPrice(double newPrice) ( mPrice= newPrice;}
string GetNameOconst{ return mName;}
intGetStockLevel() const( return mStockLevel; }
double GetPriceQconst( return mPrice;}
friend ostream! operator«(ostream! os, const Product! p) < os « "Product: " « p.mName « "\tPrice: " « p.mPrice « "\tStock on hand: " « p.mStockLevel; return os;
Глава 6
)
private: stringmName;
intmStockLevel;
doublemPrice;
);
// шаблонная функция для отображения всех элементов записи template<class Т, class А> void ShowMap(const map<T, A>6 m) ;
int main()
Product Pen("Pen", 5.99, 58);
Product Lamp("Lamp" , 28.49, 24);
Product Speaker("Speaker", 24.95, 40);
map<string, Product> productMap; productMap[Pen.GetName()] = Pen; productMap[Lamp.GetName()] = Lamp; productMap[Speaker.GetName()] = Speaker;
ShowMap(productMap); return 0 ;
)
//
// отобразить все элементы записи // template<class Т, class А> void ShowMap(const map<T, A>& m) ( cout « "Map elements:\n"; for (map<T, A>::const_iterator ci = m.begin(); ci != m.end(); ++ci) cout « ci->first « "\t" « ci->second « "\n";
cout « "\n\n"; }
Вывод из листинга 6.14 показан ниже:
Map elements:
LampProduct: LampPrice: 28.49Stock on hand: 24
PenProduct: PenPrice: 5.99Stock on hand: 58
Speaker Product: SpeakerPrice: 24.95 Stock on hand: 40
У вас есть определенный по умолчанию конструктор для класса Product. Это не строго необходимо, но требуется для конструирования productMap, поскольку productMap использует для распределения памяти под свои элементы конструктор по умолчанию Product. В productMap элементам присваиваются три пары ключ/значение. Шаблон функций ShowMapO представляет собой функцию общего назначения, которую можно использовать для отображения всех элементов для любой записи. Для доступа к элементам здесь используется итератор. Поскольку элементы в записи являются парами, то first и second используются для возврата их пар ключ/значение.
Доступ к элементу
Контейнерный класс мар предлагает несколько функций, которые можно использовать для доступа к элементам по их ключевым значениям. Для прямого получения элемента можно использовать оператор индексирования [] или можно использовать функцию-член find() для получения указателя (итератора) на желаемый элемент. Следующий программный код показывает функции доступа, обеспечиваемые контейнерным классом мар:
template<class Key, class Т, class Compare = less<Key>, class A = Allocator<T>>
class map (
public:
11 индексация
Вопросы реализации Часть II mapped_types operator[](const key_type6 к); // другие операции // число элементов с ключом к, возвратить 0, если не найдены size_typecount(const key_type6 к) ;
H найти элемент с ключом к, возвратить мар:end О, если не найдены iteratorfind(const key_typel к) ;
const_iterator find(const key_typel k) ;
// найти первый элемент с ключевым значением большим или равным ключу к iteratorlower_bound(const key_typel к) ;
const_iterator lower_bound(const key_type6 k) const; // найти первый элемент с ключом большим к iteratorupper_bound(const key_typel к) ;
const_iterator upper_bound(const keytypeS k) const; // найти все элементы с ключом к pair(iterator, iterator)equalrange(const key_types k);
pair(const_iterator, const_iterator) equal_range(const key_type6 k) const; 1;
Вы уже видели, что можете получить доступ к элементу по его ключу. Что произойдет, если указать ключевое значение, которое не существует? Рассмотрим пример:
Product Pen("Pen", 5.99, 58); Product TableLamp("Table Lamp", 28.49, 24); Product Speaker("Speaker" , 24.95, 40); map<string, Product> productMap; productMap[Pen.GetName()] = Pen ; productMap[TableLamp.GetName()] = TableLamp; productMap[Speaker.GetName()] = Speaker; cout « productMap["Speaker"] « "\n";// здесь без проблем
cout « productMap["Cup"] « "\n";// а что случилось здесь???
Последний оператор создает новый продукт, используя конструктор по умолчанию класса Т — Product в данном примере — и вставляет его в запись. Если конструктор по умолчанию не определен, то произойдет ошибка компиляции.
Функция find() с другой стороны, возвращает map::end(), когда ключ не найден. Она не создает элемент.
В листинге 6.15 пример с записью Product изменяется еще раз и выполняется проверка операций доступа к элементам. Замените функцию main() в листинге 6.14 программным кодом из листинга 6.15.
Листинг 6.15. Доступ к элементам с помощью индексов и итераторов int main () { Product Pen("Pen", 5.99, 58); Product TableLamp("Table Lamp", 28.49, 24); Product Speaker("Speaker", 24.95, 40); map<string, Product> productMap; productMap[Pen.GetName()] = Pen; productMap[TableLamp.GetName()] = TableLamp; productMap[Speaker.GetName()] = Speaker; ShowMap(productMap); // показать цену Speaker cout « "Speaker's price is " « productMap["Speaker"].GetPrice() « "\n"; // изменить цену Pen productMap["Pen"] .SetPrice (productMap["Pen"] .GetPriceO * 1.10) ; cout « "Pen's price has been changed to " « productMap! "Pen"] .GetPrice () « "\n"; // показать различные функции доступа, имеющие отношение к Реп cout « "Number of Pens is " « productMap.count("Pen") « "\n"; map<string, Product>::iterator ci = productMap.lower_bound("Pen"); cout « "First Pen is " « ci->second « "\n";
Глава 6
ci = productMap.upper_bound("Pen");
cout « "Next to Pen is " « ci->second « "\n";
// попытаться получить доступ к несуществующему элементу cout « "Try Rubber: " « productMap["Rubber"] « "\n";
ShowMap(productMap);
// показать функции доступа к несуществующему элементу cout « "Number of Red Pens is " « productMap. count ("Red Pen") « "\n";
ci = productMap.lower_bound("Red Pen");
cout « "First Red Pen is " « ci->second « "\n";
ci = productMap.upper_bound("Red Pen");
cout « "Next to Red Pen is " « ci->second « "\n";
return 0;
Вывод из измененного программного кода представлен ниже:
Map elements:
PenProduct: PenPrice:
5.99
Stock
on hand:
58
Speaker Product: Speaker
Price:
. 24.95
Stock on hand: 40
Table LampProduct: Table
Lamp
Price:
28.49
Stock on
hand:
24
Speaker’s price is 24.95
Pen ’ s price has been changed to
Number of Pens is 1
6.589
First Pen is Product: Pen
Price:
6.589
Stock on
hand: 58
Next to Pen is Product: Speaker
Price:
24.95
Stock on
hand: 40
Try Rubber: Product: New Product Number of Red Pens is 0
Price:
0
Stock on
hand:
0
First Red Pen is Product: Speaker
Price:
24.95
Stock on
hand:
40
Next to Red Pen is Product: Speaker
Price:
24.95
Stock on
hand:
40
Элементы записи можно изменить с помощью оператора индексирования []. Например, цена ручек величивается на 10%, если используется функция SetPrice() класса Product. Вы показываете количество лементов с ключевым значением "Реп", используя функцию count(). Здесь имеется только один элемент, вы знаете, что никогда не будете иметь более одного элемента с одним и тем же ключевым значением, функция Iower_bound() ищет продукт "Реп", и функция upper_bound() дает нам элемент с ключевым зна- ением, превышающим значение "Реп". Это динамик.
Затем вы пытаетесь найти продукт с ключевым значением "Rubber". Он не существует, поэтому созда- гся новый продукт. Для этого используется конструктор по умолчанию класса Product. Функция count() ля несуществующего ключевого значения возвращает 0, указывая, что продукта с таким ключевым зна- ением не существует. Обратите внимание, что здесь функция count() не создает новый элемент в записи, функция lower_bound() для данного несуществующего ключа возвращает элемент, который имеет следую- iee ключевое значение (в данном случае динамик). Функция upper_bound() работает точно так же.
Может возникнуть ощущение, что функции lower_bound(), upper _bound() и equal_range() в данном слу- ае имеют довольно ограниченное применение. Помните, что контейнерный класс мар не допускает дуб- ирующихся ключевых значений. Подробнее об этом будет рассказано, когда будет представлен класс lultimap, где дублирующиеся ключи допустимы.
Вставки и удаления
Элементы можно удалять из записи или вставлять в запись. Класс мар обеспечивает несколько функций ставки и удаления:
template<class Key, class Т, class Compare = less<Key>, class A = AllocatorCT» class map < public: // типы typedef pairCconst Key, T> value_type; // вставка и удаление pair<iterator, bool> insert(const value_types val); H вставить пару <key, T> iterator insert(iterator pos, const value_type6 val);
Вопросы реализации Часть II template<class Inputlterator> void insert(Inputiterator first, Inputiterator last); void erase(iterator pos) ; size_type erase(const key_value* k) ; void erase(iterator first, iterator last); void clear(); 1;
Операции вставки и удаления аналогичны подобным операциям в векторах. Рассмотрим следующую функцию: iterator insert(iterator pos, const value_typel val) ; Итератор pos не влияет на то, где вставляется val, поскольку позиция элемента определяется только после вставки пары <кеу, Т>.
В листинге 6.16 показано, как добавлять и удалять элементы из контейнерного класса мар. Замените функцию main() в листинге 6.14 на программный код из листинга 6.16 и получите всю программу.
Листинг 6.16. Добавление и удаление элементов typedef map<string, Product:» PRODUCT_MAP; int main() ( Product Pen("Pen", 5.99, 58); Product Lamp("Lamp", 28.49, 24); Product Speaker("Speaker", 24.95, 40); PRODUCT_MAP productMap; productMap[Pen.GetName()] = Pen; productMap[Lamp.GetName()] = Lamp ; productMap[Speaker.GetName()] = Speaker; ShowMap(productMap); // добавить новый элемент cout « "Add a new element :\n"; Product Staple("Staple", 2.99, 20); pair<string, Product:» staplePair ("Staple", Staple); pair<PRODUCT_MAP::iterator, bool> p = productMap.insert(staplePair); if (p. second) cout « "New element added!\n"; elsecout « "Insertion failed!\n";
ShowMap(productMap); // добавить новый элемент с дублирующимся ключом Реп cout « "Add a new element with duplicate key Pen:\n"; Product RedPen("Red Pen", 3.29, 12); pair<string, Product:» RedPenPair ("Pen" , RedPen); p = productMap.insert(RedPenPair); if (p.second) cout « "New element added!\n"; elsecout « "Insertion failed!\n";
ShowMap(productMap); // удалить элемент с ключом "Lamp” из productMap cout « "remove element with key \"Lamp\" from productMap:\n"; productMap.erase("Lamp"); ShowMap(productMap); return 0;
)
Вывод из модифицированного программного кода листинга 6.16 показан ниже:
Map elements:
Lamp Product: Lamp Price: 28.49 Stock on hand: 24
Pen Product: Pen Price: 5.99 Stock on hand: 58
Speaker Product: SpeakerPrice: 24.95 Stock on hand: 40
Add a new element: New element added! Map elements:
Глава 6
Lamp Product: Lamp Price: 28.49 Stock on hand: 24
Pen Product: Pen Price: 5.99 Stock on hand: 58
Speaker Product: SpeakerPrice: 24.95 Stock on hand: 40
Staple Product: Staple Price: 2.99 Stock on hand: 20
Add a new element with duplicate key Pen:
Insertion failed!
Map elements:
Lamp Product: Lamp Price: 28.49 Stock on hand: 24
Pen Product: Pen Price: 5.99 Stock on hand: 58
Speaker Product: SpeakerPrice: 24.95 Stock on hand: 40
Staple Product: Staple Price: 2.99 Stock on hand: 20
remove element with key "Lamp" from productMap:
Map elements:
Pen Product: Pen Price: 5.99 Stock on hand: 58
Speaker Product: SpeakerPrice: 24.95 Stock on hand: 40
Staple Product: Staple Price: 2.99 Stock on hand: 20
Второе значение объекта pair, возвращаемое функцией insert(), устанавливается в значение true, если вставка выполнена успешно, что видно из staplePair. Если добавляемое ключевое значение в записи уже существует, то значение устанавливается в false, указывая на то, что вставка не состоялась, что видно из RedPenPair.
Операции с записями
Библиотека Standard Template Library также обеспечивает для сравниваемых записей перегруженные операторы сравнения. Вы можете обменивать элементы двух записей, используя функцию swap(). Следующий фрагмент программного кода показывает эти операции и то, как они определены классом мар:
template<class Key, class Т, class Compare = less<Key>, class A = Allocator<T>> class map
( public: // операции над записями void swap(maps m) ;// поменять местами элементы между текущим map и m
template<class Key, class T, class Compare, class A> bool operator= (const map<Key, T, Compare, A>S ml, const map<Key, T, Compare, A>S m2);
template<class Key, class T, class Compare, class A> bool operator!=(const map<Key, T, Compare, A>S ml, const map<Key, T, Compare, A>S m2);
template<class Key, class T, class Compare, class A> bool operator<( const map<Key, T, Compare, A>S ml, const map<Key, T, Compare, A>S m2);
template<class Key, class T, class Compare, class A> bool operator<=(const map<Key, T, Compare, A>& ml, const map<Key, T, Compare, A>6 m2);
template<class Key, class T, class Compare, class A> bool operator>( const map<Key, T, Compare, A>& ml, const map<Key, T, Compare, A>& m2);
template<class Key, class T, class Compare, class A> bool operator>=(const map<Key, T, Compare, A>& ml, const map<Key, T, Compare, A>& m2);
};
Множественные записи
Класс multimap похож на мар, за исключением того, что может содержать элементы с дублирующимися ключами. Таким образом, класс multimap имеет сходное определение класса с контейнерным классом мар, за некоторыми исключениями. В нем нет операторов индексирования, поскольку может быть несколько элементов, имеющих одно и то же ключевое значение. Операция вставки всегда выполняется правиль-
Часть II
но, поскольку разрешены дублирующиеся. ключи. Шаблонный класс multimap определен в файле <тар> в namespace std
Вспомните синтакси; функции inserts г- классе мар:
paiKiterator bool>:.nsert (const value_type& val) ;
В классе мар второе элемент г возвращаемой паре используется для указания того, была ли вставка успешной Второй элемен" длг класса multimap избыточен, поскольку он всегда имес значение true Таким образом, функция insert;* реализована в классе multimap следующим образом:
iterator insert(const value_type4 val).
Функции lower_bound(b upper_bound() и range_check() являются основными средствами доступа к элементам множественной записи с указанными ключевыми значениями. Эти три функции определены в классе multimap следующим образом
template<class Key c^as; 5 class Compare = less<Key>, class к = allocat.or<T>> class multimap i public■
/ / другие члены классе.
// найти первый iteratorconst_;teratar// найти первый iteratorconst_;teratar// найти первый iteratorconst iterator// найти первый iteratorconst iteratorэлемена с ключом большим или равным ключу к lower_bound (const key_typet к_>
// найти первый iterator
const_;teratar
// найти первый iterator
const_;teratar
// найти первый iterator
const iterator
// найти первый iterator
const iterator
lower bound'const key_typeS k) const
k) ;const■k) ;const■элемена с ключом большим к uppez_bound(const key_type& upper_bound, (const key_types k)
k) ;
const■
k) ;
const■
// найти все элементы с ключом к
pair(iterator, iterator,equal_range(const key_typeS k*
pair(constiterator const :terator)equal_range(const key_typeS k:const
Функции lower_bound() и upper_bound(i кратко уже описывались, когда вы приступили к знакомству с классом мар. Теперь рассмотрим функции, equai_range().
Функция equa!_range( s возвращав пару итераторов, содержащих первый элемент с ключом к и первый элемент с ключевым значением превышающим к Функция equal_range(> действует каг комбинация функций lower_bound() и upper bound{) собранных в одной функции Это уменьшает время выполнения и упрощает программе г смысле доступа к диапазону элементов. В листинге 6.17 показан пример использования класса multimap
Листинг 6.17. Использование класса multimap
♦include <iostreami
♦include <string>
♦include <map> using namespace std
template<class Key class T> void ShowMultimap(const multimap<Key T>S m) :
templateCclass Key, class T>
void ShowMultimapRangeiconst muJtimapCKey, T>4 m, const Keys k) ;
int main() (
multimap<string, string> stateMap;
StateMap insert (make_pair , (string) "USA", (string) "California") ) .
stateMap.insert(make_pairt(string)"USA", (string)"New York"));
stateMap.insert(make_pair;(string)"USA", (string)"Washington"));
stateMap insert (make_pair , (string'. "Australia" , (string) "New South Wales")).
stateMap.insert(make_pair'(string)"Vatican City", (string)"Vatican City")):
ShowMultimap(stateMap) ShowMultimapRange'stateMar (string)"USA");
return 0;
Глава 6Глава 6
Глава 6
Глава 6
template<class Key class Т> voxa ShowMultimap(const mu]timap<Key. T>S mJ
I tor;I tor;typedef multimap<Key. T>;■const_iterator
I tor;
I tor;
for (Itor i = m. begin (); i •= m.end() ; ++i) cout « i->first « "\t" « i->second « "\n";
cout « "\n":
templateCclass Key, class T>
void ShowMultimapRange (const multimap<Key, T>4 m, const Key 4 k,
typedef multimap<Key, T>const_iterator Itor,
pair<ltor, Itor> p = it. equal_range (k, foi (Itor x = p fxrst i ■- p secono ++i cout << i->first « "\f « i->second « "\n"
cout << "\n":
Вывод программы из листинга 6.17 показан ниже:
AustraliaNew South Wales
USACalifornia
USANew York
USAWashington
Vatican CityVatican City
USACalifornia
USANew York
USAWashington
В программе создается множественная запись, которая содержит пары страна/штат. Эти пары добавляются к множественной записи с помощью функции insert(). Функция ShowMultimap() подобна функции ShowMapO, использовавшейся ранее. Функция ShowMultimapRange() использует функцию equai_range() для получения первого и последнего элементов, которые имеют одинаковые ключевые значения Все квалифицированные элементы отображаются с помощью итератора.
Контейнер-набор
Набор также подобен записи. Разница заключается в том, что в наборе нет пар ключ/значение. Вместо этого в наборе элемент содержит только ключ. Шаблонный класс set определен в файле <set> в namespace std. Оператор индексирования [] здесь неуместен. Если вы знаете ключ, то уже знаете и значение. Класс set имеет функции-члены, почти идентичные тем, которые имеются и в классе мар за исключением того, что в классе set нет оператора индексирования []. Тип значения — это просто сам ключ, а значение compare — это просто класс Compare. Следующий программный код представляет функции доступные г контейнерном классе set
templateCclass Key, class Compare = less<Key>, class A = AllocatorCKey»
class set
public:
// другие функции-члены
typedef Keytypedef Comparetypedef Keytypedef Comparevalue_type; value_compare;value_type; value_compare;И в противоположность паре Cconst Key Т>И в противоположность паре Cconst Key Т>
typedef Key
typedef Compare
typedef Key
typedef Compare
value_type; value_compare;
value_type; value_compare;
И в противоположность паре Cconst Key Т>
И в противоположность паре Cconst Key Т>
Множественные наборы
Множественный набор представляет собой набор, который допускает использование дублирующихся ключей. Он также определен в файле <set> в namespace std. Множественный набор имеет все функции- члены set с единственным исключением: функция insert() класса multiset возвращает итератор, а не pair<iterator, bool>. Вставка никогда не бывает неправильной из-за дублирующихся ключей. Следующий программный код представляет функции, доступные в контейнерном классе multiset
Часть II
template<class Key, class Compare = less<Key>, class A = Allocator<Key>> class multiset
<
public:
// другие функции-члены
iterator insert(const value type! v) ;
Вопросы производительности
Контейнеры библиотеки STL сконструированы так, чтобы удовлетворять различным требованиям разработки приложений. Последовательные контейнеры лучше всего подходят для последовательного и произвольного доступа к элементам, выполняемого с использованием оператора индексирования [] и/или итераторов. Связанные контейнеры оптимизированы для обеспечения произвольного доступа к их элементам по ключевым значениям. В этом разделе представлен краткий обзор производительности некоторых стандартных контейнерных операций. В табл. 6.1 показаны некоторые распространенные контейнерные операции и их сложности.
Таблица 6.1. Стандартные контейнерные операции
Операция
vector list
deque map
multimap
set
multiset
Конструкторы
0(1)
Пустой контейнер с элементами
0(n)
Деструктор
0(n)
begin(), end(), rbegin(), rend()
0(1)
Операции с начальными элементами
0(1)
0(1)
Операции с конечными элементами
0(1)
0(1)
0(1)
Модификаторы
0(n)
0(1)
0(n)
0(log(n))
0(log(n))
0(log(n))
0(log(n))
[]
0(1)
0(1)
0(log(n))
at()
0(1)
0(1)
Сравнение
0(n)
Производительность операции часто определяется ее сложностью, т.е. время выполнения сравнимо с числом элементов, вовлеченных в операцию. Постоянная сложность, обозначенная в табл. 6.1 как 0(1), означает, что время выполнения не зависит от количества вовлеченных элементов. Логарифмическая сложность, обозначенная как 0(log(n)), означает, что время выполнения пропорционально логарифму значения числа вовлеченных элементов. Линейная сложность, обозначенная как 0(п), означает, что время выполнения пропорционально количеству вовлеченных элементов.
При возникновении потребности в стандартном контейнерном классе необходимо принять решение о том, какой из доступных контейнеров лучше подходит для приложения. При этом следует задаться двумя вопросами:
Какие контейнеры предлагают требуемую функциональность?
Например, если доступ к элементам должен производиться по ключу, то можно воспользоваться любым из связанных контейнеров.
Какой из подходящих контейнеров наиболее эффективен?
Если доступ к элементам всегда последователен, то последовательные контейнеры имеют преимущество перед связанными. Более того, если ожидается частая вставка в середину последовательности элементов, то лучшим выбором является контейнер list.
Использование стандартной библиотеки С++
Контейнерные классы библиотеки STL являются частью Standard C++ Library. Они определены в namespace std. Программы должны ссылаться на эти классы одним из двух способов.
Первый способ ссылки на контейнерные классы STL заключается в использовании директивы using namespace для указания намерения применять библиотеку Standard C++ Library. В программе должен присутствовать оператор:
Глава 6
using namespace std;
Этот оператор разрешает компилятору использование имен классов и методов, выполняя поиск в namespace std. Все примеры программ используют этот метод.
Второй метод ссылки на классы STL заключается в явной квалификации класса ключевым словом std, как показано в следующем примере:
std::vector<int> vlnt;
Вы должны также использовать директиву #include для включения в программу уместного header-файла. Например, для использования контейнерного класса vector необходим следующий оператор:
#include <vector>
В табл. 6.2 приведены header-файлы для каждого контейнерного класса.
Таблица 6.2. Заглавные файлы контейнерных классов.
Класс
Header-файл
vector
<vector>
list
<list>.
deque
<deque>
stack
<stack>
queue
<queue>
priority_queue
<queue>
map
<map>
multimap
<map>
set
<set>
multiset
<set>
Конструирование типов элементов
Контейнерные классы библиотеки STL сконструированы для хранения любых объектов. Вы видели, что многие методы контейнерных классов STL требуют, чтобы элементы могли выполнять конкретные операции:
Класс элемента должен иметь конструктор по умолчанию.
Конструктор по умолчанию вызывается тогда, когда объект контейнера создается в следующей форме:
vector<MyClass> v(100);
Этот оператор создает вектор, содержащий 100 объектов MyClass, созданных с помощью MyClass::MyClass().
Класс элемента должен иметь конструктор копии.
Конструктор копии вызывается тогда, когда к контейнеру добавляются новые элементы, как показано в следующем примере:
MyClass myObject;
vector<MyClass> v(100, myObject);// вызывается конструктор копии
vector<MyClass> v;
v.push_back(myObject);// вызывается конструктор копии
Класс элемента должен иметь оператор присваивания (=).
Оператор присваивания вызывается тогда, когда элементу присваивается новое значение, как показано в следующем примере:
MyClass myObject; vector<MyClass> v(100); v[l] = myObject // вызывается оператор присваивания
При использовании контейнерных классов, которые требуют сравнения значений своих элементов, следует определить перегруженные операторы сравнения. Если их не определить, то нужно использовать специализированный класс сравнения и явно указать его в определении контейнера. Основ-
Часть II
ними операторами сравнения являются == и < Эти операторы можно использовать для определения других операторов, включая <=, >=, !=, что было показано при рассмотрении контейнерного класса vector.
После того как перегруженные операторы == и < определены, можно предпринять шаги по порождению остальных операторов сравнения, используя файл <utility> библиотеки Standard C++ Library, который обеспечивает реализацию шаблонного оператора сравнения на базе операторов == и <
ПРИМЕЧАНИЕ
О реализации компилятором библиотеки STL можно узнать из документации по компилятору. •
Резюме
Библиотека Standard Template Library (STL) предлагает несколько контейнерных классов, которые помогают программистам выполнять распространенные функции по управлению структурами данных. Реализация стандартных контейнеров компилятором гарантированно будет удовлетворять стандарту C++. Стандартные контейнеры обеспечивают переносимость приложений и повышают вероятность привлечения наибольшего возможного числа пользователей. Для многих приложений контейнерные классы библиотеки STL обеспечивают удовлетворительную функциональность и сокращают затраты на кодирование и отладку. В целях обеспечения данного преимущества следует корректно использовать контейнерные классы. Контейнер необходимо выбрать так, чтобы он лучше удовлетворял требованиям приложения. В общем случае контейнеры обеспечивают оптимальный последовательный и произвольный доступ к элементам с помощью индексирования и итераторов. Связанные контейнеры обеспечивают прямой доступ к элементам по ключевым значениям.
Вы обнаружите, что часто нуждаетесь в использовании стандартных контейнеров для хранения своих объектов. Такие объекты должны удовлетворять ряду требований и ограничений, которые обусловливаются выбранным контейнером. Функции-члены, такие как операторы сравнения == и <, могут оказать большое влияние на производительность выбранного контейнера и самого приложения. Убедитесь в том, что реализуете контейнер самым эффективным и надежным способом.
Итераторы и алгоритмы STL
В ЭТОЙ ГЛАВЕ
Классы итераторов
Объекты-функции
Алгоритмы STL
Часть II
Классы итераторов
При обсуждении контейнерных классов STL в главе 6 мы использовали итераторы так, как будто это указатели на элементы контейнерных классов STL, и обращали внимание на то, что итераторы можно разадресовать, чтобы оценить элементы. В этой главе мы предпримем попытку оценить классы итераторов (итераторные классы).
Классы итераторов определены в файле <iterator> в namespace std. Итераторы представляют собой абстракции или обобщения указателей, т.е. они реализуют все операции указателей. Все итераторные операции имеют тот же эффект, что и соответствующие операции с указателями.
Позиция внутри контейнера
Контейнер хранит коллекцию объектов одного типа. Как именно хранятся элементы, зависит от контейнера. Например, элементы в векторе хранятся в последовательных блоках памяти. Контейнер-список хранит элементы в любой доступной памяти. Каждый стандартный контейнер организовывает свои элементы таким образом, чтобы элементы можно было получить от начала коллекции к концу. На рис. 7.1 показаны позиции элементов в контейнере.
РИСУНОК 7.1.
Элементы в контейнере.
Доступ к элементам происходит через итератор. Если итератор указывает на элемент п, то его можно разадресовать, чтобы получить элемент п. Итератор можно нарастить для указания на элемент п+1 или уменьшить для указания на элемент п-1. Когда итератор оценивается функцией end(), то он не указывает ни на какой элемент. Для итератора неприменимо значение NULL. Говорят, что итератор действителен, если он указывает на элемент или на конец последовательности (end()). Итератор недействителен, если он не инициализирован или если последовательность изменила размеры.
Типы итераторов контейнеров
Существует пять категорий итераторов. Они представлены в иерархии на рис. 7.2.
Этот рисунок не показывает диаграмму наследования классов — он представляет уровень функциональности, обеспечиваемый разными категориями итераторов. Вводной и выводной итераторы обеспечивают наиболее ограниченную функциональность. Они могут использоваться только для пересечения последовательностей в один проход в направлении от начала к концу. Проход нельзя повторить, т.е., если пройти через последовательность второй раз, используя вводной или выводной итератор, результат будет, скорее всего, иным.
РИСУНОК 7.2.
Иерархия итераторов.
Пересылаемый итератор обеспечивает все операции вводного и выводного итераторов и ослабляет некоторые ограничения, налагаемые ими. Наиболее примечательное отличие заключается в том, что повторный проход через ту же самую последовательность с помощью пересылаемого итератора породит те же самые результаты. Подобным же образом двунаправленный итератор обеспечивает все пересылаемые операции. Он также предоставляет возможность пересекать последовательность в обратном порядке.
И наконец, итератор произвольного доступа обеспечивает все операции двунаправленного итератора и другие методы, характерные для случайного метода доступа.
В табл. 7.1 представлены операции, предлагаемые итераторами.
Глава 7
Таблица 7.1. Итераторные операции
Категория
Вводной
Выводной
Пересылаемый
Двунаправленный
Произвольного доступа
Элемент
->
->
->
->. []
Доступ
Чтение
= *i
= *i
= *i
= *i
Запись
*i =
*i =
*i =
*i =
Итерации
++
++
++
++, -
,-, +,., +=, .=
Сравнение
== 1=
—— 1=
==| 1=
==, i=, <, <=, >, >=
Базовый класс итераторов
Библиотека Standard C++ содержит базовый тип, который можно использовать для создания собственных итераторных классов:
template<class Category, class Т, class Distance = ptrdiff_t, class Pointer = T*, class Reference = T6> struct iterator { typedef Categoryiterator_category;
typedef Tvalue_type;
typedef Distancediferrence_type;
typedef Pointerpointer;
typedef Referencereference;
};
Класс Category должен быть одним из пяти итераторных категорий, которые представлены следующими пустыми классами:
struct input_iterator_tag { );
struct output_iterator_tag (} ;
struct forward_iterator_tag : public input_iterator_tag {);
struct bidirectional_iterator_tag (} : public forward_iterator_tag {};
struct random_access_iterator_tag (} : public bidirectional_iterator_tag {} ;
Все эти пустые классы не имеют ни переменных-членов, ни функций-членов — за исключением, конечно, конструкторов и деструкторов по умолчанию. Они называются итераторными ярлыками (iterator_tag), поскольку используются только как ярлыки для представления итераторных категорий. На самом деле они не используются ни в одной стандартной итераторной функции-члене.
С другой стороны, можно использовать наследование итераторных ярлыков для написания общих алгоритмов итераторов. Например, если функция ожидает вводной итератор, то мы можем передать ей пересылаемый итератор. Функция должна выполняться без проблем, поскольку этот итератор без проблем выполнит все операции вводного итератора.
С учетом важности использования наследования итераторных ярлыков можно ожидать, что forwarditeratortag будет порожден от input_iterator_tag и the output_iterator_tag, как показано на рис. 7.2. STL, однако, порождает forward_iterator_tag только от input_iterator_tag. Причина в том, что выводной и пересылаемый итераторы используются для записи вывода в различные типы контейнеров.
Выводные итераторы часто используются для записи значений в неограниченные контейнеры, такие как стандартный вывод. Когда мы пишем в стандартный вывод, то можем выводить любое значение, которое должно быть записано. Нет необходимости проверять, примет ли стандартный вывод то, что мы выводим. Хотя пересылаемые итераторы обеспечивают все выводные итераторные операции, они работают только с ограниченными контейнерами. Пересылаемый итератор должен записать элемент в контейнер. Любая попытка перешагнуть границу генерирует либо ошибку времени компиляции, либо ошибку времени выполнения. Точно так же forward_iterator_tag не порожден от output_iterator_tag.
В любом случае на практике редко появляется необходимость писать алгоритмы, которые используют выводные итераторы. Выводные итераторы не имеют типа значения, поскольку невозможно получить значение от выводного итератора. Через него можно только записывать. Кроме того, выводные итераторы не имеют типа дистанции, поскольку невозможно найти дистанцию от одного выводного итератора к другому. Поиск дистанции требует сравнения на равенство, а выводные итераторы не поддерживают оператор ==. Таким образом, существует мало алгоритмов, которые работают на выводных итераторах. Фактически итераторные операции STL distance() и advance() вообще непригодны для работы с выводными итераторами.
Часть II
Следующий тип в итераторном шаблонном классе Т представляет собой тип объекта, указанного итератором. Distance — это знаковый целый тип, представляющий дистанцию между двумя итераторами. По умолчанию это тип ptrdifft, определенный в <cstddef>. Указатель является типом указателя, который указывает на value_type итератора. Этот тип является обобщенным типом указателя, возвращаемого перегруженным оператором ->. Ссылка является ссылкой на value_type итератора. Она возвращается перегруженным оператором разадресации *.
Пересылаемый итератор можно породить от итератора шаблонного класса:
template <class Т, class Distance = ptrdiff_t> class forward_iterator : public iterator<forward_iterator_tag, T, Distance> ( public: const T6operator*() const;
const T* operator->() const; iteratorsoperator** () ;// префикс-инкремент
iterator operator**(int);// постфикс-инкремент
// другие члены };
Все итераторы, порожденные от базового итераторного шаблонного класса, обладают общим набором атрибутов, включающим iterator_category, value_type и т.д. Библиотека STL обеспечивает iterator_traits шаблонного класса:
iterator_category;value_type;difference_type;pointer;reference;iterator_category;value_type;difference_type;pointer;reference;templateCclass Itor>struct iterator_traits typedef typename Itor::iterator_category typedef typename Itor::value_type typedef typename Itor::difference_type typedef typename Itor::pointer typedef typename Itor::reference
iterator_category;
value_type;
difference_type;
pointer;
reference;
iterator_category;
value_type;
difference_type;
pointer;
reference;
Использование характерных черт итераторов в итераторных операциях демонстрируется на конкретных примерах далее в этой главе.
Вводные итераторы
Вводной итератор представляет собой итератор, который должен удовлетворять следующим требованиям:
Быть конструируемым по умолчанию: итератор должен обладать конструктором по умолчанию, так чтобы мог создаваться без инициализации в какое-либо конкретное значение. Когда вводной итератор создается, используя свой конструктор по умолчанию, то это не действительный итератор. Он остается недействительным до тех пор, пока ему не будет присвоено значение.
Быть присваиваемым: итератор должен иметь конструктор копии и перегруженный оператор присваивания. Эта характеристика предоставляет возможность копировать и присваивать значения итераторам.
Быть сравниваемым на равенство: итератор должен иметь перегруженный оператор равенства == и оператор неравенства !=. Данные операторы позволяют сравнивать два итератора.
Вводной итератор представляет собой итератор, который можно разадресовать, когда он указывает на элемент в последовательности. Кроме того, итератор может указывать на конец последовательности. Когда итератор указывает на конец последовательности, то он фактически указывает на место как раз после последнего элемента в последовательности и о нем говорят, что он "past the end". Итератор past-the-end нельзя разадресовать.
Вводной оператор действителен, если его можно разадресовать в действительный объект или он past- the-end. Вводной итератор гарантирует доступ на чтение к указываемому объекту. Например, если п является вводным итератором, у которого value_type — это Т, то доступ к указываемому объекту получают путем разадресации:
Т t = *ii; // правильно
Поскольку вводные итераторы не допускают доступ на запись к указываемым элементам, то следующие операторы содержат ошибку:
т t;*ii = t;т t;*ii = t;// ошибка// ошибка
т t;
*ii = t;
т t;
*ii = t;
// ошибка
// ошибка
Глава 7
Вводной итератор можно нарастить для указания на следующий элемент. Должны быть определены как префиксный оператор приращения (++П), так и постфиксный оператор приращения (ii++). Вводной итератор можно наращивать, если ++П имеет место. Итераторы past-the-end наращивать нельзя.
Выводные итераторы
Выводной итератор должен быть по умолчанию конструируемым и присваиваемым. Выводной итератор гарантирует доступ к записи, но не доступ для чтения. Рассмотрим следующие операторы:
т t;
*oi = t;// правильно
t = *oi;II ошибка
Кроме того, выводной итератор определяет и префиксный, и постфиксный операторы приращения.
Пересылаемые итераторы
Пересылаемый итератор похож на видеоленту — его можно проигрывать в одном направлении, но зато многократно. Он обладает всеми характеристиками вводного итератора, т.е. он по умолчанию конструируемый, присваиваемый и сравниваемый на равенство. Кроме того, пересылаемый итератор можно разадре- совывать и наращивать. Пересылаемые итераторы обеспечивают доступ к указываемым объектам как на чтение, так и на запись.
Двунаправленные итераторы
Двунаправленный итератор похож на пересылаемый, за исключением того, что он может и уменьшаться.
Итераторы произвольного доступа
Итератор произвольного доступа реализует все операции двунаправленного итератора и добавляет методы для перемещения от одного элемента к другому за постоянное время (0(1)). Дистанция между исходным элементом и элементом места назначения не важна, т.е. итератор произвольного доступа так же быстр при переходе от первого элемента к тридцать седьмому, как и при переходе от первого элемента ко второму.
Поскольку итераторы произвольного метода доступа могут перемещаться более чем на один шаг, то для них определены операторы сложения (+ и +=) и вычитания (- и -=). Итераторы произвольного доступа также определяют оператор индексирования []. Если ri является итератором произвольного доступа, то ri[n] возвращает n-й элемент в последовательности.
Итератор произвольного доступа сравним не только на равенство, но и по величине. Следовательно, он определяет операторы <, >, <= и >=. Фундаментальным из них является только оператор < Все остальные операторы можно породить от него.
Итераторные операции
В дополнение к перегруженным операторам библиотека STL обеспечивает две функции, которые возвращают количество элементов между двумя элементами и которые "прыгают" с одного элемента на любой другой элемент в контейнере.
Функция distance!)
Функция distance находит дистанцию между текущими позициями двух итераторов, т.е. если первый итератор указывает на элемент 12 в последовательности, а второй на элемент 47 в той же последовательности, то дистанция будет равна 35.
template<class Inputlterator>
iterator_traits<lnputlterator>::difference_type distance(Inputiterator first, Inputiterator last);
Как вычисляется дистанция между двумя итераторами? Для итераторов с произвольным доступом это легко, поскольку они обеспечивают операторы вычитания. Мы можем просто вычесть первый итератор из последнего и получить дистанцию:
template<class RandomAccessIterator>
iterator_traits<RandomAcces sIterator>::di fference_type
distance(RandomAccessIterator first, RandomAccessIterator last) ( return last — first;
)
Часть II
Для других итераторов это несколько усложнятся, поскольку для них не определен оператор вычитания. Необходимо пройти по последовательности от первого итератора до последнего и зарегистрировать количество шагов:
template<class Inputlterator>
iterator_traits<lnputlterator>::difference_type
distance(Inputiterator first, Inputiterator last) (
iterator_traits<lnputlterator>::difference_type n = 0;
while (first++ != last) ++n; return n;
)
Благодаря наследованию итераторных ярлыков, вторая версия функции distance!) работает для вводных, пересылаемых и двунаправленных итераторов. Поскольку выводные итераторы не имеют оператора сравнения !=, то невозможно проверить, являются ли два выводных итератора равными. Поэтому для выводных итераторов функция distance!) не определена.
Сложность (время выполнения в сравнении с количеством элементов, вовлеченных в операцию) функции distance!) зависит от категории итератора. Для итераторов с произвольным доступом это постоянное время (0(1)), поскольку операция вычитания для итераторов произвольного доступа имеет сложность с постоянным временем. Для других итераторов сложность линейна (0(п)).
Функция advance!)
До сих пор мы видели, как перемещать итераторы вперед и назад, используя операторы приращения и уменьшения соответственно. Итераторы произвольного доступа также можно перемещать сразу на несколько шагов, используя функции сложения и вычитания. Другие типы итераторов, однако, не имеют функций сложения и вычитания. Для перемещения любого итератора, кроме выводных, на несколько шагов одновременно STL предоставляет функцию advance!):
template<class Inputiterator, class Distance>
void advance(Inputiterators ii, Distances n) ;
В данном синтаксисе Distance представляет собой знаковый целый тип. Дистанция для перемещения п может быть как положительной, так и отрицательной. Если п положительна, то итератор перемещается вперед, в противном случае итератор перемещается назад. Если п равна нулю, то функция никакого эффекта не имеет и перемещения не происходит. Значение п может иметь отрицательное значение только в случае двунаправленного итератора или итератора произвольного доступа.
Существует значительная разница между разными категориями итераторов: операции сложения и вычитания обеспечивает только итератор произвольного доступа. Поэтому функцию advance!) можно перегрузить для итераторов разных категорий:
template<class Inputiterator, class Distance>
void advance(Inputlterators ii, Distances n) { while (n—)++ii;
}
template<class Bidirectionallterator, class Distance>
void advance(Bidirectionallterator S bi, Distances n) (
if (n >= 0) while (n—)++bi;
elsewhile (n++)—bi;
}
template<class RandomAccessIterator, class Distance>
void advance(RandomAccessIteratorS ri, Distances n) ( ri += n ;
)
Поскольку выводные итераторы предоставляют только возможность записи в их типовые значения, то должен иметь место пошаговый проход их. Оператор приращения предназначен именно для этой цели. Для выводных итераторов не существует необходимости в функции advance!).
Подобно функции distance!), сложность advance!) зависит от категории итератора. Функция advance!) имеет постоянное время для итераторов с произвольным доступом и линейную сложность для других итераторов.
Глава 7
Классы стандартных итераторов
Библиотека STL содержит множество классов итераторов, которые выполняют определенные общие итеративные операции. Данные классы описаны в последующих разделах этой главы.
Класс istreamjterator
Шаблонный класс istream_iterator определен в <iterator> и является вводным итератором, который выполняет форматированный ввод объектов заданного типа из istream. Класс istream_iterator можно определить следующим образом:
template <class Т, class Distance = ptrdiff_t>
class istream_iterator : public input_iterator<T, Distance> {
public:
typedef input_iterator_tag
iterator_category;
typedef Т
value_type;
typedef Distance
difference_type
typedef const T*
pointer;
typedef const T6
reference;
const T6 const T* istream_iterator6 istream iteratorconst T6 const T* istream_iterator6 istream iteratoroperator*() const;
const T6 const T* istream_iterator6 istream iterator
const T6 const T* istream_iterator6 istream iterator
operator->()const;
operator++();// префикс-инкремент
operator++(int);//постфикс-инкремент
// другие члены 1;
ПРИМЕЧАНИЕ
Приведенное определение не является стандартным определением шаблонного класса istreamjterator. Этот пример служит только иллюстрацией важных характеристик итераторов istream.
В листинге 7.1 показано применение итератора istream для чтения из стандартного ввода cin.
Листинг 7.1. Чтение ввода с использованием итератора istream
#include <iostream> #include <iterator> using namespace std;
int main() { cout « "Enter an integer: "; istream_iterator<int, char> ii(cin); int il = *ii; cout « "il = " « il « "\n";
cout « "Ready for another integer: "; int i2 = *++ii; cout « "i2 = " « i2 « "\n";
return 0;
Ниже приведен вывод, сгенерированный программой из листинга 7.1:
Enter an integer: 6 il = 6
Ready for another integer: 2 i2 = 2
Итератор istream ii создается для приема ввода из стандартного ввода cin. Второй шаблонный параметр — это базовый тип istream. Первое введенное целое присваивается И с помощью оператора разадресации *. Для приема следующего ввода итератор должен наращиваться.
Часть II
Класс ostreamjterator
Шаблонный класс ostream_iterator также определен в <iterator> и является выводным итератором, который выполняет форматированный вывод объектов заданного типа в ostream. Класс ostreamjterator можно определить следующим образом:
template Cclass Т>
class ostream_iterator : public output_iterator (
public:
typedef output_iterator_tagiterator_category;
typedef void typedef void typedef void typedef void
value_type;
difference type; pointer;
reference;
ostream
iterators
operator*();
ostream
iterators
operator** () ;
ostream_
iterators
operator**(int);
};
ostream
iterators
operators(const Ts);
С помощью листинга 7.2 демонстрируется применение итератора ostream для записи целых в стандартный вывод cout.
Листинг 7.2. Запись вывода, выполняемая с помощью итератора ostream
#include <iostream> #include <iterator> using namespace std;
int main() (
ostream_iterator<int, char> oi(cout);
oi = 6 ; *++oi = 88;
return 0;
)
Ниже приведен вывод, сгенерированный программой из листинга 7.2:
688
Сначала создается выводной итератор oi для вывода целых в стандартный вывод cout. В манере, сходной с той, как работает istreamiterator, второй параметр указывает базовый тип ostream. Когда мы присваиваем значение 6 объекту value_type, то он печатается в cout. Как и с итераторами istream, итератор ostream должен наращиваться, прежде чем выводить следующее значение. В листинге 7.2 целое значение 88 печатается через oi в позицию, следующую за 6.
Объекты-функции
Контейнеры и итераторы позволяют создавать, искать и изменять последовательность элементов. Стандартный контейнер обычно определяет множество операций, которые можно использовать для управления контейнером и его элементами. Поскольку часто возникает необходимость в конструировании специализированных контейнеров для решения проблем, когда стандартные контейнеры не совсем удобны, то мы должны уметь реализовывать контейнерные операции. Многие такие операции используют функции, которые выполняют общие операции — сравнение объектов, оценку правильности и вычисления. В этом разделе представлено несколько объектов-функций, определенных в библиотеке STL. Стандартные объекты-функции определяются в <functional> в namespace std. Объект-функция — это объект, который можно вызвать как функцию. Как показано в листинге 7.3, он может быть любого класса, который определяет operator().
Листинг 7.3. Объект-функция #include <iostream> using namespace std;
templatecdass T> class Print (
Глава 7
public:
void operator () (TS t) { cout « t « "\n"; } );
int main() { Print<int> DoPrint; for (int i = 0; i < 5; ++i) DoPrint(i); return 0;
}
Ниже приведен вывод программы из листинга 7.3: 0 1 2 3 4
Шаблонный класс Print определяется только с одной функцией-членом — operator(), которая просто печатает значение в cout. Как и обычный класс, Print должен реализовываться так, чтобы можно было вызывать его перегруженный operator(). В листинге 7.3 создается DoPrint для печати целых, т.е. показан очень простой объект-функция.
Объекты-функции могут использоваться алгоритмами STL для выполнения различных действий над контейнерами. Существует три разных типа объектов-функций:
Генераторы: объекты-функции, не принимающие аргументов. Классическим примером является генератор случайных чисел.
Унарные функции: объекты-функции, которые принимают один аргумент. Эти объекты могут возвращать значение, а могут и не возвращать. Объект DoPrint представляет собой пример унарной функции.
■ Бинарные функции: объекты-функции, которые принимают два аргумента. Они могут возвращать, а могут и не возвращать значение.
Библиотека Standard C++ Library предоставляет два базовых класса для упрощения создания объектов- функций:
template<class Arg, class Result> struct unary_function (
typedef Argargument_type;
typedef Resultresult_type;
);
template<class Argl,class Arg2, class Result>
struct binary_function(
typedef Arglfirst_argument_type;
typedef Arg2second_argument_type;
typedef Result result_type; );
Now we can redefine the Print class as follows: template<class T> class Print: public unary_function<T, void> (
);
Теперь можно переопределить класс Print следующим образом:
template<class Т>
class Print: public unary_function<T, void> { };
Предикаты
Когда тип возвращаемого значения унарного объекта-функции — bool, то функция называется унарным предикатом. Бинарный (двоичный) объект-функция, который возвращает значение bool, называется би-
Часть II
норным предикатом. Библиотека Standard C++ Library определяет в <functional> несколько распространенных предикатов. Эти предикаты перечислены в табл. 7.2.
Таблица 7.2. Предикаты, определенные в <functional>
Функция
Tun
Описание
equal_to
ДВОИЧНЫЙ
argt == arg2
not_equal_to
двоичный
argl != arg2
greater
двоичный
argt > arg2
greater_equal
двоичный
argt >= arg2
less
двоичный
argt < arg2
less_equal
двоичный
argt <= arg2
logical_and
двоичный
argt && arg2
logical_or
двоичный
argt || arg2
logicalnot
унарный
largt
Предикат equal_to определяется следующим образом:
template<class Т> struct equal_to : binary_function<T, Т, bool> (
bool operator()(T6 argl, T6 arg2) const ( return argl == arg2; } };
Для сравнения двух значений типа Т следует определить перегруженный оператор ==, используя предикат equal_to. Оператор == можно определить либо как унарную функцию-член класса Т, либо как перегруженный двоичный оператор-функцию, как показано ниже:
bool Т: :operator==() (Тб t)II функция-член класса Т
template<class Т>II общий оператор сравнения
bool operator==(Тб argl, Тб arg2);
Все другие предикаты определяются подобным образом и имеют сходные требования.
Арифметические функции
Арифметические функции выполняют над объектами арифметические операции. В табл. 7.3 перечислены функции, определенные Standard C++ Library в <functional>.
Таблица 7.3. Арифметические функции, определенные в <functional>
Функция
Tun
Описание
plus
ДВОИЧНЫЙ
argt + arg2
minus
двоичный
argt — arg2
multiplies
двоичный
argt * arg2
divides
двоичный
argt / arg2
modulus
двоичный
argt % arg2
negate
унарный
-argt
Двоичные арифметические функции принимают два аргумента одного типа и возвращают значение такого же типа. Например, функция plus() определена следующим образом:
template <class Т> struct plus : binary_function<T,T,T> <
T operator()(const T6 x, const Тб у) const; );
Функция negateQ определена как унарная:
template <class T> struct negate : unary_function<T,T>
Глава 7
Т operator()(const Is х) const;
Алгоритмы STL
Контейнер представляет собой полезное место для хранения последовательности элементов. Все стандартные контейнеры определяют операции, которые манипулируют контейнерами и их элементами. Однако реализация всех этих операций в ваших последовательностях требует скрупулезной работы и часто чревата ошибками. Поскольку большинство таких операций для множества последовательностей, скорее всего, будут одинаковы, набор общих алгоритмов может сократить потребность в написании собственных операций для каждого контейнера. Библиотека Standard C++ Library представляет около 60 стандартных алгоритмов, которые выполняют большинство базовых и наиболее часто применяемых операций для контейнеров.
Стандартные алгоритмы определены в <algorithm> в namespace std.
Операции немутирующих последовательностей
Операции немутирующих последовательностей используются для извлечения значений или позиций элементов в последовательном контейнере. Последовательность идентифицируется парой итераторов (first, last). First указывает на первый элемент в последовательности, и last является итератором past-the-end.
Операция for_each()
Операция for_each() выполняет унарную операцию над каждым элементом в последовательности.
template<class Inputiterator, class UnaryFunction>
Functionobject for_each(Inputiterator first, Inputiterator last, UnaryFunction f) ;
Функция f должна быть унарной и выполнять доступ к элементам только на чтение.
Операция find()
Операция find() проверяет последовательность с целью найти ее первый элемент, который равен заданному значению.,
template<class Inputiterator, class Т>
Inputiterator find (Inputiterator first, Inputiterator last, const Ts value) ;
Если элемент найден, то find() возвращает итератор, указывающий на элемент. В противном случае возвращается итератор last.
Операция findjf()
Операция find_if() проверяет последовательность с целью найти ее первый элемент, который при передаче предикату оценивает предикат как true.
template<class Inputiterator, class UnaryPredicate>
Inputiterator find_if(Inputiterator first, Inputiterator last, UnaryPredicate pred);
Подобно find(), операция find_if() возвращает итератор, указывающий на найденный элемент. Если элемент не найден, то возвращается итератор last.
Операция count()
С помощью операции count() подсчитывается количество элементов в последовательности, которые равны заданному значению.
template<class Inputiterator, class Т>
iterator_traits<lnputlterator>::difference_type
count(Inputiterator first, Inputiterator last, const TS value);
Элементы в последовательности сравниваются с заданным значением с помощью перегруженного оператора сравнения ==. Возвращается количество квалифицированных элементов.
Операция countjf()
С помощью операции count_if() подсчитывается количество элементов в последовательности, которые равны заданному значению. Проверка на равенство использует внешний двоичный предикат.
Вопросы реализации Часть II
template<class Inputiterator, class BinaryPredicate> iterator_traits<lnputlterator>::difference_type count_if(Inputiterator first, Inputiterator last, Binarypredicate pred) ;
Подобно count(), операция count_if() возвращает количество квалифицированных элементов. В листинге 7.4 демонстрируются операции find() и count().•
Листинг 7.4. Использование операций tindQ и countQ
#include <iostream>
#include <vector>
#include <iterator> #include <functional> #include <algorithm> using namespace std;
const int VectorSize = 5;
template<class T> class Print: public unary_function<T, void> ( public: void operator()(TS argl) { cout « argl « " " ;
} );
template<class T> class GreaterThanTwo: public unary_function<T, bool> { public: bool operator()(TS argl) { return (argl > 2) ;
) );
template<class Container, class Iterator> void ShowElement(Containers c, Iterators itor);
int main() ( Print<int> DoPrint; vector<int> vlnt(VectorSize); typedef vector<int>::iteratorItor;
for (int i = 0; i < VectorSize; ++i) vlnt[i] = i;
Itorfirst = vlnt.begin() ;
Itorlast = vlnt.end() ;
cout « "for_each () \n" ; for_each(first, last, DoPrint); cout « "\n";
Itor retltor = find(first, last, 2); cout « "find(first, last, 2) = "; ShowElement(vlnt, retltor); cout « "\n";
retltor = find(first, last, 10); cout « "find(first, last, 10) = " ; ShowElement(vlnt, retltor); cout « "\n";
GreaterThanTwo<int>IsGreaterThanTwo;
retltor = find_if(first, last, IsGreaterThanTwo); cout « "find(first, last, IsGreaterThanTwo) = "; ShowElement(vlnt, retltor); cout « "\n";
Глава 7
int retSize = count(first, last, 3) ; cout « "count(first, last, 3) = " « retSize « "\n";
retSize = count_if(first, last, IsGreaterThanTwo); cout « "count_if(first, last, IsGreaterThanTwo) = " « retSize « "\n"; return 0;
} template<class Container, class Iterator> void ShowElement(Containers c, Iterators itor) { if (itor != c.end()) cout « *itor; else cout « "last";
}
Вывод, сгенерированный программным кодом в листинге 7.4, приведен ниже:
for_each() 0 12 3 4 find(first, last, 2) = 2 find(first, last, 10) = last find(first, last, IsGreaterThanTwo) = 3 count(first, last, 3) = 1
count_if(first, last, IsGreaterThanTwo) = 2
Унарный объект-функция Print определен для упрощения печати своих аргументов в стандартный вывод cout. Для сравнения аргумента с произвольным числом 2 мы определяем еще один унарный класс GreaterThanTwo. Тип его аргумента Т должен быть сравним с целым типом, чтобы мы могли сравнивать его с целым.
Шаблонная функция ShowElement() определена для печати элемента, указываемого итератором йог в последовательности с. Если итератор является past-the-end, то он отображает строку last.
В функции mainO определен объект класса Print — DoPrint(), так чтобы можно было вызвать DoPrint(var) и напечатать значение var. Последовательность [first, last] создается из вектора vlnt, который содержит пять целых элементов: 0, 1, 2, 3 и 4. Операция for_each() выполняется для того, чтобы применить функцию DoPrint() к каждому элементу последовательности.
Чтобы найти первый элемент, имеющий заданное значение и необходимый для поиска элемента со значением 2, можно использовать функцию find(). Элемент, указываемый результирующим итератором, можно получить через разадресацию итератора. Как и ожидалось, элемент имеет значение 2. Затем функция find() при повторном к ней обращении ищет элемент со значением 10. Поскольку такого значения нет, возвращается итератор past-the-last.
Объект класса GreaterThanTwo — IsGreaterThanTwo — возвращает значение true, если его аргумент больше 2. Этот предикат используется операцией find_if() для поиска первого элемента со значением, превышающим 2. Данный поиск возвращает элемент со значением 3.
Затем для поиска количества элементов со значением 3 используется операция count(). В операции count_if() используется предикат IsGreaterThanTwo() для подсчета числа элементов, превышающих 2. В результате находятся два элемента.
Операция adjacent_find()
С помощью операции adjacent_find() проверяется последовательность в поиске двух равных смежных элементов. Библиотека STL содержит два перегруженных оператора adjacent_find(). Оператор первой версии сравнивает элементы с помощью перегруженного оператора ==:
template<class Forwardlterator>
Forwarditerator adjacent_find(Forwarditerator first, Forwarditerator last);
Оператор второй версии adjacent_find() сравнивает элементы с помощью внешнего двоичного предиката. Если предикат оценивает значением true оба смежных элемента, то они считаются равными:
template <class Forwarditerator , class...BinaryPredicate>
Forwarditerator adjacent_find(Forwarditerator first, Forwarditerator last,
Binarypredicate binary_pred);
Вопросы реализации Часть II
Операторы обеих версий возвращают итератор, указывающий на первый элемент в паре, если такая пара элементов найдена. В противном случае возвращается итератор last.
Операция search_n()
Операция search_n() выполняется для поиска заданного количества последовательных элементов последовательности. Все элементы должны быть равны заданному значению. Операция имеет две версии, которые отличаются тем, как проверяется равенство элементов:
template<class Forwarditerator, class Size, class T>
Forwarditerator search_n(Forwarditerator first, Forwarditerator last, Size count, const 16 value);
templatecclass Forwarditerator, class Size, class T, class BinaryPredicate> Forwarditerator search_n(Forwarditerator first, Forwarditerator last, Size count, const 16 value, BinaryPredicate pred);
Если search_n() находит количество count последовательных элементов в последовательности [first, last], то она возвращает итератор, указывающий на первый элемент. В противном случае возвращается итератор last.
В листинге 7.5 показано применение операций adjacent_find() и search_n().
Листинг 7.5. Использование операций adjacent find() и search n()
#include <iostream>
#include <vector>
#include <iterator>
#include <functional>
#include <algorithm> using namespace std;’
const int VectorSize = 10;
templatecclass T>
class EqualToThree: public binary_function<T, T, bool> ( public:
bool operator () (T6 argl, T6 arg2) (
return (argl == arg2) 66 (argl == 3) ; )
};
templatecclass Container, class Iterator> void ShowElement(Container6 c, Iterator6 itor);
int main() ( vectorcint> vlnt(VectorSize); typedef vectorcint>::iterator Itor;
vInt[0] = 0; vlnt[l] = 2; vlnt[2] = 2; vlnt[3] = 4; vlnt[4] = 4; vlnt[5] = 3; vlnt[6] = 3; vlnt[7] = 4; vlnt[8] = 4; vlnt[9] = 4;
Itor first = vlnt. begin () ; Itor last = vlnt.end() ; Itor retltor = adjacent_find(first, last); cout « "adjacent_find(first, last) = "; ShowElement(vlnt, retltor); cout « "\n";
EqualToThreeCint>EqualToThreeCint>IsEqualToThree;IsEqualToThree;
EqualToThreeCint>
EqualToThreeCint>
IsEqualToThree;
IsEqualToThree;
207
retltor = adjacent_find(first, last, IsEqualToThree); cout « "adjacent_find(first, last, IsEqualToThree) = "; ShowElement(vlnt, retltor); cout « "\n";
retltor = search_n(first, last, 3, 4);
cout « "search_n(first, last, 3, 4) = "; ShowElement (vlnt, retltor);
cout « "\tprevious element is " ; ShowElement(vlnt, —retltor); cout « "\n";
return 0;
}
template<class Container, class Iterator> void ShowElement(Containers c, Iterators itor) ( if (itor != c.end()) cout « *itor; else cout « "last";
)
Ниже представлен вывод программы из листинга 7.5:
adjacent_find(first, last) = 2
adjacent_find(first, last, IsEqualToThree) = 3
search_n(first, last, 3, 4) = 4 previous element is 3
Для сравнения элементов определен двоичный предикат EqualToThree(). Он возвращает значение true только в том случае, если оба аргумента равны 3. Мы определили целый вектор vlnt и присвоили его элементам проверяемые данные. Элемент со значением 2 возвращается операцией adjacent_find(). Объект EqualToThree используется предикатом версии adjacent_find() для поиска последовательных элементов со значением 3.
Операция search_n() выполняется для поиска первого элемента второй группы из четырех. Предыдущий элемент также отображается с целью проверить возвращаемое значение.
Операция find_first_of()
С помощью операции find_first_of() проверяется последовательность с целью найти первое появление элемента, идентичного любому из элементов в другой последовательности. Существует две версии find_first_of(): при выполнении операции первой версии для проверки элементов на равенство используется перегруженный оператор ==, а при выполнении второй — внешний двоичный предикат:
template <class Inputiterator, class Forwardlterator>
Inputiterator find_first_of(Inputiterator firstl, Inputiterator lastl,
Forwarditerator first2, Forwarditerator last2);
template <class Inputiterator, class Forwarditerator, class BinaryPredicate>
Inputiterator find_first_of(Inputiterator firstl, Inputiterator lastl,
Forwarditerator first2, Forwarditerator last2, Binarypredicate comp);
Если такая субпоследовательность найдена, то возвращается итератор, указывающий на первый элемент в субпоследовательности. Если при выполнении операции find_first_of() не может быть найдена субпоследовательность, то возвращается итератор lastl.
Операция search()
Существует две перегруженные операции search(), которые проверяют последовательность [firstl, lastl) для поиска первого появления субпоследовательности, которая идентична другой последовательности [firstl, last2). Как и две версии find_first_of(), две версии search() используют для проверки элементов на равенство либо перегруженный оператор ==, либо внешний двоичный предикат.
template <class Inputiterator, class Forwardlterator>
Inputiterator search(Inputiterator firstl, Inputiterator lastl,
Вопросы реализации Часть II
Forwarditerator first2, Forwarditerator last2);
template <class Inputiterator, class Forwarditerator, class BinaryPredicate> Inputiterator search(Inputiterator firstl, Inputiterator lastl,
Forwarditerator first2, Forwarditerator last2, Binarypredicate comp);
Если нужная субпоследовательность найдена, то возвращается итератор, указывающий на первый элемент в субпоследовательности. Если при выполнении операции find_first_of() не может быть найдена нужная субпоследовательность, то возвращается итератор lastl.
Операция find_end()
Операция find_end(), наверное, названа так ошибочно. Она больше похожа на операции search(), чем на операцию find(). Как и search(), find_end() проверяет последовательность [firstl, lastl) в поиске субпоследовательности, которая идентична другой последовательности [first!, last!). Разница заключается в том, что find_end() ищет последнее появление субпоследовательности.
template<class Forwardlteratorl, class Forwardlterator2> Forwardlteratorl find_end(Forwardlteratorl firstl, Forwardlteratorl lastl, Forwardlterator2 first2, Forwardlterator2 last2);
template<class Forwardlteratorl, class Forwardlterator2, class BinaryPredicate> Forwardlteratorl find_end(Forwardlteratorl firstl, Forwardlteratorl lastl,
Forwardlterator2 first2, ForwardIterator2 last2, Binarypredicate pred);
Как и версии search(), две версии find_end() различаются тем, как они сравнивают элементы в последовательностях. В листинге 7.6 показано, как работают функции поиска.
Листинг 7.6. Использование других функций поиска #include <iostream> #include <vector> #include <iterator> #include <functional> #include <algorithm> using namespace std; template<class Container, class Iterator> void ShowElement(Containers c. Iterators itor); int main() < typedef vector<int>::iterator Itor; vector<int>vlntl(10);
vlntl[0] = 0; vlntl [1] = 1; vlntl [2] = 1; vlntl[3] = 2; vlntl [4] = 3; vlntl [5] = 4; vlntl [6] = 1; vlntl (7) = 2; vlntl[8] = 3; vlntl[9] = 5; vector<int>vlnt2(3);
vInt2[0] = 1; vlnt2[l] = 2; vlnt2[2] = 3; Itor firstl = vlntl .begin () ; Itor lastl = vlntl.end() ; Itor first2 = vlnt2 .begin () ; Itor last2 = vlnt2.end();
Itor retltor = find_first_of(firstl, lastl, first2, last2); cout « "find_first_of(firstl, lastl, first2, last2) = "; ShowElement(vlntl, retltor);
209
cout « "\n"; retltor = search(firstl, lastl, first2, last2); cout « "search(firstl, lastl, first2, last2) = "; ShowElement(vlntl, retltor); cout « "\n";
retltor = find_end(firstl, lastl, first2, last2);
cout « "find_end(firstl, lastl, first2, last2) = "; ShowElement(vlntl, retltor); cout « "\n";
return 0; )
templatecclass Container, class Iterator> void ShowElement(Containers c, Iterators itor) ( if (itor •= c.endO) if (itor != c.begin ())
cout « *itor « "\tthe previous element is " « *(itor - 1) ; else
cout « "first"; ) else cout « "last";
}
Ниже приведен вывод, сгенерированный программным кодом из листинга 7.6:
find_first_of(firstl, lastl, first2, last2)= 1thepreviouselementis0
search(firstl, lastl, first2, last2) = 1thepreviouselementis1
find_end(firstl, lastl, first2, last2) = 1thepreviouselementis4
Шаблонная функция ShowElement() здесь немного изменена, с тем чтобы показать не только текущий элемент, но и предыдущий, что позволит увидеть позицию элемента в последовательности с помощью операции find_first_jf() ищется второй элемент в векторе vlntl. С помощью операции search() ищется третий элемент, поскольку это первый элемент первой субпоследовательности (1, 2, 3) в vlntl. При выполнении операции find_end() находят седьмой элемент, который является первым в последней субпоследовательности (1, 2, 3) в vlntl.
Операция equal()
Операция equal() обеспечивает сравнение последовательностей равного размера, чтобы убедиться в их совпадении. В этой операции для проверки элементов на равенство используется либо перегруженный оператор ==, либо двоичный предикат.
template<class Inputlteratorl, class Inputlterator2>
bool equal(Inputlteratorl firstl, Inputlteratorl lastl, Inputlterator2 first2);
template<class Inputlteratorl, class Inputlterator2, class BinaryPredicate>
bool equal(Inputlteratorl firstl, Inputlteratorl lastl, Inputlterator2 first2, Binarypredicate pred);
Вторая последовательность начинается c first2 и заканчивается first2+(lastl-firstl). Если ее размер такой же, как и размер первой последовательности, и если каждая пара элементов равна, то equal() возвращает значение true. В противном случае возвращается значение false.
Операция mismatch()
Операция mismatch() используется для сравнения двух последовательностей с целью найти несовпадающие элементы. При этом сравниваются соответствующие пары элементов и возвращается первая пара, которая не совпадает.
template<class Inputlteratorl, class Inputlterator2>
pair<lnputlteratorl, Inputlterator2>
mismatch(Inputlteratorl firstl, Inputlteratorl lastl, Inputlterator2 first2);
Вопросы реализации Часть II
template<class Inputlteratorl, class Inputlterator2, class BinaryPredicate> pair<lnputlteratorl, Inputlterator2> mismatch(Inputlteratorl firstl, Inputlteratorl lastl, Inputlterator2 first2, Binarypredicate pred);
Подобно многим уже обсуждавшимся алгоритмам, две версии mismatch() различаются разными методами проверки элементов на равенство.
В листинге 7.7 демонстрируются операции equal() и mismatch().
Листинг 7.7. Использование операций equalQ и mismatchQ для сопоставления элементов
#include <iostream>
#include <vector> #include <algorithm> using namespace std;
template<class Container, class Iterator> void ShowElement(Containers c, Iterators itor);
int main() ( typedef vector<int>::iterator Itor; vector<int>vlntl(4);
vlntl[0] = 1; vlntl[1] = 2; vlntl[2] = 3; vlntl[3] = 4; vector<int>vlnt2(3);
vInt2[0] = 1; vlnt2[l] = 2; vlnt2[2] = 3; Itor firstl = vlntl .begin () ; Itor lastl = vlntl.end() ; Itor first2 = vlnt2 .begin () ; if (equal(firstl, lastl, first2)) cout « "vlntl = vlnt2\n" ; else cout « "vlntl != vlnt2\n";
pair<Itor, Itor> pi = mismatch(firstl, lastl, first2); cout « "First mismatch element in vlntl = " ; ShowElement(vlntl, pi. first); cout « "\n"; cout « "First mismatch element in vlnt2 = " ; ShowElement(vlnt2, pi.second); cout « "\n"; return 0;
)
template<class Container, class Iterator> void ShowElement(Containers c, Iterators itor) { if (itor != c.endO) ( if (itor != c.beginO) cout « *itor « "\tthe previous element is " « *(itor - 1) ; else cout « "first";
} else cout « "last"; }
Ниже приведен вывод, сгенерированный программой из листинга 7.7:
Глава 7
vlntl •= vlnt2 First mismatch element in vlntl = 4the previous element is 3
First mismatch element in vlnt2 = last
В векторе vlntl есть четыре элемента, в vlnt2 — три элемента. Поскольку их размеры не совпадают, то операцией equel() возвращается значение false. При выполнении операции mismatch() возвращается пара первых несовпадающих элементов в обоих векторах.
Алгоритмы мутирующих последовательностей
С помощью операций мутирующих последовательностей выполняются действия, которые меняют элементы в последовательности.
Операция fili()
Операция fill() обеспечивает присваивание значения каждому элементу в последовательности:
template<class Forwarditerator, class Т> void fill(Forwarditerator first, Forwarditerator last, const Ts value);
Функцию fill() можно определить следующим образом:
for (Forwarditerator fi = first; fi != last; ++fi) ( *fi = value;
}
Таким образом, класс T должен быть преобразуем в тип значения Forwarditerator, с тем чтобы оператор присваивания был действительным.
Операция fill_n()
Операция fill_n() похожа на fillo, за исключением того, что в результате ее выполнения присваивается значение только первым п элементам в последовательности [first, last):
template<class Outputiterator, class Size, class T> void fill_n(Outputiterator first, Size n, const 16 value);
Поскольку операцией fill() выполняется точно n присваиваний, то [first, first + n) должна быть действительной последовательностью.
Операция generate()
Операция generate() похожа на операцию fill(), но при ее выполнении каждому элементу в последовательности присваивается результат генератора, а не значение.
template<class Forwarditerator, class Generator> void generate(Forwarditerator first, Forwarditerator last, Generator f) ;
Возвращаемое значение функции f должно быть преобразуемо в тип элемента.
Операция generate_n()
Операция generate_n() подобна операции fill_n(), но при ее выполнении первым п элементам последовательности присваивается результат генератора, а не значение.
template<class Outputiterator, class Size, class Generator> void generate(Outputiterator first, Size n, Generator f) ;
Как и в операции fill(), в операции Generate() возвращаемое значение функции f должно преобразовываться в тип элемента.
В листинге 7.8 показано, как последовательности могут быть заполнены и сгенерированы.
Листинг 7.8. Заполнение последовательностей путем выполнения операций fillQ и generateQ #include <iostream> #include <vector> #include <functional> #include <algorithm> using namespace std;
template<class T>
Часть II
class Print: public unary_function<T, void> (
public:
void operator () (T6 argl) ( cout « argl « " ";
} );
int main() ( Print<int> DoPrint;
typedef vector<int>Vectorlnt;
typedef Vectorlnt::iterator Itor;
Vectorlntvlntl(10);
Itorfirstl = vlntl.begin();
Itorlastl = vlntl.end();
fill(firstl, lastl, 5);
cout « "vlntl after fill(firstl, lastl, 5):\n"; for_each(firstl, lastl, DoPrint);
cout « "\n\n";
generate(firstl, lastl, rand);
cout « "vlntl after generate(firstl, lastl, rand):\n"; for_each(firstl, lastl, DoPrint);
cout « "\n\n";
fill_n(firstl, 7, 8);
cout « "vlntl after fill_n(firstl, 3, 8):\n”;
for_each(firstl, lastl, DoPrint);
cout « ”\n\n";
generate_n(firstl, 5, rand);
cout « "vlntl after generate_n(firstl, 5, rand):\n"; for_each(firstl, lastl, DoPrint);
cout « "\n\n";
return 0;
)
Ниже приведен вывод, сгенерированный программой из листинга 7.8:
vlntl after fill(firstl, lastl, 5):
5555555555
vlntl after generate(firstl, lastl, rand):
41 18467 6334 26500 19169 15724 11478 29358 26962 24464
vlntl after fill_n(firstl, 3, 8):
8888888 29358 26962 24464
vlntl after generate_n(firstl, 5, rand):
5705 28145 23281 16827 9961 8 8 29358 26962 24464
В листинге 7.8 мы использовали операцию for_each() с простой унарной функцией DoPrint для отображения элементов в последовательности. Всем элементам в векторе vlntl присвоено значение 5. В операции generate() используется стандартная функция int rand() для генерации случайных целых для элементов vlntl. Операции fill_n() и generate_n() вызываются для присваивания значений произвольному числу элементов в vlntl.
Операция partition()
Операция partition() обеспечивает реорганизацию порядка элементов в последовательности так, чтобы элементы, которые удовлетворяют унарному предикату, размещались перед теми элементами, которые не удовлетворяют предикату.
template<class Bidirectionallterator, class UnaryPredicate>
Bidirectionallterator partition(Bidirectionallterator first,
Глава 7
Bidirectionallterator last, UnaryPredicate pred);
С помощью операции partition() возвращается итератор, указывающий на первый элемент, который не удовлетворяет предикату.
Операция stable_partition()
При выполнении операции stable_partition() последовательность делится на две группы: одна удовлетворяет предикату, а другая — нет. Относительный порядок элементов в каждой группе сохраняется.
template<class Bidirectionallterator, class UnaryPredicate > Bidirectionallterator stable_partition(Bidirectionallterator first, Bidirectionallterator last, UnaryPredicate pred);
Подобно partition(), операция stable_partition() возвращает итератор, указывающий на первый элемент, который не удовлетворяет предикату.
Операция random_shuffle()
Операция random_shuflle() случайно реорганизует порядок элементов в последовательности:
template<class RandomAccessIterator> void random_shuffle(RandomAccessIterator first, RandomAccessIterator last);
template<class RandomAccessIterator, class RandomNumberGenerator> void random_shuffle(RandomAccessIterator first, RandomAccessIterator last, RandomNumberGenerator6 rand);
При выполнении операции первой версии для создания индексов элементов используется внутренний генератор случайных чисел, а при выполнении второй — внешний генератор случайных чисел, которые передаются как аргумент.
В листинге 7.9 показаны операции random_shuffle() и partition().
Листинг 7.9. Реорганизация последовательностей с применением операций random_shuffle() и partitionQ ♦include <iostream> ♦include <vector> ♦include <functional> ♦include <algorithm> using namespace std;
template<class T> class Print: public unary_function<T, void> ( public: void operator () (16 argl) cout « argl « " " ;
) };
template<class T> class Partitionpredicate: public unary_function<T, bool> ( public: bool operator () (T6 argl) return (argl < 8) ;
) };
int main() ( Print<int> DoPrint;
typedef vector<int>Vectorlnt;
typedef Vectorlnt::iteratorItor;
Часть II
Vectorlnt vlntl (10);
for (int i = 0; i < 10; i++) vlntl[i] = i;
Itorfirstl = vlntl .begin () ;
Itorlastl = vlntl.end();
random_shuffle(firstl, lastl);
cout « "vlntl after random_shuffie(firstl, lastl):\n";
for_each(firstl, lastl, DoPrint);
cout « ”\n\n";
Vectorlntvlnt2= vlntl;
Itorfirst2 = vlnt2 .begin () ;
Itorlast2 = vlnt2.end();
PartitionPredicate<int> pred;
partition(firstl, lastl, pred);
cout « "vlntl after partition(firstl, lastl, pred):\n";
for_each(firstl, lastl, DoPrint);
cout « "\n\n";
stable_partition(first2, last2, pred);
cout « "vlnt2 after stable_partition(first2, last2, pred):\n";
for_each(first2, last2, DoPrint); cout « "\n\n";
return 0; }
Ниже приведен вывод, сгенерированный программным кодом из листинга 7.9:
vlntl after random_shuffie(firstl, lastl):
4302678951
vlntl after partition(firstl, lastl, pred):
4302671598
vlnt2 after stable_partition(first2, last2, pred):
4302675189
Унарный предикат объекта-функции класса PartitionPredicate определяется для того, чтобы проверить, меньше ли аргумент значения 8. Этот объект-функция используется операциями partition() в целях организации элементов вектора. Функция random_shuflle() вызывается для переупорядочивания элементов вектора. Результирующий вектор копируется в vlnt2, так чтобы мы могли сравнить результаты операций partition() и stable_partition(). Когда для vlntl выполняется операция partition(), исходный порядок элементов вектора не сохраняется. С другой стороны, операция stable_partition() не изменяет упорядоченность элементов в каждой группе.
Операция transform()
Операция transform() выполняется над каждым элементом в последовательности, а результат копируется в другую последовательность. Существует две перегруженные функции transform(). Функция первой версии выполняет унарную операцию:
template-Cclass Inputiterator, class Outputiterator, class UnaryOperation>
Outputiterator transform(Inputiterator first, Inputiterator last,
Outputiterator result, UnaryOperation f) ;
Функция второй версии выполняет бинарную операцию над двумя последовательностями и копирует результат в третью:
template<class Inputlteratorl, class Inputlterator2, class Outputiterator, class BinaryOperation>
Outputiterator transform(Inputlteratorl firstl, Inputlteratorl lastl,
Inputlterator2 first2, Outputiterator result, Binaryoperation binary_op);
В обеих версиях элементы исходной последовательности не изменяются.
Глава 7
Операция сору()
С помощью операции сору() элементы копируются из одной последовательности в другую.
template<class Inputiterator, class Outputlterator>
Outputiterator copy(Inputiterator first, Inputiterator last, Outputiterator dest);
Элементы в последовательности [first, last) копируются в последовательность, начинающуюся в dest. Элемент ‘first копируется в *dest, *(first+l) to *(dest+l) и т.д. При выполнении операции сору() возвращается итератор dest+(last-first), при этом значение dest не должно быть в диапазоне [first,last).
Операция copy_backward()
Операция copy_backward() обеспечивает копирование элементов из одной последовательности в другую в обратном порядке:
template<class Bidirectionallterator, class Bidirectionallterator >
Bidirectionallterator copy_backward(Bidirectionallterator first,
Bidirectionallterator last, Bidirectionallterator dest);
При выполнении операции copy_backward() элементы *(last-l) копируются в *(dest-l), *(last-2) в *(dest-2) и т.д. Будет возвращен итератор dest-(last-first). Поскольку элементы копируются в обратном порядке, от last-1, и хранится результат от dest-1, то для обеих последовательностей необходимо применять двунаправленные итераторы.
Операция reverse!)
Операция reverse() обеспечивает копирование элементов из одной последовательности в другую в обратном порядке:
template<class Bidirectionallterator>
void reverse(Bidirectionallterator first, Bidirectionallterator last);
При выполнении операции reverse() местами меняются элементы first+i и last-1, где 0 <= i <= (last- first)^.
Операция reverse_copy()
С помощью операции reverse_copy() копируются все элементы в одной последовательности в обратном порядке в другую последовательность:
template<class Bidirectionallterator, class Outputlterator>
Outputiterator reverse_copy(Bidirectionallterator first, Bidirectionallterator last, Outputiterator result);
После операции исходная последовательность остается неизменной.
Операция rotate!)
Операция rotate() обеспечивает прокрутку элементов в последовательности.
template<class Forwardlterator>
void rotate(Forwarditerator first, Forwarditerator middle, Forwarditerator last);
После выполнения этой операции все элементы "протягиваются" вперед в начало последовательности на middle-first позиций. Элементы, выталкиваемые с первых позиций, проталкиваются в конец последовательности.
Операция rotate_copy()
Операция rotate_copy() обеспечивает прокрутку элементов в одной последовательности и копирование результата в новую последовательность.
template<class Forwarditerator, class Outputlterator>
Outputiterator rotate_copy(Forwarditerator first, Forwarditerator middle,
Forwarditerator last, Outputiterator result);
Исходная последовательность после операции остается неизменной.
В листинге 7.10 проиллюстрированы некоторые из операций копирования.
Вопросы реализации Часть II
Листинг 7.10. Копирование последовательности элементов, путем выполнения операций копирования ♦include <iostream> ♦include <vector> ♦include <functional> ♦include <algorithm> using namespace std; template<class T> class Print: public unary_function<T, void> { public: void operator () (TS argl) ( cout « argl « " " ;
) );
template<class T> class Square: public unary_function<T, T> ( public:.
T operator () (IS argl) { return argl * argl;
} );
templatecclass Container> void ShowElements(Container& c, char* text); int main() { typedef vector<int>Vectorlnt;
typedef Vectorlnt::iterator Itor; Square<int> Sqr; 11 создайте вектор целых Vectorlntvlntl (5);
Itorfirstl =vlntl .begin ();
Itorlastl =vlntl.end();
for (int i = 0; i < 5; ++i)vlntl [i] = 3 * i;
ShowElements(vlntl, "vlntl created"); 11 создайте вектор для хранения результатов Vectorlntvlnt2(7);
Itorfirst2 =vlnt2 .begin ();
Itorlast2 =vlnt2.end();
fill(first2, last2, -1); ShowElements(vlnt2, "vlnt2 created"); // трансформируйте vlntl в vlnt2 transform(firstl, lastl, first2 + 1, Sqr); ShowElements(vlnt2, "Transformed vlntl to vlnt2"); // копируйте Vlntl обратно в vlnt2 fill(first2, last2, -1); copy_backward(firstl, lastl, last2 - 1) ; ShowElements(vlnt2, "vlntl copied backwards to vlnt2"); // переупорядочивание vlntl в vlnt2 fill(first2, last2, -1) ; rotate_copy(firstl, firstl + 1, lastl, first2 + 1) ; ShowElements(vlnt2, "rotate_copy vlntl to vlnt2"); 11 обращение vlnt2 reverse(first2, last2); ShowElements(vlnt2, "reversed vlnt2");
Глава 7 return 0;
} template<class Container> void ShowElements(Containers c, char* text) ( Print<Container::value type>DoPrint;
cout « text « ":\n"; for_each(c.begin(), c.end(), DoPrint); cout « "\n\n";
}
Ниже приведен вывод, сгенерированный программным кодом из листинга 7.10: vlntl created: 0 3 6 9 12 vlnt2 created: -1 -1 -1 -1 -1 -1 -1
Transformed vlntl to vlnt2: -1 0 9 36 81 144 -1 vlntl copied backwards to vlnt2: -1 0 3 6 9 12 -1 rotate_copy vlntl to vlnt2: -1 3 6 9 12 0 -1 reversed vlnt2: -1 0 12 9 6 3 -1
Унарный объект-функция класса Square определяется для вычисления квадрата значения аргумента. Функцию transform() можно вызвать для вычисления квадрата значения каждого элемента vlntl. Результат копируется в vlntl, начиная со второго элемента. Затем выполняется обратная копия элементов из vlntl в vlntl. Элементы копируются в vlntl со второго элемента last.
Затем элементы в vlntl прокручиваются и копируются в vlntl, начиная со второго элемента. Точка разрыва установлена во второй элемент со значением 3. Он становится первым элементом в последовательности, и все следующие элементы перемещаются влево на одно место. Теперь первый элемент становится последним.
Наконец, порядок элементов в vlntl реверсируется.
Операция replace!)
С помощью операции гер!асе() на новое значение заменяются все элементы последовательности, которые равны указанному значению.
template<class Forwarditerator, class Т> void replace(Forwarditerator first, Forwarditerator last, const TS old_value, const TS new_value);
Операция replace() реализована следующим образом: for (Forwarditerator fi = first; fi != last; ++fi) { if (*fi == old_value) *fi = new_value; )
Операция replace_if()
Операция replace_if() похожа на операцию replace(), но при ее выполнении не проверяется каждый элемент на значение, а проверяется, заставляет ли элемент унарный предикат вернуть значение true.
template<class Forwarditerator, class Predicate, class T> void replace_if(Forwarditerator first, Forwarditerator last, Predicate pred, const TS new_ value);
Операция replace_if() может быть реализована следующим образом:
Часть II
for (Forwarditerator fi = first; fi ! = last; ++fi) (
if (pred(*fi) == true) *fi = new_value;
}
Операция replace_copy()
При выполнении операции replace_copy() все элементы копируются из одной последовательности в другую и выполняется операция replace() над результирующей последовательностью.
template<class Inputiterator, class Outputiterator, class T> Outputiterator replace_copy(Inputiterator first, Inputiterator last, Outputiterator result, const T6 old_value, const T6 new_value);
Операция replace_copy_if()
Операция replace_copy_if() обеспечивает копирование всех элементов из одной последовательности в другую и выполнение над результирующей последовательностью операции replace_if().
template<class Iterator, class Outputiterator, class Predicate, class T> Outputiterator replace_copy_if(Iterator first, Iterator last, Outputiterator result, Predicate pred, const 16 new_value);
Операция removed
Операция remove() удаляет те элементы из последовательности, которые равны указанному значению.
template<class Forwarditerator, class Т> Forwarditerator remove(Forwarditerator first, Forwarditerator last, const T6 value);
Если удаляются n элементов, то remove() возвращает итератор last-n. Размер последовательности не меняется. Последние п элементов все еще можно разадресовать, но их значения не определены.
Операция remove_if()
С помощью операции remove_if() удаляются все элементы в последовательности, которые заставляют унарный предикат возвращать значение true.
template<class Forwarditerator, class Predicate> Forwarditerator remove_if(Forwarditerator first, Forwarditerator last, Predicate pred);
Подобно remove(), при выполнении операции remove_if() возвращается итератор last-n. Последние n элементов в последовательности содержат неопределенные значения.
Операция remove_copy()
Операция remove_copy() обеспечивает копирование всех элементов из одной последовательности в другую и выполнение над результирующей последовательностью операции remove().
template<class Inputiterator, class Outputiterator, class T> Outputiterator remove_copy(Inputiterator first, Inputiterator last, Outputiterator result, const T6 value);
Операция remove_copy_if()
Операция remove_copy_if() обеспечивает копирование всех элементов из одной последовательности в другую и выполнение над результирующей последовательностью операции remove_if().
template<class Inputiterator, class Outputiterator, class Predicate> Outputiterator remove_copy_if(InputIterator first, Inputiterator last, Outputiterator result, Predicate pred);
Операция unique!)
С помощью операции unique() проверяется последовательность для поиска элементов, имеющих одно и то же значение, и удаляется все, кроме первого элемента. Существует две версии функции unique(): одна
Глава 7
проверяет элемент на значение, а другая проверяет, заставляет ли элемент двоичный предикат возвратить значение true.
template<class Forwardlterator>
Forwarditerator unique(Forwarditerator first, Forwarditerator last);
template<class Forwarditerator, class BinaryPredicate>
Forwarditerator unique(Forwarditerator first, Forwarditerator last,
Binarypredicate pred);
Функции обеих версий возвращают итератор, указывающий на элемент past-the-end в результирующей последовательности. Подобно remove(), операция unique() не изменяет размер последовательности. Если удаляется п элементов, то последние п элементов в последовательности будут содержать неопределенные значения.
Операция unique_copy()
Операция unique_copy() обеспечивает копирование элементов из одной последовательности в другую и выполнение над новой последовательностью операции unique().
template<class Inputiterator, class Outputlterator>
Outputiterator unique_copy(Inputiterator first, Inputiterator last,
Outputiterator result);
templatecclass Inputiterator, class Outputiterator, class BinaryPredicate>
Outputiterator unique copy(Inputiterator first, Inputiterator last, Outputiterator result, Binarypredicate pred);
Обе версии возвращают итератор, указывающий на элемент past-the-end в результирующей последовательности. Если из результирующей последовательности удаляются п элементов, то последние п элементов будут иметь неопределенные значения.
Операция swap()
С помощью операции swap() меняются местами два элемента.
template<class Т> void swap(Тб а, Тб Ь) ;
Операция iter_swap()
С помощью операции iter_swap() меняются местами два элемента, указываемых двумя итераторами.
template<class Forwardlteratorl, class Forwardlterator2>
void iter_swap(Forwardlteratorl il, Forwardlterator2 i2) ;
Операцию iter_swap() можно реализовать следующим образом:
swap(*il, *i2)
Операция swap_ranges()
С помощью операции swap_ranges() меняются местами элементы в двух последовательностях.
template<class Forwardlteratorl, class Forwardlterator2>
Forwardlterator2 swap_ranges(Forwardlteratorl firstl, Forwardlteratorl lastl,
Forwardlterator2 first2);
При выполнении операции swap_ranges() меняются местами элементы в последовательностях [first 1,lastl) и [first2, first2+(lastl-firstl)). Будет возвращен итератор first2+(lastl-firstl). Элементы в обеих последовательностях должны быть преобразуемыми.
В листинге 7.11 демонстрируется применение операций replace(), remove() и swap().
Листинг 7.11. Манипулирование элементами с помощью операций replaceQ, removeQ и swapQ
#include <iostream>
#include <vector> #include <functional> #include <algorithm> using namespace std;
//
// Унарная функция для вывода своего аргумента в cout
Часть II
// templateCclass Т> class Print: public unary function<T, void> {~
public: void operator()(16 argl) { cout « argl « " ";
} );
// // Унарная функция для проверки, является ли целое четным числом. И bool IsEven(int var);
И 11 Отобразить все элементы в контейнере И template<class Container> void ShowElements(Containers c, char* text);
int main()
typedef vector<int>Vectorlnt;
typedef Vectorlnt::iterator Itor; // создайте вектор целых Vectorlntvlntl(7);'
Itorfirstl = vlntl.begin() ;
Itorlastl = vlntl.end();
for (int i = 0; i < 7; ++i) vlntl [i] = 3 * i; ShowElements(vlntl, "vlntl created");
II создайте вектор для хранения результатов Vectorlntvlnt2(7);
Itorfirst2 = vlnt2 .begin () ;
Itorlast2 = vlnt2.end();
fill(first2, last2, -1);
// замените в vlntl все четные числа значением 10 и копируйте результат в vlnt2 replace_copy_if(firstl, lastl, first2, IsEven, 10);
ShowElements(vlnt2, "replaced all even numbers with value 10");
// удалите в vlnt2 все дубликаты кроме первого vlnt2[3] = 10; Ц это создаст три последовательных 10 ShowElements(vlnt2, "vlnt2 with 3 consecutive 10s");
unique(first2, last2); ShowElements(vlnt2, "removed duplicate values from vlnt2");
// удалите в vlntl все четные числа и скопируйте результат в vlnt2 fill(first2, last2, -1);
remove_copy_if(firstl, lastl, first2, IsEven); ShowElements(vlnt2, "removed all even numbers");
II поменяйте местами второй и третий элементы в vlntl и // третий и четвертый элементы в vlnt2 ShowElements(vlntl, "Before the swap_ranges, vlntl is") ; ShowElements(vlnt2, "and vlnt2 is") ; swap_ranges (firstl + 1, firstl + 3, first2 + 2); ShowElements(vlntl, "After the swap_ranges, vlntl is") ; ShowElements(vlnt2, "and vlnt2 is"); return 0 ;
}
H
11 Унарная функция для проверки, является ли целое число четным. И bool IsEven(int var)
Глава 7
return ((var % 2) = 0) ; )
//
// Отобразить все элементы в контейнере //
template<class Container> void ShowElements(Containers c, char* text) {
PrintcContainer::value_type>DoPrint;
cout « text « ":\n";
for_each(c.begin() , c.end() , DoPrint); cout « "\n\n";
)
Ниже приведен вывод, сгенерированный программой из листинга 7.11:
vlntl created: 0 3 6 9 12 15 18
replaced all even numbers with value 10:
10 3 10 9 10 15 10
vlnt2 with 3 consecutive 10s:
10 3 10 10 10 15 10
removed duplicate values from vlnt2:
10 3 10 15 10 15 10
removed all even numbers:
3 9 15 -1 -1 -1 -1
Before the swap_ranges, vlntl is: 0 3 6 9 12 15 18
and vlnt2 is:
3 9 15 -1 -1 -1 -1
After the swap_ranges, vlntl is:
0 15 -1 9 12 15 18
and vlnt2 is: 3 9 3 6 -1 -1 -1
Функция replace_copy_if() вызывается для замены всех четных значений в vlntl на значение 10 и копирования результата в vlnt2. Затем значение 10 присваивается четвертому элементу в vlnt2 для создания последовательности из трех последовательных значений 10. Операция unique() позволяет удалить второе и третье значения 10 из данной мини-последовательности. При выполнении такой операции должны переместиться вперед два элемента и освободить память, изначально занимаемую ими с неопределенными значениями. При отображении vlnt2 видно, что четвертый и пятый элементы теперь имеют значения 15 и 10, как и ожидалось. Последние два элемента по-прежнему имеют значения 15 и 10, поскольку компилятор сохранил тот же блок памяти для vlntl — их начальные значения не были переписаны. Такое поведение однако не гарантировано, и вы никогда не должны пытаться получить их значения, прежде чем будут присвоены новые значения.
Операция remove_copy_if() показывает, что только три действительных элемента — 3, 9 и 15 — в vlntl копируются в vlnt2. Остальные элементы в vlnt2 неизменны.
Операции сортировки и связности последовательностей
Все операции сортировки и связности имеют два варианта. Первый из них для сравнения элементов подразумевает использование перегруженного оператора <, а второй — внешнего объекта сравнения. Это означает, что все отсортированные последовательности упорядочиваются в восходящем порядке. Однако, как видно из листинга 7.12, используя внешний предикат, можно выполнять сортировку и в обратном порядке.
Часть II
Операции сортировки
Библиотека STL предлагает множество функций сортировки, которые упорядочивают последовательности элементов в соответствии с разными требованиями. В нескольких следующих разделах рассматриваются такие операции.
Операция sort()
Операция sort() обеспечивает сортировку элементов в последовательности.
templateCclass RandomAccessIterator> void sort(RandomAccessIterator first, RandomAccessIterator last); template<class RandomAccessIterator, class Compare> void sort(RandomAccessIterator first, RandomAccessIterator last, Compare comp);
После операции сортировки элементы в последовательности упорядочиваются в восходящем порядке. Можно сортировать последовательность в нисходящем порядке, определив объект сравнения.
В листинге 7.12 показано, как это делается.
Листинг 7.12. Сортировка последовательности путем выполнения операции sortQ.
♦include <iostream>
♦include <vector> ♦include <functional> ♦include <algorithm> using namespace std;
H 11 Унарная функция для вывода аргументов в cout И templateCclass Т> class Print: public unary_function<T, void> ( public:
void operator () (TS argl) ( cout « argl « " " ; ) );
// // Функция сравнения И bool ReverseCompare(int argl, int arg2);
H // Отобразить все элементы в контейнере // templateCclass Container> void ShowElements(Containers c, char* text);
int main() (
typedef vectorcint>Vectorlnt;
typedef Vectorlnt::iterator Itor; // создать вектор целых Vectorlntvlntl (7);
Itorfirstl =vlntl.begin();
Itorlastl =vlntl. end ();
generate(firstl, lastl, rand);
ShowElements(vlntl, "Random number sequence");
// отсортировать последовательность по убыванию sort(firstl, lastl, ReverseCompare);
ShowElements(vlntl, "Sorted in descending order");
return 0;
}
//
Глава 7
// Функция сравнения И
bool ReverseCompare(int argl, int arg2) { return (argl > arg2);
}
//
// Отобразить все элементы в контейнере // template<class Container» void ShowElements(Containers c, char* text) (
Print<Container::value_type> DoPrint;
cout « text « " : \n" ;
for_each (c. begin () , c.end(), DoPrint); cout « "\n\n";
}
Ниже приведен вывод, сгенерированный программой из листинга 7.12:
Random number sequence:
41 18467 6334 26500 19169 15724 11478
Sorted in descending order:
26500 19169 18467 15724 11478 6334 41
Для операции sort() используется двоичный предикат, который сравнивает два аргумента и возвращает значение true, если первый аргумент меньше, чем второй. Определяем функцию ReverseCompare(), которая возвращает значение true, если первый аргумент больше второго. Когда операция sort() выполняется с применением ReverseCompare(), то сортировка последовательности происходит в нисходящем порядке.
В этом примере можно было бы просто использовать предикат greater(), но применение ReverseCompare() демонстрирует полезную общую технологию применения любых пользовательских функций в стандартных алгоритмах.
Операция stable_sort()
С помощью операции stable_sort() также сортируются элементы в последовательности. Если два или более элемента равны, то их относительный порядок сохраняется.
template<class RandomAccessIterator>
void stable_sort(RandomAccessIterator first, RandomAccessIterator last);
template<class RandomAccessIterator, class Compare>
void stable_sort(RandomAccessIterator first, RandomAccessIterator last, Compare comp);
Операция partial_sort()
При выполнении операции partial_sort() сортируется набор элементов с самыми меньшими значениями в последовательности.
template<class RandomAccessIterator»
void partial_sort(RandomAccessIterator first, RandomAccessIterator middle, RandomAccessIterator last);
template<class RandomAccessIterator, class Compare»
void partial_sort(RandomAccessIterator first, RandomAccessIterator middle, RandomAccessIterator last, Compare comp);
Данная операция сортирует меньшие middle-first элементы и помещает их в [first, middle). Порядок остальных элементов не определен. В листинге 7.13 демонстрируется операция partial_sort(), выполняемая с помощью реверсированной функции сравнения.
Листинг 7.13. Сортировка последовательностей с помощью partial sort() и stableQ
#include <iostream>
#include <vector>
#include <functional>
Вопросы реализации Часть II tinclude <algorithm> using namespace std; H 11 Унарная функция для вывода аргументов в cout // template<class Т> class Print: public unary_function<T, void> { public: void operator()(TS argl) ( cout « argl « " " ; ) ); // // Функция сравнения // bool ReverseCompare(int argl, int arg2); // // Отобразить все элементы в контейнере // templateCclass Container> void ShowElements(Containers c, char* text); int main() ( typedef vector<int>Vectorlnt;
typedefVectorlnt::iterator Itor;
// создать вектор целых Vectorlntvlntl(7);
Itorfirstl = vlntl .begin() ;
Itorlastl = vlntl.end();
generate(firstl, lastl, rand); ShowElements(vlntl, "Random number sequence"); // отсортировать последовательность по убыванию partial_sort(firstl, firstl + 3, lastl, ReverseCompare); ShowElements(vlntl, "Partially sorted three elements in descending order"); return 0; ) П I/ Функция сравнения // bool ReverseCompare(int argl, int arg2) ( return (argl > arg2); ) // // Отобразить все элементы в контейнере // template<class Container> void ShowElements(Containers c, char* text) (
Print<Container::value_type> DoPrint; cout « text « ":\n"; for_each(c.begin() , c.end() , DoPrint) ; cout « "\n\n"; )
Ниже приведен вывод, сгенерированный программой из листинга 7.13:
Глава 7
Random number sequence: 41 18467 6334 26500 19169 15724 11478
Partially sorted three elements in descending order: 26500 19169 18467 41 6334 15724 11478
Листинг 7.13 похож на листинг 7.12. Единственная разница заключается в том, что в нем сортируются только три самых больших элемента, а не вся последовательность.
Операция partial_sort_copy()
С помощью операции partial_sort_copy() частично сортируется последовательность, а результат копируется в новую последовательность.
template<class Inputiterator, class RandomAccessIterator> RandomAccessIterator partial_sort_copy(Inputiterator first, Inputiterator last, RandomAccessIterator result_first, RandomAccessIterator result_last);
templatecclass Inputiterator, class RandomAccessIterator, class Compare> RandomAccessIterator partial_sort—copy(Inputiterator first, Inputiterator last, RandomAccessIterator result_first, RandomAccessIterator result_last, Compare comp);
Эта операция обеспечивает сортировку самых меньших п элементов, где n=min(last-first, result_last- result_first), в последовательности [first,last), а результат копируется в [result_first, result_first+n). Будет возвращено min(result_first+n, result_last).
Операция nth_element()
С помощью операции nth_element() реорганизуется порядок элементов в последовательности так, чтобы элементы, которые меньше или равны п, помещались перед элементом п-m. Элементы в данной группе сортируются. Остальные элементы помещаются после элемента п, и их сортировка не гарантирована.
template<class RandomAccessIterator> void nth_element(RandomAccessIterator first, RandomAccessIterator nth, RandomAccessIterator last);
templatecclass RandomAccessIterator, class Compare> void nth_element(RandomAccessIterator first, RandomAccessIterator nth, RandomAccessIterator last, Compare comp);
Операция nth_element() показана в листинге 7.14.
Листинг 7.14. Упорядочение последовательности с применением операции nth element() ♦include <iostream> ♦include <vector> ♦include <functional> ♦include <algorithm> using namespace std;
II 11 Унарная функция для вывода аргументов в cout И templatecclass Т> class Print: public unary_functionCT, void> ( public: void operator () (TS argl) { cout « argl « " " ;
1 1;
H 11 ^умтлдля сравнения И bool ReverseCompare(int argl, int arg2);
8 Зак. 53
Часть II
//
// Отобразить все элементы в контейнере // template<class Container» void ShowElements(Containers c, char* text);
int main() (
typedef vector<int>Vectorlnt;
typedef Vectorlnt::iterator Itor;
// создать вектор целых Vectorlntvlntl (7);
Itorfirstl = vlntl .begin () ;
Itorlastl = vlntl.end();
generate(firstl, lastl, rand);
ShowElements(vlntl, "Random number sequence");
11 отсортировать последовательность по убыванию nth_element(firstl, firstl + 1, lastl, ReverseCompare); ShowElements(vlntl, "After nth_element () ") ;
return 0; }
H 11 ^грежп/лк сравнения И bool ReverseCompare(int argl, int arg2) ( return (argl > arg2);
)
П 11 Отобразить все элементы в контейнере И template<class Container> void ShowElements(Containers c, char* text) <
Print<Container::value_type>DoPrint;
cout « text « " : \n" ;
for_each (c .begin () , c.end(), DoPrint); cout « "\n\n";
)
Ниже приведен вывод программного кода из листинга 7.14:
Random number sequence:
41 18467 6334 26500 19169 15724 11478
After nth_element() :
26500 19169 18467 15724 11478 6334 41
И здесь для сортировки элементов в нисходящем порядке мы используем реверсированную функцию сравнения. Все элементы, которые больше 18467 (элементы 26500 и 19169), помещаются перед ним и сортируются в нисходящем порядке. Интересно, что элементы, которые меньше 18467, также появляются отсортированными. Однако такое поведение зависит от реализации.
Операции с бинарным поиском
Для всех операций бинарного поиска проверяемые последовательности должны быть отсортированы, прежде чем будут выполняться операции поиска. Результат поиска в несортированной последовательности не определен.
Операция lower_bound()
При выполнении операции lower_bound() проверяется отсортированная последовательность с целью найти первую позицию, в которой можно вставить значение без нарушения порядка сортировки последовательности.
227
templateCclass Forwarditerator, class T> Forwarditerator lower_bound(Forwarditerator first, Forwarditerator last, const TS value);
templateCclass Forwarditerator, class T, class Compare» Forwarditerator lower_bound(Forwarditerator first, Forwarditerator last, const TS value, Compare comp);
Возвращается итератор last, если не будет найдена подходящая позиция для вставки.
Операция upper_bound()
При выполнении операции upper_bound() проверяется отсортированная последовательность с целью найти последнюю позицию от начала, в которой можно вставить значение без нарушения порядка сортировки последовательности.
templateCclass Forwarditerator, class Т>
Forwarditerator upper_bound(Forwarditerator first, Forwarditerator last, const TS value);
templateCclass Forwarditerator, class T, class Compare»
Forwarditerator upper_bound(Forwarditerator first, Forwarditerator last, const TS value, Compare comp);
Подобно lower_bound, операция upper_bound() также возвращает итератор last, если нс может найти подходящую позицию для вставки.
Операция equal_range()
С помощью операции equal_range() ищется диапазон, в котором можно вставить значение без нарушения порядка сортировки последовательности.
templateCclass Forwarditerator, class Т>
pairCForwardlterator, ForwardIterator» equal_range(Forwarditerator first, Forwarditerator last, const TS value);
templateCclass Forwarditerator, class T, class Compare»
pairCForwardlterator, Forwarditerator» equal_range(Forwarditerator first, Forwarditerator last, const TS value, Compare comp);
Будет возвращена пара итераторов, указывающих на первую и последнюю позиции, в которых можно вставить значение.
Операция binary_search()
Операция обеспечивает binary_search() выполнение поиска двоичного значения в последовательности.
templateCclass Forwarditerator, class Т> bool binary_search(Forwarditerator first, Forwarditerator last, const TS value);
templateCclass Forwarditerator, class T, class Compare» bool binary_search(Forwarditerator first, Forwarditerator last, const TS value, Compare comp);
Будет возвращено true, если значение найдено, и false, если значение не найдено.
Операция тегде()
Операция merge() обеспечивает объединение двух отсортированных последовательностей.
templateCclass Inputlteratorl, class Inputlterator2, class Outputiterator» Outputiterator merge(Inputlteratorl firstl, Inputlteratorl lastl,
Inputlterator2 first2, Inputlterator2 last2, Outputiterator result) ;
templateCclass Inputlteratorl, class Inputlterator2, class Outputiterator, class Compare»
Outputiterator merge(Inputlteratorl firstl, Inputlteratorl lastl, Inputlterator2 first2, Inputlterator2 last2, Outputiterator result, Compare comp);
Вопросы реализации
Часть II
Если вводные последовательности содержат равные элементы, то элементы из первой последовательности помещаются первыми. В результате выполнения операции возвращается итератор past-the-end в результирующей последовательности. Результирующая последовательность не должна перекрываться вводными последовательностями. В противном случае результат будет не определен.
Операция inplace_merge()
С помощью операции inplace_merge() объединяются две последовательности, а результат помещается в исходную последовательность.
template<class Bidirectional!terator> void inplace_merge(Bidirectional!terator first, Bidirectionallterator middle, Bidirectionallterator last);
tamplate<class Bidirectionallterator, class Compare» void inplace_merge(Bidirectionallterator first, Bidirectionallterator middle, Bidirectionallterator last, Compare comp);
Будут объединены две последовательности — [first, middle) и [middle, last), а результат помещен назад в (first, last).
В листинге 7.15 продемонстрировано несколько операций бинарного поиска.
Листинг 7.15. Проверка последовательностей с применение операций бинарного поиска •include <iostream> •include <vector> •include <functional> •include «algorithm» using namespace std;
// // Унарная функция для вывода аргументов в cout И tamplate<class Т> class Print: public unary_function<T, void» ( public: void operator () (TS argl)
cout « argl « " " ; 1;
II ll Отобразить все элементы в контейнере И template<class Container» void ShowElements(Container& c, char* text);
int main() ( typedef vector<int>Vectorlnt;
typedefVectorlnt::iteratorItor;
// создать вектор целых Vectorlntvlntl (5);
Itorfirstl = vlntl .begin ();
Itorlastl = vlntl.end();
vlntl [0] = 1; vlntl[1] = 2; vlntl[2] = 2; vlntl[3] = 4; vlntl[4] = 5; ShowElements(vlntl, "vlntl");
// найти первую и последнюю позицию для вставки целого 2 Itor lb = lower_bound(firstl, lastl, 2);
Itor ub = upper_bound(firstl, lastl, 2); cout « "The first position to insert 2 is between " « *(lb - 1)
Глава 7
« " and " « * lb « "\n"; cout « "The last position to insert 2 is between " « * (ub - 1) « " and " « * ub « "\n\n";
// найти первую и последнюю позицию для вставки целого 2, используя equal_range pair<Itor, Itor> pi = equal_range(first!, lastl, 2);
lb = pi.first; ub = pi.second;
cout « "Results of equal_range:\n";
cout « "The first position to insert 2 is between " « *(lb - 1) « " and " « * lb « "\n";
cout « "The last position to insert 2 is between " « * (ub - 1) « " and " « * ub « "\n\n";
// найти целое 4 в vlntl
if (binary_search(firstl, lastl, 4)) cout « "Found 4.\n\n";
elsecout « "Can't find 4.\n\n";
II переприсвоить элементы vlntl vlntl[0] = 1; vlntl[1] = 4; vlntl[2] = 2; vlntl[3] = 2; vlntl[4] = 5;
ShowElements(vlntl, "vlntl reassigned");
// слияние на месте
inplace_merge(firstl, firstl + 2, lastl);
ShowElements(vlntl, "After inplace merge");
I/ переприсваивание элементов vlntl vlntl [0] = 1; vlntl [1] = 4; vlntl [2] = 2; vlntl[3] = 2; vlntl[4] = 5;
ShowElements(vlntl, "vlntl reassigned") ;
// слияние на месте двух несортированных последовательностей inplace_merge(firstl, firstl + 3, lastl);
ShowElements(vlntl, "After inplace merge on unsorted sequences"); return 0;
)
//
// Отобразить все элементы в контейнере И template<class Container» void ShowElements(Containers c, char* text) {
Print<Container::value_type> DoPrint; cout « text « " : \n";
f or_each(c. begin () , c.end(), DoPrint); cout « "\n\n";
}
Ниже приведен вывод из программы в листинге 7.15: vlntl: 1 2 2 4 5
The first position to insert 2 is between 1 and 2
The last position to insert 2 is between 2 and 4
Results of equal_range:
The first position to insert 2 is between 1 and 2
The last position to insert 2 is between 2 and 4
Часть II
Found 4.
vlntl reassigned:
1 4 2 2 5
After inplace merge:
1 2 2 4 5
vlntl reassigned:
1 4 2 2 5
After inplace merge on unsorted sequences:
1 4 2 2 5
При выполнении операции equal_range() ищется первая и последняя позиции, куда можно вставить значение. Это предпочтительная операция для использования, если необходимо найти обе позиции. Операция inplace_merge() требует, чтобы обе последовательности были отсортированы до ее вызова. Второе обращение к операции показывает, что inplace_merge() не влияет на две несортированные последовательности.
Операции над наборами
Набор представляет собой коллекцию объектов. Последовательность — это набор, в котором к элементам можно получить доступ с помощью итераторов. В STL определены операции над наборами для последовательностей, как описано далее в этой главе.
Операция included
С помощью операции indude() проверяется, все ли элементы имеются в одной и других последовательностях.
template<class Inputiterator1, class Inputlterator2>
bool includes(Inputlteratorl firstl, Inputlteratorl lastl,
Inputlterator2 first2, Inputlterator2 last2);
template<class Inputlteratorl, class Inputlterator2, class Compare> bool includes(Inputlteratorl firstl, Inputlteratorl lastl,
Inputlterator2 first2, Inputlterator2 last2, Compare comp);
Если каждый элемент последовательности [first!,Iast2) является также и членом последовательности [firstl,lastl), то при выполнении операции include() возвращается значение true. В противном случае возвращается значение false.
Операция set_union()
С помощью операции set_union() две отсортированные последовательности объединяются в третью.
template<class Inputlteratorl, class Inputlterator2, class Outputlterator>
Outputiterator set_union(Inputlteratorl firstl, Inputlteratorl lastl,-
Inputlterator2 first2, Inputlterator2 last2, Outputiterator result);
template<class Inputlteratorl, class Inputlterator2, class Outputiterator, class Compare>
Outputiterator set union(Inputlteratorl firstl, Inputlteratorl lastl,
Inputlterator2 first2, Inputlterator2 last2, Outputiterator result, Compare comp);
При выполнении операции set_union() сортируется результирующая последовательность и возвращается итератор past-the-end результирующей последовательности.
Операция set_intersection()
С помощью операции set_intersection() копируются элементы, общие для двух последовательностей, в третью последовательность.
templateCclass Inputlteratorl, class Inputlterator2, class Outputlterator>
Outputiterator set_intersection(Inputlteratorl firstl, Inputlteratorl lastl,
Inputlterator2 first2, Inputlterator2 last2, Outputiterator result);,
Глава 7
templateCclass Inputlteratorl, class Inputlterator2, class Outputiterator, class Compare»
Outputiterator set_intersection(Inputlteratorl firstl, Inputlteratorl lastl,
Inputlterator2 first2, Inputlterator2 last2, Outputiterator result, Compare comp);
При выполнении операции set_intersection() сортируется результирующая последовательность и возвращается итератор past-the-end результирующей последовательности.
Операция set_difference()
Операция set_difference() обеспечивает копирование элементов из одной последовательности, отсутствующих в другой, в третью.
templateCclass Inputlteratorl, class Inputlterator2, class Outputiterator»
Outputiterator set_difference(Inputlteratorl firstl, Inputlteratorl lastl,
Inputlterator2 first2, Inputlterator2 last2,
Outputiterator result);
templateCclass Inputlteratorl, class Inputlterator2, class Outputiterator, class Compare»
Outputiterator set_difference(Inputlteratorl firstl, Inputlteratorl lastl,
Inputlterator2 first2, Inputlterator2 last2,
Outputiterator result, Compare comp);
При выполнении операции set_difference() в последовательность [firstl, lastl) копируются элементы, отсутствующие в [first!, last!). Результирующая последовательность является отсортированной. Итератор past- the-end возвращается результирующей последовательности.
Операция set_symmetric_difference()
С помощью операции set_symmetric_difference() создается новая последовательность, содержащая элементы, не являющиеся членами обеих вводных последовательностей.
templateCclass Inputlteratorl, class Inputlterator2, class Outputiterator»
Outputiterator set_symmetric_difference(Inputlteratorl firstl, Inputlteratorl lastl, Inputlterator2 first2, Inputlterator2 last2, Outputiterator -result) ;
templateCclass Inputlteratorl, class Inputlterator2, class Outputiterator, class Compare»
Outputiterator set_symmetric_difference(Inputlteratorl firstl, Inputlteratorl lastl, Inputlterator2 first2, Inputlterator2 last2, Outputiterator result, Compare comp);
При выполнении операции set_symmetric_difference() элементы копируются в последовательность [firstl, lastl), но не в [first!, last!) и элементы в [first!, last!), но не в [firstl, lastl). Результирующая последовательность является отсортированной. Возвращается итератор past-the-end результирующей последовательности.
В листинге 7.16 демонстрируются операции, выполняемые над наборами.
Листинг 7.16. Обработка последовательностей с помощью операций, выполняемых над наборами #include Ciostream» #include Cvector» tinclude Cfunctional» #include Calgorithm» using namespace std;
// Унарная функция объекта для печати своего аргумента templateCclass Т> class Print: public unary_functionCT, void» ( public:
void operator () (TS argl) { cout « argl « " " ; ) ); int main()
Вопросы реализации Часть II typedef vector<int>Vectorlnt;
typedefVectorlnt::iteratorItor;
Print<int>DoPrint;
inti = 0;
// создать первый вектор целых Vectorlntvlntl(10);
Itorfirstl = vlntl.begin();
Itorlastl = vlntl.end() ;
for (i = 0; i < 10; ++i) vlntl [i] = 2 * i; // создать второй вектор целых Vectorlntvlnt2(8);
Itorfirst2 = vlnt2 .begin () ;
Itorlast2 = vlnt2.end();
for (i = 0; i < 8; ++i)vlnt2[i] = 3 * i;
// создать третий вектор целых для хранения операций set VectorlntvInt3(20, -1) ;
Itorfirsts = vlnt3 .begin ();
Itorlast3 = vlnt3.end();
// копировать объединение vlntl и vlnt2 в vlntS set_union(firstl, lastl, first2, last2, firsts); cout « "Union: elements in either vlntl or vlnt2:\n"; for_each(fjrstS, lasts, DoPrint); cout « "\n\n"; // случайное переупорядочивание элементов в vlntS random_shuffie(firsts, lasts); cout « "Random Shuffle: elaments in vlntS reordered:\n"; for_each(firsts, lasts, DoPrint); cout « "\n\n"; // сортировка vlntS sort(first3, lastS); cout « "Sorted:\n"; for_each(firsts, lasts, DoPrint); cout « "\n\n"; H копировать пересечение vlntl и vlnt2 в vlntS fill(first3, lasts, -1); set_intersection(firstl, lastl, first2, last2, firsts); cout « "Intersection: elements in both vlntl and vlnt2\n"; for_each(firsts, lasts, DoPrint); cout « "\n\n"; // копировать элементы vlntl, которых нет в vlnt2 в vlntS fill(firsts, lasts, -1) ; set_difference(firstl, lastl, first2, last2, firsts); cout « "Difference: elements in vlntl but not in vlnt2\n"; for_each(firsts, lasts, DoPrint); cout « "\n\n"; // копировать элементы, которые имеются только в vlntl или только в vlnt2 в vlntS fill(firsts, lasts, -1); set_symmetric_difference(firstl, lastl, first2, last2, firsts); cout « "Symmetric Difference: elements not in both vlntl and vlnt2\n"; for_each(firsts, lasts, DoPrint); cout « "\n\n"; return 0;
Ниже приведен вывод программного кода из листинга 7.16:
Union: elements in either vlntl or vlnt2: 0 2 3 4 6 8 9 10 12 14 15 16 18 21 -1 -1 -1 -1 -1 -1
Итераторы и алгоритмы STL Глава 7
Random Shuffle: elements in vlnt3 reordered: 6 -1 0 -1 15 18 12 16 -1 2 9 -1 10 -1 14 4 21 3 8 -1
Sorted:
-1 -1 -1 -1 -1 -1 0 2 3 4 6 8 9 10 12 14 15 16 18 21
Intersection: elements in both vlntl and vlnt2 0 6 12 18 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1
Difference: elements in vlntl but not in vlnt2 2 4 8 10 14 16 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1
Symmetric Difference: elements not in both vlntl and vlnt2 2 3 4 8 9 10 14 15 16 21 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1
Прежде всего создаются два целых вектора vlntl и vlnt2 и их членам присваиваются различные значения. Затем создается третий целый массив, достаточно большой, чтобы хранить все элементы из vlntl и vlnt2. И наконец, выполняется операция set_union() и генерируется объединение vlntl и vlnt2. Результат содержит все элементы из vlntl и vlnt2.
Операции над кучей
Куча представляет собой последовательность, в которой элементы с наибольшими значениями всегда следуют первыми. Элементы кучи выталкиваются и проталкиваются только из начала последовательности. Приоритетная очередь библиотеки STL обычно реализуется как куча. STL обеспечивает две операции куча- последовательность и две операции доступа к элементам кучи.
Операция make_heap()
Операция make_heap() обеспечивает преобразование последовательности в кучу.
templateCclass RandomAccessIterator> void make_heap(RandomAccessIterator first, RandomAccessIterator last);
templateCclass RandomAccessIterator, class Compare> void make_heap(RandomAccessIterator first, RandomAccessIterator last, Compare coop);
При выполнении этой операции последовательность [first, last) преобразуется в кучу [first, last).
Операция sort_heap()
Операция sort_heap() позволяет преобразовать кучу в последовательность, сортируя ее элементы.
templateCclass RandomAccessIterator> void sort_heap(RandomAccessIterator first, RandomAccessIterator last) ;
templateCclass RandomAccessIterator, class Compare> void sort_heap(RandomAccessIterator first, RandomAccessIterator last, Compare comp);
Результирующая последовательность отсортирована.
Операция push_heap()
Операция push_heap() позволяет добавлять в кучу новый элемент.
templateCclass RandomAccessIterator> void push_heap(RandomAccessIterator first, RandomAccessIterator last);
templateCclass RandomAccessIterator, class Compare> void push_heap(RandomAccessIterator first, RandomAccessIterator last, Compare comp);
Операция push_heap() принимает последовательность [first, last). Она предполагает, что диапазон [first, last-1) является кучей, и проталкивает элемент *(last-l) в кучу. После выполнения операции [first, last) становится кучей.
Операция pop_heap()
При выполнении операции pop_heap() из кучи удаляется верхний элемент.
templateCclass RandomAccessIterator> void pop_heap(RandomAccessIterator first, RandomAccessIterator last);
templateCclass RandomAccessIterator, class Compare> void pop_heap(RandomAccessIterator first, RandomAccessIterator last, Compare coup);
Вопросы реализации Часть II С помощью операции pop_heap() меняются местами первый и предпоследний элементы в куче [first, last). Результирующая последовательность [first, last) больше не является кучей, поскольку наибольший элемент теперь указывается итератором last-1. При выполнении операции pop_heap() последовательность [first, last-1) преобразуется обратно в кучу. Прежний самый большой элемент теперь доступен по итератору *(last-l). В листинге 7.17 демонстрируются операции над кучей.
Листинг 7.17. Операции над кучей #include <iostream> #include <vector> #include <functional> ♦include <algorithm> using namespace std; И-
11 Унарная функция для вывода аргументов в cout И template<class Т> class Print: public unary_function<T, void> public: void operator()(IS argl) ( cout « argl « " " ; ) 1; H // Отобразить все элементы в контейнере И template<class Container> void ShowElements(Containers c, char* text); int main() ( typedef vector<int>VectorInt;
typedef Vectorlnt::iterator Itor; // создать вектор целых Vectorlntvlntl(5);
Itorfirstl = vlntl .begin ();
Itorlastl = vlntl.end();
generate(firstl, lastl, rand); ShowElements(vlntl, "vlntl"); // сделать vlntl кучей make_heap(firstl, lastl); ShowElements(vlntl, "vlntl is now a heap"); // преобразовать vlntl в отсортированную последовательность firstl = vlntl .begin () ; lastl = vlntl.end(); sort_heap(firstl, lastl); ShowElements(vlntl, "vlntl is now a sorted sequence"); // сделать vlntl кучей вновь так, чтобы мы могли проверить операции pop и push firstl = vlntl .begin () ; lastl = vlntl. end (); makeheap(firstl, lastl); ShowElements(vlntl, "vlntl is now a heap again"); 11 операция pop firstl = vlntl .begin () ; lastl = vlntl.end(); pop_heap(firstl, lastl); cout « * (lastl - 1) « " popped, "; ShowElements(vlntl, "vlntl is no longer a heap");
Глава 7
11 операция push firstl = vlntl .begin () ; lastl = vlntl.end();
*(lastl - 1) = 32000;
ShowElements(vlntl, "vlntl is ready for a push"); push_heap(firstl, lastl);
ShowElements(vlntl, "New value pushed into vlntl"); return 0;
// // Отобразить все элементы в контейнере И templatecclass Container>
void ShowElements(Containers c, char* text) (
PrintCContainer::value_type> DoPrint;
cout « text « ":\n";
for_each (c .begin () , c.end(), DoPrint); cout « "\n\n";
)
Ниже приведен вывод программного кода из листинга 7.17:
vlntl:
41 18467 6334 26500 19169
vlntl is now a heap:
26500 19169 6334 18467 41
vlntl is now a sorted sequence:
41 6334 18467 19169 26500
vlntl is now a heap again:
26500 19169 18467 41 6334
26500 popped, vlntl is no longer a heap:
19169 6334 18467 41 26500
vlntl is ready for a push:
19169 6334 18467 41 32000
New value pushed into vlntl:
32000 19169 18467 41 6334
Мы превращаем случайную последовательность чисел vlntl в кучу, выполняя операцию make_heap(). Затем мы преобразуем кучу в отсортированную последовательность и обратно в кучу. При выполнении операции pop_heap() из кучи удаляется первый элемент, который имеет наивысшее значение, и помещает его в конец последовательности. Теперь вся последовательность [vlntl.begin(), vlntl.end()) больше не является кучей. Однако первые четыре элемента [vlntl.begin(), vlntl.end()-l) — все еще из кучи. Когда мы проталкиваем большее значение в кучу, то оно становится новой вершиной кучи. Обратите внимание на то, что поскольку с помощью операции над кучей обычно реорганизуются элементы в последовательности, то прежде полученные итераторы могут после таких операций стать недействительными. Тем не менее, в этом листинге мы переприсваиваем итераторам firstl и lastl новые значения begin() и end(), перед тем как использовать их для определения новой последовательности.
Операции минимизации и максимизации
Библиотека STL содержит функции, которые можно использовать для поиска максимального или минимального значений объектов.
Операция min()
С помощью операции min() возвращается наименьший из двух элементов.
templatecclass Т> const TS min(const TS a, const TS b) ;
templatecclass T, class Compare>
const TS min(const TS a, const TS b, Compare comp);
Часть»
Операция тах()
При выполнении операции шах() возвращается наибольший из двух элементов.
templatecclass Т> const TS max(const TS a, const TS Ь) ;
templatecclass Т, class Compare»
const TS max (const TS a, const TS b, Compare comp) ;
Операция min_element()
При выполнении операции min_element() возвращается итератор, указывающий на наименьший элемент в последовательности.
templatecclass Forwarditerator»
Forwarditerator min_element(Forwarditerator first, Forwarditerator last);
templatecclass Forwarditerator, class Compare»
Forwarditerator min_element(Forwarditerator first, Forwarditerator last, Compare comp);
Операция maxjelement()
С помощью операции max_element() возвращается итератор, указывающий на наибольший элемент в последовательности.
templatecclass Forwarditerator»
Forwarditerator max_element(Forwarditerator first, Forwarditerator last);
templatecclass Forwarditerator, class Compare»
Forwarditerator max_element(Forwarditerator first, Forwarditerator last, Compare comp);
Операция lexicographical_compare()
При выполнении операции lexicographical_compare() сравниваются две последовательности в лексикографическом порядке.
templatecclass Inputlteratorl, class Inputlterator2>
bool lexicographical_compare(Inputlteratorl firstl, Inputlteratorl lastl, Inputlterator2 first2, Inputlterator2 last2);
templatecclass Inputlteratorl, class Inputlterator2, class Compare» bool lexicographical_compare(Inputlteratorl firstl, Inputlteratorl lastl,
Inputlterator2 first2, Inputlterator2 last2, Compare comp);
Эту операцию лучше объяснить, используя следующий псевдокод:
templatecclass Inputlteratorl, class Inputlterator2>
bool lexicographical_compare(Inputlteratorl firstl, Inputlteratorl lastl, Inputlterator2 first2, Inputlterator2 last2) i
II пройти по двум последовательностям до тех пор, пока одна не будет исчерпана while ((firstl != lastl) SS (first2 != Iast2)) { // сравнить каждую пару элементов if (*firstl C *first2)return true;
if (*firstl » *first2)return false;
II если *firstl == *first2, то перейти к следующей паре элементов ++firstl;
++first2; )
if ((firstl == lastl) SS (first2 •= last2)) {
II первая последовательность короче второй return true;
} else
(
Глава 7 return false; } )
В листинге 7.18 демонстрируется операция lexicographical_compare().
Листинг 7.18. Сравнение последовательностей с помощью lexicographical compare()
♦include <iostream>
♦include <string>
♦include <algorithm> using namespace std;
int main()
string si = "abed";
string s2 = "abd";
if (lexicographical_compare (si .begin () , sl.end(), s2.begin(), s2.end())) cout « "[" « si « "] < [” « s2 « "]\n";
else cout « ”[" « si « "] >= [" « s2 « "]\n";
s1 = "abed";
s2 = "abc"; if (lexicographical_compare(si.begin() , sl.end(), s2.begin(), s2.end())) cout « "[" « si « "] < [" « s2 « "]\n";
else cout « "[" « si « "] >= [" « s2 « "]\n";
si = "abed";
s2 = "abed";
if (lexicographical_compare (si .begin () , sl.end(), s2.begin(), s2.end())) cout « "[" « si « "] < [" « s2 « "]\n";
else cout « "[" « si « "] >= [" « s2 « "]\n";
return 0;
Ниже приведен вывод, сгенерированный программой в листинге 7.18:
[abed] < [abd]
[abed] >= [abc]
[abed] >= [abed]
Стандартные строки C++ являются последовательностями. В листинге 7.18 мы используем функцию lexicographical_compare() для сравнения двух строк с разными значениями.
Генераторы перестановок
Последовательность из п элементов можно упорядочить п!=п*(п - 1)*(п - 2)*.. .*2*1 способами. Каждая из упорядоченностей будет числом перестановок последовательности. Часто бывает полезно знать следующую или предыдущую перестановку по сравнению с текущей упорядоченностью последовательности. В библиотеке STL содержится два генератора перестановок, каждый из которых описан в следующих разделах.
Операция next_permutation()
С помощью операции next_permutation() генерируется следующая перестановка последовательности.
templateCclass Bidirectionallterator>
bool next_permutation(Bidirectionallterator first, Bidirectionallterator last) ;
templateCclass Bidirectionallterator, class Compare>
bool nextjpermutation(Bidirectionallterator first, Bidirectionallterator last, Compare comp);
Если при выполнении этой операции находится следующая перестановка, то реорганизуется последовательность к найденной перестановке и возвращается значение true. В противном случае упорядочивается последовательность к первой перестановке и возвращается значение false.
Часть И
Операция prev_permutation()
Операция prev_permutation() позволяет генерировать предыдущую перестановку последовательности.
templatecclass BidirectionalIterator>
bool prev_permutation(Bidirectionallterator first, Bidirectionallterator last) ;
template<class Bidirectionallterator, class Compare>
bool prev_permutation(Bidirectionallterator first, Bidirectionallterator last, Compare comp);
Если при выполнении этой операции находится предыдущая перестановка, то реорганизуется последовательность к найденной перестановке и возвращается true. В противном случае упорядочивается последовательность к первой перестановке и возвращается значение false.
В листинге 7.19 демонстрируется использование операций перестановок.
Листинг 7.19. Использование операций перестановок
#include <iostream>
#include <string>
#include <algorithm> using namespace std;
int main() (
string s = "abdc";
if (prev_permutation (s .begin () , s.end())) cout « "s = " « s « "\n";
else cout « "no previous permutation: s = " « s « "\n";
if (prev_permutation(s.begin(), s.end())) cout « "s = " « s « "\n";
else.
cout « "no previous permutation: s = " « s « "\n";
if (next_permutation(s.begin(), s.end())) cout « "s = " « s « "\n";
else
cout « "no next permutation: s = " « s « "\n";
if (next_permutation (s .begin () , s.end())) cout « ”s = " « s « ”\n";
else
cout « "no next permutation: s = " « s « "\n";
return 0;
Ниже приведен вывод, сгенерированный программным кодом в листинге 7.19:
s = abed
no previous permutation: s = deba
no next permutation: s = abed s = abdc
Операция prev_permutation() позволяет найти предыдущую перестановку последовательности abdc. Случилось так, что это первая перестановка последовательности из четырех символов a, b, с, d. Поскольку это первая перестановка, то предыдущая отсутствует. Следующая операция prev_permutation() позволяет реорганизовать строку к deba, которая является последней перестановкой последовательности. Это приводит к тому, что первая операция next_permutation() терпит неудачу в поиске следующей перестановки и при этом строка реорганизуется к abed, первой перестановке последовательности. Последняя операция next_permutation() обеспечивает поиск следующей перестановки и реорганизацию строки к найденной перестановке.
Стандартные функции
Объекты-функции часто используются стандартными алгоритмами как аргументы. Например, стандартный алгоритм for_each() принимает унарный объект-функцию в качестве своего третьего аргумента:
for_each(Iterator first, Iterator last, Functionobject f)
Глава 7
Может возникнуть желание сравнить каждый элемент с заданным значением и подсчитать, сколько элементов больше чем данное значение. Предикат greater кажется хорошим кандидатом на объект-функцию f Единственная проблема заключается в том, что алгоритм for_each() нуждается в унарной функции, а предикат greater является бинарной функцией. Что же делать?
Можно написать собственный алгоритм for_each() или собственный унарный предикат greater для сравнения аргумента со значением. Любой из подходов вынуждает переизобретать колесо. Библиотека Standard C++ Library предоставляет набор функций, порожденных от стандартных функций с минимальными отклонениями. Такими функциями являются компоновщик (binder), адаптер (adapter) и функция отрицания (negater function).
Функции компоновщика
Функция binder может связывать один аргумент двоичной функции с константой и результироваться в унарную функцию. Библиотека Standard C++ Library обеспечивает две функции binder: bindlst() и bind2st(). Рассмотрим, как определена функция bindlst(). Библиотека Standard C++ Library определяет базовый класс для функции bindlst:.
template<class BinaryOperation> class binderlst : public unary_function<BinaryOperation::second_argument_type, Binaryoperation::result_type> ( public: binderlst(const Binaryoperation & x, const BinaryOperation::first_argument_types const_argl) : op(x), value(const_argl) (} result_type operator()(const second_argument_types arg2) const ( return BinaryOperation(value, arg2);
protected: BinaryOperationop;
BinaryOperation::first_argument_type value; };
Конструктор класса binderlst принимает два аргумента: объект операции класса и постоянное значение для первого аргумента. Такая организация эффективно связывает новый объект binderlst с операцией класса и первым аргументом. Мы можем использовать этот базовый класс для создания функции компоновки, которая сравнивает объект и значение:
templatecclass BinaryOperation, class Т> binderlst<BinaryOperation> bindlst(const Binaryoperations op, const TS argl);
Перегруженный оператор () класса bindenrlst можно использовать для выполнения бинарной операции:
BinaryFunction<T>::result_type bindlst(TS arg2)
В листинге 7.20 демонстрируется использование функции bindlst() для разделения строчных и прописных букв в строке.
Листинг 7.20. Применение функции bindlstQ
♦include <iostream>
♦include <string> ♦include <functional> ♦include <algorithm> using namespace std;
int main() (
string s = "aBCdefGH";
partition (s .begin () , s.end(), bindlst (greater<char>() , 'a')); cout « "s = " « s « "\n";
return 0; }
Часть II
Ниже приведен вывод программного кода в листинге 7.20:
s = HBCGefda
Мы использовали функцию bindlst() для связывания первого аргумента функции greater() с буквой а. Поскольку строчная а больше, чем все прописные буквы, то функция bindlst(greater<char>(), 'а') возвращает значение true для прописных букв в строке. С помощью операции partition() все заглавные буквы перемещаются вперед, а все строчные буквы — в конец строки.
Функция bind2nd() определена подобно функции bindlst().
Функции адаптера
Большинство стандартных алгоритмов для выполнения конкретных операций над элементами использует обычные функции. Однако многие программисты, работающие на языке C++, для манипулирования объектами привыкли определять функции-члены. Предположим, что вы определили список указателей на оконные объекты и используете функцию-член Show() для отображения окон:
class MyWindow
{
public:
void Show();
};
list<MyWindow*> windowPtrList;
// добавить указатели на оконные объекты к списку
И - • •
for_each(windowPtrList.begin(), windowPtrList.end(),MyWindow::Show);// ошибка!
Проблема в том, что функция for_each() ожидает, что третий аргумент будет функцией f(). Функция MyWindow::Show, однако, должна быть вызвана через WindowObject.Show(). Библиотека Standard C++ Library обеспечивает набор адаптеров функций-членов, которые помогают использованию функций-членов в алгоритмах.
Функция-член adapter mem_fun() преобразует функцию-член без аргументов в унарную функцию с указателем this. Она определена с помощью базового класса mem_fun_t:
template <class S, class T>
class mem_fun_t : public unary_function<T*, S> ( public:
explicit mem_fun_t(S (T::*p)());
S operator()(T* p) ;,
};
templatecclass S, class T> mem_fun t<S, T> mem_fun(S (T::*f)()) t return mem_fun_t<S, T>(f);
}
В листинге 7.21 показано применение адаптеров функций-членов для того, чтобы предоставить возможность алгоритмам вызывать функции-члены.
Листинг 7.21. Передача функций-членов алгоритмам с помощью адаптеров функций-членов ♦include <iostream> ♦include <list> ♦include <functional> ♦include <algorithm> using namespace std; typedef unsigned int UINT;
class MyWindow { public: MyWindow(UINT newID = 0) : mlD(newID) (}
void Show() const {
cout « "Showing window " « mID « "\n";
Глава 7
//
Н MSVC++ 5 version
//
//int Show()
//<
Нcout « "Showing window " « mID « "\n";
//return 0;
//)
private:
UINT mID;
);
int main()
{
// создать и добавить элементы к списку указателей на оконные объекты MyWindow*pWin;
list<MyWindow*> winPtrList;
for (UINT winlD = 0; winlD < 5; ++winID)
pWin = new MyWindow (winlD) ; winPtrList.push_back(pWin);
)
pWin = 0 ;
// показать каждое окно в списке
for_each(winPtrList.begin(), winPtrList.end(), mem_fun(MyWindow::Show)); return 0;
}
Ниже приведен вывод программного кода из листинга 7.21:
Showing window 0
Showing window 1
Showing window 2
Showing window 3
Showing window 4
Функция-член Show() класса MyWindow определена для печати идентификаторов объектов MyWindow. В функции main() создается список из пяти указателей на объекты MyWindow. Функция for_each() вызывается для печати ID для каждого объекта MyWindow. Адаптер функции-члена mem_fun() используется для вызова функции-члена Show(). Поскольку mem_fun() требует указателя функции-члена, требуется передать адрес функции Show().
ПРИМЕЧАНИЕ
Из-за ошибки в Microsoft Visual C++ Version 5 следует использовать второй вариант функции-члена Show() при компиляции с применением Visual C++ 5.
Функции адаптера указатель-функция
Для манипулирования элементами в последовательности стандартный алгоритм может использовать функцию или указатель на функцию. В листинге 7.22 показано применение функции и указателя на функцию алгоритмом for_each().
Листинг 7.22. Использование в алгоритмах функций и указателей на функции
♦include <iostream>
♦include <list>
♦include <functional>
♦include <algorithm> using namespace std;
typedef unsigned inttypedef unsigned intUINT;UINT;
typedef unsigned int
typedef unsigned int
UINT;
UINT;
Вопросы реализации Часть II class MyWindow ( public: MyWindow(UINT newID ■ 0) : mlD(newID) {} UINT GetIDO const { return mID; }
private: UINT mID;
);
void ShowWindowUnary(const MyWindowS win);
int main() (
// создать и добавить элементы к списку указателей на оконные объекты MyWindow*pWin;
list<MyWindow>winList;
UINTwinlD;
for (winlD = 0; winlD < 5; ++winID) <
pWin = new MyWindow (winlD) ;
winList.push_back(*pWin); ) pWin = 0 ;
// показать каждое окно в списке — это хорошо.
cout « "ShowWindowUnary():\п";
for_each(winList.begin(), winList.end(), ShowWindowUnary);
cout « "\nPointer to ShowWindowUnary()\n";
for_each(winList.begin(), winList.end(), SShowWindowUnary);
return 0; )
void ShowWindowUnary(const MyWindowS win) (
cout « "Showing window " « win.GetIDO « ”.\n"; }
Ниже приведен вывод, сгенерированный программным кодом в листинге 7.22:
ShowWindowUnary():
Showing window 0.
Showing window 1.
Showing window 2.
Showing window 3.
Showing window 4.
Pointer to ShowWindowUnary():
Showing window 0.
Showing window 1.
Showing window 2.
Showing window 3.
Showing window 4.
Мы определили унарную функцию для печати переменной-члена mID объекта MyWindow. Эта функция вызывается операцией for_each() для печати themlD для каждого объекта MyWindow в winList. Указатель на данную функцию может также использоваться for_each() для выполнения той же самой операции, что демонстрируется здесь.
Для функций binder дело обстоит иначе. Их нельзя использовать для связывания указателя на функцию, поскольку они требуют копии функции. В результате Standard C++ Library предлагает два адаптера, чтобы предоставить возможность использовать указатели функций. Первый адаптер используется для унарных функций:
template <class Arg, class Result>
class pointer_to_unary function : public unary_function<Arg, Result>
(“
public:
Итераторы и алгоритмы STL Глава 7 explicit pointer_to_unary_function(Result (* f)(Arg)); Result operator()(Arg x) const; }; template <class Arg, class Result» pointer_to_unary_function<Arg, Result» ptr_fun(Result (* f)(Arg));
Второй адаптер используется для двоичных функций: template <class Argl, class Arg2, class Result» class pointer_to binary_function : public binary_function<Argl,Arg2,Result» (“
public: explicit pointer_to_binary_function(Result (* f)(Argl, Arg2)); Result operator()(Argl x, Arg2 y) const; ); template <class Argl, class Arg2, class Result» pointer_to_binary_function<Argl,Arg2,Result» ptr_fun(Result (* f)(Argl, Arg2)); После этого в операциях binder можно использовать указатели.
Функции отрицания
В листинге 7.12 для сортировки последовательности в обратном порядке использовалась функция ReverseCompare(). Это удобный, но не интуитивный подход к сортировке. В том примере мы хотели отсортировать элементы в порядке не-меньше-чем-или-равно. В Standard C++ Library предлагаются два отрицания для предикатов: одно — для унарных предикатов и другое — для двоичных.
template <class Predicate» class unary_negate : public unary_function<Predicate::argument_type,bool> ( public: explicit unary_negate(const Predicates pred); bool operator()(const argument_typeS x) const;
);
template <class Predicate» class binary_negate : public binary_function<Predicate::first_argument_type, Predicate::second_argument_type, bool» ( public: explicit binary_negate(const Predicates pred); bool operator() (const first__argument_typeS x, const second_argument_typeS y) const;
);
template <class Predicate» unary_negate<Predicate> notl(const Predicates pred);
template <class Predicate» binary_negate<Predicate> not2(const Predicates pred);
В листинге 7.12 замените оператор sort(firstl, lastl, ReverseCompare) на sort(firstl, lastl, not2(less_equal<int>())), который делает то же самое, что и пользовательский предикат ReverseCompare().
Резюме
Библиотека Standard C++ Library предлагает иерархию итераторов, которые можно применять для доступа к последовательностям. Для выполнения распространенных операций, таких как сортировка и пересечение последовательностей, в Standard C++ Library определено множество стандартных алгоритмов.
Поставщики компиляторов часто реализуют стандартные алгоритмы в наиболее эффективной и надежной форме для конкретной операционной системы. Использование стандартных алгоритмов в общем случае предпочтительнее для функций, написанных вручную и выполняющих аналогичные операции.
Исключение конфликтов имен
В ЭТОЙ ГЛАВЕВ ЭТОЙ ГЛАВЕФункции и классы, разрешаемые по именам
В ЭТОЙ ГЛАВЕ
В ЭТОЙ ГЛАВЕ
Создание пространства имен
Использование пространства имен
Ключевое слово usingКлючевое слово usingПсевдоним пространства имен
Ключевое слово using
Ключевое слово using
Неименованное пространство имен
Стандартное пространство имен
Глава 8
Конфликты имен являются источником раздражения для разработчиков, использующих как язык С, так и C++ уже много лет. Тем более странно, что Комитет по стандартизации C++ лишь недавно уделил внимание данной проблеме, представив механизм пространства имен. По мнению автора, тема конфликтов имен должна была решаться много лет назад.
Поскольку Комитет по стандартизации до недавнего времени эту проблему не затрагивал, некоторые поставщики не включали в текущие версии своих компиляторов поддержку пространств имен. Тем не менее, автор рекомендует разобраться с пространствами имен сейчас, даже если ваш компилятор не включает поддержку этого механизма.
Конфликт имен происходит тогда, когда в двух разных транслирующихся модулях встречаются дублирующиеся имена с совпадающей областью видимости. Наиболее часто такое происходит в двух библиотечных пакетах. Например, библиотека контейнерных классов объявляет и реализует класс List. Не удивительно потому, что класс List используется и в библиотеке окон. Предположим, что для своего приложения вы хотите организовать поддержку списка окон, а также используете для этого возможности класса List, найденного в библиотеке контейнерных классов. Таким образом, вы объявляете экземпляр List для хранения группы окон. К вашему разочарованию обнаруживается, что те функции-члены, которые вы хотели вызвать, недоступны. В чем дело? Очевидно, что компилятор предпочел объявление List из контейнера List, но то, что вам требуется, — это List в библиотеке окон.
Пространства имен используются для разделения глобального пространства имен и устранения или, по крайней мере, уменьшения количества конфликтов имен. Пространства имен похожи на классы и структуры. Синтаксис очень сходен. Элементы, объявленные внутри пространства имен, принадлежат пространству имен. Все элементы внутри пространства имен обладают общедоступной видимостью. Одни Пространства имен могут быть вложены в другие пространства имен. Функции можно объявлять как внутри тела пространства имен, так и вне его. Если функция определена вне тела пространства имен, то она должна квалифицироваться именем пространства.
Здесь обсуждаются эти и другие детали во всех подробностях. Но прежде всего рассмотрим несколько базовых правил, касающихся разрешения имен, требований к уникальности имен, и другие вопросы пространства имен.
Функции и классы, разрешаемые по именам
В ходе анализа исходного программного кода и построения списка функций и имен переменных компилятор проверяет, не вступают ли имена в конфликты. Конечно, компилятор не может разрешить все конфликты имен. В тех случаях, когда он не может разрешить конфликты, в дело вступает компоновщик. Компилятор не может проверить имена в кэш-памяти транслируемых модулей. Если бы компилятор мог это сделать, то он мог бы (потенциально) принять ответственность компоновщика на себя. Вы, наверное, видели следующее сообщение об ошибке, выдаваемое компоновщиком: Identifier multiply defined (Identifier, конечно, это какой-то именованный тип). Это сообщение компоновщика появляется в том случае, если одно и то же имя определено в одной и той же области видимости в разных транслирующихся модулях. Ошибка компилятора возникает тогда, когда имя переопределяется в пределах одного файла, имеющего ту же самую область видимости. В следующем примере при компилировании и редактировании будет выдано сообщение об ошибке компоновщика:
М файл first.cpp
int integervalue = 0 ;
int main( ) (
int integervalue = 0 ;
П . . .
return 0 ;
) ;
H файл second, фр
int integerValue = 0 ;
// конец second.cpp
Компоновщик поставит следующий диагноз: in second.obj: integerValue already defined in first.obj. Однако если эти имена находятся в разных областях видимости, то компилятор и компоновщик "жаловаться" не станут. От компилятора можно также получить сообщение, касающееся маскировки идентификатора. Странно, однако, что компилятором автора даже при максимальном уровне сообщений в приведенном примере ничего не сообщено о маскировке имени. Предупреждение, которое автор любит давать людям, таково: "Не доверяйте своему компилятору!" Сам автор не всегда доверяет компилятору, поэтому часто использует специ-
Часть II
альную программу в качестве предшественника компилятора. Если у вас нет такой вспомогательной программы или вы ею не пользуетесь, то настоятельно рекомендуется достать ее. Хорошая программа предупредит вас о многих критических ситуациях, а не только о конфликтах имен.
В приведенном примере целое, объявленное в main(), не конфликтует с целым вне main(). Автор упомянул, что его компилятор даже при максимальном уровне сообщений ничего не сообщает о потенциальном конфликте. Причина того, что имена не конфликтуют, заключается в их разных областях видимости. Переменная integerValue, определенная внутри main(), скрывает integerValue, определенную вне main(). Если вы хотите использовать переменную integerValue, определенную в глобальном пространстве имен, то должны использовать для имени префикс оператора разрешения области видимости. Рассмотрим следующий пример, который присваивает значение 10 переменной integerValue вне main(), а не переменной integerValue, определенной внутри main():
// файл first.срр
int integerValue = 0 ;
int main( )
(
int integerValue = 0 ;
::integerValue = 10 ; //назначить глобально "integerValue" II • • .
return 0 ;
} ;
II файл second, cpp int integervalue = 0 ;
II конец second.cpp
Проблема с двумя глобальными целыми, определенными вне какой-либо функции, заключается в том, что они имеют одинаковые имена и видимость.
Автор использует термин видимость для обозначения области видимости определенного объекта, независимо от того, является ли он переменной, классом или функцией. Например, переменная, объявленная и определенная вне любой функции, имеет файловую или глобальную область видимости. Видимость такой переменной распространяется от точки ее определения до конца файла. Переменная имеет блоковую или локальную область видимости, если находится внутри блоковой структуры. Наиболее распространенными являются примеры, когда переменные определены внутри функций. В следующем примере показана область видимости переменных:
int globalScopelnt = 5 ;
void f( )
(
int localScopelnt = 10 ;
)
int main( ) <
int localScopelnt = 15 ;
{
int anotherLocal =20 ;
int localScopelnt = 30 ;
} return 0 ;
}
Первое определение globalScopelnt видимо внутри функций f() и main(). Следующее определение находится внутри функции f() и называется localScopelnt. Эта переменная имеет локальную область видимости, что означает, что она видна только внутри определяющего ее блока. Функция main() не имеет доступа к localScopelnt функции f(). Когда функция возвращает управление, то localScopelnt уходит из области видимости. Третье определение, которое также именуется localScopelnt, находится в функции main(). Эта переменная имеет блоковую область видимости. Как только мы достигаем закрывающей скобки, две данные переменные теряют свою видимость. Обратите внимание, что localScopelnt прячет переменную localScopelnt, определенную как раз перед открывающей скобкой (вторую localScopelnt, определенную в программе). Когда программа перемещается за открывающую скобку, вторая определенная переменная localScopelnt восстанавливает видимость. Любые изменения, сделанные по отношению к localScopelnt, определенной внутри скобок, не влияют на содержимое внешней переменной localScopelnt.
Глава 8
Имена могут обладать внутренней или внешней связываемостью. Эти термины относятся к использованию или доступности имени между множеством транслируемых модулей или внутри одного транслируемого модуля. На любое имя, обладающее внутренней связываемостью, можно сослаться только внутри транслируемого модуля, в котором имя определено. Например, переменная, определенная для внутренней связываемости, может разделяться функциями внутри одного и того же транслируемого модуля. Имена, имеющие внешнюю связываемость, доступны другим транслирующимся модулям. Следующий пример демонстрирует внутреннюю и внешнюю связываемость:
// файл: first.срр
int externalInt = 5 ;
const int j = 10 ;
int main()
{ return 0 ;
)
// файл: second.cpp
extern int externalInt ;
int anExternallnt = 10 ;
const int j = 10 ;
Переменная externallnt, определенная в файле first.cpp, обладает внешней связываемостью. Хотя она определена в first.cpp, файл second.cpp также имеет доступ к ней. Две переменные js, имеющиеся в обоих файлах, являются const, что по умолчанию придает им внутреннюю связываемость. Умолчание const можно переопределить, обеспечив явное объявление, как показано ниже:
// файл: first.cpp
extern const int j = 10 ;
11 файл: second.cpp
extern const int j ;
#include <iostream>
int main()
{ std::cout « "j is ” « j « std::endl ; return 0 ;
}
После выполнения этого примера будет отображено:
j is 10
Комитет по стандартизации высказался против следующего типа использования:
static int staticlnt = 10 ;
int main () {
//...
)
Об этом примере нечего сказать, за исключением того, что использование ключевого слова static для ограничения области видимости внешней переменной для конкретного транслируемого модуля не рекомендуется. Если вы программируете на Java, то должны быть знакомы с такой терминологией. Не рекомендуется означает, что в будущем (неизвестно точно, когда именно) данная характеристика прекратит существование. Вместо static следует использовать пространства имен.
Вы хотите спрятать переменную от других транслирующихся модулей, но текущий транслируемый модуль требует полной видимости. Рекомендации автора и составляют предмет этой главы: пространства имен. Просто откажитесь от ключевого слова static и используйте предназначенное пространство имен. Это составляет намерение и Комитета по стандартизации: отказаться от такого применения static и заменить его пространством имен. Ключевое слово static следует использовать внутри функций и классов.
ПРИМЕЧАНИЕ
Не применяйте ключевое слово static к переменной, определенной в файловой области видимости. Комитет по стандартизации не рекомендует подобный тип использования. Вместо этого используйте пространство имен.
Часть II
Создание пространства имен
Если вы когда-либо уже создавали структуру или класс, то создание пространства имен будет для вас простейшей задачей. Синтаксис объявления пространства имен похож на синтаксис объявления структуры или класса: прежде всего запишите ключевое слово namespace с последующим необязательным именем пространства имен, затем откройте фигурную скобку. Пространство имен заканчивается закрывающей скобкой и не имеет завершающей точки с запятой. В этом отличие между объявлением пространства имен и объявлением класса: объявление класса завершается точкой с запятой. Следующий фрагмент программного кода является объявлением пространства имен:
namespace Window (
void move ( int x, int y) ;
}
Имя Window уникально идентифицирует пространство имен. У вас может быть много появлений именованного пространства имен. Эти многочисленные появления могут происходить в одном файле или во многих транслируемых модулях. Пространство имен стандартной библиотеки C++ — std — является прекрасным примером такой возможности. Это имеет смысл, поскольку стандартная библиотека представляет собой логическую группировку функциональности. Пространство имен std будет рассмотрено далее в этой главе.
Основная концепция в теме пространства имен заключается в том, чтобы сгруппировать связанные элементы в указанные (именованные) области. Ниже следует краткий пример пространства имен, в котором связываются несколько header-файлов:
И headerl.h
namespace Window ( void move( int x, int y) ;
)
// header2.h
namespace Window {
void resize( int x, int у ) ;
)
Объявление и определение типов
Внутри пространств имен можно определить и объявить типы и функции. Конечно, это вопрос конструкции и поддержки. Хорошая конструкция предписывает разделение интерфейса и реализации. Этому принципу необходимо следовать, когда речь идет не только о классах, но и о пространствах имен. Следующий пример демонстрирует плохо сконструированное пространство имен:
namespace Window (
/ / . . . другие объявления и определения переменных.
void move ( int х, int у) ; // объявления
void resize( int x, int у ) ;
J/ . . . другие объявления и определения переменных.
void move( int x, int у ) (
if ( X < MAX_SCREEN_X 66 X > 0 )
if ( у < MAX_SCREEN_Y 66 у > 0 )
platform.move( x, у ) ; H специфическая подпрограмма }
void resize( int x, int у )'
(
if ( x < MAX_SIZE_X 66 x > 0 )
if ( у < MAx2siZE_Y 66 у > 0 )
platform.resize( x, у ) ; H специфическая подпрограмма }
II . . . продолжение определений
)
Видите, как быстро фрагментируется пространство имен! Приведенный пример занимает приблизительно 20 строк. Вообразите, что было бы, если бы пространство имен было в 4 раза длиннее. В следующем разделе описано, как определить функции вне пространства имен. Такой прием помогает уменьшить фрагменти- рованность пространства имен.
Глава 8
Определение функций вне пространства имен
Пространство имен функций следует определять вне тела пространства имен. Это четко будет иллюстрировать разделение объявления функций и их определения, а также сохранит тело пространства имен нефрагментированным. Разделение определения функций и пространства имен также предоставляет возможность поместить пространство имен и заключенные в нем объявления в header-файл. Определения можно поместить в блок реализации. Ниже представлен краткий пример:
Н файл header.h
namespace Window {
void пюте( int x, int y) ;
// другие объявления ...
}'
// файл impl.cpp
void Window: :move( int x, int у )
(
/ / программный код для перемещения окна
)
Добавление новых членов
Новые члены можно добавить к пространству имен только внутрь его тела. Нельзя определить новые члены, используя синтаксис с квалификаторами. Самое большее, что можно ожидать в последнем случае, — это "жалоба" компилятора. Пример ниже демонстрирует подобную ошибку:
namespace Window {
// множество объявлений
)
/ / некоторый программный код
int Window::newlntegerInNamespace ; // извините, этого делать нельзя
Предыдущая строка программного кода незаконна. Ваш компилятор выдаст сообщение об ошибке. Для исправления ошибки или ее обхода просто переместите объявление внутрь тела пространства имен.
Подчеркнем еще одно различие между пространством имен и классом. Если внутри объявления класса определяется функция-член, то она подразумевает применение inline. Конечно, компилятор будет четко знать, если это не так. Помните, что компилятор не делает функций inline, даже если применить ключевое слово inline. Правило пространства имен для вставки в строку другое. Если функция определяется внутри пространства имен, то она не будет inline. Можно подумать, что это ограничение заключается в применении ключевого слова inline. К сожалению, такой подход неприменим.
Внутри пространства имен нельзя применять спецификатор доступа. Это еще одна область, где объявления пространства имен и класса расходятся. Все члены, представленные внутри пространства имен, являются общедоступными. Следующий программный код компилироваться не будет:
namespace Window {
private:
void move( int x, int у );
}
Вложение пространств имен
Одно пространство имен может быть вложено в другое. Причина для вложенности заключается в том, что определение пространства имен является также и объявлением. Как и с другим пространством имен, вы должны квалифицировать имя, используя включающее пространство имен. Если у вас вложенные пространства имен, то необходимо квалифицировать каждое имя по очереди. Следующий пример показывает одно именованное пространство имен, вложенное в другое:
namespace Window { namespace Pane { void size( int x, int у ) ;
} }
Для доступа к функции size() вне Window необходимо квалифицировать функцию обоими охватываемыми именами пространств. Следующий пример демонстрирует квалифицирование:
Вопросы реализации Часть II int main( ) { Window::Pane::size( 10, 20 ) ; return 0 ; }
Теперь, когда вы знаете, как создавать пространства имен, перейдем к исследованию того, как их применять.
Использование пространства имен
Рассмотрим пример применения пространства имен и связанное использование оператора разрешения области видимости. Синтаксис должен быть вам знаком, особенно тем, кто работает с классами. Прежде всего объявим все типы и функции для использования в пространстве имен Window. Затем определим все, что требуется еще, и все объявленные функции-члены. Эти функции-члены определяются вне пространства имен. Имена идентифицируются явно с помощью оператора разрешения области видимости. Учитывая сказанное, рассмотрим пример в листинге 8.1.
Листинг 8.1. Использование пространства имен
#include <iostream> namespace Window { const int MAX_X = 30 ; const int MAX_Y =40 ; class Pane { public: Pane() ; -Pane() ; void size( int x, int у ) ; void move ( int x, int у ) ; void show( ) ; private: static int ent ; int x ; int у ; } ;
} int Window::Pane::ent = 0 ; Window: : Pane:: Pane () : x(0), y(0) ( } Window::Pane::-Pane() { } void Window: : Pane :: size ( int x, int у ) ( if ( x < Window::MAX_X SS x > 0 ) Pane: : x = x ; if ( у < Window::MAX_Y SS у > 0 ) Pane: :y = у ;
} void Window:: Pane:: move ( int x, int у ) { if ( x < Window: :MAX_X SS x > 0 ) Pane: : x = x ; if ( у < Window: :MAX_Y SS у > 0 ) Pane: : у = у ;
} void Window::Pane::show( ) { std::cout « "x " « Pane: :x ; std::cout « " у " « Pane::y « std::endl ;
} int main( ) ( Window::Pane pane ;
pane.move( 20, 20 ) ; pane.show( ) ;
Глава 8
return 0 ;
}
Если построить и выполнить это приложение, то на экране появится следующий вывод:
х 20 у 20
Заметьте, что класс Рапе вложен в пространство имен Window. Это та причина, по которой необходимо имя Рапе квалифицировать квалификатором ::Window.
Статическая переменная ent, которая объявлена в Рапе, определяется обычным способом. Заметьте, что внутри функции Pane::size() МАХ_Х и MAXY полностью квалифицированы. Это потому, что Рапе является областью видимости. В противном случае компилятор сообщил бы об ошибке. То же самое справедливо и для функции Pane::move().
Интересно также квалифицированно Рапе::х и Рапе::у внутри обоих определений функций. Почему именно так? Если бы функция Pane::move() была написана так, то возникла бы проблема:
void Window. : Pane:: move ( int x, int у ) {
iff x < Window::MAX_X && x > 0 )
x = x ;
iff у < Window: :MAX_Y 64 у > 0 )
У = У :
Platform::move( x, у ) ;
}
Видите причину? Скорее всего, компилятор не сообщил бы ничего вразумительного. Некоторые компиляторы вообще не выдадут никаких сообщений. Даже старые заслуживающие доверия проверочные программы могут пропустить эту ошибку. "Раскрутим" картину назад, и тестирующая программа сообщит о проблеме, но эта проблема не связана с двумя операторами присваивания:
х=х;
у=у;
Источник проблемы заключен в аргументах функции. Аргументы х и у закрывают частные экземпляры переменных х и у, объявленные внутри класса Рапе. По сути, операторы присваивают х и у самим себе:
х=х;
у=у;
Попробуйте сами. Измените Window::Pane::move() так, чтобы это было как в только что показанном примере. Просто удалите текст Рапе:: из двух операторов присваивания. Перестройте приложение и выполните его вновь. Вывод будет таким:
х 10 у 10
Такой вывод отражает тот факт, что операторы присваивания просто присваивают х и у сами себе:
х=х;
У=у;
Переменные экземпляра внутри Рапе не затрагиваются.
Перепишем приложение и расширим его для поддержки директивы using. С помощью директивы using все имена извлекаются из именованного пространства имен и применяются к текущей области видимости. Это помогает сократить ненужный вывод. Соответствующий пример показан в листинге 8.2 и переписан с применением директивы using.
Листинг 8.2. Применение директивы using
#include <iostream>
namespace Window {
const int MAX_X =40 ;
const int MAX_Y =80 ;
class Pane {
public:
Pane () ;
-Pane() ;
void sizef int x, int у ) ;
void move( int x, int у ) ;
Вопросы реализации Часть II void show( ) ; private: static int ent ; int x ; int у ;
} ; } using namespace Window ;
int Pane::ent = 0 ;
Pane:: Pane () : x(10), y(10) {}
Pane: :~ Pane () (}
void Pane: :size( int x, int у ) ( if ( x < MAX X 66 x > 0 ) Pane::x = x ;
if ( у < MAX_Y 66 у > 0 ) Pane: :y = у ;
) void Pane: : move ( int x, int у ) { if( x < MAX_X 66 x > 0 ) x = x ;
if ( у < MAX_Y 66 у > 0 ) у = у ;
} void Pane::show( ) { std::cout « "x " « Pane::x « " у " « Pane::y « std: :endl ;
) int main( ) ( Pane pane ;
pane.move( 20, 20 ) ; pane.show( ) ;
return 0 ;
}
Вывод из программы показан ниже:
х ю у ю
Директива using появляется после объявления пространства имен:
using namespace Window;
Этот оператор переносит все имена, найденные в пространстве имен Window, в текущую область видимости. Приложение работает так же, как и прежде, но в новой версии меньше печати. Директива using и объявление using описываются в следующем разделе.
Ключевое слово using
Ключевое слово using используется как для директивы using, так и для объявления using. Синтаксис ключевого слова using определяет, является ли контекст директивой или объявлением.
Директива using
Директива using, по сути, выставляет все имена, объявленные в пространстве имен, в текущую область видимости. На имена можно сослаться без указания их соответствующих имен пространств. В следующем примере показано применение директивы using:
namespace Window (
int valuel =20 ;
int value2 =40 ;
)
Глава 8
Window::valuel = 10 ;
using namespace Window ; value2 = 30 ;
Область видимости директивы using начинается с ее объявления и продолжается до конца текущей области. Обратите внимание, что переменная valuel должна быть квалифицирована, чтобы на нее сослаться. Переменная value! не требует квалификации, поскольку директива представляет в текущую область видимости все имена в пространстве имен.
Директиву using можно использовать на любом уровне области разрешения. Это предоставляет возможность применять директиву внутри области блока. Когда этот блок выходит из области видимости, го же самое происходит со всеми именами внутри пространства имен. В следующем примере показано такое поведение: namespace Window ( int valuel = 20 ; int value2 =40 ;
}
//. . . void f() {
{ using namespace Window ; value2 = 30 ;
} value2 =20 ; //ошибка!
Последняя строка кода в f(), va!ue2=30; ошибочна, поскольку имя value! не определено. Это имя доступно в предыдущем блоке, поскольку директива перенесла имя в него. Как только блок вышел из области видимости, то же самое произошло и с именами в пространстве имен Window.
Имена переменных, объявленные внутри локальной области, закрывают любые имена пространства имен, введенных в данной области видимости. Такое поведение похоже на то, как локальная переменная заслоняет глобальную. Даже если пространство имен представлено после локальной переменной, эта локальная переменная будет заслонять имя пространства. Следующий пример иллюстрирует сказанное:
namespace Window { int valuel = 20 ; int value2 =40 ;
}
H - • . void f () < int value2 =10 ; using namespace Window ; std::cout « value2 « std: :endl ;
1
Вывод этой функции 10. а не 40. Такой вывод подтверждает факт, что value! в пространстве имен Window закрыта переменной value! в f(). Если надо использовать имя в пространстве имен, то его следует квалифицировать именем пространства.
Неоднозначность может появиться и при использовании имени, которое определено как глобально, так и внутри пространства имен. Неоднозначность проявляется только при использовании имени, а не просто когда представляется пространство имен. Сказанное иллюстрируется следующим фрагментом программного кода: namespace Window { int valuel =20 ;
)
//. • . using namespace Window ; int valuel = 10 ; void f( ) {
Часть II value1 =10 ;
}
Неоднозначность проявляется внутри функции f(). Директива переносит Window::value 1 в глобальное пространство имен. Поскольку уже есть глобально определенная valuel, то применение valuel в f() является ошибкой. Обратите внимание на то, что, если строку кода в f() убрать, то ошибки не будет.
Объявление using
Объявление using похоже на директиву using, за исключением того, что объявление предоставляет более тонкий уровень управления. Если точнее, то объявление using применяется для объявления указанного имени (из пространства имен), чтобы быть в текущей области видимости. Затем на специфицированный объект можно ссылаться только по имени. Следующий пример демонстрирует применение объявления using:
namespace Window { int valuel =20 ; int value2 =40 ; int value3 = 60 ;
using Window::value2 ; //перенести value2 в текущую область видимости Window::valuel = 10 ; //значение valuel должно быть квалифицировано value2 = 30 ;
Window::value3 = 10 ; //значение value3 должно быть квалифицировано
Объявление using добавляет указанное имя к текущей области видимости. Объявление не влияет на другие имена в пространстве имен. В приведенном примере переменная value2 указана без квалификации, но переменные valuel и value3 требуют квалификации. Объявление using обеспечивает лучшее управление именами пространства, которое появляется в области видимости. Это отличает его от директивы, которая переносит в область видимости все имена в пространстве.
Как только имя перенесено в область видимости, оно становится видимым до конца данной области. Такое поведение в точности соответствует другим объявлениям. Объявление using можно использовать в глобальном пространстве имен или внутри любой локальной области.
Представлять имя в локальной области, где было уже объявлено имя пространства имен, является ошибкой. Справедливо и обратное утверждение. Следующий пример иллюстрирует сказанное:
namespace Window { int valuel =20 ; int value2 =40 ;
)
H. . . void f() < int value2 =10 ; using Window::value2 ; // множественное объявление std::cout « value2 « std::endl ;
)
Вторая строка в функции f() вызовет ошибку компилятора, поскольку имя value2 уже определено. Та же самая ошибка произойдет, если объявление using будет представлено перед определением локальной переменной value2.
Любое имя, представленное в локальной области видимости объявлением using, заслоняет любое имя вне данного блока. Фрагмент программного кода иллюстрирует описанное поведение:
namespace Window ( int valuel = 20 ; int value2 =40 ;
} int value2 =10 ; //. • ■ void f() < using Window::value2 ; std::cout « value2 « std::endl ;
}
Глава 8
Объявление using в f() заслоняет value!, определенную в глобальном пространстве имен.
Как уже упоминалось, объявление using предоставляет более тонкий уровень управления именами из пространства имен. Директива using переносит в текущую область видимости все имена из пространства имен. Использование объявления предпочтительнее использования директивы, поскольку директива, по сути, сводит на нет назначение механизма пространства имен. Объявление более избирательно, поскольку явно указывает имена, которые необходимо представить в области видимости. Объявление using не засоряет глобального пространства имен, как это происходит в случае директивы using (конечно, если вы не объявляете всех имен, имеющихся в пространстве). Благодаря использованию объявления using маскирование имен, засорение глобального пространства имен и неоднозначность сокращаются.
Псевдоним пространства имен
Псевдоним пространства имен сконструирован в целях обеспечения для пространства имен другого имени. Псевдоним — сокращенный термин, который используется при ссылке на пространство имен. Сказанное особенно справедливо, если имя пространства имен очень длинное. Рассмотрим пример:
namespace the_software_company ( int value ;
// . . .
)
the_software_company::value = 10 ;
namespace TSC = the software_company ;
TSC::value = 20 ;
Недостаток, конечно, в том, что псевдоним может вступить в конфликт с существующим именем. В таком случае компилятор распознает конфликт и вы сможете решить проблему, изменив псевдоним.
Неименованное пространство имен
Неименованное пространство имен — это просто пространство имен, не имеющее имени. Неименованные пространства принято использовать для защиты глобальных данных от потенциальных конфликтов имен между транслируемыми модулями. Каждый транслируемый модуль обладает собственным уникальным неименованным пространством имен. На все имена, определенные внутри неименованного пространства (внутри каждого транслируемого модуля), можно сослаться без явного квалифицирования. Ниже приведен пример двух неименованных пространств имен, находящихся в двух разных файлах:
// файл: опе.срр namespace (
int value ;
char p ( char *p ) ;
И. . .
}
// файл: two.cpp
namespace {
int value ;
char p ( char *p ) ;
H. . .
)
int main( )
( char c = p ( ptr ) ;
}
Каждое из имен, value и p(), определены для соответствующего файла. Для ссылки на (неименованное пространство) имя внутри транслируемого модуля просто используйте имя без квалификации. Такое применение демонстрировалось в предыдущем примере при обращении к функции р(). Подобное применение подразумевает директиву using для объектов, указываемых в неименованном пространстве имен. По этой причине вы не сможете получить доступ к членам неименованного пространства в другом транслируемом модуле. Такое поведение неименованного пространства совпадает с поведением статического объекта, обладающего внешней связываемостью. Рассмотрим пример:
static int value=10;
Вопросы реализации Часть II
Помните, что использование ключевого слова static Комитетом по стандартизации не поощряется. Для того чтобы заменить только что показанное, можно использовать пространства имен. Другой способ представления неименованных пространств имен — думать, что они являются глобальными переменными с внутренней связываемостью.
Стандартное пространство имен
Лучший пример пространств имен находится в Standard C++ Library. Стандартная библиотека C++ полностью погружена внутрь пространства имен под именем std. Все функции, классы, объекты и шаблоны объявлены в пространстве имен std.
Вы увидите программный код, подобный следующему:
♦include <iostream> using namespace std;
Помните о том, что директива using вытаскивает из именованного пространства все. Использовать директиву using при работе со стандартной библиотекой — признак дурного тона. Почему? Потому, что такой прием делает бессмысленным само назначение пространства имен. Глобальное пространство имен будет засорено всеми именами, которые имеются в header-файле. Имейте в виду, что механизм пространства имен используют все header-файлы, поэтому, если вы включаете много стандартных header-файлов и указываете директиву using, то все объявленное в header-файлах будет в глобальном пространстве имен. Обратите внимание, что все примеры в этой книге нарушают данное правило. Это делается не для того, чтобы оправдать нарушение, а ради краткости примеров. Правильное использование объявления using показано в следующем фрагменте программного кода:
♦include <iostream> using std::cin ; using std::cout ; using std::endl ; int main( ) { int value = 0 ; cout « "So, how many eggs did you say you wanted?" « endl ; cin » value ; cout « value « " eggs, sunny-side up!" « endl ; return( 0 ) ;
)
Результатом выполнения программы будет следующее:
So, how many eggs did you say you wanted? 4 4 eggs, sunny-side up!
В качестве альтернативы можно было бы полностью квалифицировать используемые имена, что продемонстрировано в следующем примере:
♦include <iostream> int main( ) { int value = 0 ; std:: cout « "How many eggs did you want?" « std: : endl ; std::cin » value ; std::cout « value « " eggs, sunny-side up!" « std::endl ; return( 0 ) ;
}
Пример вывода программы показан ниже:
How many eggs did you want? 4 4 eggs, sunny-side up!
Это может оказаться предпочтительным для коротких программ, но крайне неподходящим для любого значительного программного кода. Вообразите, что для каждого имени, находящегося в стандартной библиотеке, приходится писать префикс std::.
Глава 8
Резюме
Конфликты глобальных имен являются проблемой уже много лет. Это и неудивительно, поскольку существует только одно глобальное пространство имен. Механизм пространства имен стал долгожданным дополнением к стандарту языка C++.
Создание пространства имен выполняется очень просто. Оно похоже на объявление класса. Имеют место лишь минимальные расхождения. Во-первых, после закрывающей скобкой пространства имен нет точки с запятой. Во-вторых, пространство имен открыто, тогда как класс закрыт. Сказанное означает, что определение пространства имен можно продолжать в других файлах или в отдельных разделах одного и того же файла.
В пространство имен можно поместить все, что можно объявить. Если вы конструируете классы для повторно используемой библиотеки, то следует применять механизм пространства имен. Функции, объявленные внутри пространства имен, должны определяться вне тела данного пространства. Это развивает идею разделения интерфейса и реализации и уберегает пространство имен от фрагментирования.
Пространства имен могут быть вложенными. Пространство имен — это объявление. Этот факт предоставляет возможность вкладывать пространства имен друг в друга. Не забывайте о том, что вложенные имена необходимо полностью квалифицировать.
Директива using применяется для раскрытия всех имен в пространстве текущей области видимости. По существу, это засоряет глобальное пространство имен всеми именами, находящимися в именованном пространстве. В общем случае применять директиву using — плохая практика, особенно с учетом фактора стандартной библиотеки. Объявление using используется для раскрытия текущей области видимости указанного имени из именованного пространства. Такой подход предоставляет возможность ссылаться на объект только по его имени.
Псевдоним пространства имен по природе похож на typedef. Он предоставляет возможность создать для именованного пространства другое имя. Такой подход бывает очень полезен, если пространства имен имеют длинные имена.
Каждый файл может содержать неименованное пространство имен. Неименованное пространство, как подсказывает его название, — это пространство без имени. Неименованное пространство позволяет использовать имена внутри пространства имен без квалифицирования. Оно поддерживает имена пространства локальными по отношению к транслирующемуся модулю. Неименованные пространства имен можно сравнить с объявлением глобальной переменной с ключевым словом static.
Библиотека Standard C++ Library заключена в пространство имен с именем std. При работе со стандартной библиотекой C++ избегайте использования директивы using. Вместо нее применяйте объявление.
9 Зак. 53
Манипулирование типами объектов
В ЭТОЙ ГЛАВЕ
Оператор typeid()
Класс type_info
Динамическое приведение объектов
Другие операторы приведения
Новые приведения против старых
Глава 9
Runtime Type Information (информация о типе времени выполнения) (или сокращенно RTTI) представляет собой специальный механизм получения идентификации типов и выражений во время выполнения. Хотя ключом к манипулированию типами объектов во время выполнения является динамическое связывание, тем не менее, большим преимуществом служит знание точного типа объекта, на который есть только ссылка или указатель. Часто такая информация позволяет выполнять специальные операции более эффективно и может предотвратить медленные превращения интерфейса базового класса. Программа во время выполнения может определить, на какой из нескольких известных производных типов ссылается или указывает базовый класс. RTTI решает много специальных программных проблем, которые легче решить, если известен точный тип общего указателя. Механизм RTTI состоит из трех элементов:
Оператора typeid() для идентификации точного типа объекта, заданного указателем или ссылкой.
Класса type_info для поиска дополнительной информации о типе объекта, заданного указателем или ссылкой.
Оператора dynamic_cast() для приведения указателя (ссылки) базового класса к указателю (ссылку) производного класса.
Оператор typeid()
Оператор typeid() является встроенным оператором C++. Для получения информации о типе объекта во время выполнения его можно использовать двумя способами:
■ Если операнд typeid() является разадресованным указателем или ссылкой на полиморфный тип, то typeid() возвращает динамический тип действительного объекта, на который ссылаются или указывают. Если операнд является указателем или неполиморфным типом, то typeid() возвращает объект, представляющий статический тип. Когда typeid() не может идентифицировать тип своего операнда, возбуждается исключение bad_typeid. Это исключение возбуждается и тогда, когда typeid() не может разадресовать указатель.
Оператор typeid() может выяснить, имеют ли два объекта один и тот же тип. Обращение к typeid() возвращает ссылку на const type_info. Возвращаемый объект представляет дополнительную информацию о типе операнда typeid(). Возвращаемый объект используется в сравнении типов.
Header-файл <typeinfo.h> содержит объявления и прототипы для следующих классов RTTI:
type_info
■ bad_typeid
Класс typejnfo
Класс type_info обеспечивает информацию о типе во время выполнения объекта. Его точное определение зависит от реализации, но объявление выглядит следующим образом:
class type_info
I
private:
// Присваивание type_info не поддерживается type_info6 operator= (const type_info6); type_info (const type_infos);
protected:
type_info (const char *n) : _name (n) { )
const char *_name;
public:
// деструктор virtual ~type_info () ;
bool operator== (const type_infoS arg) const;
bool operator!= (const type_infoS arg) const;
const char* name () const ( return _name; }
bool before (const type_infoS arg) const;
Вопросы реализации Часть II
Конструктор для класса typejnfo
Класс type_info не предоставляет общедоступных конструкторов. По этой причине вы не можете прямо инициализировать объекты type_info в программе. Ссылки type_info генерируются оператором typeid(). Например, с учетом выражения ехрг следующее обращение генерирует const type_info& для ехрг:
typeid(expr)
Поскольку конструктор копии и оператор присваивания для type_info приватны для класса, то объекты данного типа нельзя скопировать.
Оператор typeid() работает подобно оператору sizeof(): он может принять или выражение, или сам тип. Например, задан тип Т и объект типа t, тогда действительно применение typeid():
typeid(T); typeid(t);
Операторы сравнения
Следующие операторы обеспечивают сравнение ссылок typejnfo: bool operator— (const type_info6 arg) const; bool operators (const type_info6 arg) const;
Чтобы понять, как использовать typeid() и класс type_info, рассмотрим пример типичной иерархии классов графического интерфейса GUI, в которых класс Window служит базовым для всей иерархии GUI. Предположим, что DialogBox является производным классом, который обеспечивает функцию repaintControls(). Функция repaintControls() предоставляет специальную услугу, которая отсутствует в базовом классе. Таким образом, учитывая указатель Window во время выполнения, необходимо определить, действительно ли указатель указывает на объект DialogBox. Это можно сделать, применив оператор typeid() к разадресованному указателю Window с целью определить тип объекта, на который указывает указатель Window.
В листинге 9.1 показан программный код примера.
Листинг 9,1. Использование оператора сравнения класса typejnfo ♦include <iostream.h> ♦include <typeinfo.h> class Window //тип полиморфного класса.. ( public: virtual void show() //сделать класс Window полиморфным {/*...*/} //. . . ); class DialogBox:public Window { public: virtual void show() {/*...*/} virtual void repaintControls() ( //... } 1; void paint(Window* pw) if (typeid(*pw)==typeid(DialogBox)) ( cout«"DialogBox"«endl; DialogBox* pd=(DialogBox*)pw; pd->repaintControls(); } else cout«"Non-Dialog Object Type"«endl; 1 int main()
Манипулирование типами объектов
Глава 9
paint(new Window);
paint(new DialogBox); return 0;
Программа породит следующий вывод:
Non-Dialog Object Type DialogBox
Результирующий указатель выражения new Window не указывает на DialogBox, поэтому следующее сравнение будет неудачным и программа не вызовет функцию, уникальную для DialogBox:
if ( typeid(*pw)==typeid(DialogBox) ) ( H }
В идеале виртуальные функции являются лучшим выбором для иерархических типов. Использование typeid() для замены виртуальных функций является определенно неэффективным механизмом. Это положение рассматривается далее в этой главе.
Обратите внимание на то, что программа в листинге 9.1 использует выражение приведения в функции
paint():
DialogBox* pd=(DialogBox*)pw;
Подобные выражения не считаются в C++ хорошей практикой, так как этот язык программирования обеспечивает лучший способ выполнения приведения (читайте раздел "Оператор dynamic_cast()" далее в этой главе).
Функция-член name()
Функция-член паше() возвращает строку, которая идентифицирует тип имени операнда для typeid. Вывод typeid().name() зависит от реализации.
В листинге 9.2 демонстрируется использование функции name().
Листинг 9.2. Функция name()
#include <typeinfo.h> #include <iostream.h>
class Window //тип полиморфного класса. ( public: virtual void show() {);
//. . . 1;
class Button : public Window (/*..• */);
int f() ( Button b; Button* pb; pb=Sb;
if (typeid(*pb)==typeid(Button)) cout«"Name is " «typeid (*pb) . name () «endl;
if (typeid(*pb)I—typeid(Window)) cout«typeid(*pb) .name ()«" is not same as "«typeid(Window) . name () «endl;
typedef int (*PF) () ;
PF pf=f;
cout«"Type of PF="«typeid(PF) .name ()«endl; cout«"Type of pf="«typeid(pf) .name ()«endl;
Часть II
intS rv=v;
cout«"Type of v="«typeid(v) .name ()«endl;
cout«"Type of rv="«typeid (rv) .name ()«endl;
cout«"Type of result of typeid ()="« typeid(typeid(rv)). name () «endl;
return 0;
}
int main()
{
f 0 ;
)
При запуске этой программы на Microsoft Visual C++ 5 получается следующий результат:
Name is class Button class Button is not same as class Window Type of PF=int (cdecl*) (void) Type of pf=int (cdecl*) (void) Type of v=int Type of rv=int Type of result of typeid()=class type_info
Программа демонстрирует применение не только функции name(), но и операторов сравнения типов двух объектов (*pb и Button). Вот еще один способ сравнения, который можно было бы употребить:
if (! strcmp(typeid(*pb).name(),"class Button")) //...
Однако такой подход неприменим для всех компиляторов, поскольку вывод typeid().name() зависит от реализации. Когда пример компилировался на компиляторе GNU C++, результат был следующим:
Name is 6Button 6Button is not same as 6Window Type of PF=PFv_i Type of pf=PFv_i Type of v=i Type of rv=i
Type of result of typeid()=19builtin_type_info
Как уже говорилось, typeid() обеспечивает информацию о типе во время выполнения в том случае, если операнд typeid() является разадресованным указателем на полиморфный тип, например:
#include <iostream.h>
#include <typeinfo.h>
class Window ( public: virtual void show() //сделать полиморфным ( return; ) }; class DialogBox:public Window { public: };
Window* f() { cout«" in f () "«endl ; return (new DialogBox); } int main() (
cout«typeid (f () ) .name () «endl;
Манипулирование типами объектов Глава 9
cout«typeid (*f () ) . name () «endl; return 0; }
В Visual C++ 5 этот пример порождает следующий вывод: class Window * in f () class DialogBox
Обратите внимание на то, что вывод порожден следующим оператором: cout«typeid (f () ) . name () «endl ;
Данный оператор производит статический тип информации об указателе, который возвращается функцией f(), поскольку вывод f() здесь не разадресован. В результате функция f() не выполняется.
Теперь обратите внимание на вывод из такого оператора: cout« typeid (*f()) . name () «endl ;
Этот оператор производит динамический тип информации о возвращаемом указателе Г(), поскольку указатель разадресован. Оператор также выполняет функцию f(). Эта функция должна выполняться, в противном случае динамический тип возвращаемого из f() значения нельзя будет определить.
Функция-член before()
Функция-член before() используется для сравнения упорядоченности типов (не путать с порядком объявления или иерархическим порядком). Вывод из функции зависит от реализации. В листинге 9.3 показано применение функции before().
Листинг 9.3. Функция-член beforeQ
#include <iostream.h> #include <typeinfo.h> class Window //тип полиморфного класса { public:'
virtual void show()(); //. . . ) ; class DialogBox:public Window { public: virtual void show()(); n... ) ; template <class T,class U> void f (T t,U u) ( cout«typeid(t) .name ()«" before "« typeid (u) . name ()«" : "« typeid(t) .before (typeid(u) )«endl; return; } void g(const Windows wl,const Windows w2) { cout«typeid (wl) . name () «" before "« typeid(w2) .name ()«" : "« typeid(wl) .before(typeid(w2) )«endl; return; int main() { int i; double d; char* pc="C++"; f(d,i); //во время компиляции
Часть II
f(pc[O],pc); //во время компиляции
Window* pw=new Window;
Window* pd=new DialogBox; g(*pw,*pd); //во время выполнения return 0;
)
В Visual C++ 5 этот пример породит следующий вывод:
double before int:1 char before char *: 1 Window before DialogBox:0
Хотя идентификатор i объявляется перед идентификатором d и класс Window появляется перед DialogBox в иерархии классов, вывод before() этого не отражает, поскольку так реализация Visual C++ 5 определила порядок следования типов. Функция before() может быть не слишком полезна при ежедневном программировании или разработке банковских приложений. Однако она может применяться как механизм сравнения для определения таблиц отображения или хеширования типов.
Оператор typeid() в конструкторах и деструкторах
Оператор typeid() можно вызвать во время конструирования или разрушения объекта. Если операнд typeid() ссылается на конструируемый или разрушаемый объект, то typeid() всегда получит type_info, который представляет класс конструктора или деструктора. Рассмотрите пример:
#include <iostream.h>
#include <typeinfo.h>
class Window
(
public:
Window()
{
cout«typeid(*this) .name () «endl; //всегда производить Window
)
virtual void f()
{
cout«typeid (*this) .name () «endl; //производить Window или
//DialogBox
}
);
class DialogBox : public Window
{
public:
DialogBox(){)
};
int main()
{
DialogBox d;
d.f () ;
}
Этот код в Visual C++ 5 произведет следующий вывод:
class Window
class DialogBox
Здесь функция f() порождает динамический тип указателя this, тогда как конструктор Window() получает статический тип указателя this.
Неправильное использование typeid()
Хотя оператор typeid() представляет собой полезный механизм, его следует применять только при необходимости. Когда проверка типа времени выполнения не является реально необходимой, нужно выполнить проверку типа времени компиляции, поскольку она более эффективна. Например, когда речь не идет о полиморфных типах, то применение typeid() абсолютно не требуется. Кроме того, только потому, что
Глава 9
язык C++ обеспечивает данный механизм, не начинайте интенсивно его использовать для решения проблем, связанных с виртуальными функциями.
В листинге 9.4 показан пример одного неправильного использования typeid(). Пример показывает, как реализовать виртуальную функцию, используя typeid(). Затем мы рассмотрим, почему так нельзя использовать typeid().
Листинг 9.4. Неправильное использование оператора typeidQ
#include <iostream.h> #include <typeinfo.h>
class Window ( public: Window () { } ; virtual -Window() ( } ;
void show()(}; //не виртуальный //. . .
);
class DialogBox:public Window { public: DialogBox() ();
-DialogBox () (} ;
void show(){); //...
void f (Windows w) ( typedef DialogBoxS RDIALOG; if (typeid (w) =typeid (window)) w.show () ; else if (typeid(w)==typeid(DialogBox)) RDIALOG (w) . show() ;
) int main() ( Window* pw=new Window; Window* pd=new DialogBox;
f(*pw); //вызывает Window::show() f(*pd); //вызывает DialogBox: : show()
delete pw;
delete pd;
return 0; )
В листинге 9.4 функция show() делается виртуальной с помощью typeid(). Глобальная функция f() принимает ссылку на базовый тип Window. Во время выполнения ссылка w может ссылаться как на объект Window, так и на объект DialogBox. Внутри функции f() действительный тип w определен явно с помощью оператора typeid(), и затем вызывается корректная функция show(). Хотя этот программный код и работает, но он идет вразрез с самыми фундаментальными положениями ООП: данное сообщение можно направить любому экземпляру, который указан ссылкой (или указателем). В ООП вам не нужно определять тип экземпляра, который указан ссылкой или указателем. Другая проблема с представленным подходом заключается в том, что при добавлении новых классов в иерархию Window функция f() начинает расти — это не только затрудняет поддержку, но и делает программу очень трудной для понимания.
Динамическое приведение объектов
Почти все безопасные преобразования типов делаются в C++ неявно, и нет необходимости писать приведения. Однако иногда возникает потребность преобразовать значение из одного типа в другой. Это
Часть II
приходится делать в тех случаях, когда компилятор не выполняет преобразования автоматически, поскольку делается нечто потенциально опасное или непереносимое. Специфицируемые преобразования типов называются приведениями или приведениями типов. Примером небезопасного преобразования типа может служить приведение указателя базового объекта на производный указатель. Такое приведение небезопасно и непредсказуемо. Использование такого указателя для вызова методов производного класса приведет к катастрофическим последствиям во время выполнения, если базовый указатель на самом деле не указывает на производный объект. Следовательно, C++ требует механизм, который может выполнять преобразования объектов во время выполнения.
Оператор dynamic_cast()
C++ предлагает специальный оператор dynamic_cast(), предназначенный для устранения некоторых проблем и опасностей, присущих приведению объектов в иерархии классов. Этот оператор перемещается по иерархии классов во время выполнения и может безопасно использоваться в тех случаях, когда другие приведения не подходят. Оператор dynamic_cast() позволяет определить во время выполнения, указывает ли ссылка (или указатель) базового класса на объект указанного порожденного класса. Оператор можно применять только тогда, когда базовый класс является полиморфным (т.е. базовый класс должен содержать хотя бы одну виртуальную функцию).
Оператор dynamic_cast() имеет следующий синтаксис:
dynamic_cast <Туре>(Object)
Аргумент Туре — это тип, к которому выполняется приведение. Аргумент Object — это преобразуемый объект.'
Туре должен быть указателем или ссылкой на тип определенного класса. Аргумент Object должен быть выражением для указателя или ссылки. В случае успеха dynamic_cast преобразует Object к желаемому типу. Если приведение указателя неудачно, то возвращаемый указатель будет иметь значение 0. Если неудачно приведение ссылки, то возбуждается исключение bad_cast. Поскольку может быть возбуждено исключение, то оператор dynamic_cast гораздо безопаснее по сравнению с традиционным приведением. Когда терпит неудачу обычное приведение, то оно не указывает на ошибку — конечно, до тех пор, пока программа не будет запущена и не выявит множество ошибок.
Использование оператора dynamic_cast также называется безопасным приведением типа.
Преобразование указателя или ссылки порожденного класса в указатель или ссылку базового класса выполняется во время компиляции, а результат направляется в указатель или ссылку на подобъект базового класса. Такое преобразование называется преобразованием вверх (upcast).
В листинге 9.5 приведен пример приложений, которые используют dynamic_cast для выполнения приведения вниз (downcast). Листинг также показывает пример межиерархического приведения с использованием dynamic_cast.
Листинг 9.5. Безопасное приведение типа с использованием dynamic_cast ♦include Ctypeinfо.h> ♦include <iostream.h>
class Control // тип полиморфного класса.
(
public:
virtual void show() ();
//. . . };
class Picture (/*...*/);
class BitMap:public Control, public Picture
{/*...*/};
int f() < try (
Control* pc=new BitMap;
//попытка приведения
BitMap6 rb=dynamic_cast<BitMap6>(*pc);
Глава 9
cout« "The resulting reference's type: "«typeid(rb) .name ()«endl;
//попытка приведения через иерархию
Pictures rp=dynamic_cast<PictureS>(*pc);
cout« "The resulting pointer's type: "«typeid(rp) .name ()«endl; }
catch (const bad_castS) (
cout« "dynamic_cas t () f ailed"«endl ;
return 1 ;
} return 0;
}
int main() {
f 0 ;
)
161161В Visual C++ программа породит следующий вывод:
161
161
The resulting reference's type:class BitMap
The resulting pointer's type:class Picture
Здесь указатель *pc фактически указывает на объект типа BitMap и потому преобразование *рс в BitMap& успешно. Результирующая ссылка rb ссылается на полный объект типа BitMap. Преобразование из *рс в Picture& также безопасно, поскольку Picture является базовым классом BitMap. Результирующая ссылка гр ссылается на подобъект типа Picture.
Использование dynamic_cast()
С помощью оператора dynamic_cast() программа может определить во время выполнения, на какой из нескольких производных типов указывает или ссылается базовый класс. Этот прием очень часто используется для принятия решений во время выполнения программы. Его можно также использовать для реализации виртуальных функций в иерархии. Например, в иерархии Window вполне возможно, что порожденный объект DialogBox имеет уникальное требование к перерисовке своих элементов управления. Такое требование не разделяется всеми порожденными объектами Window. В связи с этим может показаться заманчивым избежать виртуальной функции repaintControls() в базовом классе Window. Однако с помощью dynamic_cast() указатель Window можно привести к указателю DialogBox и вызвать функцию repaintControls(). Если объект — не DialogBox или класс не порожден от DialogBox, то приведение возвратит значение 0 и программа может принять решение не обращаться к repaintControls().
В листинге 9.6 приведен пример того, как оператор dynamic_cast() может во время выполнения определить, на какой из нескольких известных производных типов указывает или ссылается базовый класс.
Листинг 9.6. Использование оператора dynamic_cast() для определения действительного типа указателя базового класса
#include <iostream.h>
#include <typeinfо.h>
class Window [l*s«n полиморфного класса. {
public:
virtual void show()
{/*•••*/}
//...
1;
class DialogBox:public Window {
public:
virtual void repaintControls() {
cout«typeid (*this) . name () «endl ;
}
1;
Часть II
class PrintDialog:public DialogBox
{/*■■ */);
void f (Window* pw) (
DialogBox* pd=dynamic_cast<DialogBox*>(pw);
if (pd)
pd->repaintControls();
else
cout«"unwanted object type"«endl; }
int main()
(
f(new Window);
f(new DialogBox);
f(new PrintDialog);
return 0;
}
Программа выдаст следующий результат:
unwanted object type
DialogBox
PrintDialog
В этом примере функция f() вызывается три раза. При первом вызове указатель pw указывает на объект типа Window. В связи с тем что pw реально не указывает на DialogBox или класс, порожденный от DialogBox, dynamic_cast() работает неудачно и возвращает 0. При втором и третьем вызове f() указатель pw действительно указывает на DialogBox и PrintDialog (соответственно). В этих случаях dynamic_cast() работает успешно и программа вызывает виртуальную функцию repaintControls().
Использование dynamic_cast() с виртуальными базовыми классами
Объект виртуального базового класса нельзя преобразовать в его производный объект — если вы попытаетесь сделать это, то получите ошибку времени компиляции, поскольку компилятор не обладает всей информацией, достаточной для проведения преобразования. Однако оператор dynamic_cast() решает проблему, поскольку он выполняет преобразование во время выполнения. Рассмотрим следующий пример:
class Control //тип полиморфного класса.
<
virtual void show();
//. . .
) ;
class BitMap:public virtual Control
{/*■. .*/};
int main()
{
Control* pc=new BitMap;
BitMap* pbl=(BitMap*)pc; //ошибка
BitMap* pb2=dynamic_cast<BitMap*>(pc); //правильно
H. . -
}
В этом примере dynamic_cast() применяется к указателю рс для Control с целью привести его к указателю BitMap. Конечно, это работает только в том случае, если нет неоднозначности при преобразовании от Control к BitMap.
Следующий оператор будет забракован при компиляции, поскольку Control является виртуальным базовым классом класса BitMap:
BitMap* pbl=(BitMap*)рс; //ошибка
Использование dynamic_cast() в конструкторах и деструкторах
Оператор dynamic_cast() можно использовать при конструировании и разрушении объектов. Если операнд dynamic_cast() является объектом конструирования или разрушения, то он не может выполнить приведения. В листинге 9.7 показан пример применения dynamic_cast() в конструкторе.
Манипулирование типами объектов Глава 9
Листинг 9.7. Использование dynamic cast() в конструкторе #include <iostream.h> #include Ctypeinfo.h> class base ( public: base(); virtual void f();
class derived : public base { public: derived() {}
base::base () { try { dynamic_cast<derivedS>(*this); //сбой } catch(const bad_castS) ( cout«"bad_cast in base: :base() "«endl; } )
void base: : f () ( try { dynamic_cast<derived6>(*this); cout«"successful dynamic_cast in base:: f () "«endl; } catch(const bad_castS) { cout«"bad_cast in base:: f () "«endl;
) }
int main() ( derived d; d.f () ; }
Эта программа сгенерирует следующий вывод:
bad cast in base::base()
successful dynamic^cast in base::f()
Во время конструирования объекта d в main() первый раз подобъект base конструируется с помощью конструктора по умолчанию base::base(). Таким образом, во время конструирования base информация о порожденном объекте во время выполнения внутри base::base() недоступна. В результате обращение к dynamic_cast() в конструкторе потерпит неудачу. С другой стороны, внутри функции base::f() обращение к dynamic_cast() будет успешным, поскольку порожденный объект d к этому моменту полностью сконструирован.
Операторы typeid() и dynamic_cast
Операторы typeid() и dynamic_cast() весьма похожи и применяются во многих похожих ситуациях. Совместно они обеспечивают манипулирование объектами во время выполнения. Тем не менее, между ними существует несколько различий:
Оператор dynamic_cast() не работает с неклассовыми типами, в отличие от оператора typeid().
Часть II
Для использования оператора dynamic_cast() указанный класс должен быть полиморфным, т.е. он должен содержать как минимум одну виртуальную функцию-член. С оператором typeid() операнд необязательно должен быть полиморфным.
Оператор typeid() может сказать только о типе объекта. Оператор dynamic_cast() может сказать, является ли объект указанным классом или классом, порожденным от указанного класса.
Другие операторы приведения
В последующих разделах описаны три оператора приведения, не выполняющие проверок времени выполнения и не ограниченные базовым или порожденными классами в одной и той же иерархии полиморфных классов. Хотя в центре внимания этой главы находится тема манипулирования объектами во время выполнения, последующее обсуждение важно, если вы желаете иметь полное представление об операторах приведения типа в языке C++.
Оператор static_cast()
Оператор static_cast() применяется для определенных пользователем, стандартных или неявных преобразований типов. В отличие от dynamic_cast(), оператор static_cast() используется в контексте типа объекта времени компиляции. Синтаксис оператора static_cast() подобен синтаксису оператора dynamic_cast():
static_cast<Type> (Object.) ;
Здесь и Туре, и Object должны быть известны во время компиляции. Если один тип можно преобразовать в другой каким-то методом, который уже обеспечивается языком C++, то выполнение такого преобразования с помощью static_cast() будет аналогичным. Ниже приведен пример, не требующий пояснений: void f() I
char ch;
int i=8;
float f=8.16;
double d;
ch=static_cast<char>(i); //приведение int в char
d=static_cast<double>(f); /приведение float к double
i=static_cast<unsigned char>(ch); //приведение char к unsigned char return;
}
Вот еще один пример, где используются классы вместо встроенных типов C++:
class Basel
{/*.-*/};
class Base2
{/*..• */};
class DerivedClass:public Basel, public Base2
{/*.•-*/};
int h()
Basel* pbl=new DerivedClass;
DerivedClass* pd=static_cast<DerivedClass*>(pbl);
if (Pd)
{
cout «"The resulting pointer's type:["« typeid(pd) . name () «"1 "endl;
)
return 0;
}
Результатом является следующий вывод:
The resulting pointer's type:[DerivedClass *]
Обратите внимание на четыре важных замечания о приведенном примере:
Поскольку указатель рМ действительно указывает на объект типа DerivedClass, приведение работает корректно.
Глава 9
Поскольку базовый класс Basel не является виртуальным базовым классом для DerivedClass, оператор static_cast() применяется успешно.
Поскольку базовый класс Basel не является полиморфным, оператор static_cast() можно применять безопасно.
Существует однозначное преобразование из Basel в DerivedClass.
Оператор reinterpret_cast()
Оператор reinterpret_cast() — это альтернатива непереносимым и потенциально небезопасным типам приведений в старом стиле. Этот оператор выполняет преобразование между указателями, их типами и числами в указатели и наоборот. Ниже приведен синтаксис оператора:
reinterpret_cast<Type>(Object);
Здесь Туре может быть указателем, ссылкой, арифметическим типом, указателем на функцию или на число. Оператор reinterpret_cast() работает в том случае, если существует неявное преобразование из Object в Туре. В противном случае он генерирует ошибку времени компиляции. Оператор не изменяет побитовый шаблон на машинном уровне. Он может удлинить или усечь размер шаблона, и, конечно, значение изменится. Результат выполнения оператора reinterpret_cast() непредсказуем и часто зависит от реализации. Например, используя reinterpret_cast(), указатель можно явно преобразовать к любому целому типу, достаточно большому, чтобы его вместить. Функция отображения определяется реализацией. Применяя reinterpret_cast(), вы должны знать, что делаете — точно так же, как это было с приведениями старого стиля.
Следующий пример поясняет, как использовать оператор reinterpret_cast():
#include <iostream.h> void func(void* pv)
(
//обратное приведение из типа указатель в целый тип
int value3=reinterpret_cast<int>(pv);//работает хорошо
cout«"Value of values : "<<value3«endl;
H
)
int main() (
//приведение из целого типа в тип указателя
int valuel=8;
double value2=13.169;
func(reinterpret_cast<void*>(valuel));
func(reinterpret_cast<void*>(Svalue2));
При компилировании с использованием Visual C++ 5 представленная программа выдаст следующий результат:
Value of value3 : 8
Value of value3:1245044
В примере для использования преобразованного значения (valuel) вы должны прежде преобразовать его обратно к исходному типу (int) в функции func(). Вторая строка вывода показывает непредсказуемое значение, полученное в результате вызова reinterpret_cast().
Когда использовать операторы dynamic_cast(), static_cast() или reinterpret_cast()
При приведении полиморфных классов одной и той же иерархии всегда используйте оператор dynamic_cast.
В других случаях необходимо применять операторы static_cast() или reinterpret_cast. Пока компилятор не выдает никаких ошибок, используйте static_cast(). В противном случае может потребоваться применение reinterpret_cast() с целью изменить битовую интерпретацию. Не переусердствуйте с операторами reinterpret_cast() и static_cast() — используйте их только в том случае, если это реально необходимо. Помните: то, что данные операции поддерживаются вашим компилятором C++, еще не означает, что их применение всегда дает безопасные и предсказуемые результаты.
Часть II
Оператор const_cast()
Три оператора приведения, описанные в предыдущих разделах, поддерживают постоянство объектов, к которым происходит приведение. Другими словами, эти операторы не предназначены для устранения постоянства объекта. Оператор const_cast() используется для добавления или удаления из типа модификаторов const (и volatile). Ниже приведен синтаксис оператора const_cast():
const_cast<Type>(Object);
Здесь Туре и Object должны иметь один и тот же тип, за исключением модификаторов const и volatile. Приведение разрешается во время компиляции. Тип результата — Туре.
С помощью const_cast() указатель (или ссылка) на const может преобразовываться в указатель (или ссылку), отличный от const. Если операция успешна, то результирующий указатель ссылается на объект, идентичный с операндом во всех остальных отношениях. Оператор const_cast() подобные преобразования выполняет и над модификатором volatile: он приводит указатель (или ссылку) на объект volatile к объекту, отличному от volatile, без каких-либо других изменений типа объекта. Пример описанных приведений представлен в листинге 9.8.
Листинг 9.8. Устранение константности объекта с помощью const cast() ♦include <iostream.h> ♦include <typeinfo.h> class myClass { public: myClass(int v) :_val(v) ,_count(0) (};
-myClass() ( cout« "_val=" <<_val«endl ; cout« "_count= " «_count«endl ; ) void f() const; private: int _val,_count; } ; void myClass: :f() const { //_count++; //ошибка: поэтому превращено в комментарий const_cast<myClass*>(this)->_count++; //ok ) int main() ( myClass m(8) ; m.f () ; m.f () ; m.f () ; return 0; }
Программный код породит следующий вывод:
_val=8
_count=3
Помните о том, что, когда функция-член объявляется как const, компилятор помечает как ошибку любую попытку изменить данные в родительском объекте из этой функции. Следовательно, внутри функции-члена f() вы не можете напрямую изменить член count. В примере внутри этой функции к объекту const this применен оператор const_cast() с целью преобразовать его к объекту, отличному от const, имеющему такой же тип (шуТуре*). Это позволило функции f() типа const изменить член данных _count.
Новые приведения против старых
Конечно, новые операторы приведения типов — dynamic_cast(), static_cast(), reinterpret_cast() и const_cast() — требуют для своей записи и чтения больше текста. Приведения языка C++ в старом стиле
Глава 9
необходимо заменить на новые. Вспомним, что язык C++ позволяет выполнять два типа приведений в старом стиле:
(Type)expression; //стиль привидения С
Type(expression); //функциональное представление приведения типов
Хотя C++ все еще поддерживает (и будет поддерживать) приведения в старом стиле, но в новых программах лучше использовать операторы приведения. В приведениях старого стиля легко сделать ошибку — случайно устранить const или преобразовать в тип несвязанный указатель вместо указателя в иерархии. Более того, приведения в старом стиле, может быть, исходно и работали, но теперь, после изменения типов, стали ошибочными (хотя компилируются без ошибок). Компилятор не поможет найти такие ошибки. Приведения в старом стиле болезненно просматривать и находить в программах. Вот пример, показывающий, почему приведения в новом стиле лучше и безопаснее по сравнению со старыми приведениями:
lass А
О;
class В
(
public: virtual void f(){)
);
class C : private В
О;
int main() (
A* pa=new A ;
В* pbl=(B*)pa; //нет ошибок времени компиляции
В* pb2=static_cast<B*>(pa); //ошибка времени компиляции
С* pc=new С;
В* рЬЗ=(В*)рс; //нет ошибок времени компиляции
BS pb4=dynamic_cast<BS>(*pc); //ошибка времени компиляции
)
В этом примере следующий оператор будет компилироваться успешно, несмотря на то, что он преобразует класс А к несвязанному классу В:
В* рЫ=(В*)ра;//нет ошибок времени компиляции
Такое приведение может быть во время выполнения по настоящему опасно. Кроме того, такую ошибку нелегко обнаружить при чтении кода.
Точно так же данная строка не будет отмечена как ошибочная даже в том случае, если программа попытается получить доступ к приватной базе В или С:
В* рЬЗ=(В*)рс;//нет ошибок времени компиляции
Как видно, такие неправильные преобразования "отлавливаются" при использовании операторов приведения нового стиля.
Резюме
Неявное приведение производного указателя на базовый класс является одним из наиболее часто употребляемых средств языка C++. Однако иногда вы будете попадать в ситуацию, в которой почувствуете, что могли бы написать эффективную программу, если бы знали (во время выполнения) точный тип объекта, указываемого базовым указателем. RTTI предоставляет подобную информацию и доказала свою полезность многим программистам. Фактически большинство коммерческих библиотек реализуют ту или иную форму виртуальной функционально-базированной RTTI. Хотя RTTI представляет собой большую услугу, ее часто используют неправильно, как новички, так и опытные программисты. Замена виртуальных функций оператором typeid() или dynamic_cast() — является самым распространенным случаем ошибочного применения RTTI. Как и большинство механизмов в языке C++, RTTI не имеет каких-либо механизмов самозащиты от неправильного употребления, и если вы определенно желаете сделать ошибку, то это вполне возможно.
1010НастройкаНастройкапроизводительностипроизводительностиприложенияприложения
10
10
Настройка
Настройка
производительности
производительности
приложения
приложения
В ЭТОЙ ГЛАВЕ
Функции inline вне определений классов
Как избежать раскрытия программного кода реализации в распространяемых header-файлах
Анализ стоимости виртуальных функций и виртуальных базовых классов
Компромиссы RTTI
Управление памятью для временных объектов
Глава 10
Среди разработчиков программ нет единого мнения относительно настройки приложений. Одни, учитывая мощь современных компьютеров, полагают, что настройка не нужна. Эта группа специалистов также полагает, что всю возможную оптимизацию программы должен выполнять компилятор, другие считают, что конструирование важно для повышения производительности приложений. Когда мы реализуем конструкцию, то выбираем оптимальные алгоритмы и используем механизмы производительности языка, поскольку не можем предсказать конфигурацию компьютеров потенциальных пользователей и поскольку компилятор не всегда знает, как выполнять оптимизацию программы.
В этой главе описываются функции inline, реализация программного кода в header-файлах, виртуальные функции и виртуальные базовые классы, информация о типе времени выполнения и временные объекты.
Функции inline вне определений классов
Ключевое слово inline подразумевает, что компилятор должен расширять тело функции всякий раз, когда он сталкивается с обращением к такой функции. Расширение уменьшает накладные расходы на вызов функции, поскольку вызова как такового и нет. Обратите внимание, на слово "подразумевает". Компилятору может быть неудобно вставлять функцию в строку. Почему компилятор может не вставить функцию, мы обсудим в этой главе далее.
Принятый стиль диктует, что вы должны обеспечить определение функции-члена inline вне объявления класса. Такой подход предохраняет определение класса от фрагментирования и облегчает чтение. Нет необходимости применять ключевое слово inline к объявлению функции-члена. Однако обязательно применяйте ключевое слово inline к определению функции-члена. Следующий фрагмент кода демонстрирует стиль:
class Object {
public:
inline void f( ); H нет в объявлении класса!
};
Этот пример поднимает вопрос, касающийся расположения определения функции-члена inline. Где его разместить? Хорошо, компилятор может вставить в строку функцию только в том случае, если он видит ее определение. Наиболее логичным местом для размещения определения является header-файл, содержащий определение класса. Автор помещает определения функций inline в отдельные header-файлы. Они не обязательно являются header-файлами в буквальном смысле, а реально являются реализацией файлов, заданных расширениями .срр, .icc или л. Вы можете использовать любое расширение. Только обязательно используйте стандартное название. Такие header-файлы inline затем вставляются в конец header-файла, содержащего определение класса. Это хороший стиль. Следование такой практике разделяет интерфейс и реализацию. Это же облегчает поддержку. Если вы решили не использовать header-файлов inline, то поместите функции-члены inline после объявления класса. И вновь, такой подход обеспечивает чистое разделение объявления класса и его реализации. Вот простой пример:
Н вводной файл: Object.h
class Object. { public: void f( ) ;
) ;
♦include "object.i”
H вводной файл: Object, i
inline void Object::f( ) {
)
С целью расширить пример также используются макросы для включения и отключения вставки функций в строку. В своем header-файле автор сделал следующее:
// вводной файл: Object.h
class Object { public: void f ( ) ;
} ;
#ifdef DO INLINE
Часть II
#include "object.!"
#endif
Затем осталось лишь возбудить переключатель -d DO_INLINE, чтобы компилятор получил механизм функций inline. Конечно, недостаток заключается в том, что автор должен повторно компилировать весь программный код, который нужен функциям-членам inline. К сожалению, способа обойти повторную компиляцию не существует. Если вы выполнили компиляцию с включенным режимом inline и позже решили удалить такой режим (или наоборот), то должны выполнить повторную компиляцию.
Некоторые разработчики утверждают, что inline — это просто хитрый способ подстановки макросов. Такое утверждение неверно, поскольку функция inline следует тем же самым правилам, касающимся проверки области видимости и типа, которым следуют и обычные функции. Функции inline имеют внутреннюю свя- зываемость везде, где происходит их вызов.
Функции inline имеют и несколько недостатков. Прежде всего, вставка текста функции в строку может привести к увеличению размера приложения. Если компилятор решит вставлять функцию в строку, то тело функции расширяется при каждом ее вызове. Если функция вызывается 200 раз, то будет 200 появлений тела функции, разбросанных по всему приложению. Это может действительно замедлить программу по сравнению с отключенными функциями inline.
Другой недостаток касается поддержки приложений. После изменения тела функции inline любые транслирующиеся модули, которые вызывают функцию, должны компилироваться повторно.
Как же решить, когда функция должна быть inline? Вот несколько общих правил:
Когда тело функции мало
Когда функция вызывается множество раз
Когда затраты на производительность операторов внутри тела функции минимальны.
О втором пункте можно спорить. В некоторых случаях компилятор будет вставлять в строку функцию с большим телом программного кода. Если такая функция вызывается много раз, то программа сильно разрастется. При таком сценарии это правило неприменимо. Почему же тогда автор включил его в перечень? Как объясняется в этой главе далее, возможно, что общее количество строчного кода будет меньше общего количества кода для вызова функции.
ПРИМЕЧАНИЕ
Для определения влияния на размер результирующего кода постройте приложение с включенным режимом inline. Запишите размер приложения. Затем перестройте приложение с отключенным режимом inline. Сравните результаты и определите эффект вставки функций в строку.
Функции-члены доступа и мутации являются первоочередными кандидатами на превращение их в функцию inline: они малы, обычно при выполнении приложения вызываются много раз и состоят из одного или двух простых операторов. Функция доступа используется для возврата значения внутреннего атрибута класса. Такие атрибуты должны объявляться с приватной видимостью. Функция-мутатор используется для установки значения (передается как аргумент) специального атрибута в классе, как показано в следующем программном коде:
long Object::getID( ) const // доступ {
return( idNumber ) ;
}
void Object::setID(const long valueln) // мутатор
( idNumber = valueln ;
}
Чтобы ввести эти функции-члены, укажите ключевое слово inline:
inline long Object::getID( ) const {
return( idNumber ) ;
)
inline void Object::setID(const long valueln)
( idNumber = valueln ;
)
Глава 10
Некоторые функции при размещении inline могут порождать меньшее количество строк программного кода. Для вызова функции компилятор должен сгенерировать код, подготавливающий вызов функции, настроить кадр стека, вызвать функцию, выполнить очистку стека и затем возвратить управление. Для функции, которая обеспечивает завершенное действие, такое как возврат значения переменной, ее программный код может оказаться значительно меньше по сравнению с кодом, который требуется для реализации вызова функции.
Когда в объявлении функции-члена употребляется ключевое слово inline, то это только объявление для компилятора. Если тело функции не существует, то компилятор не в состоянии определить, можно ли вставить функцию-член в строку. Таким образом, применение ключевого слова inline к объявлению является для компилятора бесполезной информацией. Кроме того, применение ключевого слова inline к объявлению подразумевает специфическую реализацию для пользователей класса. И здесь ключевое слово inline должно применяться только к определению функции-члена. Следующий пример показывает, что ключевое слово inline следует применять не к объявлению, а к определению.
// object.h
class Object {
public:// компилятор не знает, что делать
inline void setID( const long valueln ) ;
) ;
Применяйте ключевое слово inline к определению. В этом случае компилятор может видеть тело и решить, вставлять ли функцию в строку или нет. Если компилятор решит вставлять, то у него уже есть тело для вставки:
inline void setID ( const long valueln )
{ idNumber = valueln ;
}
Определение функции-члена при объявлении подразумевает вставку в строку. Когда вы определяете функцию-член внутри класса, то ключевое слово inline не требуется.
ПРИМЕЧАНИЕ
Не определяйте функцию-член внутри объявления класса. Если пренебречь этим правилом, то класс будет фрагментирован, а его поддержка затруднена.
Следующий пример показывает, как может выглядеть класс, если бы вы определили функции-члены внутри объявления класса. Вообразите, что вы назначены тем человеком, который будет поддерживать исходный программный код! Вот пример: // object.h class Object ( public: Object( ) : idNumber(OL) , name( new char[100] ; { /* empty */ } Object( const Object Srhs ) : IdNumber(OL), name( new char[strlen(rhs.name)+1] ) { strcpy( name, rhs.name ) ;
}const Objects operator=( const Object Srhs )
{if ( this != Srhs )
{ delete [ ] name ; name = new char[ strlen(rhs.name)+1] ; strcpy( name, rhs.name ) ; id = rhs.id ;
} return *this ; } -Object( )
Часть II
{ if( name ) delete [ ] name ;
}
void setID(const long valln)
(idNumber = valueln;};
long Object::getID( ) const
{ return! idNumber ) ; };
long Object::name( const char ‘name ) { if ( this.name )
delete [ ] this.name ;
this.name = new char[ strlen(name)+1 ]; strcpy ( this.name, name };
}
long Object::name( ) const
{ return( idNumber ) ; );
private:
long idNumber; char ‘name ;
} ;
Ранее автор упоминал, что компилятор может проигнорировать ключевое слово inline. Причины, по которым компилятор не вставляет функцию в строку, не всегда четко очерчены. Вот несколько ситуаций, в которых компилятор не удовлетворит запрос на inline:
В случае с функциями, содержащими такие циклические конструкции, как for, while, do ... while.
В случае с рекурсивными функциями
Если компилятор еще не видел определения функции
■ Если компилятору функция показалась слишком сложной
Замечание, касающееся последнего пункта: каждый поставщик реализует критерии измерения сложности по-разному, поэтому трудно определить, когда компилятор решит, что функция слишком сложна для вставки ее в строку. К тому же это признак плохого вкуса вставлять в строку виртуальные функции, поскольку они связываются динамически. Другими словами, должны быть известны их адреса. Существуют также причины, не позволяющие вставлять функции в строку, диктуемые поставщиком. В таком случае обратитесь к документации поставщика компилятора.
Следует знать об одном побочном эффекте вставки функции в строку. Предположим, что у вас есть функция-член, которую компилятор решил не вставлять в строку по причине ее большого размера. Некоторые компиляторы могут создавать внестрочные функции inline. Их также называют статическими функциями inline. Вы спросите, что же это означает? В каждом файле, в котором есть обращение к функции, компилятор создает тело функции со статической связностью. Это порождает версию функции в каждом транслирующемся модуле. Будьте внимательны — ваш исполняемый модуль может и, наверное, будет очень большим, если компилятор выберет функции outline inline. Ситуация может стать еще более сложной, если компилятор решит выбрать outline inline более чем для одной функции! К сожалению, не существует правила для определения, достаточно ли функция-член мала, чтобы превращать ее в функцию inline. Решение принимает компилятор.
ПРИМЕЧАНИЕ
Если вы подозреваете, что функция для вставки в строку слишком велика, то посмотрите, что производит компилятор. Если возможно, то изолируйте функцию в отдельном файле и просмотрите ассемблерный код, произведенный компилятором. Изучив ассемблерный код, вы можете узнать, вставлял ли компилятор функцию в строку или нет. Если исследование ассемблерного кода невозможно, то постройте приложение с функцией, вставленной в строку. Затем создайте версию приложения, в котором функция в строку не вставляется, и сравните размеры результирующих приложений.
ЧТО, ЕСЛИ ВМЕСТО ВСТАВКИ В СТРОКУ ИСПОЛЬЗОВАТЬ МАКРОС?
Макрос имеет свои недостатки. О макросах следует знать, что они выполняют только подстановку текста и не более того. Макрос не оценивает аргументов, которые ему передаются. Аргументы просто "включаются" в точку вставки. Еще важнее то, что макросы небезопасны с точки зрения типов и могут оказаться невидимыми для отладчика. Рассмотрим следующий пример:
Глава 10
#define DOUBLE_IT(a) (а * а)’
//Если вы сделаете это,
int i = 5 ;
int j = DOUBLE_IT(i) ;-.'.TV"
//препроцессор расширит его
int j = (5 * 5);/’■■?1 V:
Здесь нет ничего удивительного. А теперь рассмотрим другой пример:'
int j = DOUBLE_IT(10 + 10) ;
//препроцессор расширит макрос
int j = (10+10 * 10+10) ;
Это не то, что мы хотели. Вот еще один оператор, который породит интересные результаты:
int j = DOUBLE_IT(++i) ;
Данный оператор расширяется в следующий:,
int j = (++i * ++i) ;...=.V.,.
И это не то, что мы хотели. Если же в обеих ситуациях использовать функцию inline, то результаты будут такими, какими мы и предполагали.a :? ;»t i
Используйте макросы только для простых подстановок. Еще лучше, используйте const вместо #define. »
Как избежать раскрытия программного кода реализации в распространяемых header-файлах
Одна из основных концепций, касающихся ООП, — это маскирование реализации. Никогда не следует раскрывать то, как реализована функциональность ваших классов. Внутри header-файлов должны находиться только объявления классов, но не их реализация. Объявления класса должны раскрывать только объявления функций-членов. Вы должны скрывать определения функций-членов в файлах реализации (.срр, .схх и др.). Никогда не следует раскрывать программный код реализации в header-файлах.
Еще одна причина, по которой не следует реализовывать код в header-файлах, связана с поддержкой программного кода. Гораздо труднее поддерживать исходный код приложения, когда часть кода находится в header-файлах. Проблема еще более усложняется в случае, когда над приложением работают несколько разработчиков. Одни разработчики могут реализовывать программный код в header-файлах при каждой возможности, другие могут никогда не помещать код в header-файлы, а третьи могут делать это в зависимости от обстоятельств. Чтобы предотвратить подобную непоследовательность, команда разработчиков должна иметь документ, стандартизирующий кодирование. Если такого документа нет, то вы и ваша команда должны сесть и составить его.
ПРИМЕЧАНИЕ
Никогда не раскрывайте программного кода реализации в распространяемых header-файлах.
Код реализации в header-файлах может снизить производительность приложения. Если вы помещаете код реализации в header-файлы, то время компиляции увеличится и может выясниться, что в нескольких транслируемых модулях существует несколько версий классов и функций. Сохранение реализации отдельно от интерфейса позволит менять реализацию без повторной компиляции всей системы.
Избегайте объявления в header-файлах статических переменных и композитных типов. Если у вас есть header-файл, объявляющий определенную целую переменную как статическую, то любой транслирующийся модуль, который включает данный header-файл, будет содержать свою собственную копию переменной типа int. Если внутри заглавного файла много транслирующихся модулей и много определений статических переменных, то размер приложения может вырасти куда больше, чем вы ожидаете. Компоновщик, конечно, не будет против этого возражать, поскольку определения уникальны для каждого транслирующегося модуля. Если удалить ключевое слово static из этих же самых определений данных, то будет выдано сообщение об ошибке, предупреждающее о множественном определении переменных.
В предыдущих абзацах автор использовал термин статические переменные. В языке C++ существуют глобальные статические переменные (или внешние) и статические члены класса. Статическая глобальная переменная объявляется вне любого класса или функции и видна всем функциям, начиная с ее определения в транслирующемся модуле. Статический член класса — это член класса, который имеет статическую свя-
Часть II
зываемость. Существует только одна копия статического члена класса для всех экземпляров класса. Все экземпляры класса совместно используют единственную статическую копию. Это отличается от членов экземпляра класса, которых столько же, сколько экземпляров класса.
Анализ стоимости виртуальных функций и виртуальных базовых классов
Автор провел много обсуждений, касающихся "стоимости" виртуальных функций и виртуальных базовых классов. Чаще всего он слышал вопрос: "Каковы затраты на вызов виртуальной функции и использование виртуальных базовых классов?" Это хороший вопрос, который заслуживает ответа. Но прежде всего пересмотрим некоторые основные положения, касающиеся основ виртуальных функций и виртуальных базовых классов.
Виртуальные функции
С самого начала определим, что такое виртуальная функция. Виртуальная функция — это функция-член, которая объявлена с ключевым словом virtual. Обратите внимание, мы говорим функция-член, поскольку не можем применить ключевое слово virtual к функции, не относящейся к классу. Мы применяем ключевое слово virtual к объявлению функции-члена, а не к определению, например:
class Object {
public:
virtual void f ( ) ;
) ;
Соответствующее определение таково:
void Object::f( ) { . . . } ;
Это и все, что необходимо сделать для объявления виртуальной функции. Не требуется определять ничего специального, просто кодируете требуемую функциональность так, как вы делали бы это и для любой другой функции. Важно подумать о функциональности по умолчанию для базового класса, которую виртуальная функция обеспечивает для порожденных классов. Неожиданным для многих разработчиков является то, что ключевое слово virtual не обязательно должно применяться к любому объявлению функций порожденного класса. Компилятор знает, что функция-член в порожденном классе является виртуальной, если она виртуальна в базовом классе, поскольку ее сигнатура такая же, как и в базовом классе. Это справедливо на всем пути вниз по иерархии наследования. Следующий фрагмент кода демонстрирует тот факт, что виртуальность наследуется порожденными классами:
class Object {
public:
virtual void f ( ) ;
) ;
class Derived : public Object { public:
void f( ) ;
) ;
В этом примере функция Derived::f() автоматически виртуальна. Насколько позволяет стиль кодирования, автор всегда показывает ключевое слово virtual. Важно явно заявить об использовании каждой функции-члена, так чтобы клиенты порожденного класса понимали его использование.
Ключевое слово virtual используется в том случае, если в порожденном классе ожидается переопределяющая функция-член. Однако не всегда бывает ясно, будете ли вы переопределять функцию-член в порожденном классе. Это должно решаться при конструировании классов. При хорошей предварительной проработке вы будете знать, когда любая функция-член должна быть виртуальной. Затраты на использование виртуальных методов будут рассмотрены в данном разделе позже.
Кстати, сказанное вызывает вопросы, касающиеся объявления виртуального деструктора. Ключевое слово virtual применяется к деструктору в том случае, если вы будете порождать от базового класса, или если класс имеет хотя бы одну виртуальную функцию. Если вы точно не знаете, от чего будет порожден ваш класс, то объявляйте деструктор virtual. Если не объявить деструктор как virtual в случае, когда есть наследование, то может быть вызван не тот деструктор. Следующий пример показывает, что вызывается только деструктор Object — и это не то, что вы хотите:
Глава 10
class Object { public:
-Object( ) ;
} ;
class Derived : public Object { public:
“Derived( ) ;
} ;
//...
Object *thing = new Derived ;
delete thing ;
//...
Этот пример демонстрирует полиморфное поведение (динамическое связывание) во время выполнения. Термин динамическое связывание или позднее связывание используется потому, что вызываемая функция не разрешается до времени выполнения. С другой стороны, раннее связывание ссылается на тот факт, что компилятор может разрешить правильную функцию на момент компиляции. При раннем связывании компилятор генерирует правильный программный код для выполнения прямого вызова функции.
Как механизм времени выполнения определяет правильную виртуальную функцию, подлежащую вызову? Определение делают, базируясь на типе объекта, а не на ссылке на него. С другой стороны, если порожденный класс содержит функцию-член, которая перекрывает функцию базового класса и они не виртуальны, то вызываемую функцию определяет ссылка. Программный код в листинге 10.1 демонстрирует описанное поведение.
Листинг 10.1. Вызовы виртуальной и невиртуальной функций #include <iostream> class Base { public:
virtual void vf( ) { std::cout « "Base: :vf" « std::endl ; ) void nvf ( ) ( std: :cout « "Base::nvf" « std: :endl ; )
class Derived : public Base { public:
void vf ( )( std::cout « "Derived::vf" « std::endl ; }
void nvf ( ) ( std::cout « "Derived: : nvf" « std: :endl ; } 1 ; int main( ) { Base aBase ; Derived aDerived ;
Base *basePtrToDerived = 6aDerived ; basePtrToDerived->vf( ) ; basePtrToDerived->nvf( ) ;
return 0 ;
1
При выполнении программы из листинга 10.1 она сгенерирует следующий вывод:
Derived::vf
Base::nvf
Хотя мы используем указатель типа Base, реальный указываемый объект имеет тип Derived. Итак, как же работает виртуальный механизм? Ответ на этот вопрос возвращает нас назад к исходному вопросу, заданному в начале раздела: "Какова цена вызова виртуальных функций?"
Виртуальный механизм реализован с применением таблицы виртуальных функций (сокращенно VTBL) и указателя виртуальной функции (сокращенно VPTR). В этих двух элементах нет ничего магического. Вам, вероятно, приходилось воплощать подобные конструкции. Для каждого класса, который содержит виртуальную функцию, создается и поддерживается одна VTBL. VTBL — это массив указателей, каждый указатель указывает на реализацию каждой виртуальной функции, объявленной в классе. VTBL представляет собой массив, и фактически она может быть реализована как связный список или какой-либо другой специализированный алгоритм. Важно понимать, что в VTBL есть указатель для каждой виртуальной функции. Имейте
Часть II
в виду, что указатель может указывать на определение виртуальной функции для текущего класса, если оно существует, или на определение виртуальной функции базового класса. Подробнее это будет рассмотрено далее в этой главе.
VPTR представляет собой скрытый указатель, имеющийся в каждом классе. Единственная задача VPTR — указывать на VTBL. VPTR указывает на вход первого указателя в VTBL. При большинстве реализаций используется первый вход для хранения указателя на объект type_info класса. Более подробная информация о классе type_info и об информации о типе времени выполнения приведена в разделе "Компромиссы RTTI" далее в этой главе. Когда бы компилятор ни открывал класс с объявленной виртуальной функцией, он создает данный скрытый указатель вместе с остальной структурой класса. Независимо от того, сколько в классе виртуальных функций, VPTR бывает только один. VPTR и VTBL не существуют для классов, не содержащих виртуальных функций.
Теперь подумаем о затратах на пространство и время, связанных с виртуальными функциями. Для каждого класса, который объявляет виртуальную функцию, существует VTBL. VTBL существует также для каждого порожденного класса, содержащего виртуальные функции. Этот процесс продолжается по мере прибавления уровней наследования. Не забудьте и о том, что каждый реализованный объект содержит VPTR, который указывает на VTBL. В VTBL имеется указатель для каждой виртуальной функции класса. Чем больше объявлений виртуальных функций в классе, тем больше VTBL. Если каждый указатель занимает, например, 4 байта, то размер VTBL быстро увеличивается. И не забудьте о VPTR. Они добавляют по 4 байта к каждому экземпляру класса. Ниже представлен конкретный пример:
class Base(public:virtual virtual virtualprivate:class Base(public:virtual virtual virtualprivate:void void voidvoid void voidvfuncOne( ) vfuncTwo( ) vfuncThree(vfuncOne( ) vfuncTwo( ) vfuncThree({ ( ){ ( )1 ;1 ;{ 1 ;1 ;1 ;{ 1 ;int value ;int value ;1 ; class Derived : public {1 ; class Derived : public {BaseBasepublic: virtual void virtual voidpublic: virtual void virtual voidvfuncOne( vfuncTwo(vfuncOne( vfuncTwo({<{<}}}}// виртуальная функция private:int value ;// виртуальная функция private:int value ;vfuncThree не перекрываетсяvfuncThree не перекрывается
class Base
(
public:
virtual virtual virtual
private:
class Base
(
public:
virtual virtual virtual
private:
void void void
void void void
vfuncOne( ) vfuncTwo( ) vfuncThree(
vfuncOne( ) vfuncTwo( ) vfuncThree(
{ ( )
{ ( )
1 ;
1 ;
{ 1 ;
1 ;
1 ;
{ 1 ;
int value ;
int value ;
1 ; class Derived : public {
1 ; class Derived : public {
Base
Base
public: virtual void virtual void
public: virtual void virtual void
vfuncOne( vfuncTwo(
vfuncOne( vfuncTwo(
{
<
{
<
}
}
}
}
// виртуальная функция private:
int value ;
// виртуальная функция private:
int value ;
vfuncThree не перекрывается
vfuncThree не перекрывается
}В}ВОбъекты типа ObjectОбъекты типа ObjectРИСУНОК 10.1.РИСУНОК 10.1.Графическое описание виртуальнойГрафическое описание виртуальнойтаблицы и указателя.таблицы и указателя.
}
В
}
В
Объекты типа Object
Объекты типа Object
РИСУНОК 10.1.
РИСУНОК 10.1.
Графическое описание виртуальной
Графическое описание виртуальной
таблицы и указателя.
таблицы и указателя.
этом примере одна VTBL — для Base и одна — для
Derived. Таблицы VTBL для Base и Derived распределяют пространство для хранения трех указателей. Вы можете сказать: "Да, но функция vfuncTree не была переопределена в Derived". Это правда, но указатель все равно существует и указывает на vfuncTree Base. Обратите внимание, что каждый класс имеет только один объявленный атрибут и что тело каждой функции пусто. Предположим, что во время выполнения вы реализовали по одному объекту для каждого класса. В результате этого будут добавлены 8 дополнительных байтов (для VPTR) для двух объектов. Накладные расходы на пространство могут быть ощутимы, если, например, вы строите размещенную систему или другое критичное к пространству приложение. Тем не менее, выгоды от использования виртуального механизма должны превзойти затраты. На рис. 10.1 показано графическое представление двух классов.
Имейте в виду, что для невиртуальных функций в таблице VTBL входов указателя нет. Конструкторы также не имеют входов в VTBL. Помните, если виртуальная функция не перекрывается в порожденном классе, то указатель функции будет указывать на виртуальную функцию в базовом классе.
Глава 10
Виртуальные базовые классы
Теперь исследуем мир виртуальных базовых классов. Если работа происходит с множественным наследованием, то виртуальные базовые классы становятся важной частью программного ландшафта. У вас должно быть понимание того, как они работают, если вы хотите их правильно реализовывать. Кроме того, следует знать о некоторых побочных эффектах использования виртуальных базовых классов.
Виртуальные базовые классы объявляются с использованием ключевого слова virtual. Ключевое слово применяется к объявлению порожденного класса как раз перед именем базового класса. Преимущества применения виртуальных базовых классов становятся ясны только при создании класса, который порождается из двух базовых классов, которые разделяют общий базовый класс. Ниже приведен пример, демонстрирующий объявление:
class Base { public:
} ;
class Derived : public virtual Base { public:
) ;
Этот пример подразумевает, что используется наследование с одним корнем. Для данного типа наследования применять виртуальные базовые классы нет никакого смысла. Главная причина, по которой не стоит реализовывать виртуальное наследование для иерархии с одним корнем, заключается в накладных расходах времени выполнения, связанных с виртуальным наследованием.
Одна причина использования виртуальных базовых классов заключается в уменьшении объема копирования базового класса при работе с множественным наследованием. Другими словами, если вы используете множественное наследование и не используете виртуальные базовые классы, то будете получать лишние копии базового класса в порожденных классах. Как уже говорилось ранее, сказанное относится только к созданию класса, который наследует из нескольких порожденных классов, имеющих общий базовый класс. И наоборот, если вы используете виртуальные базовые классы, то порожденные классы будут разделять копию базового класса, например:
class Base {
protected: int value ;
} ;
class OneDerived : public Base (
} ;
class TwoDerived : public Base (
} ;
class UltimatelyDerived : public OneDerived, public TwoDerived { public:
int getValue( ) ( return( value ) ; }
} :
Класс UltimatelyDerived публично порожден от OneDerivied и TwoDerived. Это означает, что будет существовать копия класса Base как в OneDerivied, так и в TwoDerived, поскольку класс Base не объявлен как виртуальный базовый класс. Неоднозначность возникает в классе UltimatelyDerived в связи с атрибутом value, находящемся в классе Base. Функция-член в UltimatelyDerived с именем getValue возвращает содержимое value. Какую копию value она будет использовать? Копию, которая находится в OneDerivied, или ту, которая находится в TwoDerived? Компилятор будет в замешательстве и выдаст сообщение об ошибке. Неоднозначность можно разрешить явно, указав копию value, которую следует использовать. Для этого используется оператор разрешения области видимости:
int getValue( ) const { return( OneDerived::value ) ; )
Данная явная квалификация решает проблему неоднозначности и "успокаивает" компилятор, но не решает проблему множества копий базового класса. Для решения неоднозначности придется постоянно прибегать к оператору разрешения области видимости. Злоупотребление оператором разрешения области видимости может оказаться неприятным для глаз и может смутить разработчиков, унаследовавших программный код.
Вопросы реализации Часть II
Если вы привлекаете к работе виртуальное наследование, то компилятор гарантирует, что будет только одна копия базового класса. Эта исключительная копия базового класса совместно используется всеми порожденными классами. Рассмотрим пример, который демонстрирует виртуальное наследование:
class Base { protected: int value ;
class OneDerived : public virtual Base { public: void callMe( ) ;
} ; class TwoDerived : public virtual Base { public: void callMe( ) ;
} ; class UltimatelyDerived : public OneDerived, public TwoDerived { public: int getValue ( ) { return ( value ) ; }
} :
Компилятор создает единственную копию класса Base, которая совместно используется классами OneDerivied и TwoDerived. Это решает вопрос неоднозначности в отношении value, но возникает новая проблема. Обратите внимание на то, что общедоступная функция-член са11Ме() имеется и в OneDerivied, и в TwoDerived. Вновь компилятор не в состоянии определить правильную версию са!1Ме() для вызова. Во избежание неоднозначности и здесь придется использовать оператор разрешения области видимости.
В объявление са!1Ме() и в OneDerivied, и в TwoDerived вовлечен базовый вопрос конструкции. Если бы функциональность, обеспечиваемая функцией са11Ме(), могла использоваться совместно, то объявление и реализацию следовало бы перенести в Base. Другой путь решения проблемы заключается в объявлении и определении функции са11Ме() в классе UltimatelyDerived. Такой подход позволит спрятать версии са!1Ме() и в OneDerivied, и в TwoDerived. Как видно, виртуальные базовые классы не решают всех проблем множественного наследования.
Теперь исследуем некоторые накладные расходы, ощутимые при работе в виртуальным наследованием. Прежде всего проанализируем затраты для следующих классов. Обратите внимание, что мы не используем виртуального наследования.
class Base ( public: long value [ 100 ] ; } ;
class OneDerived : public Base { /* nothing */ } ; class TwoDerived : public Base { /* nothing */ } ; class UltimatelyDerived :
public OneDerived, public TwoDerived { /* nothing */ } ;
#include <iostream> int main() { Base b ; OneDerived one ; TwoDerived two ; UltimatelyDerived ultimate ; std::cout « "value =" « sizeof(b.value) « std::endl « "pointer =" « sizeof(Base*) « std::endl « "Base =" « sizeof(b) « std::endl « "One =" « sizeof (one) « std: :endl « "two =" « sizeof (two) « std::endl « "Ultimate =" « sizeof(ultimate) « std::endl ; return 0 ;
)
Вывод программы показан ниже:
Глава 10
value =400 pointer =4 Base =400 One =400 Two =400 Ultimate =800
В результате не должно быть ничего удивительного. Размер Base — 4 байта. Поскольку OneDerivied и TwoDerived наследованы от Base, то их размер также по 4 байта. UltimatelyDerived имеет размер 8 байтов, поскольку получает две копии Base. Добавим виртуальное наследование и посмотрим, что получится. Программный код остается таким же, за исключением следующих двух строк:
class OneDerived : public virtual Base { /* ничего */ } ;
class TwoDerived : public virtual Base { /* ничего */ } ;
Посмотрите на вывод из измененной программы:
value-400
pointer =4
Base ~400
One =404
Two =404
Ultimate =408
Появилось no 4 лишних байта в OneDerivied и TwoDerived. Дополнительные байты появились потому, что классы OneDeriyied и TwoDerived содержат указатель, который указывает на единственную копию Base. Помните, что классы, которые виртуально наследуются от базового класса, выражают желание совместно использовать данные базового класса. На самом деле имеет место уменьшение размера объекта UltimatelyDerived! При использовании виртуального наследования, по существу, размер объекта уменьшается вдвое, поскольку устранены лишние объекты. Хотя затраты на виртуальное наследование невелики (учитывая дополнительные указатели), размер объектов во время выполнения может быть значительным.
Коснемся еще одного момента — смешивания виртуального и невиртуального наследования. Изменим наши две строки программного кода еще раз, чтобы отразить данное соображение:
class OneDerived : public Base {/* ничего */ ) ;
class TwoDerived : public virtual Base { /* ничего */ } ;
Здесь класс OneDerivied будет иметь свою копию Base, а класс TwoDerived будет указывать на совместно используемую копию Base. После выполнения вывод программы будет следующим:
value=4
pointer =4
Base =4
One =4
Two =8
Ultimate =12
Обратите внимание, что OneDerivied теперь имеет размер только 4 байта. Это подтверждает факт, что любой класс, наследующий виртуально, содержит указатель.
Вооруженные такими знаниями о виртуальных функциях и виртуальных базовых классах, вы можете правильно проанализировать затраты на использование данных механизмов. Кроме того, для правильной реализации обсуждавшихся механизмов следует понимать, что делает компилятор.
Компромиссы RTTI
Runtime Type Information (RTTI) может быть полезной в специальных программных ситуациях, но может и вести к неэффективному поведению во время выполнения. Еще важнее то, что RTTI подрывает полиморфизм и отражает (потенциально) плохую конструкцию. Само имя говорит о природе механизмов RTTI как относящейся ко времени выполнения. Разработчики, работавшие на других языках, таких как Smalltalk или Pascal, могут испытывать искушение расширить сферу применения RTTI. Автор призывает свести активность по кодированию RTTI к минимуму. Если возможно, то полностью откажитесь от использования RTTI и применяйте только виртуальные функции.
RTTI используется для получения информации об объекте во время выполнения программы. Идентификационный механизм времени выполнения реализуется с учетом полиморфных типов. Это подразумевает, что г-ас должен содержать как минимум одну виртуальную функцию. Обсуждение виртуальных функций,
Часть II
виртуальных таблиц и виртуальных базовых классов проводилось в разделе "Анализ стоимости виртуальных функций и виртуальных базовых классов" ранее в этой главе.
Информация о классе хранится в объекте типа type_info. Указатель на объект type_info хранится как вход в виртуальную таблицу. Вход указателя обычно находится в первой ячейке таблицы. Поскольку объект type_info существует как отдельная сущность, размер таблицы увеличивается на размер указателя.
Объект type_info может показывать информацию об имени объекта, его равенстве или неравенстве по сравнению с другими объектами и место объекта в последовательности других объектов. Класс type_info имеет следующую структуру: class type_info { public: virtual ~type_info(); bool operator==(const type_infos) const; bool operator!=(const type_infos) const; bool before(const type_infoS) const; const char* name() const; private:
type_info(const type infoS) ; II предотвращение копирования type info 6 operator=(const type info&) ; II предотвращение присваивания
1;
Функция-член before используется для раскрытия места объекта в последовательности. Эта последовательность не раскрывает информации об иерархии наследования, поэтому вы не должны использовать ее для исследования отношений между объектами. Упорядоченная последовательность также зависит от реализации, поэтому не считайте, что результаты будут одинаковыми при использовании продуктов разных поставщиков.
Получение имени объекта может быть полезным для целей отладки. Атрибут паше хранится как NULL- определенная строка и возвращается функцией-членом паше.
Функции-члены operator== и operator != можно использовать для определения равенства или неравенства двух объектов type_info.
Оператор typeid используется для возврата ссылки const на объект type_info. Аргумент для typeid — это либо название типа, либо выражение. Выражение может быть ссылкой или указателем на какой-то тип. Если выражение является ссылкой или указателем, то ссылка type_info открывает действительный указываемый тип, а не тип ссылки (или указателя). Оператор typeid в случае, если выражение равно нулю, возбуждает исключение bad_typeid. Обратите внимание, что вы должны разадресовать указатель для выявления действительного указываемого объекта. Вот пример того, как можно использовать RTTI:
void walk( const Animal Sanimal ) ( if( typeid( animal ) == typeid( Tiger ) ) // ходит как тигр else if( typeid( animal ) == typeid( Monkey ) ) II ходит как обезьяна else if( typeid( animal ) == typeid( Elephant ) ) // ходит как слон
}
Как упоминалось ранее, этот программный код будет выглядеть знакомым тем, кто работал на языках Smalltalk или Pascal. Автору не нравится подобное использование RTTI, и вы не должны им злоупотреблять. Виртуальные функции представляют собой более элегантную логику времени выполнения такого типа.
Существует и другая веская причина, по которой вы должны избегать механизма RTTI: поддержка программного кода. Рассмотрим, что потребовалось бы, если бы к иерархии добавился еще один тип Animal. Пришлось бы пересмотреть всю логику RTT1, сделать соответствующие расширения и затем перекомпилировать весь код. Издержки, связанные с производительностью и надежностью для использования операторов if...else, также высоки.
RTTI обеспечивает безопасное приведение для полиморфных типов. Однако для неполиморфных объектов поведение не определено. Реально вы, скорее всего, получите информацию статического типа для объекта, переданного typeid. Вот пример программного кода, который использует иерархию неполиморфных классов:
Глава 10
Animal *ра - new Tiger ;
const typeid ЫНеТуре - typeid( *pa ) ;
if ( theType --- typeid( Animal ) ) // строка 3 cout << "It's only an Animal?" ;
else if( theType typeid ( Tiger ) )
cout <x "It's a Tiger, run!" ; II wvlxov№ не выполняется!
Оператор if в третьей строке всегда разрешается в значение true.
Оператор typeid работает для встроенных типов языка. Хотя автор не видит реальной пользы от его применения, но вы. тем не менее, можете его использовать. В следующем примере все операторы if будут выполнять соонгекдвующие операторы:
char aChar = ' с' ;
if ( typeid ( aChar ) == typeid ( char ) ) cout « "It's a char!" ;
if ( typeid ( saChar ) == typeid ( char * ) ) cout « "It's a char pointer!" ;
if( typeid( 'c' ) -= typeid( char ) ) cout « "Guess what, another char!" ;
Возможно, вы знакомы с оператором dynamic_cast, который определяет, безопасно ли приведение. Рассмотрим следующий пример, считая, что класс Animal и его порожденные классы являются полиморфными:
Animal *ра = new Tiger ;
Tiger *pt = dynamic_cast< Tiger *>( pa ) ;
if( pt ) Ц pt преобразуется в Tiger
cout « "There's a Tiger in the house!";
else
cout << "Whew, no Tigers in here";
Аргумент для dynamic cast в угловых скобках представляет собой тип, к которому вы пытаетесь выполнить приведение. Если приведение успешно, то будет получен требуемый тип. Если приведение неудачно, то возвращается указатель NULL. И в этом случае автор предпочитает использовать виртуальные функции вместо dynamic cast. При работе с виртуальными функциями не требуется проверять, опасно ли приведение, — обращение всегда разрешается в какую-нибудь виртуальную функцию в иерархии.
Используйте механизм RTTI экономно. Важно знать характеристики RTTI, чтобы правильно его применять. Следует иметь представление и о недостатках RTTI и знать, когда правильно использовать виртуальные функции, виртуальные базовые классы и RTTI. Кроме того, полезно уметь оценить затраты, связанные с использованием каждого механизма.
Управление памятью для временных объектов
Компилятор может преподнести сюрприз многими способами. Сюрприз, который получал любой программист, заключается в том, что компиляторы имеют тенденцию создавать временные конструкции. Вы даже можете и не знать, что компилятор создал что-то временное. Две наиболее распространенные ситуации — эго когда функция должна возвратить объект и когда объект передается функции. Мы также исследуем и несколько других причин, по которым компиляторы создают временные объекты.
Одно из свойств временного объекта заключается в том, что его нельзя видеть невооруженным глазом. Вы не всегда можеге увидеть временный объект, глядя на исходный программный код. Однако концептуально временный объект можно видеть, если понимать, как их создает компилятор. Временный объект всегда невидим в программном коде. Временные объекты рассматриваются как неименованные. Другими словами, имя явно им никогда не дается. Компилятор генерирует имя для временного объекта "за кулисами". Само собой разумеется, что вы не должны о них беспокоиться, поскольку обо всем заботится компилятор. Временные объекты не создаются из кучи, если, конечно, вы закодировали конструктор, распределяющий память динамически. Необходимо также следить за распределением памяти в функциях-операторах. И последнее, что необходимо пояснить, прежде чем двигаться дальше, — это то, что мы не говорим о временных переменных, которые создаются внутри функции или блока программного кода. Теперь создадим класс Integer и рабочий щъпраммный код:
class Integer !
public.
Integer( );
Часть II
Integer( const Integer 6 ) ;
Integer! int valueln ) ;
Integer! int valueln, int addedValue ) ;
Integer operator*( const Integer 61hs, const Integer 6rhs ) ; void operator=( const Integer 6 ) ;
} ;
Integer value = 2 ; // первая строка (работающий программный код) for( int i = 0; i < 100; i++ ) value = value + 2 ;
Последние три строки программного кода совершенно очевидны? С помощью первой строки конструируется Integer и используется оператор присваивания, который позволяет поместить в объект значение 2. Однако сначала компилятор должен преобразовать константу 2 в объект Integer. Компилятор делает это, используя конструктор, принимающий целый аргумент. Нагрузку на компилятор можно сократить таким образом:
Integer value(2) ;
Теперь посмотрим на цикл. Именно выражение цикла и представляет собой узкое место. Разделим выражение и фрагмент за фрагментом разберем, что же происходит. Прежде всего, существует постоянное выражение с правой стороны operator*. Компилятор должен сконструировать временный объект и инициализировать его значением 2. Компилятор должен делать это, поскольку operator* требует наличия объекта Integer для своего аргумента. Компилятор использует конструктор, который принимает целый аргумент, точно так же, как он делал это в первой строке. Затем компилятор генерирует программный код, возвращающий объект Integer, и использует конструктор копии для размещения результата в объекте value. Наконец, operator= используется для присваивания результата объекту value. Следует понимать, что компилятор не только создает временные объекты, но и разрушает их. Такое бесконечное конструирование и разрушение может "украсть" у приложения драгоценное время и пространство.
Выше упомянуто о том, что компилятор создает временный объект, если функция-член возвращает аргумент по значению. operator* для Integer демонстрирует данный эффект. Поскольку временный объект не именован, то компилятор может создать и разрушить объект Integer. Компилятор делает это даже в том случае, если игнорировать возвращаемое функцией значение. Рассмотрим пример:
Integer calculate( int valueOne, int valueTwo ) ;
calculate! 5, 10 ) ;
Компилятор по-прежнему будет создавать временный объект, хотя возврат игнорируется. И обойти проблему никак нельзя. Конструирование в расчете на эффективность — вот ключ к сокращению появления временных объектов. Существуют ситуации, которыми вы просто не управляете. Рассмотрим один способ, который можно применить в сражении с появлением временных объектов. Для демонстрации оптимизации именованного возвращаемого значения будем использовать operator*. Рассмотрим потенциальную реализацию:
Integer Integer::operator*( const Integer 61hs, const Integer 6rhs ) {
Integer temp = Ihs.value! ) + rhs.value! ) ; return temp ;
}
Этот код можно рассматривать как очень типичную реализацию. Создается экземпляр локального объекта Integer для хранения результата сложения, и затем экземпляр используется в операторе return. Если бы заменить Integer, например, родным типом int, то результирующий исходный программный код выглядел бы действительно очень естественно. Конечно, код, сгенерированный компилятором, был бы незначительным. К сожалению, код, сгенерированный для Integer::operator+, совершенно не такой. Вам требуется сократить издержки времени исполнения на создание временных объектов. Для этого необходимо использовать явный конструктор класса. Член operator* будет выглядеть следующим образом:
Integer Integer::operator*( const Integer 61hs, const Integer 6rhs ) (
return Integer! Ihs.value! ), rhs.value! ) ) ;
}
Глава 10
Скорее всего, вы думаете, что данный программный код по-прежнему генерирует временный объект для возврата. Отличие при использовании именованного конструктора состоит в том, что компилятор может оптимизировать временные объекты, т.е. делать то, что вы определенно от него хотите. Знание подобного обстоятельства позволяет вам усилить контроль за созданием временных объектов. (В данном случае следовало бы говорить за устранением, а не за созданием.) По сути, компилятор строит объект для возврата по месту. Поставщик компилятора может реализовать, а может и не реализовать оптимизацию именованного возвращаемого значения. Подробности — в документации.
Теперь, когда вы понимаете, что такое временные объекты, следует быть настороже относительно программного кода, который передает данные функциям. Следует повторно изучить код, который возвращает объекты. Скорее всего, компилятор создает временные объекты, если вы не использовали технику оптимизации именованного возвращаемого значения. Рассмотрите такое положение: если вы можете обеспечить более эффективную реализацию, то компилятор отблагодарит вас, породив более эффективный программный код.
Резюме
В этой главе были исследованы функции inline, виртуальные функции, виртуальные базовые классы, RTTI и временные объекты. Теперь вы можете оценить затраты, связанные с каждой из перечисленных конструкций.
Ключевое слово inline является только намеком компилятору. Вы знаете несколько причин, по которым компилятор может отказаться встраивать функцию в строку. Кроме того, было исследовано несколько побочных эффектов встраивания функций в строку.
Вы теперь имеете представление об эффектах раскрытия кода реализации в header-файлах. Подумайте о том, как реализован код в ваших программах и о том, как его можно усовершенствовать.
В этой главе вы узнали, как используются виртуальные функции и виртуальные базовые классы, и о том, какое влияние оказывают виртуальные функции на таблицу VTBL. Теперь вы знаете, как виртуальные базовые классы могут устранить копии базового класса в случае применения множественного наследования.
RTTI представляет собой механизм, предназначенный для специальных программных ситуаций, но вы не должны его применять для замены виртуального механизма. Помните, что RTTI подрывает полиморфизм и свидетельствует о плохом конструировании.
Не забывайте о временных объектах и о том, что компилятор молча создает эти объекты. Выясните, поддерживает ли ваш компилятор механизм оптимизации именованных возвращаемых значений. Если этот механизм поддерживается, то используйте его всегда, когда это возможно.
Обработка данных
ЧАСТЬ
В ЭТОЙ ЧАСТИ
■
и
Рекурсия и рекурсивные структуры данных
Использование методов сортировки
Алгоритмы поиска данных
Хеширование и синтаксический анализ
■
■
■
■
Рекурсия и рекурсивные
структуры данных
В ЭТОЙ ГЛАВЕ
Что такое рекурсия
Рекурсивные структуры
Обход рекурсивной структуры с помощью рекурсивной функции
Цикл и хвостовая рекурсия
Непрямая рекурсия
Рекурсия и стек
Отладка рекурсивных функций
Глава 11
Понятие рекурсии порой вселяет страх в души многих разработчиков. Рекурсия порождает понятия, определяемые через самих себя, программный код, вызывающий сам себя, и объекты, указывающие на образы самих себя. Эти определения кажутся пугающими. От них веет бесконечностью и парадоксами. Но вам не нужно их бояться, потому что мы перечеркнем все эти ужасные определения одной простой фразой: "Забудьте о них!"
Не следует, конечно, забывать о рекурсии как таковой, просто, когда вы пишете программный код, не думайте о том, что он рекурсивный. Запомните важный секрет рекурсии: не думайте о ней как о некотором бесконечном парадоксе и уровнях, ссылающихся сами на себя; думайте только об одном уровне в отдельный момент времени. Затем, когда подойдет время, вы увидите рекурсивные аспекты своей структуры или функции. Мы проиллюстрируем этот принцип на множестве примеров, от рекурсии в функции до рекурсивных структур, и покажем, как можно упростить процесс рекурсии, если не думать о ней, точнее, не думать о ней слишком много. Мы также расскажем о некоторых видах рекурсии, таких как хвостовая и непрямая рекурсия. Наконец, мы рассмотрим некоторые характерные проблемы, которые могут возникнуть при отладке рекурсивных функций.
Что такое рекурсия
Когда некоторая сущность ссылается сама на себя или на подобие самой себя, говорят о рекурсии. Например, рекурсия для объекта означает, что он ссылается на свое подобие с помощью указателя или ссылки. Часто рекурсивные структуры и рекурсивные функции идут рука об руку: многие общие операции для рекурсивных структур лучше всего выполнять с помощью рекурсивных функций. Некоторые из таких операций мы рассмотрим далее в этой главе.
Числа Фибоначчи: рекурсивное определение
Классическим примером рекурсии является определение чисел Фибоначчи. N-e число Фибоначчи определяется как сумма (N-l)-ro и (N-2)-ro числа Фибоначчи. Исключение составляют числа Фибоначчи, номер которых меньше или равняется 2. Такие числа по определению равны 1. В математической записи это определение выглядит следующим образом: Fibonacci (N) = If (N <= 2): 1
Otherwise : Fibonacci (N-l) + Fibonacci (N-2)
Это выглядит довольно сложно, поскольку, чтобы определить число Фибоначчи для любого значения N, которое больше 2, нужно знать два других числа Фибоначчи. Чтобы вычислить эти два других числа, используется точно такой же процесс, затем этот процесс повторяется снова и снова.
Но, к счастью, у нас есть компьютеры! Они отлично справляются с такими задачами и не устают от монотонных однообразных вычислений, повторяемых многократно. Рассмотрим это определение, преобразованное в исходный код C++, показанный в листинге 11.1.
Листинг 11.1. Рекурсивное вычисление Фибоначчи #include "iostream.h" long fib(long n) < if (n <= 2) return 1; else return fib (n-l) + fib (n-2); } int main() < long N; cout « "Which Fibonacci number do you want?" « endl; cin » N; cout « "Fibonacci (" « N « ") is " « fib(N) « "\n\n"; return 0; )
Часть III
Ниже показан пример работы программы, в котором текст, вводимый пользователем, выделен полужирным курсивным шрифтом:
Which Fibonacci number do you want? 14 Fibonacci (14) is 377 Which Fibonacci number do you want? 40 Fibonacci (40) is 102334155
ПРИМЕЧАНИЕ
В программе, показанной в листинге 11.1, не рекомендуется вводить номера больше 40, потому что время обработки возрастает экспоненциально. Вычисление числа Фибоначчи с номером 40 на компьютере автора требует 10 секунд. Вычисление числа Фибоначчи номер 41 выполняется почти вдвое дольше, и с каждым последующим увеличением номера на единицу время вычисления почти удваивается. ?
Определение функции ПЬ() очень похоже на математическое определение этой операции. Кого-то может насторожить то, что функция вызывает саму себя. Как она узнает, что возвращать? Компьютер решает эту задачу, создавая новую копию параметра п для каждого вызова функции.
Вместо функции, вызывающей саму себя, просто представьте себе, что вам нужно написать функцию, которая принимает один параметр, назовем его п. Если п меньше или равняется 2, то функция возвращает 1. Если п больше чем 2, то функция возвращает сумму sqrt(n-l)+sqrt(n-2), где sqrt() — функция извлечения квадратного корня. Это не вызовет никаких проблем, вам не нужно беспокоиться о том, что происходит внутри функции sqrt. Это просто функция из библиотеки math, и вы верите, что она правильно выполняет свою задачу.
Написание рекурсивной функции выполняется точно так же. Если требуется выполнить рекурсивный вызов, забудьте об этом'. Не беспокойтесь о том, что вы выполняете вызов той же самой функции, над которой работаете и написание которой еще не завершено. Просто поверьте в то, что эта функция выполнит свою задачу. Когда вы напишете и отладите эту функцию, ваше доверие будет вознаграждено.
Остановка рекурсии
Это одно из главных требований для написания рекурсивной функции. В какой-то момент следует остановить рекурсию, ибо в противном случае она приведет к тому же эффекту, что и бесконечный цикл, по крайней мере, до переполнения стека. (Стек — это специальная область памяти, в которой компьютер хранит свою позицию на каждом уровне рекурсии. Стек будет рассмотрен более подробно далее в этой главе.) Чтобы гарантировать остановку рекурсии, необходимо выполнить два требования. Во-первых, в функции должна быть часть, которая завершает ее без каких-либо рекурсивных вызовов. Условия, при которых выполняется эта часть функции, называются условиями завершения. Второе требование, необходимое для остановки рекурсии, состоит в том, что в каждом последующем рекурсивном вызове программа некоторым образом приближается к условиям завершения. Если есть какой-либо способ прекращения рекурсивных вызовов и каждый последующий рекурсивный вызов приближает вас к условиям завершения, то рекурсия не сможет продолжаться бесконечно.
В примере с числами Фибоначчи, когда параметр меньше или равен 2, функция просто возвращает 1 без рекурсивных вызовов. Таким образом, (п <= 2) — это условие завершения. Если п больше 2, то при каждом последующем рекурсивном вызове используется параметр, который меньше текущего значения п. Другими словами, каждый вызов приближает нас к условию завершения. Поскольку два описанных выше требования выполняются, можно быть уверенным в том, что рекурсия Фибоначчи в конце концов завершится.
РИСУНОК 11.1. Диаграмма классов файлов и каталогов.РИСУНОК 11.1. Диаграмма классов файлов и каталогов.Рекурсивные структуры
РИСУНОК 11.1. Диаграмма классов файлов и каталогов.
РИСУНОК 11.1. Диаграмма классов файлов и каталогов.
Рекурсивная структура — это такая структура, которая указывает на подобную себе структуру. Хорошим примером служит иерархия каталогов на компьютере: каждый каталог содержит список файлов, и одним из частных случаев файла является каталог. Диаграмма классов этого отношения показана на рис. 11.1.
Это запись на универсальном языке моделирования (Universal Modeling Language) для двух классов: File и Directory. Стрелка показывает, что Directory является дочерним классом File, отношением это есть. Стрелка с ромбом показывает отношение один-ко-многим между Directory и File (ромб означает агрегирование или принадлежность). Другими словами, Directory владеет нулем или
Рекурсия и рекурсивные структуры данных Глава 11
более файлов, и каждый File принадлежит ровно одному Directory. Поскольку Directory это есть File, каталоги могут также владеть другими каталогами. Это та рекурсивная природа, которая позволяет создать иерархию каталогов на основе двух очень простых классов.
В листинге 11.2 показана реализация этих двух классов на языке C++. Этот листинг еще не является законченной программой. Сначала рассмотрим минимум, необходимый для отображения этих классов, а затем построим законченную программу на основе этих классов.
Листинг 11.2, Классы файлов и каталогов ♦include <iostream> ♦include <list> ♦include <string> using namespace std; class File ( public:
File(const string 6 name) : m_name(name) () -File() (} const string 6 getName() const ( return m_name; } void setName(const string 6 name) ( m_name = name; } private: string m_name; ); class Directory : public File ( public: Directory(const string & name) : File(name) (} -Directory () ; void AddFile(File * fp) (m_FileList.push_back(fp);} private: list<File *> m_FileList; typedef list<File *>::iterator Filelter;
);
Directory::-Directory() ( while (!m_FileList.empty()) ( delete m_FileList. front () ; m_FileList.pop_front(); } }
По первым четырем строкам можно определить, что используется стандартная библиотека шаблонов — iostream, list и string.
Далее объявляется класс File. Его единственным свойством является его имя, которое объявлено как строка.
Класс Directory публично наследуется от File в отношении это есть. Он отвечает за поддержку списка файлов. Настоящий каталог должен, конечно, содержать множество методов для вставки, удаления, поиска и других операций. Для краткости ограничимся только одной операцией Add(), которая выполняется для добавления новых файлов в конец списка.
Поскольку Directory владеет списком файлов, он должен удалять их после завершения своего использования. Это выполняется явно в деструкторе Directory.
Теперь можно написать небольшой фрагмент программного кода, связывающего воедино всю иерархию этих объектов с каталогами, содержащими множество файлов и других каталогов. Можно прочитать иерархию файлов вашего жесткого диска и поместить ее в эти структуры, составив схему иерархии каталогов во взаимосвязанном множестве объектов File и Directory.
Часть III
Обход рекурсивной структуры с помощью рекурсивной функции
В этом разделе мы продолжим пример Directory и File и напишем рекурсивную функцию, которая обходит все дерево каталогов и отображает имя каждого файла. Мы также поговорим о передаче информации через уровни рекурсии.
Иногда при работе с рекурсивными функциями нужно узнать, на каком уровне рекурсии происходит выполнение. Эта информация может потребоваться по нескольким причинам. Например, условие завершения может заключаться в остановке рекурсии на десятом уровне глубины. Возможно, при отображении информации на экране нужно выделить каждый уровень рекурсии с помощью дополнительного символа табуляции. Эту информацию можно легко получить, передавая ее как параметр функции и уменьшая его на единицу на каждом следующем уровне. В примере Directory и File мы просто печатаем уровень в каждой строке.
При работе с рекурсией также часто возникает вопрос накопления некоторой информации при прохождении через разные уровни рекурсии. Например, при написании функции оценки ходов в шахматах может потребоваться отследить последовательность выполненных ходов. В иерархии каталог/файл мы получаем полный путь к файлу, объединяя имена каталогов при рекурсивном проходе через иерархию каталогов. В листинге 11.3 показано, как это сделать.
Листинг 11.3. Рекурсивный обход дерева каталогов
♦include <iostream> ♦include <list>
♦include <string> using namespace std;
class File <
public:
File (const string 6 name) : m_name(name) {)
-Filed О
const string 6 getNameO const ( return m_name; }
void setName(const string 6 name) { m_name = name; }
virtual void Display(ostream 6 os,
int level = 1,
const string 6 prefix = ""); private:
string m_name;
};
class Directory : public File (
public:
Directory(const string 6 name) : File(name) (}
-Directory () ;
virtual void Display(ostream 6 os, int level = 1, const string 6 prefix = "") ;
void AddFile(File * fp) (m_FileList.push_back(fp);) private:
list<File *> m_FileList;
typedef list<File *>::iterator Filelter;
);
// Чтобы отобразить File, применяется метод Display
void File::Display(ostream & os, int level, const string & prefix) <
os « level « "." « prefix « m_name « endl ;
}
// Чтобы отобразить Directory, применяется метод Display,
// использующий рекурсивный вызов, для всех составных File
Глава 11
void Directory::Display(ostream 6 os, int level, const string 6 prefix) { File::Display(os, level, prefix); string newPrefix = prefix + getName() + ":"; for (Filelter iter = m_FileList.begin();
iter •= m__FileList.end() ; iter++)
(♦iter)->Display(os, level + 1, newPrefix); )
Directory: :-Directory() { while (!m_FileList.empty()) ( delete m_FileList.front(); m Filelist.pop_front(); } “ )
int main() (
Directory * dir = new Directory("TheDir”); dir->AddFile(new File("File 1")); dir->AddFile(new File("File 2"));
Directory *subdir = new Directory("SubDir 1") ; dir->AddFile(subdir); subdir->AddFile(new File("Sub File 1")); subdir->AddFile(new File("Sub File 2"));
Directory *subdir2 = new Directory("SubDir 2") ; dir->AddFile(subdir2); subdir2->AddFile(new File("Sub File 4")); subdir2->AddFile(new File("Sub File 5")); subdir2->AddFile(new File("Sub File 6"));
Directory *subsubdir = new Directory("SubSubDir 1") ; subdir->AddFile(subsubdir); subsubdir->AddFile(new File("Sub Sub File 1")); subsubdir->AddFile(new File("Sub Sub File 2")); subsubdir->AddFile(new File("Sub Sub File 3"));
subdir->AddFile(new File("Sub File 3"));
dir->AddFile(new File("File 3")); dir->AddFile(new File("File 4"));
dir->Display(cout); delete dir; return 0;
)
Результат работы программы из листинга 11.3:
1.
TheDir
2.
TheDir:File 1
2.
TheDir:File 2
2.
TheDir:SubDir 1
3.
TheDir:SubDir l:Sub File 1
3.
TheDir:SubDir l:Sub File 2
3.
TheDir:SubDir 1:SubSubDir 1
4.
TheDir:SubDir 1:SubSubDir 1:Sub Sub File 1
4.
TheDir:SubDir 1: SubSubDir l:Sub Sub File 2
4.
TheDir:SubDir 1:SubSubDir l:Sub Sub File 3
3.
TheDir:SubDir l:Sub File 3
2.
TheDir:SubDir 2
3.
TheDir:SubDir 2:Sub File 4
3.
TheDir:SubDir 2:Sub File 5
Часть III
TheDir:SubDir 2:Sub File 6
TheDir:File 3
TheDir-.File 4
Здесь к двум классам, показанным в начале этой главы, добавлен метод Display(). Поскольку это виртуальный метод, можно проходить по списку указателей File и вызывать Display() для каждого из них, зная, что будет вызван правильный метод Display(). Функция Display() в этом примере использует три параметра: выходной поток, уровень рекурсии и префикс имени файла. Префикс — это накопленное множество имен каталогов, через которые мы прошли перед вызовом данной функции. Только не думайте об этих именах рекурсивно. Просто предположите, что функция, которую вы пишете, получает правильное значение в своем параметре и передает правильное значение в следующем вызове функции.
В функции File::Display() необходимо просто отобразить в выходном потоке номер рекурсии, префикс и имя файла.
Функция Directory::Display() более сложная. Чтобы отобразить каталог, сначала его следует отобразить как файл, а затем вызвать Display() для всех файлов, содержащихся в этом каталоге. Таким образом, для файлов, которые в действительности являются вложенными каталогами, эта функция вызывает сама себя. Но запомните первый принцип написания рекурсивных функций: не думайте при выполнении этой задачи о нескольких уровнях сразу; это простая функция Display(), которая выполняет определенную операцию для всех вложенных файлов, и следует поверить в то, что эта операция работает.
Посмотрите на однострочное тело цикла for в Directory::Display(). (Здесь выражение (*iter) возвращает текущий элемент списка в соответствии с текущей позицией счетчика. Это выражение возвращает File *, потому что список определен как list<File *>.) В цикле for выполняется шаг рекурсии. Первый параметр достаточно простой: в нем просто передается поток ostream, в который следует направлять вывод. Во втором параметре передается уровень рекурсии, который мы уменьшаем на единицу при переходе на следующий уровень. При вызове рекурсивной функции для следующего уровня она снова уменьшит значение level, но нам не нужно об этом беспокоиться. Сейчас достаточно знать только то, что следующий уровень рекурсии будет на единицу больше, чем текущий уровень.
Последний параметр — это префикс. Мы знаем, что в этом параметре передается объединенный список имен каталогов, которые мы прошли до текущей позиции. Не думайте о том, как был построен этот список, просто поверьте в то, что он правильный. Чтобы передать правильный префикс во все вложенные файлы, следует построить новый префикс, объединив текущий префикс с текущим именем. Таким образом, выполняется корректное накопление префиксов: каждый уровень получает правильный префикс и передает дальше также правильный префикс.
В этом рассуждении недостает только одного шага: необходимо обеспечить передачу правильных значений на самом первом уровне. С этой целью мы определяем для параметров функции значения по умолчанию, соответствующие правильным значениям для первого уровня. В данном случае это значения level = 1 и prefix = "", как можно увидеть в объявлении функции Display() для обоих классов, File и Directory.
Последняя проверка перед запуском этой программы. Выполнили ли мы условия завершения рекурсии? Есть ли в рекурсивной функции путь, который не приводит к дальнейшим рекурсивным вызовам? Это не совсем очевидно, потому что при поверхностном рассмотрении функции Directory::Display() сложно заметить такой путь. Однако в действительности есть два способа, позволяющих обойти рекурсивный вызов. Во- первых, если каталог не содержит ни одного файла, то тело цикла for будет пропущено, потому что условие в цикле for сразу же окажется ложным. Во-вторых, если все вложенные файлы являются именно файлами и ни один из них не является каталогом, то рекурсия также завершится.
Второе требование завершения рекурсии заключается в постоянном приближении к условию завершения. Мы допускаем, что иерархия каталогов обладает конечной глубиной, и мы можем гарантировать, что не существует каталогов, прямо или косвенно содержащих самих себя. Учитывая эти два условия, можно сказать, что на каждом этапе рекурсии мы приближаемся к каталогу, который содержит только файлы и не содержит других каталогов.
Наконец, мы готовы построить маленькую структуру каталогов в функции main() и вызвать DisplayO на верхнем уровне. Результат работы программы показан в листинге сразу после самой программы.
Чтобы действительно понять, что произошло, нужно приобрести некоторые знания, но уже сейчас можно сказать, что практический результат состоит в том, что каждый каталог последовательно отобразил все свои файлы. Если один из этих файлов являлся каталогом, то процесс для его родителя приостанавливается, пока этот каталог не закончит отображение всех своих дочерних элементов. Как показано в листинге, уровень рекурсии возрастает до 4, а затем обратно возвращается к 3 и 2. Нигде в исходном коде программы уровень не декрементируется. Уменьшение значения level означает, что один из уровней рекурсии завер-
Рекурсия и рекурсивные структуры данных Глава 11 шился и выполнился оператор return(). При возврате функции вариант level с увеличенным значением просто исчезает. Возможно, будет полезным проследить за выполнением этого кода с помощью отладчика, чтобы увидеть, как вызывается каждая функция и как восстанавливается вызывающая функция после возврата вызванной функции.
Цикл и хвостовая рекурсия
Вообще, любую рекурсивную функцию можно переписать в форме итеративной операции. В одних случаях это довольно легко, но в других случаях такое преобразование бывает очень сложным. Когда самым последним оператором в рекурсивной функции является рекурсивный вызов, это называется хвостовой рекурсией. Хвостовую рекурсию всегда очень легко заменить циклом — компилятор часто так делает, даже не информируя вас об этом! В этом разделе мы рассмотрим рекурсивное представление связанного списка и увидим, как такую рекурсию можно легко заменить циклом.
Связанный список обычно представляется в виде набора узлов Node, где каждый узел содержит один элемент и указатель на следующий узел — рекурсивное представление связанного списка. Вместо этого представьте себе, что связанный список состоит из одного узла Node, который содержит один элемент и указатель на другой связанный список. (Читатели, знакомые с языком LISP и его функциями car/cdr, смогут найти определенную аналогию.)
В таком рекурсивном представлении связанного списка предполагается, что каждый список содержит другой список. Хотя в первой модели каждый узел указывает на другой узел, такая структура только на первый взгляд кажется рекурсивной. Однако это не так, потому что узлы не содержат друг друга, они просто указывают друг на друга.
Другое представление связанного списка означает, что не нужно создавать отдельный класс, представляющий весь список. Каждый узел представляет целый список, состоящих из данных этого узла и данных, содержащихся в подсписке этого узла. Ни одна из операций над списком не включает никаких циклов или прохождений по всему списку, потому что мы считаем, что список состоит только из двух объектов: узла, с которым мы работаем, и подсписка этого узла. Единственная разновидность в этом процессе проявляется при определении, содержит ли узел подсписок или просто NULL.
Для упрощения этого примера в листинге 11.4 создается связанный список, для которого каждый узел содержит обычную строку.
Листинг 11.4. Упрощение программного кода для связанного списка
♦include <iostream> ♦include <list> ♦include <string> using namespace std; class LLNode ( public: LLNode (const string 6 data = "", LLNode * subList = NULL) : myData(data), mySubList(subList) (} -LLNode() (delete mySubList;} const string 6 GetFirstData() const (return myData;) void Sortedlnsert(const string 6 data); bool isInList(const string 6 data) const; int Count() const; void Display(ostream 6 os) const; void ReverseDisplay(ostream 6 os) const; private: string myData; LLNode * mySubList; 1; void LLNode::Sortedlnsert(const string 6 data) ( if (NULL == mySubList || data <= mySubList->GetFirstData()) mySubList = new LLNode(data, mySubList); else mySubList->Sorted!nsert(data);
Часть III
}
bool LLNode::is!nList(const string 6 data) const ( if (data == myData) return true ; else if (NULL = mySubList) return false; else return mySubList->isInList(data);
)
int LLNode::Count() const
if (NULL == mySubList) return 0;
else return 1 + mySubList->Count();
)
void LLNode::Display (ostream & os) const
if ("" ! = myData) os « myData « endl ; if (NULL != mySubList) mySubList->Display(os);
1
void LLNode::ReverseDisplay(ostream 6 os) const ( if (NULL ! = mySubList) mySubList->ReverseDisplay(os); if ("" ! = myData) os « myData « endl;
}
typedef LLNode SortedStringList; int main()
SortedStringList theList; theList.Sortedlnsert("Fred"); theList.Sortedlnsert("Barney"); theList.Sortedlnsert("Wilma"); theList.Sortedlnsert("Betty”); cout « "Display! Count is " « theList.Count() « endl; theList.Display(cout);
cout « "\nReverse Display! Count is " « theList.Count() « endl; theList.ReverseDisplay(cout); return 0;
)
Результат работы программы, показанной в листинге 11.4:
Display! Count is 4 Barney Betty Fred Wilma
Reverse Display! Count is 4 Wilma Fred Betty Barney
Глава 11
Рассмотрим функцию Sortedlnsert(). При написании этой функции мы вообще не хотели думать о рекурсии. Эта функция должна принимать строку и вставлять ее в некоторой позиции списка, представленного в Node. Поскольку мы умышленно забываем о рекурсии, то должны принять только одно решение для вставки этих данных: они будут находиться либо между этим узлом и его подсписком, либо где-то в глубине этого подсписка, и мы не заботимся о том, где именно. Первая строка этой функции говорит: "Если Node не содержит подсписка или если данные должны находиться перед подсписком этого узла, то необходимо вставить эти данные непосредственно после Node". Если данные не должны находиться перед подсписком, то их следует вставить где-то внутри подсписка. На этом этапе мы просто возлагаем эту задачу на подсписок и предполагаем, что подсписок сделает все правильно.
Теперь, когда эта функция написана, следует немного подумать о рекурсии. Во-первых, выполняется ли условие наличия в функции пути, не содержащего рекурсивных вызовов? Да, если имеет место тестирование. Во-вторых, всегда ли мы приближаемся к этому условию завершения? Да, потому что известно, что мы всегда достигнем узла, не содержащего подсписка. Наконец, правильно ли начинается рекурсия9 Это проблематичный вопрос, если следует вставить элемент перед самым первым узлом. В связи с этим предположим, что первый узел в каждом полном списке является пустым узлом, не содержащим никаких данных. Благодаря этому можно исключить ситуацию, когда новые данные нужно вставить перед первым узлом. Когда клиент хочет создать один из таких связанных списков, он создает отдельный специальный узел без данных. Обратите внимание на то, что при вставке нового узла создается полностью новый подсписок. Новый узел содержит данные и старый подсписок, благодаря чему он становится полноценным списком, который может использоваться в качестве подсписка.
Теперь рассмотрим две функции Display() и обратим внимание на простое отличие между ними. Первые проверки предназначены для обхода ложного узла, который мы добавили для упрощения процесса вставки. Изучите эти функции и воспринимайте их не как отображение данных списка, а как отображение данных Node и указание подсписку Node отобразить самого себя.
Хвостовая рекурсия
В листинге 11.4 содержится хороший пример хвостовой рекурсии в функции LLNode::Display() (но нс в функции ReverseDisplayO). В этой функции рекурсивный вызов является самым последним оператором перед возвратом функции. Это пример хвостовой рекурсии. Хвостовая рекурсия важна для разработчиков компиляторов, потому что любую хвостовую рекурсию можно легко заменить циклом. Эта замена приводи! к заметной оптимизации, так как экономится время для помещения аргументов в стек и выполнения вызова функции. Компиляторы могут уверенно выполнить такую оптимизацию, потому что функция уже не будет использовать свои переменные после выполнения рекурсивного вызова. Нет необходимости в создании новых копий всех локальных переменных, если известно, что они уже не потребуются.
Ниже показан эквивалент C++ для программного кода, сгенерированного оптимизирующим компилятором для функции LLNode::Display():
void LLNode::Display(ostream 6 os) const (
const LLNode * p = this;
TailRecursionLabell:
if ("" •= p->myData)
os « p->myData « endl;
if (NULL •= p->mySubList) <
p = p->mySubList; goto TailRecursionLabell;
}
)
ПРИМЕЧАНИЕ
He присылайте автору письма с замечаниями об операторе goto. Первоначально автор использовал здесь цикл do. .while с оператором break, но показанный пример намного ближе к тому, что в действительности создает компилятор. Откомпилированная версия любого существенного фрагмента программного кода содержит сотни операторов goto.
Часть III
Когда вам следует думать о хвостовой рекурсии? Вероятно, никогда, если только вы не работаете в компании, которая создает компиляторы. При составлении функции Count() автор намеренно написал ее так, чтобы компилятор смог ее оптимизировать. Однако если вы случайно бросите фразу хвостовая рекурсия в разговоре на работе, то вы будете выглядеть знатоком.
Непрямая рекурсия
Когда функция не вызывает саму себя, а вызывает другую функцию, которая затем может вызвать первую функцию, это называется непрямой рекурсией. В примере File/Directory в начале этой главы можно было создать метод DisplayChildren() для класса Directory. Затем можно было изменить метод Directory::Display() и вызывать в нем File::Display() и Display Children().
В листинге 11.5 показано новое объявление класса и два его метода. Как видно, метод Display() не вызывает сам себя, но он вызывает DisplayChildren(). Метод DisplayChildren() не вызывает сам себя, но вызывает Display(). Это пример непрямой рекурсии. Заметьте, это не полный листинг, в нем показаны только фрагменты, интересующие нас в данный момент.
Листинг 115. Классы File и Directory, использующие непрямую рекурсию
class Directory : public File <
public:
Directory(const string 8 name) : File(name) {) 'Directory () ;
virtual void Display(ostream 6 os,
int level = 1,
const string 6 prefix = "");
void Displaychildren(ostream 6 os, int level, const string 8 prefix);
void AddFile(File * fp) (m_FileList.push_back(fp);) private:
list<File *> m_FileList;
typedef list<File *>::iterator Filelter; };
// Чтобы отобразить Directory и все составляющие File, // выполняется рекурсивный вызов
void Directory::Display(ostream 6 os, int level, const string 6 prefix) {
File::Display(os, level, prefix);
string newPrefix = prefix + getName() + ":"; Displaychildren(os, level, newPrefix);
)
void Directory: -.DisplayChildren (ostream 8 os,
int level, const string 6 prefix) { for (Filelter iter = m_FileList.begin() ; iter != m_FileList.end();
iter++)
(*iter)->Display(os, level + 1, prefix); )
Рекурсия и стек
При рассмотрении функции Фибоначчи мы отметили, что компьютер без проблем отслеживает текущий уровень рекурсии и создает новые копии всех переменных для каждого рекурсивного уровня. Для этого компьютер использует стек. Стек — это блок памяти, выделенной компилятором для хранения функции, когда она вызывает другую функцию. Стек можно сравнить со стопкой тарелок, в которой можно добавить тарелку только на вершину стопки и можно снять тарелку только с вершины стопки. Нельзя вставить или удалить что-либо с середины такой стопки или стека.
Глава 11
При выполнении вызова функции компьютер помещает в стек (на его вершину, конечно) четыре перечисленных ниже элемента. Заметьте, этот процесс справедлив для вызовов всех функций, а не только для рекурсивных:
(в исходной функций) Параметры, передаваемые в функцию.
(при переходе к другой функции) Положение в программном коде для возврата после завершения вызываемой функции.
(в вызываемой функции) Ваш "базовый указатель", который можно будет восстановить после завершения функции. После этого функция устанавливает базовый указатель в текущее положение в стеке для своего собственного использования. Базовый указатель используется для нахождения параметров, переданных в функцию, а также локальных переменных, для которых функция будет выделять память в стеке.
(в вызываемой функции) Пространство для локальных переменных, необходимых функции.
Естественно, ваша функция при своем вызове выполняет точно такие же шаги. Каждый раз, когда функция обращается к одному из переданных ей параметров или к одной из своих локальных переменных, она получает их значения из памяти по адресу, определенному базовым указателем, плюс или минус некоторое известное значение. При завершении функции эти шаги выполняются в обратном порядке: сначала функция освобождает память для локальных переменных, затем восстанавливает базовый указатель вызвавшей функции и возвращается к сохраненному адресу кода (одновременно выталкивая этот адрес из стека). Наконец, функция освобождает память для переданных параметров.
Когда функция вызывает сама себя, этот процесс не изменяется. Фактически компилятор даже не знает ни о каком отличии. Но мы видим, что этот процесс приводит к нужному эффекту. Каждый раз, когда рекурсивная функция вызывает саму себя, в стеке сохраняются новые копии параметров, адрес возврата, если мы находимся в середине вычисления выражения (как в функции Фибоначчи), и новая функция выделяет свое собственное пространство для всех своих локальных переменных. Например, в функции Directory::Display() память для переменной newPrefix выделяется заново при каждом вызове функции. При этом новые экземпляры этой переменной не влияют на другие экземпляры, которые остаются доступными в предыдущих вызовах рекурсивной функции.
Теперь можно представить эффект бесконечной рекурсии, т.е. рекурсии, для которой разработчик не обеспечил условия завершения. При каждом очередном рекурсивном вызове расходуется некоторое пространство в стеке. Даже если создать рекурсивную функцию без параметров и без локальных переменных, все равно нельзя избежать необходимости хранить базовый указатель и адрес возврата. Если функция не прекратит рекурсию, то используемый объем в стеке будет увеличиваться, пока не израсходуется вся выделенная для него память. Обычно пространство стека примыкает к глобальной памяти, в которую может вторгнуться компьютер в отчаянной попытке сохранить базовый указатель и адрес возврата. В конце концов, компьютер перезапишет какие-либо критичные данные или вызовет ошибку доступа к памяти. В любом случае ваша программа остановится (если не зависнет компьютер). Поскольку это обычно не является желаемым результатом, следует очень тщательно проверять условия завершения.
К аналогичному результату можно прийти и при правильной реализации условия завершения. Выполнение программы или работа компьютера может остановиться, если рекурсия потребует больше стекового пространства, чем выделено. Если на каждом уровне рекурсии необходимо выделять память для больших переменных, то имеет смысл размещать их в куче, а в локальных переменных хранить только указатели. Если вам нужно подсчитать требования к памяти, то запомните, что имеет значение только максимальная глубина рекурсии, а не общее количество вызовов рекурсивной функции. В частности, рассмотрим пример Directory/File с рекурсивной функцией Directory::Display(). Хотя эта функция вызывается для каждого существующего каталога, каталоги SubDir 1 и SubDir 2 никогда не будут храниться в стеке одновременно. Программа сначала полностью обработает каталог SubDir 1, потом освободит используемую память в стеке и только после этого перейдет к каталогу SubDir 2.
Отладка рекурсивных функций
Теперь мы научились забывать о рекурсии при написании рекурсивных функций и структур. К сожалению, при отладке придется снова вспомнить о рекурсии, потому что не так легко установить точку останова внутри одной функции, не указав точку останова внутри вызываемой функции. Нельзя даже выполнить шаг через вызов функции, потому что отладчик незаметно устанавливает временную точку останова при шаге через функцию. Эта точка останова активизируется в последующем рекурсивном вызове и собьет вас с толку.
Часть III
Эту проблему можно обойти, воспользовавшись несколькими механизмами. В одном из этих механизмов используется дополнительный параметр рекурсивной функции, который позволяет всегда знать, где вы находитесь. Можно даже создать условные точки останова (если отладчик их поддерживает), которые активизируются в определенной строке программного кода, когда переменная level принимает какое-то определенное значение.
Другой механизм, позволяющий обойти проблему с указателями, состоит в создании полной копии рекурсивной функции, которая будет вызываться извне. Эту функцию можно безопасно трассировать, потому что она не является по-настоящему рекурсивной. Конечно, при использовании такого подхода необходимо тщательно следить за полным соответствием двух копий функции, а также необходимо избавиться от дополнительной функции после завершения отладки.
Резюме
Рекурсия долгое время служила источником страха и беспокойства для многих разработчиков программного обеспечения. Однако если вы начинаете забывать о рекурсии при написании класса или функции, то разработка становится проще. Несмотря на необходимость проверки условия завершения рекурсии и небольшие хлопоты с отладкой, в целом в рекурсии нет ничего страшного.
Использование методов сортировки
В ЭТОЙ ГЛАВЕ
Анализ производительности алгоритмов
Пузырьковая сортировка
Сортировка вставками
Сортировка выбором
Быстрая сортировка
Сортировка слиянием
Сортировка по методу Шелла
Пирамидальная сортировка
Выбор метода сортировки
Генерирование тестовых данных
Часть III
Сортировка является одной из наиболее важных функций во многих приложениях. В этой главе рассматривается несколько методов, используемых для сортировки данных, а также сравнивается объем работы, необходимый при использовании каждого из этих методов. По мере чтения главы вы поймете, что эффективность определенного метода зависит от нескольких факторов — реализации программного кода и сортируемых данных. Перед выбором конкретного метода сортировки следует протестировать его на различных исходных данных, в частности, на примере данных, которые будут сортироваться на практике.
При сортировке определенного набора данных (записей) используется ключевое значение, содержащееся в каждой записи. Это ключевое значение может быть просто отдельным элементом записи или строится на основе нескольких элементов записи. Однако применительно к процессу сортировки вся запись представляется только своим ключевым значением. Сортировка позволяет в конечном счете переупорядочить записи на основе их ключевых значений. Следовательно, сортировка может выполняться для повышения производительности методов поиска.
Зависимость эффективности сортировки от входных данных станет более понятной при рассмотрении различных реализаций. Для определенной реализации метода сортировки худший и лучший случаи определяются природой входных данных. Например, для некоторых методов сортировки объем выполненной работы для уже отсортированных данных является линейным, но этот же алгоритм может выполнять работу порядка п2 для записей, отсортированных в обратном порядке.
Анализ производительности алгоритмов
В отличие от оценки производительности аппаратного обеспечения, производительность алгоритмов для сортировки и поиска не измеряется по времени выполнения, потому что эти алгоритмы зависят от данных. Такие управляемые данными алгоритмы оцениваются в отношении размера входных данных с использованием символа О. Эта запись называется О-записью. В зависимости от типа алгоритма выполненная работа (и, следовательно, производительность алгоритма) определяется различными характеристиками. Например, для таких алгоритмов сортировки, как пузырьковая сортировка, очень важным показателем является количество сравнений, необходимых для перемещения элементов. При сортировке набора из N элементов алгоритм выполняет множество сравнений и меняет элементы местами, если они расположены не в том порядке. Производительность пузырьковой сортировки определяется как О((п2-п)/2). При сортировке списка, состоящего из N элементов, программа обходит список по одному разу для каждого элемента списка; после каждого обхода один из элементов помещается на свое правильное место и не учитывается при последующих обходах.
Поиск алгоритма с лучшей производительностью равнозначен поиску алгоритма с лучшей оценкой Big-O. Алгоритмы с производительностью, худшей, чем п2, считаются непригодными к использованию; следует стремиться к О(п). Необходимо понять, что степень п (размер входных данных) важнее, чем константа перед п. Другими словами, можно сказать, что О(п) не намного хуже, чем О(3п), но намного хуже, чем О(п2). В этом можно легко убедиться, подставив определенные значения п. Компьютер, который выполняет одну операцию за 1 нс, может потребовать 90 лет для выполнения алгоритма с оценкой О(п3), но этот же компьютер может потребовать только пару часов для выполнения алгоритма с оценкой 0(15000000п). В связи с этим в О-записи константа не учитывается, а учитывается только степень. Следовательно, производительность О(п) сравнима с производительностью О(4п).
Сравнение среднего, худшего и лучшего случаев
Следует быть очень осторожным, чтобы не попасть в ловушку О-записи. Имейте в виду, что производительность зависит от самих данных, а не только от их размера. Это означает, что алгоритм может иметь один порядок на одном типе входных данных и другой порядок — на другом типе входных данных. Всегда следует оценивать алгоритм, рассматривая его поведение в среднем — в сценарии лучшего и худшего случаев. Эти различные сценарии основаны на различных типах данных, которые заставляют выполнять меньше или больше работы для достижения желаемого результата. Не удивляйтесь, если встретите алгоритм, который выполняется очень хорошо в среднем случае, но настолько плохо работает в худшем случае, что приходится выбрать другой алгоритм, который хуже выполняется в среднем, но и в худшем случае работает не так плохо. В табл. 12.1 показано сравнение двух алгоритмов для различных значений п.
Таблица 12.1. Сравнение NLOGN и N2
Глава 12
Количество элементов (п)
Средний случай: nlogn
Худший случай: п2
8
24
64
64
384
4096
2048
22528
4194304
ПРИМЕЧАНИЕ
Производительность лучшего случая обычно не анализируется, потому что она не таит потенциальной опасности для алгоритма. Однако все же следует рассмотреть сценарий лучшего случая, потому что лучший случай может помочь проникнуть в суть алгоритма, чтобы извлечь из него максимальные преимущества. Например, если в методе пузырьковой сортировки входные данные уже отсортированы (лучший случай), то сортировка выполняется с производитель- . костью О(п); однако в среднем пузырьковая сортировка выполняется с производительностью О(п2). Вполне возможно, что тип данных, которые будут обрабатываться алгоритмом, известен заранее. В таком случае можно выбрать алгоритм, для которого эти данные представят лучший случай.
Оптимизация алгоритма — это попытка сократить время, необходимое для выполнения алгоритмом наиболее частых операций. Для алгоритмов сортировки оптимизация дает возможность сократить время, необходимое для выполнения сравнения и перестановки. Этого можно добиться, выполняя такие операции в памяти, а не на жестком диске. Легко увидеть, что реализация кода играет очень важную роль в оптимизации алгоритма. Однако общее поведение алгоритма не изменяется с реализацией. Другими словами, независимо то того, как реализован алгоритм пузырьковой сортировки, его производительность все равно составляет О(п2).
Запись Big-О позволяет сравнить алгоритмы, предположив, что все аспекты реализованы одинаково. Необходимо понимать, что Big-О имеет значение только для достаточно больших значений п, потому что для малых значений и константа может оказать существенное влияние на эффективность метода. Например, если А1(п)=20п, а А2(п)=3000п, то оба алгоритма имеют оценку О(п), но в действительности А] в 150 раз быстрее, чем А2. Следовательно, можно заключить, что при прочих равных условиях алгоритм О(п) всегда превосходит алгоритм О(п2).
Стабильность сортировки
Сортировка считается стабильной (стационарной), если она сохраняет любой заранее установленный порядок сортируемых записей. Предположим, что нужно отсортировать всех сотрудников компании по их коду department code, но сотрудники уже отсортированы по номеру employee_no. Стабильная сортировка по department_code должна сохранить существующий порядок по employee_no. Стабильность сортировки обеспечивается простыми методами, но при рассмотрении более совершенных методов сортировки вы увидите, что поддержка стабильности — это очень сложная задача. Например, можно превратить нестабильную сортировку в стабильную, используя составной ключ, состоящий из полей employee_no и department_code. Однако такой составной ключ может быть нежелательным при некоторых обстоятельствах.
Использование дополнительных способов
хранения во время сортировки
Еще одним фактором, существенно влияющим на выбор метода сортировки, является требование к памяти. Для некоторых методов сортировки требуется незначительный объем временной памяти, в то время как другие методы требуют объема, вдвое большего, чем размер исходных данных. Рекурсивные методы используют память стека; необходимо так аккуратно реализовать программный код, чтобы минимизировать использование дополнительного пространства.
Кроме требований к временной памяти, размер исходных данных иногда может обусловить выбор метода внешней сортировки. Внутренние методы используются в том случае, если все входные данные можно загрузит!) в память одновременно. Если же исходные данные настолько объемные, что не вмещаются в основной памяти, то для части этих данных приходиться использовать среду массового хранения. При внешней сортировке входные данные разбиваются на порции, которые можно загрузить в основную память. Эти порции сортируются отдельно, а затем сливаются вместе. Подробное рассмотрение внешней сортировки выходит за рамки этой главы. Однако мы рассмотрим сортировку слиянием, которая подскажет идеи об использовании внешних сортировок.
Часть III
Пузырьковая сортировка
Пузырьковая сортировка — это один из простейших методов сортировки. Этот метод хорошо работает на простых структурах данных или если данные, которые нужно отсортировать, уже в некоторой степени отсортированы. Пузырьковая сортировка очень неэффективна в общем случае. В алгоритме пузырьковой сортировки выполняются последовательные перемещения через сортируемые записи. Во время каждого перемещения алгоритм сравнивает ключи элементов данных и меняет эти элементы местами, если они расположены не в желаемом порядке. Такие перемещения выполняются только между соседними элементами структуры данных. В результате только один элемент помещается на свое правильное место после каждого перемещения. Отсортированные элементы не нуждаются в сравнении в последующих перемещениях. Ниже показан псевдокод для пузырьковой сортировки массива, состоящего из п элементов, расположенных в порядке возрастания:
For iteration = 0 to (n-1)
Begin
For I = 0 to (n — 1 — iteration) Begin
If array[i] > array[i+1] then swap array[i] and array[i+1] end
end
Реализация этого алгоритма на языке C++ показана в листинге 12.1.
Листинг 12.1. Реализация пузырьковой сортировки // Программа: bubble_sort.срр // Автор: Megh Thakkar // Цель: Сортировка массива, состоящего из п элементов, расположенных в порядке возрастания, // с использованием метода пузырьковой сортировки tinclude <stdio.h> #include <string.h> #include <stdlib.h> #include <iostream.h> class sort { private: int *X;//Список элементов данных
int n;//Количество элементов в списке
public: sort (int size) ( X = new int[n=size]; } ~sort( ) { delete [ ]X; } void load_list (int input[ ] ) ; void show_list (char ‘title); void bubble_sort( int input[ ]) ;
);
void sort::load_list(int input[ ])
for (int i = 0; i < n; i++) X[i] = input[i];
void sort::show_list( char ‘title) < cout « "\n" « title; for (int i = 0; i < n; i++) cout « " " « X[i] ; cout « "\n"; 1 void sort::bubble_sort( int input! ]) {
int swapped = 1 ; char ‘title;
load_list(input);
Использование методов сортировки Глава 12 show_list("List to be sorted in ascending order using bubble sort"); // Цикл FOR выполняется один раз для каждого элемента массива. // В конце каждой итерации один элемент "всплывает" на свое // правильное положение и не учитывается в последующих итерациях, for ( int i = 0; i < n 66 swapped == 1; i++) { H Если в конце итерации не нужно выполнять никаких перестановок, И это означает, что список уже отсортирован и цикл можно завершить, swapped = 0; for (int j = 0; j < n-(i+l) ; j++) // Если X[j] > X[j+1] , то элементы находятся в неправильном порядке. // Следовательно, их нужно поменять местами. if ( X[j] > X[j+1] ) ( int temp; temp = X[ j] ; X[j] = X[j+1] ; X[j+1] = temp; swapped = 1;
) } show_list("List sorted in ascending order using bubble sort"); }
//main( ) : Тест пузырьковой сортировки void main(void)
(
// Создание нового объекта с помощью метода bubble_sort sort sort_obj(5); static int unsorted_list[] = (54,6,26,73,1); sort_obj.bubble_sort(unsorted_list);
}
Как показано в листинге 12.1, максимальное количество перестановок выполняется, когда список отсортирован в обратном порядке. Другими словами, максимальное количество перестановок необходимо для сортировки списка в порядке возрастания, если он уже отсортирован в порядке убывания.
Анализ пузырьковой сортировки
Пузырьковая сортировка обладает несколькими характеристиками:
После каждой итерации только один элемент данных помещается в свою правильную позицию.
При пузырьковой сортировке сравниваются и переставляются смежные элементы данных.
В каждой итерации внутреннего цикла выполняется не более (n-iteration-1) перестановок.
Худший случай — когда элементы данных отсортированы в обратном порядке.
Лучший случай — когда элементы данных уже отсортированы в правильном порядке.
■ Пузырьковая сортировка легко реализуется.
Сортировка вставками
Сортировка вставками — очень простой метод сортировки, при котором элементы данных используются как ключи для сравнения. Этот алгоритм сначала упорядочивает А[0] и А[1], вставляя А[1] перед А[0], если А[0] > А[1]. Затем оставшиеся элементы данных по очереди вставляются в этот упорядоченный список. После k-й итерации элемент А[к] оказывается в своей правильной позиции и элементы от А[0] до А[к] уже отсортированы. Псевдокод для этого метода выглядит следующим образом:
For done = 0 to n—1 begin
temp = array [done] for I = done -1 to 0 begin
Часть III
while ( array[i] > temp ) { array[i+1] = array[i] I++;
) end array[i+1] = temp end
Реализация алгоритма сортировки вставками показана в листинге 12.2.
Листинг 12.2. Реализация сортировки вставками // Программа: insertion_sort.срр И Автор: Megh Thakkar // Цель: Сортировка массива, состоящего из п элементов, в порядке возрастания // с использованием метода сортировки вставкой #include <stdio.h> #include <string.h> #include <stdlib.h> #include <iostream.h> class sort { private:
int *X;//Список элементов данных
int n;//Количество элементов в списке
int scan_no; public: sort (int size) ( X = new int[n=size]; ) ~sort( ) { delete [ ]X; ) void load_list (int input[ ] ) ; void show_list (char *title); void insertion_sort( int input[ ]);
);
void sort::load_list(int input[ ]) ( for (int i - 0; i < n; i++) { X[i] = input[i];
}
}
void sort::show_list( char *title) { cout « "\n" « title; for (int i = 0; i < n; i++) cout « " " « X[i]; cout « "\n";
)
void sort: : insertion_sort ( int input [ ]) (
char *title;
// Массив S используется для хранения элементов в том виде, в каком они получены, int S[100]; load_list(input);
show_list("Lj.st to be sorted in the ascending order using insertion sort") ;
S[0] = X[0] ;
//На каждой итерации цикла FOR элемент X[i] сравнивается с элементами / /в отсортированном списке S для поиска его положения в массиве S.
for (int i = 1; i < n ; i++) ( int temp = X[i] ; int j = i-1;
while (( S[j] > temp ) 66 ( j >= 0)) <
Глава 12
S[j+1] = S[j);
j-;
}
S[j+1] = temp;
}.
// Метод show_list использует приватный массив X. Следовательно, мы копируем // отсортированный массив S в X, чтобы его можно было напечатать.
for (int m = 0; m < n; m++)
X[m] = S [m] ;
show_list("List sorted in ascending order using insertion sort");
)
void main(void)
(
sort sort_obj(5) ;
static int unsorted_list[] = (54,6,26,73,1);
sort_obj.insertion_sort(unsorted_list);
)
Обратите внимание, в отсортированной части списка создается "дырка". Эта "дырка" образуется при копировании данных в позицию temp и при копировании 8Щ в S[j+1], если S[j] > temp. "Дырка" перемещается в обратном направлении по отсортированному списку элементов до тех пор, пока не найдет правильную позицию для нового элемента. После этого цикл while завершается, и элемент из временной переменной вставляется в свою правильную позицию.
Анализ сортировки вставками
Сортировка вставками обладает следующими характеристиками:
После каждой итерации только один элемент данных помещается в свою правильную позицию.
При сортировке вставками выполняется меньше перестановок, чем в пузырьковой сортировке. Это очевидно, потому что в методе пузырьковой сортировки наибольший элемент всегда "всплывает" к вершине; сортировка вставками перемещает "дырку" через уже отсортированные элементы и в большинстве случаев может сместить только половину отсортированных элементов перед тем, как найти место для новой вставки.
Наихудший случай — когда все элементы данных отсортированы в обратном порядке.
Наилучший случай — когда элементы почти отсортированы в правильном порядке.
Сортировка вставками легко реализуется.
Очень важным вопросом проектирования сортировки вставками является выбор направления просмотра отсортированных записей. Другими словами, просматривать ли отсортированные записи от первой к последней или наоборот? Это решение имеет большое значение; правильный ответ зависит от самих данных, которые нужно отсортировать. Чтобы понять важность направления просмотра, предположим, что список отсортирован в обратном порядке {5, 4, 3, 2, 1}. Если просматривать отсортированные записи в направлении от последней к первой, то можно увидеть, что на каждой итерации следующий элемент для вставки меньше уже отсортированных элементов и сортировка требует меньшего количества сравнений, чем при просмотре от первой к последней записи. С другой стороны, если список почти отсортирован, то просмотр в направлении от первой к последней записи выполнится быстрее и потребует меньшего количества сравнений, чем при просмотре от последней к первой записи.
Сортировка выбором
Алгоритм сортировки выбором основан на использовании элементов в качестве ключей для сравнения, при этом в конце каждого просмотра только один элемент помещается в свою правильную позицию. Этот алгоритм простой, но очень неэффективный, потому что он не учитывает частично или полностью отсортированных списков. Другими словами, если список уже полностью или частично отсортирован, то при сортировке выбором будет выполнено то же количество сравнений, что и в случайном списке, при этом такая ситуация не послужит для повышения производительности (как могло быть при пузырьковой сортировке). В результате для метода сортировки выбором не существует сценария лучшего случая. Для списка из
Обработка данных Часть III
п элементов при сортировке выбором всегда выполняется (п-1) итерация. Ниже показан псевдокод для алгоритма сортировки выбором:
Для j от 0 до (п-1) выполнить следующие шаги:
Для элементов от X[j+1] до Х[п-1] выполнить сравнение по ключам и найти наименьший элемент; назовем этот элемент X[lower],
Поменять местами X[lower] и Х[п-1]. X[lower] теперь находится в своей отсортированной позиции.
После завершения этого цикла список будет отсортирован.
Реализация сортировки выбором на языке C++ показана в листинге 12.3.
Листинг 12.3. Реализация сортировки выбором // Программа: selection_sort.cpp И Автор: Megh Thakkar 11 Цель: Сортировка массива, состоящего из п элементов, в порядке возрастания //с использованием метода сортировки выбором
#include <stdio.h> #include <string.h> #include <stdlib.h> #include <ioatream.h> class sort ( private: int *X;//Список элементов данных
int n;//Количество элементов в списке
public: sort (int size) ( X = new int[n=size]; } ~sort( ) ( delete [ ]X; ) void load_list (int input[ ] ) ; void show_list (char ‘title); void selection_sort( int input( ]);
);
void sort::load_list(int input[ ]) ( for (int i = 0; i < n; i++) X[i] = input[i];
)
void sort::show_list( char ‘title) ( cout « "\n" « title; for (int i = 0; i < n; i++) cout « " " « X[i] ; cout « "\n";
void sort::selection_sort( int input[ ]) ( char ‘title; load_list(input);
show_list("List to be sorted in the ascending order using selection sort");
// Используя цикл FOR, итеративно находим наименьший элемент в списке // и перемещаем его в правильную позицию, for ( int j = 0; j < (n-1) ; j++) {
// На каждой итерации начинаем с элемента, имеющего индекс j, // определяя его как наименьший элемент, int lowest = j; for ( int k = i+l; k < n ; k++)
( if (X[kJ < X(lowestJ) lowest = k;
)
//Когда найден элемент, который еще меньше, чем наименьший известный до сих //пор элемент, переставляем их местами.
Глава 12
int temp; temp = X[j] ; X[j] = X[lowest] ; X [lowest] = temp;
} show_list("List sorted in the ascending order using selection sort") ;
)
//main( ) : Тест сортировки выбором void main(void) ( sort sort_obj (5) ;
static int unsorted_list[] = (54,6,26,73,1); sort_obj.selection_sort(uasorted_list);
Анализ сортировки выбором
В листинге 12.3 можно увидеть, что для каждой итерации внешнего цикла внутренний цикл выполняет не более (n-j) сравнений и внешний цикл выполняется (п-1) раз. Общее количество сравнений для сортировки выбором можно вычислить так:
(п-1) + (п-2) + (п-З) +...+ [п-(п-1)] = п(п-1)/2 = п2/2 - п/2 = О(п2) .
Таким образом, это сценарий худшего случая. Сценарий лучшего случая в действительности требует такого же количества сравнений, потому что сортировка выбором не учитывает частичной сортировки, которая может существовать в исходных данных.
Быстрая сортировка
Быстрая сортировка — это наиболее эффективный алгоритм внутренней сортировки. Его производительность сильно зависит от выбора точки разбиения. При быстрой сортировке используются три стратегии:
Массив разбивается на меньшие подмассивы.
Подмассивы сортируются.
Отсортированные подмассивы объединяются.
Быструю сортировку можно реализовать несколькими способами, но цель каждого подхода заключается в выборе элемента данных и помещении его в правильную позицию (этот элемент называется точкой разбиения), таким образом, чтобы все элементы слева от точки разбиения оказались меньше (или предшествовали) точки разбиения, а все элементы справа от точки разбиения оказались больше (или следовали за) точки разбиения. Выбор точки разбиения и метод, используемый для разбиения массива, оказывают большое влияние на общую производительность реализации. Остановимся на рекурсивной реализации быстрой сортировки. Псевдокод этой реализации можно записать следующим образом:
Выбрать элемент данных и сделать его точкой разбиения таким образом, чтобы он разбивал массив на левый и правый подмассивы, как было описано выше.
Применить быструю сортировку к левому подмассиву.
Применить быструю сортировку к правому подмассиву.
Выбор точки разбиения имеет критическое значение. В эффективном методе разбиения может использоваться следующая стратегия:
Выбрать ключ первого элемента данных как точку разбиения. Другими словами, Pivot = X[first],
Инициализировать два указателя поиска, i и j, чтобы i = first (наименьший индекс подмассива), а j = last (наибольший индекс подмассива).
Используя указатель поиска i, найти, начиная слева, элемент данных, который больше или равен точке разбиения. Это можно выполнить, используя следующий псевдокод:
Пока A[i] <= Pivot и i < last, продолжать увеличение i на 1 иначе прекратить увеличение i
Обработка данных
Часть III
Используя указатель поиска j, найти, начиная справа, элемент данных, который меньше или равен точке разбиения. Это можно выполнить, используя следующий псевдокод:
Пока A[j] >= Pivot и j > last, продолжать уменьшение j на 1 иначе прекратить уменьшение j
Если i < j, то поменять местами A[i] И АШ-
Повторить пункты 2—4, пока не выполнится условие i > j.
Поменять местами точку разбиения и АЦ].
После выполнения п.7 точка разбиения будет расположена в своей правильной позиции. В листинге 12.4 показана реализация этого кода на языке C++.
Листинг 12.4. Реализация быстрой сортировки // Программа: quick_sort.срр // Автор: Megh Thakkar // Цель: Сортировка массива, состоящего из п элементов, в порядке возрастания / /с использованием метода быстрой сортировки
#include <stdio.h> #include <string.h> #include <stdlib.h> #include <iostream.h> class sort { private: int *X;//Список элементов данных
int n;//Количество элементов в списке
public: sort (int size) ( X = new int[n=size] ; ) ~sort( ) { delete [ ]X; } void load_list (int input[ ] ) ; void show_list (char ‘title); void quick_sort( int first, int last); ); void sort::load_list(int input[ ]) for (int i = 0; i < n; i++) X[i] = input[i];
) void sort::show_list( char ‘title) ( cout « "\n" « title; for (int i = 0; i < n; i++) cout « " " « X[i] ; cout « "\n";
) void sort::quick_sort( int first, int last) ( //Переменная temp используется как временное хранилище при перестановке int temp; if (first < last) ( //Принимаем за точку разбиения первый элемент списка int pivot = X[firstJ;
//Переменная i используется для просмотра слева, int i = first; //Переменная j используется для просмотра справа, int j = last; while (i < j) ( // Поиск элемента, который больше или равен выбранной точке // разбиения. Поиск слева, while (X[i] <= pivot 66 i < last)
Глава 12
i += 1;
И Поиск элемента, который меньше или равен выбранной И точке разбиения. Поиск справа, while (X[j] >= pivot 66 j > first) j -= 1; if (i < j) //swap(X[i),X[j]) { temp = X[i] ; X[i] = X[j]; X[j] = temp;
) //swap(X[j],X[first]) temp = X[first] ; X[first] = X[j] ; X[j] = temp;
//Рекурсивное применение быстрой сортировки к двум частям массива quick_sort(first, j—1) ; quick_sort(j+l, last) ;
} )
I/main( ) : Тест быстрой сортировки void main(void) ( sort sort_obj (5) ; static int unsorted_list[] = (54,6,26,73,1); sort_obj.load_list(unsorted_list); sort_obj.show_list("List to be sorted in ascending order using quick sort"); sort_obj.quick_sort(0,4);
sort_obj.show_list("List sorted in ascending order using quick sort"); )
Анализ быстрой сортировки
Быструю сортировку следует рассмотреть одной из первых при выборе метода внутренней сортировки. Этот алгоритм содержит сложную фазу разбиения и простую фазу слияния. В лучшем случае выполняется работа порядка nlo^n; в худшем случае выполненная работа эквивалентна работе при сортировке выбором, т.е. О(п2). Производительность быстрой сортировки сильно зависит от выбора точки разбиения.
Сортировка слиянием
Сортировка слиянием — очень эффективный метод внешней сортировки, когда все сортируемые элементы не вмещаются в доступной памяти и для выполнения сортировки приходится использовать жесткий диск. При сортировке слиянием используется та же стратегия, что и при быстрой сортировке:
Разбить файл на меньшие файлы.
Отсортировать меньшие файлы.
Слить отсортированные файлы.
При быстрой сортировке разбиение является сложным шагом, а слияние — простым; при сортировке слиянием разбиение представляет собой простой шаг, а слияние — более сложный. Существует несколько разновидностей сортировки слиянием, основанных на стратегии, используемой для фаз разбиения и слияния. Мы остановимся на сортировке слиянием, использующей итеративный метод. Этот метод состоит из следующих этапов:
Открыть файл для сортировки (to_sort) в режиме чтения/записи, а также открыть два временных файла для записи.
Фаза разбиения: Скопировать элементы файла to_sort попеременно во временные файлы.
Фаза слияния: Сравнить каждый элемент из двух временных файлов и записать сначала меньший, а затем больший из двух элементов обратно в файл to_sort.
Часть III
Фаза разбиения; Скопировать элементы из файла to_sort попеременно по два элемента во временные файлы.
Фаза слияния; Сравнить каждую группу из двух элементов из временных файлов и записать сначала меньшую из двух групп элементов, а затем — большую обратно в файл to_sort.
Повторить фазы разбиения и слияния с размером групп 21 при I = 2,3,4,—log2n. В результате получим отсортированный файл to_sort.
Процесс сортировки слиянием проиллюстрирован на рис. 12.1.
РИСУНОК 12.1.
Сортировка слиянием.
Реализация сортировки слиянием на языке C++ показана в листинге 12.5.
Листинг 125. Реализация сортировки слиянием
//Программа: merge_sort.срр
//Автор: Megh Thakkar
I/Цель: Сортировка массива с использованием сортировки слиянием
#include <stdio.h>
#include <stdlib.h>
#include <iostream.h>
#define MIN(x,y) ( (x <= у) ? x : у )
enumSTATUS {UNSORTED, SORTED, DATA_AVAILABLE,
END_OF_FILE};
void open_for_split (FILE *sorted_file, FILE *sub_filel,FILE *sub_file2){ rewind (sorted_file) ;
fclose (sub_filel);
fclose (sub_file2) ;
remove ("subfilel.fil");
remove ("subfile2.fil") ;
sub_filel = fopen ("subfilel.fil", "w+");
sub_file2 = fopen ("subfile2.fil", "w+");
}
void open_for_merge(FILE *sorted_file, FILE *sub_filel,FILE *sub_file2){ fclose (sorted_file);
remove ("result.fil");
sorted_file = fopen ("result.fil", "w+");
rewind (sub_filel);
rewind (sub_file2);
}
void close_files(FILE *sorted_file, FILE *sub_filel,FILE *sub_file2)( fclose (sorted_file);
fclose (sub_filel );
fclose (sub_file2 );
remove ("subfilel.fil") ;
Использование методов сортировки Глава 12
remove ("subfile2.fil"); } void Merge_Sort (char *sorted_file_name) { FILE*sorted_file, *sub_filel, *sub_file2;
enum STATUS status = UNSORTED, status_filel, status_file2; int data_read, read_from_filel, read_from_file2, last_considered = 0; intcurr_f ile = 1 ;
sorted_file = fopen (sorted_file_name, "r+"); sub_filel= fopen ("subfilel.fil", "w+");
sub_file2= fopen ("subfile2.fil", "w+");
if (sorted_file == NULL | | sub_filel == NULL | | sub_file2 == NULL) ( cout« "\nSorry. Files cannot be opened\n"; exit (-1) ; } while (status = UNSORTED) { open_for_split( sorted_file, sub_filel, sub_file2); //Разбиение файла на sub_filel и sub_file2. //Оператор if проверяет существующий порядок элементов //и пытается и использовать его для ускорения процесса сортировки.
while (fscanf (sorted_file, "%d", 6data_read) •= EOF) { if (data_read < last_considered) ( if (curr_file == 1) fprintf (sub_file2, "%d ", data_read); else fprintf (sub_filel, "%d ", data_read) ; } else{ if (curr_file == 1) fprintf (sub_filel, "%d ", data_read); else fprintf (sub_file2, "%d ", data_read);
last_considered = data_read; ) open_for_merge( sorted_file, sub_filel, sub_file2); status_filel = DATA_AVAILABLE; status_file2 = DATA_AVAILABLE; if (fscanf (sub_filel, "%d", 6read_from_filel) == EOF) ( status = SORTED; status_filel = END_OF_FILE; } if (fscanf (sub_file2, "%d", 6read_from_file2) = EOF) { status = SORTED; status_file2 = END_OF_FILE; ) last_considered = MIN (read_from_filel, read_from_file2); while (status_filel != END_OF_FILE && status_file2 != END_OF_FILE) ( if (read_from_filel <= read_from_file2 66 read_from_filel >= last_considered) { // Запись значений из sub_filel fprintf (sorted_file, "%d ", read_from_filel); last_considered = read_from_filel; if (fscanf (sub_filel, "%d", 6read_from_filel) == EOF) status_filel = END_OF_FILE; ) else if (read_from_file2 <= read_from_filel 66 read_from_file2 >= last_considered) ( // Запись значений из sub_file2 fprintf (sorted_file, "%d ", read_from_file2);
Часть III
lastconsidered = read_from_file2;
if (fscanf (sub_file2, "%d", 6read_from_file2) == EOF) status_file2 = END_OF_FILE;
} else if (read_from_filel >= last_considered) { // Запись значений из sub_filel fprintf (sorted_file, "%d ", read_from_filel); last_considered = read_from_filel; if (fscanf (sub_filel, ”%d”, &read_from_filel) == EOF) statusfilel = END_OF_FILE;
) else if (read_from_file2 >= last_considered) { // Запись значений из sub file2 fprintf (sorted_file, "%d ", read_from_file2); last_considered = read_from_file2; if (fscanf (sub_file2, "%d", Sread_from_file2) == EOF) status_file2 = END_OF_FILE;
} else last_considered - MIN (read_from_filel, read_from_file2); ) while (status_filel != END_OF_FILE) {
//Теперь можно записать остаток sub_filel fprintf (sorted_file, "%d ", read_from_filel); if (fscanf (sub_filel, "%d", &read_from_filel) == EOF) status_filel = END_OF_FILE;
} while (status_file2 •= END_OF_FILE) ( //Теперь можно записать остаток sub_file2 fprintf (sorted_file, "%d ", read_from_file2); if (fscanf (sub_file2, "%d", 6read_from_file2) == EOF) status_file2 = END_OF_FILE;
} } close_files( sorted_file, sub_filel, sub_file2); ) void main(void) {
cout « "Sorting filename : tosort.fil" « "\n\n\n"; Merge_Sort ("result.fil");
cout « "File has been sorted. Please see filename: result.fil" «"\n\n\n";
Анализ сортировки слиянием
Сортировка слиянием представляет собой пример стратегии "разделяй и властвуй". В этом методе фаза разбиения очень простая: она просто делит список пополам. Фаза слияния более сложная. На каждом просмотре сортировка слиянием проходит весь файл и, таким образом, выполняет О(п) сравнений. На первом просмотре при сортировке слиянием рассматривается только один список. На втором просмотре этот алгоритм разбивает список на две половины, а затем сортирует и сливает их. На k-м просмотре алгоритм делит список на подсписки из 2к-1 элементов. Легко увидеть, что, поскольку список разбивается пополам, мы получаем не более log2n подсписков для списка из п элементов. В худшем случае сортировка слиянием имеет производительность порядка nlog2n — намного лучше, чем большинство других методов.
Сортировка по методу Шелла
Сортировка по методу Шелла является разновидностью сортировки вставками. Сортировка вставкой обладает ограничением, потому что позволяет сравнивать только смежные элементы, в результате при каждой перестановке элемент перемещается только на одну позицию. Элементы, далеко удаленные от своих правильных позиций, требуют многих проходов для попадания на свои правильные позиции. Сортировка по методу Шелла допускает "скачки" в порядке ее выполнения. Для этого записи делятся на чередующиеся
Глава 12
группы, и каждая группа сортируется с использованием метода сортировки вставками. Это разделение выполняется с использование значения приращения (назовем его h); значение приращения делит первоначальный массив на h подмассивов. Затем эти подмассивы сортируются с использованием метода сортировки вставками или пузырьковой сортировки. Этот шаг называется h-сортировкой. Процесс h-сортировки повторяется с использованием уменьшающегося значения h, пока это значение не окажется равным 1. Во время последнего прохода список уже почти отсортирован, и сортировка вставками на финальном проходе завершает этот процесс, образуя отсортированный список. В связи с этим сортировка по методу Шелла называется также сортировкой уменьшающегося приращения.
Выбор правильной последовательности значений для h очень важен. Проведено множество исследований выбора значений для h без четкого указания, как осуществлять этот выбор. В реализации на C++ в листинге 12.6 используется следующая стратегия для выбора значения h. Эта стратегия считается эффективной:
Пусть hj = 1 и п равняется количеству сортируемых элементов.
Пусть hs+1 = 3 * hs + 1. Остановиться, когда h > п/9..
Листинг 12.6. Реализация сортировки по методу Шелла // Программа: shell_sort.срр // Автор: Megh Thakkar // Цель: Сортировка массива, состоящего из п элементов, в порядке возрастания //с использованием сортировки по методу Шелла.
#include <stdio.h> #include <string.h> #include <stdlib.h> #include <iostream.h> class sort { private: int *X;//Список элементов данных
int n;//Количество элементов в списке
public: sort (int size) { X = new int[n=size]; } ~sort( ) ( delete [ ]X; } void load_list (int input[ ] ) ; void show_list (char ‘title); void shell_sort( int input[ ]);
};
void sort::load_list(int input[ ]) { for (int i = 0; i < n; i++) X[i] = input[i] ;
)
void sort::show_list( char ‘title) { cout « "\n" « title; for (int i = 0; i < n; i++) cout « " " « X[i] ; cout « "\n";
}
void sort::shell_sort ( int input[ ]) ( int i,h; int temp;
//Загрузка исходного списка в приватный массив load_list(input);
show_list("List to be sorted: ") ;
//Реализация алгоритма сортировки по методу Шелла for (h = 1; h <= n/9; h = 3*h + 1)
//Использование уменьшающихся значений для h
for ( ; h > 0; h /=3) {
Часть III for (i = h ; i < n ; i++) ( int j ;
temp = X[i] ; for ( j = i-h ; j >=0 ; j -“ h ) (
if (temp < X[j]) ( X[j+h] = X[j];
} else break;
) X[j+h] = temp; }
} show_list("List sorted using shell sort: "); }
//main( ) : Тест сортировки по методу Шелла void main(void) ( // Создание нового объекта класса sort sort sort_obj(10);
// Список для сортировки static int unsorted_list[] = {54,6,26,73,1,43,51,83,5,28};
// Вызов метода сортировки по методу Шелла sort_obj.shell_sort(unsorted_list);
)
Анализ сортировки по методу Шелла
Сортировке по методу Шелла посвящено множество исследований, и показано, что производительность худшего случая находится в интервале от п1-25 до 1.6nL2S. Эффективность этого метода сильно зависит от выбора последовательности значений для h. Как уже отмечалось ранее, не существует идеальной формулы для выбора этой последовательности, но хорошо подобранные последовательности показывают производительность сортировки по методу Шелла порядка n(log2n)2. Сортировка по методу Шелла практически нечувствительна к исходным данным и показывает худшую производительность, чем пузырьковая сортировка и сортировка вставками, когда исходные данные почти отсортированы. Однако для случайных наборов данных сортировку Шелла следует рассматривать в числе первых.
Пирамидальная сортировка
При пирамидальной сортировке массив рассматривается как двоичное дерево с определенными характеристиками. Этот метод, по существу, переупорядочивает элементы дерева, чтобы значение каждого узла было больше или равным значениям дочерних узлов. Имейте в виду, что узлы в пирамиде не упорядочены. Однако условие того, что любой родительский узел содержит большие элементы данных, чем все дочерние узлы, гарантирует, что самый большой элемент находится на вершине пирамиды. Если затем удалить этот элемент из вершины и поменять его местами с последним элементом массива, то этот элемент попадет на свою правильную отсортированную позицию. После этого можно повторно заставить выполниться условие пирамиды: найти наибольший элемент данных среди оставшихся элементов. Повторяя эту процедуру, мы, в конце концов, отсортируем массив.
Реализация пирамидальной сортировки на языке C++ показана в листинге 12.7.
Листинг 12.7, Реализация пирамидальной сортировки //Программа: heap_sort.срр //Автор: Megh Thakkar
//Цель: Сортировка массива, состоящего из п элементов, в порядке возрастания //с использованием метода пирамидальной сортировки
#include <iostream.h>
Использование методов сортировки Глава 12
class Heap ( private: int *X; int heap_size; public:
Heap (int n) ; -Heap (){ delete [] X; }
void establish_heap_property (int root, int limit); void construct_heap (void); void heap_sort (int input []); void show_list (char *title); void load_list(int input[]);
};
Heap::Heap (int n) <
X = new int [n + 1] ; heap_size = n;
)
void Heap::load_list(int input[ ]) { for (int i = 1; i <= 10; i++) X[i] = input[i-l] ;
} void Heap::establish_heap_property (int root, int limit) (
int done = 0; int biggest = X[root]; int j = 2 * root; while ((j <= limit) 66 (done == 0)) ( //Поиск максимального элемента между левым и правым дочерними элементами if ((j < limit) 66 (X[jJ < X[j + 1]))
//Сравнение найденного максимального элемента с наибольшим значением //Если наибольший элемент имеет максимальное значение, то условие пирамиды выполнено //и нужно выйти.
if (biggest >= X[j]) done= 1 ; else ( X[j/2] = X[j]; j = 2 * j;
} } X[j/2] = biggest; }
void Heap::construct_heap (void) ( for (int i = heap_size/2; i > 0; i--) establish_heap_property (i, heap_size);
}
void Heap::heap_sort (int input[]) (
construct_heap () ; for (int i = (heap_size - 1) ; i > 0; i--) { //Поменять местами X[i+1] и X[l] int temp = X[i + 1] ; X[i + 1] = X[l] ; X[l] = temp;
11 Поместить корень в отсортированную позицию input[i] = X[i + 1] ;
establish_heap_property (1, i) ;
11 Зак. 53
Часть III
input[O] = X[l] ; }
void Heap: :show_list (char *title) (
//В зависимости от того, завершена сортировка или нет, //отображаются различные части массива.
cout « "\n" « title;
for (int i = 1; i <= heap_size; i++) cout « "" « X[i] ;
cout « "\n"; )
void main (void) (
Heap heap_obj(10);
// Массив элементов данных для сортировки
static int input[] = (1,9,24,2,59,31,99,74,3,66); int sz = 10;
cout « "List to be sorted: ";
for (int k = 0; k < sz; k++) cout « input[k] « " "; cout «"\n\n\n";
heap_obj.load_list(input);
heap_obj.heap_sort (input);
heap_obj.show_list ("List sorted using Heap sort: ") ; )
Анализ пирамидальной сортировки
Метод пирамидальной сортировки сильно зависит от функции build_max_heap(). Эта функция используется для построения пирамиды и помещения максимального значения на вершину, чтобы его можно было удалить из дальнейшего рассмотрения. Эта функция выполняет работу О(п). Эффективная реализация этого метода дает оценку O(nlog2n).
Выбор метода сортировки
В этой главе было рассмотрено несколько алгоритмов для реализации сортировки. Каждый из этих алгоритмов представляет собой определенный компромисс, потому что сценарии лучшего и худшего случаев для каждого из них зависят от самих сортируемых данных. Однако некоторые алгоритмы в целом лучше других, потому что они минимизируют количество сравнений, количество перестановок или количество просмотров. Вообще, при небольшом количестве сортируемых записей (меньше 1000) можно выбрать любой из улучшенных методов (таких, как быстрая сортировка или пирамидальная сортировка и сортировка по методу Шелла), потому что, как показано в табл. 12.2, производительность О-записи для быстрой и пирамидальной сортировки составляет O(nlog2n), а для сортировки по методу Шелла — О(п125). Эти оценки производительности не сильно отличаются для небольших значений п.
Следует также иметь в виду, что эффективность любого метода сортировки зависит от его конкретной реализации. В табл. 12.3 приведены сравнительные характеристики различных видов техники сортировки. Например, для сортировки вставками сценарий лучшего случая (уже отсортированные записи) легко может стать сценарием худшего случая при плохой реализации (при просмотре от последней записи к первой, а не наоборот). Пирамидальная сортировка и сортировка по методу Шелла выгодны тем, что они не зависят от исходных данных и для них фактически нет худших случаев. Продуманная реализация быстрой сортировки часто может оказаться намного быстрее других улучшенных методов.
Таблица 12.2. Сравнение методов сортировки
Метод сортировкиХудший случайЛучшийслучай
Пузырьковая сортировкаО(п2)О(п)
Сортировка вставкамиО(п2)О(п)
Сортировка выборомО(п2)О(п2)
Использование методов сортировки
Глава
■ 323
Метод сортировки
Худший случай
Лучший случай
Пирамидальная сортировка
Сортировка слиянием
Быстрая сортировка
Сортировка по методу Шелла
O(nlog2n) O(nlog2n) O(n2) O(n(log2n)2)
O(nlog2n)
O(nlog2n)
O(nlog2n)
Для большого количества элементов данных (больших значений п) рост nlog2n меньше, чем п2. Следовательно, очень эффективны такие методы сортировки, как пирамидальная сортировка, сортировка слиянием и быстрая сортировка. С другой стороны, для малого количества элементов данных (малых значений п) достаточно эффективными оказываются такие методы, как пузырьковая сортировка, сортировка вставками и сортировка выбором (см. табл. 12.3).
Таблица 12.3. Сравнение характеристик методов сортировки
Метод сортировки
Преимущества
Недостатки
Сортировка вставками
Простой код
Стабильная сортировка
Сортировка массивов по месту
О(п) сравнений в лучшем случае
О(п2) сравнений в среднем
Пирамидальная сортировка
Сортировка массивов по месту
Всегда производительность O(nlog2n) и относительно быстрое выполнение
Нестабильная сортировка
Сложная сортировка
Быстрая сортировка
Самая быстрая в среднем случае
Сложный код
Очень плохая производительность в худшем случае
Необходимо дополнительное стековое пространство O(logn)
Нестабильная сортировка
Сортировка выбором
Простой код
Стабильная сортировка
Сортировка массивов по месту
Количество перестановок О(п)
Среднее количество сравнений О(п2)
Сортировка по методу Шелла
Простой код
Сортировка массивов по месту
Производительность худшего случая лучше других методов О(п’ 5)
Нестабильная сортировка
Пирамидальная сортировка и быстрая сортировка в большинстве случаев лучше сортировки Шелла
Генерирование тестовых данных
Перед выбором определенной реализации сортировки следует проверить ее на различных исходных данных, чтобы убедиться в оптимальной работе этой реализации при всех условиях. Следует, по меньшей мере, проверить реализацию на данных, отсортированных в прямом порядке, в обратном порядке, в возрастании-убывании и с дублирующимися записями. Кроме того, следует использовать примерные данные, которые предполагается сортировать на практике.
Для генерирования различных типов тестовых данных можно использовать программу, показанную в листинге 12.8.
Листинг 12.8. Генерирование тестовых данных для методов сортировки
// Программа: te st_data.срр
// Автор: Megh Thakkar
И Цель: Генерирование данных в прямом, обратном и случайном порядке, а также дублирующихся
Иданных. Эти данные можно использовать для тестирования различных методов сортировки.
Обработка данных Часть III
#include <stdio.h> #include <stdlib.h> #include <ctype.h> #include <string.h> #include <iostream.h> class test_data ( private: int *X;//Список элементов данных
int n;//Количество элементов в списке
public: test_data (int size) { X = new int[n=size]; } ~test_data( ) { delete [ ]X; }
// Метод show_list используется для отображения результата void show_list (char *title);
// Метод forward_ord используется для генерирования данных в прямом порядке void forward_ord( int n) ;
// Метод reverse_ord используется для генерирования данных в обратном порядке void reverse_ord( int n) ;
// Метод duplicate_ord используется для генерирования дублирующихся данных void duplicate_ord( int n) ;
// Метод random_ord используется для генерирования данных в случайном порядке void random_ord( int n) ;
};
void test_data::show_list( char *title) { cout « "\n" « title; for (int i = 0; i < n; i++) cout « " " « X[i] ; cout « "\n";
}
void test_data::forward_ord(int n) ( int i, step, first, last; first = 0; last = n; step = 1 ; for ( i = first; i < last; i +=step ) X[i] = i;
show_list("Test data generated in the forward order: ") ; } void test_data::reverse_ord(int n) ( int i, step, first, last; first = n; last = 0; step = 1; for ( i = first; i > last; i -=step ) X[n-i] = i;
show_list("Test data generated in the reverse order: ") ; } void test_data::duplicate_ord(int n) ( int i, step, first, last; first = 0; last = n; step = 1; for ( i = first; i < last; i +=step ) X[i] = abs( rand( ) ) % 2;
show_list("Test data generated in the forward order: ") ;
Глава 12 } void test_data;:random_ord(int n) ( int i, step, first, last;
first = 0;
last = n; step = 1,- for ( i = first; i < last; i +=step ) X[i] = abs( rand( ) ) ;
show_list("Test data generated in the forward order: ") ; } //main( ) : проверка генерирования тестовых данных void main(void) ( int n;
cout «"\n Enter the number of data elements you want to generate: ";
cin » n;
cout « "\n We will generate numbers in the following orders: \n"
«"(1)Forward-ordered\n"
«"(2)Reverse-ordered\n"
«"(3)Duplicate records\n"
«"(4)Random ordered records\n";
II Создание объекта для хранения сгенерированных данных test_data test_obj(n) ;
test_obj.forward_ord(n);
test_obj.reverse_ord(n);
test_obj.duplicate_ord(n);
test_ob j.random_ord(n);
}
Резюме
Эта глава поможет вам выбрать лучший метод сортировки данных. Следует запомнить, что эффективность любого метода сортировки сильно зависит от реализации этого метода и сортируемых данных. Лучший способ выбрать правильный метод сортировки для определенной задачи — протестировать каждый метод на различных типах исходных данных, в частности, на примере данных, которые будут сортироваться на практике.
13
Алгоритмы поиска данных
В ЭТОЙ ГЛАВЕ
Линейный поиск
Сопоставление с образцом
Алгоритмы поиска на графе
Внешний поиск
Глава 13
Поиск — это процесс нахождения данных среди набора элементов, который удовлетворяет определенному критерию. Приемлемость общей производительности многих приложений зависит от эффективности используемых методов поиска данных.
Конечная цель всех методов поиска одна и та же: найти запрашиваемую информацию. Однако пригодность определенного метода поиска в конкретной ситуации определяется несколькими факторами, такими как реализация и логика, используемая в этом методе. При сравнении методов поиска необходимо учесть следующие критерии:
86 Время подготовки. Некоторые методы требуют значительного времени для подготовки среды поиска перед началом самого поиска. Если планируется выполнять поиск в небольшом наборе данных, то время подготовки становится очень важным фактором при выборе метода.
■ Время поиска. Это время, необходимое для выполнения алгоритма поиска. Основная часть алгоритмов имеет оценку О(п), где п — это количество элементов данных, среди которых выполняется поиск. Это линейное время обычно включает в себя время подготовки (х) и время поиска (у). Другими словами, общее время равняется х+у; цель состоит в минимизации х и у. В этом выражении время подготовки равняется х, а время поиска у равняется О(п). При возрастании п время подготовки становится менее значительным фактором при подсчете общего времени.
и Необходимость возврата. Одни алгоритмы поиска выполняют простой линейный просмотр элементов данных; другие переходят вперед и назад по набору данных. Этот критерий также следует учесть при выборе алгоритма поиска.
Линейный поиск
Линейный поиск — это самый простой тип поиска, потому что при его выполнении просто просматривается множество элементов данных и эти элементы сравниваются с искомыми данными, пока не обнаружится совпадение. Таким образом, линейный поиск прост в реализации, но не всегда эффективен. Он не требует времени на подготовку. Предположим, что нужно найти определенный элемент данных S в неотсортированном массиве X, состоящем из N целочисленных элементов. Этого можно достичь с помощью следующего псевдокода:
Пусть i = О Сравнить X[i] с S. Если они совпадают, возвратить i, иначе увеличить i на 1.
Повторить шаг 2 и просмотреть массив, пока не обнаружится совпадение или пока не будет просмотрен весь массив.
Реализация этого алгоритма на языке C++ показана в листинге 13.1.
Листинг 13.1. Реализация метода линейного поиска //Программа: linear search.срр //Автор: Megh Thakkar //Цель: Поиск целочисленного значения S в неотсортированном массиве //из N целых чисел. #include <stdio.h> #include <string.h> #include <stdlib.h> #include <iostream.h> class Search { private: int *X;//Список элементов данных
int N;//Количество элементов
public:
Search (int size) ( X = new int[N=size] ; ) -Search( ) { delete [ ]X; } void load_list (int input[ ] ); void show_list (char *title); int linear_search ( int S) ; }; void Search::load_list(int input[ ]) for (int i = 0; i < N; i++) X[i] = input[i];
Часть III
void Search::show_list( char ‘title) ( cout « "\n" « title; for (int i = 0; i < N; i++) cout « " " « X[i]; cout « "\n";
}
int Search: :linear_search( int S) { for ( int j = 0; j < N; j++) { if (X[j) = S) // Совпадение найдено. //Возвратить j return(j);
} return(-1); )
//main( ) : Тест для линейного поиска void main(void) < int search_key;
Search search_obj (10);
static int list_to_search[] = (54, 6,26,73,1,100,36,41,2,83); cout « ”\n”
« "C++ Inpiementation of Linear search" « " \n" ;
search_obj.load_list(list_to_search);
cout «"\n" « "Enter the key to search:";
cin » search_key;
search_obj.show_list("Searching the following list: ") ; cout « "\n\n\n";
int result = search_obj.linear_search(search_key); if (result != -1)
cout « "\n" « "Search Result: " « "X[" « result « "] = " « search_key;
else cout « "\n" « "Search Result: " « search_key « " is not found in the list \n" ;
cout « "\n\n\n";
}
Анализ линейного поиска
Алгоритм, представленный в листинге 13.1, является линейным, потому что в худшем случае он выполняет п сравнений; таким образом, он выполняет работу О(п). Лучший случай — когда совпадение находится в самом первом сравнении. Средний случай: О(п/2)=О(п).
Поиск в отсортированном массиве
Если массив, в котором нужно найти данные, уже отсортирован, то можно увеличить производительность поиска, используя стратегию "разделяй и властвуй", называемую двоичным поиском (листинг 13.2).
Псевдокод двоичного поиска выглядит следующим образом:
Разбить массив на две половины в точке middle.
О-авнить ключ поиска search_key с X [middle] .
Глава 13
Если они совпадают, то поиск завершен. Если search_key > X[middle], то search_key находится в правой половине; Значит, повторить предыдущие шаги для правой половины массива.
Иначе search_key находится в левой половине массива и следует повторить предыдущие шаги для левой половины.
Листинг 13.2. Реализация двоичного поиска в отсортированном массиве //Программа: binary_search.срр //Автор: Megh Thakkar
//Цель: Поиск значения в отсортированном массиве из N элементов ♦include <stdio.h> ♦include <string.h> ♦include <stdlib.h> ♦include <iostream.h> class Search ( private:
int *X;//Список элементов данных
int N;//Количество элементов
public:
Search (int size) ( X = new int[N=size]; )
-Search( ) { delete [ ]X; ) void load_list (int input[ ] ) ; void show_list (char ‘title); int binary_search( int S);
};
void Search::load_list(int input[ ]) <
for (int i = 0; i < N; i++) X[i] = input[i];
}
void Search::show_list( char ‘title)
cout « "\n" « title;
for (int i = 0; i < N; i++) cout « " " « X[i] ; cout « "\n";
}
int Search::binary_search( int S) { int head; int tail; int middle;
int bmiddle; //Элемент перед средним элементом int amiddle; //Элемент после среднего элемента head = 0; tail = N;
if ( (S < X[0]) || S > X[N-1]) //Искомый элемент вне границ диапазона, //следовательно, нет смысла продолжать поиск, return(-1);
while (head < tail) (
middle = ((head + tail)/2) + 1; bmiddle = middle - 1; amiddle = middle + 1;
if (S = X [middle] | | S == X [bmiddle] | | S = X[amiddle]) //Такое сравнение не только //со средним элементом, но и //с предшествующим и последующим элементами
Часть III
//считается более эффективным, //чем простое сравнение со средним элементом.
{
if (S == X[middle]) return(middle);
if (S == X[bmiddle]) return(bmiddle);
if (S == X[amiddle]) return(amiddle); } else if (S > X[middle]) //Искомый элемент (если он существует) //должен находиться в правой половине списка, //поэтому исключим левую половину.
head = middle + 1 ; else //Искомый элемент (если он существует) //должен находиться в левой половине списка, //поэтому исключим правую половину.
tail = middle - 1;
) return (-1) ;
)
//main( ) : Тест для двоичного поиска void main(void) (
int search_key;
Search search_obj (10);
static int sorted_list[] = (2,5,10,31,45,48,52,58,66,82); cout « "\n"
« "C++ Implementation of Binary search" « " \n";
search_obj.load_list(sorted_list);
cout «"\n" « "Enter the key to search:";
cin » search_key;
search_obj.show_list("Searching the following sorted list: ”); cout « "\n\n\n";
int result = search_obj. binary_search(search_key);
if ( result != -1) cout « "\n" « "Search Result: " « "X[" « result « "] = ” « search_key;
else cout « "\n" « "Search Result: " « search_key « " is not found in the list \n";
cout « "\n\n\n";
)
Двоичный поиск отличается от линейного тем, что при его выполнении используется стратегия "разделяй и властвуй" для исключения тех частей отсортированного массива, в которых не может находиться искомый элемент. При двоичном поиске используется информация о том, что массив отсортирован; на каждом сравнении алгоритм делит массив на две части: одну из этих частей можно исключить из дальнейшего поиска, а в другой необходимо продолжить поиск.
Сопоставление с образцом
Сопоставление с образцом — это распространенная операция, выполняемая над строками. Сопоставление с образцом можно определить как поиск вхождения образца длины В в тексте длины А. Алгоритмы, используемые для сопоставления с образцом можно легко расширить до поиска всех вхождений данного образца в тексте, потому что после обнаружения первого вхождения можно продолжить просмотр текста и
Глава 13
найти следующее вхождение, начиная с позиции сразу после начала совпадения. В такой задаче сопоставления с образцом образец, по существу, используется как ключ поиска.
Грубый алгоритм
При решении задачи сопоставления с образцом сначала можно использовать грубый метод. Это самый простой алгоритм поиска, но не самый эффективный и не очень творческий. Грубый метод позволяет просмотреть весь текст, чтобы узнать, существует ли определенная строка. Этот механизм описан в следующем псевдокоде, который находит первое вхождение строки в тексте:
int brutesearch (char ‘s, char ‘t) (
int i, j, m, n;
i = 0;
j = °;
m = strlen(s);
n = strlen(t);
while ( j < m ss i < n) {
if ( t[i] ' = s[j] )
{
i -= j-1;
j = -i;
)
if (j = m)
return (i - m) ;
else
return i;
}
Объяснение грубого алгоритма
В представленном выше псевдокоде указатель i указывает на символы в тексте; указатель j указывает на символы в образце, который нужно найти. После инициализации эти указатели указывают на начало текста и образца. Переменные тип используются для хранения длины двух строк. Указатели увеличиваются до тех пор, пока они указывают на совпадающие символы и пока не достигнуты концы строк. Если символы не совпадают, то указатели переустанавливаются таким образом, чтобы указатель j указывал на начало образца, a i возвратился на следующую позицию после первого символа, совпавшего с образцом. После этого можно найти возможные совпадения в оставшемся тексте. Если достигнут конец образца (j == m), то получаем совпадение, при этом образец начинается в позиции t[i-mj. Если достигнут конец текста без совпадения с образцом (j < га && i < п), значит, совпадений не существует.
Анализ грубого алгоритма
В сценарии худшего случая все символы в образце (га) проверяются для всех возможных позиций образца в тексте (п-ш+1). Таким образом, выполняется работа порядка O(m(n-m+l)) = O(mn).
Представление образца
Образцы могут быть представлены как символы, связанные с помощью следующих основных операций:
6 Соединение (конкатенация). Два символа считаются соединенными, если они примыкают друг к другу. Совпадение существует только в том случае, если эти два символа в тексте также примыкают друг к другу. Например, АВ означает, что в образце сразу за А следует В.
OR. OR представляется знаком плюс (+). Если два символа объединены операцией OR, то они расцениваются как альтернативы. Другими словами, А+В означает, что в данной позиции находится либо А, либо В.
Замыкание. Замыкание представляется символом звездочки (*); если за символом следует знак замыкания, это означает нуль или более вхождений этого символа.
Часть III
Рассмотрим несколько примеров:
6 (АВС)* означает повторяющиеся образцы АВС. Другими словами, это может быть АВСАВСАВСАВС. Это видно из представления замыкания, которое означает повторение образца АВС.
A(B+C)D означает, что допустимыми образцами являются ABD и ACD. Это следует из применения записи OR, которая означает, что за А следует В или С, а затем следует D.
Поиск совпадения с образцом можно упростить с помощью алгоритмов, реализующих конструкцию конечных автоматов, которые могут искать ключевые слова (ключевые слова означают возможные образцы, которые нужно найти).
Построение конечных автоматов
Образец для поиска можно использовать для построения конечных автоматов. Такие автоматы графически представляются как сеть узлов, в которой каждый узел указывает определенное состояние. Узлы связаны друг с другом в соответствии с образцом. Входной символ, соответствующий переходу, указывается на дуге между узлами. Конструкцию и применение конечных автоматов можно проиллюстрировать на следующих нескольких примерах. Сначала рассмотрим, как можно представить основные операции, а затем увидим, как можно составить целый образец, построив конкретные автоматы, которые можно объединить для образования более крупных автоматов:
Распознать один символ — А. Это можно представить с помощью автомата с двумя состояниями: начальным и конечным, переход в конечное состояние при этом осуществляется, когда встречается символ А (рис. 13.1).
Объединить два автомата — Ml и М2. Это можно выполнить, объединив конечное состояние первого автомата и начальное состояние второго (рис. 13.2).
(А)[ ]{М1} and (М2) = >{Ml}(М2)-
РИСУНОК 13.1. Автомат с двумяРИСУНОК 13.2. Конечный автомат,
состояниями.представляющий конкатенацию.
Выполнить операцию OR для двух автоматов — Ml и М2. Операция OR изображается введением нового пустого состояния как начального состояния для обоих автоматов и конечного состояния одного автомата, которое также является конечным состоянием второго автомата (рис. 13.3).
Выполнить операцию замыкания. Для этого конечное состояние замыкается на начальное состояние (рис. 13.4).
РИСУНОК 13.4. Конечный автомат, представляющийРИСУНОК 13.4. Конечный автомат, представляющийоперацию замыкания.операцию замыкания.()(М1)[ ]
РИСУНОК 13.4. Конечный автомат, представляющий
РИСУНОК 13.4. Конечный автомат, представляющий
операцию замыкания.
операцию замыкания.
(М2)
РИСУНОК 13.3. Конечный автомат, представляющий операцию OR.
ПРИМЕЧАНИЕ
На рисунках 13.1-13,4 используются следующие обозначения:
() — начальное состояние.
[] —конечное состояние.
(X) — переход, если встречается символ X.
{М} — конечный автомат.
Строго следуя этим правилам, можно построить конечный автомат для любого образца.
Например, построим конечный автомат для следующего образца: A(BC+DE)F (рис. 13.5).
РИСУНОК 13.5.Конечный автомат, представляющийA(BODE)F.РИСУНОК 13.5.Конечный автомат, представляющийA(BODE)F.()(А)(1)(В)(2)(С)(3)(F)(5)
РИСУНОК 13.5.
Конечный автомат, представляющий
A(BODE)F.
РИСУНОК 13.5.
Конечный автомат, представляющий
A(BODE)F.
।(D)(4)(Е)1
Переходы, не определенные на рис. 13.5, ведут в состояние FAIL_STATE. Можно также создать таблицу переходов на основе этой диаграммы (табл. 13.1).
Глава 13
Таблица 13.1. Таблица переходов для A(BC+DE)F
Состояние
0
1
2
3
4
5
Входной символ
А
С
F
Е
Следующее_состояние1
1
2
3
5
3
0
Следующее_состояние2
1
4
3
5
3
0
Эта таблица переходов интерпретируется следующим образом: если автомат находится в состоянии X и читает символ Y, то он переходит в состояние Следующее_состояние1 или Следующее_состояние2.
В этой таблице переходов представлен недетерминированный автомат, потому что, когда он находится в состоянии 1, он может перейти в состояние 2 или 4 в зависимости от прочитанного символа. Другими словами, он содержит более одного состояния FAIL_STATE для состояния 1.
Можно построить результирующую функцию, которая ищет успешные совпадения, определяемые конечным состоянием в конечном автомате.
Конечные автоматы могут быть детерминированными или недетерминированными. В детерминированных автоматах переход из любого состояния можно легко определить на основе следующего входного символа. Недетерминированные автоматы, с другой стороны, обычно представлены операциями OR или замыканиями, потому что на основе простого сравнения с одним символом нельзя определить, возможно ли совпадение с образцом на данном этапе.
Псевдокод для реализации такого конечного автомата можно описать следующим образом:
State = 0; While ((с = getchar( ) ) != EOF ) { if (transition (state, c) IS NOT NULL) state = transition(state,c) ; if (result(state) IS NOT NULL) //Совпадение найдено printf("Match found.");
)
В этом псевдокоде используется несколько функций: функция transition(state,c) исследует таблицу переходов и определяет, возможен ли переход, на основе текущего состояния и следующего прочитанного символа. Функция result(state) определяет, указывает ли текущее состояние на достижение совпадения и, следовательно, завершение алгоритма. Этот программный код очень обобщен; его реальная реализация определяет производительность поиска.
Алгоритмы поиска на графе
Графы можно использовать для представления некоторых повседневных ситуаций. Например, вы запланировали поездку из Киева в Симферополь. Можно поставить следующий вопрос: Какой воздушный маршрут следует избрать, чтобы добраться из Киева в Симферополь быстрее всех? Еще одним примером служит составление расписания работ, когда есть множество задач и множество зависимостей между ними, согласно которым выполнение определенной задачи можно начать только после завершения одной или нескольких других задач. Конечные автоматы, рассмотренные в предыдущем разделе, также можно считать примерами графов.
Рассмотрим несколько простых определений, связанных с графами: ■ Граф. Это совокупность узлов и соединений между ними.
6 Узлы. Это объекты, с которыми могут быть связаны имена и другие свойства.
Связи (или дуги). Это соединение между двумя узлами.
Путь. Путь из узла А в узел В — это список узлов, через которые необходимо пройти при переходе из узла А в узел В. Например, на рис. 13.6 ABCD и ACD — это два пути из узла А в узел D.
РИСУНОК 13.6.(А)—(В)
Графические представление задачи's.
определения пути между узлами.mi^(С)
Часть III
■ Связный граф. Граф называется связным, если существует путь из одного узла в любой другой узел в этом графе.
й Простой путь. Путь между двумя узлами является простым, если ни один узел в нем не повторяется. Другими словами, такой путь ни через один узел не проходит больше одного раза.
а Цикл. Это простой путь, в котором первый и последний узлы совпадают.
й Дерево. Это граф, который не содержит циклов.
и Двоичное дерево. Это дерево, в котором каждый узел соединен только с тремя другими узлами. Один из этих узлов является родителем данного узла, а два других являются дочерними узлами.
Дерево можно легко преобразовать в цикл, добавив к нему только одно дополнительное соединение (это соединение завершит цикл). Граф обладает следующими свойствами:
® Дерево с N узлами содержит N-1 соединений.
в Граф с N узлами, но менее чем с N-1 соединениями не может быть связными.
в Граф с N узлами и более чем с N-1 соединениями обязательно содержит цикл.
® Граф с N узлами и с N-1 соединениями не обязательно является деревом.
и Граф с N узлами может содержать соединения в количестве от 0 до N(N-l)/2.
И Разреженный граф — граф с очень маленьким количеством соединений.
в Плотный граф — граф с большим количеством соединений.
в Полный граф — граф, который содержит N(N-l)/2 соединений.
и Неориентированный граф — граф, в котором соединения между узлами двунаправленны.
а В ориентированном графе соединения между узлами однонаправленны.
8 Взвешенный граф — граф, в котором соединениям между узлами назначены веса. Вес можно представить как стоимость перехода из узла А в узел В.
ы Два узла называются смежными или соседними, если между ними существует соединение. Соединение, или ребро, называется инцидентным с узлами А и В.
а Степень узла А — это количество инцидентных с ним узлов.
и Узел называется изолированным, если его степень равна нулю.
Алгоритмы, которые работают с графами, не должны посещать один и тот же узел более одного раза, чтобы увеличить свою эффективность. Для обхода графа широко используются два метода — поиск в глубину и поиск в ширину.
Поиск в глубину
Поиск в глубину называется также поиском с возвратом. Это название станет понятным из принципа работы этого метода. Рассмотрим неориентированный граф, который начинает свой обход с узла А. Предположим, что степень этого узла равна d. Это означает, что к узлу А прилегают узлы А,, где i=l,2,3,...,d. Основной принцип этого механизма состоит в пометке узлов, которые уже просмотрены. Поиск в глубину начинается с просмотра узла А, а затем со всех непросмотренных узлов, смежных с узлом А, например, А,. Остальные смежные узлы хранятся в стеке, чтобы их можно было просмотреть позже. После того как будут просмотрены все узлы, смежные с узлами, которые, в свою очередь, являются смежными с узлом Ар выполняется возврат назад и просмотр всех остальных непросмотренных смежных узлов Ар где i=2,3,...,d. Этот процесс продолжается до тех пор, пока не будут просмотрены все узлы графа. Поиск в глубину является примером полного перебора, потому что он позволяет исследовать все узлы графа для определения самого лучшего решения задачи. Поиск в глубину можно реализовать как рекурсивно, так и нерекурсивно. Имейте г. виду, что поиск в глубину, по существу, ведется вниз по графу, пока не будет достигнут тупик или узел, который не содержит ни одного непросмотренного смежного узла; после этого он начинает вестись снова вверх, чтобы можно было просмотреть другие дочерние (смежные) узлы родителя.
В следующем фрагменте программного кода C++ показана рекурсивная реализация поиска в глубину:
void Depth first search(int n ) {"
int i ;
const int TRUE = 1;
const int FALSE = 0;
Алгоритмы поиска данных Глава 13 int ‘checked; struct node (int nodeid; struct node ‘adj; }; for ( i = 0; i <= n; i++) checked[i] = FALSE; for ( i = 0; i <= n; i++) if (checked[ij == FALSE) check(i);
1 void check (int A) ( checked [A] = TRUE; For all adjacent vertices Ai ( i = 1,2,3,,d) of node A
( if (Ai is not yet checked) check(Ai);
} } Метод поиска в глубину можно также показать графически (рис. 13.7).
РИСУНОК 13.7.W
Иллюстрация метода поискау' I х. I
в глубину.(D)—(Е) — (F)
На рис. 13.7 поиск в глубину начинается с просмотра и печати узла А. С узлом А смежны узлы В и D, следовательно, теперь можно просмотреть любой из этих узлов. Предположим, что выбран узел В. При поиске в глубину после просмотра и печати узла В другие узлы, смежные с узлом А (в данном случае — D) оставляются для просмотра позже. С узлом В смежны узлы А, С, D, Е и F. Узел А уже просмотрен, поэтому допустим, что выбран и просмотрен узел С. С узлом С смежны узлы В и F. Поскольку узел В уже просмотрен, то просматривается узел F. После этого просматривается узел Е и наконец, узел D. Таким образом, узлы будут просмотрены в следующем порядке: ABCFED.
Путь, пройденный при поиске в глубину, не является уникальным. Если в начале поиска выбрать другой смежный узел, то будет пройден совсем другой путь.
Поиск в ширину
Поиск в ширину является альтернативным методом обхода графов. Это также метод полного перебора, т.е. обходятся все узлы графа. В отличие от поиска в глубину, при поиске в ширину сначала просматривают все смежные узлы A, (i=l,2,3,...,d) данного узла А, прежде чем перейти к узлам, смежным с узлами Af Этот метод также помечает просмотренные узлы для повторного просмотра.
Псевдокод поиска в ширину:
Создать объект queue_obj, который иожет функционировать как очередь. Инициализировать этот объект очереди пустым значением.
Инициализировать массив checked!], чтобы все его элементы были равны FALSE. Начнем с узла А. Присвоить checked [А] = TRUE. Добавить узел А к началу очереди. Выполнить следующие действия, пока объект очереди не пустой: Присвоить current_node = head(queue_obj). Просмотреть все непросмотренные узлы, смежные с current_node.
Добавить все узлы, смежные с current_node, в очередь, чтобы их смежные узлы можно было просмотреть позже. Освободить очередь queue_obj.
Этот метод можно проиллюстрировать на той же диаграмме, которая использовалась для объяснения работы поиска в глубину (см. рис. 13.7). Начнем, как и прежде, с узла А. После просмотра узла А просмотрим все его смежные узлы (в данном случае узлы В и D). Теперь можно просмотреть узлы, смежные с узлом В или с узлом D. Например, выберем узел D. После просмотра узлов, смежных с D (в данном случае узла Е), просмотрим узлы, смежные с В. В результате получится путь поиска ADBEFC.
Как можно понять из псевдокода и рис. 13.7, в этом методе путь поиска также не уникален, а зависит от выбора следующего узла во время обхода.
Часть III
Сравнение поиска в глубину и поиска в ширину
Метод поиска в глубину прост для реализации и может быстро привести к хорошему решению. Недостаток этого метода в том, что можно потратить много времени на просмотр путей, которые не приведут к успеху. Простейшей альтернативой поиску в глубину является поиск в ширину, который позволяет гарантированно найти наилучший путь к решению (если оно существует), если количество ветвей является конечным. Чтобы понять это, предположим, что есть путь длины р от исходного узла к конечному. При поиске в ширину сначала будут найдены все узлы на уровне 1, затем все узлы на уровне 2 и т.д. Наконец, будут просмотрены все узлы на уровне р. Таким образом, будет найден не просто путь, а лучший путь.
Поиск в ширину обладает двумя недостатками:
Он требует много памяти, поскольку количество узлов возрастает экспоненциально при просмотре последующих уровней.
Если кратчайший путь длинный, то будет просмотрено очень много уровней, что потребует больше времени, чем при поиске в глубину, поскольку поиск в ширину должен проверить все узлы на определенном уровне перед тем, как перейти к следующему уровню.
Поиск в глубину лучше, чем поиск в ширину, в тех ситуациях, когда существует множество путей из исходного узла в конечный, но каждый из этих путей очень длинный.
Поиск по первому наилучшему совпадению
Метод поиска по первому наилучшему совпадению — это интеллектуальный алгоритм, объединяющий поиск в глубину и поиск в ширину в одном методе. При поиске по первому наилучшему совпадению используется лучшее из обоих алгоритмов, он называется так потому, что предлагает исследовать путь, который кажется наиболее многообещающим. Этот алгоритм можно реализовать, выполнив такие действия:
Найти узлы, доступные из текущего узла.
Применить эвристическую функцию к каждому из этих узлов. Эвристическая функция — это, по существу, определенная пользователем функция, которая на основе исходных данных определяет важность этого узла. Например, каковы шансы найти решение, находясь в этом узле?
Выбрать следующий узел, который обладает наилучшими шансами (по результату эвристической функции) быстрее достичь решения.
Если решение найдено, можно выйти из алгоритма; иначе сохранить оставшиеся узлы для рассмотрения позже.
Присвоить current_node = best_next_node и повторять пункты 1—4 до тех пор, пока не будет найдено решение.
ПРИМЕЧАНИЕ
Эвристическая функция — это определенная пользователем функция, которая на основе исходных данных определяет важность этого узла. Например, "Каковы шансы выиграть, находясь в этом узле?" или "Каковы шансы найти решение, выйдя из этого узла?"
При реализации алгоритма поиска по первому наилучшему совпадению частично выполняется поиск в глубину, пока не будет найден узел, который выглядит достаточно многообещающим. При этом исследуются остальные смежные узлы по методу поиска в ширину. Выбрав наиболее перспективный узел, снова выполняется поиск в глубину, пока не будет найдено решение. Этот подход в конечном счете позволяет найти решение меньшими усилиями, чем поиск в глубину или в ширину.
Рассмотрим граф, представленный на рис. 13.8. Первоначально есть только один узел (узел А), но он связан с четырьмя другими узлами. После применения эвристической функции к этим узлам узел С выбирается как наиболее многообещающий. Узел С связан с двумя другими узлами. Применим эвристическую функцию и к этим узлам. Видно, что узел Е не такой многообещающий, как узел В. Следовательно, применим эвристическую функцию к узлам, связанным с узлом В. Этот процесс продолжается, пока не будет найдено решение.
Глава 13Глава 13
Глава 13
Глава 13
РИСУНОК 13.8.
Пример, иллюстрирующий поиск по первому наилучшему совпадению.
Реализация объектов графов
Как известно, граф состоит из множества узлов, связей между этими узлами и стоимостей, сопоставленных этим связям. Существует несколько способов реализации таких структур:
Матрица смежности
Массив указателей на одно- или двусвязные списки смежных узлов
Связный список указателей на одно- или двусвязные списки смежных узлов
Массив указателей на одно- или двусвязные списки связей
Связный список указателей на одно- или двусвязные списки связей
Наиболее часто используемой структурой среди этого списка является матрица смежности; она обычно представляется как двумерный массив. Дейкстра (Dijkstra, 1960) предложил метод, который может использоваться для нахождения кратчайшего пути между начальным узлом и всеми другими узлами. Алгоритм Дейкстры предлагает общий метод для решения задачи кратчайшего пути. Это пример "жадного" алгоритма. Жадные алгоритмы реализуют алгоритмы по стадиям; на каждой стадии алгоритм пытается выполнить то, что кажется наилучшим на данной стадии. На каждой стадии алгоритм Дейкстры выбирает узел, который содержит наименьшее расстояние среди непросмотренных узлов, и заявляет, что найден кратчайший путь от начального узла до данного узла. Далее на этой стадии алгоритм обновляет значение ранее известного кратчайшего пути. Пример реализации алгоритма кратчайшего пути Дейкстры на C++ показан в листинге 13.3. Матрица смежности может использоваться для представления всех типов графов: неориентированных, ориентированных, взвешенных, невзвешенных, разреженных, плотных и др.
Для связного графа матрица смежности adj_mtx[][] удовлетворяет следующим условиям:
adj_mtx[i] [j] = cij, если узлы Ai и Aj смежные
adj_mtx[i] [j] = 0, если узлы Ai и Aj не смежные
Необходимо отметить следующее:
Для невзвешенных графов cij = 1, но для взвешенных графов cij представляет затраты на переход от А к А-
Для неориентированных графов матрица смежности является симметричной; для ориентированных графов матрица смежности может быть симметричной или несимметричной.
Листинг 13.3. Алгоритм кратчайшего пути Дейкстры //Программа: dap.срр //Автор: Megh Thakkar
//Цель: Реализация взвешенного ориентированного графа U и алгоритма кратчайшего пути Дейкстры.
#include <stdio.h>
#include <string.h>
#include <stdlib.h> #include <iostream.h>
class graph (
HQ ■
JJO |
private: int adj_matrix[100][100];
//хранит стоимости, введенные пользователями int shortest_path_matrix[100][100] ;
//хранит кратчайший путь между узлами
public: graph (int size) { } -graph( ) { } void setup_matrix(int num_nodes);
int find_cost_of_shortest_path(int source, int destination); void init_adj_matrix(int num_nodes);
void init_shortest_path_matrix(int num_nodes);
void load_adj_matrix(int num_nodes); void load_shortest_path_matrix( int num_nodes) ;
);
int graph::find_cost_of_shortest_path(int source, int destination) (
int result;
//shortest_path_matrix уже содержит //вычисленный кратчайший путь между узлами.
result = shortest_path_matrix[source][destination];
return(result);
void graph::setup_matrix(int num_nodes) (
■ init_adj_matrix(num_nodea);
//инициализация матрицы смежности нулями
' load_adj_matrix(num_nodes);
init_shortest_path_matrix(num_nodes); load_shortest_path_matrix(num_nodes);
}
void graph::init_adj_matrix(int num_nodes) (
int i, j;
for ( i = 1; i <= num_nodes; i++) for ( j = 1; j <= num_nodes; j++) adj_matrix[i][j] = 0;
}
void graph::init shortest_path_matrix(int num_nodes)
(“
//Инициализация матрицы кратчайшего пути
//значениями матрицы смежности int i, j;
for ( i = 1; i <= num_nodes; i++) for ( j = 1; j <= num_nodes; j++)
shortest_path_matrix[i][j] = adj_matrix[i][j];
)
void graph::load_adj_matrix(int num_nodes) (
int i, j;
int cost;
char direct[1] ;
const int LARGE_COST = 999999;
for ( i = 1; i <= num_nodes; i++) ( for ( j = 1; j <= num_nodes; j++) (
//Ввод пользователем значений для матрицы смежности
Глава 13
cout « "Is there a direct path from Node " « i « " to Node " « j « "? : ";
cin » direct;
if ( ( direct[0] == 'y' ) I I (direct[0) == '¥')) {
cout « "Enter the cost of the path from Node "
« i « " to Node " « j « " : "; cin » cost; cout « "\n"; adj_matrix[i][j] = cost; } else
adj_matrix[i][j] = LARGE_COST;
11 очень большая стоимость }
}
}
void graph::load_shortest_path_matrix( int num_nodes) (
int i, j, к;
const int LARGE_COST = 999999;
for ( j = 1; j <= num_nodes; j++) (
for ( i = 1; i <= numnodes; i++) {
for (k = 1; к <= num_nodes; k++) {
if ( (shortest_path_matrix[j, к] > 0 ) ) ("
//Если существует непрямой путь между двумя узлами, //который короче найденного ранее пути, то
//он становится кратчайшим путем между этими узлами.
if ( (shortest_path_matrix[j][k] == LARGE_COST) ||
(shortest_path_matrix[i][j] + shortest_path_matrix[j] [k]
< shortest_path_matrix[i][k]))
shortest_path_matrix[i][k] = shortest_path_matrix[i][j] + shortest_path_matrix[j][k];
}
)
} }
}
void main(void) {
int num_nodes;
int source;
int destination; int scost;
cout « "Enter the number of nodes in the graph (less than 100):";
cin » num_nodes;
if ( num_nodes > 100 ) (
cout « " Sorry. This program has a limitation of only 100 nodes ";
cout « "\n\n\n";
exit (-1);
}
graph graph_obj(num_nodes);
graph_obj.setup_matrix(num_nodes);
cout « " Enter the node id of the source node: ";
Часть III
cin » source ;
if (source > num_nodes) {
cout « " Invalid source! ";
cout « "\n\n\n";
)
cout « " Enter the node id of the destination node: "; cin » destination;
if (destination > num_nodes) (
cout « " Invalid destination! ";
cout « "\n\n\n";
)
scost = graph obj.find_cost_of_shortest_path(source, destination);
cout « "Shortest path between Node " « source « " and Node " « destination
« " has a cost of " « scost;
cout « "\n\n\n";
)
Представление игры Tic-Tac-Toe
Игру Tic-Tac-Toe можно представить как структуру, содержащую девятиэлементный вектор, который представляет позиции на игровом поле. Кроме того, следует хранить список позиций, которые достижимы из данной позиции, и оценочные значения, которые отражают вероятность возможного выигрыша.
Алгоритм состоит из просмотра возможных следующих ходов и выбора наилучшего. Лучший ход определяет следующим образом:
Если следующий ход приводит к победе, то это лучший ход.
Если ни один из возможных ходов не приводит к победе, то для каждого из возможных ходов требуется рассмотреть возможные ответные ходы противника. Имейте в виду, что цель противника состоит в поиске хода, который максимально снизит вероятность его выигрыша. Следовательно, оценкой нашего следующего хода является наихудшая оценка всех дочерних узлов.
Лучшим узлом (т.е. лучшим следующим ходом) для нас является узел с наибольшей оценкой.
Эта процедура называется минимаксной, потому что на чередующихся уровнях дерева цель состоит в попеременной максимизации и минимизации шансов выигрыша.
Применение альфа-бета-отсечений
Минимаксная процедура является примером поиска в глубину, потому что она сначала как можно глубже исследует один путь и применяет оценочную функцию к последнему узлу в пути. Затем она поднимается на один уровень вверх и т.д. Подобные процедуры поиска в глубину можно улучшить, используя такие методы, как методы ветвей и границ, описанные выше. Эффективность поиска в глубину можно улучшить, отказавшись от просмотра путей, которые заведомо хуже наилучшего найденного до сих пор пути. В то же время очень важно понять, что на общую производительность этого метода очень сильно влияет порядок анализа путей. Это связано с тем, что если начать анализ с худшего пути, то это сведет на нет всю идею этого метода.
Эту технику поиска лучше всего описать на примере, в котором используется минимаксная процедура в игре Tic-Tac-Toe.
При альфа-бета-отсечении используются два пороговых значения: альфа и бета. Значение альфа — это нижняя граница значения, которое можно присвоить узлу максимизации. Значение бета — это верхняя граница значения, которое можно присвоить узлу минимизации.
Рассмотрим на примере (рис. 13.9). Предположим, что мы выполняем поиск в глубину: поиск выполнен во всем поддереве узла В, и результаты применения оценочной функции показали, что узлу D соответствует оценка 6, а узлу Е — оценка 9. Таким образом, можно гарантировать, что узлу А соответствует оценка
не менее 6. Это значение становится значением альфа для узла А, и его можно использовать для исключения некоторых ветвей всего дерева из просмотра. После анализа узла К мы видим, что его оценка равна 0. Таким образом, узел I содержит оценку не более 0. Значит, уже не нужно анализировать остальные поддеревья узла I, потому что путь из узла С в узел I заведомо невыгоден, так как переход в узел В дает оценку 6. Теперь предположим, что для узла J получена оценка 10. Таким образом, оценка узла С будет не более 10. Это бета-значение.
Рассмотрим использование бета-значения, которое является верхней границей узла минимизации. Предположим, что оценка в узле М равняется 15; это больше, чем значение бета для узла С. Проще говоря, если мы выберем из узла С узел G, то получим оценку 15, которая больше 10 (оценка узла F). Однако в узле С
Глава 13Глава 13РИСУНОК 13.9. Пример, иллюстрирующий алъфа-бета-отсечения.РИСУНОК 13.9. Пример, иллюстрирующий алъфа-бета-отсечения.выполняется минимизация оценок, поэтому нет смысла выбирать узел G (оценка которого не меньше оценки
Глава 13
Глава 13
РИСУНОК 13.9. Пример, иллюстрирующий алъфа-бета-отсечения.
РИСУНОК 13.9. Пример, иллюстрирующий алъфа-бета-отсечения.
узла М, поскольку в узле G выполняется максимизация). В результате можно отказаться от анализа всех остальных поддеревьев узла G. В очень больших деревьях использование альфа-бета-отсечений может при
вести к значительному сокращению путей поиска и к повышению общей производительности.
При использовании альфа-бета-отсечений на уровне максимизации можно исключить некоторый путь,
если становится понятно, что его оценка меньше значения альфа для родительского узла; на уровне минимизации можно исключить путь, если становится понятно, что его оценка больше значения бета для ро
дительского узла.
Как уже упоминалось, эффективность альфа-бета-отсечений зависит от порядка обхода путей. Можно доказать, что при существовании идеального порядка обхода количество просмотренных конечных узлов с применением альфа-бета-отсечений и глубине просмотра d примерно вдвое больше количества просмотренных конечных узлов без использования альфа-бета-отсечений и с глубиной просмотра d/2.
Дальнейшая модификация альфа-бета-отсечений, называемая отсечениями пустот, может обеспечить значительное улучшение производительности. Идея этого вида отсечений состоит в том, что можно исключить дополнительные пути, если ожидаемое улучшение минимально по сравнению с наилучшим найденным в данный момент путем. Смысл в том, что можно потратить больше времени на анализ других путей дерева в надежде найти лучший путь.
Задача коммивояжера
Изложение методов поиска не было бы полным без упоминания задачи коммивояжера. Эта задача заключается в поиске ответа на вопрос среди очень большого количества возможных решений. Для таких задач поиска неприемлем линейный поиск, используемый в других типах задач, потому что задача коммивояжера потребовала бы огромного количества времени для выполнения линейного поиска.
Определение задачи. Дано множество из N городов. Найти кратчайший маршрут, соединяющий все города без посещения одного и того же города более одного раза.
Проведено множество исследований задач такого рода; на данный момент не существует эффективного метода их решения с использованием полного перебора, потому что необходимо искать решение среди большого количества маршрутов, каждый из которых может быть очень длинный в зависимости от значения N.
Если предположить, что коммивояжер может перемещаться только между определенными парами городов, то эту задачу можно представить с помощью графа. Тогда задача будет состоять в поиске цикла. Существует несколько способов решения этой задачи, но ни один из них не является достаточно эффективным. Первый метод заключается в применении полного перебора. Полный перебор можно выполнить с помощью разновидности поиска в глубину, если не просто помечать просмотренные узлы, но и снимать пометки с узлов, если путь привел в тупик. Другими словами, выполняется поиск в глубину, и на просмотренных узлах ставятся пометки (чтобы не просматривать их повторно). Если текущий путь приводит в тупик, необходимо выполнить возврат и попробовать другой маршрут. Такой возврат требует снятия пометок с узлов, которые уже были просмотрены на маршруте. После этого можно попробовать другую ветвь. Такой полный перебор может потребовать очень много времени, особенно на полном графе.
Для сокращения количества путей используется множество различных методов. Они основаны на применении определенных проверок на узлах в целях исключения тех ветвей, которые заведомо не могут привести к правильному решению. В таких методах применяются различные формы возврата.
Часть III
При попытке найти простой цикл в таких задачах можно исключить некоторые альтернативы, осознав, что циклы являются симметричными, т.е. всегда существует два пути, представляющих один и тот же маршрут. Это можно определить, применив ограничение, согласно которому три узла должны появляться в определенном порядке. Другими словами, из узлов А, В и С узел В должен идти после А, но перед С. При этом узел С будет проверяться только после проверки узла В.
Задачи поиска пути с наименьшей стоимостью можно выполнить более эффективно, если не продолжать просматривать путь, когда стоимость его части становится выше стоимости наилучшего найденного на данный момент. Поиск решения будет более эффективным, если проверять дочерние узлы данного узла в порядке возрастания стоимостей, чтобы найти оптимальное решение первым. Применяя возвраты и продуманные методы отсечения, можно получить эффективные результаты поиска.
Внешний поиск
Поиск — это очень важная и часто используемая операция, применяемая к дисковым накопителям. На дисках хранятся файлы, и для доступа к ним требуется эффективный способ. В большинстве приложений, использующих дисковый ввод/вывод, скорость доступа к диску является самым критичным элементом, поэтому это время необходимо сократить до минимума.
Индексированный последовательный доступ
Основные методы поиска можно применить и к дискам. Однако методы последовательного доступа, рассмотренные в этой главе, недостаточно эффективны. Методы линейного поиска позволяют просто просматривать ключи, пока не будет найдено совпадение или достигнут конец списка. Можно улучшить поиск, используя индекс, чтобы отследить, какие ключи принадлежат каким страницам на диске. Первая страница каждого диска может быть его индексной страницей. Этот подход можно улучшить, используя главный индекс, чтобы страница главного индекса содержала информацию о том, на каких дисках находятся различные ключи. Например, главный ключ может указывать, что ключи на первом диске меньше, чем D, ключи на втором диске находятся в интервале от Е до К и т.д. Главный индекс может быть достаточно маленьким и размещаться в памяти, позволяя очень быстро обратиться к записи, выполняя для этого только два вида поиска — по главному индексу, чтобы найти диск, содержащий ключ, и по индексной странице на диске, чтобы найти, где на диске хранится запись. Этот метод объединяет методы индексирования с последовательным доступом и поэтому называется индексированным последовательным доступом. Недостаток этого метода заключается в том, что при большом количестве обновлений индекса (при добавлении и удалении записей) для обслуживания индексов может потребоваться много времени.
Двоичные деревья
Для улучшения индексного просмотра можно использовать двоичные деревья, помещая ключи в структуру дерева, чтобы ключи в левом поддереве любого узла следовали перед этим узлом, а ключи в правом поддереве —- после этого узла. Существует три метода обхода, которые можно использовать с объектом binary_tree: до упорядочивания, при упорядочивании и после упорядочивания. Хотя просмотр можно улучшить, используя любой метод обхода, особенно эффективным является обход при упорядочивании. Ниже показан псевдокод для каждого их этих трех методов. Основное отличие между методами заключается в порядке сравнения узлов с ключом поиска. Следующие фрагменты программного кода иллюстрируют представленные методы обхода.
Метод обхода до упорядочивания:
preorder_traversal(aode parent) {
check parent node;
preorder_traversal(parent->left child) preorder_traversal(parent->right child)
)
Метод обхода при упорядочивании:
inorder_traversal(node parent) {
inorder_traversal(parent->left child) check parent node;
inorder_traversal(parent->right child)
}'
Глава 13
Метод обхода после упорядочивания:
postorder_traversal(node parent) {
postorder_traversal(parent->left child) postorder_traversal(parent->right child) check parent node;
}
Метод поиска в двоичном дереве более гибок, чем индексированный последовательный поиск, потому что можно достаточно легко найти место в дереве для новых узлов, а также удалить узлы, сохранив структуру дерева.
Деревья 2-3-4
Деревья 2-3-4 обладают несколькими именами — деревья 2—4 и симметричные двоичные В-деревья. В В-дере- ве узлы (которые являются структурами памяти) обычно достаточно большие, чтобы сохранить как минимум один блок, прочитанный с диска. Деревья 2—4, с другой стороны, могут использоваться для хранения одного, двух или максимум трех элементов в узле. Деревья 2—4 — это, по существу, В-деревья меньшего порядка; они могут использоваться для создания более эффективного способа поиска, чем двоичные деревья. В деревьях 2—4 существует три положения для хранения элементов и четыре положения для хранения указателей. Показано, что в среднем в таких узлах используется только 2/3 пространства. Имейте в виду, что основная причина использования деревьев 2—4 состоит в улучшении производительности поиска в оперативной памяти — и очень важно сэкономить объем оперативной памяти. Решение этой дилеммы состоит в преобразовании 2—4 деревьев в двоичные деревья (рис. 13.10), чтобы каждый узел мог хранить только один элемент данных. Это позволяет сэкономить пространство и в то же время достичь высокой производительности, поскольку можно использовать те же методы поиска, которые использовались бы и в дереве 2—4. Еще одна важная характеристика такого преобразования заключается в использовании двух типов связей:
Для представления обычной связи между родительским и дочерним узлами
Для объединения ключей, принадлежащих одному и тому же узлу в дереве 2—4
Эти типы связей имеют несколько названий: горизонтальные/вертикальные указатели или красные-и-черные указатели.
На рис. 13.10 показаны различия между вертикальными/горизонтальными и красно-черными деревьями:
Красно-черные деревья лучше представляются в форме двоичных деревьев; в горизонтальных/вертикальных деревьях лучше оставить структуру дерева 2—4.
Горизонтальные/вертикальные деревья лучше подходят для представления В-деревьев любого порядка.
Независимо от выбранного представления необходимо как-то различать два типа связей. Этого можно легко добиться, используя флаг в представлении связи.
Реализация горизонтальных/вертикальных и красно-черных деревьев заметно отличается от двоичных деревьев. Однако в них используется один и тот же метод поиска: если ключ поиска совпадает с текущим узлом, то нужно остановиться. В противном случае необходимо перейти в левое поддерево, если ключ поиска больше элемента узла, и в правое поддерево, если ключ поиска меньше элемента узла.
Очень важным вопросом при использовании таких реализаций, как красно-черные деревья, является сохранение исходной структуры дерева. Например, для реализации горизонтальных/вертикальных деревьев всегда справедливы следующие утверждения:
н Путь от корня в любой лист должен содержать одно и то же количество вертикальных связей.
Ни в каком пути не может содержаться две последовательные горизонтальные связи.
Эти факты порождают сложности в обслуживании таких деревьев, потому что вставка ключа требует вставки узла и связи; необходимо определить, какой должна быть эта связь — горизонтальной или вертикальной. Удаление ключа также приводит к слиянию узлов. Обычно при работе с такими деревьями хорошо использовать метод разделения узлов.
Часть III
Красно-черное мреюКрасно-черное мрею
Красно-черное мрею
Красно-черное мрею
РИСУНОК 13.10.
Преобразование дерева 2-3-4 в горизонтальное/вертикальное дерево и в красно-черное дерево.
Резюме
Поиск — это важная задача для многих приложений. Обычно информация разделяется на записи, идентифицируемые с помощью ключей. Процесс поиска, как правило, включает в себя не только поиск ключа, но и доступ к информации в соответствующих записях. Для решения задач поиска может использоваться несколько алгоритмов; выбор эффективного алгоритма зависит от представления данных. Для определения объема работы алгоритма поиска может использоваться О-запись.
Вообще, алгоритмы поиска подразумевают просмотр набора данных. Этот процесс можно улучшить, используя информацию о представлении данных. При методе бинарного поиска используется информация о том, что данные предварительно отсортированы.
Задачу сопоставления с образцом можно представить как конечный автомат. В этой главе также рассмотрено построение конечных автоматов на основе образца для поиска.
Множество практических задач можно представить с помощью графа; для решения задач, которые могут быть представлены на графе, разработано большое количество алгоритмов, таких как алгоритм Дейкстры (Dijkstra). Идеального метода поиска не существует, поэтому всегда следует творчески подходить к реализации метода поиска.
Хеширование и
синтаксический анализ
В ЭТОЙ ГЛАВЕ
Сравнение поиска и хеширования
Функции хеширования
Разрешение конфликтов
Синтаксический разбор
^«1 <-> .s
Часть III
Сравнение поиска и хеширования
При использовании различных методов поиска, рассмотренных в главе 13, для нахождения нужных данных выполнялось сравнение с ключом. При последовательном поиске данные искались в таблице, сравниваясь с ключом, чтобы определить имеется ли ключ поиска в этой таблице. При двоичном поиске сравнение с ключом необходимо для разбиения таблицы на две части и определения, в какой из половин может находиться ключ поиска.
Что же касается хеширования, — это способ, который подразумевает использование значения ключа поиска для определения его позиции в таблице без сравнения с ключом. При хешировании используется произвольная функция отображения для определения местоположения любого элемента данных и обеспечения приблизительно равномерного распределения элементов данных в памяти, выделенной для выполнения хеширования.
В идеальном случае расположение элементов определяется с помощью функции хеширования, примененной для ключа поиска, при этом выполняется работа независимо от фактического количества элементов данных. Однако в действительности метод хеширования только приблизительно соответствуют этому расчету и приводит к конфликтам, когда одна и та же функция хеширования, примененная к двум или более различным ключам поиска, может указать на одну и ту же позицию. Поэтому очень важно уметь разрешать конфликты.
Методы хеширования сильно зависят от выбора функции хеширования. Цель хеширования состоит в нахождении функции хеширования h, которая при применении к ключу поиска К преобразует этот ключ в индекс таблицы, содержащей набор данных. Требуется идеальная функция хеширования, которая не приводит к конфликтам. Можно создать идеальную функцию хеширования, если таблица содержит как минимум столько же позиций, сколько имеется элементов. Однако мы не всегда знаем количество элементов заранее, кроме того, может не оказаться достаточно пространства для хранения всех элементов. Сложные функции хеширования могут избежать конфликтов, но могут также потребовать много времени для выполнения. Поэтому при использовании метода хеширования нужно найти компромисс между размером и временем.
Во многих приложениях предпочтительнее использовать хеширование, а не методы двоичного поиска, описанные в главе 13. Хеширование — относительно дешевый способ реализации (в отношении скорости выполнения и используемого пространства), который к тому же обеспечивает очень быстрый поиск, особенно если можно выделить достаточно много пространства для хранения большой таблицы хеширования. С другой стороны, двоичный поиск обладает несколькими преимуществами:
Двоичный поиск является динамическим и не зависит от предварительной информации о количестве элементов данных.
Производительность худшего случая для двоичного поиска вполне приемлема. Худший случай при хешировании — это когда все ключи указывают на одну и ту же область; разрешение конфликтов при этом может превратиться в настоящее бедствие.
При двоичном поиске могут выполняться дополнительные операции, такие как сортировка.
Функции хеширования
Функция хеширования является важной частью процесса хеширования. Эта функция используется для преобразования ключей в адреса таблицы. Она должна легко вычисляться и преобразовывать ключи (обычно целочисленные или строковые значения) в целые числа в интервале от 0 до TR-1 (где TR — это количество записей, которое может вместиться в таблице).
Поскольку большинство распространенных функций основано на арифметических операциях, то нужно уметь преобразовывать ключи в числа, над которыми можно выполнять арифметические операции. Рассмотрим несколько функций хеширования, которые можно легко вычислить:
Деление. Если нет информации о характеристиках ключей, то деление является наилучшей функцией хеширования. Предположим, что TR — это количество записей в таблице, а К — это ключ поиска; тогда h(K) = К mod TR. Эта функция лучше всего работает, когда TR — простое число. Если TR — не простое число, то можно использовать функцию h(K) = (К mod р) mod TR, где р — простое число, большее, чем TR. Важно, чтобы функция хеширования возвращала значение, меньшее, чем TR; для этого выполняется деление по модулю. Для ключей, представленных строками, число К можно получить, проанализировав двоичное представление символов строки и преобразовав его в десятичное значение. Имейте в виду, что этот метод может привести к конфликтам.
Глава 14
Свертывание. В основе методов свертывания лежит разбиение ключей на подключи с последующим выполнением некоторых арифметических операций над ними. Предположим, что ключом для хеширования является телефонный номер. Каждый номер можно разбить на пять частей, которые можно сложить, а результат разделить по модулю. Например, телефонный номер (407) 555-1212 можно разбить на пять частей: 40, 75, 55, 12, 12. В результате сложения этих чисел получаем значение 194. Результат хеширования: 194 mod TR. Различные вариации только что описанного метода свертывания отличаются по способу разбиения ключей и по операциям, выполняемым над подключами. Строки можно разбить на подстроки, а затем применить к подстрокам операцию XOR, как показано в следующем примере: h(”xyzw”)="xy" XOR "zw".
и Средняя площадь. В некоторых случаях очень эффективным оказывается метод средней площади. Сначала используется один из методов свертывания, а затем результат возводится в квадрат. Адрес в таблице извлекается как средняя часть полученного значения. В предыдущем примере с телефонным номером в результате свертывания получено значение 194. Теперь возведем его в квадрат: (194)2 = 37636. Для таблицы из 1000 записей можно использовать адрес 763. Таким образом, 6(407-555-1212) = 763.
Теперь вы знакомы с основными методами создания функций хеширования. Как можно догадаться, при реализации этих методов потребуется приложить немалые усилия для разрешения конфликтов.
Разрешение конфликтов
В практических ситуациях сложно реализовать идеальные функции хеширования. Таблицы хеширования должны обладать возможностью разрешения конфликтов, возникающих в тех случаях, когда функция хеширования возвращает один и тот же результат при применении к двум различным элементам данных. В последующих разделах рассматриваются несколько вариантов решения этой задачи.
Линейное повторное хеширование
Линейное повторное хеширование — это простейший метод разрешения конфликтов. При обнаружении конфликта алгоритм проверяет таблицу в поисках пустой ячейки, а затем помещает элемент в эту ячейку. Когда нужно найти элемент, функция хеширования указывает на положение в таблице и обнаруживается конфликт (на него указывает no_match). Затем выполняется просмотр таблицы до тех пор, пока не возникнет одна из следующих ситуаций:
И Будет найден элемент.
Будет просмотрена вся таблица.
Будет найдена пустая ячейка в таблице, означающая, что элемент не найден.
При пошаговом просмотре таблицы вовсе необязательно выполнять единичные шаги — лучше перешагивать через несколько записей. Величина шага должна быть простым числом и не являться делителем размера таблицы, чтобы обеспечить обход всех записей. Единичный шаг не следует выполнять, чтобы избежать перегруженности, вызванной конфликтами в какой-либо области таблицы.
Линейному повторному хешированию свойственны следующие недостатки:
Элементы нельзя удалять из таблицы. При удалении элементов следует помечать ячейки как недействительные, в противном случае можно достичь пустой ячейки и сделать ошибочный вывод о том, что искомый элемент отсутствует в таблице. Предположим, что элементы Mike, Michael и Mikhael при хешировании указывают на одну и ту же позицию в таблице. Если удалить элемент Michael и не пометить ячейку как недействительную, то образуется пустая ячейка. Если затем попытаться найти элемент Mikhael, то поиск может завершиться в пустой ячейке в предположении, что элемент Mickhael не существует. При нахождении ячейки, помеченной как недействительная, поиск продолжится.
Производительность иногда может оказаться низкой, поскольку этот метод позволяет просматривать оставшуюся часть таблицы от точки возникновения конфликта и, таким образом, перебирать множество несовпадающих записей.
Нелинейное повторное хеширование
При линейном повторном хешировании выполняется последовательный просмотр от точки конфликта к пустой ячейке. Нелинейное повторное хеширование подразумевает повторное хеширование ключа поиска для определения нового значения. Если обнаружится еще один конфликт, то повторное хеширование будет выполняться до тех пор, пока не будет найдена пустая ячейка. Если таблица почти полная, то, возможно,
Часть III
потребуется выполнить хеширование несколько раз. Одна из разновидностей такого подхода называется двойным хешированием. Этот метод, в частности, решает задачу кластеризации. Используя этот метод, мы применяем фиксированный "шаг" для последовательного просмотра вместо просмотра каждой записи. Для определения "шага" применяется вторая хеш-функция. Выбор хеш-функции важен, потому что неподходящее значение может привести к неправильной работе программы. В эффективной реализации используются следующие ограничения для второй хеш-функции. Предположим, что h2 = hash2(K) и таблица содержит TR ячеек.
Значение Ь2 не должно равняться нулю, в противном случае программа "зациклится".
Ь2 и TR должны быть простыми числами, чтобы были просмотрены все записи и не было кластеризации.
Используемые хеш-функции должны отличаться друг от друга.
Примером простой вторичной функции хеширования является h2(K) = 16 п (К mod 16).
Важно иметь в виду, что двойное хеширование иногда может привести к большему количеству сравнений, чем линейное повторное хеширование, но двойное хеширование дает меньший размер кластера. Однако, вообще, в разреженных таблицах при двойном хешировании выполняется меньше работы, чем при использовании линейного метода.
Теперь рассмотрим пример простой хеш-функции, которая читает строки в форме "mmddyyyy" и преобразует их в позицию в таблице из 360 ячеек. В листинге 14.1 представлена реализация хеш-функции, которая приводит к конфликтам.
Листинг 14.1. Простая хеш-функция, которая приводит к конфликтам И hash_date.срр
// Эта программа получает строки вида "mmddyyyy". При
// вычислении адреса ячейки хеш-функция использует только И компоненты "mm" и "dd".
// Заметьте, что год вообще не учитывается.
// Другими словами, никак не учитывается, является ли год
И високосным. Эта простая хеш-функция также полагает, И что каждый месяц содержит 30 дней.
// Примечание. Эта функция может привести к конфликтам.
int hash_date( char *datestring)
int mm, dd;
datestring[4] = '\0';
dd = atoi (datestring + 2) ;
datestring[2] = ■ \0*;
mm = atoi (datestring) ;
if (mm < 1 | | mm > 12 || dd > 30 ) (
cout « "Error in input string. "; return (-1) ;
)
return( mm*30 + dd) ;
}
Результат функции в листинге 14.1 следует разделить по модулю на TR, чтобы получить значение в интервале от 0 до TR-1. Эта функция представляет собой простой способ, который показывает, как можно выполнить хеширование, но она не решает проблему конфликтов.
Коэффициент загрузки (альфа)
Коэффициент загрузки — это понятие, которое сильно влияет на производительность метода хеширования. Коэффициент загрузки определяется как отношение количества элементов (п), вставленных в таблицу, к количеству ячеек в таблице хеширования:
Alpha = n / TR
Глава 14
Коэффициент загрузки 1.0 означает, что количество вставленных элементов равняется количеству свободных ячеек. Как линейное, так и нелинейное хеширование, рассмотренное ранее в этой главе, хорошо выполняется в следующих ситуациях:
Если коэффициент загрузки низкий
Если удаление не является часто выполняемой операцией
По мере возрастания коэффициента загрузки возрастает также стоимость повторного хеширования и соответственно стоимость просмотра таблицы хеширования. Для проверки метода хеширования необходимо определить производительность с большими коэффициентами загрузки. При высоких коэффициентах загрузки хорошо работает метод связывания в цепочку, рассмотренный в следующем разделе.
Связывание в цепочку
Метод связывания в цепочку позволяет рассматривать таблицу хеширования как массив указателей на связанный список. Каждая ячейка в таблице хеширования либо является пустой, либо просто содержит указатель на связанный список. Для разрешения конфликтов элементы, указывающие при хешировании на одну и ту же ячейку, добавляются к связанному списку, на который указывает эта ячейка. В то же время можно легко удалять элементы: нужно просто удалить элемент из связанного списка. В табл. 14.1 приведены сравнительные характеристики методов связывания в цепочку и повторного хеширования.
Таблица 14,1. Сравнение методов связывания в цепочку и повторного хеширования
Свойство
Повторное хеширование
Связывание в цепочку
Число вхождений в хеш-таблице
Ограничено количеством доступных ячеек
Ограничено доступной памятью
Простое удаление
Нет
Да
Простая реализация
Да
Нет
Использование связанных списков
Нет
Да
Возможность конфликтов
Да
Да (но очень эффективное их разрешение)
Производительность с большим коэффициентом загрузки
Плохая
Хорошая
Использование дополнительного
Нет
Да
пространства
Упорядочивание связанных списков может еще больше сократить время поиска. Упорядочивание предотвращает полный перебор элементов всего списка. Связывание в цепочку имеет несколько недостатков:
Оно требует дополнительного пространства для указателей.
Стоимость поиска иногда может оказаться высокой из-за необходимости разадресации указателей вместо прямого обращения к данным, как в методах повторного хеширования.
Однако благодаря снижению цен на память и высокой скорости современных процессоров эти недостатки не очень существенны.
Один из методов связывания в цепочку, называемый связыванием в группу цепочек, объединяет в себе лучшие свойства обоих методов. В этом методе при обнаружении конфликта выполняется поиск первой доступной позиции для нового ключа и позиция ячейки для этого нового ключа сохраняется вместе с ключом, который уже находится в таблице. Другими словами, создается указатель, который ссылается на другую ячейку в таблице. На языке C++ этот метод можно реализовать таким образом, что каждая позиция является структурой с двумя полями: одно поле содержит ключ, а второе — позицию следующего ключа. Можно использовать значение -2 для указания того, что для данной ячейки доступна следующая позиция, и -1 — для указания достижения конца цепочки. При использовании этого метода может произойти переполнение таблицы. Однако можно использовать область переполнения, чтобы при необходимости выделить дополнительное пространство.
Адресация областей памяти
Метод адресации областей памяти похож на связывание в цепочки в том отношении, что для разрешения конфликтов используется дополнительная память. Однако вместо связанных списков используются области памяти. Область памяти можно определить как блок памяти, который можно использовать для
Часть III
хранения множества элементов, которые при хешировании указывают на одну и ту же позицию. В этом методе необходимо организовать области памяти некоторого фиксированного размера; выбор этого размера иногда может быть непростой задачей. Конфликты все еще возможны, потому что можно полностью заполнить область — в этом случае ключ необходимо где-то сохранить. Его можно сохранить в одной из доступных областей памяти или в области переполнения. Для устранения необходимости фиксированного размера областей можно использовать другую разновидность: позволить каждой области иметь указатель на динамически выделяемый массив.
Для эффективного применения методов хеширования необходимо иметь в виду следующее:
Желательно, чтобы коэффициент загрузки находился в интервале от 0,2 до 0,7. Это означает, что в случае необходимости следует использовать большие таблицы хеширования. Для некоторых типов исходных данных поддержка больших таблиц хеширования может оказаться непростой задачей.
Количество ячеек (TR) в таблице хеширования должно быть простым числом. Это позволит проявить творчество при использовании функции хеширования.
Ограничьте функцию хеширования только одной операцией деления (предпочтительно делением по модулю на размер таблицы).
Протестируйте алгоритм хеширования на некоторых простых данных, а также на критическом наборе данных. Этот подход чреват нарушением равномерного распределения данных.
и Примите конфликты как факт и приготовьтесь разрешать их, предпочтительно с помощью связывания в цепочки. Имейте в виду, что использование очень больших таблиц не защищает от конфликтов, если выполняется повторное хеширование.
В табл. 14.2 показаны простые числа, которые можно выбрать в зависимости от размера таблиц.
Таблица 14.2. Простые числа для некоторых распространенных размеров таблиц
Размер таблицы
Простое число, которое можно использовать
100
250
500
1000
1500
2000
5000
8000
10000
97
241
499
997
1499
1999
4999
7999
9973
Хеширование строк символов
Хотя существуют приложения с числовыми ключами, часто применяются ключи, являющиеся строками символов (такими, как имена, адреса и ключевые слова в тексте). Один из подходов к хешированию строк состоит в сложении ASCII-значений символов и делении по модулю на TR, где TR — это количество ячеек в таблице хеширования. В следующем фрагменте программного кода показана простая и эффективная функция хеширования, которая в качестве исходных данных принимает ключ и информацию о количестве ячеек TR и возвращает адрес области памяти:
int smpl_hash ( char * key, int TR) {
int bucket = 0;
while (*key) bucket += *key++;
bucket = bucket % TR; return bucket;
}
Очень сложно найти функцию хеширования общего назначения, которая хорошо подходит для всех типов строк, но следующая функция ElfHash() очень эффективна. Эта функция является частью UNIX System V Release 4 и используется в целях хеширования строк символов для файлов формата ELF (Executable and Linking Format).
Глава 14
unsigned long ElfHash(const unsigned char *key) { unsigned long h = 0; while (*key)( h = (h « 4) +*key++;
unsigned long g = h S OxFOOOOOOOL; if (g) h A= g » 24; h S= ~g ; ) return h;
}
Результат этой функции нужно разделить по модулю на TR. Эта функция очень эффективно работает в общем случае и может использоваться для равномерного распределения ключей в областях памяти. В табл. 14.3 показана производительность хеширования для различных методов. Производительность хеширования оценивается так же, как и производительность поиска, — с использованием О-записи. Числа в этой таблице соответствуют сравнениям ключей.
Таблица 14.3. Сравнение методов разрешения конфликтов
Количество
ключевых элементов
Линейное хеширование
Повторное хеширование
Простое связывание в цепочку
Связывание в группу цепочек
100
43
35
15
24
250
51
44
32
36
500
842
186
134
182
750
1983
641
299
504
1000
531
962
4971
64,594
Открытая адресация
Метод открытой адресации работает лучше, если есть достаточно четкое представление о количестве элементов. В каждой области памяти хранится только один элемент, и функция хеширования применяется к ключу для определения его позиции в таблице. Если область памяти пустая, то ключ копируется в эту область; в противном случае возникает конфликт и для определения положения используется одна из нескольких стратегий, рассмотренных ранее в этой главе. Каждая из стратегий, по существу, предполагает использование предопределенной последовательности, с помощью которой выполняется поиск пустой области; затем элемент вставляется в эту область. Если пустая область не найдена, значит, таблица полностью заполнена. Из этого описания следует, что последовательность проверок должна, в конце концов, охватить всю таблицу. Имейте в виду, что та же последовательность проверок выполняется при поиске ключа (вместо вставки). Поиск продолжается до тех пор, пока не будет найдено совпадение или пустая область. Если найдена пустая область, значит ключ отсутствует в таблице.
Метод открытой адресации содержит две трудности:
Сложно определить, когда встречается пустая область. Вообще, каждая область использует некоторый флаг, который указывает, пустая ли эта область.
Сложно выбрать лучшую тестовую последовательность. Это, вероятно, самое важное решение, потому что необходимо убедиться в том, что с помощью этой последовательности при необходимости проверяется вся таблица, а также можно минимизировать группирование и исключить конфликты.
Предположим, что тестовой последовательностью является р0, рр р2, ... ps р где р0 — это результат функции хеширования, а оставшаяся часть последовательности образуется перестановкой чисел от 0 до s-1.
Линейное тестирование
В линейном тестировании используется следующая тестовая последовательность (последовательность проб): р0, р0+1, Р0+2, ... p0+TR-l, с применением деления по модулю на TR. Деление по модулю обеспечивает переход к началу таблицы при достижении ее конца. Эта последовательность легко представляется с помощью следующих равенств:
Ро = hash (key)
р1 = (p0+i) mod TR
Часть III
В этих равенствах Pj — это i-й тестовый элемент.
Пример. Используем линейное тестирование для вставки следующих телефонных номеров в таблицу хеширования: 8881234, 8882345, 8883456, 8884321, 8886543. Мы будем использовать функцию хеширования hash(key) = key mod 11 и вставлять ключи в таблицу размером 11. Это упражнение состоит из следующих шагов:
hash (8881234)=8881234mod11=10
hash (8882345)=8882345mod11=10(конфликт)
Этот конфликт разрешается с помощью следующей тестовой последовательности:
рх = (р0+1) mod11 = 11mod11=0
hash (8883456)=8883456mod11=10(конфликт)
Этот конфликт разрешается с помощью следующей тестовой последовательности:
рх = (р0+1) mod 11 = 11 mod 11 = 0
Этот конфликт, в свою очередь, разрешается с помощью последовательности:
р2 = (р0+2) mod11 = 12mod11=1
hash(8884321) =8884321mod11=6
hash(8885432) =8885432mod11=6(конфликт)
Этот конфликт разрешается спомощьюследующей тестовой последовательности:
р2 = (p0+l) mod 11 = 7 mod 11 = 7
hash (8886543) = 8886543 mod 11 = 6 (конфликт)
Этот конфликт разрешается с помощью следующей тестовой последовательности:
pt = (p0+l) mod 11 = 7 mod 11 = 7
Этот конфликт разрешается с помощью последовательности:
р2 = (р0+2) mod 11 = 8 mod 11 = 8
В результате телефонные номера будут вставлены в таблицу следующим образом:
ИндексЗначение
0
8882345
1
2
8883456
3
4
5
6
8884321
7
8885432
8
9
8886543.
10
8881234
Любое тестирование может привести к последующим конфликтам, но при линейном тестировании ситуация особенно невыгодная. При выполнении линейного тестирования элементы начинают группироваться, и чем больше группа, тем больше вероятность последующего конфликта. Группирование, вызванное линейным тестированием, называется первичным.
Среднее количество проб, необходимое для успешного поиска (нахождения совпадения) и неуспешного поиска (нахождения пустой ячейки при вставке элемента данных в таблицу хеширования), оценивается с помощью следующих равенств:
S (alpha) = 1/2 + 1/2 (1/(1-alpha)), U (alpha) = 1/2 + 1/2 (1/(1-alpha))2
Глава 14
В этой последовательности alpha — это коэффициент загрузки.
Эти формулы оценивают среднее количество попыток для успешного и неуспешного поиска. Эти формулы сформулированы Дональдом Кнутом (Donald Knuth) и подробно описаны в его книге The Art of Computer Programming.
По мере заполнения таблицы оба типа поиска — A(alpha) и U(alpha) — требуют все больше и больше времени. Когда таблица почти заполнена, количество проб достигает O(TR). Поиск, выполняемый при хешировании, зависит от коэффициента загрузки и не обязательно от количества элементов.
Квадратичное тестирование
Проблема производительности при линейном тестировании вызвана образованием групп в результате использования линейной тестовой последовательности. При квадратичном тестировании используется другая последовательность, позволяющая избежать группировки. Эта последовательность представляется с помощью следующих равенств:
Ро = hash (key)
Pi = <Po+i2> mod TR
В этой последовательности р. — это i-я проверка, a TR — количество элементов в таблице.
Квадратичное тестирование переходит через большее количество ячеек по сравнению с линейным тестированием и, следовательно, приводит к меньшему группированию. Однако для всех ключей, которые отображаются в одну и ту же ячейку, используется одна и та же тестовая последовательность. Это приводит к ситуации, которая называется вторичным группированием. Самый большой недостаток квадратичного тестирования состоит в том, что, после того как просмотрена примерно половина ячеек, последовательность начинает повторяться. В результате пустые ячейки пропускаются. Для улучшения ситуации можно использовать разновидность квадратичного тестирования:
Ро = hash (key)
Pi = <Po+i2> mod TR
Однородное тестирование — это теоретическая тестовая последовательность, которая, по существу, является случайной и в которой каждая ячейка может быть выбрана с одинаковой вероятностью. При таком тестировании не возникает ни первичного, ни вторичного группирования. Среднее количество проб для такой последовательности можно выразить с помощью следующих равенств:
S (alpha) = l/alpha*ljl/(1-alpha))
U(alpha) = 1/(1-alpha)
Здесь alpha — это коэффициент загрузки.
В результате будет получено приблизительное число попыток для успешного и неуспешного поиска. Эти формулы написаны Дональдом Кнутом (Donald Knuth) и подробно рассмотрены в его книге The Art of Computer Programming.
На практике не существует методов генерации такой последовательности. Однако очень похожих результатов можно достичь с помощью метода двойного хеширования.
Тестирование двойного хеширования
При двойном хешировании используются две функции хеширования. Первая функция применяется к ключу. В случае возникновения конфликта к результату первой функции применяется вторая функция и ее результат используется как смещение для нахождения новой ячейки. При этом используются следующие равенства:
р0 = hashl(key)
offset = hash2(key)
Pt = (p^ + c) mod TR
Здесь TR — это количество ячеек.
Для гарантии проверки всей таблицы числа с и TR должны быть взаимно простыми.
Очень важно обеспечить, чтобы функции hashl и hash2 не зависели друг от друга. Если hash! приводит к конфликту, то hash2 не должна приводить к другому конфликту.
В свой книге Mathematics for the Analysis of Algorithms Дональд Кнут предлагает функции, помогающие избежать конфликтов:
Обработка данных Часть III hashl (key) = key mod TH hash2 (key) = key mod (TR-2) Генерирование случайных чисел можно увеличить еще больше, если выбрать такое значение TR, чтобы TR и TR-x были простыми числами.
Пример. Применим метод двойного хеширования для набора телефонных номеров, который использовался в предыдущем примере:
8881234, 8882345, 8883456, 8884321, 8886543
Мы будем вставлять ключи в таблицу размером 11 и использовать следующие функции хеширования: hashl (key) = key mod 11 hash2 (key) = key mod 7 При использовании этого метода выполняются следующие шаги: 1. hashl (8881234) = 8881234 mod 11 = 10 2. hashl (8882345) = 8882345 mod 11 = 10 (конфликт)
Этот конфликт разрешается с помощью следующей последовательности: hash2(8882345) = 8882345 mod 7=3
hashl (8883456) = 8883456 mod 11 = 10 (конфликт)
Этот конфликт разрешается с помощью следующей последовательности: hash2(8883456) = 8883456 mod 7=1
hashl (8884321) = 8884321 mod 11 = 6
hashl (8885432) = 8885432 mod 11 = 6 (конфликт)
Этот конфликт разрешается с помощью следующей последовательности: hash2(8885432) = 8885432 mod 7=3 (конфликт)
Теперь можно использовать линейное тестирование; следующая пустая ячейка — 4.
hashl (8886543) = 8886543 mod 11 = 6 (конфликт)
Этот конфликт разрешается с помощью следующей последовательности: hash2(8886543) = 8886543 mod 7=1 (конфликт)
Используя линейное тестирование, получим следующую пустую ячейку с индексом 2.
В результате телефонные номера будут вставлены в таблицу следующим образом: ИндексЗначение
8883456
28886543
38882345
48885432
5 68884321
7 8 9 108881234
Как видно, метод двойного хеширования не устраняет конфликтов и группирования, но минимизирует их число.
В листингах 14.2-4.6 показана реализация связанного списка и его операций previous_node(), current_node(), insert() и remove().
Глава 14
Листинг 14.2. Определение класса связанного списка
//Класс связанного списка можно определить следующим образом, template <class Etype> class linked—list { protected: struct node { Etype data; node ‘next;
//Конструктор узла
node ( Etype D = 0, node ‘n = NULL) : data(D) , next(n) { } );
node *list_head; void delete_list( );
linked_lists( linked_list S value);
public: linked_list( ) : list_head ( new node ) { } virtual ~linked_list( ) (delete_list( ); }
virtual Etype * find_previous_node( const Etype S key) ; virtual int remove( const Etype S key) ;
virtual Etype * find_node( const Etype S key); virtual int insert( const Etype S key) ;
};
Листинг 14.3. Функция поиска узла, который предшествует узлу, содержащему ключ
//Эта функция находит в связанном списке элемент //"key" и возвращает указатель на узел, который //непосредственно предшествует узлу, содержащему "key".
template <class Etype>
Etype *
linked_list<Etype>::
find_previous_node( const Etype S key) { node *p;
p = list_head;
while ( p->next != NULL) ( if (p->next->data == key ) { return p;
p = p->next; } return p->next;
};
Листинг 14.4. Функция удаления узла, содержащего ключ
//Эта функция находит в связанном списке элемент //"key" и удаляет узел, содержащий этот элемент. //При этом используется функция find_previous_node.
template <class Etype> int
Часть III
linked_list<Etype>:: remove( const Etype S key) {
node *to_delete, *previous_node;
previous_node = find_previous_node( key ) ; if (previous_node != NULL)
{ to_delete = previous_node->next; previous_node->next = to_delete->next; delete to_delete;
return 1; } return 0;
1;
Листинг 14.5. Функция поиска узла, содержащего ключ
//Эта функция находит в связанном списке элемент //"key" и возвращает указатель на узел, содержащий этот элемент. И the "key".
template <class Etype> node *
linked_list<Etype>::
find_node( const Etype S key) (
node *p ; p = list_head;
while ( p->next != NULL) { if (p->next->data == key ) {
return p->next; } p = p->next;
} return p->next; );
Листинг 14.6. Функция вставки ключа в связанный список
//Эта функция вставляет ключ в начало связанного списка.
template <class Etype> int linked_list<Etype>:: insert( const Etype S key) ( node *p ;
node *i = new node ( key, list_head->next);
if ( i == NULL ) { cout « "Error. Out of space"; return 0;
) p = list_head; p->next = i; return 1;
1;
Листинги 14.7—14.11 содержат определение таблицы open_hash и реализацию ее функций для вставки, поиска и удаления ключей.
Глава 14
Листинг 14.7. Определение класса таблицы open hash
//Класс open_hash_table можно определить следующим образом, template <class Element_Type> class open_hash_table (
private:
unsigned int tablesize:
linked_list<Element_Type> *lists;
open_hash_table( open_hash_table S value);
public:
open_hash_table( unsigned int sz = 11);
~open_hash_table( ) { delete [ ] lists; }
int search_key( const Element_Type * key );
void remove( const Element_Type S key );
void insert( const Element_Type S key );
};
Листинг 14.8. Функция инициализации класса open hash
//Эта функция инициализирует open_hash_table. template <class Element_Type> open_hash_table<Element_Type>:: open_hash_table( unsigned int sz ) < table_size = sz ;
lists = new linked_lists<Element_Type> [table_size]; if ( lists == NULL)
cout « "Error. Out of space"; };
Листинг 14.9. функция поиска ключа в таблице open hash
//Эта функция может использоваться для поиска ключа //в таблице открытого хеширования.
//Она возвращает ячейку, содержащую ключ. Эта функция //использует класс linked_list и его общедоступные методы.
template <class Element_Type> int
open_hash_table<Element_Type>::
search_key( const Element_Type * key ) (
unsigned int hash_value = hash( key, table_size );
if ( lists [hash_value] . find_node (key) ) {
bucket = hash_value;
return bucket;
)
return 0;
Листинг 14.10. функция удаления указанного ключа из таблицы open hash
//Эта функция может использоваться для удаления указанного ключа //из таблицы открытого хеширования.
//Она использует класс linked_list и его общедоступные методы, template <class Element_Type> void
Часть III
open_hash_table<Element_Type>:: remove( const Element_Type S key ) (
unsigned int hash_value = hash( key, table_size ); lists[hash_value].remove(key);
};
Листинг 14.11. Функция вставки указанного ключа в таблицу open hash
//Эта функция может использоваться для вставки указанного //ключа в таблицу открытого хеширования.
//Она использует класс linked_list и его общедоступные методы, template <class Element_Type> void
open_hash_table<Element_Type>::
insert( const Element_Type S key ) {
node *p ;
unsigned int hash_value = hash( key, table_size );
//Дубликаты не допускаются.
p = lists[hash_value].find_node(key);
if (p = NULL )
lists[hash_value].insert(key); else
cout « "Sorry. Key already exists";
};
Синтаксический разбор
Синтаксический разбор — это общая операция, которая позволяет определить правильность выражений и разбить их в форму, пригодную для дальнейшей обработки. Синтаксический разбор выполняется во многих различных сферах. Например, компиляторы выполняют синтаксический разбор для преобразования языка высокого уровня в язык низкого уровня или для интерпретации человеческого языка. В языках программирования для определения правильности выражений широко используется понятие "грамматика".
При синтаксическом анализе используются два подхода (их сравнительные характеристики приведены в табл. 14.4):
■ Нисходящий подход. Этот метод позволяет сначала рассмотреть программу и рекурсивно определить части, которые в конечном счете сопоставляются с входным выражением.
■ Восходящий подход. Этот метод дает возможность рассмотреть входное выражение и объединить части выражения для составления из него правильной программы.
Таблица 14.4. Сравнение подходов синтаксического разбора
Характера стика
Нисходящий подходВосходящий подход-
Подход
Позволяет разбить большие компоненты Позволяет объединить меньшие части для образования на более подходящие для дальнейшейболее крупных компонентов
обработки меньшие части
Реализация Эффективность Методы реализации
ПростаяСложная
ПлохаяХорошая
РекурсивныйИтеративный
Рассмотрим синтаксический разбор более подробно, используя структуру данных stack. Эта структура будет содержать только две операции: push() (проталкивание элемента на вершину стека) и рор() (выталкивание элемента с вершины стека). Реализация класса stack на языке C++ показана в листинге 14.12.
Глава 14
Листинг 14.12. Реализация класса stack и его методов
class stack {
private:
char * s tack_s tore ;
//"top" — это указатель на вершину стека int top;
public:
stack( int max_size=1000);
-stack( ) (delete stack_store;);
void push(char data); char pop ( ) ;
)
stack::stack( int max_size ) {
stack_store = new char[max];
top = 0; }
stack::push( char data ) (
stack[top++] = data;
}
char stack::pop().
{ return stack[--top];
}
Синтаксический разбор числовых выражений
Числовые выражения обычно представляются в инфиксной записи, в которой оператор находится между операндами и для указания порядка применения операторов при необходимости используются скобки. Числовые выражения лучше решаются с использованием постфиксной записи, в которой оператор находится после двух операндов, и скобки не нужны. Реализация стека, показанная в листинге 14.12, идеальна для решения таких числовых выражений. В табл. 14.5 приведено несколько примеров числовых выражений в инфиксной и соответствующей постфиксной записи.
Таблица 14.5. Преобразование инфиксных выражений в постфиксные эквиваленты
Инфиксное представлениеПостфиксное представление
5+454+
6 * (7 + 4 )6 7 4 + *
(7 + 8) * (2 - 9 ) + 17 8 + 2 9 - * 1 +
2 * ( (3 + 1) * 6)2 3 1+6**
Далее показан алгоритм, используемый при работе со стеками и постфиксными выражениями:
Просмотреть выражение слева направо.
Если встретился операнд, то поместить его на вершину стека.
Если встретился оператор, то вытолкнуть из стека два верхних элемента и применить к ним соответствующую операцию; результат поместить на вершину стека.
В листинге 14.13 показана реализация разбора числовых выражений с использованием класса stack.
Листинг 14.13. Реализация синтаксического разбора числовых выражений char с ;
stack num_stack(100); int sum;
int operl, oper2;
while ( cin.get(c) )■
Часть III sum = 0; while ( с = ' ') cin.get(c); if (с == ' + ' ) (
operl = num_s tack, pop ( ); oper2 = num_stack.pop( ), sum = operl + oper2;
1; if (c == •*' ) {
operl = num_stack .pop ( ) ; oper2 = num_stack .pop ( ) ; sum = operl * oper2 ;
}; if (c == '-' ) {
operl = num_s tack. pop ( ) ; oper2 - num_stack.pop ( ); sum = oper2 - operl ;
operl = num_stack .pop ( ); oper2 - num_stack.pop ( ); sum = oper2 / operl;
1;
while (c >=' 0' SS c<= '9') {
sum = 10*sum + (c-'O'); cin.get(c);
1;
num_stack.push(sum);
cout « "The result of the expression is " S sum ;
Синтаксический разбор строковых выражений
РИСУНОК 14.1. Дерево разбора для выражения A*((B+C)+D).РИСУНОК 14.1. Дерево разбора для выражения A*((B+C)+D).Синтаксический разбор строковых выражений — более сложный, чем разбор числовых выражений, и может быть представлен с помощью дерева разбора. Для выражения дерево разбора можно построить, используя следующее простое рекурсивное правило: сделать оператор корнем; дерево, соответствующее левому операнду (выражению), будет левой ветвью, а дерево, соответствующее правому операнду (выражению), --- правой ветвью. Применяя это простое правило, можно построить двоичное дерево разбора. На рис. 14.1 показано дерево разбора для выражения A*((B+C)+D).
РИСУНОК 14.1. Дерево разбора для выражения A*((B+C)+D).
РИСУНОК 14.1. Дерево разбора для выражения A*((B+C)+D).
Можно также создать дерево разбора для более сложных выражений, таких как выражения английского языка. В таком синтаксическом анализе строк используются более сложные методы, рассмотрение которых выходит за рамки этой книги. Объект stack, который использовался для разбора числовых вы
ражений, можно также использовать для анализа строк; отличие в том, что вместо промежуточных результатов мы сохраняем в стеке промежуточные деревья выражений. В листинге 14.14 показана реализация для A*((B+C)+D). Сначала мы преобразуем это выражение в его постфиксный эквивалент: А В С + D + *. Здесь используется постфиксный эквивалент выражения, потому что его проще реализовать в объекте stack.
Листинг 14.14. Реализация для А*((В+С)+Р) с использованием класса stack
struct node (
char data;char data;
char data;
char data;
Глава 14
struct node *left, *right;
);
stack string_stack(100);
char c ;
struct node *x, *dummy;
dummy = new node ;
dummy->left = dummy;
dummy->right = dummy;
while (cin.get(c))
{ while ( c=' ' ) cin.get(c); x = new node ; x~>data = c; x->left = dummy; x->right = dummy;
if ( c = ' + ' II c= '*’ ) { x->right = stack.pop( ); x->left = stack.pop( );
} stack.push(x);
Контекстно-свободная грамматика и синтаксический анализ
Контекстно-свободная грамматика часто используется для определения правильных конструкций. Она может использоваться для определения того, является ли данная строка символов правильной в некотором языке. Это может быть язык программирования или естественный язык (такой, как английский). Например, следующая запись может использоваться как контекстно-свободная грамматика, определяющая множество всех допустимых регулярных выражений:
<expression> : := <term> | <term> + <expression>
<term> : := <factor> | <factor> <term>
<factor> : := (<expression>) | c | (<expression>) * | c*
Каждая строка в описании грамматики называется порождающим правилом или правилом замены. Порождающее правило, или правило замены, состоит из следующих элементов:
Терминальные символы, такие как (, ), + и -, которые используются в языке.
Нетерминальные символы, такие как <expression>, <term> и <factor>, которые используются в грамматике.
Специальный символ "с", который представляет букву или цифру.
Метасимволы, такие как ::= (означает "это есть") и | (означает "или"), которые используются в грамматике.
Можно использовать дерево разбора для описания строковых выражений, применяя эти порождающие правила. Можно выполнить синтаксический разбор для определения того, можно ли получить данную строку, применив порождающие правила. Если строку нельзя породить, то она недопустима в языке, представленном грамматикой.
Пример дерева разбора для выражения А*В+АС показан на рис. 14.2.
Выполнение нисходящего синтаксического разбора для проверки правильности регулярных выражений
В методе нисходящего синтаксического разбора для распознавания строк используется рекурсия. В этом методе используется процедура для представления каждого из порождающих правил, определенных в грамматике. Таким образом, нетерминальные символы в левой части правил становятся именами процедур, а нетерминальные символы в правой части становятся рекурсивными вызовами. Терминальные символы используются для просмотра строки в процессе получения дерева разбора. Чтобы подробнее узнать о генерации компиляторов для таких регулярных выражений, обратитесь к книге Седгевика (Sedgewick) An introduction to the Analysis of Algorithms.
Часть III
РИСУНОК 14.2.
Дерево разбора для выражения
А*В+АС.
Анализируемое регулярное выражение можно представить в массиве р; индекс i будет представлять текущий анализируемый символ. Идея состоит в том, чтобы начать с i = 0; если при выполнении программы, реализующей анализатор, мы достигнем i = strlen(p), значит, строка допустима в языке. Программа должна быть способной обработать ошибки, а также предотвратить бесконечные циклы, вызванные неправильной рекурсией.
Реализация этой идеи показана в следующем псевдокоде:
for(i=0;i<strlen(р);i++)(
// Здесь следует программный код, реализующий синтаксический раэбор )
Резюме
Выбор наилучшего метода хеширования для конкретного приложения может оказаться очень сложным. Однако, когда вопрос объема памяти не стоит остро, вы обнаружите, что все методы хеширования, рассмотренные в этой главе, работают примерно с одинаковой производительностью. Если вы знаете размер множества ключей, то мы предлагаем использовать метод двойного хеширования. В противном случае лучше использовать метод связывания в цепочку. Имейте в виду, что следует также принять во внимание коэффициент загрузки, который является важной частью метода хеширования. Используемый метод проходит настоящую проверку только при больших коэффициентах загрузки; следует также тщательно обдумать размер связей, используемых в методе открытой адресации.
Вообще, хеширование лучше двоичного поиска, потому что хеширование проще реализовать и оно может обеспечить минимальное и почти постоянное время поиска (при наличии достаточного объема памяти для хранения большой таблицы хеширования). Метод двоичного поиска лучше использовать, если важен вопрос объема памяти или если размер множества ключей неизвестен заранее, потому что для двоичного поиска можно предсказать производительность худшего случая.
Методы синтаксического анализа широко используются компиляторами для интерпретации входных данных и преобразования их в другую форму, которая может интерпретироваться компьютером. При синтаксическом разборе используется нисходящий или восходящий подход; выбор подхода обычно зависит от конкретной грамматики. Числовые выражения обычно анализируются с использованием постфиксного представления; при анализе строк обычно используется дерево разбора для представления анализируемого выражения. Метод синтаксического разбора широко используется в искусственном интеллекте для интерпретации языков. Используя порождающие правила и грамматику, для конкретного языка можно создать дерево разбора выражений.
IVЧАСТЬIVЧАСТЬЖивучесть объектов и шифрование
IV
ЧАСТЬ
IV
ЧАСТЬ
15
Живучесть объектов
В ЭТОЙ ГЛАВЕ
Создание хранимых объектов
Кэширование
Подкачка данных на жесткий диск
Глава 15
Используя язык C++, можно манипулировать объектами в памяти. Временами, впрочем, будет появляться необходимость в сохранении этих объектов на жестком диске и восстановлении их в оперативной памяти позже. Сохранение объектов на жестком диске позволяет манипулировать большим количеством объектов, которые могут находиться и в оперативной памяти одновременно; также оно позволяет закрыть программу и продолжить ее работу в дальнейшем.
Производитель компилятора предоставляет объект ofstream, реализующий основные механизмы манипуляции файлами. В разрабатываемой вами программе может быть создан объект fstream, с помощью которого будут открываться файлы для чтения и записи данных в них. Впрочем, объекты fstream ничего не знают о ваших данных. Они слишком громоздки для чтения потоков символов, но ваши объекты часто бывают более сложными.
Вашей задачей является научить классы направлять данные на жесткий диск. Реализации этого механизма может быть осуществлена несколькими способами.
Итак, во-первых, уточните, будете ли вы в своих файлах сохранять объекты многих видов. Если вам придется считывать запись, не зная, что за тип объекта в ней содержится, то в таком случае в файле необходимо сохранить информации больше, чем это было бы в том случае, когда все записи в файле были бы одного типа.
Во-вторых, решите, собираетесь ли вы сохранять записи фиксированной длины. Если вы знаете, что следующий считываемый объект занимает например, 20 байтов, то у вас задача проще, чем если бы вы не знали точного размера объекта. В последнем случае вместе с объектом должна сохраняться и его длина.
В листинге 15.1 показана очень простая программа, которая открывает файл, сохраняет в нем некоторый текст, а затем опять открывает файл и считывает из него этот текст.
Листинг 15.1 Простая программа чтения/записи #include <fstream.h> void main() 1 char fileName[80]; char buffer [255]; cout « "File name: "; cin » fileName;
ofstream fout(fileName);// открыть для записи
fout « "This line written directly to the file...\n"; cout « "Enter text for the file: "; cin.ignore(1,'\n'); II "съесть" строку после имени файла cin.getline(buffer,255);// получить ввод пользователя
fout « buffer « "\n";// и записать его в файл
fout.close();// закрыть файл
ifstream fin(fileName);// повторно открыть для считывания
cout « "Here's the contents of the file:\n";
char ch ; while (fin.get(ch)) cout « ch;
cout « "\n*“End of file contents.***\n";
fin.close();II всегда закрывать по окончании работы
1
В этой программе ясно показаны основы механизма записи текста в файл. Вы создаете имя файла, открываете объект ofstream с этим именем и записываете в него с помощью cout. Считывать данные из файла вы можете, используя fin-способ, аналогичный cin-способу.
Создание хранимых объектов
Запись объектов, в отличие от встроенных типов, таких как символы, — задача намного более сложная, чем запись текста. Объектно-ориентированным решением этой дилеммы является обучение каждого объекта самостоятельно записываться на диск и самостоятельно считывать себя с диска. Для этого класс необходимо определить как хранимый, что можно сделать с помощью метода наследования.
Живучесть объектов и шифрование Часть IV
Заметим, что возможность хранимости не встроена в язык C++; эту возможность вы добавите к программе самостоятельно, создав абстрактный базовый тип Storable и реализовав все необходимые методы в хранимых подклассах. Поскольку хранимый базовый класс довольно прост, вы часто сможете прибегать к множественному наследованию — от настоящего базового класса и от хранимого.
Storable — это абстракция, поэтому один из его методов мы объявим как чисто виртуальный: class Storable // Абстрактный тип данных { public: Storable () {} Storable(Readers) (} virtual void Write(Writers)=0; private: }
Классы, наследуемые от Storable, должны реализовать метод Write, принимающий Writer в качестве параметра. Объекты Storable могут восстанавливаться с диска, принимая в параметре Reader. Вот объявления классов Reader и Writer: class Writer ( public: Writer(char *fileName):fout(fileName,ios::binary)(}; ~Writer() (fout. close ();} virtual Writers operator« (ints) ; virtual Writers operator«(char*) ;
private: ofstream fout; 1; class Reader < public: virtual Readers operator» (ints) ; virtual Readers operator»(char*S) ; Reader(char *fileName):fin(fileName,ios::binary)(} -Reader()(fin.close();) private: ifstream fin;
};
Задача Reader и Writer состоит в инкапсуляции ответственности за считывание и запись элементарных данных (таких, как целые, строки символов и др.) в место их долговременного хранения. Здесь мы реализовали только механизмы хранения int и char*. Возможно добавление double, float и других типов.
Чтобы увидеть эти классы в работе, сохраним на диске некоторые объекты, а затем считаем их. Начнем с создания класса Note, порожденного от Storable, как показано в листинге 15.2. Класс Note сохраняет некоторый текст, его длину и некоторую дополнительную информацию. Сейчас в качестве этой дополнительной информации мы сохраним два целых (reserved 1 и reserved!). В коммерческих приложениях эти переменные могут хранить дату создания и другие приемлемые данные.
Листинг 15.2. Note.h class Note : public Storable ( public: Note(const Strings text): itsText(text), reservedl(0L), reserved2(0L) {) Note(const char* text): itsText (text) , reservedl(0L), reserved2(0L) () Note::Note(Readers rdr) :itsText(rdr) (
Глава 15 rdr » reservedl; rdr » reserved2 ;
}
-Note () (}
const Strings GetTextOconst ( return itsText; } //...
void Write(Writers wrtr) (
itsText.Write (wrtr) ; wrtr « reservedl; wrtr « reserved2;
)
private:
String itsText;
int reservedl; int reserved2;
);
Текст, сохраняемый Note (itsText), имеет тип String. Это пользовательский тип, управляющий строками символов. Класс Note возлагает на Reader ответственность за создание reservedl и reservedl, которые мы передадим прямо в конструктор. Reader отвечает за reservedl и reservedl, поскольку те являются элементарными типами данных (целыми). Впрочем, класс Note просит объект String сконструироваться самостоятельно, поскольку тот является пользовательским типом. Для этого при инициализации класс Note передает объект Reader конструктору String:
Note::Note(Readers rdr) : itsText(rdr)
Поскольку класс Note унаследован от Storable, он имеет конструктор, принимающий объект Reader, а также должен переопределить метод Write(). Опять же, класс Note возлагает ответственность за запись переменной String на itsText (передавая ему объект Writer), но за запись двух целых здесь отвечает объект Writer.
Напомним еще раз, правило состоит в том, что элементарные (встроенные) типы, такие как целые и символы, управляются объектами Reader и Writer, но пользовательские типы, такие как String, должны самостоятельно читать и записывать свои данные.
Чтобы увидеть, как это делает String, мы должны рассмотреть его более подробно. String — это гибкий служебный класс, который вы должны написать самостоятельно, если в библиотеке STL или в вашей любимой библиотеке не было предоставлено такового. Реализация, приведенная в листинге 15.3, очень простая.
Листинг 15.3. String.h
class String : public Storable ( public:
// конструкторы String () ;'
String(const char *) ; String (const char *, int length); String (const strings); String(istreamS iff) ; String(Readers);
-String () ;
// помощники и манипуляторы int GetLength() const ( return itsLen; } bool IsEmptyO const ( return (bool) (itsLen = 0) ; ) void Clear();// установить строку нулевой длины
// функции доступа char operator[](int offset) const; chars operator[] (int offset) ; const char * GetStringOconst ( return itsCString; }
Часть IV
// операторы сбрасывания operator const char* () const { return itsCString; } operator char* () { return itsCString;}
// операторы
const Strings operator=(const Strings); const Strings operator=(const char *) ;
void operator+=(const Strings); void operator+=(char);
void operator+=(const char*);
bool operatore(const Strings rhs)const;
bool operator»(const Strings rhs)const; bool operator<=(const Strings rhs)const; bool operator>=(const Strings rhs)const; bool operator==(const Strings rhs)const; bool operator! =(const Strings rhs)const;
11 дружественные функции String operator*(const Strings); String operator*(const char*); String operator*(char);
void Display () const ( cout « itsCString « " "; } friend ostreams operator« (ostreams, const Strings); ostreams operator () (ostreams);
void Write(Writers) ;
private:
// возвращает 0 для нескольких, -1 — если меньше аргумента, // 1 — если больше аргумента int StringCompare(const Strings) const;
char * itsCString; int itsLen;
Блок реализации этого класса включает конструктор для Reader, а также перекрывает метод Write(), используя Writer:
String::String(Readers rdr)
(
rdr»itsLen ;
rdr»itsCString;
)
void String::Write(Writers wrtr)
{
wrtr«itsLen ;
wrtr«itsCString;
)
Обратите внимание на то, как String управляет считыванием и записью. Элементарные данные он поручает Reader и Writer. Если бы String имел в своем составе другие пользовательские объекты, он попросил бы их самостоятельно записаться и считываться; а те, в свою очередь, передали бы свои составные элементарные типы объектам Reader и Writer.
В этом есть некий смысл: все пользовательские типы состоят из некоторой смеси других пользовательских типов и элементарных данных. Рано или поздно все сведется к встроенным типам, а объекты Reader И Writer оснащены всеми средствами для манипулирования ими. Каждый пользовательский тип должен знать только свои составляющие; его задача состоит в правильном возложении ответственности на другие пользовательские типы или на объекты Reader и Writer.
Узнав об этих методах, мы готовы перейти к тестовой программе, приведенной в листинге 15.4.
Листинг 15.4. Программа драйвера для Reader и Writer
include "note.h" // см. листинг 15.2
#include <fstream.h>
И не функция-член!
Глава 15
void operator«(Writers wrtr, Notes note) {
note.Write(wrtr); ) const int howMany = 5; int main() { char fileName[80]; char buffer[255];// для ввода пользователя
cout « "File name: "; cin » fileName;
cin.ignore(1, '\n');// пропускается строка после имени файла
Writer * writer = new Writer(fileName);
Note* theNote;
for (long i = 0; KhowMany; i++) < cout « "Please enter a word: " ; cin.getline(buffer,255); theNote = now Note(buffer);
(♦writer) « *theNote; // записывает его в файл delete theNote;
) delete writer;
Reader * reader = new Reader(fileName);
cout « "Here are the contents of the file: \n" ;
for (i = 0; i<howMany; i++) <
theNote = new Note(*reader); // создает из файла cout « theNote->GetText()« endl;
delete theNote; )
cout « "\n***End of file contents.***\n"; delete reader; return 0;
}
Первое, что мы здесь видим, — глобально определенный оператор «, принимающий объекты Writer и Note. Такая перегрузка позволяет нам подражать формату cout (круглые скобки необходимы в связи с приоритетом оператора):
(♦writer) « *theNote; // и записывает его в файл
Эта простая программа драйвера начинается с запроса имени файла. Затем с использованием этого имени создается объект Writer. Конструктор Writer использует это имя для инициализации объекта fout, передавая ему полученное имя файла:
Writer(char* fileName): fout(fileName, ios::binary)();■
Затем мы входим в цикл, запрашивающий слова. Каждое слово используется для создания нового объекта Note:
Note (const char* text) : itsText(text) , reservedl (0L) , reserved2 (0L) {)
Текст инициализируется введенным пользователем словом, а целые переменные reservedl и reserved! устанавливаются нулевыми значениями. Writer записывает Note в файл:
(♦writer) « *theNote;
Взглянув внутрь этой функции, мы придем к глобальной функции, ранее нами определенной:
void operator«(Writers wrtr, Notes note) {
Часть IV note .Write (wrtr) ;
}
Этот программный код вызывает метод Write() класса Note. Взглянув на этот метод, мы увидим, что Note фактически возлагает задачу записи своего текста на класс String, тогда как запись целых возлагается на объект Writer. Никто не записывает длину строки, поскольку нет необходимости в сохранении этой информации для Note; при восстановлении класса Note мы просто вызовем strlen, как вы вскоре и увидите.
void Write(Writers wrtr) < itsText.Write(wrtr); wrtr « reservedl; wrtr « reserved2;
}
Рассматривая itsText. Write(wrtr), мы увидим, что String возлагает ответственность за запись длины и содержимого текста на Writer. Это критический момент: строка знает, что записать, a Writer знает, как это записать:
void String::Write(Writers wrtr) ( wrtr«itsLen ; wr tr«itsCString ;
)
Причина того, что класс Note не делает этого самостоятельно, состоит в том, что он не должен знать, что записывать, — это внутренняя деталь класса String. Все, что необходимо знать класс Note, — это то, что String является пользовательским типом и, следовательно, знает, как себя сохранить.
Метод Write() класса Note, указав объекту String записаться самостоятельно, указывает объекту Writer записать значение переменной reservedl:
void Write(Writers wrtr) { itsText.Write(wrtr); wrtr « reservedl; wrtr « reserved2;
}
Этот вызов выполняет оператор Writer:: operator«(int&);
Writers Writer:: operator« (intS data) {
fout.write((char*)Sdata,szlnt); return *this;
}
После того как будут сохранены текст и два зарезервированных поля, объект Note тоже будет сохранен. После записи всех слов мы больше не нуждаемся в объекте Writer и удаляем его. Прошло время, и теперь мы хотим прочитать сохраненные на диске объекты Note. Создается объект Reader, и мы входим в цикл, воссоздающий с диска пять (как минимум) объектов Note, что достаточно для получения представления об их содержимом.
Reader инициализируется тем же именем файла, что и Writer, и вызывается конструктор класса Note, принимающий Reader в качестве параметра:
theNote = new Note (*reader) ;
Конструктор класса Note в обратном порядке прокручивает процесс, с помощью которого Note был записан. Обратите внимание на инициализацию itsText; она должна быть осуществлена, прежде чем выполнится содержимое тела конструктора:
Note::Note(Readers rdr) : itsText(rdr) { rdr » reservedl; rdr » reserved2;
}
В процессе этой инициализации вызывается конструктор String, принимающий Reader в качестве параметра:
Глава 15
String::String(Readers rdr) { rdr»itsLen ; rdr»itsCString; )
Сначала строка считывает свою длину.
rdr»itsLen ;
Здесь вызывается оператор класса Reader », принимающий по ссылке itsLen. Переменная заполняется значением, сохраненным на диске:
Readers Reader:: operator»(intS data) ( fin.read((char*)Sdata,szlnt); return *this; )
To же самое он делает и для получения строки:
Readers Reader::operator»(char *S data)
int len; fin.read((char*) Slen,szlnt); data = new char[len+l];
fin.read(data,len);
data[len]='\0'; return *this; }
Обратите внимание на то, что Reader не использует длину, сохраненную объектом String. Длина строки действительно находится на диске. Когда Writer сохранял строку, в первых четырех байтах он записывал ее длину; String хранит эту избыточную информацию в качестве оптимизации для использования другими методами:
Writers Writer::operator«(char * data) { int len = strlen(data) ;
fout.write((char*)Slen,szlnt);// записываем длину
fout.write(data,len);//записываем данные
return *this;
}
Затем конструктор Note просит Reader восстановить поле reservedl: rdr » reservedl ;
Здесь опять вызывается оператор >>, читающий четыре байта в эту переменную:
Readers Reader::operator»(intS data) (
fin.read((char*)S data,szlnt) ; return *this;
)-
To же самое происходит и с reservedl. В конце выводится Note и уничтожается Reader.
Когда Reader и Writer будут полностью разработаны для поддержки всех встроенных типов, реализация сериализации ваших классов будет несложной. Для этого необходимо будет выполнить следующее:
Унаследовать от класса Storable.
Реализовать метод Write() и конструктор, принимающий Reader.
Возложить ответственность за элементарные типы на Reader и Writer.
Возложить ответственность за считывание и запись включаемых объектов Storage на них самих.
Манипуляция файлами
Хотя запись и считывание довольно эффективны для многих задач, иногда вам потребуется более изящный контроль над файлами. Чтобы рассмотреть эти вопросы, создадим В-дерево, которое используется для со-
Часть IV
хранения объектов Note в надлежащем порядке и для сохранения их на жестком диске в файле данных и индексах.
Что такое В-дерево
В-дерево было изобретено в 1972 г. Р. Байером (R. Bayer) и Е. Маккрайтом (E.McCreight) и с самого начала было предназначено для создания мелких деревьев для быстрого доступа к диску.
Мелкие деревья имеют мало уровней; продвинуться к цели по такому дереву можно, выполнив небольшое количество проходов. Поскольку поиск по дереву часто требует обращения к диску за необходимой информацией, производительность мелких деревьев может быть значительно выше производительности глубоких. В-деревья — это мощное решение проблемы дискового хранения; фактически каждая коммерческая система баз данных уже давно использует вариации В-деревьев.
В-дерево состоит из страниц. Каждая страница имеет набор индексов. Каждый индекс содержит значение ключа и указатель. Указатель в индексе может указывать либо на другую страницу, либо на данные, сохраняемые в дереве.
Таким образом, каждая страница содержит индексы, указывающие либо на другие страницы, либо на данные. Если индекс страницы указывает на другую страницу, такая страница называется узловой; если индекс указывает на данные, страница называется листовой (от слова "листва"). Максимальное количество индексов на странице называется порядком страницы.
Следовательно, каждая страница максимально может иметь количество дочерних страниц, равное ее порядку. Для В-деревьев существует правило: ни одна из страниц, кроме верхней и листовых, не может иметь индексов, количество которых меньше половины порядка (order/2). Листовая страница может иметь меньшее количество индексов(ог<1ег/2-1).
Новые индексы всегда добавляются в листовые страницы. Этот факт критический: вы никогда не добавляете индекс к узловой странице. Узловые страницы создаются В-деревом при разбиении существующих.
Вот как это работает. Предположим, что вы создаете В-дерево порядка 4 для сохранения слов. В целях упрощения примера ключом индекса будет само слово (т.е. мы не будем делать различий между ключами и данными).
В этом примере мы постепенно создадим дерево из слов "Four score and seven years ago, our fathers brought forth on this continent..." Когда дерево пусто, его корневой указатель никуда не указывает (рис. 15.1).
КореньКореньРИСУНОК 15.1. Пустое В-дерево.РИСУНОК 15.1. Пустое В-дерево.] страница 1] страница 1Первое слово — Four — добавляется в новую страницу, как показано на рис. 15.2. Эта новая страница является листовой, и индекс Four указывает на реальные данные.
Корень
Корень
РИСУНОК 15.1. Пустое В-дерево.
РИСУНОК 15.1. Пустое В-дерево.
] страница 1
] страница 1
Корень
(Лунные)
РИСУНОК 15.2. Листовая страница.
КоренКоренfour | score | seven [страница 1four | score | seven [страница 1Слова добавляются к странице до тех пор, пока их количество не станет равным порядку страницы (в этом случае четырем), к этому моменту страница будет заполнена (рис. 15.3).
Корен
Корен
four | score | seven [страница 1
four | score | seven [страница 1
РИСУНОК 15.3.
Заполненная листовая страница.
Когда придет время добавить слово years, страницу необходимо будет разбить для выделения места. Алгоритм следующий:
Разбить страницу пополам.
Добавить новое слово в подходящую позицию (в этом случае по алфавиту).
Если новое слово меньше первого (и, следовательно, должно быть добавлено в первую позицию), • установить соответственно указатель.'
Вернуть указатель на новую страницу.
Если корень определит, что требуется новая верхняя (узловая) страница, создать ее.
В новую верхнюю страницу добавить запись, на которую будет указывать корень.
В новой верхней странице добавить запись для возвращаемого значения шага 4.
Глава 15
Указать корню на новую верхнюю страницу.
В случае, показанном на рис. 15.4, добавляется слово years. Для этого страница должна быть разбита. Возвращается новая страница, и корневой указатель распознает, что требуется новая верхняя страница. В новую страницу вносится индекс, указывающий на запись (and), которую myRoot будет использовать для обращения к странице, и вторая запись, указывающая на новую страницу. Затем myRoot ссылается на эту новую узловую страницу.
РИСУНОК 15.4.
Разбиение страницы.
Корень—>•Корень—>•Следующим добавляемым словом является слово ago. Поскольку в алфавитном порядке оно выше слова and, то добавляется перед листовой страницей and, и узловая страница "исправляется", чтобы указывать на этот более ранний индекс, как показано на рис. 15.5.
Корень—>•
Корень—>•
РИСУНОК 15.5.
Исправление узла.
| ago | and | four | our | | score | seven | years"[
РИСУНОК 15.6.Готовность к разбиению страницы.РИСУНОК 15.6.Готовность к разбиению страницы.Когда страница заполнена (рис. 15.6), она разбивается, как показано на рис. 15.7, и к корневому указателю добавляется новый. Это продолжается до тех пор, пока не заполнится узловая страница (рис. 15.8).
РИСУНОК 15.6.
Готовность к разбиению страницы.
РИСУНОК 15.6.
Готовность к разбиению страницы.
РИСУНОК 15.7.
(Данные)(Данные)После разбиения страницы.
(Данные)
(Данные)
РИСУНОК 15.8.
Дерево с заполненной узловой страницей.
Добавление следующего слова new представляет проблему. Должна быть разбита первая страница, и, когда это происходит, она передает запись своему родительскому узлу. Этот узел, впрочем, заполнен и
Часть IV
также должен быть разбит. В такой ситуации корневой указатель должен распознать, что требуется новый узел (рис. 15.9).
РИСУНОК 15.9.
Третий ярус.
Запись В-дерева на диск
Общее предназначение В-дерева состоит в сохранении данных на диске. Каким-то образом В-дерево должно сохранить на диске страницы, индексы и данные. Одной из задач является кэширование.
Немного отклонимся от темы, чтобы поговорить о создании базы данных в реальном мире. По всей видимости, вот как вы будете программировать свою следующую базу данных: возьмете телефон, позвоните по 800 номерам магазинов "Товары-почтой" с названиями типа Programmer Credit-Card Heaven и спросите, есть ли у них самая быстрая и максимально надежная система баз данных, отвечающая вашим спецификациям.
Интерфейс вы напишете в MFC, ASP или какой-либо другой технологии, свяжете ваше приложение с базой данных через СОМ, обернув его в ODBC, DAO или другую технологию доступа к базам данных.
И только в самых редких случаях вы будете на самом деле "раскручивать" свою собственную базу данных "с нуля".
В наших целях создадим малый PIM (Personal Information Manager — персональный информационный диспетчер), который позволит создавать отметки (строки или символы) и сохранять их в базе данных. Несколько позже вы сможете запросить базу данных на наличие строки и найти каждый объект Note, содержащий такую строку. Эти объекты будут различной длины. В каждом из них мы сохраним дату, длину и набор символов, представляющих данные.
ПРИМЕЧАНИЕ
В этой упрощенной версии объектом Note будет строка символов. В более сложной реализации вы могли бы использовать класс Note для хранения этих строк и другой дополнительной информации о состоянии.
Индексный файл (WORD.IDX) будет состоять из страниц индексов, ведущих в конечном счете к листовой странице с индексом на искомое слово. Эта запись все еще не указывает на данные. Как-никак, одно слово может находиться во многих объектах Note. Мы создадим индексный файл, который будет содержать список указателей на записи в самой базе данных.
Все это основано на приложении Р1М, которое автор создал для редкой сейчас книги Teach Yourself More C++ in 21 Days. Здесь он обновил этот программный код и изменил его в соответствии с целями этой книги так, чтобы основное внимание обратить на живучесть объектов. Первый PIM был назван ROBIN (в честь первой дочери автора); этот он называет RACHEL (в честь второй).
Работает он следующим образом. Пользователь просит провести поиск по слову THE. Просматривается файл WORD.IDX, являющийся В-деревом, расположенным на диске. Записи индексов узловых страниц ведут к другим записям узловых страниц, и так происходит до тех пор, пока не будет найдена листовая страница. Число, записанное там, является смещением в файле RACHEL.IDX. Каждая запись в RACHEL.IDX указывает на запись в RECORD.DB, а также на следующую запись в RACHEL.IDX (или на NULL, если записей больше нет).
Глава 15
Кэширование
Вместо того чтобы считывать страницу в оперативную память при необходимости и затем удалять ее оттуда, намного эффективнее и удобнее хранить несколько страниц кэшированными в памяти. Индекс, впрочем, не будет отслеживать, где находится страница, на которую он указывает, — в оперативной памяти (когда указатель необходим) или на жестком диске.
Отслеживание местонахождения страниц (в оперативной памяти или на жестком диске) требует того, чтобы индекс хорошо знал, как страницы сохраняются на диске. Все, что должен знать индекс, — это то, что он указывает на страницу номер 5, например.
Нам необходим диспетчер дисков. Узловая страница передает этому диспетчеру значение смещения и получает в ответ указатель на требуемую страницу. Определение того, какая из страниц уже находится в оперативной памяти, а какая должна быть взята с жесткого диска, — это задача диспетчера дисков.
В простейшем случае диспетчер дисков будет хранить массив страниц. Когда массив переполняется, он удаляет страницу и на ее место помещает другую. Но какие из страниц необходимо удалить? Намного эффективнее удалить редко используемую страницу. Удаление страницы, используемой постоянно, просто приведет к тому, что вскоре она будет вновь загружена, что сведет на нет ваши попытки уменьшить количество обращений к жесткому диску.
Как же решить эту проблему? Для этого существует очередь типа "наименее недавно используемый". Очередь "наименее недавно используемый" — это очередь, в которой элементы размещаются в порядке их использования, при этом первым выдается элемент, который использовался последним; очередь "наименее недавно используемый"упорядочивает страницы в обратном порядке. Идея состоит в том, что если страница не была нужна какое-то время, то, вероятно, не понадобится и в ближайшее время.
Определение размера страниц
Цель заключается в быстром считывании и записи. Оказывается, большинство персональных компьютеров работают быстрее, если считывают блоки данных, размер которых кратен 2. Идеальный размер определяется размером сектора жесткого диска. Поскольку эта величина варьируется, вам будет необходима программа для получения этого размера из файла конфигурации, но сейчас автор использует размер 512 как предположительно разумный.
Каждая запись индекса должна быть делителем порядка, чтобы на каждой странице помещалось четное число. Длина индекса будет составлять 16 байтов: 4 байта на указатель (теперь смещение) и 11 байтов на данные, с завершающим строку байтом NULL. На странице размером 512 байтов существует 32 набора по 16 байтов, т.е. каждая страница может содержать 32 объекта индекса. Следовательно, порядок В-дерева составляет 32. 32-порядковое дерево может содержать 1024 слова в двух уровнях, 32768 слов в трех уровнях и 33554432 слова в пяти уровнях. В большинстве случаев поиск может завершиться за несколько обращений к жесткому диску, что идеально.
Определение количества страниц, которые
могут одновременно находиться в памяти
Алгоритм, используемый ранее для определения числа страниц, которые одновременно могут находиться в памяти, — это рекурсия, начинающаяся с верхнего узла и прокладывающая себе путь вниз по дереву. Поскольку каждая страница может содержать 32 индекса, то в любое время половина из них будет задействована: 16 индексов на страницу.
уровней страниц с 16 индексами на страницу предоставляют триллион ключей. 10 страниц, впрочем, занимают всего лишь 2 Кб памяти. (16 байтов на индекс умножить на 16 байтов на страницу и умножить на 10 страниц, равно 2560 байтов, или 2 Кб.)
Вот и мощь В-деревьев: постоянно занимая в памяти всего 2 Кб, они обеспечивают доступ к триллиону ключей!
Подкачка данных на жесткий диск
Самый быстрый способ свопинга (подкачки данных) на жесткий диск заключается в вызове memcpy(). Язык C++ унаследовал от С возможность при необходимости захватить память так, чтобы все индексы на странице могли были загружены с диска в оперативную память за один шаг.
При нахождении объекта Page в памяти вы можете обходиться с ним так, будто он содержит 4 объекта int и массив объектов Index. При получении объекта Page с жесткого диска вы будете работать с ним как
Живучесть объектов и шифрование
Часть IV
с блоками по 512 байтов. Для этого вы создадите объединение массива символов размером 512 байтов с другими переменными
Поскольку массив объектов, имеющих конструкторы, нельзя поместить в объединение, нужно создать массив символов с размером вашего массива объектов Index. Затем с помощью указателя на этот массив мы приведем типы к таким, которые нам необходимы. Это эффективный метод перемещения объектов в оперативную память и из нее. Он жертвует безопасностью типов ради скорости. Потеряв гарантию безопасности типов, вы рискуете написать ненадежный программный код, поэтому будьте внимательны и убедитесь в том, что знаете, что делаете.
Реализация В-дерева
Только что вы ознакомились с теорией. Практическая же реализация бывает несколько более сложной. В этом случае мы используем объектно-ориентированную технологию и один нехороший трюк.
При работе с этой сложной программой необходимы всего лишь два элемента: полный исходный программный код и после ознакомления со структурой кода от вас требуется его прокрутка в отладчике.
Автор не стал создавать код промышленной мощности, но упростил его, чтобы показать, как он работает, и, что более важно, чтобы показать живучесть объектов.
Главным объектом является ВТгее, что и показано в листинге 15.5.
Листинг 15.5. Объявление ВТгее
class ВТгее {
public:
// конструктор и деструктор
ВТгее () ;
-ВТгее () ;
// служебные функции
void AddKey(char* data, int offset);
bool Insert(char*);
void PrintTree() ;
int Find(char* data);
// методы страницы
Page* GetPagefint page)
( return theDiskManager. GetPage (page) ,- ) int NewPage(IndexS plndex, bool IsLeaf) { return theDiskManager.NewPage(plndex,false); }
static int myAlNum(char ch) ;
// общедоступный статический член!
static IDXManager theDiskManager;
static WNJFile theWNJFile;
static DataFile theDataFile;
static bool GetWord(char*, char*, ints) ;
static void GetStatsO;
static int NodelndexCtr;
static int LeafIndexCtr;
static int NodePageCtr;■
static int LeafPageCtr;
static int NodelndexPerPage[Order+1];
static int LeafIndexPerPage[Order+1];
private:
int myRoot;
1;
Задача ВТгее -- выступать в качестве точки входа в дерево и хранить некоторую статистическую информацию о дереве. Работа выполняется с помощью страницы и индекса, как показано в листинге 15.6.
Листинг 15.6. Объявление Раде
class Раде {
public:
Живучесть объектов
Глава 15 // конструктор и деструктор
Раде () ;
Page (char*) ;
Page(IndexS,bool);
Page(Index*, int, bool, int) ;
-Page(){} // операции вставки и поиска int Insert(IndexS, bool findOnly = false); int Find(Indexs idx) { return Insert(idx,true); } int InsertLeaf(IndexS); int FindLeaf(Index6,bool findOnly); int InsertNode(IndexS,bool findOnly = false); void Push(IndexS,int offset=0, bool=true);
// функции доступа Index GetFirstlndex() { return myKeys[0]; } bool GetlsLeafQ const ( return myVars.IsLeaf; } int GetCount() const { return myVars.myCount; } void SetCount(int ent) ( myVars.myCount=cnt; } time_t GetTimeO ( return myTime; ) bool GetLocked() ( return myVars.IsLocked; } void SetLocked (bool state) { myVars.IsLocked = state; ) // манипуляция страницей int GetPageNumber() const { return myVars.myPageNumber; } Page* GetPage(int page) ( return BTree::theDiskManager.GetPage(page) ; } int NewPage(IndexS rlndex, bool IsLeaf) ( return BTree::theDiskManager.NewPage(rlndex,false); ) int NewPage (Index* arr, int off,bool isL, int ent) { return BTree::theDiskManager.NewPage(arr,off,isL, ent); }
II служебные функции void Nullify(int offset); void Print(); fstreams Write(fstreams); void ReCount (); static int GetgPageNumber() { return gPage; ) static void SetgPageNumber (int pg) ( gPage = pg; } private:
Index * const myKeys; // будет указывать на myVars.mk union ( char myBlock[Pagesize];// страница с диска
struct < int myCount; bool IsLeaf; int myPageNumber;.
bool IsLocked; char mk[Order*sizeof(Index)];// массив индексов
)myVars; }; // только в памяти static int gPage; time_t myTime; // для очереди LIFO (последним вошел — первым вышел)
Для класса Page существует несколько конструкторов; мы их увидим в работе при подробном рассмотрении примера. Методы Insert(), Find() и другие предназначены для помещения индексов на страницы и для нахождения их позже. Отметим, что GetPage() является inline-функцией; она просто передает полномочия классу DiskManager, который обеспечивает быстрое считывание и чистую инкапсуляцию работы по кэшированию страниц в оперативной памяти, а также получения их с жесткого диска.
Живучесть объектов и шифрование Часть IV
Внимательно рассмотрите раздел private. Мы имеем объединение массива символов и структуры myVars, которая включает в себя переменную-счетчик, флаг (указывающий, листовая ли это страница), номер страницы, флаг блокировки и массив индексов. Индексы являются пользовательскими объектами, конструкторы которых принимают параметр. Впрочем, при создании массива пользовательских объектов каждый объект должен создаваться с помощью стандартного конструктора.
Что же мы делаем? Во-первых, хотим прочитать все эти индексы страницы за один раз и не можем использовать стандартный конструктор. Во-вторых, мы должны обмануть компилятор, сделав вид, будто вовсе не загружаем массив индексов, а просто загружаем массив символов. Вот почему mk объявляется как массив символов. Размер массива равен порядку Order (32), умноженному на размер одного индекса Index (16).
Мы также объявляем указатель на индекс: myKeys. Для указателя не нужен конструктор.
Нехороший трюк, о котором упоминалось несколько ранее, появляется в конструкторе Page. Мы устанавливаем указатель myKeys на первый байт шк. Для этого необходимо привести myVars.mk для указания на Index:
Раде::Раде(IndexS index, bool bLeaf) : myKeys((Index*)myVars.mk)
У нас есть указатель на массив индексов, и, как вы знаете, указатель может использоваться в точности как массив, т.е. позже можно написать следующее:
myKeys[i1.PrintKey();
Это классический пример обмена безопасности типов на производительность. Вы не будете делать это так часто. Попробуйте-ка сделать так в Java!
В листинге 15.7 приведено объявление класса Index.
Листинг 15.7. Объявление класса Index
class Index ( public: // конструктор и деструктор Index(); Index(char *) ; Index (char*, int) ; Index(IndexS);
-Index () {) 11 ^щвжазюи доступа const char * GetData () const { return myData; } void SetData (const IndexS rhs) ( strcpy (myData,rhs . GetData () ) ; } void SetData(const char * rhs) ( strcpy(myData,rhs); ) int GetPointer()const ( return myPointer; } void SetPointer (int pg) { myPointer = pg; ) // служебные функции void PrintKey(); void PrintPage();
int Insert(IndexS ref,bool findOnly = false);.
// перегруженные операторы int operator==(const IndexS rhs); int operator < (const IndexS rhs) (return strcmp(myData,rhs.GetData())<0;} int operator <= (const IndexS rhs) (return strcmp(myData,rhs.GetData())<=0;} int operator > (const IndexS rhs) (return strcmp(myData,rhs.GetData()) >0;} int operator >= (const IndexS rhs) (return strcmp(myData,rhs.GetData())>=0;) public:
Глава 15
int myPointer ;
char myData[Sizeitem - SizePointer];
Заметьте, вы можете сравнивать два индекса, в результате чего получаете сравнение данных, которым соответствуют эти индексы. Эта возможность позволяет поддерживать упорядоченность объектов. Более обобщенный подход состоит в хранении данных не как массива символов, а скорее, как параметризированного типа. В дальнейшем лучше было бы возложить ответственность за сравнение на сами объекты данных. Автор не сделал этого здесь, чтобы сохранить пример как можно более простым.
Обратите внимание также на то, что Sizeitem является константой, равной 16. (Прочитайте раздел "Определение размера страницы", чтобы увидеть, почему размер каждого элемента равен 16 байтам.) Мы вычитаем из него размер указателя (учитывая размер переменной-члена myPointer) для того, чтобы размер Index остался равным значению Sizeitem — 16.
В этой программе имеется три дополнительных класса: DataFile, DiskManager и WNJFile. Первый класс представляет собой реальную базу данных объектов Note. Каждый Note последовательно добавляется в дисковый файл, и при этом сохраняется его смещение:
class DataFile {
public:
// конструктор и деструктор
DataFile();
-DataFile () {}
// управление файлами
void Closed;
void Created;
void GetRecord(int, char*, int6, time_t6) ;
int Insert(char *) ;
private:
fstream myFile;
};
Класс DiskManager отвечает за управление страницами в памяти: кэширование наиболее недавно используемой, сохранение остальных страниц на диске и получение их при необходимости:
class DiskManager
{
public:
// конструктор и деструктор
DiskManager () ;
-DiskManager(){}
// управление файлами void Close (int) ;
int Create () ;
// манипуляция страницей
Page* GetPage (int) ;
void SetPage(Page*);
void Insert(Page * newPage);
void Save(Page* pg) ;
int NewPage(Index6, bool);
int NewPage(Index *array, int offset, bool leaf, int count);
private:
Page* myPages[MaxPages];
fstream myFile;
int myCount;
1;
Класс WNJFile (Word Node Join) отвечает за отображение ключей (Word) на данные (Note). Он позволяет находить каждое вхождение данного слова, а не просто объект Note, содержащий искомое слово:
class WNJFile
(
Часть IV
public: // конструктор и деструктор WNJFile () ;
-WNJFile(){} // управление файлами void Close();
void Create (); int* Find (int NextWNJ) ; int Insert(int, int); int Append (int) ;
private: static int myCount; fstream myFile; union { int myints[ 5 ] ; char myArray[5*szlnt];
}; };
Здесь вы видите массив пяти целых, о котором говорилось ранее. Первые четыре представляют смещение в файле данных, а пятый (если используется) является смещением следующего массива в WNJFile.
В последующих листингах приведен полный программный код этого примера, который позже будет рассмотрен более подробно. В листинге 15.8 приведен BTree.hpp, единственный header-файл для всей программы. Он содержит объявления всех классов, а также значения констант, используемых в программе.
Листинг 15.8. Из header-файла BTree.hpp
#ifndef BTREE_HPP// защита от повторного включения
#define BTREE_HPP
#include <time.h> #include <string.h> #include <fstream.h>
const int SIZE_TIME =
4;
const int SIZE_INT = const int Order =
4;
31;// 31 индекс и 1 заголовок
const int dataLen =
11;// длина ключа
const int MaxPages =
20;Ц больше, чем нам нужно
const int Sizeitem =
16;//Datalen + NULL
const int SizePointer =
SIZE_INT;II размер смещения
const int PageSize =
(Order+1) * Sizeitem;
const int N_DATA_SETS = 5;
const int DATA_SET_POINTER_OFFSET = N_DATA_SETS-1;
const int N_ITEMS_IN_HEADER = 2; const int MAX ARRAY SIZE = 256;
// упреждающие объявления class Page ;
class Index;
class DataFile ( public: // конструктор и деструктор DataFile () ;
-DataFile(){}.
// управление файлами void Close(); void Create(); void GetRecord(int, char*, int&, time_t&); int Insert(char *) ; private: fstream myFile;
Глава 15
class WNJFile ( public:
// конструктор и деструктор WNJFile () ;
-WNJFile () (}
// управление файлами void Close();
void Create ();
int* Find(int NextWNJ);
int Insert(int, int); int Append (int) ;
private:
static int myCount;
fstream myFile; union
( int myints[5]; char myArray[5*SIZE_INT];
); );
// DiskManager хранит информацию о том, какие страницы уже находятся в // оперативной памяти, и при необходимости перекачивает на жесткий диск class DiskManager
( public: // конструктор и деструктор DiskManager() ;
-DiskManager()(}
// управление файлами void Close (int);
int Create ();
// манипуляция страницей Page* GetPage (int) ;
void SetPage(Page*);
void Insert(Page * newPage);
void Save(Page* pg);
int NewPage(Index&, bool);
int NewPage(Index *array, int offset, bool leaf, int count); private:
Page* myPages[MaxPages];
fstream myFile; int myCount ;
};
// BTree имеет указатель на первую страницу class BTree
{ public: // конструктор и деструктор BTree() ;
-BTree () ;
// служебные функции void AddKey(char* data, int offset); bool Insert(char*);
void PrintTree(); int Find(char* data);
// методы страницы Page* GetPage(int page) { return theDiskManager.GetPage(page); }
Часть IV
int NewPage(IndexS plndex, bool IsLeaf) ( return theDiskManager.NewPage(plndex,false); )
static int myAlNum(char ch) ;
И общедоступный статический член!
static DiskManager theDiskManager;
static WNJFile theWNJFile;
static DataFile theDataFile;
static bool GetWord(char*, char*, ints) ;
static void GetStatsO;
static int NodelndexCtr;
static int LeaflndexCtr;
static int NodePageCtr;
static int LeafPageCtr;
static int NodelndexPerPage[Order+1];
static int LeafIndexPerPage[Order+1];
private:
int myRoot; );
// объекты индекса указывают либо на страницы, либо на реальные данные class Index { public:
// конструктор и деструктор Index();
Index(char *) ; Index (char*, int) ; Index (IndexS) ;
-Index () (} 11 функции доступа const char * GetData () const ( return myData; ) void SetData(const IndexS rhs) { strcpy(myData,rhs.GetData()); } void SetData(const char * rhs) ( strcpy(myData,rhs); } int GetPointer()const ( return mypointer; } void SetPointer (int pg) { myPointer = pg; } 11 служебные функции void PrintKeyO ; void PrintPage (); int Insert(IndexS ref,bool findOnly = false); // перегруженные операторы int operator==(const IndexS rhs);
. int operator < (const IndexS rhs) (return strcmp(myData,rhs.GetData())<0;} int operator <= (const IndexS rhs) {return strcmp(myData,rhs.GetData())<=0;} int operator > (const IndexS rhs) (return strcmp(myData,rhs.GetDataO)>0;} int operator >= (const IndexS rhs) {return strcmp(myData,rhs.GetData())>=0;) public:
int myPointer; char myData[Sizeitem - SizePointer];
};
// страницы состоят из заголовка и массива индексов class Page < public:
конструктор и деструктор
Глава 15
Раде () ; Page(char*); Page(Index&,bool); Page(Index*, int, bool, int); ~Page() () 11 операции вставки и поиска int Insert(IndexS, bool findOnly = false); int Find(Index& idx) ( return Insert(idx,true); } int InsertLeaf(IndexS); int FindLeaf(IndexS,bool findOnly); int InsertNode(IndexS,bool findOnly = false); void Push(Index&,int offset=0, bool=true); 11 ^укквут доступа Index GetFirstlndexQ ( return myKeys[0]; } bool GetIsLeaf() const ( return myVars.IsLeaf; ) int GetCount() const ( return myVars.myCount; ) void Setcount(int ent) { myVars.myCount=cnt; ) time_t GetTimed { return myTime; ) bool GetLocked() ( return myVars.IsLocked; } void SetLocked (bool state) ( myVars.IsLocked = state; ) 11 traMMXvyiuaiytsi страницей int GetPageNumber() const ( return myVars.myPageNumber; } Page* GetPage(int page) { return BTree::theDiskManager.GetPage(page); } int NewPage(IndexS rlndex, bool IsLeaf) { return BTree::theDiskManager.NewPage(rlndex,false); } int NewPage (Index* arr, int off,bool isL, int ent) ( return BTree::theDiskManager.NewPage(arr,off,isL, ent); } // служебные функции void Nullify(int offset); void Print () ; fstreamt Write(fstreams); void ReCount() ; static int GetgPageNumber() { return gPage; ) static void SetgPageNumber(int pg) { gPage = pg; } private: Index * const myKeys; H будет указывать на myVars.mk union < char myBlock[PageSize];M страница с диска
struct { int myCount; bool IsLeaf; int myPageNumber; bool IsLocked; char mk[Order*sizeof(Index)]; Ц массив индексов )myVars;
); 11 только в памяти static int gPage; time_t myTime; H для очереди LIFO (последним вошел — первым вышел)
#endif
Программа драйвера и вспомогательные функции находятся в файле Persist.cpp, приведенном в листинге 15.9.
Живучесть объектов и шифрование Часть IV
Листинг 15.9. Persist.cpp
♦include "btree.hpp"
♦include <Windows.h> ♦include <iostream.h> ♦include <stdlib.h>
H статические объявления DiskManager BTree::theDiskManager; DataFile BTree::theDataFile; WNJFile BTree::theWNJFile;
int WNJFile : :myCount =OL;
int Page::gPage =1;
int BTree::NodeIndexCtr =0;
int BTree::LeafIndexCtr =0;
int BTree:: NodePageCtr =0;
int BTree::LeafPageCtr =0;
int BTree::NodeIndexPerPage[Order+1]; int BTree::LeafIndexPerPage[Order+1];
// прототипы void parseCommandLines(char *buffer,int argc,char **argv); void Sb.owMenu (long*) ;
void DoFind(char*, BTreeS) ; void ParseFile(BTree&);
I/ программа драйвера int main()
(
BTree myTree;
for (int i = 0; i < Order +1; i++) (
BTree::NodeIndexPerPage[i] = 0; BTree::LeafIndexPerPage[i] = 0;
}
char buffer[PageSize+1];
bool fQuit = false;
while ( ! fQuit )
( cout « "?: "; cin.getline(buffer,Pagesize);
if ( buffer[0] == '-' ) ( switch (buffer[1]) ( case ' ? ' :
DoFind(buffer+2,myTree); break;
case ' ! ' : myTree.PrintTree(); break;
case ' F' : case ' f' :
ParseFile(myTree); break; case 'O': fQuit = true; break;
}
} else (
if ( myTree.Insert(buffer) )
Глава 15
cout « "Inserted.\n"; buffer[0] = *\0';
) ) return 0; )
II Ищем совпадения, показываем меню найденных вариантов, II где каждая запись пронумерована и датирована void ShowMenu(int ‘list) ( int j=0;
char buffer[PageSize+1] ; time_t theTime; int len; char listBuff[256]; struct tin * ts; int dispSize;
while (list[j] И j < 20) {
BTree::theDataFile.GetRecord(list[j],buffer,len, theTime); dispSize =min(len,32);
strncpy(listBuff,buffer,dispSize); if (dispSize = 32)
listBuff[29] = '.'; listBuff[30] = '.'; listBuff[31] = ) listBuff[dispSize]='\0'; ts = localtime(StheTime); cout « "[" « (j+1) « "] " ; cout « ts->tm_mon « "/"; cout « ts->tm_mday « "/"; cout « ts->tm_year « " " ; cout « listBuff « endl;
) 3 )
I/ обрабатываем команду -? II ищем совпадения, показываем меню, запрашиваем выбор, // выводим запись и опять выводим меню void DoFind(char * searchstring, BTreeS myTree)
И создаем массив всего набора смещений WNJ. // Это будет использоваться для вывода выборов и / / для нахождения самого текста int list[PageSize];
11 заполняем массив нулями for (int i = 0; i<PageSize; i++) list[i] = 0;
int k = 0;
char * pl = searchstring;
while (pl[0] == ' ') pl++;
int offset = myTree.Find (pl) ; if (offset) (
II получаем массив смещений из WNJFile int ‘found = BTree::theWNJFile.Find(offset); int j = 0 ;
13 Зак. 53
Часть IV
Н добавляем те, которых у нас пока нет,
for (;k < PageSize St found[j];j++,k++) { for (int 1=0; 1 < k; 1++) { if (list[l] == found[j]) continue;
) list[k] = found [j] ; } delete [] found;
)
cout « "\n";
if (!list[0]) { cout « "Nothing found.\n"; return;
}
ShowMenu(list);
int choice;
char buffer[PageSize]; int len;
time_t theTime;
for (;;) ( cout « "Choice (0 to stop) : " ; cin » choice; cin.ignore(PageSize,'\n'); if ( choice < 1 ) break;
BTree::theDataFile.GetRecord(list[choice-1],buffer,len, theTime); cout « "\n>> "; cout « buffer; cout « "\n\n"; ShowMenu(list);
) }
// открываем файл и создаем новый объект Note для каждой строки
// индексируем каждое слово в строке void ParseFile( BTreeS myTree) (
char fileName[256]; cout « "FileName: "; cin.getline(fileName,PageSize);
char buffer[PageSize];
char theString[PageSize];
ifstream theFile(fileName,ios::in );
if (!theFile) { cout « "Error opening input file " « fileName « endl; return;
)
int offset = 0;
for (; ;) ( theFile.read(theString,PageSize); int len = theFile.gcount(); if (lien) break;
Глава 15
theString[len]='\0';
char *pl, *р2, *рО;
рО = pl = р2 = theString;
while (pl[0] 66 (pl[O] = 1 \п1 || р1[0] = '\г')) р1++ ;
р2 = pl;
while (р2[0] 66 р2[0] •= '\п' 66 р2[0] ■= '\г') р2++;
int bufferLen = р2 - pl;
int totalLen = p2 - рО ;
if (•bufferLen) continue;
H Istrcpyn(buffer,pl.bufferLen);
strncpy(buffer,pl,bufferLen);
buffer[bufferLen]='\01;
II for (int i = 0; i< PageSize; i++) cout « "\r";
cout « "Parsing " « buffer;
myTree.Insert(buffer);
offset += totalLen;
theFile.clear();
theFile.seekg(offset,ios::beg);
) cout « "\n\nCompleted parsing " « fileName « endl;
)
В листингах 15.10—15.15 приведены файлы реализации различных классов.
Листинг 15.10. Btree.cpp •include "btree.hpp" •include <ctype.h> •include <stdlib.h> 11 создаем дерево 11 устанавливаем указатель myRoot в NULL 11 создаем либо считываем индексный файл BTree: :BTree() :myRoot (0) ( myRoot = theDiskManager.Create(); theDataFile.Create(); theWNJFile.Create();
} 11 записываем индексный файл BTree: :~BTree() (
theDiskManager.Close(myRoot);
theDataFile.Close() ; theWNJFile.Closed ;
)
// находим существукщую запись int BTree::Find(char * str) ( Index index(str); if (! myRoot)■
return 0L; else return GetPage(myRoot)->Find(index);
) bool BTree::Insert(char* buffer) (
Часть IV if (strlen(buffer) < 3) return false; char *buff = buffer; char word[PageSize] ; int wordoffset = 0; int offset; if (GetWord(buff,word,wordoffset)) ( offset = theDataFile.Insert(buffer); AddKey(word,offset);
} while (GetWord(buff,word,wordoffset)) ( AddKey(word,offset);
) return true ; ) void BTree::AddKey(char * str, int offset ) { if (strlen(str) < 3) return;
int retVal =0;
II создаем Index, где str — в верхнем регистре, II а смещение записано в Index.myPointer Index index(str,offset);
if (’myRoot) ( myRoot = theDiskManager.NewPage (index,true);
) else ( Page * pPage = GetPage(myRoot); retVal = pPage->Insert(index); if (retVal) J J корень разбивается ( I! создаем указатель на старую вершину pPage = GetPage (myRoot) ;
Index index(pPage->GetFirstIndex()) ; index.SetPointer(myRoot);
11 создаем новую страницу и вставляем индекс int PageNumber = NewPage(index,false);
pPage = GetPage(PageNumber); //получаем указатель на новую (соседнюю) страницу Page * pRetValPage = GetPage(retVal);
Index Sib(pRetValPage->GetFirstIndex()); Sib.SetPointer(retVal);
I / помещаем его в страницу pPage->InsertLeaf(Sib);
II устанавливаем myRoot на новую вершину myRoot = PageNumber;
) } }
void BTree::PrintTree() {
NodePageCtr =0 ;
LeafPageCtr =0;
Глава 15
NodelndexCtr =0;
LeafIndexCtr =0;
for (int i = 0; i < Order + 1; i++) { NodelndexPerPage[i] = 0; LeafIndexPerPage[i] = 0;
)
GetPage (myRoot) ->Print () ;
cout « "\n\nStats:" « endl;
cout « "Node pages:" « NodePageCtr « endl;
cout « "Leaf pages:" « LeafPageCtr « endl;
cout « "Node indexes: " « NodelndexCtr « endl; cout « "Total leaves: " « LeafIndexCtr « endl; for (i = 0; i < Order + 1; i++) ( if (NodelndexPerPage[i]) ( cout « "Pages with " « i « " nodes: "; cout « NodelndexPerPage[i] « endl;
}
if (LeafIndexPerPage[ i ]) { cout « "Pages with " « i « " leaves: "; cout « LeafIndexPerPage[i] « endl;
} }
bool BTree::GetWord(char* string, char* word, ints wordoffset) (
int i ; if (•string(wordoffset]) return false;
char *pl, *p2;
pl = p2 = string+wordOffset; И "съедаем" ведущие пробелы for ( i = 0; i< (int) strlen (pl) St ! BTree:: myAlNum (pl [ 0 ]) ; i++)
II смотрим, есть ли у нас слово if (!BTree::myAlNum(pl[0])) return false;
p2 = pl; II указатель на начало слова 11 p2 направляем в конец слова while (BTree::myAlNum(р2[0])) p2++ ;
int len = int (p2 - pl) ;
int pgSize = PageSize;
#if defined(_MSVC_16BIT) || defined(_MSVC_32BIT) {
len = min(len,(int)PageSize); ) #else {
len = min(len,(int)PageSize); } #endif
strncpy (word,pl,len); word[len]='\0';
for (i = int (p2-string) ;
Часть IV
i<(int)strlen(string) 66 IBTree::myAlNum(р2[0]); i++) p2++ ;
wordoffset = int(p2-string); return true;
}
int BTree::myAlNum(char ch) { return isalnum(ch) || ch == '-' || ch = -\" II ch == ' (' II ch = ') 1 ;
Листинг 15.11. Index.cpp
♦include "btree.hpp"
♦include Cotype.h>
Index::Index(char* str):myPointer(1) <
strncpy(myData,str,dataLen);
myData[dataLen]=' \0 1;
for (size_t i = 0; i < strlen(myData); i++) myData[ i ] = toupper(myData[ i ]) ;
)
Index::Index(char* str, int ptr) : myPointer (ptr) (
strncpy(myData,str,dataLen);
myData[dataLen]=’\0’;
for (size_t i = 0; i< strlen (myData) ; i++) myData[i] = toupper(myData[i]);
)
Index::Index(IndexS rhs): myPointer(rhs.GetPointer())
strcpy(myData, rhs.GetData()) ;
for (size_t i = 0; i< strlen(myData); i++) myData[i] = toupper(myData[ i ]) ;
)
Index::Index():myPointer(0) (
myData[0]='\0';
)
void Index::PrintKey() (
cout « "" « myData ;
}
void Index::PrintPage() {
cout « myData « ": " ;
BTree::theDiskManager.GetPage(myPointer)->Print(); )
int Index::Insert(Index6 ref, bool findOnly) (
return BTree::theDiskManager.GetPage(myPointer)->Insert(ref,findOnly); }
int Index::operator==(const Index6 rhs)
Глава 15
return (strcmp(myData,rhs.GetData()) == 0) ; // не учитывая регистр
Листинг 15.12. DataFile.cpp
#include "btree.hpp"
#include <assert.h>
11 при конструировании пытаемся открыть файл, если он существует DataFile::DataFile():
myFile("RACHEL.DAT",
ios: ."binary | ios::in | ios::out | ios::nocreate | ios::app) { }
void DataFile::Create() {
if (ImyFile) // noreate сработал — первое создание (
// открываем файл и создаем на этот раз myFile.clear();
myFile.open
("RACHEL.DAT" ,ios: ".binary | ios::in | ios::out | ios::app);
char Header[SIZE_INT];
int MagicNumber = 1234; // число для проверки
memcpy(Header,SMagicNumber,SIZE_INT);
myFile.clear();
myFile.flush();
myFile.seekp(0);
myFile.write(Header,SIZE_INT); return;
}
// мы открыли файл, он уже существует
// необходимо получить числа int MagicNumber;
myFile.seekg(O);
myFile.read((char *) SMagicNumber,SIZE_INT);
11 проверяем магическое число. Если оно неправильно, // то файл поврежден или это не индексный файл if (MagicNumber ! = 1234)
(
11 заменить на исключение! !
cout « "DataFile Magic number failed!"; } return;
}
II записать числа, которые понадобятся в следующий раз void DataFile::Close() (
myFile.close();
)
int DataFile::Insert(char * newNote) (
int len = strlen(newNote);
int fullLen = len + SIZE_INT + SIZE_TIME;
time_t theTime;
theTime = time(NULL);
char buffer[PageSize] ;
memcpy(buffer,Slen,SIZE_INT);
memcpy(buffer+SIZE_INT,StheTime,SIZE_TIME);
Часть IV
memcpy(buffer+SIZE_INT+SIZE_TIME,newNote,len);
my File. clear () ;
myFile.flush();
myFile.seekp(0,ios::end);
int offset = (int) myFile. tellp () ;
myFile.write(buffer,fullLen);
myFile . flush () ; return offset;
}
void DataFile::GetRecord(int offset, char* buffer, intb len, time_ts theTime)
( char tmpBuff[PageSize] ; myFile.flush();
myFile.clear();
myFile.seekg(offset);
myFile.read(tmpBuff,PageSize);
memcpy(&len,tmpBuff,SIZE_INT);
memcpy(&theTime,tmpBuff+SIZE_INT,SIZE_TIME);
strncpy(buffer,tmpBuff+SIZE_INT+SIZE_TIME,len);
buffer[len] = '\0';
}
Листинг 15.13. Page.cpp #include "btree.hpp" #include <assert.h>
конструкторы
// конструктор по умолчанию
Page::Page()
( }
// создать страницу из буфера, заполненного с жесткого диска Page::Page(char *buffer):
myKeys((Index*)myVars.mk)
(
assert(sizeof(myBlock) == PageSize);
assert(sizeof(myVars) = PageSize);
memcpy(SmyBlock,buffer.PageSize);
SetLocked(false);
myTime = time(NULL);
)
создать Page из первого индекса
Page::Page(IndexS index, bool bLeaf):
myKeys((Index*)myVars.mk)
{
myVars.myCount=l;
myVars.IsLeaf = bLeaf;
SetLocked(false);
11 Если это лист, то это первый индекс первой страницы.
// Установите его указатель, основанный на И создании нового wnj. В ином случае, // если вы создаете здесь новый узел, не устанавливайте И указатель, он уже установлен.
if (bLeaf) ( int indexPointer = index.GetPointer(); int appendResult = BTree::theWNJFile.Append(indexPointer); index.SetPointer(appendResult);
}
myKeys[ 0 ]=index;
myVars.myPageNumber = gPage++;
Глава 15
myTime = time(NULL); }
// создать страницу, разбив другую страницу
Page::Page(Index *array, int offset, bool bLeaf,int count): myKeys ((Index*)myVars ,mk)
{
myVars.IsLeaf = bLeaf;
myVars.myCount = 0 ;
for (int i=0, j = offset; j-COrder St i < count; i++, j++) {
myKeys[i]= array[j];
myVars.myCount++; ) myVars.myPageNumber = gPage++; SetLocked(false); myTime = time(NULL);
)
void Page::Nullify(int offset) {
for (int i = offset; i<Order; i++) {
myKeys[i].SetPointer(0); myVars.myCount—;
} )
И решить какой лист или узел, а
// передать этот индекс правой функции.
И Если findOnly истинно, то правильный возврат // номера страницы не вставляется int Page::Insert(Index& rlndex, bool findOnly) ( int result; if (myVars.IsLeaf) (
SetLocked(true); result = FindLeaf(rlndex,findOnly); SetLocked(false); return result;
} else { SetLocked(true); result = InsertNode(rlndex,findOnly); SetLocked(false); return result;
) )
11 найти правую страницу для этого индекса int Page::InsertNode(IndexS rlndex, bool findOnly) ( int retVal =0; bool inserted = false; int i,j;
assert(myVars.myCount>0); J J узлы имеют как минимум 1 assert (myKeys [0] .GetPointer ()) ; II RoiatM. быть корректной
// Выше ли она моей первой записи? if (rlndex < myKeys[0])
if (findOnly).
return 0L; J J не найдено
myKeys[0].SetData(rlndex);
retVal=myKeys[0].Insert(rlndex);
Часть IV inserted = true; } И Ниже ли она моей последней записи? if (!inserted) for (i = myVars.myCount-1; i>=0; i—) ( assert(myKeys[i].GetPointer()); if (rlndex >= myKeys[i]) ( retVal=myKeys[i].Insert(rlndex,findOnly); inserted = true; break;
} }
II определить, где она должна находиться if (!inserted) for (j = 0; j<i st j+1 < myVars.myCount; j++) ( assert(myKeys[j+1] .GetPointer ()) ; if (rlndex < myKeys[j+1]) { retVal=myKeys[j].Insert(rlndex,findOnly); inserted = true; break;
} )
assert(inserted);// заменить на исключение в случае неудачи!
// если вы имеете разделение if (retVal St !findOnly) J J получить обратно указатель на новую страницу ( Index * plndex = new Index(GetPage(retVal)->GetFirstIndex()); pIndex->SetPointer(retVal); retVal = InsertLeaf(*plndex);
} return retVal; }
II вызывается, если текущая страница листовая int Page::FindLeaf(Index& rlndex, bool findOnly) ( int result = 0;
J J без дубликатов! for (int i=0; i < myVars.myCount; i++) if (rlndex = myKeys[i]) ( if (findOnly)// вернуть первый WNJ
//вернуть BTree::theWNJFile.Find(myKeys[i].GetPointer()); return myKeys[i].GetPointer();
else return BTree::theWNJFile.Insert( rlndex.GetPointer(), myKeys[i].GetPointer());
if (findOnly)J J не найдено
return result;
II Этот элемент индекса пока не существует, // прежде чем поместить его в индекс, // поместите запись в wnj.idx // и установите индекс на эту запись. rlndex.GetPointer(BTree::theWNJFile.Append(rlndex.GetPointer() )) ; return InsertLeaf(rlndex);
Глава 15
int Page::InsertLeaf(Indext rlndex) { int result = 0; if (myVars.myCount < Order) Push(rlndex); else// страница переполнена
< // сделать соседа int NewPg =
NewPage(myKeys,Order/2,myVars.IsLeaf,myVars.myCount); Page* Sibling = GetPage(NewPg);
Nullify(Order/2);// обнулить правую половину
// помещается ли на этой стороне? if (myVars.myCount>Order/2-l 66 rlndex <= myKeys[Order/2-1]) Push(rlndex); else // поместить в нового соседа Sibling->Push(rlndex);
result = NewPg; // разбиваем страницу ) return result;
)
// помещаем новый индекс на эту страницу (в порядке) void Page::Push(Index6 rlndex,int offset,bool first) ( bool inserted = false; assert(myVars.myCount < Order);
for (int i-offset; KOrder 66 KmyVars.myCount; i++) (
assert (myKeys [i] . GetPointer ()) ; if (rlndex <= myKeys[i])
Push(myKeys[i],offset+1,false); myKeys[i]=rlndex; inserted = true; break;
)
if (!inserted) myKeys[myVars.myCount] = rlndex;
if (first) myVars.myCount++;
}
void Page::Print() (
if ('myVars.IsLeaf) <
BTree::NodePageCtr++;-
BTree::NodelndexPerPage[myVars.myCount]++; BTree::NodeIndexCtr+=myVars.myCount;
} else ( BTree::LeafPageCtr++; BTree::LeafIndexPerPage[myVars.myCount]++; BTree::LeafIndexCtr+=myVars.myCount;
for (int i = 0; KOrder 66 i < myVars.myCount; i++) (
assert(myKeys[i].GetPointer()); if (myVars.IsLeaf)
(
Часть IV
cout « "\nPage " « myVars.myPageNumber « ": "; myKeys[ i ] .PrintKey();
)
else (
cout « "\nPage " « myVars.myPageNumber « ": " ; myKeys[i].PrintPage();
}
} )
11 записываем всю страницу в виде блока fstream& Page::Write(fstreams file)
<
char buffer[PageSize];
memcpy(buffer,SmyBlock,PageSize);
file. flush () ; file.clear();
file.seekp(myVars.myPageNumber*PageSize);
file.write(buffer,PageSize); return file;
)
Листинг 15.14, WNY.cpp
#include "btree.hpp"
#include <assert.h>
// при конструировании пытаемся открыть файл, если тот существует
WNJFile: :WNJFile () :
myFile("RACHELWNJ.IDX", ios::binary | ios::in | ios::out | ios::nocreate)
for (int i = 0; i<5; i++) myints[i]=0L;
)
void WNJFile:-.Create()
{
char Header[2*SIZE_INT];
int MagicNumber=0; // число, которое мы можем проверить int zero = 0;
if (ImyFile) // сбой nocreate, первое создание файла <
// открываем файл и создаем его
myFile.clear();
myFile.open("RACHELWNJ.IDX", ios::binary | ios::in | ios::out) ;
MagicNumber = 1234;
memcpy(Header,SMagicNumber,SIZE_INT);
memcpy(Header+SIZE_INT,Szero,SIZE_INT);
myFile.seekp(0) ;
myFile.write(Header,2*SIZE_INT);
myFile.flush(); return;
)
II мы открыли файл, он уже существует // получить необходимые нам числа
myFile.seekg(O);
myFile.read(Header,2*SIZE_INT);
memcpy(&MagicNumber,Header,SIZE_INT);
memcpy(SmyCount,Header+SIZE_INT,SIZE_INT);
II проверяем магическое число. Если оно неправильно, / / то файл поврежден или это не индексный файл
Глава 15
if (MagicNumber != 1234) (
11 заменить на исключение!
cout « "WNJ Magic number failed!”;
cout « "Magic number: " « MagicNumber; cout « "\nmyCount: " « myCount « endl;
) return;
}
// записать числа, которые понадобятся в следующий раз void WNJFile::Close() {
myFile.seekg(SIZE_INT);
myFile.write((char*)SmyCount,SIZE_INT); myFile.close();
}
int WNJFile::Append(int DataOffset) (
int newPos = (N_ITEMS_IN_HEADER*SIZE_INT) H начальный заголовок + myCount++ * (N_DATA_SETS*SIZE_INT); // сколько наборов int offsets[N_DATA_SETS];
offsets[0] = DataOffset;
for (int i = 1; i<N_DATA_SETS; i++) offsets[i]=0;
myFile.seekg(newPos) ;
myFile.write((char*)offsets,N_DATA_SETS*SIZE_INT);
return newPos; }
int WNJFile::Insert(int DataOffset,int WNJOffset) {
int ints [5];
myFile.seekg(WNJOffset);
myFile.read((char*)ints,5*SIZE_INT);
int offset=WNJOffset;
while (ints[4]) <
offset = ints[4];
myFile.clear();
myFile.flush();
myFile.seekg(ints[4]) ;
myFile.read((char*)ints,5*SIZE_INT) ; ) if (ints[3]) И заполнен!
{
ints[4] = Append(DataOffset);
myFile.clear();-
myFile.flush(); myFile.seekg(offset);
myFile.write((char*)ints,5*SIZE_INT); } else ( for (int i = 0; i<4; i++) { if (ints[i] == 0) < ints[i] = DataOffset;
myFile.clear() ; myFile.flush(); myFile.seekg(offset);
myFile.write((char*)ints,5*SIZE_INT);
Часть IV
break;
)
}
return 0;
>
int* WNJFile::Find(int NextWNJ) (
int ints[N_DATA_SETS];
int * results = new int[MAX_ARRAY_SIZE] ;
int i = 0, j=0;
while (j<256) results[j++] = 0;
j = 0;
myFile.seekg(NextWNJ);
myFile.read((char*)ints,N_DATA_SETS*SIZE_INT);
while (j < MAX_ARRAY_SIZB) (
if (ints[i])
if (i = N_DATA_SBTS-1) {
myFile.seekg(ints[DATA_SBT_POINTER_OFFSBT]); myFile.read((char*)ints,N_DATA_SETS*SIZE_INT); i = 0;
continue;
} results[j++] = ints[i++); } else
break;
} return results;
Листинг 15.15. DiskManager.cpp
♦include "btree.hpp"
♦include <assert.h>
II при конструировании пытаемся открыть файл, если тот существует DiskManager::DiskManager():
myFile("RACHEL.IDX",ios::binary | ios::in | ios::out I ios::nocreate) {
// установить указатель в пустое значение
for (int i = 0; i< MaxPages; i+4-)
myPages[i] = 0;
myCount = 0;
}
II вызывается конструктором btree
11 если файл открыт, прочитать необходииые числа, // в ином случае создать новый файл int DiskManager::Create()
(
if (imyFile) J J сбой nocreate, первое создание файла <
// открываем файл и создаем его
myFile.open("RACHEL.IDX",ios::binary | ios::in | ios::out);
char Header[PageSize];
int MagicNumber = 1234; // число, которое мы можем проверить memcpy(Header,(MagicNumber,SIZE_INT);
Глава 15
int NextPage = 1 ;
memcpy(Header+SIZE_INT,SNextPage,SIZE_INT);
memcpy(Header*(2*SIZE_INT),SNextPage,SIZE_INT); Page::SetgPageNumber(NextPage);
char title[]="RACHEL.IDX. Ver 1.01";
memcpy(Header*(3*SIZE_INT),title,strlen(title));
myFile. flush () ;
myFile.clear();
myFile.seekp(0);
myFile.write(Header,PageSize); return 0;
)
И мы открыли файл, он уже существует // получить необходимые числа int MagicNumber;
myFile.seekg(0);
myFile.read((char *) SMagicNumber,SIZE_INT);
)I проверяем волшебное число. Если оно неправильно, то файл И поврежден или это не индексный файл if (MagicNumber ! = 1234)
(
И заменить на исключение!!
cout « "Index Magic number failed!"; return 0;
}
int NextPage;
myFile.seekg(SIZE_INT);
myFile.read((char*) SNextPage,SIZE_INT);
Page::SetgPageNumber(NextPage);
int FirstPage;
myFile.seekg(2*SIZE_INT);
myFile.read((char*) SFirstPage,SIZE_INT);
const int room = PageSize - (3*SIZE_INT) ; char buffer[room];
myFile.read(buffer,room);
buffer[20]='\0';
II cout « buffer « endl;
11 считать все страницы for (int i = 1; i < NextPage; i++) { myFile.seekg(i * PageSize); char buffer[PageSize];
myFile.read( buffer, PageSize); Page * pg = new Page (buffer) ; Insert (pg) ;
)
return FirstPage; }
II записать числа, которые понадобятся в следующий раз void DiskManager::Close(int theRoot) {
for (int i = 0; i< MaxPages; i++) if (myPages[i]) Save(myPages[ i ]);
int NextPage = Page::GetgPageNumber();
if (!myFile)
cout « "Error opening myFile!" « endl;
myFile.flush();
myFile . clear () ;
myFile.seekp(SIZE_INT);
myFile.write ( (char *) SNextPage,SIZE_INT);
myFile.seekp(2*SIZE_INT);
Часть IV
myFile.write((char*) StheRoot,SIZE_INT); myFile.close();
)
// функция оболочки int DiskManager::NewPage(IndexS index, bool bLeaf) ( Page * newPage = new Page(index, bLeaf); Insert(newPage); Save(newPage); return newPage-XSetPageNumber () ;
int DiskManager::NewPage( Index *array, int offset, bool leaf, int count) (
Page * newPage = new Page(array, offset, leaf, count) ; Insert(newPage);
Save(newPage);
return newPage->GetPageNumber () ; )
void DiskManager::Insert(Page * newPage)
// добавить новую страницу в массив диспетчеров страниц if (myCount < MaxPages) ( assert(!myPages[myCount]); myPages[myCount++] = newPage;
) else // нет места, пришло время вытолкнуть страницу на диск ( int lowest = -1;
for (int i = 0; i< MaxPages; i++) ( if (myPages [i]->GetLocked() == false ) lowest = i;
) if (lowest — -1) assert(lowest *= -1) ;
for (i = 0; i< MaxPages; i++) if (myPages [i]->GetTime () < myPages[lowest]->GetTime() St myPages[i]->GetLocked () = false) lowest = i;
assert(myPages[lowest]); Save(myPages[lowest]); delete myPages[lowest]; myPages[lowest] = newPage;
) )
// попросить страницу записаться самостоятельно void DiskManager::Save(Page* pg) < pg->Write(myFile);
}
// посмотреть, находится ли страница в памяти, если да — // вернуть ее, иначе получить с диска // примечание: при увеличении числа диспетчеров страниц // необходим более эффективный алгоритм поиска. 10 уровней // диспетчеров страниц с 31 индексом на страницу предоставляют И место для 800 триллионов слов. Даже если каждая страница заполнена
Глава 15
// в среднем лишь наполовину, 10 уровней глубины представляют 64 миллиона // ключей, не говоря уже о настоящих записях.
Page * DiskManager:.GetPage(int target)
for (int i = 0; i< MaxPages; i++)
< if (myPages[i]->GetPageNumber() = target) return myPages[i];
}
myFile.seekg(target * PageSize);
char buffer[PageSize];
myFile.read( buffer, PageSize);
Page * pg = aew Page (buffer) ;
Insert (pg) ;
return pg;
)
Как это работает
Лучший способ увидеть, как работает эта программа, — поработать с примером, в котором она используется. Сначала мы создадим текстовый файл из трех абзацев и назовем его test.txt, а затем запустим программу.
В приглашении ? введите We hold these truths to be self-evident that all men are created equal. После того как фраза будет принята, введите I regret that i have but one life to give to my country.
Теперь пришло время предоставить программе созданный нами тестовый файл. В приглашении введите -f и когда появится запрос имени файла, введите test.txt:
?: We hold these truths to be self-evident that all men are created equal Inserted.
?: I regret that I have but one life to give for my country Inserted. ?: -f FileName: test.txt Parsing Here you see the array of 5 integers as discussed earlier. The first fou Parsing the fifth <if used> is an offset back into the WNJFile for the next arra Parsing For demonstration purposes I've created a simple driver program which di Parsing and accepts input. Any text added is turned into a Note and stored in th Parsing characters or greater is indexed. If the user types a dash as the first Parsing <?>, bang <!> or the letter "f" the system treats it as a flag and takes Parsing a search, a bang forces a report of the structure of the tree and the le Parsing read into the system. When a file is read in, each line is treated as a Parsing extending this so that the user has the option that a file is considered Parsing imagine any number of improvements, starting with a more reasonable user Parsing _e_
Completed parsing test.txt ■> •
Пришло время увидеть, работает ли все это. Все ли строки захвачены? Был ли создан индекс? Один способ протестировать программу — поискать слово that:
?: -? that
7/6/98 We hold these truths to be se...
7/6/98 I regret that I have but one ...
7/6/98 a search, a bang forces a rep...
7/6/98 extending this so that the us...
7/6/98 extending this so that the us...
Choice <0 to stop>: 1
» We hold these truths to be self-evident: that all men are created equal
7/6/98 We hold these truths to be se...
7/6/98 I regret that I have but one ...
7/6/98 a search, a bang forces a rep...
7/6/98 extending this so that the us...
7/6/98 extending this so that the us...
Часть IV
Choice <0 to stop>: 2
» I regret that I have but one life to give for my country
7/6/98 We hold these truths to be se...
7/6/98 I regret that I have but one ...
7/6/98 a search, a bang forces a rep...
7/6/98 extending this so that the us...
7/6/98 extending this so that the us...
Choice <0 to stop>:
Ясно видно, что программа работает хорошо. Найдено пять объектов Note со словом that, и мы можем просмотреть их один за другим. Взглянем на структуру самой базы данных:
Page 2
PROGRAM
Page 2
PURPOSES
Page 2
QUESTION
Page 2
READ
Page 2
REASONABLE
Page 3
REGRET
Page 4
REGRET
Page 4
REPORT
Page 4
REPRESENT
Page 4
SEARCH
Page 4
SEE
Page 4
SELF-EVIDENT
Page 4
SIMPLE
Page 4
SINGLE
Page 4
SPECIAL
Page 4
STARTING
Page 4
STORED
Page 4
STRUCTURE
Page 4
SYSTEM
Page 4
TAKES
Page 4
TEXT
Page 4
THAT
Page 4
THE
Page 3
THESE:
Page 6
THESE
Page 6
THIS
Page 6
THREE
Page 6
TREATED
Page 6
TREATS
Page 6
TREE
Page 6
TRUTHS
Page 6
TURNED
Page 6
TYPES
Page 6
USED>
Page 6
USER
Page 6
WANTS
Page 6
WHEN
Page 6
WITH
Page 6
WNJFILE
Page 6
WORD
Page 6
YOU
Stats:
Node
pages:
1
Leaf
pages:
5
Node
indexes: 5
Total
leaves: 109
Pages
with
5 nodes:1
Pages
with
17 leaves: 1
Pages
with
18 leaves: 1
Pages
with
21 leaves: 1
Pages
with
22 leaves: 1
Pages
with
31 leaves: 1
Глава 15
Большая часть этой структуры прокручивается на экране, но мы можем увидеть, что страница 2 имеет несколько проиндексированных слов, а страница 3 имеет узловую запись со словом REGRET, указывающую на страницу 4. Страница 4 имеет 17 записей (как мы и рассчитывали) и начинается со слова Regret. Страница 3 также указывает на страницу 6 со словом These, а страница 6 имеет 18 записей и начинается с THESE.
Следующая статистика довольно интересна. Существует только одна узловая страница (которой должна быть страница 3) и пять листовых страниц (таким образом, последней страницей является страница 6, что мы и видим). Существует пять согласованных индексов страниц. Вместе эти пять узлов вмещают 109 листьев, распределенных следующим образом: первая страница имеет 17 листьев, вторая — 18, третья — 21, четвертая — 22, и последняя имеет 21 лист.
Таким образом, мы имеем всего два уровня. Верхний уровень (страница 3) — это узел, указывающий на пять страниц (страницы 1, 2, 3, 4 и 6). Эти пять ключевых страниц вместе содержат все 109 ключей. Запомните правило: страница должна иметь order/2 или более записей. Здесь порядок — 31, и мы можем рассчитывать увидеть как минимум 15 записей. Единственным исключением является верхний узел, и это имеет смысл, поскольку он создается при разбиении первой страницы.
Давайте пройдемся по коду, чтобы увидеть, как все это работает.
Прогулка по программному коду
В демонстрационных целях автор создал простую программу-драйвер, которая выводит пользователю знак вопроса и принимает ввод. Весь введенный текст переводится в объекты Note и сохраняется в базе данных. Индексируется каждое слово длиной более двух символов. Не индексируются одно- и двухсимвольные слова во избежание сохранения вездесущих слов типа as, of, or, and и др. Вы могли бы придумать более изощренный механизм, с помощью которого пользователь смог бы устанавливать "стоп-слова", игнорируемые системой.
Если в приглашении пользователь введет дефис (-) перед знаком вопроса (?), восклицательным знаком (!) или буквой f, эти команды будут пониматься системой как флаги и будет выполнено особое действие. Знак вопроса инициализирует поиск по базе, восклицательный знак служит для вывода отчета по структуре дерева, а буква f указывает, что пользователь хочет внести файл в систему. При этом каждой строке отводится отдельный Note. Вы могли бы расширить функциональные возможности так, чтобы пользователь имел опцию, при включении которой весь файл был бы занесен в один большой объект Note. Фактически вы могли бы придумать очень много усовершенствований, начав с более удобного пользовательского интерфейса.
Повторим еще раз, эта программа создана в демонстрационных целях и не является полнофункциональным диспетчером Personal Information Manager.
Программа начинается с блока main():
int main () ( BTree myTree;
for (int i = 0; i < Order +1; i++) ( BTree::NodeIndexPerPage[i] = 0; BTree::LeafIndexPerPage[i] = 0; }.
char buffer[PageSize+1];
ool fQuit = false;
while ( !fQuit ) { cout « "?: "; cin.getline(buffer,PageSize); if ( buffer[0] = ’-' ) ( switch (buffer[1]) ( case '?': DoFind(buffer+2,myTree); break;
Живучесть объектов и шифрование Часть IV case ' ! ' : myTree.PrintTree(); break;
case 'F' : case ' f' : ParseFile(myTree) ; break; case 'O': fQuit = true; break;
} } else ( if ( myTree.Insert(buffer) ) cout « "Inserted.\n"; buffer[0] = '\0';
) } return 0; )
Функция main() создает дерево и ожидает ввода пользователя. Если пользователь введет флаг, вызывается соответствующий (глобальный) метод (doFind() или ParseTree()). Если введен флаг с восклицательным знаком, то дереву посылается команда распечатать себя (таким образом выводится статистика).
Первая исполняемая строка программы такова:
ВТгее myTree ;
Здесь, конечно, вызывается конструктор ВТгее:
ВТгее::ВТгее():myRoot(0) ( myRoot = theDiskManager.Create(); theDataFile.Create(); theWNJFile.Create();
)
Член класса myRoot устанавливается в нуль, а затем ему присваивается значение, возвращаемое методом Create() объекта theDiskManager. Запомните, что theDiskManager, theWNJFile и theDataFile являются статическими. Это простая реализация единичной модели проекта (Singleton design pattern). Если бы эту программу автор создавал для коммерческого пользования, то сделал бы эти переменные приватными и более внимательно управлял бы временем их жизни. Опять же, для сохранения простоты примера эти члены представлены общедоступными.
Рассмотрев DiskManager.Create(), мы увидим, что первым делом проверяется, открыт ли файл для DiskManager. Поскольку мы впервые в этом коде, то файл пока не открыт, поэтому открываем его и подготавливаем к использованию. Первые PageSize символов имеют специфический формат, который мы сейчас и определим:
4-байтовое "магическое число", использующееся для проверки файла на корректность .
4 байта для "следующей страницы" (которые мы установим в 1) ■ 4 байта для "первой страницы" (которые мы также установим в 1) ■ Строка идентификации (которую мы установим в RACHEL.IDX Ver. 1.01) Все эти числа записываются, и функция возвращает значение 0, которое присваивается ВТгее::myRoot: int DiskManager::Create() ( if (imyFile) И сбой nocreate, первое создание файла { // открываем файл и создаем его myFile.open("RACHEL.IDX",ios::binary | ios::in | ios::out);
Глава 15
int NextPage = 1;
memcpy(Header+SIZE_INT,6NextPage,SIZE_INT);
memcpy(Header+(2*SIZE_INT),6NextPage, SIZE_INT); Page::SetgPageNumber(NextPage); char title[]="RACHEL.IDX. Ver 1.01";
memcpy(Headert(3*SIZE_INT),title,strlen(title));
myFile. flush () ; myFile.clear(); myFile.seekp(O); myFile.write(Header,PageSize) ; return 0;
) //...
}
Затем мы входим в метод Create() объекта theDataFile. Опять же, поскольку мы первый раз в этом программном коде, необходимо подготовить файл — на этот раз, создав только магическое число (никакой другой заголовочной информации не нужно) и записав его в файл:
void DataFile::Create() {
if ((myFile) H сбой nocreate, первое создание файла (
// открываем файл и создаем его myFile. clear () ;
myFile.open ("RACHEL.DAT",ios::binary | ios::in | ios::out | ios::app);
char Header[SIZE_INT];
int MagicNumber = 1234; // число, которое мы можем проверить memcpy(Header,SMagicNumber,SIZE_INT);
myFile.clear();
myFile.flush();
myFile.seekp(O);
myFile.write(Header,SIZE_INT); return;
) H
Затем то же самое мы делаем с WNJFile.
Вернувшись в main(), мы сбрасываем все счетчики ВТгее в нуль и устанавливаем несколько локальных переменных:
for (int i = 0; i < Order +1; i++) ( BTree::NodeIndexPerPage[i] = 0; BTree::LeafIndexPerPage[i] = 0;
}
char buffer[PageSize+1] ;
bool fQuit = false;
Теперь мы готовы к входу в цикл приглашения. Запомните, что при первом проходе кода пользователь ввел текст, поэтому флаги пропускаются и мы переходим к следующему коду:
if ( myTree.Insert(buffer) ) cout « "Inserted.\n";
Рассмотрев Insert(), мы убеждаемся, что в буфер было записано не менее трех символов. Затем мы проходим по буферу, извлекая из него слова. В результате весь буфер вставляется в файл данных, и каждое слово добавляется к индексу в виде ключа:
bool BTree::Insert(char* buffer) (
if (strlen (buffer) < 3) return false;
Живучесть объектов и шифрование Часть IV char *buff = buffer; char word[PageSize] ; int wordoffset = 0; int offset; if (GetWord(buff,word,wordoffset)) ( offset = theDataFile.Insert(buffer); AddKey(word,offset);
)
while (GetWord(buff,word,wordoffset)) ( AddKey(word,offset);
) return true;
}
GetWord() — это приватный (служебный) метод, принимающий массив символов (буфер), смещение этого буфера и заполняющий массив символов word следующим словом.
Рассмотрим theDataFile.Insert (buffer), чтобы увидеть, как текст добавляется к файлу данных:
int DataFile::Insert(char * newNote) { int len = strlen (newNote) ; int fullLen = len + SIZE_INT + SIZE_TIME; time_t theTime; theTime = time(NULL);
char buffer[PageSize]; memcpy(buffer,61en,SIZE_INT); memcpy(buffer+SIZE_INT,6theTime,SIZE_TIME); memcpy(buffer+SIZE_INT+SIZE_TIME,newNote,len);
myFile.clear(); myFile.flush(); myFile.seekp(0,ios::end); int offset = (int) myFile .tellp () ; myFile.write(buffer,fullLen); myFile . flush () ; return offset;
)
Этот программный код довольно прост. Длина устанавливается в длину самого Note и оставляется место для сохранения длины и текущего времени. Создается буфер, в который копируются длина и время, а затем и сам объект Note. Объект Note записывается в конец файла, и возвращается смещение.
Вернемся к BTree::Insert() и проследим за добавлением ключа:
void BTree::AddKey(char * str, int offset ) (
if (strlen(str) < 3) return;
int retVal =0;
Опять же, мы убеждаемся, что ключ имеет размер не менее трех букв.
Возвращаемое значение устанавливается в нуль. Далее строка (содержимое Note) и смещение передаются конструктору Index для создания нового объекта index:
Index index(str, offset);
Так мы попадаем в конструктор Index, где строка заносится в данные Index, а смещение используется для установки myPointer объекта index. Запомните, это смещение является смещением в файле Data. На самом деле это не то, что нам нужно сохранить в конечном счете, но это необходимо для начала.
Теперь сделаем краткий обзор: на самом деле мы хотим сохранить в WNJFile индекс, который, в свою очередь, будет указывать на смещение в файле данных. Вскоре мы увидим, как это работает. Заметьте так-
Живучесть объектов Глава 15
же, что значение ключа преобразуется в верхний регистр, который позволяет реализовать поиск независимо от регистра.
ПРИМЕЧАНИЕ
Альтернативный и, вероятно, более гибкий подход к хранению ключей состоит в хранении ключей в оригинальном формате и последующем сравнении ключей с вводом символов пользователя в верхнем регистре в случае, если пользе- ватель захочет провести поиск независимо от регистра:
Index: : Index (char* str, int ptr) :myPointer(ptr)
strncpy(myData,str,dataLen);
myData[dataLen]='\0';
for (size_t i = 0; i< strlen(myData); i++) myData[i] = toupper(myData[i]);s
После возвращения в BTree::AddKey() следующим этапом является опрос myRoot:
if (!myRoot)
myRoot = theDiskManager.NewPage (index,true); }
Ранее мы установили myRoot в нуль, поэтому вводим здесь оператор if:
myRoot = theDiskManager.NewPage (index,true);
Этот оператор выполняет метод NewPage() класса DiskManager, передавая ему только что созданный Index и флаг true. Флаг указывает, что эта новая страница действительно является листовой (этот метод также вызывается и при создании узла). Вот метод NewPage() в работе:
int DiskManager::NewPage(Indexs index, bool bLeaf) {
Page * newPage = new Page(index, bLeaf); Insert(newPage);
Save(newPage);
return newPage-PGetPageNumber(); )
Новая страница создана, помечена как листовая, вставлена в массив страниц DiskManager и записана на диск. Номер новой страницы возвращается классом ВТгее, и это значение присваивается переменной BTree::myRoot.
Рассмотрим этапы создания, вставки и записи новой страницы. Первым этапом является создание новой страницы:
Page::Page(Indexs index, bool bLeaf): myKeys((Index*)myVars.mk)
{
myVars.myCount=l;
myVars.IsLeaf = bLeaf; SetLocked(false);
/ / если это лист, первый индекс на первой странице, // установите указатель, основанный на новом wnj.
// Иначе вы, создавая новый узел, // не установите указатель, он всегда установлен. if (bLeaf) <
int indexPointer = index.GetPointer() ;
int appendResult = BTree::theWNJFile.Append(indexPointer); index.SetPointer(appendResult);
)
myKeys[0]=index;
myVars.myPageNumber = gPage++; myTime = time(NULL);
)
Живучесть объектов и шифрование Часть IV
Конструктор страницы начинается с установки переменной myKeys, как было рассказано ранее. Здесь устанавливаются несколько переменных-членов (например, страница устанавливается как листовая или как узловая, в зависимости от значения bLeaf) и разблокируются.
Запомните, что это конструктор. Мы знаем, что пока на этой странице не существует индексов. Если это лист, то необходимо вставить запись в WNJFile, если узел, то этого делать не надо. Подразумевая, что это лист, мы сначала извлекаем из индекса хранящийся в нем указатель (вы знаете, что индекс является смещением в файле данных). Затем этот указатель мы передаем методу WNJFile::Append() и результирующий указатель помещаем обратно в Index. Таким образом, Index больше не указывает на файл данных (это делает запись в WNJFile); теперь Index указывает на соответствующую запись в WNJ (Word-Node-Join).
Взглянем внутрь метода WNJFile::Append(), чтобы увидеть, как все это происходит:
int WNJFile::Append(int DataOffset) {
int newPos = (N_ITEMS_IN_HEADER*SIZE_INT) // начальный заголовок + myCount++ * (N_DATA_SETS*SIZE_INT); // сколько наборов int offsets[N_DATA_SETS];
offsets[0] = DataOffset; for (int i = 1; i<N_DATA_SETS; i++) offsets[i]=0;
myFile.seekg(newPos);
myFile.write((char*)offsets,N_DATA_SETS*SIZE_INT);
return newPos; )
Переменная newPos устанавливается на новое смещение в файле WNJFile. Прежде всего отводится место для двух целых заголовка, затем — для наборов данных. (Набор данных состоит из пяти целых.) Первые четыре целых указывают на записи в файле данных, пятое — на новый набор данных в WNJFile. Поскольку myCount первоначально устанавливается в нуль, сумма равна 8 байтам (что достаточно для заголовка).
Переменная offsets создана для хранения нового набора данных и инициализируется смещением данных для этого объекта Index. Остаток набора данных заполняется нулями. В заключение набор данных записывается на жесткий диск и конструктору Page возвращается значение newPos, где оно присваивается Index.
После возвращения в DiskManager: :NewPage() следующим этапом будет вставка только что созданной страницы в массив страниц DiskManager:
void DiskManager::Insert(Page * newPage) ( // добавить новую страницу в массив диспетчеров страниц if (myCount < MaxPages) { assert(’myPages[myCount]); myPages[myCount+t] = newPage;
) else // нет места, пришло время выталкивать страницу на диск < int lowest = -1;.
for (int i = 0; i< MaxPages; i++) { if (myPages[i]->GetLocked() == false ) lowest = i;
} if (lowest == -1) assert(lowest != -1) ;
for (i = 0; i< MaxPages; i++) if (myPages[i]->GetTime() < myPages[lowest]->GetTime() 66 myPages[i]->GetLocked() == false)
lowest = i;
assert(myPages[lowest]); Save(myPages[lowest]); delete myPages[lowestj;
Живучесть объектов Глава 15 myPages[lowest] = newPage; ')
)
Новая страница вставляется в массив страниц DiskManager, если для нее есть место в оперативной памяти. В ином случае дольше остальных не используемая страница удаляется из памяти, если она не заблокирована.
Заключительный этап: страница записывается на жесткий диск:
void DiskManager::Save(Page* pg) { pg->Write(myFile);
)
Когда ключ добавлен, BTree::AddKey() не увидит нуля в значении корня (myRoot) — тот будет указывать на уже созданную страницу:
if (’myRoot) (
myRoot = theDiskManager.NewPage (index,true); ) else
Page * pPage = GetPage(myRoot); retVal = pPage->Insert(index); if (retVal) H разделить корень (
И ) }
Корневая страница извлекается с помощью GetPage(), и ей сообщается о вставке нового индекса. Ответственность за получение страницы BTree::GetPage() возлагает на DiskManager:
( return theDiskmanager.GetPage(page); }
DiskManager, возможно, уже содержит эту страницу в памяти: в ином случае он берет ее с жесткого диска и заносит в свой массив страниц.
ПРИМЕЧАНИЕ
Несколько слов о терминологии. Объект, который мы назвали Index, должен, по всем соображениям, называться key — • файлы, хранящие ключи и связывающие их с записями, должны называться Index. Впрочем, страницы вызывают индексы ключей, что приводит к нескончаемой путанице в именах.
Далее Index в методе BTree::AddKey() добавляется к странице при вызове Page::Insert(), которой передается новый Index. Page::Insert() принимает два параметра, где второй по умолчанию установлен в значение false и показывает, что мы не ищем записи с переданным ключом, а добавляем ключ к индексу:
int Раде::Insert(Indexs rlndex, bool findOnly) (
int result;
if (myvars.IsLeaf) ( SetLocked(true); result = FindLeaf(rlndex,findOnly); SetLocked(false);
return result;
1 else ( SetLocked(true); result = InsertNode(rlndex,findOnly); SetLocked(false); return result;
)
Часть IV
Вид страницы (узловая или листовая) определяется переменной myVars. Если страница листовая, индекс добавляет указатель на данные. Страница заблокирована (чтобы не выйти из памяти!) и выполняется функцией FindLeaf(), которой передается индекс и флаг, указывающий, что это не запрос, а вставка:
int Page::FindLeaf(Index6 rlndex, bool findOnly) { int result = 0;
// нет дубликатов I for (int i=0; i < myVars.myCount; i++) if (rlndex == myKeys[i]) (
if (findOnly)// вернуть первый WNJ
(/вернуть BTree::theWNJFile.Find(myKeys[i]. GetPointer()); return myKeys [i] . GetPointer () ;
else return BTree::theWNJFile.Insert( rlndex.GetPointer(), myKeys[i].GetPointer() ) ;
}
if (findOnly)// не найдено
return result;
H Этот элемент индекса пока не существует, // прежде чем поместить его в индекс, Н поместите запись в wnj.idx и Н установите индекс на эту запись.
rlndex.SetPointer(BTree::theWNJFile.Append(rlndex.GetPointer())); return InsertLeaf(rlndex);
}
Мы проходим через индексы, существующие в странице, чтобы проверить, существует ли уже этот ключ. Если так, мы добавляем результат добавления этого смещения в WNJFile, создавая еще одно соединение между этим ключом и файлом данных.
Убедившись, что этот ключ не существует в Index, мы вставляем его:
rlndex.SetPointer(BTree::theWNJFile,Append(rlndex.GetPointer()));
Это тот же механизм, который мы рассмотрели ранее: получить указатель Index (указывающий на данные) и передать его WNJFile::Append(). Возвращаемое значение задайте указателю Index. На этот раз все три этапа мы скомбинировали в одной строке кода.
Теперь необходимо только добавить модифицированный индекс (с новым указателем на позицию в WNJFile) на эту страницу, вызвав InsertLeaf(), передав ей Index и вернув результирующее значение:
int Page::InsertLeaf(Index6 index) <
Page::InsertLeaf() начинается с проверки того, заполнена ли страница. Если нет, то Index мы помещаем на эту страницу:
if (myVars.myCount < Order) Push(rlndex);.
Если страница заполнена, мы должны ее разбить:
elseИ страница заполнена
int NewPg =
NewPage(myKeys,Order/2,myVars.IsLeaf,myVars.myCount);
Page* Sibling = GetPage(NewPg);
Nullify(Order/2);
if (myVars.myCount>Order/2-1 66 rlndex <= myKeys[Order/2-1]) Push(rlndex);
else Sibling->Push(rlndex); result = NewPg;
Глава 15
В двух словах, если мы должны разбить страницу, то мы берем правую половину страницы, помещаем ее в новую страницу и возвращаем результат, который является номером новой страницы. В ином случае мы возвращаем нуль, указывая, что разбиения не было.
В этом конкретном случае наша страница пока не заполнена, поэтому мы помещаем в нее новый индекс:
void Page::Push(Index6 rlndex,int offset,bool first) (
bool inserted - false;
assert(myVars.myCount < Order);
for (int i=offset; KOrder 66 KmyVars. myCount; i++) (
assert(myKeys[i].GetPointer());
if (rIndex <= myKeys[i]) {
Push(myKeys[i],offset+1,false);
myKeys[i]=rlndex;
inserted = true; break;
)
} if ((inserted) myKeys[myVars.myCount] = rlndex;
if (first) myVars.myCount++;
Проходя по ключам, мы ищем нужную позицию для нового. В случае вставки мы продвигаемся к следующему ключу, повторяя цикл столько раз, сколько необходимо для сортировки вставляемого ключа. Обратите внимание на рекурсию в этом алгоритме: при вставке мы должны сдвинуть следующий элемент в индексе. Когда все рассортируется, вставляется новый Index, а другие элементы на странице сдвигаются.
Поскольку, прежде чем начать, мы убедились, что у нас есть место для вставки (либо разбили страницу в случае нехватки места), количество рекурсий ограничено максимум до (order-1) повторов.
Поскольку мы не разбили страницу, в ВТгее возвращается нуль, указывая, что страница не была разбита и не должны производиться никакие корректировки. Рассмотрим, что же произойдет позже, когда мы разобьем страницу. Значение, возвращаемое функцией Page::Insert() функции BTree::AddKey(), должно быть ненулевым. Ненулевой результат указывает, что разбита корневая страница, в результате чего создается наша первая узловая страница:
if (retVal) // корневая страница разбита {
pPage = GetPage(myRoot);
Index index(pPage->GetFirstIndex());
index.SetPointer(myRoot);
int PageNumber = NewPage(index,false);
pPage = GetPage(PageNumber);
Page * pRetValPage = GetPage(retVal);
Index Sib(pRetValPage->GetFirstIndex()) ;
Sib.SetPointer(retVal);
pPage->InsertLeaf(Sib);
myRoot = PageNumber;
Первой задачей является создание нового объекта Index и направление его указателя на старую корневую страницу:
pPage = GetPage(myRoot);
Index index(pPage->GetFirstIndex());
index.SetPointer(myRoot);
Это сделано ловко. На первом этапе получается страница, указывающая на текущий корень. На втором этапе у этой страницы запрашивается firstlndex:
Index GetFirstlndex () (return myKeys[0];)
Часть IV
На третьем этапе выполняется конструктор Index, которому передается только что полученный Index:
ndex::Index(Indexs rhs): myPointer(rhs.GetPointer())
(
strcpy(myData, rhs.GetData()) ;
for (size_t i = 0; i< strlen(myData); i++) myData[i] = toupper(myData[i]);
}
Новый индекс инициализируется теми же данными и указателем, но его указатель мы устанавливаем на старую корневую страницу. Таким образом, теперь из корневой страницы мы сделали узловую и имеем указатель на нее.
Затем создаем новую страницу и передаем ей этот индекс, а назад получаем номер новой страницы. Этот номер страницы мы используем для получения указателя на новую страницу:
int PageNumber = NewPage(index,false); pPage = GetPage(PageNumber);
Далее из возвращаемого значения мы получаем указатель на страницу (т.е. только что разбитую страницу). Мы создаем индекс для указания на эту только что созданную страницу, который назовем Sib, и устанавливаем этот индекс на только что созданную страницу:
Page * pRetValPage = GetPage(retVal); Index Sib(pRetValPage->GetFirstIndex());
Sib.SetPointer(retVal);
Новый индекс (Sib) теперь вставляется в созданную нами новую (корневую) страницу:
pPage->InsertLeaf(Sib);
Затем myRoot мы устанавливаем в PageNumber новой корневой страницы.
КореньКореньСделаем краткий обзор. Прежде чем мы разобьем страницу, наше дерево будет выглядеть, как показано на рис. 15.10.
Корень
Корень
РИСУНОК 15.10.
Перед разбиением страницы.
КореньКорень№нные)№нные)score ||| страница 2страница 3I I I score seven I years I(№нные)score ||| страница 2страница 3I I I score seven I years I(№нные)Корневая страница была листовой, и все ее индексы указывали на данные. После разбиения наше дерево выглядит, как показано на рис. 15.11.
Корень
Корень
№нные)
№нные)
score ||| страница 2
страница 3
I I I score seven I years I
(№нные)
score ||| страница 2
страница 3
I I I score seven I years I
(№нные)
РИСУНОК 15.11.
После разбиения страницы.
Создана новая страница (страница 2), которая имеет два индекса. Первый указывает на старую страницу 1, а второй — на новосозданную страницу 3. Как же теперь добавляются данные?
Вызов функции BTree::AddKey() проходит как обычно, и она вызывает, в свою очередь, Page::Insert() корневой страницы. Корневая страница, теперь распознающая себя узлом (а на листом), вызывает функцию InsertNode(), передавая ей новый Index:
int Page::InsertNode(Index6 rlndex, bool findOnly) (
Здесь, опять же, retVal (возвращаемое значение) устанавливается в нуль, и локальный флаг inserted остается установленным в значение false. Прежде всего проверяется, выше ли индекс первой записи в узле. Если так, то мы устанавливаем данные первого ключа (MyKeys[0J) так, чтобы те указывали на новые данные из Index, и вставляем Index в страницу, на которую указывает myKeys[0]:
Глава 15
if (rlndex < myKeys[0]) ( if (findOnly) return OL; // не найдено
myKeys[0].SetData(rlndex);
retVal=myKeys[0].Insert(rlndex); inserted = true;
}
Если новый индекс не вставляется перед первым ключом, мы ищем нужную для него позицию в массиве и вставляем его на корректную страницу: if (!inserted) for (i = myVars.myCount-1; i>=0; i--) (
assert (myKeys [i] . GetPointer ()) ; if (rlndex >= myKeys[i]) {
retVal=myKeys[i].Insert(rlndex,findOnly); inserted = true;
break; )
)
В каждом случае от Insert мы получаем возвращаемое значение, и поэтому можем сказать, была ли разбита страница. Если значение ненулевое, вставка вызывает разбиение и мы должны создать индекс для управления новой страницей и вставить его в эту страницу. Опять же, это может привести к разбиению этой страницы, поэтому возвращаемое значение вставки передается обратно ВТгее:
if (retVal 66 !findOnly) // вернуть указатель на новую страницу (
Index * plndex = new Index(GetPage(retVal)->GetFirstIndex()); pIndex->SetPointer(retVal); retVal = InsertLeaf(*plndex);
)
Вот полный метод InsertNode():
int Page::InsertNode(Index6 rlndex, bool findOnly) (
int retVal =0; bool inserted = false; int i,j;
assert(myVars.myCount>0); H узлы имеют как минимум 1
assert(myKeys[0].GetPointer()); H должен быть корректен
// идет ли он перед моей первой записью? if (rlndex < myKeys[0]) ( if (findOnly) return OL; H не найдено
myKeys[0].SetData(rlndex);
retVal=myKeys[0].Insert(rlndex); inserted = true;
)
11 Ниже ли она моей последней записи? if (!inserted)
for (i = myVars.myCount-1; i>=0; i—) <
assert(myKeys[i].GetPointer()); if (rlndex >= myKeys[i]) ('
retVal=myKeys[i].Insert(rlndex,findOnly); inserted = true; break;
Живучесть объектов и шифрование Часть IV
}■
// определить, где она должна находиться if (!inserted) for (j = 0; j<i 66 j+1 < myVars .myCount; j++) { assert(myKeys[j+1].GetPointer()); if (rlndex < myKeys[j+1]) { retVal=myKeys[ j ] .Insert(rlndex,findOnly); inserted = true; break;"
} } assert(inserted);// заменить на исключение в случае неудачи!
И если вы имеете разделение if (retVal 66 IfindOnly) Н получит» обратно указатель на новую страницу ( Index * plndex = new Index(GetPage(retVal)->GetFirstIndex()); pIndex->SetPointer(retVal); retVal = InsertLeaf(*plndex);
) return retVal;
Поиск
Объекты Note помещаются в базу данных для того, чтобы их можно было найти позже. Цель этого приложения заключается в возможности поиска по любому из ключевых слов и нахождении каждого Note, содержащего это слово.
Предположим, что вы ввели такие фразы: We hold these truths to be self-evident that all men are created equal If we can send one man to the moon why can't we send all of them Now is the time for all good men to come to the aid of their country Ask not what your country can do for you ask what you can do for your country Теперь предположим, вы выполняете поиск по слову man с помощью команды -? шал. В результате получите следующий вывод: ?: -?man [1] 7/7/98 If we can send one man to the... Choice <0 to stop>: 1 » If we can send one man to the moon why can't we send all of them Только одна из этих фраз содержит слово man. Если мы будем искать слово теп, то обнаружим, что два объекта Note содержат это слово: ?: -?men [1] 7/7/98 We hold these truths to be se... [2] 7/7/98 Now is the time for all good ... Choice <0 to stop>: 2 » Now is the time for all good mean to come to the aid of their country [1] 7/7/98 We hold these truths to be se... [2] 7/7/98 Now is the time for all good ... Choice <0 to stop>: 1 » We hold these truths to be self-evident that all men are created equal [1] 7/7/98 We hold these truths to be se... [2] 7/7/98 Now is the time for all good ... Choice <0 to stop>:
Живучесть объектов Глава 15
В этом меню выводятся первые 32 символа каждого объекта Note; если выбрать один из них, будет выведен весь объект Note. Давайте разберем этот второй поиск шаг за шагом.
Когда функция main() обнаруживает дефис (-) как первый символ и ? — как второй, она вызывает функцию doFind(), передавая ей буфер, содержащий строку для поиска (в этом случае теп) и сам объект ВТгее. Строка анализируется для того, чтобы удалить предваряющие ее пробелы, и передается myTree::Find():
void DoFind(char * searchstring, ВТгее6 myTree) ( int list[PageSize]; for (int i = 0; KPageSize; i++) list[i] = 0; int k = 0; char * pl = searchstring; while (pl[0J == ' ') pl++ ;
int offset = myTree. Find (pl) ;
Функция BTree::Find() очень проста. Если страница назначена корню, она создает индекс из буфера и вызывает функцию Find() на корневой странице, передавая ей новосозданный индекс:
int ВТгее::Find(char * str) ( Index index(str); if (!myRoot) return 0L; else return GetPage(myRoot)->Find(index);
}
Функция GetPage() просто возвращает страницу. Page::Find() — это inline-функция, которая вызывает Insert(), передавая ей булевый флаг true и указывая, что это не настоящая вставка, а поиск:
int Find(Index6 idx) (return Insert(idx, true);}
Такой вариант повторного использования программного кода элегантен, но соглашение об именах, вероятно, не совсем удачно. Когда вы вызываете функцию Insert() со вторым параметром, установленным в значение true, то вовсе не вставляете, а просто ищете данные.
Функция Page::Insert() остается такой же, как была: она определяет, какая это страница — листовая или узловая, и вызывает либо InsertLeaf(), либо InsertNode() соответственно. Во всяком случае, она также передает флаг, указывающий, что мы выполняем поиск, а не вставку. Такая же логика, которая ранее использовалась для выявления "копий" ключей, применяется здесь для поиска смещения этого ключа для WNJFile.
Как вы могли догадаться, возвращаемое значение в конечном счете будет содержать смещение в WNJFile. Это значение передается обратно вверх к функции DoFind(). Затем оно используется в качестве параметра для вызова WNJFile::Find():
int offset = myTree.Find(pl); if (offset) (
int *found = BTree::theWNJFile.Find(offset);
Функция WNJFiIe::Find() просматривает свой массив смещений в файле данных. Для каждого набора данных она создает массив результатов. Когда (и если) она попадает на пятый элемент в своем наборе данных, то он используется как смещение в его собственном файле:
int* WNJFile::Find(int NextWNJ) ( int ints[N_DATA_SETS]; int * results = new int[MAX_ARRAY_SIZE] ; int i = 0, j=0; while (j<256) results[j++] = 0;
j = 0;
Часть IV
myFile.seekg(NextWNJ);
myFile.read((char*)ints,N_DATA_SETS*SIZE_INT);
while (j < MAX_ARRAY_SIZE) ( if (ints[i]) {
if (i == N_DATA_SETS-1) (
myFile.seekg(ints[DATA_SET_POINTER_OFFSET]); myFile.read((char*)ints,N_DATA_SETS*SIZE_INT); i = 0;
continue;
} results[j++] = ints[i++]; } else break;
} return results;
}
Затем выдается список отобранных Note, и пользователь может прочитать полный объект Note, выбрав его из списка.
Существует еще множество деталей, но мы представили необходимый программный код. Живучесть в этом случае более детализирована, чем в простом примере записи объекта на диск, хотя основные принципы передачи ответственности остались такими же.
К счастью, в коммерческих приложениях большинство этих тонкостей скрыто. Если вы захотите использовать структуру данных, такую как В-дерево, то, несомненно, сначала обратитесь к библиотеке Standard Template Library. Зачем заново изобретать велосипед, если можно использовать полностью готовый и тщательно протестированный программный код, который был обобщен для хранения практически любых видов данных?
Если вы собираетесь разрабатывать программы, использующие базу данных, то, несомненно, воспользуетесь одним из оптимизированных доступных коммерческих пакетов. Цель этих примеров заключается не столько в подготовке вас к написанию комплексных структур данных, сколько в рассмотрении деталей перемещения больших объектов с диска и на диск и трансляции объектов кучи в объекты на диск и обратно.
Резюме
В этой главе сделан краткий обзор нескольких объектно-ориентированных подходов к сохранению данных на жестком диске. Во всех этих несопоставимых подходах постоянной остается инкапсуляция хранимо- сти ответственных объектов, и создание невидимыми для клиентов объектов размещения (в оперативной памяти или на жестком диске).
В развитых системах, таких как CORBA и СОМ, клиент не видит не только местонахождение объекта, но и место его хранения. Эта возможность известна как прозрачность размещения и рассматривается в следующих главах.
Реляционные базы данных и живучестьРеляционные базы данных и живучестьИ Основные концепции реляционных баз данных
Реляционные базы данных и живучесть
Реляционные базы данных и живучесть
й Архитектура реляционных баз данных
И Язык структурированных запросов SQL
В Живучесть для реляционных баз данных
В Скрытие деталей
К Операторы SQL
Часть IV
Подавляющее большинство приложений коммерческого программного обеспечения используют реляционные базы данных (БД) для реализации живучести объектов. Реляционные БД — это развитая технология, тогда как объектные БД все еще находятся на стадии становления. Кроме того, большинство бизнес-приложений содержат унаследованный программный код и дизайн; компания хранит свои данные в реляционной БД, и поэтому все подразделения компании должны продолжать использовать эти данные.
Такие обстоятельства ставят перед нами сложную проблему, поскольку отображение связи объектов — это не всегда простое дело. И хотя существует множество решений этой задачи, большей частью вы будете просто позволять среде приложения заниматься отображением за вас.
Эта глава начинается с краткого обзора основ управления реляционными БД и продолжается дискуссией о том, как программы на языке C++ могут взаимодействовать с реляционными БД. В этой главе будет рассмотрена поддержка БД в библиотеке Microsoft Foundation Class (MFC).
Основные концепции реляционных баз данных
Сущность реляционной БД довольно проста: все данные организованы в таблицы. Каждая таблица состоит из записей (строк) и полей (столбцов), как показано на рис. 16.1.
РИСУНОК 16.1.Реляционные базы данных организованы в записи и поля.РИСУНОК 16.1.Реляционные базы данных организованы в записи и поля.AuthorAddressCityStateZipJohn Doe100 Main StreetAnytownAnyState01320Jane Writer50 Apple WaySomeCityST09939Jack Sprat100 CandlestickHotTownXY99939AuthorAddressCityStateZipJohn Doe100 Main StreetAnytownAnyState01320Jane Writer50 Apple WaySomeCityST09939Jack Sprat100 CandlestickHotTownXY99939
РИСУНОК 16.1.
Реляционные базы данных организованы в записи и поля.
РИСУНОК 16.1.
Реляционные базы данных организованы в записи и поля.
Author
Address
City
State
Zip
John Doe
100 Main Street
Anytown
AnyState
01320
Jane Writer
50 Apple Way
SomeCity
ST
09939
Jack Sprat
100 Candlestick
HotTown
XY
99939
Author
Address
City
State
Zip
John Doe
100 Main Street
Anytown
AnyState
01320
Jane Writer
50 Apple Way
SomeCity
ST
09939
Jack Sprat
100 Candlestick
HotTown
XY
99939
Мощь реляционных БД проявляется в том случае, когда вы создаете две таблицы и связываете их. Такую связь можно создать при условии, что для каждой записи существует совместно используемый уникальный идентификатор. Например, если одна таблица содержит список авторов, пишущих для вашего издательства, а вторая — список публикуемых книг, то две эти таблицы можно связать по автору. Один из способов, позволяющий сделать это, — задать каждому автору уникальный идентификатор authorlD и добавить поле таких идентификаторов в таблицу Titles.
Из рис. 16.2 видно, что каждый автор имеет уникальный идентификатор. Этот идентификатор появляется также и в таблице Titles, связывая обе эти таблицы и позволяя увидеть, что книги What A Wonderful Program и Another Wonderful Program были написаны автором, номер которого 102. Автором с номером 102 является Jane Writer, который живет по улице 50 Apple Way в SomeCity, ST.
Первичный ключ
Author ID
Author
Address
City
State
zip
101
John Doe
100 Main Street
Anytown
AnyState
01320
102
Jane Writer
50 Apple Way
SomeCity
ST
09939
103
Jack Sprat
100 Candlestick
HotTown
XY
99939
РИСУНОК 16.2.
Каждый автор имеет уникальный идентификатор.
Title ID
Title
Description
AuthorlD
1001
Teach Yourself C++ In 21 Days
Primer on C++
101 .
1002
What A Wonderful Program
Intro to programming
102
1003
Another Wonderful Program
Sequel to What A Wonderful...
102
1004
You Gotta' Be Quick
Nursery rhymes for programmers
103
Поле AuthorlD уникально идентифицирует каждого автора в таблице Author и поэтому является первичным ключом этой таблицы. Когда ключ появляется в таблице Titles, он создает связь между таблицами, таким образом, он является внешним ключом таблицы Titles. Внешний ключ — это просто ключ, который служит первичным ключом в какой-либо другой таблице.
Это прекрасная идея, к которой мы вернемся вскоре. Такие таблицы, как мы могли предположить, могут быть довольно большими. Поиск отдельного автора будет занимать некоторое время. Для ускорения поиска столбцы могут быть проиндексированы. При создании индекса система создает второй файл, сортируемый по индексируемому значению. Второй файл предоставляет указатель на оригинальный файл. В большинстве случаев этот указатель является обычным смещением записи в файле, измеряемым в количестве записей или в байтах.
Глава 16
Таблица всегда индексируется по своему первичному ключу. Вы также могли бы проиндексировать любое другое поле, чтобы ускорить поиск записи, основанный на информации, содержащейся в этом поле. Например, вы могли бы проиндексировать поле не только по AuthorlD (первичный ключ), но и по имени автора. Проиндексировав таблицу по имени автора, вы сможете пройти по индексу Author Name, выискивая нужную запись, а затем использовать смещение для перехода к этой записи в файле данных:
ключ)
Author
Address
City
State
Zip
101
John Doe
100 Main Street
Anytown
AnyState
01320
102
Jane Writer
50 Apple Way
SomeCity
ST
09939
103
Jack Sprat
100 Candlestick
HotTown
XY
99939
104
Jesse Liberty
1 Fake Street
Boston
MA
01297
105
Ernest Hemming
50 Famous Dr.
NY
NY
11209
AuthorlD (Первичный
Author
Offset
John DoeО
Ernest Hemming4
Jesse Liberty3
Jack Sprat2
Jane Writer1
Использовать индексы очень удобно, поскольку индекс может быть найден очень быстро путем бинарного поиска (который описан в главе 13). С увеличением файла данных количество необходимых записей увеличивается очень медленно. Чтобы найти запись в файле, содержащем и записей, необходимо просмотреть в среднем log 2(п) записей. Если, с другой стороны, просматривать каждую запись в оригинальной БД, потребуется просмотреть в среднем п/2 записей.
В базе данных, содержащей 65536 (п) записей, последовательный поиск приведет к просмотру в среднем 32768 записей (п/2), тогда как при бинарном поиске будет просмотрено всего лишь 16 записей (log 2(п))! Учтите также, что поиск каждой из 32768 записей должен выполняться из большого блока данных, а поиск этих 16 записей будет производиться из намного меньших блоков. Производительность несравнима; индексы очень эффективны.
Конечно, ничего не бывает бесплатным. При использовании индексов увеличивается время добавления записей в базу данных. Каждая запись должна быть проиндексирована, и это требует времени. Кроме того, каждый индекс занимает дисковое пространство и оперативную память.
После того как данные найдены в индексном файле, используется указатель. Указатель — это просто смещение в файле данных. Операционная система предоставляет очень быстрый доступ при перемещении к позиции файла по указанному смещению.
Архитектура реляционной базы данных
Базы данных могут быть представлены тремя уровнями. Первый — это представление данных в приложении, включая специфическую для приложения семантику и ограничения. На этом уровне мы вовсе не говорим о моделировании данных. Мы говорим об объектах, сущностях, правилах и т.д.
Следующий уровень — это уровень моделирования взаимосвязей сущностей. При таком взгляде мы думаем о таблицах, строках, столбцах и индексах. На этом уровне необходимо понимать, как отношения между различными объектами домена транслируются в таблицы и их взаимосвязь.
Самым фундаментальным является, конечно, физический, или внутренний уровень, который работает с файлами на жестком диске. На этом уровне вы практически не заботитесь о БД, если только не создаете свою собственную технологию БД. На этом уровне рассматривается размер записей в файле, блоки доступа к данным, методы доступа к жесткому диску и т.д.
Любая конкретная база данных имеет трех пользователей:
Конечного пользователя, который думает только об объектах и правилах.
Программиста, который отвечает за разработку приложений, используемых конечными пользователями, и думает только об объектах и правилах, а также о моделировании связи сущностей.
Часть IV
Администратора базы данных, который определяет физические компоненты, а также должен понимать связи сущностей. Кроме того, он должен знать, как достичь максимальной производительности.
Ограничения и соображения
Реляционные БД работают лучше всего при соблюдении нескольких следующих правил:
Одинаковые строки не допускаются.
Первичные ключи должны быть уникальными внутри каждой таблицы.
Значение каждого поля атомарно, т.е. каждое поле представляет одну неделимую единицу информации. (Смысл этого правила в том, что в реляционной БД вы не можете в одно поле поместить список.)
Внутри любой базы данных существует три типа таблиц:
Таблица сущностей: Содержит прикладные данные, имеющие значение для конечного пользователя.
Таблица отношений: Показывает связь между таблицами и не представляет интереса (и невидима) для конечного пользователя.
Виртуальная таблица: Используется для "представления" базы данных приложению.
Язык структурированных запросов SQL
Промышленным стандартом для запросов к реляционным БД является Structured Query Language (SQL — язык структурированных запросов). К сожалению, существует большое число расширений SQL, которые, хотя и делают язык SQL более мощным для отдельных БД, но и усложняют этот смешанный язык доступа к БД.
SQL также является языком определения данных (DDL — Data Definition Language) и может использоваться для создания БД и определения структуры таблиц. Кроме того, SQL предоставляет язык манипуляции данными (DML), используемый для запроса данных. Операторы DDL используются для создания таблиц, представлений, ключей и ограничений, определяющих БД, а операторы DML — для запроса по БД, выбора записей и сортировки результатов.
Объявление данных включает имя таблицы и каждое из полей, содержимое, условия и ограничения для каждого поля (его тип, длина, может ли оно быть установлено в значение NULL и т.д.). DDL также используется в целях определения ключей для установления ссылочной целостности. (.Ссылочная целостность подразумевает, что каждая из различных таблиц в БД согласована с другой таблицей в их представлении состояния объектов.)
В результате запроса БД вы получаете набор строк в таблице или представлении. Это называется вашим ответным набором. Представление является виртуальной базой данных, которая для пользователя выглядит как и обычная БД, но на самом деле представляет собой подмножество настоящей базы данных, основанное на результате запроса. Итератор, называемый курсором, используется для перемещения по полученному по запросу набору данных.
Обычный метод поиска в БД заключается в вызове оператора SELECT:
SELECT AuthorlD, Author, City FROM Author WHERE AuthorlD > 101 AND Zip AuthorlD < 104
Оператор SQL требует возвращения БД AuthorlD, Author и City из таблицы Author для каждой записи, в которой AuthorlD больше 101 и меньше 104. В результате, вероятно, вы получите две записи: AuthorlD 102 и 103 (при условии, что две записи не могут иметь одинаковое значение в поле AuthorlD).
Нормирование
Одной из целей проектировщика БД является исключение повторов в таблицах БД. Повторение стоит дорого: для сохранения одной и той же информации дважды требуется дополнительное дисковое пространство, а поиск записей в таком случае будет продолжаться дольше. Повторения также вызывают опасность повреждения ссылочной целостности: если вы сохраните данные дважды, то существует вероятность, что эти копии данных выйдут из синхронизации.
Процесс исключения повторений называется нормированием.
Теоретики БД выделили несколько нормальных форм реляционной базы данных. Нормальная форма определяет то, до какой степени исключаются повторения; каждая последующая нормальная форма более ограничительна, чем предыдущая. Например, третья нормальная форма более ограничительна (т.е. позволяет меньше повторений), чем вторая.
Глава 16
Первая нормальная форма (1NF) предписывает, что каждое поле (часто называемое атрибутом) хранит только одно значение (в отличие от списка значений). Вторая нормальная форма (2NF) вносит требование, чтобы каждая строка (запись) была уникальна. В целях выполнения этого требования для каждой строки создается уникальный идентификатор, называемый опознавательным столбцом или опознавательным полем. Если у вас нет других значений для индексации, вы могли бы сделать опознавательный столбец первичным ключом, но это не обязательно. Опознавательный столбец создается только в целях сохранения уникальности.
Третья нормальная форма (3NF) — это наиболее популярный компромисс между слишком слабым и слишком сильным нормированием. В 3NF вы удаляете из таблиц всю лишнюю информацию, за исключением полей, используемых в качестве внешних ключей.
Существует четвертая и пятая формы, но они не нашли широкого применения в связи со сложностью реализации, а также из-за свойственной им низкой производительности. Фактически некоторые базы данных умышленно денормированы в целях повышения производительности. Не пытайтесь сделать этого дома; этим занимаются профессионалы.
Соединения
Нормирование БД ограничивает результаты простого запроса, если только вы не соедините две или более таблицы. В результате соединения нескольких таблиц получается одна виртуальная таблица так, как е:сли бы две таблицы стали одной большой таблицей с повторяющимися значениями. Благодаря этому вы можете эффективно искать и так же эффективно сохранять данные.
Существует несколько методов соединения таблиц. Первым и наиболее широко используемым является естественное соединение (natural join), называемое также эквисоединением. Эквисоединение двух таблиц осуществляется по их общему столбцу. Выполняется соединение с помощью такой конструкции SQL, как WHERE:
SELECT ТаЫеА. Title, ТаЫеВ. Author FROM ТаЫеА, ТаЫеВ WHERE ТаЫеА. Author ID = ТаЫеВ. Author ID
Конструкция WHERE создает эквисоединение. Заметим, что в конструкции FROM указано две таблицы, которые должны быть соединены. Если вы не включите конструкцию эквисоединения и соедините две или более таблицы, указав их в конструкции FROM, то в результате создадите кросс-соединение.
Одним из вариантов эквисоединения является тета-соединение. Тета-соединение подобно эквисоединению, за исключением того, что вместо оператора выравнивания двух столбцов вы используете другой реляционный оператор.
Вы можете обобщить эти операторы в операторы внутреннего соединения, явно назвав столбцы таблицы для подбора. При внутреннем соединении не нужно сравнивать одинаковые столбцы двух таблиц. Таким образом, вы можете написать следующий запрос:
SELECT ТаЫеА.Title
FROM ТаЫеА inner join ТаЫеВ ON Table .A. Author ID = ТаЫеВ. Author ID AND ТаЫеВ.State = ST
Если существует внутреннее соединение, то вы могли бы догадаться, что существует и внешнее соединение. Внешнее соединение обеспечивает просмотр двух таблиц и возвращает записи одной таблицы в тех случаях, когда существует соответствующая запись во второй таблице. Вот как это можно выразить: "Покажи мне все записи таблицы А; также покажи мне все записи таблицы В, похожие на записи таблицы А".
Внешние соединения являются направленными, т.е. они могут быть либо лево-, либо правосторонними. Левостороннее внешнее соединение выглядит подобно следующему:
SELECT * FROM ТаЫеА left outer join ТаЫеВ ON ТаЫеА. Author ID =
ТаЫеВ .Author ID
Этот оператор возвращает каждую запись таблицы А (левой таблицы) и все соответствующие условию записи таблицы В.
Живучесть для реляционной базы данных
При создании объектно-ориентированной программы перед нами встает классическая дилемма — как отобразить объекты в строки и столбцы реляционной БД. Каждая переменная-член объекта соответствует полю (столбцу) БД.
Часть IV
После разработки этого отображения вы будете иметь несколько вариантов для выбора: обучить классы самостоятельно записывать себя в реляционную БД либо использовать среду приложения для выполнения этой работы ею.
Прежде чем рассмотреть возможности среды, было бы полезным осмыслить задачу. Обсудим, как можно написать такой программный код самостоятельно без использования среды.
Предположим, что вы собираетесь записать свои атрибуты (переменные-члены) в поля. Сначала определите, как вы будете отображать состояние объекта в строке в одной или нескольких таблицах. Далее решите, как вы будете представлять элементарные данные объекта, и, таким образом, вы должны отобразить типы данных C++, такие как int и char, на типы данных вашей БД. Заметьте, что программный код, который вы пишете, не обязательно должен быть переносимым между БД. Вы могли бы добавить уровень абстрагирования, для того чтобы можно было выбрать новую БД без разбиения конструкции объектов. Существуют средства, способные читать ваши header-файлы и генерировать необходимые DDL.
Вы должны определить методы для создания таблиц БД и для поддержания их обновленными. Затем можете создать запросы, необходимые для манипуляции данными, а также API для самой БД (API обычно предоставляется производителем БД, но он не поддерживает концепции объектов).
Каждый объект и все его данные-члены должны преобразовываться в таблицы и из таблиц. Члены данных включают не только элементарные данные (такие, как int и char), но и агрегированные и базовые объекты.
Перемещения с идентификаторами объектов
Одной из проблем в отображении объектов на реляционные БД является отображение объектов, хранящихся в куче. Чтобы найти ключ к решению этой проблемы, мы должны рассмотреть, как объект обращается к другому объекту в памяти? Например, если создается содержание книги, как сообщить ему об авторе? Обычно это делается с помощью указателей. Одна из переменных-членов содержания книги будет указателем на автора:
class BookOrder (
public:
И . . .
private:
Author * pAuthor;
I;
При отображении его в реляционную БД мы отображаем указатель на идентификатор объекта (OID — Object Identifier). OID уникально определяет объект и может быть использован для перемещения такого объекта с диска и на диск. Перемещение — это процесс использования указателя, получения объекта, записи его в таблицу или чтения из таблицы и вызова оператора new для создания объекта в оперативной памяти.
Идентификаторы объектов часто хранятся как числа или короткие символьные массивы. В идеале каждый OID уникален. Если он неуникален в общем, то должен быть уникален внутри одного экземпляра программы. В приложении клиент-сервер, OID должен быть уникален в сети; если этой сетью является Internet, OID должен иметь глобально уникальное значение. Компания Microsoft предложила глобально уникальные идентификаторы, которые будут рассмотрены в главе 21.
Использование пятен
Процесс чтения и записи данных может быть очень медленным, особенно в связи с тем, что каждый элемент данных должен преобразовываться в соответствии в требованиями БД.
Альтернатива состоит в сохранении всего объекта как двоичного потока байтов. Значения ключей могут быть сохранены как атрибуты в столбцах, а множество данных будет трактоваться как пятно, т.е. не имеющее никакого семантического значения. Когда потребуются данные, вы будете использовать ключ для доступа к ним, а затем воссоздадите объект в оперативной памяти.
Преимущество хранения большей части объекта в виде потока байтов состоит в уменьшении числа необходимых отображений между объектами и таблицами. Недостаток заключается в том, что семантика объектов (и даже индивидуальность многих атрибутов) теряется в БД.
Если вы собираетесь использовать пятно, необходимо принять важное решение, состоящее в том, какие данные запускать в пятно, а какие использовать в виде ключей. Запомните, как только данные попадают в пятно, они становятся невидимыми до тех пор, пока не будут вынесены из него, что может быть
Глава 16 сделано только с помощью явно указанных значений ключей. Использование пятен вместо отображения каждого атрибута может повысить производительность, но вместе с нею и стоимость внесения изменений в проект.
Скрытие деталей
Если только вы не создаете полную прикладную среду для перепродажи, то можете избежать этих проблем, используя стандартный интерфейс базы данных, предоставляемый ее поставщиком. Чтобы сделать правильный выбор, важно понимать различные уровни абстракции:
Прямое сохранение объектов
Использование API БД
Использование ODBC (или чего-либо подобного)
Использование MFC (или чего-либо подобного)
Нужно сказать, вы можете писать собственный программный код для получения объектов из базы данных и для помещения их туда или для решения своей задачи можете воспользоваться преимуществами API производителя. Поднимаясь на ступеньку вверх по лестнице абстракции, вы можете использовать технологию Microsoft Open Database Connectivity (ODBC), предназначенную для создания универсального API для всех баз данных, поддерживающих SQL. На следующем уровне абстракции находится модель Microsoft Data Object (DAO), предоставляющая объектно-ориентированное завершение ODBC. В заключение мы обращаемся к библиотеке Microsoft Foundation Classes (MFC), предоставляющей самый высокий уровень абстракции над ODBC или DAO.
Непосредственное сохранение объектов
Непосредственное сохранение объектов — это любимый выбор программиста. Мы оставляем в стороне обывательские коммерческие БД и пишем собственную базу данных. Создаем свои индексы и свои файлы данных и напрямую записываем их на диск. Забавно — и совершенно безумно для какого бы то ни было приемлемого коммерческого приложения.
Использование API БД
Использование API вашей БД — это несколько более реалистичный подход. При таком подходе вы прямо пишете в интерфейс API, предоставляемый поставщиком БД. Проблема такого подхода в том, что он совершенно не инкапсулирован. Мы запираем себя в рамках API одной БД, и при желании сменить БД нам придется повторить заново всю эту работу. Кроме того, эти API очень часто являются интерфейсами С, а не C++, и придется потратить довольно много времени на создание оболочек для программного кода C++.
Доступ к источникам данных ODBC
Open Database Connectivity (ODBC) — это процедурный API С-стиля, позволяющий приложениям получать доступ к любой базе данных, для которой у конечного пользователя имеется драйвер ODBC. В результате ODBC позволяет приложениям Windows подключаться к различным средам на нескольких платформах.
ODBC включает следующие компоненты:-
ODBC API. Библиотека вызовов функций, кодов ошибок и стандарта языка обработки структурных запросов (SQL), который может использоваться для доступа к БД.
ODBC Driver Manager (Диспетчер драйверов ODBC). Динамически компонуемая библиотека (ODBC32.DLL), загружающая драйверы БД ODBC, которые предоставляют API БД.
ODBC Database Drivers (Драйверы баз данных ODBC). Одна или несколько DLL, направляющих функции ODBC на особые системы управления БД.
ODBC Cursor Library (Библиотека курсоров ODBC). Динамически компонуемая библиотека (ODBCCR32.DLL), предоставляющая поддержку курсоров (виртуальных представлений БД). Библиотека курсоров находится между диспетчером драйверов ODBC и драйверами.
ODBC Administrator (Администратор ODBC). Средство, используемое для конфигурирования источников данных, используемых приложениями для подключения к БД.
Часть IV
В своих приложениях C++ вы можете подключиться к любому источнику данных, имеющему драйвер ODBC. Эти источники данных могут быть следующими:
Реляционные БД, такие как Oracle или Microsoft SQL Server
БД индексно-последовательного метода доступа (ISAM)
Электронные таблицы Microsoft Excel
И Текстовые файлы
Источники данных состоят из нескольких компонентов, полностью описывающих данные для доступа. Эти компоненты включают следующие элементы:
Особый набор данных
S Информацию о соединении, необходимую для доступа к данным
Информацию о местонахождении источника данных
Источники данных должны конфигурироваться с помощью администратора ODBC. Полный набор функциональных возможностей зависит от установленного драйвера ODBC и его возможностей.
Администратор ODBC используется для управления источниками данных. В системном реестре Windows он хранит информацию об источниках данных и их соединениях. С помощью администратора ODBC можно выполнять следующее:
Добавлять и удалять драйверы ODBC
Добавлять, изменять и удалять источники данных
Модель курсора ODBC
Большинство систем управления БД предоставляют простой метод получения данных из запроса. Строки возвращаются приложению по одной до тех пор, пока не будет возвращена последняя строка результирующего набора. Вернуться обратно к строке, не вызвав запрос еще раз, невозможно. С другой стороны, интерактивные приложения обычно позволяют пользователю перемешаться вперед и назад по набору данных с помощью клавиш со стрелками или клавиш Page Up и Page Down. Курсор — это механизм, позволяющий обрабатывать одновременно отдельные строки, возвращаемые в результате запроса; курсор указывает на текущую строку в наборе данных. Курсор, предоставляющий возможность перемещаться вперед и назад по результирующему набору, называется прокручиваемым курсором. Курсор, позволяющий изменять и удалять полученные данные, называется прокручиваемым, обновляемым курсором. При использовании курсоров большую роль играет контроль параллелизма (особенно при работе с транзакциями).
ODBC предоставляет гри следующих типа курсоров:
Статические курсоры. В статическом курсоре принадлежность, расположение и значения результирующего набора неизменны при открытом курсоре. Эти элементы остаются неизменными до тех пор, пока курсор не закроется. Такой тип курсора особо полезен для приложений типа "только-для-чте- ния". Статический курсор предоставляет очень стойкое представление данных. Большим недостатком этой модели является тот факт, что изменения, сделанные другими транзакциями при открытом курсоре, невидимы для такого курсора.
Курсоры, управляемые с клавиатуры. В курсоре, управляемом с клавиатуры, принадлежность и расположение результирующего набора неизменны, тогда как значения при открытом курсоре можно изменять..
Динамические курсоры. В динамическом курсоре изменения, внесенные кем-либо, а также незавершенные изменения, вносимые владельцем курсора, видимы для него. Изменения, внесенные кем- либо, могут повлиять на принадлежность и расположение результирующего набора:
Тип курсораТочностьПостоянствоПараллелизм
СтатическийНизкаяВысокоеСредний
Управляемый с клавиатуры СредняяСреднееСредний
ДинамическийВысокаяНизкоеВысокий
Контроль параллелизма с помощью ODBC
Блокирование (или пессимистический контроль параллелизма) и оптимистический контроль параллелизма являются двумя главными методами управления параллелизмом. Оптимистический подход используется в
Глава 16
том случае, когда приложение оптимистично и надеется, что другие транзакции не обновят данные прежде, чем те будут записаны в БД. С другой стороны, блокирование используется для гарантии того, что другие транзакции не смогут внести изменения в то время, когда текущая транзакция владеет данными в эксклюзивном режиме.
В ODBC приложение может определить тип контроля параллелизма, передав SetStmtOption одну из исключительных опций:
SQL_CONCUR_READ_ONLY. Устанавливает доступ "только-для-чтения" и не предоставляет возможности изменять данные.
SQL_CONCUR_LOCK. Указывает, что для предотвращения изменения данных другими транзакциями будет использовано блокирование.
SQL_CONCUR_ROWVER и SQL_CONCUR_VALUES. Указывают, что будет использоваться оптимистический контроль параллелизма.
Прокрутка
С помощью ODBC можно получить строки из набора данных, используя SQLFetch (поочередно получает строки из курсоров для перемещения только вперед). В SQLExtendedFetch доступны следующие операции прокрутки:
SQL_FETCH_NEXT. получить следующий набор строк
SQL_FETCH_PRIOR: получить предыдущий набор строк
SQL_FETCH_FIRST: получить первый набор строк
SQL_FETCH_LAST. получить последний набор строк
SQL_FETCH_RELATIVE: получить набор строк, находящийся на расстоянии в п строк от текущего набора строк (п может быть как положительным, так и отрицательным числом)
SQL_FETCH_ABSOLUTE: Получить набор строк, начинающийся с n-й строки в результирующем наборе
Программисты приложений могут использовать прокручиваемые курсоры, не создавая собственного кода курсора, а воспользовавшись библиотекой курсоров ODBC. Диспетчер драйверов вызывает библиотеку курсоров, а библиотека курсоров вызывает драйвер. Библиотека курсоров будет доступна после того, как приложение вызовет SQLSetConnectOption с опцией SQL_ODBC_CURSORS. Эта опция позволяет приложениям использовать функции прокручиваемых курсоров, определенные на втором уровне соответственности ODBC (ODBC Conformance Level 2) (SQLExtendedFetch, SQLSetPos и опции курсора в SQLSetStmtOption).
Использование MFC
При использовании среды приложения все нюансы по управлению ODBC она берет на себя. В среде вы получаете объектно-ориентированную оболочку не только над интерфейсом API БД, но и над слоем ODBC. Ваше приложение может полностью сконцентрироваться на своей внутренней логике. Лучший способ увидеть, как это работает, — рассмотреть пример. И хотя вы, возможно, никогда и не воспользуетесь MFC, уроки, полученные здесь, должны применяться как минимум на самом высоком уровне абстракции к любой используемой вами библиотеке каркасов.
Вопрос, как всегда, состоит в том, насколько сложным сделать пример. Слишком простой будет игрушкой, не связанной с действительностью, слишком сложный будет слишком трудным для изучения. Автор стремится доверять довольно простым примерам, которые позже после ознакомления со всеми принципами можно расширить и обобщить.
Начнем с анализа. Что мы пытаемся смоделировать? Нам нужна программа, которая помогла бы следить за книгами. Например, пользователь хочет знать, кто написал книгу, когда он ее читал, какие вопросы в ней поднимаются и т.д.
Поскольку этот пример создан для демонстрации БД, пока сосредоточимся исключительно на таблицах.
Таблица Book хранит информацию об отдельной книге; ее предварительные поля показаны на рис. 16.3.
Необходима также таблица Author, и мы создадим отношение многие-ко-многим между книгами и автором, создав таблицу AuthorBookJoin, как показано на рис. 16.4.
Затем в эту таблицу добавим некоторые образцы данных (рис. 16.5).
Часть IVЧасть IV
Часть IV
Часть IV
® AuthorTable: Table
РИСУНОК 16.3. Таблица Book.РИСУНОК 16.3. Таблица Book.AulhorlDAuthor NameAuthor NotesAulhorlDAuthor NameAuthor Notes: AuloNumber : . Text _ _ \ Text i: AuloNumber : . Text _ _ \ Text iРИСУНОК 16.4. Таблица AuthorBookJoin.РИСУНОК 16.4. Таблица AuthorBookJoin.Eh Е* Йе* 8*М Fewt 8w* Io*Eh Е* Йе* 8*М Fewt 8w* Io*Author Meme"Author Meme"QUbb | Sow*. ] d Pm КQUbb | Sow*. ] d Pm КИИ2 Dostoyevsky3 Powers4 Thompson5 Caleb Can ■6 Ench Gamma ;7 Richard Helm ■В Ralph Johnson:9 John Vlissides10 Orson Scott Ca2 Dostoyevsky3 Powers4 Thompson5 Caleb Can ■6 Ench Gamma ;7 Richard Helm ■В Ralph Johnson:9 John Vlissides10 Orson Scott CaAuthorBookJotiM .«•«***?•*DetaWoAuthorBookJotiM .«•«***?•*DetaWoI Audiei Note*I Audiei Note*ИИРИСУНОК 16.5.Пример данных.РИСУНОК 16.5.Пример данных.Beek ID ~Beek ID ~о 1 Teach Yourself C++ In 21 Days 2EII2 Teach Yourself C++ In 24 Hours||i 3Beginning Object Oriented Analysts And DesignQ4 Clouds To Code5 C++ UnleashedШБCrime & Punishmente7Galatea 2.2_||8Fear and Loathing in Las Vegas9 Alienist||IDDesign PatternsЦ11Brothers KaramazovЯвоаШ i<l « Il « a iMbel4 is <lо 1 Teach Yourself C++ In 21 Days 2EII2 Teach Yourself C++ In 24 Hours||i 3Beginning Object Oriented Analysts And DesignQ4 Clouds To Code5 C++ UnleashedШБCrime & Punishmente7Galatea 2.2_||8Fear and Loathing in Las Vegas9 Alienist||IDDesign PatternsЦ11Brothers KaramazovЯвоаШ i<l « Il « a iMbel4 is <lGonzo JournalGonzo JournalBook»Book»ъф»ц» ;ъф»ц» ;»««* И1±1Г»««* И1±1Г56562i32i35i6:5i6:2i3?2i3?2 32 35 65 68:9;108:9;10FictionFiction Fiction: ProgrammingFictionFiction Fiction: Programming5 Excellent tour det 4 Hunter Thompson" 3 Great mystery, wc 5 Classic ;7 All time favorite nc5 Excellent tour det 4 Hunter Thompson" 3 Great mystery, wc 5 Classic ;7 All time favorite ncBeekTWe[BeekTWe[IDID•sr•sr9101091010
РИСУНОК 16.3. Таблица Book.
РИСУНОК 16.3. Таблица Book.
AulhorlD
Author Name
Author Notes
AulhorlD
Author Name
Author Notes
: AuloNumber : . Text _ _ \ Text i
: AuloNumber : . Text _ _ \ Text i
РИСУНОК 16.4. Таблица AuthorBookJoin.
РИСУНОК 16.4. Таблица AuthorBookJoin.
Eh Е* Йе* 8*М Fewt 8w* Io*
Eh Е* Йе* 8*М Fewt 8w* Io*
Author Meme"
Author Meme"
QUbb | Sow*. ] d Pm К
QUbb | Sow*. ] d Pm К
И
И
2 Dostoyevsky
3 Powers
4 Thompson
5 Caleb Can ■
6 Ench Gamma ;
7 Richard Helm ■
В Ralph Johnson:
9 John Vlissides
10 Orson Scott Ca
2 Dostoyevsky
3 Powers
4 Thompson
5 Caleb Can ■
6 Ench Gamma ;
7 Richard Helm ■
В Ralph Johnson:
9 John Vlissides
10 Orson Scott Ca
AuthorBookJoti
M .«•«*
**?•*
DetaWo
AuthorBookJoti
M .«•«*
**?•*
DetaWo
I Audiei Note*
I Audiei Note*
И
И
РИСУНОК 16.5.
Пример данных.
РИСУНОК 16.5.
Пример данных.
Beek ID ~
Beek ID ~
о 1 Teach Yourself C++ In 21 Days 2E
II2 Teach Yourself C++ In 24 Hours
||i 3Beginning Object Oriented Analysts And Design
Q4 Clouds To Code
5 C++ Unleashed
ШБCrime & Punishment
e7Galatea 2.2_
||8Fear and Loathing in Las Vegas
9 Alienist
||IDDesign Patterns
Ц11Brothers Karamazov
ЯвоаШ i<l « Il « a iMbel4 is <l
о 1 Teach Yourself C++ In 21 Days 2E
II2 Teach Yourself C++ In 24 Hours
||i 3Beginning Object Oriented Analysts And Design
Q4 Clouds To Code
5 C++ Unleashed
ШБCrime & Punishment
e7Galatea 2.2_
||8Fear and Loathing in Las Vegas
9 Alienist
||IDDesign Patterns
Ц11Brothers Karamazov
ЯвоаШ i<l « Il « a iMbel4 is <l
Gonzo Journal
Gonzo Journal
Book»
Book»
ъф»ц» ;
ъф»ц» ;
»««* И1±1Г
»««* И1±1Г
5
6
5
6
2i
3
2i
3
5i
6:
5i
6:
2i
3?
2i
3?
2 3
2 3
5 6
5 6
8:
9;
10
8:
9;
10
Fiction
Fiction Fiction
: Programming
Fiction
Fiction Fiction
: Programming
5 Excellent tour det 4 Hunter Thompson" 3 Great mystery, wc 5 Classic ;
7 All time favorite nc
5 Excellent tour det 4 Hunter Thompson" 3 Great mystery, wc 5 Classic ;
7 All time favorite nc
BeekTWe[
BeekTWe[
ID
ID
•sr
•sr
9
10
10
9
10
10
Заметьте, что каждая их этих таблиц была создана в одной базе данных Access, которая названа BookCollec- tionUnleashed.mbd. Создадим также источник данных, указывающий на эту коллекцию. Для этого воспользуемся утилитой ODBC32 в панели управления Windows NT.
ODBC32 предлагает несколько типов соединений DataSource (DSN). Соединение User DNS доступно для одного пользователя, а соединение System DSN доступно для любого пользователя вашей системы. Если источник DSN предназначен для совместного использования на нескольких компьютерах, необходимо создать File DSN. Создадим System DSN, указывающий на созданный файл .mdb, и назовем его BookCollectionUnleashed.
Пришло время перейти в Microsoft Visual C++ и создать новый проект. Первым делом выберем тип интерфейса приложения — однодокументный (SDI), многодокументный (MD1) или основанный на окне диалога. Если выбрать SDI- или MDI-интерфейс, появится возможность установить поддержку БД и обеспечить в этом случае возможность просмотра базы данных.
При выборе этой возможности мы должны указать источник данных ODBC и то, какие из таблиц хотим включить в просмотр (рис. 16.6).
Мастер может создать только один вид, но при разработке приложения можно самостоятельно добавлять дополнительные представления записей. Наше главное представление мы создадим по таблице Book, поскольку это — хороший способ продемонстрировать возможности представления, создаваемого с помощью мастера. В коммерческом приложении вы могли бы выбрать опцию Header Files Only, если хотели бы получить возможность более полного контроля над всеми представлениями, и создали бы их самостоятельно.
Когда мастер соберет всю необходимую информацию, вы увидите обзор классов (рис. 16.7).
Глава 16
РИСУНОК 16.6. Выбор таблицы.РИСУНОК 16.6. Выбор таблицы.РИСУНОК 16.7. Обзор классов в мастере.РИСУНОК 16.7. Обзор классов в мастере.
РИСУНОК 16.6. Выбор таблицы.
РИСУНОК 16.6. Выбор таблицы.
РИСУНОК 16.7. Обзор классов в мастере.
РИСУНОК 16.7. Обзор классов в мастере.
Заметим, что классом представления в этом случае является CRecordView, как и ожидалось. При сборке приложения мастер обеспечил нас всех необходимым, но MFC ожидает от создания объекта диалогового окна для представления, в котором будет отображаться содержимое таблицы. Поэтому предварительное диалоговое окно, показанное на рис. 16.8, выглядит таким спартанским.
Для вывода полей таблицы установим на форме несколько элементов управления (рис. 16.9).
РИСУНОК 16.8. Предварительное диалоговое окно для примера приложения.РИСУНОК 16.8. Предварительное диалоговое окно для примера приложения.РИСУНОК 16.9. Элементы управления для вывода полей.РИСУНОК 16.9. Элементы управления для вывода полей.
РИСУНОК 16.8. Предварительное диалоговое окно для примера приложения.
РИСУНОК 16.8. Предварительное диалоговое окно для примера приложения.
РИСУНОК 16.9. Элементы управления для вывода полей.
РИСУНОК 16.9. Элементы управления для вывода полей.
Затем эти элементы управления необходимо подключить к содержимому базы данных. Для этого перейт дем к функции DoDataExchange класса CRevordView. Программный код, созданный мастером приложений, выглядит следующим образом:
void CBookCollectionUnleashedView::DoDataExchange(CDataExchange* pDX) (
CRecordView::DoDataExchange(pDX);
//((AFX_DATA_MAP(CBookCollectionUnleashedView) // Примечание: здесь мастер Classwizard добавляет вызовы DDX и DDV
//} )AFX_DATA_MAP }
Теперь в мастере классов (Class Wizard) создадим переменные-члены для каждого из элементов управления диалогового окна (рис. 16.10).
В результате Class Wizard создаст следующий программный код обмена данными:
void CBookCollectionUnleashedView::DoDataExchange(CDataExchange* pDX) (
CRecordView::DoDataExchange(pDX);
//((AFX_DATA_MAP(CBookCollectionUnleashedView)
DDX_Text(pDX, IDC_CATEGORY, m_Category);
DDX_Text(pDX, IDC_NOTES, m_Notes);
DDX_Text(pDX, IDC_RATING, m_Rating);
Часть IV
DDX_Text(pDX, IDC_TITLE, m_Title) ; / / ) ) AFX_DATA_MAP
К сожалению, это еще не все, что нам необходимо. Мы хотим непосредственно общаться с базой данных, а не помещать содержимое элементов управления в локальные переменные. Этот недочет можно исправить, заменив вызовы DDX_Text на вызовы DDX_FieldText. Внесем это изменение самостоятельно, отредактировав программный код, созданный мастером. Макрос DDX_FieldText принимает четыре параметра: первые два совпадают с параметрами DDX_Test, третий является переменной-членом CRecordSet, подключенной к классу представления, а четвертый — это сам объект CRecordSet. Во-первых, покажем, как это выглядит, а затем рассмотрим загадочный CRecordSet:
void CBookCollectionUnleashedView::DoDataExchange(CDataExchange* pDX) (
CRecordView::DoDataExchange(pDX);
//((AFX_DATA_MAP(CBookCollectionUnleashedView)
DDX_FieldText(pDX, IDC_CATEGORY, m_pSet->m_Category, m_pSet) ;
DDX_FieldText(pDX, IDC_NOTES, m_pSet~>m_Notes, m_pSet) ;
DDX_FieldText(pDX, IDC_RATING, m_pSet->m_Rating, m_pSet);
DDX_FieldText(pDX, IDC_TITLE, m_pSet->m_Book_Title, m_pSet);
//}}AFX_DATA_MAP }
В результате сборки и запуска этого кода получится приложение, которое фактически напрямую читает из БД, как показано на рис. 16.11.
РИСУНОК 16.11. Приложение прочитало запись из базы данных.РИСУНОК 16.11. Приложение прочитало запись из базы данных.
РИСУНОК 16.11. Приложение прочитало запись из базы данных.
РИСУНОК 16.11. Приложение прочитало запись из базы данных.
Видно, что приложение работает, но как? Вся суть заключается в переменной-члене данных m_pSet класса CRecordView. Эта переменная является указателем на объект CBookCollectionUnleashedSet. И сам класс, и переменная-член были созданы и добавлены мастером приложений (Application Wizard).
Так что же такое CBookCollectionUnleashedSet? Этот тип был создан мастером приложений и унаследован от класса CRecordSet:
class CBookCollectionUnleashedSet : public CRecordset {
public:
CBookCollectionUnleashedSet(CDatabase* pDatabase = NULL);
DECLARE_DYNAMIC(CBookCollectionUnleashedSet)
И Поля/Param Data
//((AFX_FIELD(CBookCollectionUnleashedSet, CRecordset)
longm_Book_ID;
CString m_Book_Title;
CStringm_Category;
longm_Rating;
CStringm_Notes;
//}}AFX_FIELD
Глава 16
// Перекрытия И Мастер Classwizard сгенерировал перекрытие виртуальной функции //{(AFX_VIRTUAL(CBookCollectionUnleashedSet) public: virtual CString GetDefaultConnect();// Строка соединения по умолчанию
virtual CString GetDefaultSQL();// По умолчанию SQL для набора записей
virtual void DoFieldExchange(CFieldExchange* pFX);// Поддержка RFX
// } } AFX_VIRTUAL
// Реализация #ifdef _DEBUG virtual void AssertValid() const; virtual void Dump(CDumpContextS de) const;
#endif
};
CRecordSet, по существу, является итератором в вашей таблице. Он имеет одну переменную-член для каждого из полей выбранной таблицы, и использовать вы их можете для обмена данными с самой таблицей. Более подробно мы изучим объекты CRecordSet несколько позже, а сейчас просто примем подаренное мастерами: при запросе записи RecordSet будет получать ее из БД.
Далее допустим, что мы хотим видеть автора (или авторов) книг. Это требование несколько хитрое в реализации, поскольку в таблице Book мы не храним имена авторов (и даже их идентификаторы). (Вспомните, мы связали таблицы Book и Author по записям таблицы BookAuthorJoin.)
Вот как можно решить нашу задачу. Каждый раз при чтении книги мы хотим использовать m_book_ID, чтобы найти каждую подходящую запись в таблице BookAuthorJoin. Для каждой найденной записи мы хотим посмотреть имя автора в таблице Author и затем вывести ее.
Сделать это проще, чем вы думаете. Во-первых, для обеих таблиц нужно создать объекты CRecordSet:
class AuthorRecordSet : public CRecordSet { public: AuthorRecordSet(CDatabase* pDatabase = NULL); DECLARE_DYNAMIC(AuthorRecordSet)
// Поля/Param Data
//((AFX_FIELD(AuthorRecordSet, CRecordSet)
long m_AuthorID;
CStringm_Author Name;
CStringm_Author_Notes;
//))AFX_FIELD
// Перекрытия
// Мастер Classwizard сгенерировал перекрытие виртуальной функции И((AFX_VIRTUAL(AuthorRecordSet) public: virtual CString GetDefaultConnect () ; J) Строка соединения по умолчанию virtual CString GetDefaultSQL() ;// По умолчанию SQL для набора записей
virtual void DoFieldExchange(CFieldExchange* pFX) ;// Поддержка RFX
IJ } ) AFX_VIRTUAL
// Реализация
#ifdef _DEBUG
virtual void AssertValid() const;
virtual void Dump(CDumpContextS de) const;
#endif };
class BookAuthorRecordSet : public CRecordset ( public: BookAuthorRecordSet(CDatabase* pDatabase = NULL); DECLARE_DYNAMIC(BookAuthorRecordSet)
// Поля/Param Data
//{(AFX_FIELD(BookAuthorRecordSet, CRecordset)
longm_ID;
longm_AuthorID;
Часть IV
long m_BookID; //))AFX_FIELD
// Перекрытия // Мастер Classwizard сгенерировал перекрытие виртуальной функции //{(AFX_VIRTUAL(BookAuthorRecordSet) public: virtual CString GetDefaultConnect();// Строка соединения no умолчанию
virtual CString GetDefaultSQL();// По умолчанию SQL для набора записей
virtual void DoFieldExchange(CFieldExchange* pFX);// Поддержка RFX
//) )AFX_VIRTUAL
// Реализация
#ifdef _DEBUG
virtual void AssertValid() const;
virtual void Dump(CDumpContextS de) const;
#endif 1;
Здесь приведен программный код, сгенерированный Class Wizard. Заметьте, что каждый класс имеет переменную-член, соответствующую полю одной из таблиц.
Следующим шагом будет создание элементов управления, выводящих имена авторов. Для упрощения задачи воспользуемся окном списка. В заключение создадим в классе представления метод-член для обновления списка авторов и будем вызывать этот метод каждый раз при обновлении элементов управления:
void CBookCollectionUnleashedView::DoDataExchange(CDataExchange* pDX) (
CRecordView::DoDataExchange(pDX);
//{(AFX_DATA_MAP(CBookCollectionUnleashedView) DDX_Control(pDX, IDC_Authors, m_Author_Control); DDX~FieldText(pDX, IDC_CATEGORY, m_pSet->m_Category, m_pSet); DDX_FieldText(pDX, IDC_NOTES, m_pSet->m_Notes, m_pSet) ; DDX_FieldText(pDX, IDC_RATING, m_pSet->m_Rating, m_pSet);
DDX~FieldText(pDX, IDC_TITLE, m_pSet->m_Book_Title, m_pSet);
//)Tafx_data_map
UpdateAuthor(m_pSet->m_Book_ID);
bool CBookCollectionUnleashedView::UpdateAuthor(int id) (
AuthorRecordSet ars; BookAuthorRecordSet bars; CString bookAuthorSQL; CString authorSQL; CString display; int pos;
m_Author_Control.ResetContent();
bookAuthorSQL.Format("BookID = %d’’,id); bars.m_strFilter = bookAuthorSQL;
bars.Open() ; while ( ! bars.IsEOF() ) {
authorSQL.Format("AuthorlD = %d",bars,m_Author!D); ars.m_strFilter = authorSQL; if ( ars. IsOpen () ) ars.Reguery();
else
ars - Open () ;
display = ars.m_AuthorName;
pos = m_Author_Control.AddString(display);
bars .MoveNext () ; ) return true;
)
Глава 16
Суть UpdateAuthor в том, что мы передаем ему идентификатор выбранной в данным момент книги, полученный из объекта CBookCollectionUnleashedSet, подключенного к RecordView. После входа в функцию UpdateAuthor() мы просим окно списка самостоятельно очиститься (m_Author_Control.ResetContent();) и настраиваем запрос в БД на записи в таблице AuthorBookJoin, соответствующие этому идентификатору BookID.
Рассмотрим этот программный код шаг за шагом:
Установить строку поиска, которая станет конструкцией WHERE оператора SQL SELECT:
bookAuthorSQL.Format("BookID = %d" , id); bars,m_strFilter = bookAuthorlD;
Автор делает это в два этапа. Сначала он создает объект CString, а затем присваивает эту строку переменной-члену m_strFilter класса CRecordSet. В результате CRecordSet при каждом открытии и повторном запросе на данные будет возвращать набор соответствующих данных. (Обратите внимание на то, как это делается: CRecordSet добавляет к запросу конструкцию WHERE и вставляет строку, содержащуюся в m_strFilter.)
Открыть набор данных:
bars.Open();
Провести итерацию по записям:
while ( ! bars.IsEOF() ) { //:
bars.MoveNext(); }
Суть в том, что вы будете двигаться по набору записей до тех пор, пока не достигнете его конца (указываемого включенным флагом IsEOF).
В теле цикла необходимо извлечь authorlD из таблицы AuthorBookJoin и использовать этот идентификатор для поиска соответствующей записи в таблице Author. Для этого создадим CString с конструкцией WHERE, а затем присвоим эту строку переменной m_strAuthor набора данных таблицы Author. Затем произведем повторный запрос набора данных, в результате которого одну запись, соответствующую этому автору, извлекаем и добавляем ее в список. Вот как это выглядит:
authorSQL.Format("AuthorlD = %d",bars,m_AuthorID); ars,m_strFilter = authorSQL; if ( ars.IsOpen() ) ars.Requery();
else
ars.Open () ;
display = ars.m_AuthorName;
pos = m_Author_Control.AddString(display);
По завершении мы сможем выводить имена авторов, связанных с каждой из записей, как показано на рис. 16.12.
Редактирование
Попробуйте изменить рейтинг или категорию какой-либо из книг. Вот это да! БД обновляется при переключении к другой записи. Это волшебство происходит в методе класса представления DoDataExchange(), который вызывается автоматически при смене текущей записи.
Метод DoDataExchangeQ вызывается функцией CWnd::UpdateData, принимающей параметр bSavaAndVa- lidate — булевый флаг, по умолчанию установленный в значение true. При переходе от одной записи к другой старая запись обновляется и значения в элементах управления диалогового окна записываются обратно в БД с помощью механизма DDX_FieldText.
Немедленные обновления
Но что делать в том случае, когда вы хотите немедленно получить воздействие? Добавим кнопку Update, обновляющую текущую запись на месте. Программный код довольно прост. В Class Wizard создается обра-
Часть IV
ботчик, а затем остается перенести данные из элементов управления в переменные CRecordSet и обновить БД:
void CBookCollectionUnleashedView::OnBookUpdate() {
m_pSet->Edit() ;
UpdateData() ;
m_pSet->Update();
AfxMessageBox("Updated",MB ICONEXCLAMATION | MB_OK);
1“
К сожалению, только что описанный подход редактирования неприменим для поля авторов, поскольку оно является коллекцией. С этим полем нам придется немного потрудиться. Когда функция UpdateData() вызывает функцию DoDataExchange(), она передает ей объект CDataExchange. Одним из членов объекта CDataExchange является m_bSaveAndValidate — флаг, указывающий направление обмена данными (если флаг установлен в значение false, данные из БД передаются в элементы управления; если флаг установлен в значение true, мы записываем значения элементов управления в БД).
Эту информацию можно передать методу UpdateAuthor(), но это нам не очень поможет в нашем приложении, поскольку само по себе окно списка используется только для чтения. Для редактирования списка авторов нам необходимо обновить таблицу BookAuthorJoin, проведя нетривиальное редактирование.
Что нам может понадобиться? Мы можем при желании добавить автора — в случае чего можем добавить автора, уже существующего в таблице Author, или можем обновить эту таблицу. В дальнейшем нам может понадобиться отредактировать список авторов в этом списке, взятом из таблицы BookAuthorJoin.
Завершая описание пользовательского интерфейса, необходимо четко понимать, чего мы пытаемся достигнуть, и написать соответствующий программный код.
Добавление записей
РИСУНОК 16.13. Обработка ввода данных.РИСУНОК 16.13. Обработка ввода данных.Чтобы добавить новую книгу в систему, необходимо создать новую запись. Для начала добавим кнопку Add Book и в Class Wizard создадим код обработки этой кнопки. Затем создадим новое диалоговое окно для ввода данных (рис. 16.13).
РИСУНОК 16.13. Обработка ввода данных.
РИСУНОК 16.13. Обработка ввода данных.
Пользователь заполняет поля и щелкает на кнопке ОК (чтобы убедиться, что пустых полей нет, запрещаем использовать кнопку ОК до тех пор, пока все поля не будут заполнены). Затем мы извлекаем данные из полей, создаем новую запись и вставляем ее в БД, как показано в листинге 16.1.
Листинг 16,1. OnBookAdd
void CBookCollectionUnleashedView::OnBookAdd() {
CAddBookDialog dig;
if ( dig.DoModal () == IDOK ) {
CBookCollectionUnleashedSet book; ASSERT ( ! book.IsOpen() ) ;
try {
book.Open () ;
ASSERT ( book.CanAppend() );
book.AddNew();
book,m_Book_Title = dlg.m_Title;
book.m_Category = dig.m_Category;
book,m_Notes = dlg.m_Notes;
book,m_Rating = dlg.m_Rating; book.Update();
book.Close();
catch(CDBException * e) (
CString s;
s.Format("DB Error in OnBookAdd: %d",e->m_nRetCode);
Глава 16
AfxMessageBox("s",MB_ICONSTOP | МВ_ОК ); )
catch(...) { CString s2;
s2.Format("Unknown Error in OnBookAdd!"); AfxMessageBox("s2",MB_ICONSTOP | MB_OK );
} } else AfxMessageBox("No Record Added",MB_ICONEXCLAMATION | MB_OK );
Локальный экземпляр CAddBookDialog создается в стеке:
CAddBookD1g dig;
Затем открываем его в модальном режиме и проверяем возвращаемое значение. Если пользователь щелкнул на кнопке Cancel, оператор if терпит неудачу и с помощью конструкции else вызывается AfxMessageBox:
if (dlg.DoModal()==ID_OK)
Если же пользователь щелкает на кнопке ОК, необходимо добавить новую запись. Создаем экземпляр CBookCollectionUnleashedSet и открываем его:
CBookCollectionUnleashedSet book;
//...
book.Open();
Теперь мы готовы вызвать для добавления новой записи функцию AddNewQ, устанавливающую RecordSet на новую пустую запись:
book.AddNew();
Поля новой записи заполняем значениями элементов управления диалогового окна:
book,m_Book_Title = dlg.m_Title;
book.m_Category = dig.m_Category;
book.m_Notes = dig.m_Notes;
book,m_Rating = dig.m_Rating;
Затем обновляем новый набор данных, добавляя таким образом новую запись в базу данных:
book.Update();
В заключение закрываем RecordSet, поскольку мы закончили работу с ним:
book.Close();
Заметьте, что все это мы делали в блоке try, чтобы иметь возможность обработать исключения CDBException, порождаемые системой ODBC. Такой подход помогает при отладке.
Следующие шаги
В это приложение можно внести еще несколько усовершенствований. Большинство из них основано на полученных здесь навыках, поэтому оставим их в качестве упражнений для читателя. Вы можете начать с возможности добавления новых авторов и присвоения авторов новым книгам. У вас есть все средства, позволяющие придать приложению должный вид, хотя остаются нерешенными еще несколько вопросов.
Операторы SQL
Иногда необходимо выполнить прямые операторы SQL без посредничества объекта CRecordSet. Сделать это можно, если вызвать функцию ExecuteSQLQ любого объекта CDataBase.
Типичное применение этого метода состоит в вызове параметризированной хранимой процедуры:
CString sqlCmd;
sqlCmd.Format("exec usp_SomeProcedure V%sV, \'%s\', %ld, VtsV, V%s\'",
Stringl,
String2,
String3,
Часть IV
String4, Strings) ; try ( CDatabase * pDB = new CDatabase; pDB-X)penEx("DSN=MySystemDSN") ; pDB->ExecuteSQL( sqlCmd ) ;
H
В этом программном коде мы создаем CString, который вызывает хранимую процедуру. Затем создаем объект CDatabase, используем его для открытия БД, передав имя DSN, и вызываем функцию ExecuteSQL(), передав ей объект CString.
Установка характеристик БД
При использовании объекта CDatabase фактически можно установить LoginTimeout и QueryTimeout, чтобы отрегулировать промежуток времени, в течение которого мы будем ожидать ответа от БД, прежде чем обработать сбой:
pDB->SetLoginTimeout(loginTimeOut); pDB->SetQueryTimeout(queryTimeOut);
Резюме
В этой главе были рассмотрены фундаментальные понятия технологии реляционных БД и было показано, как реляционные БД могут использоваться для хранения объектов. Отображение между реляционной БД и вашими объектами может быть обработано на различных уровнях абстракции. Многие поставщики предоставляют стандартизированные технологии (например, ODBC), упрощающие создание интерфейсного слоя между вашей моделью объектов и самой реляционной БД.
Создание итераторов по таблицам и отображение полей в БД на переменные-члены ваших объектов может стать намного проще, если в библиотеках каркасов приложений предоставлены классы поддержки. MFC — наиболее популярная библиотека такого типа — предоставляет два критических класса для работы с БД с помощью ODBC: CDataBase и CRecordSet. В этой главе было рассмотрено применение этих классов для работы с реляционными БД.
Реализация живучести объектов с помощью реляционных баз данных
В ЭТОЙ ГЛАВЕ
Объекты в Oracle 8
Использование внешних процедур, разработанных на языке C++
Отображение UML-диаграмм на объектнореляционные базы данных
Пример: система заказа покупок
Часть IV
Появившись более десяти лет назад, реляционные базы данных (БД) стали очень популярны. Приложения создавались на языке третьего поколения (3GL), таком как C++, чтобы получить доступ к данным, хранимым в реляционных БД, с помощью операторов SQL или какого-то другого средства разработки приложений. Реляционные БД очень хорошо подходят для хранения данных приложения, хотя не способны охватить его логику. Завершенное приложение должно содержать логику, а соблюдение этого условия часто приводит к проблемам при попытках согласования модели приложения с моделью данных. Объектно-реляционная БД сохраняет не только данные, но и логику. Такой результат достигается благодаря использованию объектов для захвата атрибутов и функциональных возможностей, связанных с конкретными сущностями.
С ростом конкуренции в торговле корпорации пытаются создавать эффективные приложения, простые для поддержки, а также рентабельные с точки зрения стоимости. Кроме того, необходимо, чтобы приложения полностью соответствовали отдельным моделям и процессам бизнеса. Приложения могут быть написаны для имитации различных состояний и процессов, например, сборочной линии на производстве. Стало обычным делом создавать такие приложения и использовать объекты для представления различных сущностей сборочной линии. Такие системы требуют многого от используемых ими приложений. Стоимость проектирования, разработки и поддержки этих приложений должна быть сведена к минимуму.
Другим фактором серьезного обсуждения является тип данных, используемых в БД. Революция Internet и World Wide Web привела к повышению сложности используемых данных. Вместе с обычными скалярными данными стали широко использоваться гипертекст, рисунки и аудиоданные. И поскольку большая часть современных приложений работает с реляционными БД, вы должны гладко (бесшовно) соединить объектную технологию с существующей реляционной.
Огас1е8 — пример популярной объектно-реляционной БД. Кроме своих встроенных типов, Oracle предоставляет набор объектов для создания типов объектов и манипулирования ими. Типы объектов дают гибкость в моделировании сущностей реального мира вместе с операциями, которые могут выполняться над этими сущностями. Огас1е8 позволяет получать доступ к хранимым данным из сред 3GL и предоставляет ряд возможностей, таких как кэширование объектов на клиенте и одновременное получение связанных объектов, что повышает производительность приложений, основанных на объектах. Из числа других объектно-реляционных БД можно выделить IBM DB2 и Sybase System 10.
Традиционные приложения реляционных БД могут извлечь выгоду из моделирования объектов и возможностей мультимедиа, предоставляемых объектно-реляционными БД. Вы можете написать приложение на языке C++, которое получит выгоду из таких объектно-реляционных БД.
Объекты в OradeS
Система типов Огас1е8 была расширена для поддержки не только объектов, но и их коллекций. Вот несколько ключевых возможностей Огас1е8, которые могут использоваться в объектно-реляционной парадигме:
Тип OBJECT — поддерживает определение структурированного объекта.
Тип REF — поддерживает возможность создания ссылок на объекты.
Тип LOB — поддерживает определение больших и неструктурированных объектов.
Тип TABLE — поддерживает неупорядоченные коллекции объектов.
Тип VARRAY — поддерживает упорядоченные коллекции объектов. VARRAY — это сокращение от variable-size array (массив переменного размера).
Для облегчения создания, получения и модификации объектов и коллекций были внесены расширения в языки SQL, DLL и DML.
Типы объектов
Тип объекта — это пользовательский тип, который может применяться для моделирования сущности реального мира. Типы объектов имеют следующие свойства:
Один или несколько атрибутов. Атрибут — это, по существу, характеристика определяемой сущности. Атрибут может быть скалярным (числом, символом и т.п.), типом объекта, коллекцией (вложенной таблицей или массивом переменного размера), REF (ссылкой на экземпляр типа объекта) или LOB (large object — большим объектом).
Нуль или более методов. Методы используются для определения логики приложения, связанной с действиями, которые могут выполняться над сущностью. Методы могут быть написаны на языке PL/SQL
Глава 17
либо в среде 3GL, таком как C++, либо в Огас1е8 с помощью Java. Несколько методов ассоциируются с объектами. Примерами таких методов являются конструктор (используемый для реализации и создания экземпляров объектов) и деструктор (используемый для удаления объектов при устранении их из системы). Эти методы создаются Oracle автоматически.
Типы объектов могут использоваться в качестве типа данных столбца.
Можно создавать таблицы типов объектов, записи которых будут заполняться с использованием типа объекта.
Реализации пользовательских типов сохраняются естественным образом в БД и могут управляться с помощью расширений SQL и DML.
Возможности БД, доступные реляционным данным (такие, как индексы и триггеры), также доступны и типам объектов.
В следующем примере показано, как с помощью языка SQL создается простой объект (для получения подробной информации о синтаксисе команды CREATE TYPE обратитесь к руководству Oracle SQL Language).
С помощью этой команды SQL из SQL*PLUS создается заголовок типа объекта:
create type complex as object ( real_part real, im_part real, member function addcomplex (x complex) return complex ) ;
I
С помощью этой команды SQL из SQL*PLUS создается тело типа объекта:
create type body complex as member function addcomplex (x complex) return complex is begin return complex (real_part + x.real_part, im_part+ x.im_part); end addcomplex;
end;
/
Ссылки на объекты
Ссылки на объекты используются для уникальной идентификации объектов и для их поиска. Для каждого объекта, хранимого в таблице ссылок, система создает уникальный идентификатор.
Тип REF в полях
Тип поля таблицы, или атрибут типа объекта вы можете объявить как REF. Такое поле может содержать ссылки на объекты объявленного типа независимо от таблицы, в которой сохранен этот объект. Впрочем, можно ограничить возможности поля типа REF так, чтобы оно содержало только ссылки на объекты определенной таблицы объектов.
Между REF и полем внешнего ключа есть несколько различий:
REF предоставляет навигационный доступ к адресуемому объекту.
■ Могут быть "висячие" ссылки, указывающие, что значение, сохраненное в поле, может быть ссылкой на несуществующий объект.
В следующем примере создается таблица ACCOUNTS со ссылкой на таблицу CUSTOMERS: /* Создать тип объекта */
CREATE TYPE customer_type AS OBJECT (cust_noCHAR(5),
accountsaccount_array);
I
I* Создать таблицу с использованием этого типа объекта */
CREATE TABLE customers OF customer_type;
Create type ACCOUNTS as object (
ACCTNO number,
Часть IV
ACCTYPE char(4), BALANCE number, CUST ref CUSTOMERS );
ПРИМЕЧАНИЕ
■ Ссылки могут создаваться только на объекты, хранящиеся в таблице объектов.
Оператор REF
С помощью оператора REF можно получить ссылку на объект в таблице объектов. Следующий оператор показывает, как вставить объект в таблицу объектов, а также как получить ссылку на объект. В следующем примере предположим, что Person является таблицей объектов:
/* Объявить переменную как ссылку на таблицу объектов*/ DECLARE reftojane REF Person;
/* Оператор REF возвращает ссылку на таблицу 'pref */ /* и эта ссылка сохраняется в переменной 'reftojane' */
BEGIN INSERT INTO people pref VALUES (Person('Jane','Doe','02-FEB-1965')) RETURNING REF(pref) INTO reftojane;
END;
После того как ссылка на объект получена, можно использовать ее несколькими способами: ■ Для поиска (pin) объекта в кэше.
Для оперирования объектом с помощью Oracle Call Interface (OCI).
В предикате операторов SELECT и UPDATE.
Оператор DEFREF
Оператор DEFREF может использоваться для получения объекта по ссылке на объект. Следующий оператор демонстрирует, как оператор DEFREF может использоваться для получения объекта и присвоения его столбцу:
UPDATE employees SET personal = DEFREF(reftojane) WHERE empid = 21145;
Коллекции
Коллекция — это упорядоченная группа элементов одного типа. Коллекция имеет следующие характеристики:
Уникальную подпись (индекс), используемую для определения позиции элемента в коллекции.
Коллекции подобны массивам; впрочем, коллекции могут иметь только одну размерность и должны индексироваться целыми значениями. Целые значения могут быть размером до 4 Гб.
Типы объектов могут использовать коллекции в качестве своих атрибутов.
Огас1е8 предоставляет два типа коллекций: вложенные таблицы (которые являются неограниченными и не поддерживают порядок, в котором элементы добавляются в коллекцию) и массивы переменного размера (которые ограничены и поддерживают упорядоченность элементов).
В следующем примере приведены команды SQL, демонстрирующие использование вложенных таблиц: /* Создать тип объекта для "projects" */
CREATE OR REPLACE TYPE project_type as OBJECT (projnoNUMBER(5),
projnameCHAR(30),
projlocationCHAR(30));
I
I* Создать таблицу проектов с использованием типа объекта */
Реализация живучести объектов с помощью реляционных баз данных Глава 17 CREATE TABLE orl_projects OF project_type; /* Вставить записи в таблицу объектов */ INSERT INTO orl_projects VALUES (111, ' venus','orlando'); INSERT INTO orl_projects VALUES (112, 'saturn','miami'); /* Создать тип для использования в качестве вложенной таблицы */ CREATE TYPE project_table AS TABLE OF project_type; I /* Создать таблицу, использующую вложенную таблицу */ CREATE TABLE employees (empnoNUMBER(5),
empnameCHAR(20),
empprojsproject_table)
NESTED TABLE empprojs STORE AS nested_proj_table; /* Вставить в таблицу объектов */ INSERT INTO employees VALUES (12345, 'MIKE', project_table(project_type(55555,'honda','orlando'), project_type(66666,'toyota','miami'))) ;
В этом примере показано, как создавать VARRAY с помощью операторов SQL: /* Создать тип объекта */ CREATE TYPE account_type as OBJECT (account_noINT,
account_typeCHAR(2),
balanceDEC(10,2));
/ /* Создать VARRAY с использованием типа объекта */ CREATE TYPE account_array AS VARRAY(10) OF account_type; I J* Создать тип объекта */ CREATE TYPE customer_type AS OBJECT (cust_noCHAR(5),
accountsaccount_array);
/ /* Создать таблицу с использованием типа объекта */ CREATE TABLE customers OF customer_type; /* Вставить в таблицу объектов */ INSERT INTO customers-
VALUES (55555, account_array(account_type(11,'C',1000.00) , account_type(22,'S', 2000.00)));
Использование внешних процедур, разработанных на языке C++
Поскольку Огас1е8 позволяет вызывать DLL-функции и процедуры кода PL/SQL, методы объектов могут быть реализованы как внешние процедуры языка C++. Использование внешних процедур, написанных на языке C++, позволяет получить эффективность 3GL; также можно использовать Win32 API.
Прежде, чем вызвать внешние процедуры PL/SQL, вы должны выполнить два следующих шага: 1. Зарегистрировать местонахождение DLL в словаре данных Огас1е8.
Create or replace library external.lib as 'e:/datacartridge/debug/cartridge.dll';
Объявить прототип функции C++ в словаре данных Огас1е8.
Живучесть объектов и шифрование Часть IV
Рассмотрим пример, в котором используется тип объекта, методы которого реализованы как внешние процедуры C++:
Create or replace package data_package as function ext_func (data CLOB) return binary_integer; end;
/ create or replace package body data_package as function ext_func (data CLOB) return binary_integer is external name "c_func" library external_lib language C with context parameters ( context, data OCILOBLOCATOR
) ;
end; / create or replace type ext_objtype as object ( dataCLOB,
member functionext_objtype_func return binary_integer
) ; I Create or replace type body ext_objtype is member function ext_objtype_func return binary_integer is begin return data_package.ext_func(data); end;
end; I
Прототип внешней функции может быть следующим:
#Include <oci.h>
#define DLLEXPORT declspec(dllexport) cdecl
int DLLEXPORT c_func (OCIExtProcContext *ctx, OCILobLocator *lobl);
int c_func (OCIExtProcContext *ctx, OCILobLocator *lobl) ( /•Здесь разместить программный код функции */ return 0 ;
}
Внешний вызов может быть протестирован с помощью PL/SQL: declare i binary_integer; х ext_objtype; begin
x : = ext_obj type(EMPTY_CLOB()) ;
i := x.ext_objtype_func();-
DBMS_OUTPUT.PUT_LINE('ext_objtype_func() returned ' || i) ;
end;
Если процесс загружает DLL без символьной отладочной информации, то вы можете воспользоваться отладчиком Visual C++ для установки точек останова и выявления ошибки. При реализации методов объектов в виде внешних процедур, написанных на языке C++, слушатель вызывает процедуру по требованию; следовательно, вы не сможете отладить процедуру с помощью стандартных методов. Для отладки в таких ситуациях в начало внешней процедуры можно поместить вызов Win32 API DebugBreakQ.
Рассмотрим объект DataStore, который может сохранять набор данных в символьном типе Oracle8 LOB (называемом CLOB). Над хранимыми данными можно проводить некоторые манипуляции, например, найти минимум, максимум, выполнить регрессию данных и т.п. Вот главные действия, которые необходимо выполнить для такой реализации:
Создать тип объекта для представления DataStore. Атрибуты и методы этого объекта должны отражать хранимые данные и функциональные возможности этого объекта. Объявим методы как external, поскольку тип требуемой обработки лучше реализуется с помощью языка C++:
Глава 17
create or replace type DataStore as object ( pid integer, name varchar2(10) , date_created date, value clob, member function DataMinimum return integer, member function DataMaximum return integer, map member function DataToInt return integer, pragma restrict_references(DataMinimum, WNDS, WNPS), pragma restrict_references(DataMaximum, WNDS, WNPS));
Объявить пакет, который будет использоваться для хранения всех внешних процедур:
create or replace package DataStore_package as function datastore_findmin (data IN clob) return integer; function datastore_findmax (data IN clob) return integer; pragma restrict_references (datastore_findmin, WNDS, WNPS); pragma restrict_references (datastore_findmax, WNDS, WNPS); end;
Для вызова упакованной функции из выражений SQL вы должны вызвать pragma RESTRICT_REFE- RENCES в объявлении пакета (а не в теле пакета, поскольку тело упакованной функции скрыто). Pragma указывает компилятору PL/SQL не давать функции доступа на чтение/запись к таблицам и(или) переменным пакета. В предыдущем примере истинны следующие утверждения:
WNDS означает "Writes no database state" (т.е. не изменять таблицы базы данных).
WNPS означает "Writes not packaged state" (т.е. не изменять переменные пакета).
Реализовать тело типа объекта DataStore (Заметьте, что следующее тело пакета приведено в демонстрационных целях и зависит от реализации функций findmin и findmax; в приведенном виде оно не будет компилироваться.):
create or replace type body DataStore is member function DataMinimum return integer is x integer := DataStore_package.datastore_findmin(data); begin return x ; end;
member function DataMaximum return integer is у integer := DataStore_package.datastore_findmax(data); begin return у ; end;
map member function DataToInt return integer is z integer := id; begin return z; end;
end;
I
Предоставить имя PL/SQL библиотеке, содержащей реализацию внешней процедуры:
create or replace library datastore_lib as '<directory_of_library> / libdatastore.so'
Объявить тело пакета и определить связи между функциями пакета и ЗОЕ-функциями библиотеки:
Create or replace package body DataStore_package as function datastore_findmin(data clob) return integer is external name "c_minimum" library datastore_lib language . c with context; function datastore_findmax(data clob) return integer is external name "c_maximum" library datastore_lib language c with context; end;
Реализовать внешние процедуры на C++. Внешняя процедура принимает параметр CLOB и использует его при вызове БД в качестве локатора LOB:
#Include <oci.h> int c_minimum (OCIExtProcContext *ctx, OCILobLocator *lobl) { ubl bufp[MAXBUFLEN]; sword retval; init_handles (ctx); retval = OCILobRead(.. . . , lobl, bufp, ....); return (process_min(bufp));
Часть IV
#Include <oci.h> int c_maximum (OCIExtProcContext *ctx, OCILobLocator *lobl) ( ubl bufp[MAXBUFLEN]; sword retval; init_handles (ctx); retval = OCILobRead(. . . . , lobl, bufp, ....); return (process_max(bufp));
)
Чтобы протестировать разработанный нами объект DataStore, выполните следующие действия:
Создайте в БД таблицу, указав функцию OCI OCILobWrite() для загрузки атрибута CLOB объекта DataStore.
Поместите в DataStore_table запись:
Insert into DataStore_table values (1, 'testl', to_date('03-28-1998', 'MM-DD-YYYY'), EMPTY_CLOB() ) ;
commit;
Используйте OCI-программу, использующую функцию OCI OCILobWrite() для загрузки атрибута CLOB объекта DataStore.
Откомпилируйте внешние процедуры (c_minimum и c_maximum) и поместите их в библиотеку.
Вызовите методы объекта, использующие процедуры PL/SQL:
Select d.DataMinimum(), d.DataMaximum() from DataStore_table d;
Отображение UML-диаграмм на объектно-реляционную базу данных
РИСУНОК 17.1. Проект простого банковскогоРИСУНОК 17.1. Проект простого банковскогоприложения.приложения.Чтобы понять, как UML-диаграммы можно отобразить на объектно-реляционную БД, используем пример приложения, показанного на рис. 17.1 и представляющего простую банковскую систему, содержащую сущности CUSTOMERS, ACCOUNTS и ADDRESS. Взаимосвязь между сущностями может быть описана следующим образом:
РИСУНОК 17.1. Проект простого банковского
РИСУНОК 17.1. Проект простого банковского
приложения.
приложения.
CUSTOMERS : ACCOUNTS = один : много
ACCOUNTS : ADDRESS = один : один
CUSTOMERS : ADDRESS =один : один
UML-спецификации для этого сценария включают следующие стадии:
Проектирование базы данных. Определяется, как UML- спецификации будут представлены с использованием типов объектов.
Генерирование на C++. Создаются заголовочные и исходные файлы для набора типов, определенных во время проектирования базы данных.
Генерирование сервера. Генерируется DLL, используемые для реализации проекта базы данных на сервере.
Для иллюстрирования процедуры на примере приложения используем OracleS.
Проектирование базы данных
Каждый тип, определенный в UML, отображается на тип данных OracleS. Затем этот тип может использоваться при определении таблиц; взаимосвязь типов реализуется в OracleS с помощью типа REF. Впрочем, поскольку ADDRESS смоделирован по строгому агрегированию, этот тип внедряемый. Отображение типа на экземпляры нескольких таблиц устанавливается с помощью идентификатора зоны (Object Database Designer — ODD — особый тип, используемый при генерировании класса C++):
Глава 17
create type address_type as object (
addressl address2 city state zip
varchar2(100), varchar2(100), varchar2(40), char(2), varchar2(10)) ;
I
create type account_type as object (
account_no
NUMBER(10),
account_spec
CHAR(5),
balance
DECIMAL(10,2),
customer
REF customer_type
address
) ;
address_type
I
Генерирование классов на C++
Object Database Designer (ODD — конструктор объектных баз данных) — это новый продукт из набора продуктов Designer/2000. Он предоставляет эффективный доступ из программ на языке C++ к базам данных Огас1е8. Этот продукт может использоваться на всех стадиях проектирования, создания и доступа к объектно-реляционной БД. ODD имеет несколько ключевых возможностей:
ODD моделирует абстрактные типы с помощью UML.
ODD трансформирует модель абстрактного типа в проект БД.
Моделирование схемы БД Огас1е8 может использоваться для определения физических объектов Огас1е8.
Генерирование SQL может выполняться для транслирования проектов объектно-реляционных БД в SQL DLL.
Генератор C++ создает объявления классов C++ и реализует UML-объекты. Сложности и тонкости доступа к БД скрыты от пользователя. В результате сгенерированные классы C++ могут трактоваться как и обычные.
Процесс разработки
Генератор объектного слоя C++ в Designer/2000 позволяет программистам C++ использовать Oracle в качестве долговременного хранилища. Кроме того, он предоставляет прозрачный способ использования хранилища. При этом требуется создать:
Модель взаимосвязи сущностей для представления хранимых классов
Реляционную схему, представляющую постоянное хранилище
Отображение между двумя предыдущими компонентами •
С использованием этих определений генератор сервера и генератор объектного слоя C++ в составе
РИСУНОК 17.2. Компоненты Object Database Designer.РИСУНОК 17.2. Компоненты Object Database Designer.Designer/2000 создадут следующие компоненты клиента и сервера:
РИСУНОК 17.2. Компоненты Object Database Designer.
РИСУНОК 17.2. Компоненты Object Database Designer.
Обычные классы C++ с общедоступными методами для реализации живучести
Код библиотеки объектного слоя C++ для прозрачной обработки процессов клиента-сервера
Определения БД Oracle для сохранения экземпляров объектов
На рис. 17.2 показаны компоненты ODD, используемые для генерирования программного кода C++. Программисты могут контролировать процесс генерирования кода C++, разбивая исходный код на отдельные исходные файлы — по одному файлу на класс.
Часть IV
Генерирование программного кода C++
Далее описано, как сгенерировать классы C++ с помощью генератора программного кода C++:
Запустите C++ Object Layer Generator.
Загрузите набор классов.
Исправьте ошибки в модели.
Выберите цель генерирования.
Установите классы, которые должны быть сгенерированы.
Убедитесь, что были заданы исходные файлы, в которых будет размещен генерируемый и глобальный программный код.
При необходимости измените стандартные установки генерирования во вкладке Options.
Перейдите во вкладку Generate.
Если программный код должен быть создан для файлов объявлений или реализаций классов модели приложения, установите соответствующие флажки.
Если должны быть сгенерированы новые разделы кода, установите флажок Generate Classes.
Если должны быть обновлены разделы существующего кода, установите флажок Regenerate Classes.
Щелкните на кнопке Generate.
C++ API состоит из сгенерированных классов и библиотеки классов времени выполнения C++. Библиотека классов времени выполнения C++ инкапсулирует функции для работы с БД, такие как соединение с БД, транзакции и описания. В этом интерфейсе API ссылки на объект Огас1е8 представлены smart-указателями; отображение классов C++ предоставляется для различных типов данных Oracle, таких как:
Строки
Числа
Даты
Коллекции
LOB
Ссылки на объекты
Следующие объявления классов генерируются для одного типа UML:
Класс для самого типа
Класс smart-указателя
Класс коллекции
■ Класс-итератор
Взгляните на пример класса, который будет сгенерирован для UML-типа Customers:
Class COLCustomers : public OLDBObject { public:
COLCustomers () ;
-COLCustomers();
static COLCustomersRef Create(COLLIFETIME life = COLLIFETIME :: Persistent, OLZONEID id = OLZONEID:;Default);
static void Query(OLZONEID id, COLCustomersRefSet SSet, OLString SPredicate);
// Доступ к атрибутам
OLNumber SGetID() const;
void SetID(const OLNumber SID);
OLString SGetNameO const;
void SetName(const OLString SName);
COLAccountsRefSet SGetAccount() const;
COLAddressRef SGetAddress() const;
Глава 17
И Методы bool AddAccount ( COLAccountsRef SAccounts); bool RemoveAccount(COLAccountsRef SAccounts); private: // Атрибуты OLNumber m_ID ; OLString m Name;
// Связи COLAccountsRefSet m Accounts; COLAddressRef mAddress; );
Генерирование сервера
Стадия генерирования сервера включает генерирование SQL DDL, используемого для реализации на сервере проекта БД.
Инициализация
Прежде чем сможет произойти любое взаимодействие с объектами C++, приложение должно инициализировать библиотеку классов времени выполнения C++ и предоставить соединение с БД, определяемое сгенерированным идентификатором зоны. Библиотека классов времени выполнения C++ содержит следующие важные компоненты:
Диспетчер контекста для управления соединениями.
Метаданные для настроек времени выполнения, таких как потребление памяти, политика блокирования и т.д.
Кэш для управления хранимыми экземплярами объектов C++.
В следующем фрагменте программного кода показано, как может инициализироваться контекст времени выполнения C++ и как создается соединение с БД для конкретной зоны:
COLContext::Initialize();
COLDatabaseConnection *db = new COLDatabaseConnection(OLZONEID::Default); if (db->Connect(OLZONEID::Default, "scott/tiger@ORCL”) ! = true) { // Ошибка! Соединение установить не удалось.
}
Запрос объекта
Объект C++ может быть восстановлен из долговременного хранилища пользовательским приложением, выполняющим запрос с предикатом, который определяет требуемый объект. В следующем примере предикат является частью конструкции SQL WHERE, которая отбирает набор ссылок на заказчиков с идентификатором 25. Корневой объект пользовательское приложение получает с помощью метода Query(), как показано в следующем коде:
COLCustomersRefSet Custrs;
COLCustomers::Query(OLZONEID::Default, custrs, "ID = 25");
Коллекции с итераторами
Коллекциям итераторы необходимы для того, чтобы управлять относящимися к этим коллекциям объектами. В результате разыменования итератора представляется ссылка C++ на генерируемый объект. В следующем фрагменте кода показано, как итераторы могут быть созданы для коллекции заказчиков, которая получается в результате вызова Query() над таблицей Customers:
for (COLCustomers iter(custrs); iter; iter++) < COLCustomersRef Customers = *iter;
OLString name = customers-XSetName () ; }
Живучесть объектов и шифрование
Часть IV
Перемещение по связям
Связь между сгенерированными классами С+-’ представляется с помощью REF или SET, в зависимости от моши соединения. Операторы REF и SET могут использоваться для перемещения по связям между классами. Библиотека времени выполнения C++ должна убедиться, что объект Accounts уже восстановлен из долговременного хранилища; в ином случае REF будет некорректен. Подобным образом набор объектов восстанавливается только тогда, когда SET итерируется и разыименовывается. В следующем фрагменте программного кода показано движение по связям:
COLAccountRef Accounts = *iter;
COLCustomersRef Customers = Accounts-XSetCustomer();
Изменение хранимых объектов
Над хранимыми объектами может быть выполнена модификация трех видов:
Создание
Обновление
Удаление
По завершению изменений пользовательское приложение должно пометить объект C++ как модифицированный. Классы C++ могут быть созданы с методами get или set для каждого атрибута или без них; следовательно, библиотека времени выполнения C++ не может определить, изменен ли объект, если приложение не сообщит библиотеке о произведенном им изменении. В следующем фрагменте программного кода показано, как можно изменить адрес и пометить объект как модифицированный:
COLAddressKet address = Customers->GetAddress() ;
address->Set3txeecl("Wisconsin Avenue.");
address->MarkModi£iedО;
По завершении 'екущей транзакции объект записывается в долговременное хранилище.
ПРИМЕМПРИМЕМ
ПРИМЕМ
ПРИМЕМ
После создания се-ъек: автоматически помечается как созданный..
Удаление хранили то объекта также требует отдельного рассмотрения. Хотя вы можете использовать оператор C++- delete для удаления объекта из памяти, однако с его помощью вы не сможете удалить объект из долговременного хранилища.
В следующем фрагменте кода показано, как Account может быть помечен как удаленный:
OlAccount^Rvcf Accounts ~ *iter;
Accounts->Dei;troy () ;
Ниже показано, как новый объект Customer может быть создан с использованием стандартной зоны:
COLCustomersRaf. Customer = new COLCustomers(COLIFETIME::Persistent, OLZONEID: :Default) ;
Работа с транзакциями
Набор изменений, вносимых в объекты C++, объединяется в транзакцию. Изменения, связанные с текущей транзакцией, вносятся при совершении транзакции. Библиотека времени выполнения C++ поддерживает вложенные транзакции. У вас может быть внешняя транзакция, которая открывает внутренние транзакции дня выполнения особых задач. Синхронизация транзакций достигается с использованием долговременного хранилища. Другими словами, при открытии внутренней транзакции все изменения, внесенные до этого момента, помещаются в долговременное хранилище; в случае прерывания внутренней транзакции все внесенные изменения могут быть отменены с помощью информации, помещенной в долговременное хранилище:
COlTransaction trans (OLZONEID::Default);
trans.Begin(),
При выполнении транзакции вносятся все изменения, содержащиеся в ней:
trans.Commit();
Глава 17
ПРИМЕЧАНИЕ
Если в предыдущем примере транзакция будет внешней, все изменения во внутренних транзакциях станут постоянными, ч
Блокирование
Приложение, используемое в многопользовательской среде, должно предоставлять контроль параллелизма. Контроль параллелизма достигается путем блокирования объекта, которое может выполняться на нескольких уровнях:
В рамках контекста времени выполнения C++:
COLContext::SetLockMode(COLLockMode::Default);
По требованию (например, при обновлении объекта). Следующий фрагмент программного кода получает текущее значение объекта из долговременного хранилища, а затем блокирует его:
COLCustomersRef Customers = *iter;
Customers->Refresh(true);
По завершению работы с объектом приложение должно снять блокировку. Блокировка может быть снята в результате транзакции.
Важно понимать, что метод RefreshQ охраняет целостность и постоянство БД. Если объект не заблокирован, то любой объект в памяти может выйти из синхронизации с долговременным хранилищем.
SQL-интерфейс
Операции SQL могут выполняться на соединении с БД с помощью SQL-интерфейса, предоставляемого библиотекой времени выполнения C++. В следующем фрагменте кода показано, как интерфейс может использоваться для инкапсуляции Oracle Call Interface (OCI) в C++:
Unsigned long rows = 0;
CCRDBCursor statement;
CCRDBVariable var(EVType:: CT_REF);
statement.SetConnection(db->GetConnection());
statement.Setstatement("select ref(cust_tab) from cust_tab");
statement.AddVariable(Svar);
statement.Bind(true);
statement.Execute(trows);
Здесь создается объект SQL, устанавливается оператор SQL SELECT, связывается переменная и затем выполняется оператор.
Интерфейс CURSOR для вложенных таблиц
Огас1е8 предоставляет новую конструкцию CURSOR(SELECT...), которая может использоваться для создания результирующего набора вложенной таблицы. С помощью этой возможности можно получать данные вложенных таблиц и манипулировать ими. В листинге 17.1 показано, как использовать курсоры результирующего набора для вывода информации о всех счетах конкретного заказчика.
Листинг 17.1. Использование курсоров для манипуляции вложенными таблицами
Select_customer_accounts() {-
account_type *account_p;
account_type_ref *account_ref_p;
sql_cursor account_cur;
char accountname[20]; int custid;
EXEC SQL DECLARE custaccount_cur CURSOR FOR
SELECT c.id, CURSOR(SELECT * FROM TABLE(c.accounts)) FROM customers c;
EXEC SQL OPEN custaccount_cur;
EXEC SQL ALLOCATE :account_cur;
EXEC SQL ALLOCATE :account_ref_p;
while (1){
Часть IV
EXEC SQL FETCH custaccount_cur INTO :custid, :account_cur;
cout « "Customer ID :" « custid « " has the following accounts :
\n \n";
while(1) {
EXEC SQL FETCH :account_cur INTO :account_ref_p;
EXEC SQL OBJECT DEREF :account_ref_p INTO :account_p;
EXEC SQL OBJECT GET name FROM : account_p INTO : accountname; cout « accountname « "\n";
}
}
ЕХЕС
SQL
CLOSE
custaccount_cur
ЕХЕС
SQL
CLOSE
:account_cur;
ЕХЕС
SQL
FREE
:account_cur;
ЕХЕС
SQL
FREE
:account_ref_p;
Функция select_customer_accounts() используется для вывода счетов всех заказчиков с помощью атрибута вложенной таблицы accounts в объекте customers. В листинге 17.1 также используется два цикла: один для открытия курсора custaccount_cur и получения объекта каждого заказчика, а второй — для прохождения по всем счетам конкретного заказчика. Конструкция CURSOR (SELECT * FROM TABLE(c.accounts)) определяет курсор результирующего набора по счетам; курсор custaccount_cur используется для получения курсора результирующего набора.
Доступ к атрибутам объектов
Атрибуты объекта не могут изменяться обычными функциями и присваиваниями C++, поскольку те представлены непрозрачными типами OCI8. Pro С C++ предоставляет механизм преобразования OCIString к типу С char и OCINumber к типам С int, float и double. Например, следующий оператор может использоваться для установки баланса счетов в новое значение:
EXEC SQL OBJECT SET balance OF :account_p TO :new_balance;
ПРИМЕЧАНИЕ
Pro C/C++ — это среда разработки приложений, предоставляемая Oracle. Для подключения к БД в ней используется C/C++ и Oracle Call Interface(OCI).
Операторы управления транзакцией
Несколько операторов могут использоваться для управления транзакциями. Совершение и откат транзакций могут обрабатываться с помощью операторов EXEC SQL COMMIT и EXEC SQL ROLLBACK:
EXEC SQL COMMIT
Этот оператор закрепляет в БД изменения, вызванные операторами INSERT, UPDATE и DELETE, содержащимися в транзакции. Кроме того, изменения объектов, которые были созданы, обновлены и удалены навигационно, сбрасываются на сервер. Этот оператор уведомляет кэш объектов, что эти объекты больше не используются, и удаляет их из кэша, снимая с них метку.
EXEC SQL ROLLBACK-
Этот оператор закрепляет в БД изменения, вызванные операторами INSERT, UPDATE и DELETE. Этот оператор уведомляет кэш объектов, что эти объекты больше не используются, и удаляет их из кэша, снимая с них метку.
Пример: Система заказа покупок
Рассмотрим пример системы заказа покупок, которая преобразует модель взаимоотношений сущностей в объектно-реляционный проект.
Описание системы
Система заказа покупок содержит несколько важных сущностей. Покупатели могут размещать заказы, а заказ на приобретение является коллекцией таких заказов.
Глава 17
Реляционная модель
Реляционная модель может использоваться для представления такой системы с использованием четырех следующих таблиц:
customers. Содержит специфическую информацию о заказчике, такую как его адрес.
purchase_order. Содержит информацию о конкретных заказах.
line_items. Содержит информацию о конкретном заказе.
stock. Содержит информацию о специфическом запасе элементов, которые могут быть приобретены заказчиками при размещении заказов на покупку.
Отношения между таблицами
Заказчик может сделать много заказов. Чтобы указать этот факт, атрибут внешнего ключа размещается в таблице purchase_order, которая ссылается на таблицу customers. Каждый заказ на покупку может включать много строк, описывающих отдельные заказы. Для указания этого факта атрибут внешнего ключа размещается в таблице line_items, которая ссылается на таблицу purchase_order. Таблица line_items также ссылается на таблицу stock для получения информации о наличии товаров:
Create table customers ( custnonumber,
custnamevarchar2(50),
addresslvarchar2(100),
address2varchar2(100),
cityvarchar2 (40) ,
statechar (2) ,
zipvarchar2 (10) ,
phone1varchar2 (20) ,
phone2varchar2(20) ,
primary key(custno));
create table purchase_order ( pononumber,
custnonumber references customers(custno) ,
orderdatedate,
shiptoaddresslvarchar2(100),
shiptoaddress2varchar2(100),
shiptocityvarchar2(40),
shiptostatechar(2),
shiptozipvarchar2(10),
primary key(pono));
create table stocks stockno stockname price
primary key
( number, varchar2(100), number, (stockno));
create table line_items ( lineitemnonumber,
pononumber referencespurchase_order(pono),
stocknonumber referencesstocks(stockno),
quantitynumber,
primary key(pono,lineitemno));
custno custname
address!address 2citystate
УР
phone 1
phone2
1John Doe
2 Foothill Dr.NULLOrlandoFL
32817
407-555-1296
NULL
2Mike
36 CollegeNULLR. Shores CA
95054
415-555-7620
NULL
Jordan
Park Avenue
ponocustno
orderdate shiptoaddresslshiptoaddress 2
shipcity
shipstate
shipzip
1011
sysdate18 Madison St.NULL
NY
NY
92312
1022
sysdate5 Church St.NULL
Miami
FL
39815
15 Зак. 53
450
I Живучесть объектов и шифрование
Часть IV
stockno
stockname
price
1001
Intel Pentium
2100.00
1002
NC-workstation
220.50
1003
Sun Ultra
6900,00
lineitemno
pono
stockno
quantity
01
101
1001
25
02
101
1002
50
01
102
1002
20
02
102
1003
5
Хотя вы можете писать приложения в среде 3GL типа C++, предыдущая модель для получения желаемого результата требует нескольких сложных соединений. Впрочем, эта система может быть упрощена с помощью объектного подхода, как показано в следующем программном коде:
ПРИМЕЧАНИЕ
Этот пример показывает, как преобразовать модель взаимоотношений сущностей в объектно-реляционный проект. Действительные объекты создаются с помощью SQL. Хотя приложения C++ могут быть написаны как с реляционным, так и с объектно-реляционным подходом, второй упрощает работу над такими приложениями.
create type stocks_t as object ( stocknonumber,
stocknamevarchar2(100),
pricenumber);
I
create type address_t as object ( addresslvarchar2(100),
address2varchar2(100),
cityvarchar2(40) ,
statechar (2),
zipvarchar2(10)) ;
I
create type phone_t as varray(10) of varchar2(20) ; I
create or replacetype customer_tasobject(
custnonumber,
custnamevarchar2(50),
addressaddress_t,
phone_listphone_t,
po_listpo_reflist_t,
member function add_po(porefREFpurchase_order_t) return po_reflist_t);
I
create type line_item_t as object ( lineitemnonumber,
stockrefref stocks_t,
quantitynumber);
I create type line_item_list_t as table of line_item_t;
I
create or replace type purchase_order_t as object ( pononumber,
custrefref customers_t,
orderdatedate,
line_item_list line_item_list_t, shipto_addraddress_t);
I
create type po_ref_list as table of REF purchase_order_t;
I
В ЭТОЙ ГЛАВЕВ ЭТОЙ ГЛАВЕ■ Обзор объектно-ориентированных баз данных■ Обзор объектно-ориентированных баз данных
В ЭТОЙ ГЛАВЕ
В ЭТОЙ ГЛАВЕ
■ Обзор объектно-ориентированных баз данных
■ Обзор объектно-ориентированных баз данных
Объектно-ориентированные базы данных
Стандарт ODMG
Приложение на C++ для ведения счетов
Живучесть данных
Базы данных и транзакции
Технические вопросы объектно-ориентированных баз данных
Часть IV
Для большинства корпоративных разработчиков почти вся их деятельность посвящена обеспечению покупателей лучшими средствами для сбора, получения и анализа данных. Управление данными обычно является центром стратегического планирования и разработки информационных технологий. Практически невозможно найти бизнес, в котором не применяется какая-либо из форм баз данных.
Эта глава начинается с описания объектно-ориентированных базы данных (ООБД) и систем управления этими базами данных (СУООБД). В этой главе мы разработаем простое приложение, использующее стандарт интерфейса объектных БД C++ Object Data Management Group (ODMG, ранее Object Database Management Group). Завершается глава обсуждением некоторых вопросов проектирования и реализации СУООБД.
Обзор объектно-ориентированных баз данных
Система управления объектными БД предоставляет несколько ключевых возможностей:
Живучесть. Данные сохраняются на постоянном накопителе и постоянны среди различных приложений и сеансов.
Транзакция. Целостность данных поддерживается, поскольку набор операций в транзакции выполняется как неделимая единица.
Параллелизм. Доступ к данным могут одновременно получать несколько приложений. Механизмы блокирования служат для гарантии, что данные не будут повреждены или непостоянны при одновременном доступе к ним из нескольких приложений.
Запрос. Запрос по данным может осуществляться с помощью одного или нескольких языков запросов. Наиболее популярным языком запросов для реляционных баз данных является язык SQL.
Другие. Другие возможности включают восстановление, устойчивость к сбоям и воспроизведение данных (реплицирование данных).
Системы управления реляционными БД в текущее время занимают доминирующую позицию на рынке БД благодаря своей устойчивости и зрелости. В них данные организованы в таблицы, моделирующие сущности приложения. Каждая таблица содержит набор записей, или строк, представляющих собой экземпляры сущностей. Поля таблицы соответствуют атрибутам сущности.
Таблица имеет первичный ключ, уникально идентифицирующий каждую запись. Например, счет может идентифицироваться номером счета, поскольку два счета не могут иметь одинаковые номера. Следовательно, номера счетов являются первичным ключом таблицы счетов. Первичный ключ не обязательно должен содержаться в одном поле. Например, если в одной школе нет ни одного ученика с одинаковыми именем и фамилией, то комбинация фамилии и имени ученика уникально идентифицируют ученика.
Взаимоотношения сущностей представлены концепцией внешнего ключа. Если каждый счет должен относиться к заказчику, мы можем объявить поле номера заказчика таблицы счетов в качестве внешнего ключа таблицы заказчиков.
Доступ к данным в реляционных БД обычно осуществляется с помощью запросов SQL или с использованием других методов, указанных производителем. Часто приложения создаются на языках программирования, не зависящих от реляционной БД. Такой подход позволяет получать доступ к одной реляционной БД с помощью приложений, написанных на различных языках, таких как C++ и COBOL. Приложения должны предоставить отображение типов данных своего языка на тип данных БД. Например, для отображения класса C++ Person на таблицу приложение должно отобразить каждую переменную-член класса на поле таблицы:
class Person <
string mSurname;
string mFirstName;
int mAge;
}
Несмотря на раннюю стадию своего развития, ООБД предоставляет следующие преимущества над реляционными БД:
■ Плотную интеграцию с языками объектно-ориентированного программирования. ООБД предоставляют интерфейс между данными и языком программирования. Такое взаимоотношение позволяет разработчикам приложений создавать и получать доступ к объектам БД прямо из обычного языка программирования.
Глава 18
■ Гибкие объявления типов данных. ООБД поддерживают создание пользовательских типов данных и манипуляцию ими. Они способны непосредственно сохранять экземпляры типов (классов) приложения, определенных в обычном языке программирования.
В Автоматические сети поддержки объектов. ООБД поддерживают хранение, получение и перемещение объектов в сетях. Каждый объект в сети связан с одним или несколькими другими объектами. Отношения могуч иметь типы: один-к-одному, один-ко-многим или многие-ко-многим.
Каждый производитель БД предоставляет покупателю такую реализацию СУООБД, которая лучшим образом подходил для его бизнеса. Такая классификация обеспечивает тесное моделирование областей применения приложения в реализации БД; она также позволяет покупателям разрабатывать лучшие приложения, способные использовать все преимущества СУООБД. Побочный эффект, конечно, состоит в том, что пользователь остается привязанным к конкретному производителю. Перенос приложений из одной реализации СУООБД в другую чрезвычайно сложен, если вообще возможен. Покупатели часто озабочены тем, что им приходится ограничиваться рамками частной реализации особого продукта. Поскольку компании — производители СУООБД относительно малы, и, следовательно, их деятельность в большой степени зависит от изменений рынка, надежда на отдельный продукт — далеко не лучшее вложение капитала. Без наличия стандартов невозможно надеяться на признание СУООБД на рынке.
Стандарт ODMG
Исходя из необходимости в стандарте ODBMS, в 1991 г. была сформирована группа управления объектными 6у захш штых (Object Database Management Group (ODMG)) для определения и продвижения стандарта хранения объектов. В состав ее членов в настоящее время входят главные производители объектных БД, а также ipyine заинтересованные лица. Стандарт ODMG определил связь БД с тремя наиболее популярными объектно-ориентированными языками программирования: C++, Java и Smalltalk. Используя связь с языками программирования, разработчики могут включать в приложения возможность манипулирования объектами на родном синтаксисе языка.
В слелующи?. разделах мы реализуем очень простую программу ведения счетов, позволяющую управлять ассортиментом товаров и создавать счета. Начнем с обычного приложения C++, не реализовав в нем хра- нимости данных; затем улучшим его, добавив возможность сохранять и получать данные из объектной БД с помощью ODMG стандарта.
Приложение на C++ для ведения счетов
В листингах 18J —18.6 приведен полный программный код приложения для ведения счетов. В этом приложении автор сделал три отклонения от обычной практики кодирования. Во-первых, все члены классов в этом примере объявлены общедоступными, поэтому программный код проще в отслеживании. В приложениях реального мира переменные-члены обычно объявляются частными, а для доступа к ним реализуется набор функций. Во-вторых, приложение использует генерируемые компилятором копии конструктора и операторов присваивания, поскольку в предоставлении наших собственных нет необходимости. Хорошей практикой также является явное определение этих функций. Последнее отклонение от стандартной практики кодирования состоит в очень незначительном коде обработки ошибок. Пример приложения предположительно менее устойчив без возможностей исчерпывающей обработки ошибок. Единственная причина таких отклонений от общепринятой практики программирования в этом примере заключается в уменьшении объема программного кода для лучшей демонстрации основной функциональности этого приложения.
Приложение разделено на шесть листингов, поэтому описание каждого из компонентов мы можем поместить рядом с программным кодом. Для компиляции приложения в проект должны быть добавлены все файлы. Сначала рассмотрим класс Product, приведенный в листинге 18.1.
Листинг 18.1. Приложение ведения счетов: класс Product
//==============--======■=:.-========================:===============
// Product.h - Объявление класса Product
#ifndef Product_H
#define Product_H
#include <iostream>
#include <string>
using namespace std;
Часть IV
class Product < public: // Конструктор и деструктор Product();
Product(const strings newID, const strings newName, const int newPrice) ; virtual -Product() ;
II Переменные-члены stringmID;
stringmName;
intmPrice;
// Перегруженный оператор вставки friend ostreams operator«(ostreams os, const Product* const aProduct);
#endif
Н-=====»===я«1=====га=====м=======-ж==^^ I( Product.cpp - Реализация класса Product //==============================================^^
#include "Product.h"
// Конструктор и деструктор Product::Product() : mID(""), mName(""), mPrice (0) {}
Product::Product(const strings newID, const strings newName, const int newPrice) : mID(newID), mName(newName), mPrice(newPrice) {)
Product::-Product() {)
// Перегруженный оператор вставки ostreams operator«(ostreams os, const Product* const aProduct) (
os « aProduct->mID « "\t" « aProduct->mName « "\t" « aProduct->mPrice; return os;
)
Класс Product относительно прост. Класс string, определенный в стандартной библиотеке, используется для хранения значений идентификатора продукта и его названия. По сравнению с заканчивающимися нулем строками С-стиля, класс string C++ более мощный и намного проще в использовании. Класс string объявлен в header-файле <string> в namespace std и предоставляет набор функций для манипулирования строками символов.
Здесь определены два конструктора. Стандартный конструктор просто устанавливает все переменные - члены в пустые или нулевые значения. Второй конструктор используется для создания продукта, переменные-члены которого устанавливаются в соответствии с принимаемыми параметрами конструктора. В нашем приложении стандартный конструктор используется неявно. Он предназначен для создания списка продуктов; стандартные контейнеры C++ требуют, чтобы элемент контейнера имел стандартный конструктор. Более подробно вы узнаете об этом при рассмотрении класса карты продуктов.
Перегруженный оператор вставки определен для использования указателя объекта продукта в качестве параметра и для вывода переменных-членов объекта продукта. Объекты продуктов автор решил передавать по указателям, поскольку такой подход будет полезен позже при обсуждении некоторых возможностей ODMG.
Счет обычно состоит из заголовка и списка элементов. Каждый элемент определяет продаваемый продукт и его количество. Класс Invoiceitem приведен в листинге 18.2.
Листинг 18.2. Приложение ведения счетов: класс Invoiceitem
И Invoiceitem.h - Объявление класса Invoiceitem
//==-===к====«===ю=»==-же=======м-===и=--===^^
Глава 18
#ifndef InvoiceItem_H
#define InvoiceItem_H
#include <iostream> using namespace std;
#include "Product.h"
class Invoice;
class Invoiceitem { public:
11 Конструкторы и деструктор Invoiceitem();
Invoiceitem(Product* pProduct, const int newQuantity); virtual ~lnvoiceltem();
// Переменные-члены Invoice*mplnvoice;II Указатель на связанный объект счета
Product*mpProduct;И Указатель на объект продукта
intmQuantity; // Продаваемое количество
// Другие функции-члены // Получить количество элементов intGetTotalO const;
// Перегруженный оператор вставки friend ostreams operator«(ostreams os, const Invoiceitems anlnvoiceltem); };
#endif
11===============^=================================^^
11 Invoiceitem.cpp - Реализация класса Invoiceitem
//=============================================================
#include "Invoiceitem.h"
// Конструкторы и деструктор
Invoiceitem::Invoiceltem() : mpProduct(0), mQuantity(0) {}
Invoiceitem::Invoiceitem(Product* pProduct, const int newQuantity) : mpProduct(pProduct), mQuantity(newQuantity) (}
// Деструктор
// He удалять объекты продуктов Invoiceitem::~lnvoiceltem() {}
// Получить количество элементов int Invoiceitem::GetTotal() const {
return mpProduct->mPrice * mQuantity; }
// Перегруженный оператор вставки ostreams operator«(ostreams os, const Invoiceitems anlnvoiceltem) (
os « anlnvoiceltem.mpProduct « "\t" « anlnvoiceltem.mQuantity « "\t" « anlnvoiceltem.GetTotal(); return os;
}
Класс Invoiceltem имеет три переменных-члена. Первая является указателем на родительский объект Invoice, как мы и обсудили ранее. Вторая является указателем на продукт, а третья — количеством единиц продаваемого продукта. Вместо указателя мы могли бы поместить в класс и сам объект продукта — оба метода корректны. Преимущество хранения указателя состоит в том, что указатель позволяет ссылаться на продукт в списке продуктов и устанавливает связь между элементом счета и продуктом. Если мы решим
Часть IV
изменить имя продукта, элемент счета автоматически получит обновленное имя продукта. Это поможет сохранить целостность данных.
Сохранение объекта, напротив, не поддерживает связи между продаваемым продуктом и списком доступных продуктов. Следовательно, любые изменения в продукте не скажутся на элементе счета.
ПРИМЕЧАНИЕ
Часта предпочтительнее создавать снимок информации о продукте во время создания счета и сохранить этот снимок вместе со счетом. Если позже мы выведем счет, то увидим информацию о продукте, которая была представлена на'«ж время его продажи. В этом примере, впрочем, мы более заинтересованы в поддержании связи между элементом счета и продуктом. ., -
Стандартный конструктор также определяется явно (хотя и не используется в проекте). Этот конструктор пригодится при создании списка пустых элементов счетов (стандартный контейнер C++ для выделения памяти под элементы вызывает их стандартные конструкторы). Деструктор выполняет обычную очистку; нам не нужно удалять объект продукта при удалении счета.
Функция-член GetTotal() предназначена для вычисления полной стоимости элемента. Для хранения этого значения мы могли бы создать переменную-член, но в нашем случае это излишне. Кроме того, это могло бы привести к противоречивости данных при изменении стоимости продукта. Элемент счета может быть выведен с помощью перегруженного оператора вставки. Этот оператор выводит информацию о стоимости продукта, количестве продуктов и полной стоимости.
Полностью определив класс Invoiceitem, можно переключить внимание на класс Invoice. В листинге 18.3 приведены объявление и реализация класса Invoice.
Листинг 18.3. Приложение ведения счетов: класс Invoice
//======================================================
// Invoice.h - Объявление класса Invoice
//==^===========^=================^^^
♦ifndef Invoice_H
♦define Invoice_H
♦include <iostream>
♦include <string>
♦include <list> using namespace std;
♦include "Invoiceltem.h"
typedef listCInvoiceltem *>InvoiceltemList;
typedef InvoiceltemList::const_iteratorIIL_CItor;
typedef InvoiceltemList::iteratorIIL_Itor;
class Invoice <
public:
// Конструкторы и деструктор
Invoice!) ;
Invoice(const long mID, const strings newDate); virtual -Invoice ();
II Переменные-члены
longmID;II Идентификатор счета
stringmDate;II Дата создания счета
InvoiceltemListmlterns;// Список указателей на элементы счетов
void Additem(Invoiceitem* pltem);// Добавить элемент в список счетов
int GetTotal()const;II Полная стоимость счета
// Перегруженный оператор вставки
friend ostreams operator«(ostreams os, const Invoices anlnvoice) ;
♦endif
//===============================================
// Invoice, cpp - Реализация класса Invoice
Глава 18
#include "Invoice.h"
II Конструкторы и деструктор Invoice::Invoice() : mID(0), mDate("") (}
Invoice::Invoice(const long newID, const strings newDate) : mID(newID), mDate(newDate) (}
Invoice::-Invoice() <
// Удалить все элементы счета
for (IIL_Itor i = mltems .begin () ; i != ml terns .end () ; ++i) delete *i;
)
// Добавить элемент в список счетов void Invoice::Additem(Invoiceitem* pltem) (
mltems.push_back(pltem);// Используем стандартную функцию списка push_back()
pltem->mplnvoice = this; // Связываем элемент счета с текущим счетом
}
// Получить полное количество счетов int Invoice::GetTotal() const
int intTotal = 0;
// Итерация по списку элементов и вычисление полной стоимости
for (IIL_CItor i = mltems .begin () ; i != mltems. end() ; ++i) intTotal += (*i)->GetTotal () ;
return intTotal; )
// Перегруженный оператор вставки ostreams operator«(ostreams os, const Invoices anlnvoice) (
// Вывести номер счета
os « "Invoice: " « anlnvoice.mID « "\n" « "Date : " « anlnvoice.mDate « "\n";
// Вывести все элементы счета
for (IIL_CItor ci = anlnvoice.mlterns.begin(); ci != anlnvoice.mltems.end(); ++ci) os « “ci « "\n";
// Вывести полную стоимость os « "Total: ” « anlnvoice .GetTotal () « "\n";
return os;
Счет имеет номер, или идентификатор, уникально определяющий счет. Другими переменными-членами являются дата создания счета и список элементов. Имеется два способа привязки элементов к счетам: мы могли бы сохранить сами объекты элементов или указатели на внешние объекты элементов. С точки зрения связи классов большого различия между этими способами не существует, поскольку элементы счета не могут существовать не связанными со счетом. На практике внешнее сохранение элементов упрощает создание запросов. Например, если нам необходим отчет о движении товара, мы можем просмотреть все элементы счета для получения списка элементов счета, связанных с конкретным продуктом. В этом случае в счете мы сохраняем список указателей на элементы, позволяя тем самым объектам элементов находиться за пределами класса Invoice.
При создании счета создается пустой список счетов с использованием стандартного конструктора стандартного класса списка. При удалении счета удаляются все его элементы, поскольку элементы не могут существовать без самого счета.
Часть IV
Функция-член Addltem() просто добавляет указатель на элемент счета в список элементов, т.е. элемент связывается со счетом. Кроме того, эта функция устанавливает переменную-член mplnvoice элемента в указатель на счет. Функция GetTotal() просто подсчитывает стоимость всех элементов и возвращает результат. Перегруженный оператор вставки выводит идентификатор счета, дату создания счета и все его элементы.
К этому моменту определены все элементы нашего приложения ведения счетов. Впрочем, наше приложение — это не просто коллекция несвязанных продуктов и счетов. Оно должно организовать их так, чтобы объекты были легко доступными. Например, при создании счета мы должны знать, существует ли продукт и где его искать. Для этого необходимо создать список всех доступных продуктов. Желательно, чтобы к продукту можно быть получить доступ по его идентификатору. В листинге 18.4 определен список продуктов.
Листинг 18.4. Приложение ведения счетов: карта продуктов
//==================================================== И ProductMap.h - Объявление класса ProductMap
//Этот класс инкапсулирует список продуктов и связанные
Ис ним функции.
//=====================================================
♦ifndef ProductMap_H
♦define ProductMap_H
♦include <iostream>
♦include <string>
♦include <map> using namespace std;
♦include "Product.h"
typedef map<string, Product *> typedef ProductPtrMap::const_iterator typedef ProductPtrMap::iteratorclass ProductMap { public:// Конструктор и деструктор ProductMap () ;virtual “ProductMap();11 Функции-члены voidAdd() ;voidEdit() ;voidDelete ();Product* Find(const strings id); boolIsEmptyO const;ProductPtrMap mProducts;// Перегруженный оператор вставки friend ostreams operator«(ostreams os, );♦endiftypedef map<string, Product *> typedef ProductPtrMap::const_iterator typedef ProductPtrMap::iteratorclass ProductMap { public:// Конструктор и деструктор ProductMap () ;virtual “ProductMap();11 Функции-члены voidAdd() ;voidEdit() ;voidDelete ();Product* Find(const strings id); boolIsEmptyO const;ProductPtrMap mProducts;// Перегруженный оператор вставки friend ostreams operator«(ostreams os, );♦endifProductPtrMap; PPM_CItor; PPM Itor;ProductPtrMap; PPM_CItor; PPM Itor;// Добавить продукты в список// Отредактировать продукт// Удалить продуктыII Найти продукт с заданным идентификатором // Проверить, пуста ли карта// Карта указателей на продуктыconst ProductMapS aProductMap);// Добавить продукты в список// Отредактировать продукт// Удалить продуктыII Найти продукт с заданным идентификатором // Проверить, пуста ли карта// Карта указателей на продуктыconst ProductMapS aProductMap);
typedef map<string, Product *> typedef ProductPtrMap::const_iterator typedef ProductPtrMap::iterator
class ProductMap { public:
// Конструктор и деструктор ProductMap () ;
virtual “ProductMap();
11 Функции-члены voidAdd() ;
voidEdit() ;
voidDelete ();
Product* Find(const strings id); boolIsEmptyO const;
ProductPtrMap mProducts;
// Перегруженный оператор вставки friend ostreams operator«(ostreams os, );
♦endif
typedef map<string, Product *> typedef ProductPtrMap::const_iterator typedef ProductPtrMap::iterator
class ProductMap { public:
// Конструктор и деструктор ProductMap () ;
virtual “ProductMap();
11 Функции-члены voidAdd() ;
voidEdit() ;
voidDelete ();
Product* Find(const strings id); boolIsEmptyO const;
ProductPtrMap mProducts;
// Перегруженный оператор вставки friend ostreams operator«(ostreams os, );
♦endif
ProductPtrMap; PPM_CItor; PPM Itor;
ProductPtrMap; PPM_CItor; PPM Itor;
// Добавить продукты в список
// Отредактировать продукт
// Удалить продукты
II Найти продукт с заданным идентификатором // Проверить, пуста ли карта
// Карта указателей на продукты
const ProductMapS aProductMap);
// Добавить продукты в список
// Отредактировать продукт
// Удалить продукты
II Найти продукт с заданным идентификатором // Проверить, пуста ли карта
// Карта указателей на продукты
const ProductMapS aProductMap);
//ет============================================^^ И ProductMap.срр - Реализация класса ProductMap //====================================================
♦include <iostream> using namespace std;
♦include "ProductMap.h"
// Конструктор ProductMap::ProductMap() {
// Заполнить список продуктами mProducts["OS-WinNT4"] = new Product("OS-WinNT4", "Windows NT 4.0", 500); mProducts["OS-Linux"]= new Product("OS-Linux", "Linux", 20);
mProducts["OS-MacOS8"] = new Product("OS-MacOS8", "MacOS 8.0", 300);
Глава 18
mProducts["OS-Win98"]= new Product("OS-Win98", "Windows 98", 89);
}
II Деструктор ProductMap::-ProductMap() (
И Удалить все продукты, на которые указывают элементы карты for (PPM_Itor i = mProducts.begin(); i ! = mProducts . end () ; ++i) delete i->second;
)
// Интерактивное добавление продуктов в список void ProductMap::Add() ( stringstrID;
stringstrName;
intintPrice;
cout « "Add Products:\n" ; while(1) {
cout « "ID (Type 0 to finish) : "; getline(cin, strlD);
if (strlD == "0") break;
cout « "Name: " ;
getline(cin, strName); cout « "Price: ";
cin » intPrice; cin. ignore () ;
mProducts[strID] = new Product(strlD, strName, intPrice); cout « "Product added to the list\n\n";
) )
// Изменить продукты void ProductMap::Edit() { PPM_Itor i; stringstrlD;
stringstrName;
intintPrice;
// Использовать стандартную функцию класса карты find() И и изменить продукт, если тот существует cout « "Edit Products:\n";
while(1) (
cout « "ID (Type 0 to finish): "; getline(cin, strlD);
if (strlD == "0") break; i = mProducts.find(strlD);-
if (i != mProducts . end ()) (
cout « "Editing product: " « i->second « "\n"; cout « "New name : " ;
getline(cin, strName); cout « "New price: "; cin » intPrice; cin.ignore();
i->second->mName = strName; i->second->mPrice = intPrice;
cout « "Product modified: " « i->second « "\n"; ) else
cout « "Product not found!\n" ;
Часть IV
) }
// Удалить продукты void ProductMap::Delete() { stringstrlD;
// Использовать стандартную функцию класса карты find() И и изменить продукт, если тот существует cout « "Delete Products:\п";
while(1) (
cout « "ID (Type 0 to finish) : "; getline(cin, strlD); if (strID == "0") break;
if (mProducts.find(strlD) != mProducts.end()) ( mProducts.erase(strlD); cout « "Product deleted\n";
) else cout « "Product not found!\n"; } }
// Найти продукт с заданным идентификатором
И ввод: идентификатор продукта // вывод: если продукт найден, то указатель на продукт, / /иначе — 0 .
Product* ProductMap::Find(const strings id) (
// Использовать стандартную функцию класса карты find() //и изменить продукт, если тот существует PPM_CItor ci = mProducts.find(id); if (ci != mProducts. end() ) return ci->second;
else return 0; }
II Проверить, пуста ли карта bool ProductMap::IsEmpty() const ( return mProducts.empty();
}
// Перегруженный оператор вставки ostreams operator«(ostreams os, const ProductMapS aProductMap) (
// Выполнить итерации по карте и вывести все продукты, // на которые указывают элементы карты os « "Product Listing:\n";
for (PPM_CItor ci = aProductMap.mProducts.begin(); ci != aProductMap.mProducts.end(); ++ci) os « ci->second « "\n"; return os;
)
Стандартный контейнер карты в C++ позволяет осуществлять прямой доступ к объектам по значениям их ключей, что идеально для нашего списка продуктов. Функция Add() используется для интерактивного добавления продуктов к карте. Функции Edit() и Delete() могут использоваться для изменения объектов продуктов в карте и для удаления их карты соответственно. Функцию find() можно использовать для получения указателя на продукт с заданным идентификатором. Функция IsEmptyO возвращает булево значение, указывающее, существует ли продукт в карте. В заключение перегруженный оператор вставки выводит все продукты карты.
Глава 18
Теперь определим список счетов, чтобы можно было получать доступ к счетам. В листинге 18.5 показан список счетов для нашего приложения.
Листинг 18.5. Приложение ведения счетов: класс InvoiceList 11============^=======^==============^^ И InvoiceList.h - Объявление класса InvoiceList //Этот класс инкапсулирует список счетов и связанные
Ис ним функции.
//=========™=т======;^т^;^^.^^
♦ifndef InvoiceList_H
♦define InvoiceList_H
♦include <iostream> ♦include <list> using namespace std;
♦include "Invoice.h"
♦include "ProductMap.h"
typedef list<Invoice *>InvoicePtrList;
typedef InvoicePtrList::const_iteratorIPL_CItor;
typedef InvoicePtrList::iteratorIPL_Itor;
class InvoiceList { public: // Деструктор virtual -InvoiceList() ;
II Интерактивный ввод счетов. Ссылка на карту продуктов передается, чтобы связать // элементы счета с продуктами. void Add(ProductMaps РМар); // Список счета реализуется со стандартным списком InvoicePtrList minvoices;
// Перегруженный оператор вставки friend ostreamS operator« (ostreamS os, const InvoiceListS anlnvoiceList);
private:
// Интерактивный ввод элементов в счет, ссылка на карту продуктов передается,
// чтобы проверить элементы счета.
void Additems(ProductMap& РМар, Invoice* pinvoice); };
♦endif
//============================================================== // InvoiceList.cpp - реализация класса InvoiceList //============================================================= ♦include <iostream> using namespace std; ♦include "InvoiceList.h"
// Деструктор
InvoiceList::-InvoiceList() { // Удалить все счета, на которые указывают элементы списка for (IPL_Itor i = minvoices.begin(); i != minvoices . end () ; ++i) delete *i; } // Интерактивный ввод счетов void InvoiceList::Add(ProductMapS PMap) ( if (PMap. IsEmpty ()) ( cerr « "No product available, " « "please enter products and try again!" « endl;
Часть IV
return; )
Invoice*pinvoice;
LongIngID;
stringstrDate;
cout « "Add Invoices:\n"; while(1) {
cout « "ID (Type 0 to finish): "; cin » IngID ;
cin.ignore() ;
if (IngID — 0)break;
cout « "Date: ";
getline(cin, strDate);
pinvoice = new Invoice(IngID, strDate); minvoices,push_back(pinvoice);
Additems(PMap, pinvoice); • cout « "Invoice added to the list\n\n";
} )
// Интерактивный ввод элементов в счет
// Ввод: РМар — список (реализуемый картой) доступных продуктов, //pinvoice — указатель на счет
void InvoiceList::Additems(ProductMapS PMap, Invoice* pinvoice) ( Invoiceitem* pltem; Product*pProduct;
stringstrProductID;
intintQty;
cout « "Add Invoice Items:\n"; while(1) ( do (
cout « "Product ID (Type 0 to finish) : "; getline(cin, strProductID);
if (strProductID = "0")return;
} while ((pProduct = PMap.Find(strProductID)) = 0);
cout « "Quantity: " ;
cin » intQty; cin. ignore () ;
pltem = new Invoiceitem(pProduct, intQty); pInvoice->AddItem(pltem);
cout « "Item added\n"; ) }
// Перегруженный оператор вставки ostreams operator«(ostreams os, const InvoiceListS anlnvoiceList) (
// Вывести все счета, на которые указывают элементы списка
os « "Invoice Listing:\п";
for (IPL_CItor ci = anlnvoiceList.minvoices.begin(); ci != anlnvoiceList.minvoices.end(); ++ci) os « **ci « "\n";
return os; )
Обычно мы вводим новые счета, изменяем существующие и ищем счета по их идентификаторам. Функции этого класса практически идентичны с соответствующими функциями класса ProductMap. Чтобы
Глава 18
упростить этот пример, автор реализовал только один конструктор. Для сохранения списка счетов выбран стандартный контейнер списка C++.
Функция Add() позволяет вводить счета и добавлять их в список счетов. Она принимает ссылку на ProductMap, чтобы мы могли связать элементы счета с объектами продуктов. Перегруженный оператор вставки выводит все счета в списке. Деструктор заботится об удалении всех объектов счетов.
До сих пор мы реализовали практически все возможности нашей довольно маленькой программы. Последняя задача заключается в создании программы драйвера, которая свяжет их вместе. В листинге 18.6 приведена главная функция нашего приложения.
Листинг 18.6. Приложение ведения счетов: главная программа
//==========---=========«---=»==»
И main.cpp - программа драйвера для приложения
Н============================^^
#include <iostream> using namespace std;
#include "ProductMap.h"
#include "InvoiceList.h"
int main() (
intProductMap InvoiceListintProductMap InvoiceListintChoice = 0;
int
ProductMap InvoiceList
int
ProductMap InvoiceList
Products;// создать пустую карту продуктов
Invoices;// создать пустой список счетов
do
{
cout « "\nA Trival Invoice Entry Program\n\n";
cout « "1. Enter invoices\n";
cout « "2. Print invoice list\n";
cout «"3.Add products\n";
cout «"4.Edit product\n";
cout «"5.Delete products\n";
cout « "6. Print product list\n";
cout «"9.Exit\n\n";
cout « "Enter your choice: ";
cin » intChoice;
cin . ignore () ;
cout « endl;
switch(intChoice) {
case1:Invoices.Add(Products);break;
case2:cout « Invoices «endl;break;
case3:Products.Add();break;
case4:Products.Edit();break;
case5:Products.Delete();break;
case6:■ cout « Products «endl;break ;
case9:cout « "Bye!\n\n";break;
default:cout « "Please select 1, 2, 3, 4, 5, 6 or 9.\n";
)
} while (intChoice != 9);
return 0;
}
Функция main() предоставляет меню для выполнения различных операций. Теперь сделаем тест.
A Trival Invoice Entry Program
Enter invoices
Print invoice list
Add products
Edit product
Delete products
Print product list
Живучесть объектов и шифрование Часть IV
9. Exit
Enter your choice: 3
Add Products: ID (Type 0 to finish) : Compiler-MSVCS
Name: Microsoft Visual C++ 5.0
Price500
Product added to the list
ID (Type 0 to finish): Compiler-BCB Name: Borland C++ Builder
Price: 520
Product added to the list
ID (Type 0 to finish) : 0
A Trival Invoice Entry Program
Enter invoices
Print invoice list
Add products
. Edit product
Delete products
Print product list
Exit
Enter your choice:
6
Product Listing: Compiler-BCB
Borland C++ Builder520
Compiler-MSVCS
Microsoft Visual C++ 5.0500
OS-Linux
Linux 20
OS-MacOS8
MacOS 8.0300
OS-Win98
Windows 9889
OS-WinNT4
Windows NT 4.0500
A Trival Invoice Entry Program
Enter invoices
Print invoice list
Add products
Edit product
Delete products
Print product list
Exit
Enter your choice: 4
Edit Products:
ID (Type 0 to finish): Compiler-BCB
Editing product: Compiler-BCB Borland C++ Builder 520 New name : Borland C++ Builder 3.0 New price: 550
Product modified: Compiler-BCB Borland C++ Builder 3.0 550 ID (Type 0 to finish) : 0
A Trival Invoice Entry Program
Enter invoices
Print invoice list
Add products
Edit product
Delete products
Print product list
Exit Enter your choice: 5
Delete Products:
ID (Type 0 to finish): Compiler-BCB Product deleted
ID (Type 0 to finish) : 0
Глава 18
A Trival Invoice Entry ProgramEnter invoicesPrint invoice listAdd productsEdit productDelete productsPrint product list9. ExitA Trival Invoice Entry ProgramEnter invoicesPrint invoice listAdd productsEdit productDelete productsPrint product list9. Exit
A Trival Invoice Entry Program
Enter invoices
Print invoice list
Add products
Edit product
Delete products
Print product list
9. Exit
A Trival Invoice Entry Program
Enter invoices
Print invoice list
Add products
Edit product
Delete products
Print product list
9. Exit
Enter your choice: 6Product listing:Compiler-MSVCS Microsoft Visual C++ 5.0OS-LinuxLinux 20OS-MacOS8MacOS 8.0300OS-Win98Windows 9889OS-WinNT4Windows NT 4.0 500Enter your choice: 6Product listing:Compiler-MSVCS Microsoft Visual C++ 5.0OS-LinuxLinux 20OS-MacOS8MacOS 8.0300OS-Win98Windows 9889OS-WinNT4Windows NT 4.0 500500500
Enter your choice: 6
Product listing:
Compiler-MSVCS Microsoft Visual C++ 5.0
OS-LinuxLinux 20
OS-MacOS8MacOS 8.0300
OS-Win98Windows 9889
OS-WinNT4Windows NT 4.0 500
Enter your choice: 6
Product listing:
Compiler-MSVCS Microsoft Visual C++ 5.0
OS-LinuxLinux 20
OS-MacOS8MacOS 8.0300
OS-Win98Windows 9889
OS-WinNT4Windows NT 4.0 500
500
500
A Trival Invoice Entry Program
Enter invoices
Print invoice list
Add products
Edit product
Delete products
Print product list
Exit Enter your choice: 1 Add Invoices:
ID (Type 0
to finish) :
: 1
Date Add Invoice Product ID
Items: (Type 0 to
: 07/24/98 finish): OS-Linux
Quantity Item added Product ID
(Type 0 to
: 1 finish): OS-MacOS8
Quantity Item added Product ID
(Type 0 to
: 10
finish): OS-Win98
Quantity Item added Product ID
(Type 0 to
: 6
finish): 0
Invoice added to the .
list
ID (Type 0
to finish)
: 2
Date Add Invoice Product ID
Items:
(Type 0 to
: 07/25/98
finish) : OS-WinNT4
Quantity Item added Product ID
(Type 0 to
: 2 finish): OS-Win98
Quantity Item added Product ID
(Type 0 to
: 50 finish): Compiler-MSVCS
Quantity Item added Product ID
(Type 0 to
: 5 finish): OS-MacOS8
Quantity Item added Product ID
(Type 0 to
: 3
finish): 0
Invoice added to the
list
ID (Type 0 to finish) : 0
A Trival Invoice Entry Program
Enter invoices
466
I Живучесть объектов и шифрование
Часть IV
Print invoice list
Add products
Edit product
Delete products
Print product list
Exit
Enter your choice: 2
Invoice Listing:
Invoice: 1 Date : 07/24/98
OS-LinuxLinux 20
OS-MacOS8MacOS 8.0
OS-Win98Windows 98
Total: 3554
1
300
89
20 10
6
3000
534
Invoice: 2
Date : 07/25/98 OS-WinNT4Windows NT4.0500
OS-Win98Windows 9889
Compiler-MSVC5 Microsoft Visual C++ 5.0 OS-MacOS8MacOS 8.0300
Total: 8850
2 50
3
1000 4450 500
900
5
2500
A Trival Invoice Entry Program
Enter invoices
Print invoice list
Add products
Edit product
Delete products
Print product list
Exit Enter your choice: 9 Bye!
Сначала в нашу карту продуктов мы добавили два компилятора. Затем изменили продукты и удалили их. После просмотра нашей карты продуктов мы создали два счета и вывели список счетов.
Наше маленькое приложение делает все, что нам нужно, за исключением одного: все продукты и счета при выходе из программы разрушаются. Такое приложение не нашло бы широкого применения. Необходимо добавить возможность сохранения данных в этом приложении постоянными.
Живучесть данных
Чтобы сделать объект живучим, его необходимо унаследовать от класса d_Object:
class MyClass : public d_Object {}
ODMG предоставляет подмножество элементарных типов C++ (табл. 18.1)..
Таблица 18.1. Элементарные типы ODMG
Tun ODMG
Размер
Описание
d_Char
8 битов
ASCII-символ
dBoolean
Зависит от реализации
d_True или d_False
d_Short
16 битов
Краткое целое со знаком
d_UShort
16 битов
Краткое целое без знака
d_Long
32 бита
Длинное целое со знаком
dULong
32 бита
Длинное целое без знака
d_Float
32 бита
IEEE-стандарт 754-1985; число с плавающей точкой одинарной точности
dDouble
32 бита
IEEE-стандарт 754-1985; число с плавающей точкой двойной точности
Объектно-ориентированные базы данных Глава 18
Заметьте, что типы C++ int и unsigned int не отображены в ODMG, поскольку размеры целых в языке C++ сильно варьируются в различных операционных системах и платформах.
Начнем с изменения класса Product и сделаем его хранимым. В листинге 18.7 приведен хранимый класс
Product.
ПРИМЕЧАНИЕ
В оставшихся листингах этой главы показана стандартная связка ODMG с языком C++. Они не могут быть откомпилированы только одним компилятором C++. При желании протестировать это приложение вы должны получить СУООБД, поддерживающую стандарт ODMG; также вы должны будете внести в листинг изменения, специфические для вашей реализации.
Листинг 18.7. Объявление хранимого класса Product
11=========================^^
И Product.h - Объявление хранимого класса Product
11====================^===^=======^^
♦ifndef Product_H
♦define Product_H
♦include <iostream.h>
♦include <odmg.h>// header-файл ODMG
♦include <d_String.h>II header-файл ODMG для типа d_String
class Product : public d_Object {
public:
// Конструкторы и деструктор
Product () ;
Product(const d_String& newID,
const d_String6 newName, const d_Long newPrice);
virtual -Product О;
// Переменные-члены
d_String
mID;
// идентификатор продукта
d_String
mName;
// название продукта
d_UShort
mPrice;
// стоимость продукта
11 Перегруженный оператор вставки friend ostreamS operator«(ostreamS os, const Product* const BaProduct);
1;
♦endif
Во-первых, мы больше не используем namespace std, поскольку ODMG пока не поддерживает концепций пространства имен C++. Теперь класс Product унаследован от класса ODMG d_Object. Для использования специфических возможностей ODMG необходимо включить поставляемый производителем файл odmg.h.
ПРИМЕЧАНИЕ
Имена header-файлов ODMG не определены в стандарте и варьируются в различных реализациях ODMG.
В первоначальном классе Product (см. листинг 18.1) переменные-члены m_ID и m_Name были объявлены как стандартные строки символов C++. ODMG пока не поддерживает прямого использования строк C++. Вместо этого он определяет класс d_String, предназначенный для сохранения в БД строк символов переменного размера. Для этого существуют исторические предпосылки: во время первого определения стандарта ODMG еще не существовало стандартной библиотеки C++. Мы объявим эти переменные как d_String. Для использования класса d_String необходимо включить header-файл d_String.h. Тип переменной mPrice также сменяется на тип ODMG d_UShort.
Часть IV
Схемы базы данных и средства захвата этой схемы
Чтобы сохранить объекты в БД, необходимо создать описание хранимых классов, тогда БД будет знать, как сохранять эти объекты. Такое описание называется схемой. Из схемы БД создает словарь данных. Различные средства доступа к БД, такие как генераторы форм, оптимизаторы запросов и создатели отчетов, общаются с БД посредством этой схемы.
Обычный компилятор C++ ничего не знает об объектных БД. Он не может создать ни схему БД, ни программный код для доступа к объектам БД. В стандарте ODMG header-файл C++ для хранимого класса должен быть обработан средством захвата схемы. На рис. 18.1 показаны полная компиляция приложения и процедура компоновки.
Средство захвата схемы разбирает объявления классов C++ и создает схему базы данных. Кроме того, это средство также создает расширенный заголовок и исходные файлы C++, что поможет компиляторам C++ понимать операции, выполняемые над базами данных. Эти файлы могут быть откомпилированы обычным компилятором C++. Исходные файлы реализации классов должны включать (используется директива #include) не первоначальные, а расширенные header-файлы C++. Затем компоновщик связывает все объектные файлы и библиотеки баз данных, предоставляемые производителем, и создает исполняемый файл приложения.
РИСУНОК 18.1.
Процедура создания приложения.
Измененная реализация класса Product приведена в листинге 18.8.
Листинг 18.8. Реализация хранимого класса Product
И ProductSrc. срр - Реализация хранимого класса Product
Глава 18
#include <odmg.h>// header-файл ODMG
#include "Product.hxx" // конструктор и деструктор Product::Product() : mID(""), mName(""), mPrice(O) {}
Product::Product(const d_StringS newID, const d_String6 newName, const d_Long newPrice) : mID(newID), mName(newName), mPrice(newPrice) ()
Product::-Product() {)
// перегруженный оператор вставки ostreams operator«(ostreams os, const Product* const aProduct) { os « (const char *)aProduct->mID « "\t"
« (const char *)aProduct->mName « "\t" « aProduct->mPrice; return os ;
)
Напомним, что обработчик схемы генерирует зависимые от реализации новые header-файлы и реализации класса Product. Мы переименуем исходный файл реализации класса Product из Product.срр в ProductSrc.cpp, поскольку обработчик схемы, возможно, сгенерирует Product.cpp, в котором будут реализованы различные функции поддержки класса Product. Мы также включаем сгенерированный header-файл, который обработчик схемы мог назвать Product.hxx. Стандартные конструктор и деструктор остались нетронутыми. Поскольку переменные d_String могут инициализироваться массивом символов C++, мы можем установить m_ID и m_Name в пустую строку "". Несмотря на то что стандартный конструктор d_String также устанавливает экземпляр в значение NULL, автор предпочитает делать это явно. Это главным образом предмет персональных предпочтений; NULL — это не всегда то же самое, что пустая строка. Пользовательский конструктор теперь принимает параметры d_String и d_UShort и вызывает копирующий конструктор d_String для инициализации mID и mName.
Перегруженный оператор вставки << изменен для приведения типов переменных mID и mName к const char * перед их выводом. Мы делаем так, поскольку класс d_String не поддерживает стандартный интерфейс C++ iostream.
Прежде чем перейти к классам Invoice и Invoiceitem, рассмотрим три понятия: коллекции, итераторы и отношения.
Коллекции
Коллекция — это набор объектов одного типа. Стандартная библиотека C++ предоставляет несколько контейнерных классов для управления коллекциями. Стандарт ODMG также определяет набор классов коллекций. ODMG не использует стандартные контейнеры C++ по двум причинам: во-первых, стандартные контейнеры C++ характерны для языка C++; стандарт ODMG разработан для поддержки также Java и Smalltalk. Во-вторых, когда группа ODMG выпустила спецификацию ODMG-93, комиссия по стандарту C++ еще не утвердила спецификации STL (Standard Template Library).
Со времени принятия STL и спецификаций стандартной библиотеки C++ интерфейс C++ в ODMG был расширен для поддержки стандартной библиотеки C++. Класс итератора ODMG реализовал стандартный постоянный двунаправленный итератор C++. Коллекции классов ODMG теперь поддерживают стандартные операции контейнеров C++ begin() и end(). Для коллекций ODMG также могут использоваться стандартные алгоритмы C++.
Коллекции ODMG унаследованы от абстрактного базового класса d_Collection<T>. Этот базовый класс определяет набор широко используемых операций над коллекциями, приведенных в табл. 18.2.
Часть IV
Таблица 18.2. Операции класса ODMG dCollection
ФункцияОписания
Операции над коллекциями
d_Collection();Создает коллекцию без элементов
d_Collection(const d_Collection<T>& с); Создает коллекцию и копирует все элементы из коллекции с, используя копирующий конструктор Т.
d_Collection<T>&operator= (const d_Collection<T>& с); d_Collection<T>&assign_from (const d_Collection<T>& c);
Удаляет все существующие элементы с помощью деструктора Т и копирует все элементы коллекции с с помощью копирующего конструктора Т. Функция assign_from() обычно используется для копирования элементов коллекции другого типа, скажем, копирования элементов из списка в множество.
-d_Collection();
Удаляет все существующие элементы с помощью деструктора Т.
Операции над элементами
unsigned long cardinality() const;
Возвращает количество элементов в коллекции
d_Boolean is_empty() const;
Определяет, пуста ли коллекция
d_Boolean is_ordered()const;
Определяет, упорядочена ли коллекция
d Boolean allow_duplicates() const;
Определяет, могут ли элементы иметь одинаковые значения
d_Boolean containselement (const T& v) const;
Определяет, могут или нет один или более элементов принимать значение v.
d_Boolean insert_element (const t& v) const;
Вставляет элемент. Возвращает D true, если вставка прошла успешно, и d_False — в противном случае. Используйте эту операцию для вставки элемента в множество, которое уже содержит значение v.
void remove_element(const T& v);
Удаляет первый или все элементы со
void remove_all (const T& v);
значением v.
void remove_all();
Удаляет все элементы.
Итерация
d_lterator<T> create_iterator() const; d_lterator<T> begin() const;
Возвращает итератор, указывающий на первый элемент. Функция begin() совместима со стандартными константными двусторонними итераторами.
d_lterator<T> end() const;
Возвращает итератор, указывающий на последний элемент.
Сравнение
friend d_Booleanoperator== (const d_Collection<T>& c1, const d„Collection <T>& c2);
d и с2 равны, если имеют одинаковое количество элементов и каждый элемент с1 равен соответствующему элементу с2. Элементы с1 и с2 могут быть различными типами коллекций.
friend d_Boolean operator!= (const d_Collection<T>& cl, const d_Collection <T>& c2);
Возвращает !(с1 == с2)
ODMG предоставляет пять коллекций, приведенных в табл. 18.3.
Таблица 18.3. Коллекции ODMG
Коллекция
Описание
d_Varray<T>
Массив переменного размера (подобен стандартному контейнеру C++ vector).
d_List<T>
Упорядоченная коллекция элементов, которые могут иметь одинаковые значения (подобна стандартному контейнеру C++ list, но d_List<T> еще и сортируется.
d_Set<T>
Неупорядоченная коллекция элементов, которые не могут иметь одинаковых значений (подобна стандартному контейнеру C++ set).
d_Bag<T>
Неупорядоченная коллекция элементов, которые могут иметь одинаковые значения (подобна стандартной коллекции C++ multiset).
d_Dictionary<Key, Value>
Неупорядоченная коллекция nap key, value. Доступ к значениям осуществляется посредством ключа (подобна стандартному контейнеру C++ multimap).
Глава 18
Кроме всего прочего, ODMG поддерживает подмножество стандартных контейнеров C++, приведенных в iao;i 18 .4.
Таблица 18.4. Стандартные контейнеры C++, поддерживаемые ODMG
Имя в C++
Имя в ODMG
vector<T>
d_vector<T>
list<T>
d_list<T>
map<Key, Value>
d_map<Key, Value>
multimap<Key, Value>
d_multimap<Key, Value>
set<T>
d_set<T>
Аргумент распределителя шаблона C++ не поддерживается, поскольку за управление памятью отвечает БД. Фактически приложения никогда не должны предпринимать попыток выделения памяти под контейнеры.
Класс Invoice в нашем приложении является неупорядоченной коллекцией элементов. Если допустить, что в счете не могут находиться два идентичных элемента, можно определить коллекцию элементов как d_Set:
d_Set <lnvoiceltem> mlterns;
Итераторы
Стандарт ODMG предоставляет класс d_Iterator, который может использоваться для итераций по коллекциям. Как говорилось ранее в этой главе, класс d_Iterator реализует стандартный двунаправленный константный итератор C++. Перегруженный оператор разадресации указателя * определен для возвращения копии адресуемого элемента. Мы можем использовать d_Iterator для доступа к элементам коллекции таким же образом, как и итератор C++:
d_Set<T> aSet;
for (d_Iterator<T> i = aSet .begin () ; i •= aSet.end(); ++i) {
cout « *i;Ц *i возвращает копию адресуемого элемента
*i = someValue;
}
Отношения
Еще одним важным понятием ODMG является отношение. Отношение представляет собой связи между объектами. Количество классов в отношении называется его степенью. Наиболее широко используемый тип отношения — это бинарное отношение, моделирующее связь между двумя классами.
Ненаправленные отношения
Бинарное отношение может быть либо ненаправленным, либо двунаправленным. Ненаправленное отношение моделирует однонаправленную секущую между двумя объектами. В приложении ведения счетов, например, элемент счета должен относиться к продукту, но по продукту мы не должны искать элемент счета. Мы можем перейти от элемента счета к продукту, но не сможем найти элемент счета из продукта.
Стандарт ODMG определяет класс шаблона d_Ref<T>, сохраняющий ссылку на хранимый объект типа Т. Каждому хранимому объекту назначается неизменный идентификатор объекта, который не может быть изменен приложением. Когда экземпляр d_Ref<T> сохраняется в БД, он содержит идентификатор объекта, на который ссылается. При повторной загрузке он опять ссылается на тот же объект, если тот загружен в память. Если объекта в памяти не найдено, то он извлекается из БД. Реализация ODMG автоматически обрабатывает этот процесс.
ПРИМЕЧАНИЕ
В стандарте ODMG также определен класс d_Ref_Any. Он является ссылкой на любой хранимый объект. Класс d_Ref_Any может приблизительно рассматриваться как void* в C++, тогда как d_Ref<T> подобна Т* в C++.
Часть IV
Перегруженный оператор ->() класса d_Ref ведет себя точно так же, как и его аналог для обычного указателя. Предположим, что имеется следующая переменная:
d_Ref<Product> гProduct;
Получить доступ к члену mPrice продукта, на который ссылается rProduct, можно с помощью следующего оператора:
rProduct->mPrice.
Класс d_Ref перегружает оператор *() для разадресации ссылки, так же, как и при работе с указателями. Таким образом *rProduct определяет объект продукта, на который ссылается rProduct. Функция-член Ptr() класса d_Ref определяет адрес объекта ссылки, или указатель C++ на объект, в памяти приложения.
Копирующий конструктор класса d_Ref выполняет поверхностное копирование: он копирует только ссылку, а не ссылаемый объект. Мы можем использовать этот факт для обхода ограничений класса d_Iterator. Поскольку d_Iterator — итератор, мы не можем прямо изменять его элементы. Чтобы избежать появления такой проблемы, необходимо сохранять набора ссылок на объекты в коллекции:
d_Set<d_Ref<T> > aRefSet; for (d_Iterator<d_Ref<T> > i = aRefSet.begin () ; i != aRefSet.end(); ++i) <
cout « *i;// *i возвращает копию ссылки на объект типа Т
*i->aMember = someValue;
)
Двунаправленные отношения
Двунаправленное отношение, напротив, моделирует двустороннюю секущую между двумя объектами. Например, мы сможем получить доступ ко всем элементам счета и наоборот.
В стандарте ODMG определены классы шаблонов, представляющих двунаправленные отношения:
d_Rel_Ref<T, const char * Member>
Этот класс предоставляет отношение один-к-одному на объект типа Т, т.е. существует только один объект типа Т в таком отношении между текущим классом и классом Т. Второй параметр, Member, — это строка символов, содержащая имя переменной-члена со ссылкой на текущий объект. С точки зрения текущего объекта переменная-член Member в классе Т называется обратным отношением. Простое двунаправленное отношение один-к-одному продемонстрировано в листинге 18.9.
Листинг 18.9. Простое двунаправленное отношение один-к-одному И A.h class В;
extern const char _mRelVarB [];
class A : public d_Object {
d_Rel_Ref<B, _mRelVarB> mA; };
H B.h class A;.
extern const char _mRelVarA [] ; class В : public d_Object ( d_Rel_Ref<A, _mRelVarA> mB; 1;
II C.cpp const char _mRelVarA [] = "mA"; const char _mRelVarA [] = "mB";
Вы можете также иметь двунаправленное отношение один-ко-многим. Например, элемент счета может иметь только один связанный счет, но счет может иметь много элементов. В ODMG определены два класса двунаправленных отношений один-ко-многим: d_RelSet и d_RelList. Первый из них представляет неупорядоченное отношение, тогда как второй — упорядоченное. Классы двунаправленных отношений приведены в табл. 18.4.
Глава 18
Таблица 18.4. Классы двунаправленных отношений ODMG
Класс
Базовый класс
d_Rel_Ref<T, const char* Member> d_Rel_Set<T, const char* Member> d_Rel_List<T, const char* Member>
d_Ref<T> d_Set<d_Ref<T» d_List<d_Ref<T>>
Например, отношение между классами Invoice и Invoiceitem в приложении ведения счетов может быть определено, как показано в листинге 18.10.
Листинг 18.10. Отношение между классами Invoice и Invoiceitem extern const char _mlnvoice[], _mlterns[]; class Invoice : public d_Object < d_Rel_Set<InvoiceItem, _mlnvoice>mlterns;
}; class Invoiceitem : public d_Object { d_Rel_Ref<Invoice, _mltems> mplnvoice; 1; // В некотором (исходном) файле const char _mlnvoice[] = "mplnvoice"; const char _mlterns[] = "ml terns";
Теперь рассмотрим изменения, которые будут внесены в классы Invoice и Invoiceitem. Два header-файла соединены и показаны в одном листинге, чтобы проще можно было описать отношения между ними. В листинге 18.11 приведены объявления хранимых классов Invoice и Invoiceitem.
Листинг 18.11. Объявления хранимых классов Invoice и Invoiceitem
;/==»========================^^ И Invoice.h - Объявление класса Invoice 11=================================^^
♦ifndef Invoice_H
♦define Invoice_H
♦include <iostream.h>
II header-файлы ODMG ♦include <odmg.h> ♦include <d_Date.h> ♦include <d_Ref.h> ♦include <d_RelRef.h> ♦include <d_RelSet.h> ♦include <d_Iterat.h> class Product; class Invoiceitem;
extern const char _mlnvoice[], _mltems[]; II Определены в исходных файлах typedef d_Rel_Set<InvoiceItem, _mlnvoice>InvoiceltemList;
typedef d_Iterator<d_Ref<InvoiceItem> >IIL_CItor;
class Invoice : public d_Object (
public:
// Конструкторы и деструктор Invoice();
Invoice(const d_ULong mID) ;
virtual -Invoice();
// Переменные-члены d_ULongmID;// Идентификатор счета
d DatemDate;/ / Дата создания счета
Часть IV
InvoiceltemListmlterns;// Список ссылок счетов
void Additem(d_Ref<Invoice!tem> pltem);// Добавить элемент в список счетов
d_ULong GetTotal() const;// Полная стоимость счета
// Перегруженный оператор вставки friend ostreams operator« (ostreams os, const Invoices anlnvoice); );
class Invoiceitem : public d_Object (
public:.
// Конструкторы и деструктор
Invoice Item () ;
Invoiceitem(d_Ref<Product> pProduct, const d_UShort newQuantity);
virtual -Invoiceitem();
// Переменные-члены
// Ссылка на связанный объект счета
d_Rel_Ref<Invoice, _mltems> mplnvoice;
d_Ref<Product> mpProduct;// Ссылка на объект продукта
d_UShortmQuantity;// Продаваемое количество
// Другие функции-члены
// Получить количество элементов
d_UShort GetTotal() const;
// Перегруженный оператор вставки
friend ostreams operator«(ostreams os, const Invoiceitems anlnvoiceltem) ;
#endif
Сначала рассмотрим класс Invoiceitem. Переменная количества продаваемых продуктов m Quantity объявлена как d_UShort и, следовательно, может сохраняться в БД. Переменная mpProduct первоначально была определена как указатель на объект Product — Product*. В C++ указатель хранит адрес объекта в памяти, это хорошо до тех пор, пока объект остается в памяти. После закрытии приложения и при его повторном запуске адрес в памяти будет другим. После сохранения элемента счета в БД адрес в памяти станет неуместен. Хранение такого указателя в БД бесполезно. Таким образом, переменная mpProduct определена следующим образом:
d_Ref<Product> mpProduct;
Подобным образом отношение один-ко-многим между классами Invoice и Invoiceitem представлено членом d_Rel_Ref<Invoice, _mlnvoice> mplnvoice; в классе Invoiveltem и членом d_RelSet<Invoiceitem, _mlnvoice> mltems; в классе Invoice.
Переменная-член mID в классе Invoice теперь определена как d_ULong и, таким образом, может быть сохранена в БД. Переменная-член mDate предназначена для сохранения даты создания счета. В ODMG определен класс d_Date, представляющий дату как "год, месяц, день". Он имеет статическую функцию- член currentQ, возвращающую объект d_Date с текущей системной датой. Стандартный конструктор класса d_Date инициализирует экземпляр текущей системной датой. Это идеально для класса Invoice, поскольку тот сохраняет именно дату создания счета.
ПРИМЕЧАНИЕ
ODMG также предлагает класс d_Time, представляющий время суток. Он содержит часы, минуты и секунды. Стандартный конструктор устанавливает экземпляр в текущее время. Еще один класс, d_TimeStamp, представляет вместе дату и время. Последний класс, djnterval, предназначен для хранения промежутков времени.
Должна быть изменена реализация классов Invoice и Invoiceitem. В листинге 18.12 приведен измененный файл реализации класса Invoiceitem.
Листинг 18.12. Реализация хранимого класса Invoiceitem
//====_===============_=========^^
И InvoiceltemSrc. ерр - Реализация класса Invoiceitem
//===========================================================
Глава 18
#include <odmg.h>
#include "Product.hxx"
#include "Invoice.hxx"
// Переменная-член представляет связь c Invoice const char _mlnvoice[] = "mplnvoice";
// Конструкторы и деструктор
Invoiceitem::Invoiceitem() : mpProduct (0) , mQuantity(0) (}
Invoiceitem: : Invoiceitem (d__Ref<Product> pProduct, const d_UShort newQuantity) : mpProduct(pProduct), mQuantity(newQuantity) {)
// Деструктор
// He удаляйте объект продукта Invoiceitem::-Invoiceitem() ()
// Получить количество элементов d_UShort Invoiceitem::GetTotal() const {
return mpProduct->mPrice * mQuantity; )
// Перегруженный оператор вставки ostreams operator«(ostreams os, const Invoiceitems anlnvoiceltem) (
os « anlnvoiceltem.mpProduct.ptr() « "\t" « anlnvoiceltem.mQuantity « "\t" « anlnvoiceltem.GetTotal() ; return os;
}
Здесь мы инициализировали _mlnvoice именем переменной-члена Invoice. Перегруженный конструктор теперь инициализирует переменную-член mpProduct ссылкой на продукт. Перегруженный оператор ->() класса d_Ref используется в функции GetTotal() для получения доступа к члену mPrice продукта, на который указывает mpProduct, с помощью mpProduct->mPrice. Перегруженный оператор вставки << использует функцию d_Ref<T>::ptr() для получения указателя на продукт.
Базы данных и транзакции
До сих пор мы имели дело с объектами, загруженными в память. Для доступа к объектам БД необходимо открыть ее. Для создания объекта БД можно воспользоваться классом ODMG d_Database. База данных может быть открыта следующим образом:
d_Database db;
db.Open("DB_Name");
// Операции с базой данных
db.Close () ;
Функция Ореп() объявлена следующим образом:"
enum access status (not_open, read_write, read_only, exclusive);
void open(const char* db_name, access_status access = read_write) ;
Открытие БД в исключительном режиме предотвращает доступ других процессов к этой базе. Исключительный режим необходимо использовать в тех случаях, когда вы хотите выполнить определенные операции по обслуживанию БД. Режим "только-для-чтения" не позволяет вносить изменения в объекты БД.
В БД транзакция является набором операций, выполняемых над базой данных. Для завершения транзакции должны быть выполнены все операции, содержащиеся в транзакции. Если любая из операций в одной транзакции привела к сбою, все остальные операции выполняться не должны. Для операций, выполненных перед сбоем, должен быть выполнен откат в целях сохранности целостности данных. Классическим примером такого требования к целостности является система передачи капиталов банка, в которой обе операции снятия с одного счета и вклада на другой счет пройдут успешно, если успешно завершится тран-
Живучесть объектов и шифрование Часть IV
закция. Если приложение потерпит сбой при снятии капитала с одного счета, оно не должно делать вклад на другой счет и наоборот.
ODMG предоставляет класс d_Transaction, выполняющий набор операций над транзакциями, таких как запуск транзакции и совершение транзакции (успешное завершение транзакции). В следующем фрагменте кода продемонстрирована типичная транзакция в ODMG:
d_Transaction tx; // создать транзакцию tx.begin ();// запустить транзакцию
DeleteObjects() ; II операции над объектами базы данных AddObject () ; tx.Commit;II совершить транзакцию
Операции в транзакции должны заключаться между операторами begin() и end(). Функция commit() выполняет операции DeleteObjects() и AddObjects(). Если одна из них потерпит сбой, другая не должна выполняться; если одна из них была выполнена, то должна быть откачена. В определенных ситуациях приложение также может вызвать команду abort():
d_Transaction tx; // создать транзакцию try { tx.begin;// запустить транзакцию
DeleteObjects () ;II операции над объектами базы данных
AddObjects(); tx. Commit ();II совершить транзакцию
) catch (SomeException)// произошла ошибка!
{ tx.abort();// прервать транзакцию
}
В предыдущем примере мы использовали метод обработки исключений C++, поддерживаемый стандартом ODMG. Класс ODMG d_Error унаследован от класса исключения C++. Функция-член get_kind() класса d_Error возвращает код ошибки, а функция-член what() предоставляет описание ошибки.
Пришло время вернуться к нашему приложению ведения счетов. В листинге 18.13 показана измененная реализация класса Invoice.
Листинг 18.13. Реализация хранимого класса Invoice //====================================^==^=====^^ И InvoiceSrc.срр - Реализация класса Invoice //===================================================
#include <odmg.h> II header-файл ODMG
#include "Product.hxx" #include "Invoice.hxx"
II Переменная-член, представляющая связь c Invoiceitem const char _mltems[] = "mltems";
// Конструкторы и деструктор Invoice::Invoice() : mID(0) <1.
Invoice::Invoice(const d_ULong newID) : mID(newID) (1
Invoice: :-Invoiced { // Удалить все элементы счета for (IIL_CItor i = mltems-begin() ; i != mltems .end () ; ++i) delete (*i) ,ptr() ;
}
// Добавить элемент в список счетов void Invoice::Additem(d_Ref<InvoiceItem> pltem) I d_Transaction tx;
Глава 18
try ( tx. begin () ; mlterns.insert_element(pltem); tx.commit();
) catch (d_Error derr) {
tx.abort();
cerr « "ODMG Error [" « derr. getjcind () « "] " « derr.what() B« endl; }
}
// Получить полную стоимость счета
d ULong Invoice::GetTotal() const Г
int intTotal = 0;
// Выполним итерации по списку элементов и вычислим полную стоимость
for (IIL_CItor i = ml terns . begin () ; i != mltems .end() ; ++i) intTotal += (*i) ->GetTotal () ;
return intTotai; )
// Перегруженный оператор вставки
ostreams operator«(ostreams os, const Invoices anlnvoice) {
// Вывести номер счета
os « "Invoice: " « anlnvoice.mID « "\n"
« anlnvoice.mDate.month() « ”/"
« anlnvoice.mDate.day() « "/" « anlnvoice.mDate.year() « "\n";
// Вывести все элементы счета
for (IIL_CItor ci = anlnvoice.mltems.begin(); ci != anlnvoice.mltems.end(); ++ci) os « **ci « "\n";
// Вывести полную стоимость
os « "Total: " « anlnvoice. GetTotal () « "\n";
return os;
)
Константа mltems инициализируется именем переменной-члена mltems. Переменная-член mDate больше не инициализируется в конструкторе, поскольку текущая дата в нее вносится стандартным конструктором d_Date. В деструкторе теперь для удаления всех элементов используется итератор d_Iterator, который, проходя по всем элементам коллекции, удаляет их. Для получения указателя на элемент коллекции мы используем функцию d_Ref<T>::ptr() и удаляем ссылаемый элемент.
Функция Addltem() демонстрирует способ доступа к объектам БД. Если мы попытаемся добавить элемент в счет, в котором есть элемент, идентичный добавляемому, будет возбуждено исключение d_Error_NameNotUnique. В таком случае мы прерываем транзакцию и выводим сообщение об ошибке.
Функция GetTotal() также использует d_Iterator для прохода по списку элементов и вычисления полной стоимости счета. Функция практически идентична первоначальной GetTotal(), в которой для доступа к каждому из элементов списка мы использовали стандартный итератор.
Реализация перегруженного оператора вставки << также подобна первоначальной версии. Единственное заметное отличие состоит в способе вывода члена mDate. Поскольку класс d_Date прямо не поддерживает iostream из C++, мы поочередно выводим значения года, месяца и дня переменной mDate.
В первоначальной версии нашего приложения мы создали карту Product для хранения указателей на доступные объекты продуктов. Поскольку теперь все продукты хранятся в БД, необходимость в такой карте отпадает. Вместо нее мы создадим класс, инкапсулирующий все операции над объектами продуктов, находящимися в базе данных. В листинге 18.14 приведен класс операций над продуктами.
478
I Живучесть объектов и шифрование
Часть IV
Листинг 18.14. Класс операций над продуктами
И ProductOps.h - Объявление класса операций над продуктами //Этот класс инкапсулирует операции над объектами
Ипродуктов,находящимися в базе данных
11=======^===================^^
#ifndef ProductOps_H
#define ProductOps_H
#include "Product.hxx"
class ProductOps {
public:
// Конструктор и деструктор
ProductOps(d_Database* pDatabase);
virtual “ProductOps () ;
// Функции-члены
void void void int void
Add() ;
Edit() ;
Delete () ;
IsEmpty ()
List () ;
const;
// Добавить продукты в базу данных
// Изменить продукты в базе данных
// Удалить продукты из базы данных
II Проверить, ёсть ли продукты в базе данных / / Перечислить все продукты в базе данных
// Переменные-члены d_Database*pDB;
И
Указатель на базу данных
};
#endif
//=======,=============================^^ II ProductOps.cpp - Файл реализации класса операций над продуктами
#include <iostream.h>
#include <algorithm>
#include <odmg.h>
#include <d_Extent.h>
#include "InvApp.h"
#include "Product.hxx"
#include "ProductOps .h"
// Конструктор ProductOps::ProductOps(d_Database* pDatabase) : pDB(pDatabase) {)
// Деструктор ProductOps::“ProductOps() {}
// Добавить продукты в базу данных void ProductOps::Add() { d_Transactiontx;
Product*pProduct;
charstrlD[MAXLEN+1] ;
charstrName [MAXLEN+1] ;
unsignedshortushortPrice;
cout « "Add Products: \n" ; while(1) {
cout « "ID (Type 0 to finish) : " ; cin.getline(strlD, MAXLEN);
if (strID[O] == '0') break;
Глава 18
MAXLEN);MAXLEN);cout « "Name
MAXLEN);
MAXLEN);
cin.getline(strName, cout « "Price
cin » ushortPrice cin.ignore();
try (
tx.begin();
pProduct = new(pDB) Product(strID, strName, ushortPrice);
pDB->set_object_name(pProduct, strlD);
tx. commit () ;
cout « "Product " « pProduct « " added\n"; } catch (d_Error derr) {
tx.abort();
if (derr. get_kind() == d_Error_NameNotUnique) (
cerr « "Product already exists!" « endl; continue;
} else (
cerr « "ODMG Error [" « derr. get_kind () « "] " « derr.what() « endl;
return; ) }
tx;pProduct;str ID [MAXLEN + 1] ;StrNewID [MAXLEN + 1] strName[MAXLEN + 1] ; ushortPrice;tx;pProduct;str ID [MAXLEN + 1] ;StrNewID [MAXLEN + 1] strName[MAXLEN + 1] ; ushortPrice;// Изменить продукты void ProductOps :: Edit () (
tx;
pProduct;
str ID [MAXLEN + 1] ;
StrNewID [MAXLEN + 1] strName[MAXLEN + 1] ; ushortPrice;
tx;
pProduct;
str ID [MAXLEN + 1] ;
StrNewID [MAXLEN + 1] strName[MAXLEN + 1] ; ushortPrice;
d_Transaction d_Ref<Product> char char char unsigned short
II Используем функцию d_Database lookup_object() для поиска продукта // и изменения его в случае нахождения cout « "Edit Products:\n";
while(1)
{ do ( cout « "ID (Type 0 to finish) : " ; cin.getline(strID, MAXLEN);
if (strID[0] == '0') break;
tx.begin();
pProduct = pDB->lookup_object(strlD); tx. commit () ;
if (pProduct.is_null() == d_True) cerr « "Invalid product ID, please try again." « endl;
) while (pProduct.is_null() == d_True)
cout « "Editing product: " « pProduct.ptr () « "\n"; cout « "New ID : " ;
cin.getline(strNewID, MAXLEN);
cout « "New name : " ;
cin.getline(strName, MAXLEN);
cout « "New price: "; cin » ushortPrice;
Часть IV
cin. ignore () ;
try { tx. begin () ; pProduct->mID = strNewID; pProduct->mName = strName; pProduct->mPrice = ushortPrice; pDB->rename_object(strID, strNewID); tx.commit() ;
cout « "Product modified: " « pProduct.ptr() « "\n";
} catch (d_Error derr) ( cerr « "ODMG Error [" « derr.get_kind() « "] " « derr.what() « endl;
} ) }
// Удалить продукты void ProductOps::Delete() ( d_Transactiontx;
d_Ref<Product>pProduct;
charstrID[MAXLEN + 1] ;
И Используем функцию d_Database lookup_object() для поиска продукта // и изменения его в случае нахождения cout « "Delete Products:\n"; while(1) ( cout « "ID (Type 0 to finish): "; cin.getline(strlD, MAXLEN); if (strID[0J = '0') break;
tx.begin(); pProduct = pDB->lookup _object (strlD) ; tx.commit() ;
if (pProduct.is_null() — d_False) ( try { tx.begin();
pProduct.delete_object(); tx.commit() ;
cout « "Product deleted\n" ; } catch (d__Error derr) ( cerr « "ODMG Error [" « derr.get_kind() « "] " « derr.what() « endl;
return; ) ) else cerr « "Invalid product ID, please try again." « endl; ) )
// Проверяем, есть ли в базе данных объекты продуктов int ProductOps::IsEmptyO const < d_Extent<Product> ProductExtent(pDB); return ProductExtent.is_empty();
}
void ProductOps::List()
Глава 18
d_Extent<Product>ProductExtent(pDB);
d_Bag<d_Ref<Product> >ProductBag;
ProductExtent.query(ProductBag, "this-XnlD != \"\"");
cout « "Product Listing:\n";
// Метод 1
for (d_Iterator< d_Ref<Product> > i2 = ProductBag.begin();
i2 != ProductBag.end() ; ++i2) cout « (*i2).ptr() « "\n”;
// Метод 2
PrintPtr<d_Ref<Product> > DoPrintPtr;
std::for_each(ProductBag.begin(),ProductBag.end(), DoPrintPtr);
}
Функция Find() была удалена из класса карты продуктов в связи с тем, что класс d_Database предоставляет функцию-член lookup_object() для поиска объекта по его имени. Все остальные функции реализованы. Единственная переменная-член теперь является указателем на БД. Она инициализируется адресом открытого в текущее время объекта БД. Объект БД мы не удаляем в деструкторе, поскольку тот может использоваться другими объектами.
В функции Add() для приема ввода пользователя мы используем массив символов фиксированного размера, поскольку ODMG не поддерживает стандартного класса строк C++. Константа MAXLEN определена для задания максимальной длины строки. Следующий оператор демонстрирует создание хранимого объекта:
pProduct = new(pDB) Product(strlD, strName, ushortPrice);
ODMG определяет перегруженный оператор new(d_Database *), который принимает указатель на БД. Объекты, созданные с использованием оператора new(), сохраняются в БД автоматически. Для задания уникального ключа — в нашем случае идентификатора продукта —• вызываем функцию-член set_object_name() класса d_Database. Это значение ключа можно использовать для доступа к продукту в БД, что мы и делали в функции Edit(). Значение ключа должно быть уникальным среди всех объектов БД, а не только среди объектов Product. Эти две операции мы объединим в транзакцию.
Поскольку значение ключа, или имя объекта, должно быть уникальным, мы должны проверить, существует ли в БД продукт с таким именем. Если существует — возбуждается исключение с кодом ошибки d_ErrorNameNotUnique. Это исключение обрабатывается следующим блоком catch. БД может автоматически прервать транзакцию в такой ситуации, но никогда не помешает прервать ее явно. При возникновении другого исключения блок catch просто выводит сообщение об ошибке.
В функции Edit() показаны две полезные функции класса d_Database, Первой является функция lookup_object(). Она принимает строку, содержащую имя объекта, и возвращает ссылку d_Ref_Any на найденный объект. Если объект с таким именем не найден, возвращается ссылка NULL. Проверить результат мы можем с помощью функции is_null() класса d_Ref_Any.
Если изменен идентификатор продукта, необходимо обновить имя объекта этого продукта. Для этого используется функция d_Database::rename_object(), которая принимает два параметра: старое и новое имя. Опять же, все операции по изменению объекта Product заключены в транзакцию.
Функция Delete(), как и Edit(). вызывает функцию lookup_object() для получения продукта из БД. Затем она вызывает функцию d_Ref<T>::delete_object() для удаления объекта из БД и из памяти приложения.
Функция List() опрашивает все объекты Product в БД. В ODMG набор всех хранимых экземпляров класса называется его пространством. Класс шаблона d_Extent<T> предназначен для доступа ко всем объектам типа Т в БД. Он, по существу, является неупорядоченной коллекцией ссылок на эти объекты, подобно d_Set<d_Ref<T>>. Синтаксис его инициализации следующий:
d_Extent<T> TExtent(pDB);
В этом синтаксисе pDB является указателем на БД, a TExtent — экземпляром пространства Т. Пространство автоматически обновляется БД, поэтому в него добавляются и удаляются элементы при добавлении либо удалении их из БД. Процесс синхронизации может заметно сказаться на производительности приложений. Поэтому вы должны избегать долговременного хранения экземпляра пространства.
Часть IV
Пространства могут быть опрошены функцией-членом query(). Первый параметр является ссылкой на коллекцию, в которой будут сохранены результаты запроса; второй параметр является строкой, содержащей предикат. В функции List() мы получаем все продукты с помощью такого предиката: this->mdID= "";
Оператор вызова ProductExtent. Query () выглядит довольно необычно:
ProductExtent.Query(ProductBag, "this->mID != \"\"") ;
Здесь символ \ используется для того, чтобы компилятор не интерпретировал двойные кавычки как внедренные в строку предиката.
По результирующей коллекции можно провести итерацию, как мы видим в методе 1. Поскольку ODMG работает со стандартными алгоритмами C++, в целях упрощения программного кода воспользуемся алгоритмом for_each. Функциональный объект PrintPtr<d_Ref<Product>> определен в header-файле Inv.h, приведенном в листинге 18.15.
Листинг 18.15. Разнообразные функции
11========================================= // InvApp.h - Разнообразные функциональные объекты и другие объявления //============================================================= ♦include <iostream.h> ♦include <functional>
// Выводит объект, на который ссылается d_Ref<T> или указывает указатель Т* в параметре И Используется перегруженный оператор вставки «(ostreamS, TS) template<class Ref > class PrintRef : public std::unary_function<Ref, void> ( public: void operator () (Refs r) { cout « *r « "\n";
} );
II Выводит объект, на который ссылается параметр d_Ref<T>
II Используется перегруженный оператор вставки «(ostreamS, Т*) template<class Ref> class PrintPtr : public std::unary_function<Ref, void> { public: void operator () (Refs r) { cout « r.ptr() « "\n";
) };
// Произвольная длина строки const unsigned MAXLEN = 30;
Функциональный объект PrintPtr принимает параметр d_Ref<T> и выводит его значение с помощью T::operator(ostream&, Т*). В листинге 18.16 он используется для вывода всех счетов. В этом файле также определена стандартная константа MAXLEN, используемая в листингах 18.14 и 18.16 для принятия ввода пользователя.
Первоначальный класс InvoiceList был переименован в InvoiceOps и изменен таким образом, чтобы получить все преимущества живучести объектов. В листинге 18.16 приведен класс InvoiceOps.
Листинг 18.16. Класс операций над счетами
11====================================== Ц InvoiceOps.h - Объявление класса InvoiceOps //Этот класс инкапсулирует операции над объектами Invoice
/ /в базе данных
11================================
♦ifndef InvoiceOps_H
Глава 18
♦define InvoiceOps_H
♦include <iostream.h>
♦include <odmg.h>
♦include "ProductOps.h"
♦include "Invoice.hxx"
class InvoiceOps (
public:
// Конструктор и деструктор
InvoiceOps(d_Database* pDatabase);
virtual -InvoiceOps();
void Add(ProductOpsS POps);
//
Создать
счета
void Delete ();
//
Удалить
счета
void List();
//
Вывести
список счетов
d_Database*pDB;
private:
H Интерактивное добавление продуктов в список, ссылка на продукт передается // для проверки на наличие продукта в элементах счета.
void Additems(ProductOpsS POps, Invoice* pinvoice); };
♦endif
//===========================================:=======
// InvoiceOps. cpp - Реализация класса InvoiceOps
♦include <iostream.h>
♦include <algorithm>
♦include <odmg.h>
♦include <d_Extent.h>
♦include "InvApp.h"
♦include "Product.hxx"
♦include "InvoiceOps.h"
♦include "Invoice.hxx"
// Конструктор
InvoiceOps::InvoiceOps(d_Database* pDatabase) pDB(pDatabase)
(}
// Деструктор
InvoiceOps::-InvoiceOps() {)
// Ввести счета void InvoiceOps::Add(ProductOpsS POps) (
if (POps . IsEmpty ()) (
cerr « "No product available, "
« "please enter products and try again!" « endl; return;
)
d_Transaction
tx;
Invoice*
pinvoice;
long
IngID;
char
strID[MAXLEN + 1]
cout « "Add Invoices:\n";
while(1) (
cout « "Invoice ID (Type 0 to finish): ";
cin » IngID ;
cin.ignore();
Часть IV
if (IngID = 0)if (IngID = 0)break;break;
if (IngID = 0)
if (IngID = 0)
break;
break;
_ltoa(lngID, strlD, 10);
try { tx. begin () ; pinvoice = new(pDB) Invoice(IngID); pDB->set_object_name(pinvoice, strlD); tx.commit() ;
cout « "Invoice added to the list\n\n"; Additems(POps, pinvoice);
} catch (d_Error derr) { tx.abort() ;
if (derr.get_kind() == d_Error_NameNotUnique) { cerr « "Invoice already exists!" « endl; continue;
else < cerr « "ODMG Error [" « derr .get_kind() « "] " « derr.what() « endl;
return; } } ) }
// Удалить счета из базы данных void InvoiceOps::Delete() { d_Transactiontx;
d_Ref<Invoice>pinvoice;
longIngID;
charstrlD[MAXLEN + 1] ;
II Используеи функцию d_Database lookup_object() для поиска счета II и удаления его в случае нахождения cout « "Delete Invoices:\n";
while(1) ( cout « "ID (Type 0 to finish): " ; cin » IngID ;
if (IngID == 0) break;
_ltoa(lngID, strlD, 10);
tx.begin () ;
pinvoice = pDB->lookup_object(strlD); tx.commit() ;
if (pinvoice.is_null() == d_False) ( try
tx.begin () ;
pinvoice.delete_object(); tx.commit() ; cout « "Invoice deleted\n";
}
catch (d_Error derr)
cerr « "ODMG Error [" « derr. get_kind () « "] " « derr.what() « endl;
return;
Глава 18
}
}
else
cerr « "Invalid invoice ID, please try again." « endl;
// Интерактивное добавление элементов в счет // Ввод: POps — ссылка на объект ProductOps
Иуказатель на счет
void InvoiceOps::Additems(ProductOpsS POps, Invoice* pinvoice)
{
d_Transaction Invoiceitem* d_Ref<Product> char intd_Transaction Invoiceitem* d_Ref<Product> char inttx;
d_Transaction Invoiceitem* d_Ref<Product> char int
d_Transaction Invoiceitem* d_Ref<Product> char int
pl tern;
pProduct;
StrProductID[MAXLEN + 1] ; intQty;
cout « "Add Invoice Items:\n";
while (1)
(
do ( cout « "Product ID (Type 0 to finish): "; cin.getline(strProductID, MAXLEN);
if (strProductID[0] == ’O’)return;
tx.begin();
pProduct = pDB->lookup_object(strProductID); tx. commit () ;
if (pProduct.is_null() == d_True) cerr « "Sorry, wrong product ID. Please try again." « endl;
} while (pProduct.is_null() == d_True);
cout « "Quantity: ";
cin » intQty;
cin.ignore(),
try ( tx.begin(); pltem = new(pDB) Invoiceitem(pProduct, intQty); pltem->mplnvoice = pinvoice;
plnvoice->mlterns.insert_element_last(pltem);
tx.commit();
cout « "Item added\n";
}
catch (d_Error derr) (
cerr « "ODMG Error [" « derr ,get_kind() « "] " « derr.what() « endl;
return;
}
)
// Вывести все счета, находящиеся в базе данных void InvoiceOps::List() (
d_Extent<Invoice>InvoiceExtent(pDB);
d_Bag<d_Ref<Invoice> > Invoices;
InvoiceExtent.query(Invoices, "this->mID > 0") ;
cout « "Invoice Listing:\n";
PrintRef<d_Ref<Invoice> > DoPrintRef;
std: :for_each(Invoices.begin(), Invoices.end(), DoPrintRef); )
Часть IV
Большинство функций InvoiceOps подобны своим аналогам из класса ProductOps. Конструктор инициализирует переменную-член pDB указателем на БД. Функция Add(), прежде чем добавить счет, проверяет, есть ли вообще продукты в БД. Имя объекта счета устанавливается в его идентификатор. Поскольку имя объекта должно быть строкой символов, преобразуем идентификатор из длинного целого в строку с помощью функции _ltoa(). Хоть она и не является стандартной функцией С или C++, практически все производители предоставляют функцию преобразования типа _Иоа(). (Вы всегда можете написать собственную версию функции, если в вашем компиляторе нет таковой.)
Функция Addltem() демонстрирует одну важную возможность баз данных ODMG. Вспомните, между счетом и его элементом существует двунаправленное отношение. Это отношение автоматически поддерживается БД. Нам всего лишь необходимо задать объекты, между которыми необходимо установить отношения. Объекту pltem->mplnvoice можно присвоить ссылку pinvoice на объект Invoice:
pltem->mplnvoice = pinvoice;
В этом операторе мы сообщаем БД, что элемент относится к счету, на который указывает pinvoice. БД автоматически добавит этот элемент в множество plnvoice->mltems.
Мы можем использовать функцию d_Set<T>::insert_element_last() для добавления pltem в множество plnvoice->mltems, как показано в следующем примере:
plnvoice-^ml terns . insert_element__last (pltem) ;
В этом операторе БД автоматически присваивает pinvoice переменной-члену pltem->mplnvoice. Это гарантирует, что отношение между элементами и счетами нарушаться не будет.
Функция Delete() практически идентична ProductOps::Delete(), за исключением того, что идентификатор счета мы должны из длинного целого преобразовать в строку символов. Функция ListO также использует стандартный алгоритм поиска C++ for each и функцию PrintRef, определенную в листинге 18.15, для вывода всех счетов, находящихся в БД.
Теперь недостает только одного — функции main() Она приведена в листинге 18.17.
Листинг 18.17. Главная программа
H^=====»aZ=====^==============^^
// main.срр - Программа для приложения
//===================,======================^^
♦include <iostream.h>
♦include <odmg.h>
♦include "ProductOps.h"
♦include "InvoiceOps.h"
const char dbName[] = "InvApp";
int main() {
d_Database* db = new dDatabase; try { db->open(dbName);
1 catch (d Error derr) (
cerr « "ODMG Error [" « derr.get_kind() « "] " « derr.what() « endl; return 1 ;
1
intintChoice =0;
ProductOpsProducts(db);
InvoiceOpsInvoices(db);
do {
cout «
"\nA
Trival Invoice Entry Program\n\n"
cout «
" 1
Enter invoices\n";
cout «
"2
Delete invoices\n";
cout «
3
Print invoice list\n";
cout «
"4
Add products\n";
cout «
"5
Edit product\n";
Глава 18
cout
«
"6. Delete products\n";
cout
«
"7. Print product list\n"
cout
«
"9. Exit\n\n";
cout
«
"Enter your choice: ";
cin
»
intChoice;
cin.ignore();
cout
«
endl;
switch(intChoice) (
case
1:
Invoices.Add(Products);
break;
case
2:
Invoices .Delete () ;
break;
case
3:
Invoices .List () ;
break;
case
4:
Products .Add () ;
break;
case
5:
Products .Edit () ;
break;
case
6:
Products . Delete () ;
break;
case
7:
Products .List () ;
break;
case
9:
cout « "Bye!\n\n";
break;
default: }
cout « "Please select
1, 2, 3, 4, 5, 6, 7 or 9.\n"
) while (intChoice ! = 9);
db->close () ; delete db;
return 0;
Основное отличие от первоначальной версии главной программы заключается в том, что перед выполнением операций, должна быть открыта БД. По завершению всех операций мы закрываем БД и удаляем объект БД.
Мы рассмотрели, как с использованием стандарта ODMG, может быть разработано приложение для работы с БД. Теперь обсудим некоторые технические вопросы, связанные с СУООБД.
Технические вопросы объектно-ориентированных баз данных
Некоторые технические вопросы проектирования СУООБД могут повлиять на производительность конкретных БД. Производители должны исследовать, как их покупатели будут использовать продукт, и определить лучшую стратегию оперирования этими вопросами.
Архитектура клиент/сервер
В среде клиент/сервер процессы сервера БД отвечают за передачу данных между диском и кэшем сервера. Эти процессы также управляют обработкой транзакций и параллельным доступом к объектам БД. В зависимости от реализации ООБД может существовать один или несколько процессов на сервере. Процесс сервера имеет несколько потоков, каждый из которых может выполнять операции для одного клиентского процесса. Такая организация дает возможность параллельного доступа к БД многим клиентам. Представление объекта в кэше в целом идентично представлению объекта при сохранении в БД: оно не зависит от приложения-клиента, которое может быть приложением C++, работающим на станции UNIX, или приложением Java, работающим на рабочей станции Windows NT.
Кэш клиента выступает представлением БД. Объекты представляются в естественном формате приложения-клиента. Например, приложение C++ будет видеть эти объекты — хранимые или нет — как обычные объекты C++. Отображение объектов между кэшами клиента и сервера автоматически выполняется СУООБД, прозрачно для программного обеспечения приложения.
Когда приложение-клиент требует объект, которого нет в кэше клиента, оно посылает запрос процессу сервера. Процесс сервера может вернуть требуемый объект вместе с другими, в зависимости от скорости передачи данных.
Хранение данных и кластеризация объектов
Данные сохраняются на страницах в носителях информации, обычно на жестких дисках. Размер каждой страницы колеблется от 2 до 16 Кб. Диспетчеры дисков считывают с диска блок страниц данных за один раз. Когда страница загружена в память или в кэш приложения, последовательные чтения и записи в объекты
Часть IV
на одной странице выполняются в кэше. Если доступ часто производится к какой-либо группе объектов, сохранение этих объектов на одной странице может сэкономить время доступа к диску и может существенно повысить производительность приложения. Способ группирования объектов называется кластеризацией объектов.
ООБД поддерживают хранение и получение сетей объектов. В нашем простом приложении ведения счетов объект Invoice, множество объектов Item и связанные объекты Product формируют сеть объектов, и доступ к ним, вероятно, будет производиться одновременно. Следовательно, имеет смысл сохранить их на одной странице. Для кластеризации объектов существует две широко используемые модели.
Первой из них является априорная модель. В этой модели разработчики приложения определяют, какие объекты должны быть сгруппированы. Это решение базируется на осведомленности разработчика о наиболее вероятных структурах. Стандарт ODMG предоставляет метод для задания группирования объектов. Когда новый хранимый объект создан, используется перегруженный оператор new():
pProduct = new(pDB) Product(strlD, strName, ushortPrice);
Перегруженный оператор new() другой версии принимает ссылку на объект (pProduct) и помещает новый объект за pProduct:
pProductl = new(pProduct) Product(strID, strName, ushortPrice);
Этот подход требует только того, чтобы новый объект размещался сразу же за ссылаемым объектом, причем не обязательно на той же странице.
Недостаток этого подхода заключается в том, что структура доступа к объектам непостоянна; с течением времени она может изменяться. Структуры доступа также могут перекрываться. Например, анализатор уровня запаса может обращаться к счетам и связанным с ними продуктам, а при создании отчета о заказчиках могут использоваться данные о счетах и заказчиках. Если мы не можем на одной странице разместить все объекты, связанные со счетом, какие же объекты должны находиться на одной странице?
Чтобы преодолеть этот недостаток, СУООБД может использовать другую модель — постприорную. В этой модели БД или администратор принимают решение после анализа статистики доступа к данным в реальное время. Объекты могут быть реорганизованы в соответствии с лучшей стратегией кластеризации. Эта модель, впрочем, имеет недостаток — требуется реорганизация объектов в реальном времени, что сказывается на производительности БД.
Передача данных
Объекты в БД загружаются в кэш сервера. Когда объект обрабатывается приложением-клиентом, он должен быть перенесен в кэш клиента. Технически можно просто передать объект, который необходимо обработать, клиенту, поскольку производительность сети ограничена. К сожалению, приложения часто должны получать доступ к группе объектов одновременно. Например, для вывода счета требуется объект Invoice, несколько объектов Invoiceitem и несколько объектов Product. Если мы будем передавать по одному объекту за один раз, то вызовем большое число сетевых запросов и ответов. Время передачи будет расти при увеличении количества вовлеченных объектов.
Естественное решение этой проблемы состоит в том, что в комбинации с кластеризацией объектов можно передавать по странице за один раз. Ведь страница полностью уже загружена в кэш сервера. Приятный эффект в том, что страница может содержать следующий объект для доступа. Такой подход может быть очень эффективен, когда требуется выполнить операции над последовательностью объектов. Например, при выводе списка продуктов имеется большая вероятность получить следующий продукт списка на той же странице.
Перенос объектов страницами, впрочем, не очень эффективен, когда необходим доступ к малому числу объектов. Например, если процесс приложения заинтересован лишь в двух объектах — счете и заказчике, то перенос страницы, содержащей 100 объектов, сильно повлияет на ширину полосы пропускания сети.
Более рациональный подход состоит в передаче между кэшами сервера и клиента только требуемых объектов. Это исключает бесполезное использование сети при передачи объектов. Данный подход требует более изощренного процесса общения сервера и клиента. Клиент должен указывать требуемые объекты, а сервер должен корректно отвечать. Этот подход, впрочем, увеличивает загрузку процесса сервера и снижает время доступа к серверу.
Блокирование данных
БД поддерживают параллельный доступ к одним объектам из разных приложений. Для предотвращения повреждения данных, вызываемого несколькими процессами, изменяющими один и тот же объект, один
Глава 18
процесс, который хочет внести изменения в конкретный объект, должен заблокировать его для того, чтобы другие процессы не могли его изменить. Когда изменение завершается, объект разблокируется и к его изменению могут приступать другие объекты. Возможно несколько ситуаций, приводящих к взаимоблокировке. Предположим, процесс А блокирует объект X, а процесс В блокирует объект Y. Если процесс А требует блокировки объекта Y, прежде чем разблокирует объект X, то он должен ждать до тех пор, пока процесс В разблокирует объект Y. Если в то же время процесс В пытается заблокировать объект X, он должен ждать, пока А его разблокирует. Теперь процессы А и В находятся во взаимоблокировке: ни один из них не может сделать второй блокировки или разблокировать объект.
ООБД может заблокировать отдельный объект или же всю страницу, на которой этот объект содержится. Блокирование объекта уменьшает вероятность взаимоблокировки, поскольку в таком случае блокируется меньше объектов. Блокирование отдельных объектов эффективно лишь при работе с небольшим числом объектов. Когда число обновляемых объектов увеличивается, приложению придется прибегнуть к десяткам или даже сотням блокировок, что снижает быстродействие. Блокирование отдельных объектов не получает преимуществ кластеризации объектов.
С другой стороны, заблокировав страницу, удобно работать при хорошей кластеризации объектов. Если блокируемые объекты находятся на одной и той же странице или страницах, несколько блокировок страниц могут заменить сотни блокировок объектов. Впрочем, блокирование страницы увеличивает вероятность появления ложных взаимоблокировок, поскольку в этом случае блокируются и те объекты, которые не должны быть заблокированы. Этот недостаток становится менее заметным при корректной реализации кластеризации объектов; в хорошо спроектированной кластеризации несвязанные объекты будут занимать лишь малую часть заблокированной страницы.
Резюме
В этой главе вы узнали о СУООБД и ознакомились с некоторыми техническими вопросами этих систем. Здесь продемонстрированы возможности связи стандарта ODMG с C++ в простом приложении ведения счетов. По сравнению с традиционной реляционной БД, на технологию ООБД возложены очень большие надежды. Она еще не так широко принята на рынке, поскольку менеджеры информационных систем сопротивляются переносу важных корпоративных данных на относительно молодую и развивающуюся систему БД.
Сейчас производители ООБД сильно усовершенствовали свои продукты для поддержки практически всех возможностей, предоставляемых традиционными реляционными БД. Кроме того, текущие продукты СУООБД предлагают возможность сохранения сложной структуры объектов, упрощают разработку и поддерживают объектно-ориентированные языки программирования. Многие производители также начали подчиняться стандарту ODMG, который поможет поддержать промышленную адаптацию ООБД в ближайшем будущем.
Защита приложении с помощью шифрования
В ЭТОЙ ГЛАВЕ
Краткая история шифрования
Понятие шифрования
Криптография по частному ключу
Криптография по общему ключу
Использование Pretty Good Privacy
Ограничения криптографии
Юридические ограничения на криптографию
Криптографические атаки
Цифровые подписи
Коммерческие криптографические продукты
Глава 19
Краткая история шифрования
Шифрование — это способ защиты данных от несанкционированного использования. Несколько способов используются для защиты данных от людей, которые могли бы злоупотребить информацией. Сотни лет назад главное применение шифрования заключалось в защите данных во время войны. Дэвид Канн (David Kahn) в своей впечатляющей работе The Codebreakers: The Comprehensive History Of Secret Communications from Acient Times to the Internet проследил историю криптографии, начиная со времен древнего Египта, проходя через Индию, Месопотамию, Вавилон, первую и вторую мировые войны, до современности, где шифрование обрело новое значение.
Обширное использование телеграфа и радио, ворвавшихся в современность, увеличило необходимость в шифровании информации, поскольку были доступны изощренные технические приемы ее перехвата. Военные коммуникации без использования шифрования ничего не стоили. Громадные достижения в области криптографии можно приписать работе Алана Тьюринга (Alan Turing) во времена второй мировой войны. При помощи Алана Тьюринга в Британии союзники могли взламывать код Enigma, используемый Германией во время войны.
После второй мировой войны Национальное агентство безопасности (National Security Agency — отделение Министерства обороны) стало центром исследований и деятельности в области криптографии. Существование этой засекреченной организации до недавнего времени отрицалось, и о ней шутливо говорили: "Нет такого агентства". Бюджет и деятельность этого агентства были строго засекречены. Шла молва, что NSA нанимает самое большое в мире число математиков и активно подслушивает телефонные разговоры.
Область применения кодов и шифров была ограничена NSA и военными организациями. Гражданское население довольствовалось использованием конвертов и курьеров в целях защиты данных. С появлением компьютеров, и особенно Internet, у гражданского населения тоже появилась необходимость в шифровании.
В I960 г. группа по исследованию криптографии была учреждена председателем IBM Томасом Ватсоном (Tomas Watson, Jr). Эта группа, руководимая Хорстом Фейстелем (Horst Feistel), разработала метод шифрования по частному ключу, названный Люцифером. Этот метод использовался в Лондоне для защиты систем автоматических кассиров. Успех Люцифера побудил IBM сделать его доступным для коммерческого использования. Группа, сформированная в этих целях, была возглавлена доктором Уолтером Тукманом (Walter Tuchman) и доктором Карлом Мейером (Dr. Carl Meyer), которые протестировали шифр и исправили несколько дефектов, найденных ими в этом методе. С 1974 года шифр был готов и доступен на кремниевом чипе.
Впрочем, IBM не единственная компания, сделавшая шифры доступными для коммерческого использования. Другие компании делали доступными другие шифры, но всех их объединяли некоторые проблемы:
Они не могли общаться друг с другом
Не было способа определить их надежность
Роль Национального бюро стандартов
В 1968 г. в Национальном бюро стандартов (NBS), позже переименованном в Национальный институт стандартов и технологии (NIST), попытались исследовать потребности гражданского населения и правительства в компьютерной безопасности. Было определено, что Соединенным Штатам необходим единый и межоперационный стандарт шифрования данных в целях хранения и передачи незасекреченной информации. Было также решено, что засекреченные данные останутся под юрисдикцией NSA.
Заявка на предложения была опубликована NBS в Федеральном Реестре (Federal Registry) 15 мая 1973 г., где было указано несколько необходимых требований к алгоритму:
Он должен был обеспечивать высокий уровень безопасности
Быть общим и рецензированным
Быть простым для понимания и обоснования
Эффективно выполняться за разумный промежуток времени
Алгоритм и устройства, реализующие его, должны быть экспортируемы
Безопасность алгоритма должна заключаться в ключе, а не в алгоритме
Быть достаточно гибким для приспособления в различных приложениях
Реализация алгоритма должна быть рентабельна с точки зрения стоимости
Часть IV
Первоначальные варианты этой заявки не были обнадеживающими, поскольку она не удовлетворяла всем требованиям. Еще одна заявка в Federal Register была сделана 27 августа 1974 г. и был один ответ: версия алгоритма Люцифера, который был в некотором роде ослаблен и одновременно укреплен NSA. Этот алгоритм стал стандартом шифрования данных США (U.S. Data Encryption Standard (DES)).
DES был создан совместными усилиями IBM и NSA. Вместо 128-разрядного ключа, используемого в алгоритме Люцифера, DES использовал 56-битовые ключи. По рекомендациям NSA, в алгоритм было внесено несколько изменений, и окончательно NBS опубликовала его в Federal Register 17 марта 1975 г. А 23 ноября 1976 г. алгоритм был формально принят для широкого использования. Описан алгоритм в Federal Information Processing Standart (FIPS PUB 46).
Внешне кажется, что безопасность DES несколько меньше безопасности Люцифера. По сведениям историка криптографии Дэвидом Канном, стандарт DES возник в результате компромисса между двумя сторонами NSA: одна сторона — создатели кодов — хотела гарантировать, чтобы код, который они сделают общим, был безопасным; другая сторона — группа взлома кодов — хотела, чтобы любой код, сделанный общим, NSA могло взломать, если бы этот код использовался иностранными правительствами. Результирующий алгоритм был ослабленной версией, использующей 56-разрядный ключ, но усилен "S-боксами", выполняющими подстановку.
На конференции Crypto Conference, проходившей в 1993 г. в Калифорнийском университет в СантаБарбаре, Майкл Винер (Michael Wiener) из Bell Northern Research представил научный доклад, в котором описал создание компьютера для взлома кода DES. Этот компьютер содержал чип, проверяющий 50 миллионов DES-ключей в секунду. С использованием подхода грубой силы приблизительное время, затрачиваемое на взлом сообщения, зашифрованного DES, составляло 3,5 часа; такая машина должна была стоить 1 млн долларов.
Из-за недостатков DES велся поиск альтернатив. И все же DES до сих пор широко используется. Другие вариации DES используют более длинные ключи, большие размеры блока, и увеличивают число циклов шифрования (такой подход характерен для способа Triple-DES). Недавно в качестве альтернативы DES правительство предложило Clipper, впрочем, эту альтернативу уже раскритиковали. Clipper будет описан далее в этой главе.
Понятие шифрования
Шифрование для защиты информации полагается на раздел математики, называемый криптографией. Принципы криптографии просты, а практическая система шифрования состоит из четырех частей:
Обычный текст. Сообщение, которое необходимо зашифровать.
Текст шифра. Сообщение после шифрования.
Алгоритм шифрования. Математическая функция, или алгоритм, используемый для шифрования сообщения.
Ключ шифрования. Несколько слов или фраз, используемых алгоритмом шифрования.
Главная цель выполнения шифрования заключается в создании из обычного текста зашифрованного текста так, чтобы невозможно было преобразовать зашифрованный текст в обычный без использования ключа шифрования.
Коды
Коды являются простейшим способом шифрования, поскольку для шифрования информации они используют таблицу кодов. Предположим, вы хотите послать другу секретное сообщение. Вот примерные четыре варианта:
Иди обедать и в кино
Иди обедать
Иди в кино
Оставайся дома
Для этих сообщений можно воспользоваться кодами, приведенными в табл. 19.1.
Вы можете сделать две копии этой таблицы и одну передать другу. Теперь, когда вы скажете слово Земля, ваш друг поймет, что вы этим подразумеваете "иди в кино". Этот способ хорош для нескольких случаев, но при постоянном использовании он станет менее эффективным: кому-либо подслушивающему понадобится не много времени, чтобы определить значение этих слов. (Одна из вариаций подхода кода
Глава 19
состоит в использовании нескольких таблиц кодов и в использовании в различных случаях разных таблиц.) Другой недостаток этого метода в том, что количество сообщений, которое вы можете послать, ограничено размером таблицы.
Таблица 19.1. Использование кодов для шифрования
Слово-код
Сообщение
Меркурий Венера Земля Марс
Иди обедать и в кино
Иди обедать
Иди в кино
Оставайся дома
Шифры
Шифры являются альтернативой кодам. Они позволяют использовать технологию смешивания букв в сообщении; сообщение может быть расшифровано с помощью рассекречивающего ключа. Шифры снимают проблему ограничения числа сообщений. Простейшим способом шифрования является использование подстановочных шифров", в подстановочном шифре каждый символ сообщения заменяется другим символом. Шифр Caesar является очень распространенной технологией для подстановочных шифров; он просто сдвигает алфавит на три позиции вправо. Вариации этой технологии можно создавать, сдвигая символы на п позиций вправо. Предположим, вы решили сдвинуть символы в сообщении на пять позиций, как показано на рис. 19.1.
РИСУНОК 19.1.Использование шифра сдвига на пять позиций.РИСУНОК 19.1.Использование шифра сдвига на пять позиций.ABCQEFGH15§789АВ£ОSTUVWXY^ONQPQRSTUVABCQEFGH15§789АВ£ОSTUVWXY^ONQPQRSTUV1KLMNQPQREESH11K1.M123456282WXYZ612341KLMNQPQREESH11K1.M123456282WXYZ61234
РИСУНОК 19.1.
Использование шифра сдвига на пять позиций.
РИСУНОК 19.1.
Использование шифра сдвига на пять позиций.
ABCQEFGH1
5§789АВ£О
STUVWXY^O
NQPQRSTUV
ABCQEFGH1
5§789АВ£О
STUVWXY^O
NQPQRSTUV
1KLMNQPQR
EESH11K1.M
123456282
WXYZ61234
1KLMNQPQR
EESH11K1.M
123456282
WXYZ61234
Сообщение теперь кодируется следующим образом:
MOVIES => HJQD9N DINNER => 8DII9M HOME => CJH9
Самый большой недостаток шифров в том, что они без труда могут быть взломаны опытным криптоанализатором. Реализация подстановочного шифра приведена в листинге 19.1.
Листинг 19.1 Реализация подстановочного шифра //Автор: Megh Thakkar // Простая реализация подстановочного шифра // Назначение: Этот шифр запрашивает у пользователя // количество позиций для сдвига и затем сдвигает символы // на заданное число позиций.
#include <stdio.h> #include <iostream.h>
int main() { int c;
int sf_places; cout « "\n Enter the number of places to " « "shift characters:";
cin » sf_places;
while ((c = getcharO) !=EOF) { if ('a' <= c SS c <= ' 1' ) c += sf_places;
else if ('A' <= c SS c <= 'L' )
Часть IV
с += sf_places;
else if ('m' <= c SS с <= ' z' ) с -= sf_places;
else if ('M' <= c SS c <= ' Z' ) c -= sf_places;
putchar(c) ;
}
return 0;
)
В листинге 19.2 приведена реализация шифра Caesar.
Листинг 19.2. Реализация шифра Caesar // Автор: Megh Thakkar // Простая реализация шифра Caesar // Формат запуска: входной файл и выходной файлы //Назначение: Эта программа принимает обычный текстовый файл
Ии шифрует его с использованием шифра Caesar.
Н Она может также использоваться для расшифровки файла, //зашифрованного шифром Caesar.
♦include <iostream.h> ♦include <stdio.h> ♦include <ctype.h> ♦define 12n (X) (toupper(X) - 'A') ♦define n21(X) ((X) + 'A') ♦define ALPHABET_LEN 26 int main(int argc,char *argv[]) { char c; char *key; int z ; int decrypt = 0; FILE *infile; FILE *outfile; infile = fopen(argv[l], "rb"); outfile = fopen(argv[2], "wb"); key = argv[3] ; if (infile = NOLL I | outfile == NULL) { cout« "\nSorry. Files cannot be opened\n"; return -1; ) cout « "\n Do you want to encrypt[0] or decrypt[ 1] : "; cin » decrypt;
while ( (z = getc (infile) ) != EOF) { c = (char)z; if (isalpha(c)) { c = 12n(c); if (• decrypt) ( c = (c + 12n(*key)) % ALPHABET_LEN; ) else ( c = (c + ALPHABET_LEN - 12n(*key)) % ALPHABET_LEN; ) c = n21 (c) ; ) putc(c,outfile); ) return (0) ;
)
Глава 19
Шифр VernamШифр Vernam
Шифр Vernam
Шифр Vernam
Шифры Vernam также называются одноразовыми прокладками. Используя их, вы получите гибкость шифровШифры Vernam также называются одноразовыми прокладками. Используя их, вы получите гибкость шифрови безопасность кодов. В своей основе эта технология использует набор кодовых таблиц, каждая из которых представляет отдельную часть сообщения. По существу, она использует слово в одной таблице для создания части сообщения, а затем смешивает слова в таблице для создания новой таблицы, которая используется для записи другой части сообщения, слова второй таблицы затем смешиваются опять для записи третьей части сообщения. Этот процесс продолжается до тех пор, пока сообщение не зашифруется полностью. Единственный способ взлома зашифрованного таким образом текста заключается в использовании точно такого же смешивания и точно такого же количества слов. На рис. 19.2 показана одноразовая прокладка.и безопасность кодов. В своей основе эта технология использует набор кодовых таблиц, каждая из которых представляет отдельную часть сообщения. По существу, она использует слово в одной таблице для создания части сообщения, а затем смешивает слова в таблице для создания новой таблицы, которая используется для записи другой части сообщения, слова второй таблицы затем смешиваются опять для записи третьей части сообщения. Этот процесс продолжается до тех пор, пока сообщение не зашифруется полностью. Единственный способ взлома зашифрованного таким образом текста заключается в использовании точно такого же смешивания и точно такого же количества слов. На рис. 19.2 показана одноразовая прокладка.YesNoTakeGiveCarYesNoTakeGiveCarGiveTakeNoCarYesGiveTakeNoCarYesCarTakeNoYesGiveCarTakeNoYesGiveРИСУНОК 19.2.РИСУНОК 19.2.NoTakeGiveYesCarNoTakeGiveYesCarПример одноразовой прокладки.Пример одноразовой прокладки.Чтобы определить, как смешивать символы, можно воспользоваться генератором случайных чисел; перемешиваться должен каждый символ. Одноразовое использование кодовой таблицы прежде чем та будет перемешана, дает ей необходимую безопасность. Каждая таблица может представлять листок из одноразовой прокладки. Другими словами, только одноразовая прокладка, используемая для шифрования сообщения, может быть использована для преобразования зашифрованного текста в обычный.Каждая страница прокладки содержит различный набор кодов. Часть сообщения можно создать с помощью одной страницы, другую часть сообщения — с помощью второй страницы и т.д. Иными словами, части сообщения конструируются с помощью различных кодовых таблиц; для расшифровки этого сообщения вы должны использовать кодовые таблицы точно в такой же последовательности, что и при шифровании сообщения.Чтобы определить, как смешивать символы, можно воспользоваться генератором случайных чисел; перемешиваться должен каждый символ. Одноразовое использование кодовой таблицы прежде чем та будет перемешана, дает ей необходимую безопасность. Каждая таблица может представлять листок из одноразовой прокладки. Другими словами, только одноразовая прокладка, используемая для шифрования сообщения, может быть использована для преобразования зашифрованного текста в обычный.Каждая страница прокладки содержит различный набор кодов. Часть сообщения можно создать с помощью одной страницы, другую часть сообщения — с помощью второй страницы и т.д. Иными словами, части сообщения конструируются с помощью различных кодовых таблиц; для расшифровки этого сообщения вы должны использовать кодовые таблицы точно в такой же последовательности, что и при шифровании сообщения.
Шифры Vernam также называются одноразовыми прокладками. Используя их, вы получите гибкость шифров
Шифры Vernam также называются одноразовыми прокладками. Используя их, вы получите гибкость шифров
и безопасность кодов. В своей основе эта технология использует набор кодовых таблиц, каждая из которых представляет отдельную часть сообщения. По существу, она использует слово в одной таблице для создания части сообщения, а затем смешивает слова в таблице для создания новой таблицы, которая используется для записи другой части сообщения, слова второй таблицы затем смешиваются опять для записи третьей части сообщения. Этот процесс продолжается до тех пор, пока сообщение не зашифруется полностью. Единственный способ взлома зашифрованного таким образом текста заключается в использовании точно такого же смешивания и точно такого же количества слов. На рис. 19.2 показана одноразовая прокладка.
и безопасность кодов. В своей основе эта технология использует набор кодовых таблиц, каждая из которых представляет отдельную часть сообщения. По существу, она использует слово в одной таблице для создания части сообщения, а затем смешивает слова в таблице для создания новой таблицы, которая используется для записи другой части сообщения, слова второй таблицы затем смешиваются опять для записи третьей части сообщения. Этот процесс продолжается до тех пор, пока сообщение не зашифруется полностью. Единственный способ взлома зашифрованного таким образом текста заключается в использовании точно такого же смешивания и точно такого же количества слов. На рис. 19.2 показана одноразовая прокладка.
Yes
No
Take
Give
Car
Yes
No
Take
Give
Car
Give
Take
No
Car
Yes
Give
Take
No
Car
Yes
Car
Take
No
Yes
Give
Car
Take
No
Yes
Give
РИСУНОК 19.2.
РИСУНОК 19.2.
No
Take
Give
Yes
Car
No
Take
Give
Yes
Car
Пример одноразовой прокладки.
Пример одноразовой прокладки.
Чтобы определить, как смешивать символы, можно воспользоваться генератором случайных чисел; перемешиваться должен каждый символ. Одноразовое использование кодовой таблицы прежде чем та будет перемешана, дает ей необходимую безопасность. Каждая таблица может представлять листок из одноразовой прокладки. Другими словами, только одноразовая прокладка, используемая для шифрования сообщения, может быть использована для преобразования зашифрованного текста в обычный.
Каждая страница прокладки содержит различный набор кодов. Часть сообщения можно создать с помощью одной страницы, другую часть сообщения — с помощью второй страницы и т.д. Иными словами, части сообщения конструируются с помощью различных кодовых таблиц; для расшифровки этого сообщения вы должны использовать кодовые таблицы точно в такой же последовательности, что и при шифровании сообщения.
Чтобы определить, как смешивать символы, можно воспользоваться генератором случайных чисел; перемешиваться должен каждый символ. Одноразовое использование кодовой таблицы прежде чем та будет перемешана, дает ей необходимую безопасность. Каждая таблица может представлять листок из одноразовой прокладки. Другими словами, только одноразовая прокладка, используемая для шифрования сообщения, может быть использована для преобразования зашифрованного текста в обычный.
Каждая страница прокладки содержит различный набор кодов. Часть сообщения можно создать с помощью одной страницы, другую часть сообщения — с помощью второй страницы и т.д. Иными словами, части сообщения конструируются с помощью различных кодовых таблиц; для расшифровки этого сообщения вы должны использовать кодовые таблицы точно в такой же последовательности, что и при шифровании сообщения.
В листинге 19.3 приведена реализация одноразовой прокладки.В листинге 19.3 приведена реализация одноразовой прокладки.Листинг 19.3. Реализация одноразовой прокладки //Автор: Megh Thakkar //Реализация одноразовой прокладки //Формат запуска: входной файл и выходной файлы //Описание: Программа принимает в параметрах три файла. // Над первыми двумя файлами она выполняет исключающее ИЛИ Ии помещает результат в третий файл.И #include <stdio.h> #include <stdlib.h> #include <iostream.h>#define BUF_SZ 32768Ulong offset = 0;// Смещение в файле ключей.FILE *infile = 0;FILE *keyfile = 0;FILE *outfile = 0;size_t amt_read = BUF_SZ;size_t amt_write = BUF_SZ; size_t amt_key;//amt_read, amt_write и amt_key используются во время шифрования для определения //количества используемых символов.И Выделить дисковые буферы. / / Ы используется для входного и выходного файлов //Ь2 используется для ключевого файлаchar * iobuf; char * kbuf;int i; //Используется для цикла, int main(int argc, char * argv[j) { if (argc != 5) {Листинг 19.3. Реализация одноразовой прокладки //Автор: Megh Thakkar //Реализация одноразовой прокладки //Формат запуска: входной файл и выходной файлы //Описание: Программа принимает в параметрах три файла. // Над первыми двумя файлами она выполняет исключающее ИЛИ Ии помещает результат в третий файл.И #include <stdio.h> #include <stdlib.h> #include <iostream.h>#define BUF_SZ 32768Ulong offset = 0;// Смещение в файле ключей.FILE *infile = 0;FILE *keyfile = 0;FILE *outfile = 0;size_t amt_read = BUF_SZ;size_t amt_write = BUF_SZ; size_t amt_key;//amt_read, amt_write и amt_key используются во время шифрования для определения //количества используемых символов.И Выделить дисковые буферы. / / Ы используется для входного и выходного файлов //Ь2 используется для ключевого файлаchar * iobuf; char * kbuf;int i; //Используется для цикла, int main(int argc, char * argv[j) { if (argc != 5) {
В листинге 19.3 приведена реализация одноразовой прокладки.
В листинге 19.3 приведена реализация одноразовой прокладки.
Листинг 19.3. Реализация одноразовой прокладки //Автор: Megh Thakkar //Реализация одноразовой прокладки //Формат запуска: входной файл и выходной файлы //Описание: Программа принимает в параметрах три файла. // Над первыми двумя файлами она выполняет исключающее ИЛИ Ии помещает результат в третий файл.
И #include <stdio.h> #include <stdlib.h> #include <iostream.h>
#define BUF_SZ 32768U
long offset = 0;// Смещение в файле ключей.
FILE *infile = 0;
FILE *keyfile = 0;
FILE *outfile = 0;
size_t amt_read = BUF_SZ;
size_t amt_write = BUF_SZ; size_t amt_key;
//amt_read, amt_write и amt_key используются во время шифрования для определения //количества используемых символов.
И Выделить дисковые буферы. / / Ы используется для входного и выходного файлов //Ь2 используется для ключевого файла
char * iobuf; char * kbuf;
int i; //Используется для цикла, int main(int argc, char * argv[j) { if (argc != 5) {
Листинг 19.3. Реализация одноразовой прокладки //Автор: Megh Thakkar //Реализация одноразовой прокладки //Формат запуска: входной файл и выходной файлы //Описание: Программа принимает в параметрах три файла. // Над первыми двумя файлами она выполняет исключающее ИЛИ Ии помещает результат в третий файл.
И #include <stdio.h> #include <stdlib.h> #include <iostream.h>
#define BUF_SZ 32768U
long offset = 0;// Смещение в файле ключей.
FILE *infile = 0;
FILE *keyfile = 0;
FILE *outfile = 0;
size_t amt_read = BUF_SZ;
size_t amt_write = BUF_SZ; size_t amt_key;
//amt_read, amt_write и amt_key используются во время шифрования для определения //количества используемых символов.
И Выделить дисковые буферы. / / Ы используется для входного и выходного файлов //Ь2 используется для ключевого файла
char * iobuf; char * kbuf;
int i; //Используется для цикла, int main(int argc, char * argv[j) { if (argc != 5) {
496I Живучесть объектов и шифрование1 Часть IVcout «"\nUsage: onetime input_file "496I Живучесть объектов и шифрование1 Часть IVcout «"\nUsage: onetime input_file "« "key_file output_file offset.";« "key_file output_file offset.";
496
I Живучесть объектов и шифрование
1 Часть IV
cout «"\nUsage: onetime input_file "
496
I Живучесть объектов и шифрование
1 Часть IV
cout «"\nUsage: onetime input_file "
« "key_file output_file offset.";
« "key_file output_file offset.";
return -1; )
offset = strtol(argv[4], NULL, 0) ; if (offset < 0) { cout « "\n " « argv[4] « " is not a valid value."
« "\nOnly positive offset values" « "are allowed.\n"; return -1;
}
11 Открыть файлы infile = fopen(argv[l], "rb"); keyfile = fopen(argv[2], "rb"); outfile= fopen(argv[3], "wb"); if ((infile == NULL) | | (keyfile == NULL) | | (outfile = NULL)) ( cout « "\n Sorry. Unable to open files"; )
// Перейти к заданному смещению в ключевом файле if (offset != 0) (
if (fseek(keyfile, offset, SEEK_SET)) ( cout « "\nError: Unable to seek to " « "the offset value" « "\n";
} } iobuf = new char[BUF_SZ]; kbuf = new char[BUF_SZ]; if ((iobuf == NULL) | | (kbuf == NULL) ) { cout « "Error. Insufficient memory\n"; return -1; }
//Выполнить шифрование.
while (amt_write > 0) {
amt_write = fread(iobuf, 1, amt_read, infile); amt_key = fread(kbuf, 1, amt_write, keyfile), if (amt_key < amt_write) { cout « "\nERROR: Key length after " « "offset is too short";
amt_write = amt_key; } for (i = 0; i < BUF_SZ; i++) iobuf [i] A= kbuf[i];
amt_key = fwrite(iobuf, 1, amt_write, outfile); )
// Закрыть файлы fclose(infile);
Глава 19
fclose(keyfile);
fclose(outfile);
return 0;
}
Криптография по частному ключу
Вид криптографии, используемой в ранние времена в методах кодирования и шифрования, таких как шифры Caesar и Vernam, называется криптографией по частному, или секретному (закрытому), ключу. Термин "частный" используется в связи с тем, что такая технология предполагает, что и отправитель, и получатель сообщения имеют ключ, который должен быть сохранен как частный. Криптография по частному ключу использует тот же ключ и для отправителя, и для получателя и поэтому также называется симметричной криптографией.
Всякий раз, когда вы хотите передать какому-либо человеку сообщение с использованием этих методов, вы должны передать ему криптографический ключ. Процесс обмена криптографическим ключом называется распространением ключа и может быть очень сложным. Ключ — это секрет для взлома шифра; если существует действительно безопасный метод сообщения ключа, почему же этот метод не используется в первую очередь для передачи сообщения? Многие годы метод распространения ключа, используемый правительством США, был таким: ключи помещали в закрытый портфель, который наручниками приковывали к курьеру. Курьер должен был сесть в самолет, а по прибытии в страну его встречали должностные лица, вместе с которыми он направлялся в посольство. В посольстве наручники снимались, и ключи становились доступными для расшифровки дипломатических сообщений. Курьер не мог снять наручники или открыть портфель. Если "плохие парни" ловили курьера, дипломаты Соединенных Штатов узнавали от этом и не использовали эти ключи для шифрования сообщений.
Алгоритмы частного ключа
Существует несколько популярных алгоритмов частного ключа. Кратко опишем некоторые из них:
DES. Стандарт шифрования данных был принят в 1977 г. правительством США, а в 1981 г. стал ANSI- стандартом. Для шифрования информации он использовал 56-разрядные ключи.
Triple-DES. Этот алгоритм является вариацией DES; он использует алгоритм шифрования DES три раза с помощью двух различных ключей. Эта технология в текущее время используется финансовыми учреждениями.
RC-коды. Коды Ривеста были названы так в честь профессора MIT Рональда Ривеста (Ronald Rivest), также являющегося соавтором алгоритма шифрования по общему ключу RSA. Эти методы являются патентованными алгоритмами, разработанными RSA Data Security. Два наиболее популярных кода — это RC2 (метод блочного шифрования типа DES) и RC4 (который является поток шифра, генерирующего поток псевдослучайных чисел, которые с помощью операции исключающего ИЛИ объединялись с информацией).
Эти коды могут работать с ключами от 1 до 1024 байтов длиной. Не существует оценки действительной безопасности этих кодов, поскольку они являются частной собственностью.
IDEA. В 1990 г. Джеймс Л. Массе (James L. Massey) и Ксуждия Лей (Xuejia Lai) разработали и опубликовали Международный алгоритм шифрования данных в Цюрихе. Эта технология использует 128-раз- рядный ключ и кажется очень сильной (хотя истинная природа обеспечиваемой им безопасности неизвестна).
Skipjack. Этот секретный алгоритм был разработан NSA для гражданских целей. Он использует 80- разрядный ключ.Он является сердцем Chip Clipper, используемого правоохранительными организациями дтя выполнения легальных прослушиваний.Chip Clipper не настолько безопасен, как Skipjack.
Шифрованию по частному ключу свойственны несколько недостатков:
Как отмечалось ранее, самой большой проблемой с шифрованием по частному ключу является метод распространения ключа. На рис. 19.3 показано, что для секретной связи между тремя людьми необходимо три ключа. На рис. 19.4 показано, что для подобной связи между четырьмя людьми уже необходимо вдвое больше ключей. Если в этот секретный узел добавить еще двух человек, потребуется 15 ключей. В общем, для секретного узла, включающего п человек, вам необходимо п(п-1)/2 ключей. Для управления ситуациями реального мира, например финансовыми учреждениями, может
Часть IV
понадобиться очень большое число ключей. Такое большое число ключей необходимо в связи с тем, что каждая пара людей, вовлеченных в шифрование по частному ключу, совместно использует этот ключ.
Второй проблемой в шифровании по частному ключу является безопасность, касающаяся распространения самого ключа.
РИСУНОК 19.3. Частное шифрование для трех человек.РИСУНОК 19.3. Частное шифрование для трех человек.РИСУНОК 19.4. Частное шифрование для четырех человек.РИСУНОК 19.4. Частное шифрование для четырех человек.
РИСУНОК 19.3. Частное шифрование для трех человек.
РИСУНОК 19.3. Частное шифрование для трех человек.
РИСУНОК 19.4. Частное шифрование для четырех человек.
РИСУНОК 19.4. Частное шифрование для четырех человек.
Механизмы шифрования по секретному ключу
Наиболее удачные методы шифрования по секретному ключу для преобразования обычного текста в зашифрованный предполагают использование простого набора процедур и функций. Очень широко для этой цели используется понятие поблочное шифрование; вместо одного байта или символа при шифровании оно использует блок, или группу байтов.
Каждый блок может обрабатываться любой комбинацией нескольких процессов. Окончательный зашифрованный текст может быть сгенерирован с применением следующих процессов во время нескольких итераций или раундов шифрования:
Методы подстановки. Очень широко используются в алгоритмах шифрования. Как мы видели ранее, знание языка и контекста сообщения дает хакеру массу информации о способе расшифровки кода. Поэтому вместо выполнения подстановки битов или символов используется поблочное шифрование. Безопасность достигается отображением один-к-одному блоков символов обычного текста и блоками зашифрованного текста того же размера, но связь между ними определить не так-то просто. Методы подстановки обычно предполагают использование некоторой простой стратегии, например, таблицы поиска или функции XOR.
Перестановка. Вы можете переупорядочить символы обычного текстового сообщения для преобразования его в анаграмму, которая выглядит как сообщение со случайными символами. Например, большинство сообщений состоят из 7-разрядных ASCII-символов. Смешивая биты для создания случайного набора битов, вы можете получить желаемую шифровку. Методы перестановки обычно используются совместно с другими способами, такими как подстановка. .
Функции шифрования. Исключающее ИЛИ — пример функции шифрования (будет описана в следующем разделе). Широко используются и другие функции, такие как двоичное сложение, умножение и модульные арифметические функции.
Использование исключающего ИЛИ (XOR)
для выполнения поблочного шифрования
Популярным методом поблочного шифрования является функция XOR. Функция исключающее ИЛИ используется для указания, что если существует два условия (например, условия А и В), то истинно либо условие А, либо условие В, но не оба. Ниже приведены полный набор всех возможных сочетаний оперируемых значений и результат операций над ними:
XOR (0,0) = о
XOR(0,1) = 1
XOR (1,0) = 1
XOR (1,1) = 0
Глава 19
Преимущество функции XOR в том, что она может обращать себя и, следовательно, может использоваться для шифрования.
Предположим, что мы взяли значения А = 10101000 и В = 00111001. Следовательно, С = XOR(A,B) = 10010001. Теперь если мы выполним операцию над В и С, то получим А:
XOR(B,C) = XOR(00111001, 10010001) = 10101000 = А
В листинге 19.4 показано, как можно реализовать функцию XOR:
Листинг 19.4. Реализация функции XOR
//Автор: Megh Thakkar // Простая реализация функции XOR // Формат запуска: входной файл и выходной файлы // Предназначение: эта программа принимает текстовый файл, выполняет / /операцию XOR между каждым символом и предоставленным ключом
// и помещает зашифрованный результат в "зашифрованный" файл. #include <stdio.h> #include <iostream.h> int main(int argc, char *argv[J) { FILE *plain, *cipher; char *cp; int c; if ( (cp = argv[l]) 66 *cp! = '\0') { plain = fopen(argv[2], "rb"); cipher = fopen(argv[3], "wb"); if (plain = NULL | | cipher == NULL) ( cout« "\nSorry. Files cannot be opened\n"; return -1; ) while ((c = getc (plain)) != EOF) ( if (*cp == '\0') cp = argv[l] ; c A= *cp++; putc(c, cipher); ) fclose(cipher); fclose(plain) ; ) return 0; )
Использование блоков подстановки
Популярный метод реализации функции подстановки состоит в использовании конструкции, называемой блоком подстановки или S-блоком. Функция S-блока принимает на вводе некоторый бит или набор битов и предоставляет на выводе другой бит или набор битов. Для выполнения преобразования в методе используется таблица замещения.
ПРИМЕЧАНИЕ -
Такие таблицы замещения могут на один и тот же вывод отображать более одного ввода. В результате хакер не mo-’SW жет получить вывод S-блока и определить, сколько вводов выполнялось для генерирования вывода.>
Использование расширенной перестановки
Расширенная перестановка принимает блок данных и расширяет его в набор перекрывающихся групп; каждая группа может быть мало похожа на оригинальный блок. Предположим, что у нас есть 24-разрядный
Часть IV
блок; чтобы преобразовать его в 36-разрядный блок мы можем выполнить расширенную перестановку следующим образом:
Разбить 24 бита на шесть групп по 4 бита в каждой.
В каждую группу добавить по биту спереди и сзади.
Теперь мы имеем шесть групп по 6 битов каждая, что в сумме составляет 36 битов.
Технологии, приведенные в этом и предыдущих разделах, являются просто широко используемыми методами в алгоритмах шифрования. Для получения окончательного алгоритма можно смешивать и сочетать их. Впрочем, секретность алгоритма не связана с использованием особых методов или, по крайней мере, определенного числа этих методов. О безопасности алгоритмов рассказывается в разделе "Строгие алгоритмы" далее в этой главе.
Использование циклов шифрования
Алгоритмы шифрования становятся более сложными и одновременно более безопасными при использовании в них различных методов шифрования по очереди. Впрочем, важно на различных циклах использовать различные методы шифрования. Например, если в первом цикле шифрования вы воспользовались подстановкой (или итерацией), а во втором цикле опять воспользовались подстановкой (даже если символы подстановки на двух циклах различаются), результирующий текст будет не безопаснее зашифрованного в одном цикле. Фактически, даже если вы используете тысячи циклов подстановки, безопасность будет иметь такой же уровень, что и при одном цикле, поскольку между обычным текстом и окончательным шифром в обоих случаях будет отображение типа один-к-одному. Более безопасное шифрование может быть достигнуго благодаря использованию одного цикла подстановки, за которым будет следовать второй цикл перестановки. В популярных алгоритмах используется от 8 до 16 циклов различных методов шифрования.
Использование центров распространения ключей
Одной из широко используемых технологий в шифровании по частному ключу являются центры распространения ключей (Key Distribution Center — КОС). Всякий раз, когда пользователь А хочет связаться с пользователем В, он вызывает KDC, который генерирует случайный одноразовый ключ сеанса. Сгенерированный ключ шифруется и рассылается пользователям, которые хотят связаться друг с другом. Проблема этого метода is том, что ключ, используемый для шифрования ключей сеансов находится в файле в KDC. Поэтому любой имеющий доступ в KDC может получить ключи шифрования. Недавно секретная служба разоблачила агента NSA, который продавал криптографические ключи в другие страны.
Из предыдущей дискуссии ясно, что частные ключи трудно использовать для передачи информации между гражданским населением. Решением этой проблемы являются общедоступные ключи шифрования.
Криптография по общему ключу
Общий ключОбщий ключРИСУНОК 19.5. Понятие криптографии по общему ключу и использование общих и частных ключей.РИСУНОК 19.5. Понятие криптографии по общему ключу и использование общих и частных ключей.Криптография по общему ключу называется также асимметричной криптографией и является результатом математического открытия, сделанного в 1970 г. В отличие от методов симметричного ключа, использующих один ключ для шифрования и расшифровки, в асимметричных методах используются два ключа; секретный и общий (общедоступный). Общий ключ используется для шифрования сообщения, а секретный — для его расшифровки. Получатель имеет секретный ключ, который должен быть защищен. Для генерирования двух ключей, связанных математически, используется математический процесс. Обратитесь к рис. 19.5, чтобы понять, как в криптографии по общему ключу могут использоваться различные ключи.
Общий ключ
Общий ключ
РИСУНОК 19.5. Понятие криптографии по общему ключу и использование общих и частных ключей.
РИСУНОК 19.5. Понятие криптографии по общему ключу и использование общих и частных ключей.
Цель криптографии по общему ключу заключается в устранении самой большой проблемы с распространением по частному ключу. С годами в области криптографии по общему ключу было определено несколько методов, которые описаны в следующих разделах.
Глава 19
Метод головоломки Ральфа Меркле
Ральф Меркле (Ralph Merkle) опубликовал свою работу в Communications of the ACM — передовом журнале по теории вычислительной техники, где он определил, что его работа — это безопасная связь по небезопасным каналам. В основу своего подхода он положил головоломки. Чтобы понять этот метод, предположим, что Джон и Джейн хотят связаться друг с другом по каналу, о котором известно, что тот небезопасен. Сначала Джон создает большое количество ключей шифрования, например, миллион. Ключи Джон помещает в эти головоломки — по одному на головоломку. Затем эти ключи Джон помещает в головоломки. Решение каждой головоломки занимает около одной минуты. Джон посылает головоломки Джейн, которая выбирает одну из головоломок и связанный с нею ключ. С помощью этого ключа Джейн шифрует сообщение и посылает его Джону. Ключ, выбранный Джейн, Джон определяет по своему списку ключей. Последующие сеансы связи Джона и Джейн будут проходить по этому ключу. Подслушивающий будет знать, что туда и назад ходили головоломки, но определение нужного ключа займет чрезвычайно долгое время.
Говоря проще, Джон создает очень много ключей и "заворачивает", или прячет их в некоторую "обертку" — по одному ключу на обертку — и посылает их Джейн. Джейн случайным образом выбирает одну из оберток и, следовательно, один ключ, шифрует с его помощью сообщение и посылает его Джону. Джон может определить, какой из ключей был выбран, поскольку у него есть весь список ключей. Затем этот ключ станет ключом для последующих переговоров.
Многопользовательские криптографические методы Диффи-Хельмана
В 1975 г. Уайтфилд Диффи (Whitfield Diffie) и Мартин Хельман (Martin Helman) опубликовали научную работу "Многопользовательские криптографические технологии". В их криптографических методах использовались концепции, широко применяемые сейчас в криптографии по общему ключу. Основная идея этой стратегии заключалась в том, чтобы сообщение можно было зашифровать с помощью этого ключа, а расшифровать — другим ключом. Диффи и Хельман внесли несколько предложений о том, как можно достичь такого эффекта, среди которых были следующие:
Умножение простых чисел, что делается легко; разложение же соответствующего результата на простые множители намного сложнее.
Использование дискретного возведения в степень; дискретное логарифмирование соответствующего результата на простые множители намного сложнее.
Для проведения последующих исследований Диффи и Хельман выбрали второй подход. Подход обмена экспоненциальным ключом Диффи-Хельмана был опубликован в их научной работе "Новые направления в криптографии" в IEEE Transactions on Information Theory.
Подход Диффи-Хельмана основан на предложении Джона Гилла (John Gill): взять экспоненты чисел и вычислить результат по модулю некоторого простого числа.
Этот метод работает следующим образом:
Оба участника должны оговорить два числа, р и q. Числа р и q могут быть общеизвестны.
Теперь каждый участник должен выбрать число, провести математическую операцию, включающую р, q и выбранное число, и передать результат другому участнику.
Предположим, первый участник выбрал Ml, а второй — М2. Результатами их отдельных математических операций будут числа N1 и N2.
С помощью второй математической формулы оба участника могут выбрать еще одно число, К, являющееся результатом функции от Ml и N2 или от М2 и N1, но не от чисел N1 и N2. Последующая связь будет проходить по ключу сеанса К.
Подслушивающий может получить доступ к р, q, N1 и N2, но не к Ml и М2. В результате подслушивающий не сможет вычислить К. Таким образом, К может использоваться в качестве ключа сеанса для алгоритма шифрования по частному ключу, такого как DES.
Этот метод применяется для связи между двумя людьми и предполагает использование трех ключей: двух секретных (по одному на участника) и ключа сеанса, определяемого при переговорах. Иначе говоря, переговоры начинаются с двух человек, использующих свои собственные ключи; в ходе переговоров они определяют ключ сеанса, который и будет использоваться для всех дальнейших сообщений.
Часть IV
Метод RSA
Метод RSA является одним из наиболее известных методов шифрования. Он используется в качестве системы общедоступного ключа в PGP (Pretty Good Privacy — популярный метод шифрования, описанный далее в этой главе). Для шифрования в RSA используется общеизвестный ключ, но расшифровка может быть проведена только человеком, имеющим секретный ключ. RSA также может использоваться в качестве системы электронной подписи.
Самая большая проблема подхода Диффи-Хельмана в том, что два участника должны активно общаться между собой. Такое обычно невозможно при связи между двумя людьми по электронной почте, которые не обязательно общаются активно.
В 1976 г. три профессора из лаборатории теории вычислительной техники MIT — Рональд Ривест (Ronald Rivest), Эди Шамир (Adi Shamir) и Лен Эдельман (Len Adelman) — начали работу по предложению, сделанному в работе Диффи-Хельмана "Новые направления в криптографии" для поиска практической многопользовательской криптографической системы. После нескольких месяцев исследований ими было сделано заключение, что такая общественная система невозможна. Затем в 1977 г. они осознали основной факт: очень легко умножить два простых числа, чтобы получить большое составное число, но не так легко затем найти простые компоненты этого числа. Результатом этих исследований стал метод, названный по инициалам его изобретателей: RSA. Этот метод лучше метода обмена ключом Диффи-Хельмана, поскольку он не базируется на активных переговорах человека, выполняющего шифрование, и человека, выполняющего расшифровку.
Чтобы понять, как работает алгоритм RSA, приведем пример из книги Брюса Шнайера (Bruce Schneier) Apllied Cryptography.
Выберите случайным образом два очень больших простых числа, например, М и N.
Получите Z (модуль шифрования), умножив М на N. Другими словами, Z = M*N.
Выберите Е, относительно простое число для (M-1)*(N-1).
Объявите Z и Е RSA-ключами, которые могут использоваться для шифрования информации.
Предположим, что вы выбрали M = 47hN = 71. Следовательно, Z = 47*71 = 3337. Теперь вы должны выбрать Е, для которого вычисляется (M-1)*(N-1) = 46*70 = 3220. Предположим, что вы выбрали Е = 79. Теперь ключ шифрования вычисляется с использованием расширенного алгоритма Эвклида и простых чисел следующим образом:
D = 79 - 1 (mod 3220) = 1019
ПРИМЕЧАНИЕ
Этот алгоритм находится в исходном коде PGP; здесь он не описан, поскольку выходит за рамки этой книги. .
Теперь пользователь, который хочет зашифровать и послать нам некоторую информацию, для шифрования может использовать Z и Е. Предположим, что кто-либо хочет послать нам число 688. Для этого он делает следующее вычисление:
688” mod 3337 = 1570
Мы получим число 1570 и расшифруем его следующим образом:■
15701"1’ mod 3337 = 688
Безопасность RSA зависит от следующих факторов:
Числа М и N должны храниться в секрете. Это очевидно: если Z и Е общеизвестны, а вы знаете еще и М и N, не очень сложно определить ключ шифрования D.
Z должно чрезвычайно сложно раскладываться на множители. Если Z будет раскладываться легко, то вы очень просто сможете извлечь из него М и N, и затем определить ключ шифрования.
Не должно быть других математических методов изъятия ключа с использованием Z и Е.
Использование Pretty Good Privacy
Pretty Good Privacy (PGP) — очень популярный метод шифрования, использующий криптографию как по частному ключу, так и по общему ключу. Последняя версия, PGP 5.x, для создания безопасной среды передачи сообщений выполняет следующие операции:
Глава 19
Создает случайный ключ сеанса для каждого сообщения.
Шифрует сообщение по ключу сеанса с помощью алгоритма шифрования по частному ключу IDEA.
Шифрует ключ сеанса по общему ключу получателя с помощью алгоритма общедоступного ключа RSA.
Отправляет по почте зашифрованные сообщение и ключ сеанса.
В табл. 19.2 представлено сравнение систем общедоступного и частного ключей.
Таблица 19.2. Сравнение методов шифрования по общему и частному ключам
Системы общедоступного ключа
Системы частного ключа
Могут сделать ключ шифрования общим
Только получатель владеет секретным ключом
Асимметричное шифрование
Может использоваться для электронной подписи
Перед переговорами нет необходимости в обмене ключом
Не могут сделать ключ шифрования общим
Секретный ключ должен быть как у получателя, так и у отправителя
Симметричное шифрование
Не может использоваться для электронной подписи
Перед переговорами необходимо обменяться ключом
Выбор простых чисел в PGP
Для генерирования простого числа, состоящего из п битов, PGP использует следующий алгоритм:
Сначала PGP генерирует случайное двоичное число, содержащее две единицы, за которыми следуют (п-2) случайных двоичных разрядов.
Для определения, является ли сгенерированное число простым, PGP использует очень эффективный алгоритм (называемый алгоритмом быстрого отсеивания).
Если алгоритм определит, что сгенерированное случайное число не является простым, PGP пробует следующее нечетное число.
Если алгоритм определит, что число может быть простым, PGP выводит точку (.) и использует малую теорему Ферма для точного определения, является ли это число простым. Как минимум 50% всех чисел отсеивается в каждом проходе алгоритма Ферма. При каждом успешном тесте PGP выводит звездочку (*).
Эта информация может использоваться для понимания вывода PGP, когда вы пытаетесь найти пару ключей. Предположим, что вы пытаетесь сгенерировать 512-разрядное простое число и получаете следующий вывод PGP:
Этот вывод указывает, что алгоритм PGP нашел 20 кандидатов в простые числа, которые были просеяны через решето алгоритма Ферма. Четыре звездочки указывают, что первое число, прошедшее первый тест Ферма, прошло все четыре теста. Следующий набор точек (.) указывает, что, прежде чем было найдено второе простое число, алгоритм Ферма отсеял 12 различных чисел.
В следующем фрагменте программного кода показана простая реализация алгоритма отсеивания, который может использоваться для проверки того, является ли определенное число г простым:
int primes[100];// Массив, используемый для хранения
// сгенерированных простых чисел.
int total_primes = 0;// 'total_primes' используется для хранения количества
// сгенерированных на данный момент простых чисел
check_for_prime(int г) {
int i ;
int p = sqrt((double)r) ;
for(i=0;i<total_primes;i++) {
if (r % primes [i] == 0) return 0; if (primes[i] > p ) return 1;
1 return 1;
Часть IV
Использование случайных чисел в криптографии
В целях повышения безопасности в криптографических алгоритмах широко используются случайные числа. Случайные числа в алгоритме PGP используются для определения:
Секретного ключа
Ключа сеанса, используемого для шифрования каждого сообщения
Ключа, используемого в оговоренных криптографических алгоритмах
Случайные числа могут генерироваться с помощью случайного природного процесса, такого как радиоактивный распад. Компьютеры оснащены функциями случайных чисел, генерирующих числа, случайные в том плане, что их нелегко предсказать. Хакер может знать внутреннее состояние компьютера и начальное число и взломать такие псевдослучайные функции. PGP генерирует случайное число, попросив пользователя ввести что-либо с клавиатуры. Он определяет время между нажатиями клавиш и создает случайное число. Как только генерирование случайного числа завершено, PGP просит пользователя прекратить ввод. Это случайное число может использоваться в качестве начального числа для генератора случайных чисел. Для каждого нового ключа генерируется новое начальное число. Генерирование начального числа с учетом скорости ввода с клавиатуры не совсем случайно; фактически в программе PGP 2.6 было выявлено упущение в генерировании начального числа. Это упущение препятствовало генерированию действительно случайного числа, и число становилось предсказуемым опытному хакеру. Это упущение было исправлено в программе PGP 2.6.1.
Шифрование файлов с помощью PGP
Простейшее использование программы PGP заключается в шифровании файлов и защиты информации на компьютере от хакеров. Имеется несколько причин для шифрования файлов на вашем компьютере:
Если вы совместно с другими используете компьютер и компьютер содержит частную информацию, которую вы хотите сохранить в тайне.
Если на своем компьютере вы храните конфиденциальную информацию, которую не хотите открыть общественности, чтобы она не была украдена или потеряна.
Если вы не хотите сделать информацию доступной, чтобы та не была продана или подарена, прежде чем вы уничтожите все данные.
Шифрование файла производится с помощью опции PGP -с. Например, чтобы зашифровать файл secret, необходимо ввести команду:
с:> рдр -с secret
Программа PGP отвечает на эту команду, запрашивая фразу пароля. Затем она шифрует файл и создает файл secret.pgp.
Теперь безопасность файла, защищенного PGP, зависит от фразы пароля и ее охраны. Для обеспечения секретности фразы можно принять следующие меры:
Не записывать фразу.
Не хранить фразу пароля в файле на компьютере.
Не выбирать фразу пароля, легко угадываемую другими.
Выбрать для пароля фразу, которую вы можете легко запомнить.
Следующие виды информации легко угадываются; вы не должны выбирать для пароля фразы, состоящие исключительно из таких элементов, как:
Ваше имя
Имя вашего супруга
Имя родителей
Ваше ласкательное имя
Имя вашего ребенка
Ваш любимый герой мультфильма
Название используемой вами операционной системы
Номер вашего телефона
Глава 19
Номер вашей кредитной карточкой
Номер чьей-либо социальной страховки
Чей-либо день рождения
Ваш любимый фильм
Ваше любимое место отдыха
Любой из предыдущих элементов в обратном направлении
■ Любой из предыдущих элементов, за которым следует или которому предшествует цифра
После того как файл зашифрован, следующим шагом должно быть удаление из системы исходного незашифрованного файла. Стандартные команды удаления во многих операционных системах на самом деле не уничтожают файл, а просто удаляют из каталога запись о файле. В результате файл можно восстановить. Опция -w используется в PGP для безопасного удаления. В процессе удаления, выполняемом в PGP, файл заполняется случайными данными. Использование случайных данных вместо нулей или единиц усложняет получение текста хакером. Впрочем, существует несколько случаев, когда простого удаления файла недостаточно. Если вы, например, используете накопитель для одноразовой записи, то в результате удаления старые секторы не будут уничтожены. Убедитесь также, что все резервные копии исходного текста (включая программные кэши, автоматические и пользовательские резервные копии) тоже уничтожены.
Файл, зашифрованный PGP, может быть расшифрован при запуске PGP с именем файла в качестве единственного параметра:
с:> pgp secret.pgp
Программа PGP читает файл, определяет шифрование и запрашивает ввод пароля. Когда пароль предоставлен, PGP расшифровывает файл и записывает расшифрованную информацию в отдельный файл.
По фразе пароля PGP с помощью хеш-функции генерирует 128-разрядный код. Для шифрования PGP использует алгоритм IDEA со 128-разрядным ключом.
В табл. 19.3 перечислены стандартные расширения файлов и дана их интерпретация.
Таблица 19.3. Расширения файлов, используемые PGP
Расширение файла
Использование
.txt
Текстовый файл, созданный с помощью текстового редактора или текстового процессора
■pgp
Зашифрованный файл; он является двоичным файлом PGP
.asc
ASCII-файл; файл, зашифрованный с использованием в PGP опции -а, имеет такое расширение.
.bin
Файл, созданный при использовании опции -kg (key generate). Этот файл хранит начальное число для генератора случайных чисел PGP.
Версии программы PGP для UNIX и DOS используют несколько переменных среды, приведенных в табл. 19.4.
Таблица 19.4. Переменные среды, используемые PGP
Переменные среды PGP
Использование
PGPPASS
Хранит пароль. Не рекомендуется использовать эту переменную, поскольку сохранение пароля в памяти — это, вероятно, самый легкий способ для хакера найти его.
PGPPASSFD
Задает файл, из которого должен быть прочитан пароль.
PGPPATH
Задает каталог, содержащий стандартные файлы PGP.
ТМР
Определяет каталог, в котором PGP будет хранить свои временные файлы. Будьте особо внимательны, чтобы другие не смогли получить доступ к этому каталогу, иначе вы дадите возможность взломать пароль.
TZ
Указывает временную зону, в которой вы находитесь.
В операционной системе DOS можно установить переменные среды с помощью команды SET: с:> SET PGPPATH=c: \pgptemp
Часть IV
В системах UNIX способ установки переменных определяется используемым командным интерпретатором. В командных интерпретаторах Borne и Korn установка переменных производится следующим образом:
$ PGPPATH=/usr/temp; export PGPPATH
В командном интерпретаторе С установка переменных среды выполняется так:
% setenv PGPPATH /usr/temp
Ограничения в криптографии
Необходимо помнить об ограничениях в использовании криптографии для шифрования ваших сообщений:
Незашифрованная информация не может быть защищена. Вы можете иметь доступ к лучшим методам шифрования, но если данные остаются на вашем компьютере незашифрованными, они будут под угрозой. Хакер, который каким-либо образом получит доступ к вашему компьютеру, сможет получить информацию. Даже если вы удаляете информацию, необходимо знать, что существуют утилиты для восстановления и получения неправильно удаленных данных.
Ключ шифрования должен быть защищен. Ключи шифрования являются разгадкой для расшифровки зашифрованного текста. Если ключи должным образом не защищены, хакеры смогут получить эти ключи, и вся цель шифрования станет ненужной.
Защитить данные от разрушительных атак. Если главная цель хакера состоит не в получении ключей шифрования, а в том, чтобы воспрепятствовать вам в просмотре файлов, хакер может удалить зашифрованный файл.
Остерегаться программ шифрования с нежелательными скрытыми возможностями. Если только вы самостоятельно не напишете программу шифрования, нет способа выяснить полностью, какие функции может выполнять программа шифрования, кроме своей непосредственной функции шифрования информации. Например, она может размещать секретный ключ в заголовке каждого шифруемого файла, а также отправлять хакеру по почте зашифрованный файл.
Остерегайтесь предателей. Вы должны убедиться, что людям, вовлеченным в процесс распространения ключа, можно полностью доверять.
Эффективность метода шифрования определяется его защищенностью от атак. Хорошие криптографические методы — это методы, стойкие к атакам грубой силы над алгоритмами ключей. Другими словами, хороший криптографический метод предполагает использование очень длинных ключей и сложных алгоритмов, которые чрезвычайно сложны для взлома. Даже при проведении атаки грубой силы на взлом ключа уйдут миллионы лет. Фиктивная криптография, напротив, означает использование достаточно простых методов. В фиктивной криптографии используются небольшие ключи или алгоритмы, легкие для взлома. Crypt является популярной утилитой для UNIX, относящейся к категории фиктивной криптографии. Другим широко используемым техническим приемом в фиктивной криптографии является использование пароля, сохраненного в файле; файл не откроется до тех пор, пока пользователь не введет определенный пароль.
Не всегда легко отличить хороший алгоритм от фиктивного. Опытный хакер может обычно найти изъяны в фиктивных методах за короткое время. До недавних пор использование шифрования было важно для правительства и главных корпораций, но в текущее время использование персональных компьютеров, Internet, систем электронной продажи, электронной почты настолько распространены, что методы шифрования стали обычными в повседневном использовании. Табл. 19.5 поможет быстро определить, является алгоритм хорошим или фиктивным.
Таблица 19.5. Сравнение строгих и фиктивных алгоритмов
Строгий алгоритм
Фиктивный алгоритм
Использует длинные ключи и сложный алгоритм
Сложен для взлома
Запатентован
Основан на схемах шифрования, которые опубликованы и проанализированы, а также всецело протестирован экспертами
Пример: DES и IDEA
Компании, предоставляющие эти алгоритмы, не уклоняются от объяснения, какие схемы они используют
Использует короткие ключи и простой алгоритм
Прост для взлома
Доступен бесплатно
Основан не неизвестных схемах шифрования
Пример: CRYPT
Компании, предоставляющие эти алгоритмы, не особо охотно объясняют свои методы шифрования
Глава 19
В системе шифрования слабейшим звеном является человек, хранящий ключ шифрования. Он должен принять все меры защиты, чтобы ключ не попал в плохие руки. Использование слабого или фиктивного алгоритма подобно использованию дешевого замка для картотеки. Это зависит от важности данных и от выгоды, которую кто-либо может получить, заполучив вашу информацию. Если вы хотите защитить данные от людей, не имеющих больших познаний в технике, или от людей, которых совершенно не интересует ваша информация, может подойти и слабый алгоритм. В других случаях вы будете использовать один из строгих алгоритмов, который сможете себе позволить.
Юридические ограничения на криптографию
На использование криптографии в США правительство этой страны наложило два главных ограничения:
U.S. Patents. U.S. Patent и Trademark Office все больше и больше критикуются за предоставление патентов на продукты, которые явно устарели. Все патенты криптографии по общему ключу теперь закреплены исключительно за Public Key Partners (РКР). Взыскание за нарушение патента находится под юрисдикцией гражданского суда.
Много программ, таких как Lotus Notes, и другие программы для Microsoft Windows поставляются с лицензией на патенты шифрования по общему ключу RSA и Stanford. Алгоритм RSA был опубликован прежде, чем его изобретатели зарегистрировали патент; в результате Япония и страны Европы могут использовать большинство видов шифрования, не беспокоясь за условия патентов на RSA или РКР.
Управление экспортом. Жители США не должны думать о передаче копии PGP своим друзьям, живущим за пределами США. Это может привести к высокому штрафу, тюремному заключению или к тому и другому вместе. Экспортом криптографических материалов управляет Defense Trade Regulations (формально известная как International Traffic in Arms Regulation или ITAR).
Программа, реализующая шифрование, может экспортироваться только после получения лицензии в Defense Trade Controls (DTC). Перед предоставлением такой лицензии DTC вместе с National Security Agency (NSA) будут оценивать программу. Частью этой оценки будет определение схемы шифрования. В целом при слабой реализации шифрования экспорт позволяется; в ином случае запрещается. В 1992 г. между State Department и Software Publishers Accotiations (SPA) было достигнуто соглашение о разрешении экспорта программ, реализующих алгоритмы RSA Data Security — RC2 и RC4 — с размером ключа в 40 или менее битов. Канада — это особый случай в отношении прав на экспорт криптографических материалов. Канада ведет либеральную политику и позволяет экспортировать без лицензирования любое криптографическое программное обеспечение, сделанное в Канаде. Текущая политика США позволяет экспортировать любое программное обеспечение в Канаду без лицензирования. Впрочем, согласно одному из канадских постановлений, программное обеспечение не может в дальнейшем экспортироваться в третью страну.
Из-за таких жестких ограничений политики США большинство компаний разрабатывают свое программное обеспечение за границей, а затем экспортируют его в США. Альтернативой получению необходимой секретности и избежания трудностей получения лицензии является программа PGP версии 2.6, которая свободно доступна в США, для связи с PGP 2.6ui, свободно доступной за пределами США.
Криптографические атаки
Хакер может без вашего разрешения использовать несколько методов для взлома кода или расшифровки сообщения, зашифрованного вами. Рассмотрим некоторые из этих методов.
Атака грубой силы
Атака грубой силы называется также атакой поиска ключа, поскольку пробует каждый возможный ключ до тех пор, пока код не будет взломан. Этот метод подразумевает, что хакер знает, как определить успешность поиска ключа. Стратегия проста: применять ключи поочередно до тех пор, пока не будет получен доступ.
Метод грубой силы неэффективен и непрактичен в связи с большим количеством возможных ключей. Если вы, например, рассматриваете алгоритм, использующий 64-разрядные ключи, количество возможных ключей равно 2м. Даже если вы используете компьютер, перебирающий миллиард ключей в секунду, ему на взлом кода придется затратить значительное время.
Часть IV
Криптоанализ
Важность криптоанализа может быть понята с осознанием того факта, что длина ключа является не единственным фактором силы метода шифрования. Вы также должны осознавать, что знание ключа — не единственный путь к взлому кода. Опытный хакер может использовать математический и компьютерный опыт для взлома большинства алгоритмов, объявленных невзламываемыми. Существует две вероятные цели для атаки:
Для расшифровки зашифрованного текста и поиска обычного текста
Для поиска ключа шифрования
Большинство основных типов криптоаналитических атак полагаются на знание языка обычного текста. Другими словами, следующая информация поможет атакующему легко взломать код:
Частота букв. Общее знание языка, используемого в исходном сообщении, позволит атакующему в процессе расшифровки сделать некоторые угадывания. Например, знание букв, широко используемых в языке (в английском наиболее часто используются буквы а, е, s, о и г), и букв, используемых редко (таких, как x,q и г), могут помочь хакеру.
Родство букв. Некоторые буквы часто идут вслед за какой-либо другой (например, сочетания io и ing), тогда как другие очень редко идут за другими (сочетания tp и /». Это тоже может помочь в отгадывании букв.
Длина слов. В английском языке однобуквенными словами обычно являются / или А. Существует также ограниченное количество двухбуквенных слов.
Контекст сообщения. Может помочь атакующему узнать контекст сообщения по информации об отправителе, получателе, а также общее содержание сообщения и его вид.
Довольно широко используются следующие атаки:
Атака для поиска ключа шифрования, предпринимаемая после того, как известен прямой текст сообщения. Этот тип атаки довольно обычен при взломе зашифрованной электронной почты или защищенного жесткого диска. Электронная почта имеет стандартный заголовок, который хакер может использовать в качестве основы для расшифровки. Жесткие диски используются для сохранения информации в определенных позициях. Другими словами, хакер может использовать обычные знания для определения остатка зашифрованной информации.
Атака для поиска ключа шифрования по предопределенному прямому тексту. При таком подходе хакер может заставить жертву бессознательно зашифровать определенную информацию, а затем для получения ключа поработать над зашифрованным текстом, созданным из известных данных. Хакер сможет использовать ключ для расшифровки всех сообщений, созданных с помощью этого ключа шифрования.
Дифференциальный криптоанализ. Эта атака дает возможность сравнить результаты шифрования нескольких сообщений, очень похожих между собой, за исключением некоторых второстепенных деталей.
Взлом файла, зашифрованного программой PGP
Для взлома файла, зашифрованного PGP, хакеры могут применить несколько стратегий. Некоторые из них основаны на факте использования в программе PGP алгоритма IDEA:.
Атаки грубой силы против ключа IDEA. Алгоритм IDEA, применяемый в PGP, использует 128-раз- рядные ключи, что приводит к возможности существования нескольких миллиардов ключей (2128 ключей). Хакер может попробовать все ключи поочередно (такой подход займет очень много времени на угадывание ключа). Очевидно, этот подход не идеален для взлома кода.
Атаки грубой силы против фразы пароля. Вместо того чтобы определять ключ шифрования, хакер может атаковать фразу пароля, выбранную для шифрования файла. Выбранная длина фразы пароля очень важна в оценке ее безопасности. Автор рекомендует использовать как минимум 12 символов во фразе пароля и смешивать в ней буквы нижнего и верхнего регистров, цифры и специальные символы.
Поиск на жестком диске незашифрованной копии зашифрованного файла. Защититься от этого можно, уничтожив исходный текст с помощью PGP.
Установка мин-ловушек. Опытный хакер может попытаться "заминировать" PGP, так чтобы программа стала размещать копию каждого ключа или незашифрованный файл и скрытом каталоге, шиф-
Глава 19
ровала файлы по фразе пароля, отличной от выбранной, или не шифровала файлы вообще, а просто создавала такое впечатление, что файлы зашифрованы.
Похищение или угрозы. Если информация действительно крайне необходима, хакер может даже прибегнуть к похищению или угрозам. К такому виду атаки нужно действительно относиться с осторожностью.
Цифровые подписи
Мы видели, что алгоритмы шифрования по общему ключу используют одностороннюю функцию, которая позволяет получить зашифрованные данные, передав их через функцию, использующую общий ключ. Исходный текст можно получить, "прогнав" зашифрованные данные через функцию, использующую частный ключ. Весь процесс работает и в обратном направлении: можно "прогнать" исходный текст через функцию, использующую частный ключ и создать зашифрованный текст; расшифровать этот текст мы сможем, "прогнав" его через функцию, использующую общий ключ.
Цифровая подпись — это мощная возможность PGP, позволяющая устанавливать подлинность сообщений. Бывают ситуации, когда не нужно шифровать сообщение, но необходимо предохранить сообщение от изменения его кем-либо. Другими словами, вы хотите убедить других, что именно вы являетесь автором сообщения.
Должно быть понятно, что, хотя данные, зашифрованные по общему ключу, не могут быть интерпретированы кем-либо, не имеющим корректного ключа, цифровые (их еще называют электронными) подписи вносят данные в сообщение без всякого шифрования самого сообщения и без какого бы то ни было изменения сообщения. Другими словами, вы можете читать сообщение, даже не проверив цифровую подпись. Единственный способ убедиться, что цифровая подпись действительно утверждает корректность сопровождаемого сообщения, заключается в использовании подходящего программного обеспечения и выполнении процесса проверки сообщения по общему ключу отправителя.
Программа PGP создает цифровую подпись, обрабатывая сообщение с помощью функции сбора сообщения для генерирования 128-разрядного числа. Функция сбора сообщения в целом является математической функцией, преобразующей всю информацию в файле в одно большое число. После подписания сообщения с помощью частного ключа создается блок подписи PGP. Этот блок подписи размещается в конце сообщения. При получении "электронно" подписанного сообщения PGP проверяет подпись, извлекая сообщение с помощью той же функции сбора, которая применялась для исходного сообщения. Затем общий ключ отправителя используется для расшифровки блока подписи. Далее сравниваются два блока сбора сообщения, и, если они совпадают, значит, сообщение не было изменено со времени помещения в него цифровой подписи.
Единственная проблема электронных подписей в том, что они могут сообщить только о том, было ли сообщение изменено. Они не могут сообщить о природе изменения или о количестве измененных данных.
В текущее время для цифровых подписей используются два популярных стандарта: Public Key Cryptography Standard (PKCS) и Digital Signature Standard (DSS). Эти стандарты описаны в следующих разделах.
Public Key Cryptography Standard (PKCS)
Это стандарт цифровых подписей, в котором используется тот же алгоритм шифрования по общему ключу, что и в методе шифрования RSA. Этот стандарт предоставляет набор стандартов .для набора функций и набора типов данных. Он стандартизирует метод обмена общими ключами и метод форматирования зашифрованных данных. Стандарт цифровой подписи RSA является частью PKCS #1 и определяет способ представления зашифрованных данных. Для генерирования цифровой подписи используются следующие операции:
К подписываемым данным применяется криптографическая хеш-функция.
Для шифрования результатов криптографической хеш-функции используется алгоритм шифрования по общему ключу RSA.
В цифровую подпись помещаются результат хеш-функции и тип криптографического смешивания, используемого в формате PKCS.
Digital Signature Standard (DSS)
В 1991 г. организация NIS при сотрудничестве с NSA предложила алгоритм цифровой подписи Digital Signature Algorithm для использования с Digital Signature Standard (DSS). Эта заявка была немедленно подвергнута критике в связи с тем фактом, что в этом процессе принимало участие NSA.
Часть IV
NSA выступает против широкого использования строгой криптографии, поскольку строгие алгоритмы серьезно влияют на функционирование этой организации. По сравнению с RSA, DSA имеет несколько недостатков:
DSA сложнее, чем RSA
Общедоступные ключи DSA должны выбираться с большей, чем в RSA, осторожностью.
DSA медленнее, чем RSA, поскольку включает значительную обработку как со стороны лица, подписывающего сообщение, так и со стороны лица, проверяющего подпись.
Общедоступные ключи DSA содержит четыре следующих значения:
Простое число р, длина которого больше 512 битов, но меньше 1024 битов.
160-разрядное простое число q, являющееся сомножителем (р-1).
Значение V, такое, что V = 1 mod р. Заметьте, что v не может равняться 1.
Значение п, вычисляемое с помощью частного ключа К, такое, что gk = 1 moa р и К < q.
Из всех этих значений p,q и v можно сделать общими; п не может быть общедоступно, поскольку оно основано на секретном ключе К.
Реальный процесс подписи и проверки сообщения с помощью DSA намного сложнее и включает генерирование новой пары ключей для каждого сообщения.
Неотрицание
Неотрицание — это понятие, означающее качество цифровой подписи. Оно предоставляет получателю гарантию того, что цифровая подпись подлинна при условии, что отправитель защитил частный ключ и что алгоритм цифровой подписи надежен. Предположим, что Джон заказал 1000 акций компании XYZ, послав электронно подписанное сообщение своему брокеру как раз перед закрытием биржи. На следующий день компания объявляет, что потерпела значительные убытки за текущий квартал и ее фонд падает на 20%. Джон не может доказать, что он послал заказ вчера, поскольку он является единственным человеком, хранящим частный ключ.
Коммерческие криптографические продукты
Для коммерческих продуктов используются разнообразные технологии шифрования. В следующих разделах этой главы исследуются различные категории таких продуктов.
Безопасные Web-клиенты
Уже довольно долгое время двумя популярными Web-клиентами, или броузерами, на рынке являются Microsoft Internet Explorer и Netscape Communicator/Navigator. Криптографическая безопасность стала важным компонентом Web-броузеров с того времени, как компания Netscape использовала технологию Secure Socket Layer (SSL) в своей первой версии Navigator в 1994 г. SLL предоставляет безопасные зашифрованные каналы связи между броузером и сервером. В начале 1997 г. Netscape включила поддержку S/MIME (Secure MIME), позволяющую пользователю генерировать свои собственные ключевые пары, получать сертификаты ключевой пары от службы сертификации и хранить эти сертификаты в броузере. Этот ключ затем может использоваться для цифровой подписи почты или шифрования данных по общему ключу. В сентябре 1997 г. Microsoft включила поддержку S/MIME в Internet Explorer 4.0 и почтовых клиентах Outlook и Outlook Express. S/MIME требует тройного DES-шифрования, что позволяет создать довольно безопасную среду. Эра Internet выдвинула на первый план и другие возможности Web-клиентов, включая следующие:
Проверку пользователей на обоих концах канала связи Web
Обработку cookies (заданной пользователем информации, хранимой в броузере)
Безопасность загружаемого программного обеспечения, включая элементы управления ActiveX и Java- аплеты
Microsoft Internet Explorer
Microsoft Internet Explorer, вероятно, является одной из лучших программ из всего бесплатного программного обеспечения. Его можно получить с Web-узла Microsoft www.microsoft.com. В нем реализованы следующие возможности по безопасности:
Глава 19
Защита содержимого, позволяющая пользователям устанавливать ограничения на материалы, получаемые из Internet. Неуместные материалы могут быть закрыты для доступа детей.
Средства управления сертификатами, которые могут использоваться для отслеживания сертификатов пользователя и Web-узла.
Возможность доступа к секретной информации на просматриваемой Web-странице.
Опции, позволяющие определить, как Web-броузер будет взаимодействовать с данными, поступающими из разнообразных источников.
Средства управления сертификатами доступны также в почтовых клиентах Outlook и Outlook Express.
Netscape Communicator/Navigator
В начале 1998 г. компания Netscape неожиданно сделала бесплатным для пользователей свой стандартный клиент, а в целях улучшения и добавления новых компонентов — доступным другим разработчикам его исходный программный код. Бесплатное программное обеспечение клиента можно получить по адресу: www.nctscape.com. В этом Web-броузере реализованы следующие возможности по безопасности:
Возможность доступа к секретной информации на просматриваемой Web-странице
Защита сертификатов броузера
Опции для настройки шифрования и другие установки для каналов SSL
Установка системы безопасности электронной почты, включая возможность делать сообщение зашифрованным и/или подписанным, возможности выбора сертификата для использования в электронных подписях и установки опций шифрования S/MIME
Java- и JavaScript-аплеты
Просмотр и управление криптографическими модулями, используемыми броузером
Функции управления сертификатами для управления сертификатами пользователя и сайта
Безопасные почтовые клиенты
Почтовый клиент может считаться безопасным, если решает следующие задачи:
Шифрование сообщений и их вложений
Выполнение цифровой подписи сообщений и их вложений
Расшифровка зашифрованных сообщений по частному ключу
Проверка сообщений с помощью общего ключа отправителя
Эти функции требуют, чтобы почтовый клиент мог управлять общими и частными ключами отправителя, а также общими ключами, относящимися к получателю сообщения электронной почты. Существует несколько коммерческих почтовых клиентов, но самыми популярными являются описанные ниже.
Netscape Messenger
Почтовая программа Netscape Messenger, поставляемая с Communicator, является полнофункциональным почтовым и news-клиентом. Communicator — это набор средств связи, среди которых: электронная почта, Web-броузер, средства для просмотра дискуссионных групп, для конференций с другими компонентами, для ведения календаря и эмуляции терминала. Пакет доступен для разнообразных платформ, включая Windows 3.x, Windows 95, Windows NT и др. Он поддерживает протоколы SNMP и POP и может посылать и получать защищенную электронную почту S/MIME.
Microsoft Outlook и Outlook Express
Компания Microsoft предоставляет Outlook 97 и Outlook Express — пару почтовых клиентов, поставляемых с Internet Explorer, которые доступны для Windows 95 и Windows NT. Outlook и Outlook Express поддерживают протоколы SNMP и POP и способны посылать и получать электронную почту, засекреченную S/MIME. Outlook 97 предоставляет несколько расширенных возможностей, а также функций для работы сетевых пользователей, включая расписание заданий и возможности управления задачами.
Qualcomm Eudora и Eudora Light
Программа Eudora является пионером в области почтовых клиентов. Первый продукт, поддерживающий протокол Post Office Protocol (POP), появился в 1990 г. Сейчас Eudora куплена компанией Qualcomm, Inc.,
и бесплатная версия Eudora Light доступна на Web-узле по адресу: www.eudora.com/eudoralight. Клиенты Eudora доступны для Windows 3.x, Windows 95, Windows NT, Macintosh OS и Macintosh Newton. В 1997 г. Qualcomm начала поставку Eudora с полнофункциональной включаемой версией PGP Personal Privacy 5.0, позволяющей отправлять и получать почту, зашифрованную с помощью программы PGP.
Продукты для защиты рабочего стола
Главный фокус безопасности рабочего стола заключается в защите информации, хранимой на рабочем столе. Эти продукты позволяют сохранять информацию так, чтобы она была доступна только человеку, владеющему ею, а также другим людям, получившим разрешение владельца. Вот некоторые наиболее общие возможности продуктов для защиты рабочего стола:
ж Шифрование и расшифровка файлов. Выбранные файлы могут шифроваться или расшифровываться на месте, позволяя только зарегистрированным пользователям получать доступ к файлам после предоставлю ниш пароля для расшифровки фразы. Программный продукт может требовать верификации пользователя во время загрузки системы или при каждой попытке доступа к файлу.
и Шифрование и расшифровка каталогов. Некоторые программы позволяют задавать определенные каталоги зашифрованными, так что любой файл, созданный или перемещенный в этот каталог, авто- мстичсски шифруется и может быть доступен только зарегистрированному пользователю.
и Проверка пароля во время загрузки. Некоторые программы позволяют защищать данные, требуя ввода пароля прежде, чем успешно завершится загрузка.
и Блокировка системы. Некоторые программы блокируют систему в целях защиты от несанкционированного использования. Такая системная блокировка реализуется блокированием клавиатуры или экранной заставкой, защищенной паролем.
в Безопасное удаление. Файлы и каталоги, удаляемые в среде DOS и Windows, на самом деле не уничтожаются, просто удаляются их записи в каталогах. Различные коммерческие программы позволяют воссоздавать удаленную информацию. Это может быть хорошо, когда вы действительно хотите восстановить потерянные данные, но может стать проблемой, когда вы хотите уничтожить конфиден- писыьную информацию. Некоторые продукты позволяют удалять данные таким образом, чтобы не юлько удалялись записи в каталогах, но и биты каждого байта устанавливались в случайный набор нуле!'» и единиц.
и Желательна поддержка электронной подписи и криптографии по общему ключу.
я Доступ к данным лиц-хозяев. Некоторые программы сохраняют данные таким образом, что некоторое особое лицо по паролю и идентификатору может расшифровать данные, зашифрованные любым пользов-делем программы в отдельной системе или в нескольких системах.
RSA SecurPC v2.0
Компания RSA Data Security, Inc. обладает некоторыми наиболее важными реализациями криптографических алгоритмов, используемыми в текущее время. Она разработала продукт шифрования персонального рабочего стола SecurPC, использующий криптографию по общем ключу. Хотя этот продукт и не использует цифровые подписи, он имеет следующие важные возможности:
я Шифрования файлов и каталогов по секретному ключу
Шифрования файла и сохранения его в виде исполняемого модуля, который может быть открыт только при вводе пароля после запуска программы
Атематического шифрования каталогов
Идентификации пользователя при загрузке системы
Безопасного удаления
Доступа к данным
Инт-'^щии с Windows 95
Защиты системы по "горячим" клавишам
Pretty Good Privacy (PGP) for Personal Privacy 5.0
Программа PGP предоставляет несколько важных возможностей в версии Personal Privacy 5.0:
t-i Шифрования и/или цифровой подписи данных, сообщений электронной почты и файлов
Глава 19
Расшифровки и/или проверки цифровой подписи данных, почтовых сообщений и файлов
Поддержки пользователей Windows
Поддержки пользователей Macintosh
Использования в приложениях PGPtray (PGPtray позволяет пользователям выполнять криптографические функции над данными, находящимися в буфере обмена; PGPtray может использоваться для предотвращения записи на жесткий диск незашифрованных данных и для предотвращения расшифровки и проверки данных в буфере обмена)
В PGP for Personal Privacy не реализованы несколько важных возможностей:
Блокировка системы
Автоматическое шифрование каталогов
Безопасное удаление
Symantec Norton Your Eyes Only
Программа Symantec Norton Your Eyes Only доступна для Windows 95 и предоставляет шифрование персонального рабочего стола. Вы можете использовать административную надстройку, позволяющую осуществлять управление сертификатами. Эта программа использует шифрование по общему ключу и цифровые подписи при условии, что все участники используют этого продукт. Некоторые из ключевых возможностей этого продукта перечислены ниже:
Шифрование файлов и каталогов
Необязательное использование цифровой подписи
Автоматическое шифрование каталогов
Безопасное удаление
Возможности блокировки экрана
Возможности блокировки загрузки
Выбор различных строгих алгоритмов шифрования
NSA Clipper Chip
16 апреля 1993 г. Белый Дом открыл сверхсекретный алгоритм Clipper Chip, разработанный NSA. Планировалось создать этот алгоритм как общий стандарт и заменить им алгоритм DES. Clipper использует секретный алгоритм Skipjack и может применяться для шифрования голосового сообщения по телефонам и факс-машинам. Данные и электронная почта обрабатываются картой PCMCIA, называемой Fortezza, которая имеется в большинстве портативных компьютеров. Skipjack — это 80-разрядный алгоритм шифрования, который считается чрезвычайно безопасным. Единственная проблема этого алгоритма в том, что у правительства США есть ключи для него. Следовательно, оно может взломать любое сообщение, зашифрованное Clipper.При этом Clipper использует два уникальных кода: серийный номер и главный ключ шифрования. Ключи являются защищаемые искажением и уничтожаются при попытке выявления кода.
Для обмена ключей Clipper использует алгоритм обмена ключом Диффи-Хельмана. Всякий раз, когда вы хотите зашифровать сообщение, Clipper берет копию ключа сеанса (используется для расшифровки сообщения) и шифрует его по главному ключу шифрования. Вместе с зашифрованным сообщением он посылает зашифрованный ключ сеанса и серийный номер.
У правительства есть план, называемый Escrowed Encryption Standard (EES), целью которого является создание двух баз данных, в каждой из которых будет храниться серийный номер каждого Clipper и половина главного ключа шифрования. Каждая половина ключа сохраняется различными "escrow key" — в частности, одна половина сохраняется Национальным институтом стандартов и технологии (NIST), а вторая — организацией Automated Systems Divisions of the Department of the Treasury.
Код Clipper очень безопасен.Когда правоохранительные агентства хотят подслушать разговоры, зашифрованные Clipper, агентство обращается к каждому из escrow-агентов.Если запрос одобрен, escrow-агенты посылают свои соответственные ключи в "черный ящик", который принимает зашифрованный ввод и выдает расшифрованное сообщение. Ключи имеют дату истечения их срока работы, после которой "черный ящик" больше не может принимать их для расшифровки.
В1994 г. Мэтью Блейз (Mattew Blaze) из AT&T Labs открыл ошибку в "обратных дверях" Clipper, которая была названа Law Enforcement Access Field (LEAF). С помощью LEAF правоохранительные агентства
Часть IV
могли получить ключи Clipper, которые могли использоваться для чтения зашифрованных данных. LEAF защищен 16-разрядной контрольной суммой, и Блейз понял, что, если вы можете повредить ключ, правительственные агентства не смогут расшифровать зашифрованные данные.
Резюме
Криптография и шифрование долгое время были сферой правительственных организаций и большого бизнеса. Эра использования компьютеров, Internet и криптографии по общему ключу дала людям возможность защищать свои данные с помощью различных методов шифрования. В этой главе были рассмотрены стратегии, используемые алгоритмами шифрования, а также наиболее широко используемые методы, с помощью которых опытный хакер может взломать код и расшифровать сообщение. Некоторые коммерческие продукты (такие, как PGP for Personal Privacy и Norton Your Eyes Only) могут использоваться для шифрования сообщений. Различные виды безопасности предоставлены в Internet-броузерах, таких как Netscape Navigator/Communicator и Microsoft Internet Explorer.
Здесь были проанализированы широко используемые способы, среди которых подстановка, перестановка, исключающее ИЛИ и другие криптографические функции. Не имеет значения, какой из способов вы используете, — запомните, что отъявленный хакер всегда сможет расшифровать сообщение. Вы должны принять необходимые меры предосторожности, чтобы защитить данные. Эти меры разнообразны — от правильного выбора пароля до физической защиты компьютера.
VЧАСТЬVЧАСТЬРаспределенные вычисления
V
ЧАСТЬ
V
ЧАСТЬ
CORBA
В ЭТОЙ ГЛАВЕ
Теория и обоснование
IDL: соглашение связывания
Брокер объектных запросов
Сравнение сред CORBA
Создание C++ Client
Создание C++ Server
Java Client
Стратегии тестирования
Служба имен и способность к взаимодействию
Производительность
Глава 20
При традиционном подходе к сетевому программированию разработчик ответствен за структуру сообщений, процедуру восстановления системы при возникновении ошибок и за процесс управления сервером. В этот перечень не включено масштабирование и механизмы восстанопления в случае отказов. Внесение изменений в программное обеспечение, связанных с платформами и языками программирования, может потребовать больших ресурсов не только на разработку, но и финансовых. Более 800 организаций, занимающих ведущие места в сфере разработки ПО, пришли к необходимости образования группы Object Management Group (Группа объектного управления) (http://www.omg.org) для определения инфраструктуры объектного программирования.
Самым значительным достижением в процессе работы 0MG было создание спецификации Common Object Request Broker Architecture (CORBA — Архитектура брокеров объектных запросов). CORBA определяет объектную шину, допуская интеграцию и управление объектами, определенными в языках C++, Java, Smalltalk, а также большинством других объектно-ориентированных языков. Главное, что среды, поддерживаемые этой архитектурой, обеспечивают устойчивую работу сетевой модели программирования.
В этой главе рассматривается разработка в среде CORBA, которая непрерывно совершенствуется. Для программиста, работающего на языке C++, CORBA важна тем, что позволяет объектам функционировать распределенным способом. Обращения, производимые к локальному объекту C++, перенаправляются удаленному объекту. Механизм обращения, в сущности, прост, а программный код при этом остается ясным и простым. Примеры программ в этой главе иллюстрируют эти возможности.
Разработчики CORBA могут выступать в нескольких ролях:
Разработчик клиентов. Работает на уровне интерфейса API, выполняя методические обращения к локальному объекту. Эти объекты относятся к классу прокси-объектов, взаимодействующих с объектом сервера при выполнении необходимых функциональных задач.
Разработчик серверов. Во многом подобен разработчику классов C++. Он создает интерфейс для объекта и выполняет его правильную упаковку.
Системный архитектор. Архитектор CORBA — это разработчик, создающий каркас, в котором находятся и взаимодействуют объекты.
Рассмотрим некоторые общие термины CORBA, используемые в этой главе:
Заглушка. Прокси-код, сгенерированный компилятором IDL, который посылает запросы ORB. Именно класс C++ вызывает их появление так, как будто методы выполняются локально.
Скелет. Классы сервера, сгенерированные компилятором IDL. Скелет расширяется через наследование, обеспечивая функциональные возможности, которые необходимы объекту сервера.
BOA (Basic Object Adapter — Базовый объектный адаптер). Интерфейс, который используется объектом сервера для связи с ORB.
ORB (Object Request Broker — Брокер объектных запросов). Псевдообъект, необходимый для реализации постоянного доступа как в среде выполнения клиента, так и в среде выполнения сервера. ORB — это типичное серверное приложение, опрашивающее определенный порт, ожидающее доступа в процессе соединения канала TCP/IP. Клиентские приложения соединяются с ORB, посылают сообщения о запросах и получают ответные сообщения от ORB.
Запрос. Метод обращения от клиентской заглушки к объекту сервера, причем запрос представляет собой пакет параметров метода и возвращаемых значений.
Объект. Экземпляр объекта, определенного интерфейсом; постоянно находится под контролем ORB.
IDL (Interface Definition Language — Язык определения интерфейса). Модифицированное подмножество типов C++ и определений класса. IDL обычно используется с той же целью, что и служебные классы C++.
Упорядочение. Процесс изменения данных от формата, который зависит от языка, до формата, не зависящего от языка.
Служба. Интерфейс, определенный спецификацией. Представляет собой эквивалент API операционной системы, обеспечивая доступ к специфической системной службе CORBA.
Теория и обоснование
Уместно представлять CORBA в качестве интегрированного комплекта инструментальных средств, который облегчает работу со сложными элементами сетевого программирования. Программистам C++ нет
Часть V
никаких оснований опасаться технологии CORBA — вы уже имеете практические навыки для эффективного ее использования.
Для разработок в среде CORBA не требуется никаких специальных знаний. У вас уже есть навыки работы с компиляторами, операционными системами и приложениями сервера Internet. Здесь будет уместно применить знания по ООП и созданию шаблонов для понимания и использования CORBA.
В следующих разделах этой главы рассматривается подробно сама среда CORBA, а также ее роль в программировании на языке C++.
Минимальная среда CORBA
Для реализации методов, используемых с удаленным объектом, необходимы следующие элементы:
ORB
Серверное приложение
Клиентское приложение
ПРИМЕЧАНИЕ
Многие поставщики ORB используют метод _bind () для присоединения клиентского объекта к объекту сервера. Метод _bind () представляет собой самостоятельный метод, который не обеспечивает взаимодействия между сетями различных поставщиков ORB. : + " +
Возможности взаимодействия ORB рассматриваются в разделе "Служба имен и способность к взаимодействию" далее в этой главе.
РИСУНОК 20.1. Связь между клиентом и сервером CORBA, а также ORB.РИСУНОК 20.1. Связь между клиентом и сервером CORBA, а также ORB.Среды CORBA значительно отличаются в зависимости от того, каким образом внедряется ORB конкретным поставщиком. Как клиент, так и сервер могут иметь доступ к своим собственным интерфейсам ORB.
РИСУНОК 20.1. Связь между клиентом и сервером CORBA, а также ORB.
РИСУНОК 20.1. Связь между клиентом и сервером CORBA, а также ORB.
И уже от поставщика зависит, представляют ли эти интерфейсы несколько ORB или же это библиотеки, компилируемые в каждый исполняемый модуль. Важно лишь то, что интерфейс ORB доступен для среды. На рис. 20.1 показано взаимодействие клиента, сервера и ORB.
Для взаимодействия в CORBA клиент и сервер соединяются друг с другом с помощью среды CORBA. Конкретное наполнение среды существенно зависит от поставщика, но следующие три операции являются необходимыми:
Объект сервера самостоятельно регистрируется в среде CORBA.
Клиент запрашивает среду CORBA о связи объекта сервера с клиентским объектом.
Клиент начинает выполнять вызовы метода для клиентского объекта; эти вызовы направляются объекту сервера.
Каркас объектной технологии
Полное понимание ООП и проектирования чрезвычайно важно для разработчика высокой квалификации. Поскольку OMG сформулировала определение каркаса, то все разработчики CORBA используют в своей работе одни и те же программные коды и стандарты. Многие организации используют CORBA в своих промышленных разработках. Дело в том, что каркас интерфейсов и служб определен спецификацией CORBA. Большим удобством для разработчика также оказывается наличие полной распределенной вычислительной архитектуры. Разработчик имеет возможность многократно использовать заранее определенные интерфейсы для создания новых компонентов.
Технология CORBA позволяет разработчикам создавать объектную технологию в виде более сложного пакета, чем это было возможно ранее. В CORBA определены объектные интерфейсы, которые отсутствуют в сетях и операционных системах. Поэтому устраняются неуклюжие "перемычки" для объектов, которые обычно необходимы для интегрирования объектов с процедурными интерфейсами.
Стремление применять объектную модель наиболее распространено при работе с языками, операционными системами и БД. Объекты были заново приспособлены к языкам программирования Visual Basic, Lisp,
Глава 20
Rexx и даже COBOL. Большинство операционных систем имеет собственные присоединенные к ним объектные каркасы. Создание объектных БД преследовало цель заполнить образовавшуюся брешь в процессах работы с объектами, но эта мера принесла лишь небольшой успех. CORBA позволяет завершить поиск в этом направлении, реализуя взаимодействие объектов.
0MG выполняет роль посредника между поставщиками, указывая обязательства для каждого и реализуя постоянный контроль за проблемами, связанными с архитектурой, такими как переносимость и способность к взаимодействию.
ПРИМЕЧАНИЕ
Open Group (Открытая группа) (http://www.opengroup.org) предложила сертификацию реализаций CORBA. Поставщики, желающие реализовать возможности взаимодействия сетей, теперь получили в свое распоряжение независимый объект для резервирования своих требований. .
ПОР: объектное склеивание
Протокол Internet Inter-ORB (ПОР) обеспечивает склеивание объектов в сети. ПОР добавляет уровень для перемещения составных типов данных с помощью TCP/IP. Пересылка данных и проверка типов происходит с помощью соединений каналов. ПОР также определяет структуры сообщений для вызовов метода и обработки ошибок. В качестве транспортного протокола ПОР использует не только CORBA, но и RMI из Java.
Как данные перемещаются с помощью ПОР
На самом нижнем уровне ПОР обращается к каналам. ПОР посылает потоки закодированных байтов с помощью TCP/IP. Байты закодированы или размещены в определенном порядке в структурах, содержащих специфические языковые данные до потоков необработанных байтов.
Структуры данных составлены из параметров, возвращаемых значений и структур сообщений, которые составляют определение IDL для объектов CORBA. Если данные из канала не принадлежат типу данных ПОР, то они отбрасываются. Сохранение типов существенным образом обеспечило успех C++; CORBA использует этот же прием.
Каждое сообщение представляет собой заранее определенную структуру. Как клиент, так и сервер осведомлены о формате сообщения. Существует также динамический механизм вызова CORBA, но эта глава
посвящена статическому механизму.
РИСУНОК 20.2. Связь CORBA с ПОР.РИСУНОК 20.2. Связь CORBA с ПОР.На рис. 20.2 показана диаграмма, содержащая слои и отображающая соотношение различных элементов CORBA с ПОР.
РИСУНОК 20.2. Связь CORBA с ПОР.
РИСУНОК 20.2. Связь CORBA с ПОР.
ПОР определяет восемь различных типов сообщения:
Request (Запрос)
Reply (Ответ)
Cancel Request (Запрос отмены задания)
Locate Request (Разместить запрос)
Locate Reply (Разместить ответ)
Close Connection (Закрыть соединение)
Message Error (Ошибка сообщения)
Fragment (Фрагмент)
Большинство этих сообщений имеет заголовок и тело. Часть типов сообщений содержит дополнительные данные, присоединяемые к их телу.
Среди данных в заголовке сообщения содержится тип сообщения и размер тела сообщения. В заголовке указано, кодируются ли целые числа для Intel или Motorola. Кодирование позволяет процессорным архитектурам подключать байты, которые составляют целые числа. Если два компьютера используют одни и те же целочисленные типы, то целые числа транслироваться не будут.
Только клиенту позволено пересылать и отменять задания, а посылать ответы и исключения может только сервер. Сообщение о запросе — это метод работы с объектом. Тело сообщения содержит входные параметры метода в качестве дополнения. Ответ содержит возвращаемое значение и любые параметры, содержащие выходные данные, присоединенные к телу сообщения.
Распределенные вычисления Часть V
Механизм ПОР совершенно прост и фактически универсален. Каждый индивидуальный тип закодирован специфическим способом. Если данные перемещаются между клиентом и заглушкой одним и тем же поставщиком, то нет необходимости использовать протокол ПОР. Если клиент и сервер постоянно находятся на одном и том же компьютере, многие поставщики оптимизируют связь, пересылая данные, использующие межпроцессорную связь.
Компонентная модель
Одни разработчики могут использовать CORBA для формирования компонентов ПО без привязки к определенному языку, платформе или сети. Другие разработчики могут использовать CORBA для интегрирования их приложений с теми те же самыми компонентами ПО, при этом 1DL они используют в качестве компонента.
Снабжая объект дополнительными интерфейсами, вы создаете компоненты, которыми можно управлять и манипулировать. Например, если ваш объект осуществляет транзакцию интерфейса ресурса, просмотр транзакции, то системы, устойчивые к сбоям, а также другие службы могут взаимодействовать с вашим объектом. Объект, который осуществляет транзакцию интерфейса ресурса, позволяет выполнять транзакции с вашим объектом с большой степенью надежности. Поэтому на этот объект можно положиться при работе со сложными системами транзакции.
IDL соглашение связывания
В среде CORBA IDE представляет соглашение между клиентом и сервером. IDL представляет собой согласование функциональных возможностей, которые должен поддерживать компонент. В качестве эквивалента процедуры объявления класса в языке C++ IDL является первичный механизмом интеграции CORBA.
Творческий проект, который больше всего напоминает CORBA IDE, — это Facade. В книге Design Patterns указано, что Facade призван поддерживать унифицированный интерфейс пользователя по отношению к набору интерфейсов в подсистеме. Facade определяет интерфейс более высокого уровня, который значительно облегчает применение подсистемы. (Гамма: 185). Типичным примером реализации Facade как интерфейса объекта является Singleton (Гамма: 127). Интерфейсы, определенные в IDE, не имеют подобного ограничения. При определении интерфейса необходимо обеспечить гибкость, предусмотреть принцип многократного использования и замены компонентов ПО. Ниже перечислены другие преимущества IDE:
Независимый от языка механизм для определения методов и параметров.
Обработка исключительных ситуаций способом, исключающим противоречия в процессе применения различных языков с IDE.
Листинг 20.1 представляет собой практический пример использования IDE в приложении workflow. Мы будем рассматривать этот пример до конца этой главы.
Листинг 20.1. IDL для системы Workflow: Workflow.IDL
/* Workflow.IDL */ module Workflow { typedef string Taskld; typedef string Personld; enum TaskStatusCode // TSC ( TSC_NOT_STARTED, TSC_IN_PROGRESS,*
id;status;description;id;status;description;TSC_ON_HOLD, TSC_FINSHED }; struct Task {
id;
status;
description;
id;
status;
description;
Taskld TaskStatusCode string
Глава 20
typedef sequence<Task> TaskList;
interface Workflow ( void createNewTask ( in Task aTask ); void updateTask( inout Task aTask ); void updateTaskList( inout TaskList aList );
void assignTask( in Taskld aTaskld, in Personld aPersonld );
oid getTask( in Taskld aTaskld, out Task aTask ); void getTasksForPerson( in Personld aPersonld, out TaskList aList );
void taskStarted( in Taskld aTaskld ); void taskOnHold( in Taskld aTaskld ); void taskFinished( in Taskld aTaskld );
};// конец определения интерфейса };// конец определения модуля
В листинге объявляется интерфейс Workflow, что позволяет разработчику клиента взаимодействовать с системой Workflow. Подробности системы скрыты от разработчика клиента. Этот интерфейс почти полностью задокументирован, что позволяет разработчику сервера явно определять доступ к системе Workflow, которая расположена ниже.
Ниже приведены элементы IDL:
Structural: module, interface
Simple Types: Void, boolean, char, octet, string, unsigned short, short, unsigned long, long, float, double, enum
Aggregate Types: typedef, struct, union
Collections: sequence, array
Argument Modifiers: in, out, inout
Special Constructs: exception, attribute
Directives: #include, #pragma
Сравнение IDL с определением класса C++
Спецификация CORBA устанавливает отображение между типами 1DL и типами в различных языках. Разработчики, которые работают на языках программирования C++ и Java, имеют дело с непосредственными отображениями между языками. Типы данных и определения IDL тесно связаны с проблемой языка. IDL должен читать header-файл C++. IDL содержит директивы #include и #pragma.
IDL определяет такие типы: целые и десятичные числа, булевы и строковые. Есть только один тип для хранения: последовательность. Многие из простых типов C++ имеют эквиваленты CORBA. Например, typedefs, enums и structs эквивалентны конструкциям C++ с аналогичными именами. Ключевое слово class заменено ключевым словом interface.
Параметры метода имеют дополнительные спецификаторы, неизвестные разработчикам C++: in, out и inout. Эти спецификаторы указывают необходимое направление данных: в метод или из него. Спецификатор in подобен const. Не предполагается, что значение параметра обновляется методом.
Для большинства интерфейсов typedefs, structs, enums и interfaces могут быть объединены при выполнении многих типичных проектов. Однако если необходимы дополнительные возможности, то IDL обеспечивает наличие атрибутов и исключений.
Хотя объектные данные реализации не являются непосредственно доступными для клиента, отображение языка предполагает, что компилятор 1DL генерирует методы доступа для установки (_set) и восстановления (_get) значения дтя каждого атрибута. Атрибут readonly указывает компилятору IDL на тот факт, что нет необходимости генерировать метод _set.
Можно определять структуру для удержания исключенных данных и затем указывать на то, чтобы конкретный метод отбросил определенное исключение. Разработчик клиента может отбирать исключение и обрабатывать условие ошибки, используя предварительно определенные данные исключения. Структурная
ЧастьV
обработка исключительных ситуаций упрощает исходный текст и помогает в отображении возможных отказов, с которыми сталкивается клиент.
Наследование
При многократном использовании и расширении не многие конструкции языка обеспечивают гибкость в наследовании. Наследование в CORBA 1DL очень схоже с наследованием в C++, но спецификатор доступа отсутствует.
Наследование может использоваться при расширении существующего интерфейса. Например, если клиентские приложения зависят от Workflow и нуждаются в применении дополнительных методов, то можно реализовать наследование из Workflow и добавлять методы к полученному классу. Таким образом, клиенты, зависящие от интерфейса Workflow не должны изменяться; только новые клиентские интерфейсы должны иметь преобразованные заглушки. Этот подход позволяет применять расширение во время выполнения интерфейса CORBA.
Брокер объектных запросов
Object Request Broker (ORB) представляет собой определенный набор методов, который имеет для каждого поставщика специализированную реализацию. Фактическое расположение ORB не определено; требуется лишь, чтобы он был доступен для любого объекта в среде CORBA — как для объектов клиента, так и для объектов сервера.
Основная функция ORB — инициализировать посреднические запросы для вызова метода. ORB с легкостью справляется с этой задачей и может шифровать, сжимать или проверять правильность прав доступа при вызове данных по запросу ORB.
ПРЕДОСТЕРЕЖЕНИЕ
Одни ORB встроены с помощью временного файла; другие могут быть целиком находиться на сервере приложений или Web-сервере. Поставщики ORB склонны повторно использовать ORB, опираясь на свои прежние разработки. Эти ORB могут быть чрезвычайно нестандартны или же характеризоваться недостаточными возможностями по взаимодействию.
Если поставщик помещает сервер приложений в конец ORB, поинтересуйтесь о способности ко взаимодействию с другими ORB и соответствием ПОР.
Архитектура ORB подобна архитектуре сервера каналов. ORB опрашивает порт, ожидая присоединения канала. Если клиент связывается с сокетом, ORB выдает запрос и направляет его объекту сервера.
ПРЕДОСТЕРЕЖЕНИЕ
Portable Object Adapter (РОА — Переносимый объектный адаптер) — это часть спецификации CORBA 2.2, которая гарантирует переносимость со стороны сервера. При наличии РОА можно выдвигать специфические требования к способу обработки объектов сервера. Конкретно объект сервера определяет опции для реализации и постоянно их поддерживает.
Время жизни объектов .
Объекты в CORBA являются постоянными: требуется, чтобы объект существовал в течение всего времени. В C++ большинство объектов временные: создаются во время выполнения и уничтожаются по окончании приложения.
Один из методов по обработке объектов состоит в создании рабочего объекта сервера для того, чтобы создать тип необходимого объекта. Создание рабочего интерфейса полезно при определенных обстоятельствах, но выполнение этой процедуры для каждого интерфейса чрезвычайно утомительно. Поставщики ORB могут использовать псевдообъект для того, чтобы имитировать постоянство объекта сервера. Псевдообъект упрощает просмотр клиентом распределенной среды, позволяя ей проявляться так, как если бы это был одиночный объект, в то время как фактически может иметь место множество объектов.
Для реализации этого эффекта одиночный псевдообъект регистрируется на сервере имен или же используется механизм расположения ORB. Если клиенту необходимо внедрить объект, то он пытается связаться с ORB, содержащим объект. Затем ORB создает объект и перенаправляет клиент новому объекту.
Для клиента все выглядит так, как если бы объект всегда присутствовал в клиенте. Для сервера объект создается и разрушается на основании связи с клиентом.
Глава 20
Среды разработки
Поскольку CORBA — открытый стандарт, реализованный несколькими поставщиками, можно совмещать и согласовывать среды разработки и выполнения. Так можно поступать, если избегать частных расширений, поддерживаемых некоторыми поставщиками. Однако, как всегда и происходит при наличии нескольких поставщиков, необходимо быть уверенным в том, что при комбинировании поставщиков можно получить от этого существенную выгоду, которая примирит вас с потенциальными проблемами.
Например, можно генерировать заглушки клиента Java, используя VisiBroker, и применить Orbix для заглушек клиента C++ и скелетов сервера. Необходимость применения VisiBroker со стороны клиента может быть обоснована наличием ORB VisiBroker на каждом броузере Netscape 4.х/5.х. Такой подход ускоряет распределение клиентов Java CORBA для среды выполнения.
В среде выполнения можно использовать Wcb-сервер Netscape для HTTP, службы имен и службы каталогов LDAP при использовании среды выполнения Orbix для перехода к управлению Microsoft ActiveX.
ПРИМЕЧАНИЕПРИМЕЧАНИЕ
ПРИМЕЧАНИЕ
ПРИМЕЧАНИЕ
Если ваша организация не имеет приложений в среде CORBA, некоторые поставщики предлагают воспользоваться их инструментальными средствами разработки (например, Inprise и Iona). Другие предлагают воспользоваться лишь их ORB без среды разработки (например, Objectspace).
Само наличие лишь одного ORB не обязательно принесет пользу, если не производить проверку способности взаимодействия.
Двумя ведущими средами разработки C++ CORBA являются VisiBroker от Inprise и Orbix от Iona. Оба этих программных продукта похожи и поддерживают все инструментальные средства, которые необходимы при создании решений даже для наиболее сложных проблем. Эти поставщики способствуют тому, чтобы ваша разработка была успешной. Многие компании используют эти ORB в качестве ускорителя для серверов приложения и более сложных сред разработки.
ПРЕДОСТЕРЕЖЕНИЕ
Будьте внимательны при обращении к общедоступным доменам ORB. Разработка CORBA — не самое лучшее поле ■ для экспериментов без поддержки поставщика. Если вы достаточно опытны, просмотрите ORB по адресу:
research.iphil.net/Corba. Не забудьте проверить лицензионные ограничения для коммерческой разработки при использовании ORB свободного доступа.
Сравнение сред CORBA
Каждая среда разработки имеет свои сложности, особенности и недостатки. Если необходимо получить среду с большим перечнем возможностей, то нелегко указать, какие возможности являются частными расширениями спецификации CORBA. Недостаточные исследования могут вынудить вас остановиться на одном частном решении.
Способность ORB к взаимодействию
Реализация способности к взаимодействию являлась большой проблемой для поставщиков ORB. Несмотря на то что стандартизация на ПОР была подспорьем, неоднозначность в спецификации означает, что несовместимость все еще существует. Большинство поставщиков ORB не уделяют должного внимания тестированию своих ORB в сравнении с конкурирующими ORB. Если вы когда-либо имели дело с несовместимостью между реализациями компиляторов C++, то поймете, что при реализациях CORBA возникают аналогичные затруднения.
Ниже приведены некоторые вопросы, на которые следует ответить прежде, чем выбрать определенный ORB:
Имеются ли все отображения языка на одном и том же уровне спецификации CORBA? Поставщики могут быть больше заинтересованы в языке Java, чем в C++. Это может привести к впечатлению, что поставщик предлагает среду CORBA 2.1 дня C++, хотя фактически в данном случае среда Java является совместимой с версией 2.1, а среда C++ совместима с какой-либо ранней версией.
Взаимодействует ли данный ORB с JavalDL/Visigenics/Orbix? Если они не взаимодействуют (или не протестированы на взаимодействие), то ORB может вызвать некоторые проблемы. Требуйте подтверждения способности к взаимодействию и информации о поддержке в будущем.
ЧасгьУ
Какие компиляторы языка и версии тех компиляторов требуются для вашей среды разработки? Компоновка библиотеки C++ пока что несколько затруднительна, и поставщики компилятора сохраняют свой стиль кодирования; необходимо быть внимательным при определении подробностей вашей целевой среды разработки. Поставщики должны предоставить список версий компилятора, которые являются совместимыми.
Какая версия операционной системы необходима? Как и в случае с компилятором, убедитесь в том, что ORB проверен для работы с последней версией вашей операционной среды. Если это не так, то можно сделать вывод о том, что поставщик не заинтересован в поддержке платформы.
Имеется ли механизм прокси для брандмауэров и броузеров? Механизмы защиты в брандмауэрах и Web-серверах редко разрабатываются для управления ПОР. Удостоверьтесь в том, что у поставщика есть решение для работы ORB в сложных условиях, в реальных сетях. Не столь важно, является ли оно совершенным, но необходимо, чтобы было произведено тестирование.
Какой уровень НОР поддерживается? Большинство поставщиков должно поддерживать ПОР версий 1.0 или 1.1. Если они не сообщают об этом или не знают сами, то это существенный недостаток. Убедитесь в том, что поставщик не использует частный протокол interORB. Некоторые поставщики, перестроившие свои серверы приложений в ORB, столкнулись с этой проблемой. Если это так, то при реализации взаимодействия могут возникнуть серьезные проблемы, особенно если проявится несовместимость с ПОР.
Когда будет поддерживаться спецификация CORBA 2.2/3.0? Убедитесь в том, что поставщик ORB постоянно работает над усовершенствованием ORB, уделяя внимание вопросу поддержания совместимости с новыми спецификациями. Поставщики могут требовать столь большой объем ресурсов для частных расширений, что будет утеряна способность к взаимодействию.
ПРИМЕЧАНИЕ
Приложения, представленные в этой главе, совместимы с VisiBroker Inprise для C++ и компилятором C++ Borland. Если используется Orbix от Iona, то сгенерированные файлы будут иметь различные имена, но типовой программный код должен быть совместимым.
Детали реализации CORBA и среды разработки C++ не описываются в этой книге, цель авторов — сформировать подробное концептуальное представление о предметной области..
Скомпилируйте приложения этой главы и обратитесь к документации поставщика для установки среды CORBA и работы с ней.$
Создание клиента С+
Клиентское приложение состоит из связующего ORB-кода и обращений к методам со стороны проксиклиентов. Прокси-клиент — это класс C++; он отображается для разработчика клиента так, как будто это локальный, а не удаленный класс.
Вызовы в клиентской разработке обычно осуществляют связь со службой имен или сервером ORB, который управляет объектом. В разделе "Стратегии тестирования" далее в этой главе описаны некоторые методы устранения проблем, связанных с соединениями.
Воспользуйтесь этими действиями при создании клиентского приложения:
Получите IDL для интерфейса, к которому будете обращаться.
Сгенерируйте клиентский программный код заглушки из IDE.
Запишите программный код для соединения клиентского объекта с ORB.
Используя метку Workflow, создайте реализацию клиентской заглушки. Реализованная заглушка и является вашим клиентским объектом.
Теперь напишите программный код для вызова методов клиентского объекта.
Генерирование заглушки
Воспользуемся модулем workflow.idl из листинга 20.1. В среде VisiBroker имеется следующая команда для генерирования классов CORBA из шаблона IDL:
dos> idl2cpp -src_suffix срр -no_tie workflow.idl
Глава 20
При выполнении этой команды создаются четыре файла: ■ workflow_c.hh ■ workflow_c.срр ■ workflow_s.hh ■ workflow_s.cpp
Header-файлы и файлы исходного кода с добавлением _с предназначены для клиентского приложения; файлы с добавлением s предназначены для серверного приложения. Сразу же обратите внимание на файлы, которые сгенерированы для клиента. Классы, определенные в этих файлах, и образуют клиентскую заглушку. Будьте внимательны: программный код сгенерирован с учетом производительности, а не для удобства чтения.
Для ознакомления с подробностями интерфейса класса C++ просмотрите файл workflowc. hh. Откройте его и обратите внимание на метод bind(). Ознакомьтесь с методами, расположенными непосредственно после этого метода. Для удобства можно скопировать следующие объявления метода и переместить их в комментарий в программном коде клиента:
/*
Методы:
virtual void taskStarted( const char* _aTask!d ) ;
virtual void updateTask( Tasks _aTask ) ;
virtual void taskOnHold( const char* _aTask!d );
virtual void createNewTask( const Tasks _aTask ) ;
virtual void assignTask( const char* _aTask!d, const char* _aPerson!d);
virtual void taskFinished( const char* _aTask!d ) ;
virtual void getTasksForPerson( const char* _aPerson!d, TaskList*S _aList ) ;
virtual void getTask( const char* _aTask!d, Task*S _aTask );
virtual void updateTaskList( TaskListS _aList ); */
Эти методы будут использоваться для обращения к функциональным возможностям сервера. Утилита idl2cp генерирует специфические отображения для каждого метода, типа и параметра.
Связь с ORB
Воспользуйтесь этими действиями для соединения с объектом сервера:
Инициализируйте локальный ORB.
Осуществите привязку к объекту.
Надеемся, что вам не покажется это слишком сложным. Необходимо задать имя объекта, определенное разработчиком сервера. В этом случае разработчик сервера предоставил имя Workflow Server. Рассмотрим, каким образом написан программный код:
CORBA::ORB_ptr broker = CORBA::0RB_init( argc, argv ); Workflow::Workflow var workflow =
Workflow::Workflow::_bind( "Workflow Server" ) ;
argc и argv пересылаются ORB для определения опций ORB. He беспокойтесь об этом; разработчик сервера сообщит, если возникнет необходимость в каких-либо определенных опциях.
Вызовы методов
Вызовы, произведенные клиентским объектом workflow, перенаправляются для выполнения объекту сервера. Например, при создании новой задачи необходимо использовать следующий программный код:
Task task;
task.id= "TASK123";
task.status= TSC_NOT_STARTED;
task.description = "Create the server application";
Распределенные вычисления Часть V workflow->createNewTask( task ) ; workflow->getTask( "TASK123", &task ); cout « "TASK123: " « task. description () « endl; Метод description() противоположен объекту клиента workflow. Функциональные возможности по добавлению новой задачи постоянно находятся на объекте сервера и на удаленном объекте. В этом фрагменте программного кода полностью скрыты распределенные функциональные возможности. Объект сервера может выполняться на том же самом компьютере, что и клиентский объект, либо в локальной сети или даже в Internet. Специалисты, создававшие ORB, проявили заботу о сетевом программировании.
Завершенное клиентское приложение C++
В листинге 20.2 показано завершенное клиентское приложение.
Листинг 20.2. Завершенное клиентское приложение: dientxpp /* client.срр */ #include "workflow_c.hh" #include <iostream.h> int main( int argc, char* argv[] ) { try { CORBA::ORB_ptr broker = CORBA::ORB_init( argc, argv ); Workflow::Workflow_var workflow = Workflow::Workflow::_bind( "Workflow Server" ) ; Task task ; task.id= "TASK123";
task.status- TSC_NOT_STARTED;
task.description = "Create the server application"; workflow->createNewTask( task ); workflow->getTask( "TASK123", &task ); cout « task.id()« ” : ”
« task.description() « endl; } catch ( CORBA::SystemExceptionS anException ) cout « "CORBA Exception: " « anException « endl; return 1; ) return 0; }
Клиентское приложение создает структуру Task и запрашивает сервер о создании задачи, основанной на этой структуре. Клиентское приложение затем запрашивает ту же самую задачу из сервера. Ожидается, что Task будет возвращена из getTask() в результате выполнения методом createNewTask() процедуры. Сервер может добавлять либо заменять данные при необходимости. Статус задания может измениться при установке задания в начальное состояние.
Создание сервера C++
Будем работать над созданием той части приложения CORBA, которая имеет отношение к серверу. Для создания объекта сервера C++ выполните следующие действия:
Создайте IDL.
Сгенерируйте скелет сервера.
Наследуйте свой класс сервера из скелета.
Инициализируйте соединение с ORB.
Глава 20
Создайте образец класса сервера и определите метку в конструкторе.
Инициализируйте ВОА.
Присоедините объект сервера к BOA/ORB.
Сервер теперь готов принять вызовы метода из клиентского объекта.
Генерирование скелета
Воспользуемся модулем workflow.idl, который описан в листинге 20.1. В среде VisiBroker имеется следующая команда для генерирования классов CORBA из шаблона IDL:
dos> id!2cpp -src_suffix срр -no_tie workflow, idl
При выполнении этой команды создаются четыре файла:
workflow_c. hh
workflow_c. xpp
workflow_s. hh
workflow_s. xpp
Для клиентской заглушки предназначен заголовок, заканчивающийся _с. Заголовки, оканчивающиеся на _s, предназначены для скелета сервера.
Опция -no_tie подавляет генерирование классов TIE. Эти классы поддерживают альтернативную модель внедрения для объектов сервера. TIE может использоваться в тех ситуациях, при которых ваш объект сервера должен наследоваться из класса, не являющегося скелетом.
Процесс наследования используется в данном случае для обеспечения выполнения объекта сервера. Компилятор IDE создал чисто виртуальный метод для каждого метода, определенного в интерфейсе для Workflow. Каждый из этих методов должен выполняться в полученном классе.
Реализация методов сервера
Генератор IDE создал скелетный класс _sk_Workflow с чисто виртуальными методами для каждого метода, определенного в интерфейсе IDE.
Получим теперь Workflow из _sk_Workflow, реализуя методы объекта сервера, как показано в листингах 20.3 и 20.4.
Листинг 20.3. Объявление класса сервера: Workflow.hpp
// Workflow.hpp
class Workflow : public _sk_Workflow {
void
taskstarted
(
const char* aTaskld );
void
updateTask
(
Workflow::Tasks aTask );
void
taskOnHold
(
const char* aTaskld );
void
createNewTask
(
const Workflow::Tasks aTask)
void
assignTask
(
const char* aTaskld, const char* aPersonld );
void
taskFinished
(
const char* aTaskld );
void
getTasksForPerson
(
const char* aPersonld, Workflow::TaskList*S aList )
void
getTask
(
const char* aTaskld, Workflow::Task*S aTask );
void
updateTaskList
(
Workflow::TaskListS aList );
};
Листинг 20.4. Определения метода сервера: Workflow.cpp void Workflow::createNewTask( const Workflow::Tasks aTask ) { И writeTaskToDatabase( aTask ); }
Workflow::getTask( const char* aTaskld, Workflow::Task*s aTask ) (
Распределенные вычисления Часть V
И readTaskFromDatabase( aTaskld, aTask ) ;
// Установить значения для тестирования aTask->setId( aTaskld ) ;
aTask->setDescription( "The method was called!" ) ;
}
Подсоединение класса сервера
Подсоединение к ORB в случае с объектом сервера проходит аналогично тому, как это выполняется в случае с объектом клиента. Представляйте себе ORB в виде вездесущего интерфейса:
CORBA::ORB_ptr broker = CORBA::ORB_init (argc, argv);
Загрузка BOA в ORB
Basic Object Adapter представляет собой плохо определенный интерфейс, который используется объектами сервера для соединения с соответствующими ORB. Спецификация CORBA 2.2 поддерживает Portable Object Adapter — интерфейс, который значительно лучше ВОА, и он должен переноситься различными поставщиками ORB.
Теперь продолжим реализацию, используя ВОА, который определен спецификацией CORBA 2.1 подобно тому, как это реализовано VisiBroker.
Теперь снабдим наш объект сервера меткой. Такую метку клиент использует при ссылке на этот объект сервера:
Workflow::Workflow_ptr workflow =
new Workflowlmpl( "Workflow Server" ) ;
Now connect the server object to the ORB:
CORBA::BOA_ptr adapter = broker->BOA_init( argc, argv ) ;
adapter->obj_is_ready( workflow ) ;
Осталось лишь начать получать запросы:
adapter -> impl_is_ready( ) ;
Приложение сервера теперь будет получать запросы до тех пор, пока этот процесс не будет прерван ORB (см. листинги 20.5 и 20.6).
Листинг 20.5. Завершенный заголовок сервера C++: Workflow.hpp
И Workflow.hpp
♦include "workflow_s.hh"
class Workflow : public _sk Workflow
1;1;voidtaskStarted(const char* aTaskld ) ;voidupdateTask(Workflow::Tasks aTask );voidtaskOnHold(const char* aTaskld );voidcreateNewTask(const Workflow::Tasks aTask)voidassignTask(const char* aTaskld, const char* aPersonld );voidtaskFinished(const char* aTaskld );voidgetTasksForPerson(const char* aPersonld, Workflow::TaskList*S aList )voidgetTask(const char* aTaskld, Workflow::Task*S aTask );voidupdateTaskList(Workflow::TaskListS aList ) ;voidtaskStarted(const char* aTaskld ) ;voidupdateTask(Workflow::Tasks aTask );voidtaskOnHold(const char* aTaskld );voidcreateNewTask(const Workflow::Tasks aTask)voidassignTask(const char* aTaskld, const char* aPersonld );voidtaskFinished(const char* aTaskld );voidgetTasksForPerson(const char* aPersonld, Workflow::TaskList*S aList )voidgetTask(const char* aTaskld, Workflow::Task*S aTask );voidupdateTaskList(Workflow::TaskListS aList ) ;
1;
1;
void
taskStarted
(
const char* aTaskld ) ;
void
updateTask
(
Workflow::Tasks aTask );
void
taskOnHold
(
const char* aTaskld );
void
createNewTask
(
const Workflow::Tasks aTask)
void
assignTask
(
const char* aTaskld, const char* aPersonld );
void
taskFinished
(
const char* aTaskld );
void
getTasksForPerson
(
const char* aPersonld, Workflow::TaskList*S aList )
void
getTask
(
const char* aTaskld, Workflow::Task*S aTask );
void
updateTaskList
(
Workflow::TaskListS aList ) ;
void
taskStarted
(
const char* aTaskld ) ;
void
updateTask
(
Workflow::Tasks aTask );
void
taskOnHold
(
const char* aTaskld );
void
createNewTask
(
const Workflow::Tasks aTask)
void
assignTask
(
const char* aTaskld, const char* aPersonld );
void
taskFinished
(
const char* aTaskld );
void
getTasksForPerson
(
const char* aPersonld, Workflow::TaskList*S aList )
void
getTask
(
const char* aTaskld, Workflow::Task*S aTask );
void
updateTaskList
(
Workflow::TaskListS aList ) ;
Листинг 20.6. Завершенное тело сервера C++: Workflow.cpp
// Workflow.cpp
♦include "workflow.hpp"
void Workflow::taskStarted( const char* aTaskld )
)
Глава 20
void Workflow::updateTask( Workflow::Tasks aTask ) ( }
void Workflow::taskOnHold( const char* aTaskld ) { }
void Workflow::createNewTask( const Workflow::Tasks aTask ) { II writeTaskToDatabase( aTask ) ;
}
void Workflow::assignTask( const char* aTaskld, const char* aPersonld ) { }
void Workflow::taskFinished( const char* aTaskld ) { }
void Workflow::getTasksForPerson( const char* aPersonld, Workflow::TaskList*S aList )
( )
Workflow::getTask( const char* aTaskld, Workflow::Task*S aTask ) { II readTaskFromDatabase( aTaskld, aTask );
// Установить значение для тестирования aTask->set!d( aTaskld ) ;
aTask->setDescription( "The method was called!" ) ; }
void Workflow::updateTaskList( Workflow::TaskListS aList ) { }
/* server.cpp */
#include "workflow.hpp"
#include <iostream.h>
int main( int argc, char* argv[] ) { try { CORBA::ORB_ptr broker = CORBA::ORB_init( argc, argv ) ;
Workflow::Workflow_ptr workflow = new Workflowlmpl( "Workflow Server" );
CORBA::BOA_ptr adapter = broker->BOA_init( argc, argv );
adapter->obj_is_ready( workflow ) ; adapter->impl_is_ready();
} catch ( CORBA::SystemExceptions anException ) (
cout « "CORBA Exception: " « anException « endl; return 1;
return 0;return 0;}
return 0;
return 0;
}
Часть V
Клиент Java
Способность к интеграции с другими языками — одна из известных возможностей CORBA. Имея лишь определение IDL, можно создавать клиентское приложение, которое органически интегрируется с вашим объектом сервера.
В следующих разделах этой главы описан эквивалент Java для клиента C++. Здесь будут описаны лишь различия в генерировании объектного кода, запуске и вызове метода. Наша цель состоит в том, чтобы ознакомить вас с альтернативными привязками языка для CORBA.
ПРИМЕЧАНИЕ
Клиент Java использует VisiBroker для Java.
Компания JavaSoft включила свободно распространяемый ORB в пакет JDK 1.2. Для того чтобы этот ORB работал с VisiBroker, необходимо использовать не всегда удобную ссылку IOR. Более подробная информация об этом содержится в разделе этой главы "Служба имен и способность к взаимодействию".
Генерирование заглушки
Для генерирования клиентских заглушек в Java используется workflow.idl. Выполните следующую строку: dos> idl2java -no_tie workflow.idl
VisiBroker создает каталог Workflow co следующими файлами:
■ PersonldHelper.java И PersonldHolder.java И Task.java
8 TaskHelper.java
■ TaskHolder.java
И TaskldHelper.java
H TaskldHolder.java
H TaskListHelper.java ■ TaskListHolder.java G TaskStatusCode.java El TaskStatusCodeHelper.java И TaskStatusCodeHolder.java ■ Workflow.java ■ WorkflowHelper.java ■ WorkflowHolder.java И WorkflowOperations.java И _example_Workflow.java ■ _sk_Workflow.java ■ _st_Workflow.java И _WorkflowImplBase.java Здесь приведены классы Java как для клиентской заглушки, так и для скелета сервера. Укажем файлы, специфические для сервера: _exampIe_Workflow. java и _sk_Workflow. java. Все другие файлы необходимы для клиентского приложения.
Запуск и программный код вызова метода
Ниже приведен Java-эквивалент для запуска клиента C++ и программный код вызова метода для клиента Workflow:
org.omg.CORBA.ORB broker =
org.omg.CORBA.ORB.init( args, null );
Workflow.Workflow workflow =
Глава 20
Workflow.WorkflowHelper.bind( "Workflow Server" ) ; Task task; task.id= "TASK123";
task.status= TaskStatusCode._TSC_NOT_STARTED;
task.description = "Create the server application"; workflow.createNewTask( task ); TaskHolder taskHolder; TaskldHolder taskld( "TASK123" ) ; workflow.getTask( taskld, StaskHolder );
He считая специфических для языка синтаксических различий, программный код Java очень похож на код клиента C++. Поскольку Java испытывает недостаток в определяемых пользователем операторах преобразования и enums, то генерируется дополнительный программный код.
Стратегии тестирования
Какого рода проблемы могут возникать с объектами, выполняющими удаленные вызовы методов? Да, возможно, эти проблемы не столь значительны, как можно предположить. Поскольку ОКБ используются в приложениях, которые выполняют сложные задачи, брокеры ОКБ должны быть устойчивыми в различных ситуациях. Может сложиться мнение, что дополнительный уровень объектов CORBA снижает вероятность отказов.
Может быть, и невозможно смоделировать среду, в которой произошла ошибка. В этих случаях могут использоваться более традиционные инструментальные средства сетевого программирования.
В последующих разделах описаны методы, которые помогут при отладке приложений CORBA.
Трассировка
Почти каждый ORB обладает возможностью трассировки. Воспользуйтесь этим. При реализации некоторых возможностей трассировки используется Implementation Repository (Архив реализаций) для хранения информации; можно также использовать пульты управления или файлы двумерных массивов.
Обратите внимание, каким образом осуществляется этот процесс, когда все идет успешно. Затем при возникновении проблемы можно будет быстро ее устранить.
Службы мониторинга и регистрации
Регистрация события является трассировкой во время выполнения. Перешлите события регистрирующим интерфейсам и дайте вашим объектам вторую жизнь.
Если происходят сбои, то пользователи могут воспользоваться файлом регистрации и, возможно, четко зафиксировать свои собственные проблемы.
Обработка исключений
1DL позволяет выявлять исключения структурированно и подробно. Используя раскрытие динамических типов, инструментальные средства регистрации могут отобразить данные, оказавшиеся в особой ситуации.
Например, следующее исключение может быть добавлено к примеру Workflow:
II Добавить к Workflow.IDL: exception TaskAlreadyAssigned■
{ Taskld assignedToId; } II Заменить метод в Workflow.IDL void assignTask( in Taskld aTaskld, in Personld aPersonld ) raises (TaskAlreadyAssigned);
II Добавить к Workflow.cpp try < workflow->assignTask( "TASK123", "CHUCKPACE" );
} catch ( TaskAlreadyAssignedS anException ) {
Часть V
cout « "TASK123 already assigned to: " « anException.assignedTo!d() « endl;
)
Этот метод поддерживает подробности для исключений, и IDL определяет исключение, которое метод может упустить.
Удаленная отладка
Некоторые компиляторы C++ позволяют ограничивать удаленную отладку приложений. В прошлом эти отладчики работали над последовательными соединениями. С развитием Internet они работают с помощью протокола TCP/IP.
При отсутствии удаленной отладки можно использовать ПО дистанционного управления под управлением Windows и, конечно, Telnet или X Window под управлением UNIX.
Служба имен и способность к взаимодействию
Службы CORBA представляют собой спецификацию более чем 10 служб, которые необходимы при создании приложений CORBA. IDL определен для каждой из этих служб. Самая близкая аналогия — это системные службы для операционной системы. Служба имен — наиболее часто используемая служба.
Для большинства приложений служба имен не является необходимой. Поскольку возрастает степень зависимости приложений от различных систем, возрастает и потребность в инструменте организации. Служба имен поддерживает способ назначения имен объектам и обеспечивает организацию этих объектов в объединенные иерархии. Эта идея близка идее, лежащей в основе создания каталогов файловых систем, которые определяются с помощью сетей.
Служба имен имеет определенный интерфейс. Объект сервера самостоятельно регистрируется с помощью службы имен. Регистрация представляет собой связывание объектной ссылки (IOR) и имени. Имя представляет собой последовательность именующих контекстов, подобных имени каталога в файловой системе.
Клиентский объект предоставляет службе имен имя, и служба имен возвращает IOR. Затем клиент связывается с ORB, определенным IOR. Вызовы методов для объектов сервера запрашиваются с помощью ORB.
При отсутствии службы имен клиенту потребуется узнать специфическое расположение сервера, который подобен URL. Более того, если расположение объекта сервера изменится, то связь будет потеряна. И тогда клиент больше не сможет связаться с ORB.
Легко видеть, что служба имен очень похожа на каталоги объектов. Последовательности, именующие объект, имеют иерархическую природу, поскольку они находятся в структуре каталогов. Для простых сред объекты идентифицированы с помощью имен на одиночном корневом уровне. Поскольку архитектура усложняется, то иерархический элемент дифференцирует интерфейсы, разбивая их по категориям.
Interoperable Object Reference (IOR)
Удобно представлять ссылку Interoperable Object Reference (IOR — Объектная ссылка взаимодействия) как эквивалент ссылке в C++. IOR содержит IP-адрес хоста, номер порта и объектный ключ, специфический для данной реализации. Клиент использует эту информацию для обнаружения и связи с ORB согласно указаниям IOR, о том где содержатся адреса хоста и порта. После установления связи с ORB можно приступать к выдаче запросов с помощью соединения, ссылаясь на объектный ключ при направлении запроса к определенному объекту.
Объектная ссылка может быть преобразована в строку ASCII-кода путем вызова для любого объекта метода object_to_string(). Спецификация CORBA явно определяет структуру этой строки. Строка может затем оказаться вне среды CORBA, и IOR может быть вновь создана на другом компьютере с помощью объектного метода string_to_object(). Этот процесс представляет, каким образом могут взаимодействовать ORB различных поставщиков.
Однако IOR не обязательно будет существовать вечно. Вследствие несоответствий и неоднозначности в спецификации CORBA IOR перегружается.
ПРЕДОСТЕРЕЖЕНИЕ
Различные поставщики ORB используют IOR нестандартными способами. Будьте внимательны и всегда требуйте службу имен для IOR, а также используйте IOR в приемлемом интервале времени.
После того как установлена связь с вашим целевым ORB, больше не должно быть проблем. На этом этапе связь с каналом . установлена, и объект должен существовать до тех пор, пока не будет устранен класс заглушки прокси-заглушки.
Глава 20
Обнаружение службы имен на первоначальном этапе может вызвать затруднения. В спецификации CORBA 2.1 отсутствует какой-либо протокол начальной загрузки для клиента, который помог бы в нахождении первого брокера ORB.
Именование контекстов
В CORBA именование контекстов призвано решать проблемы конфликта имен в языке C++. Именование контекстов производится в иерархии, подобной структурам каталогов файловой системы или иерархии классов. Контекст имени — это последовательность именований контекстов, которые идентифицируют одиночный объект уникальным образом.
На рис. 20.3 показано, каким образом имена создаются из именования контекстов. Имя, разделяемое точкой с запятой, представляет собой имя объекта. Если пространства имен C++ сохраняют имена переменных классов, то именование контекстов CORBA гарантирует уникальность имен объектов.
РИСУНОК 20.3.
Иерархическое вложение для предотвращения конфликтов имен объектов.
Проблемы взаимодействия
Метод _bind() представляет собой механизм начальной загрузки VisiBroker, который позволяет выполнить разрешение имен. Этот метод _bind() исключает возможность переноса клиентского программного кода, но позволяет его прочесть.
Универсальная начальная загрузка должна быть включена в спецификацию CORBA 3.0. Для различных ORB вам, вероятно, придется заменить _bind() специализированным механизмом, разработанным поставщиком, для выполнения разрешения имен.
Чрезвычайно важно понять три уровня возможной начальной загрузки:
Использование частного метода в клиентском объекте, например, _bind(). Этот метод используется для небольших приложений и для приложений среднего размера, где клиент и сервер находятся на одном и том же компьютере или в локальной сети.
Частное соединение со службой имен, когда эта служба и ORB имеют частный механизм инициирования соединения. В данном случае приложение более сложно и нуждается в службе имен для того, чтобы найти объекты сервера, которые могут находиться на локальной сети или обнаруживаться в Internet.
Если служба имен и клиентский объект получены от разных поставщиков, то необходима стринги- фикация. Это означает, что служба имен записывает стрингифицированную IOR (строка, которая может быть повторно сконструирована в IOR) в файловую систему. Клиент затем читает строку из файловой системы, повторно конструируя IOR и связываясь с первым ORB.
Производительность
Производительность в среде CORBA достаточно высокая. Когда автор приступал к разработке, то ожидал получить невысокую производительность при опытной разработке OLE/COM.
В следующих разделах описаны области, в которых ваши приложения будут работать наиболее устойчиво. Многие из этих областей подобны тем, которые имеются в разработке C++, — там, где возникает связь с распределенными вычислениями.
Перерасход памяти со стороны ORB
Если приложение выполняется с порожденным процессом, то очень трудно определить причины перерасхода памяти. Операционная система действует как сборщик мусора для приложения Но если перерасход памяти возник внутри одиночного процесса, приложение должно выполнить собственную сборку "мусора". Большинство приложений C++ не содержит схемы сборки "мусора". Следовательно, программисты C++ должны быть внимательны к утечкам в приложениях сервера при работе с большинством сред CORBA. Простая утечка, которая может быть скрыта в CG1-порожденном процессе, может превратиться в огромную проблему для ORB. Стоимость со стороны CG1 обычно отображается во время запуска процесса и неуклюжих архитектур.
Степень детализации интерфейса
Поскольку механизмом, соединяющим ПОР, являются каналы, передача данных обычно происходит намного медленнее, чем при использовании IPC или при прямой передаче в память. Для того чтобы это компенсировать, можно консолидировать методы доступа в одиночные вызовы метода, которые устанавливают или получают значения из передаваемой структуры.
примечание
Поставщики брокеров ORB могут оптимизировать саои модули с помощью IPC или прямого доступа в память, когда клиент и сервер находятся на одном компьютере. Некоторые ORB даже используют более быстрые протоколы, чем ПОР, если их программный продукт используется на клиенте и сервере.
Если клиент и сервер находятся на различных компьютерах, то ORB, выполняющий роль прокси, может быть назначен самому медленному соединению в целях ускорения прохождения запросов ORB.
Некоторые реализации TCP/IP используют IPC, если клиент и сервер находятся на одном компьютере.
Можно поддерживать дополнительные методы, которые выполняют пакетную обработку для часто вызываемых методов. /Б'я выполнения этого создайте структуру с параметрами, возвращаемыми значениями и информацией об исключениях. Создайте последовательность этих структур и используйте ее как параметр для пакетной версии функции.
Избегайте передачи слишком большого количества данных. Если сервер обеспечивает клиент списком данных из таблицы базы данных и пользователь отмечает строки для удаления, вставки или изменения, передавайте обратно только строки, которые должны измениться.
Высокая производительность аппаратных средств устраняет много проблем, имеющих отношение к степени детализации, если клиент и сервер работают на одном компьютере.
Ссылки на передаваемый объект
IOR в CORBA является эквивалентом ссылки в C++. Ссылка IOR может быть передана как параметр в методе. Затем объект сервера может обращаться к тому объекту для выполнения любых необходимых операций. Этот косвенный подход предполагает непроизводительные затраты; не перегружайте 1OR таким образом.
Резюме
Спецификация CORBA и ее реализации представляют собой крупное достижение в сфере разработки ПО. Теперь разработчик может сосредоточиваться на решении архитектурных проблем, не тратя свои усилия на решение проблем интеграции.
Разработчики, имеющие скромный опыт, и даже новички могут воспользоваться гибкостью и вездесущностью CORBA при создании ПО следующего поколения. Эти системы будут отличаться тем ценным качеством, что можно будет управлять распределенными вычислительными средами и поддерживать их работу точно так же, как в случае со старыми мэйнфреймами, а гибкости работы с ними смогут позавидовать старые приложения клиент-сервер.
COMCOM81
COM
COM
■ ■ ■■ ■ ■
■ ■ ■
■ ■ ■
В ЭТОЙ ГЛАВЕ
Основы СОМ
Использование СОМ-объектов в C++
Создание COM-объектов в C++
Объектно-ориентированные языки — значительный шаг вперед в развитии индустрии ПО. Большое количество книг посвящено обоснованию процесса построения объектно-ориентированных проектов на базе C++. Поскольку индустрия по созданию программного обеспечения непрерывно развивается, возникают новые требования к средам разработки ПО. Создалась такая ситуация, когда отдельные языки программирования уже не могут удовлетворить возросшим запросам: необходимо использовать двоичные стандарты и стандарты, основанные на связи между языками.
Камнем преткновения при работе с C++ (так же, как и с иным объектно-ориентированным языком) является тот факт, что эти языки поддерживают объектную парадигму на уровне исходного программного кода. Программа формируется с помощью многократно используемых классов в исходном виде. Это применимо к пакетным классам, размещенным в многократно используемых двоичных модулях — библиотеках динамических связей (DLL), и позволяет распространять только заголовки класса. К сожалению, этот подход чреват возникновением больших проблем. Поскольку нет какого-либо стандарта для регламентации того, каким образом различные компиляторы C++ реализуют возможности языка, двоичная DLL оказывается тесно связанной с компилятором, который ее генерирует. Заголовок должен содержать все подробности о реализации (данные и скрытые методы), поскольку компилятор должен генерировать соответствующее распределение памяти класса, включая виртуальные таблицы. Другая проблема заключается в наличии различных версий программного кода. Несмотря на то что новая версия может быть доступной для клиентов, которые уже использовались, однако новые клиенты обречены на неудачу при работе с более старыми версиями библиотек. Эти проблемы вынуждают поставщиков библиотек классов использовать неэффективные приемы в работе (например, библиотека основных классов (Foundation Classes) компании Microsoft сопровождается для каждой последующей версии другим именем DLL) или поставлять исходный программный код библиотеки.
Для решения этих проблем, а также для обеспечения независимости языка программирования компания Microsoft создала спецификацию Component Object Model (COM — Компонентная объектная модель) — двоичный стандарт для развертывания и использования двоичных компонентов ПО.
Если обратиться к предыстории, то СОМ создавалась для удовлетворения запросов другой технологии Microsoft: Object Linking and Embedding (OLE — Связь и внедрение объектов). Со временем создатели поняли, что они получили чрезвычайно простую, мощную и легко расширяемую архитектуру. Microsoft использовала технологию СОМ практически во всех своих программных продуктах, даже превращая свои операционные системы в наборы компонентов. Это не значит, что СОМ приносит пользу только Microsoft. При обращении к стандартной схеме создания версий многие поставщики исходных компонентов нашли удачное решение проблем распределения двоичных библиотек. Было введено новое понятие: componentware.
Основным достижением, возможностями которого позволяет воспользоваться СОМ, является отсрочка времени выполнения соединения между двоичными компонентами и их клиентами (не следует путать время выполнения соединения; клиент все еще нуждается в заголовке, содержащем определение для компонента во время компиляции для C++; для других языков программирования может потребоваться другое определение). Если развертывается новейший компонент, то более ранний клиент по-прежнему использует его через функциональные возможности наследования. Более новые клиенты могут также использовать новые функциональные возможности. Если более новый клиент "сталкивается" со старой версией компонента, то производится распознание этого факта стандартным образом. При этом клиент может отказаться от новых и использовать только более старые функциональные возможности.
Появление технологии СОМ никоим образом не преуменьшает пользу C++ как языка программирования. Фактически поскольку технология СОМ была разработана на основании понятий C++, то вполне естественно создавать программы СОМ на языке C++. Эта глава посвящена вопросам использования и создания объектов СОМ. Глава состоит из трех разделов. В первом разделе, содержащем основные принципы СОМ, раскрываются основы этой технологии. Читатели, имеющие представление о СОМ, могут при желании пропустить это введение. Во втором и третьем разделах описана методология C++ по использованию и созданию объектов СОМ. Тематика, связанная с технологией СОМ, слишком обширна, чтобы ее можно было изложить в одной главе. Поэтому в конце главы приведен раздел "Дополнительная литература".
Основы СОМ
В этой часть главы представлена начальная информация о СОМ. Читатели, которые знакомы с СОМ в основных чертах, могут по желанию пропустить материал вплоть до раздела "Другие технологии СОМ" или до раздела "Использование объектов СОМ".
Архитектура COM
Модель Component Object Model определяет следующие основные сущности:
Объект. Этот элемент тесно связан с классом C++. Объекты реализуют функциональные возможности и представляют основное содержание СОМ.
Интерфейс. Этот элемент обеспечивает определение некоторых функциональных возможностей, которые являются общими для многих объектов. Интерфейсы определяют правила для взаимодействия с объектами.
Библиотека СОМ времени выполнения. Этот элемент содержит небольшой набор подпрограмм для необходимой поддержки объектных служб.
Каждый объект реализует один или большее количество интерфейсов, которые дают возможность пользователям осуществлять доступ к нему. Интерфейсы, реализуемые объектами, представляют их функциональные возможности. Выбор реализуемых интерфейсов осуществляется конструктором объектов.
Каждый объект может быть выбран для создания другого объекта при использовании метода с помощью одного из интерфейсов (при реализации метода интерфейса создается объект и возвращается указатель на один из его интерфейсов через выходной параметр). Этот процесс представляет собой часть семантики интерфейса. Не часто извлекают выгоду из того, что один объект производит другой, и затем второй объект производит иной объект, относящийся к объектам первого класса. Чаще происходит так: имеется какой- либо объект, создаваемый непосредственно, который, в свою очередь, создает другие объекты. Эти объекты создают другие объекты и т.д. В результате получим дерево объектных классов, в котором каждый родительский класс может производить объекты своих потомков. Иерархия объектов, созданных одним корневым объектом и его потомками, называется компонентом. Компонент представляет собой нечетко определенный термин. Иногда он относится к группе объектных иерархий, реализованных в одиночном модуле (исполняемая программа или DLL), который инкапсулирует необходимые функциональные возможности для выполнения определенной задачи. Объекты из различных иерархий обычно строго взаимосвязаны (например, один объект активно используется другим при выполнении задачи).
Один специфический класс объектов СОМ, который может быть создан непосредственно во время выполнения СОМ, представляет особый интерес. Это класс СОМ, или кокласс. Как будет показано далее в этой главе, этот класс связан с другим классом объектов СОМ — фабрикой классов, которая производит их. Объект фабрики классов представлен методом в одном из своих интерфейсов, которые производят экземпляр кокласса и, таким образом, могут рассматриваться как независимая от языка конструкция, аналогичная оператору new в языке C++.
COM-объекты могут использоваться их клиентами в двух различных режимах (которые прозрачны для объекта и клиента): прямом или упорядоченном. Если объект и клиент находятся в едином контексте выполнения (который называется апартаментом'), клиент использует прямые указатели на объектные интерфейсы. Если объект и клиент находятся в различных апартаментах, то СОМ вклинивается между ними и обеспечивает необходимую поддержку для вызова методов интерфейса и возврата результатов. Два объекта обеспечивают необходимую поддержку. Прокси постоянно находится з клиентском контексте и действует как объект для этого определенного интерфейса (клиент не может различить объект и его прокси). Заглушка находится в контексте объекта и действует как клиент интерфейса объекта. Соединение между объектами использует протоколы, соответствующие определенному соединению (сообщения Windows, RPC или др.).
Интерфейсы
В СОМ интерфейсы определяют функции, которые реализуются объектами. Они больше всего относятся к классам C++, которые не содержат каких-либо элементов данных, а лишь чисто виртуальные методы. Таким образом интерфейсы СОМ реализуются в языке C++. Ключевое слово interface, которое применяется в программном коде для описания интерфейса, заменяется на struct в заголовке C++: #define interface struct. Все методы интерфейса объявляются как чисто виртуальные методы в операторе struct из C++. Рассмотрим следующее определение C++ для интерфейса ICar:
interface ICar
void SetSpeed( long nSpeed );
1;
Часть V
Это определение транслируется в СОМ в следующее:
struct ICar (
virtual void SetSpeed( long nSpeed ) = 0;
}
Поскольку для структур C++ все методы и элементы общедоступны, достаточно применить простую команду препроцессора. Если же используется класс, то необходимо добавить ключевое слово public. Подобие между интерфейсами и классами C++ создается намеренно: это следует из того факта, что практически все компиляторы C++ генерируют одну и ту же таблицу виртуальных методов (VTBL) — в этом особом случае класс содержит только чисто виртуальные методы, a VTBL представляет собой двоичное определение интерфейса. Для полноты картины все методы должны соответствовать соглашению stdcall о вызовах для достижения двоичной совместимости с другими языками, например, с Pascal и Fortran (предшествующая трансляция должна включать _stdcall после void или какое-либо подобное ключевое слово, в зависимости от компилятора).
Интерфейсы как классы C++ подчиняются следующим ограничениям:
Не обеспечивают реализацию, а являются лишь чисто виртуальными методами.
Не могут содержать элементы данных.
Могут быть получены из других интерфейсов, но производные могут добавлять только новые, чисто виртуальные методы; разрешается только одиночное наследование>
Ограничение элементов данных существенно для архитектуры СОМ. Данными нельзя управлять непосредственно; вместо этого клиент должен соответствовать имеющимся интерфейсам. Необходимо сохранять двоичную и языковую независимость клиента от объекта и поддерживать основу для выполнения контекста путем независимого переключения, как будет показано далее в этой главе. Ограничение одиночного наследования необходимо, поскольку нет какого-либо стандарта для двоичного размещения базовых классов VTBL при множественном наследовании. При наличии этого ограничения можно избежать осложнений, связанных с виртуальным наследованием. При обсуждении IUnknown далее в этой главе рассмотрим решение СОМ для множественного наследования.
Хотя VTBL является превосходным двоичным заместителем интерфейса, необходимо иметь некоторые основания для исходного замещения (т.е. некоторый способ, которым C++ или другой язык может именовать интерфейс в исходном программном коде). Один из путей разрешения этой проблемы состоит в использовании текстового имени интерфейса, поскольку оно введено в определение. Этот подход приемлем для исходных данных в C++, но если рассмотреть его с точки зрения целей СОМ, он обладает рядом недостатков: не будучи представленным в двоичной форме, он не может быть воспринят другими языками и не является уникальным (два разработчика могут дать своим интерфейсам одно и то же имя). Решение представлено в виде двоичного идентификатора, называемого идентификатором интерфейса (IID). IID представляют собой разновидность GUID или UUID — 16-байтовых массивов, которые гарантированно будут уникальными для того алгоритма, который используется для их генерирования. Globally Unique Identifier (GUID — Глобальный уникальный идентификатор) — такое имя дала компания Microsoft для Universally Unigue Identifier (UUID — Универсальный уникальный идентификатор). Рассматриваемый объект определен консорциумом Open Software Foundation Distributed Computing Environment (OSFDCE — Открытый фонд по распределенным компьютерным средам). IID является GUID, который присваивается в качестве имени интерфейса. Функция COM CoCreateGuid() генерирует новый GUID всякий раз, когда непосредственно вызывается. Конечно, редко возникает необходимость в непосредственном вызове этой функции. Некоторые инструментальные средства типа guidgen.exe компании Microsoft поддерживают интерфейс пользователя для распределения GUID. Идентификатор GUID необходим лишь однажды — при разработке интерфейса. Далее в этой главе будет показано, как использовать IID для интерфейса IUnknown.
В предыдущем примере интерфейс описывался произвольно с помощью синтаксиса, подобного синтаксису C++, и с нестандартным ключевым словом interface. В действительности интерфейсы описываются с помощью языка программирования, и при этом нет ничего удивительного в том, что язык этот называется Interface Definition Language (IDL — Язык определения интерфейсов). IDL — это часть OSFDCE Remote Procedure Call (RPC — Вызов удаленных процедур). IDL используется для того, чтобы сформулировать ясное определение для вызовов RPC с помощью сетевых транспортов и генерировать соответствующий сетевой удаленный программный код. Специалисты Microsoft добавили некоторые расширения для специальных сообщений СОМ. В результате COM-интерфейсы могут применяться удаленно с помощью RPC (это значит, что как для процессов, так и для компьютеров эта тема не является предметом рассмотрения данной главы).
Глава 21
Синтаксис iDL подобен синтаксису объявлений C++ с некоторыми дополнительными аннотациями. Ниже приводится вариант определения интерфейса ICar, написанного на языке IDL:
[object, uuid(C21U0200-2FBf.-lld2-8952-444553540000) ]
interface ICar
{
void SetSpeed' [in] long nSpeed ) ;
}-•
Ключевое слово object указывает на то, что это COM-интерфейс (а не RPC-интерфейс), uuid() предоставляет IID-интерфейс, ip. обозначает параметр, используемый в качестве входного. Имеются инструментальные средства, которые обращаются к определению файла IDL, описывающему интерфейс и создающему header-файл, совместимый г С/С ++. Этот файл используется при выполнении интерфейса. Эти инструментальные средства также генерируют программный код прокси/заглушки для удаленного интерфейса и файл, который определяет' t i D (GUID представлены как структуры и должны быть распределены). Множественные интерфейсы могут быть определены в одиночном файле IDE.
Компиляции нс состоится, если отдельный интерфейс будет определен таким образом. Если использовать компилятор Microsoft IDE (MIDL), то в результате выдаются две ошибки. Первая ошибка вытекает из того факта, чт. СОМ-интерфейсы должны наследоваться из IUnknown, как будет отмечено далее в этой главе. Вторая ошибка имеет отношение к тому факту, что все методы в интерфейсах СОМ должны возвращать HRESULT. Запишем пример иным образом, чтобы он мст восприниматься MIDL:
import "unknwn.idl"
[object, uuid(C21D0200-2F36-lld2-8952-444553540000) ] interface ICar ; IUnknown {
HRESULT SetSpeed( [in] long nSpeed );
I;
Возникающий в результате header-файл содержит среди всего прочего следующее определение интерфейса C+ + :
interface
DECLSPEC_UUID("C21D0200-2FB6-lld2-8952-444553540C00")
ICar : public Unknown {
public:
virtual HRESULT STDMETHODCALLTYPE
SetSpeed( /* [inJ */ long nSpeed ) = 0;
);
Поскольку interface определяется как класс в заголовке СОМ, то в записи появляется public. Параметр STDMETHODCALL ГУРЕ усиливает соглашение о вызовах stdcall. Параметр DECLSPEC_UUID можно игнорировать, поскольку это расширение C++. представленное Microsoft в Visual C++ 5.0. Интерфейс IUnknown наследуется общедоступным способом, поскольку основной интерфейс является частью VTBL, и это должно быть заметно. Возникающий в результате класс C++ содержит только чисто виртуальные и общедоступные методы.
С помощью HRESULT представлены 32-разрядные целые числа, которые описывают состояние вызова метода. Имя подразумевает, что это будет читаться как дескриптор для результата, которого не было. (Ранее предполагалось, что HRESULT будет дескриптором, и имя было установлено: позже было замечено, что 32 разряда вполне достаточно для непосредственного описания результата.) СОМ предписывает всем методам возвращать HRESULT, поскольку необходимо сообщать о сетевых отказах вызывающему оператору универсальным способом. HRESULT состоит из следующих разрядных полей:
SRRFFFFFFFFFFFFFCCCCCCCCCCCCCCCC
R (2 бита)F (11 битов)С (16 битов)R (2 бита)F (11 битов)С (16 битов)S (I бит)Важный код: SEVERITY_SUCCESS или SEVERITY_FAIL
R (2 бита)
F (11 битов)
С (16 битов)
R (2 бита)
F (11 битов)
С (16 битов)
Резервный, должен быть нуль Возможность
Код состояния
Код состояния непосредственно занимает первые 16 битов HRESULT. Затем располагается указание на то, какая подсистема возвратила программный код. Все пользовательские коды привязаны к FACILITY_ITF
Часть V
(4), что определяет специфику интерфейса. Наиболее важный бит представлен битом важности: SEVERITY_SUCCESS (0) означает, что действие произошло успешно, SEVERITY_FAIL (1) означает, что операция завершилась неудачно. Два макроса FAILED (hr) и SUCCEEDED (hr) проверяют, успешен ли возвращенный результат HRESULT hr.
Символические имена, соответствующие определенному HRESULT, также состоят из трех частей, которые расположены в следующем порядке: возможность, важность и описание кода состояния. Довольно часто при использовании HRESULT опускают часть, соответствующую возможности. Примерами такого пропуска могут служить CO_E_NOTINITIALIZED, DRAGDROP_S_DROP, E_OUTOFMEMORY, E_FAIL, S_OK и S_FALSE. Следует использовать стандарт HRESULT там, где это необходимо. Параметр HRESULT должен использоваться при формировании сообщений об исключениях, поскольку исключениям не позволяется выходить через границы интерфейса (они представляют собой реализацию, специфическую для C++ либо являются полностью различными, либо не допускают реализаций в других языках). Исключения, которые выходят за границы интерфейса, вынуждают RPC возвращать RPC_E_SERVERFAULT (0x80010105), если интерфейс является удаленным. Если объект находится в процессе, то исключение, вероятно, разрушит клиент.
Интерфейсы могут наследоваться. Это кажется естественным, поскольку они — всего лишь классы C++. Однако в этом аспекте имеется существенная разница. Поскольку интерфейсы состоят исключительно из чисто виртуальных методов, полученный интерфейс содержит все методы основного интерфейса. Это означает, что объект, который осуществляет полученный интерфейс, должен также выявить основной интерфейс (он реализуется в любом случае). Если же два или более интерфейса получены из некоторой основы, их связывают с помощью интерфейса и все они реализованы на одном объекте, то методы основного интерфейса могут иметь различные реализации (иногда это желательно). В общем, нежелательно использовать наследование интерфейса, за исключением особых обстоятельствах. СОМ предлагает лучший способ достижения полиморфизма — метод Querylnterface() интерфейса IUnknown (будет рассмотрен в следующем разделе). Наследование интерфейса необходимо только в том случае, если интерфейс не может существовать без реализации методов основного интерфейса. В качестве примера можно рассмотреть IUnknown, который должен быть основным интерфейсом для всех интерфейсов СОМ.
В листинге 21.1 показан IDL, используемый в примерах этой главы. Некоторые подробности этого программного кода станут ясны при дальнейшем рассмотрении главы, при обсуждении библиотек типов.
Листинг 21.1. Пример IDL для Car Object и его интерфейсов import "unknwn.idl" ( object, uuid(C21D0200-2FB6-lld2-8952-444553540000), helpstring("Car driving") 1 interface ICar : IUnknown { HRESULT SetSpeed( [in] long nSpeed ); 1; [ object, uuid(C21D0200-2FB6-lld2-8952-444553540000),.
helpstring("Engine control") interface lEngine : IUnknown { HRESULT Start(); HRESULT Stop(); ); [ uuid(310C97F4-3ABE-lld2-915E-52544C004D83), version(1.0), helpstring("Car library 1.0") 1 library YourLib {
importlib "stdole2.tlb"
Глава 21
importlib "stdole32.tlb" [
uuid(310C97D0-3ABE-lld2-915E-52544C004D83), helpstring("Car class") ] coclass Car {
[default] interface ICar;
interface lEngine;
Интерфейс IUnknown
Все интерфейсы COM должны наследоваться из IUnknown. Их определение таково:
[
local, object, uuid(00000000-0000-0000-С000-000000000046), pointer_default(unique)
]
interface IUnknown (
HRESULT Queryinterface( [in] REFIID riid, [out, iid_is(riid)] void **ppvObject ) ;
ULONG AddRef( void );
ULONG Release( void );
};
Ключевое слово local означает, что интерфейс не является удаленным (т.е. никакой упорядоченный код не должен быть сгенерирован компилятором 1DL). Это кажется странным, потому что интерфейсы СОМ вообще являются удаленными (т.е. они могут вызываться через границы контекста выполнения). Интерфейс IUnknown является удаленным. Удаленная схема полагается на другой интерфейс (IRemUnknown), используемый внутренним образом, который позволяет выполнять некую оптимизацию вызовов. Указатель Pointer_default (unique) инструктирует упорядоченный код о том, что указатели NULL могут быть возвращены вызовами методов. I1D инструктирует упорядоченный код о том, что необходимо обработать указатель как указатель интерфейса и именовать его. Здесь IID впервые вступает в игру. REFIID является IID и единственным не С-совместимым элементом, используемым в методах интерфейса. При этом используется тот факт, что компиляторы C++ передают ссылочные параметры как указатели. Учитывая эти дополнения, исследуем методы интерфейса IUnknown.
Метод Querylnterface() используется для опроса объекта в результате которого выясняется, поддерживает ли он специфический интерфейс, получивший имя с его IID. Результат возвращается во втором параметре. Поскольку Querylnterface() возвращает общий указатель интерфейса, недопустимо определение, сохраняющее типы. Однако возможно, по крайней мере, возвращать IUnknown **. Вызывающий оператор передает адрес указателя интерфейса типа, именованный с помощью параметра riid (при этом необходимо ссылаться на корректную VTBL для возвращаемого интерфейса). Метод Querylnterface() должен возвратить S_OK при успешной реализации или E_NOINTERFACE, если рассматриваемый интерфейс не реализован с объектом (в этом случае выходному параметру ppv присваивается значение NULL). Все это напоминает оператор dynamiccast из C++ (который можно принять в качестве модели). Метод Querylnterface() предлагает клиенту способ выполнения запроса функциональных возможностей объекта во время выполнения. Ниже будет показано, что к поведению Querylnterface() предъявляются строгие требования.
Методы AddRef() и Release() используются для управления указателем интерфейса во время его существования путем подсчета ссылок. Изначально каждый указатель интерфейса имеет ссылку 1 (которая является первым требованием для Querylnterface() — в случае успеха вызывается AddRef() для возвращенного указателя интерфейса). Когда клиент заканчивает использование интерфейса, для указателя вызывается метод Release(). Каждый раз, когда указатель интерфейса дублируется (сохраняется в другой переменной или в поле структуры или в другом месте), метод AddRef() должен вызываться для увеличения числа подсчитанных ссылок. Перед разрушением копии (например, при освобождении кучи или когда локальная перемен-
Часть V
ная выходит за пределы диапазона) должен вызваться метод Release(). Реализация метода Release() должна позволить проверить, когда подсчет ссылок достигает нуля, чтобы освободить любые ресурсы, требуемые интерфейсом.
ПРИМЕЧАНИЕ
Каждый интерфейс объекта управляет собственным подсчетом ссылок. Фактическая реализация освобождается для выполнения единственного подсчета ссылок, а следовательно, освобождаются и все интерфейсы для объекта. Однако если количество ссылок для всех интерфейсов объектов достигает нулевого значения, объект самоуничтожается. (Ко- нечно, количество ссылок для любого интерфейса не должно быть меньше нуля, иначе клиент сгенерирует ошибку.) 4.44
Поскольку все интерфейсы наследуются от IUnknown, они имеют метод Querylnterface() для своих VTBL. Объекты обычно заполняют VTBL для всех интерфейсов с помощью единственной реализации Querylnterface(), хотя это и не обязательно. Все реализации Querylnterface() для объектов должны соответствовать следующим правилам (все рассматриваемые интерфейсы соответствуют одному объекту):
Querylnterface() для IUnknown всегда успешно выполняется и возвращает один и тот же указатель на интерфейс, независимо от интерфейса, который вызывался.
IUnknown — это базовый интерфейс для любого другого интерфейса; обычно один интерфейс выбирается с тем, чтобы быть возвращенным, и никакой специальной VTBL для IUnknown не назначается. Это правило гарантирует, что IUnknown уникален для объекта и может использоваться как объектная сущность. Когда возникают сомнения относительно того, указывают ли два интерфейса на один и тот же объект, можно использовать Querylnterface() для IUnknown на обоих интерфейсах и сравнивать генерируемые в результате указатели.
Если Querylnterface() для интерфейса IX успешно выполняется один раз, то он должен успешно выполняться во время жизни объекта независимо от интерфейса, для которого используется.
Это правило гарантирует стабильность реализации объекта и поддерживает для клиентов некоторую степень доверия так, что они не должны хранить указатели интерфейса, если только получают их: клиент гарантированно получает указатели на интерфейс, если это потребуется в будущем. Конечно, клиент должен включать, по крайней мере, один указатель интерфейса, если требуется хранить объект в активном виде.
Если Querylnterface() для интерфейса IY успешно выполняется на интерфейсе IX, то Querylnterface() для интерфейса IX также успешно выполняется на интерфейсе IY.
Querylnterface() для интерфейса IX, вызываемый интерфейсом IX, должен всегда успешно выполняться.
Если Querylnterface() для интерфейса IY успешно выполняется на интерфейсе IX, и Querylnterface() для интерфейса IZ успешно выполняется на интерфейсе IY, то Querylnterface() для интерфейса IX успешно выполняется на интерфейсе IZ.
Эти три правила гарантируют безопасность при управлении с помощью объектных интерфейсов и гарантируют, что все интерфейсы будут достижимы отовсюду.
Ниже находится пример простой реализации Querylnterface() для объекта, который предлагает два интерфейса, lEngine и 1Саг, путем множественного наследования:
STDMETHODIMP CCar::QueryInterfасе( REFIID riid, void **ppv )
HRESULT hr = S_OK;
// Обратите внимание, что lEngine выделен для запросов IUnknown if ( IsEqualIID( riid, IID_IUnknown ) ) (
*ppv = (void*)static_cast<IEngine*>( this ) ;
) else if ( IsEqualIID( riid, IID_IEngine ) ) (
*ppv = (void*)static_cast<IEngine*>( this ) ;
) else if ( IsEqualIID( riid, IID_ICar ) ) {
*ppv = (void*) static_cast<ICar*> ( this );
) else (
hr = E_NOINTERFACE;
*ppv = NULL ;
)
// Обратите внимание, что вызывается AddRef для возвращенного указателя if ( SUCCEEDED ( hr ) ) {
reinterpret_cast<IUnknown*>( *ppv )->AddRef(); )
return hr;
}
Важный момент — это обработка запроса IUnknown и заключительный вызов метода AddRef(). Объект назначает интерфейс lEngine как сущность и возвращает его, когда выполняется запрос для lUnknow. Перед возвратом указателя интерфейса метод AddRef() обращается к возвращенному указателю интерфейса. Этот процесс выполняется согласно правилу, когда каждый интерфейс поддерживает собственный подсчет ссылок.
ПРИМЕЧАНИЕ
Реализации, подобные описанным, редко используются. Если имеются несколько интерфейсов, используемых объектом, размеры программного кода увеличиваются. Подход, предполагающий использование таблиц, является более предпочтительным. Однако рассмотренная реализация является хорошим примером.
СОМ-объекты
До сих пор механизм использования и управления с помощью интерфейсов иллюстрировался для единственного объекта, но клиент не получал начальный указатель lUnknow на объект.
Существует четыре различных способа получения начального указателя интерфейса для объекта:
Через обобщенные COM-функции создания, подобные CoGetClassObject(), CoCreatelnstance() и др.
Через метод интерфейса, осуществляющий вызов другого объекта, возвращающего интерфейс для нового объекта.
Когда клиент объекта передает указатель интерфейса другого объекта первому объекту, используя вызов метода интерфейса. Таким образом первый объект получает указатель на новый объект в целях внутреннего использования — первый объект является клиентом.
Через другую функцию API, которая генерирует специфический объект и возвращает один из интерфейсов вызывающему оператору. В качестве примера может служить CreateStreamOnHGlobal(), который создает стандартный OLE-объект, представляющий поток памяти и возвращающий интерфейс IStream.
Последний метод используется редко. Он был актуальным в эпоху начала развития OLE (которая базировалась на Win32 API). Второй метод представляет объектную навигацию в иерархии. Третий метод представляет объекты согласно способу, с помощью которого они взаимодействуют с клиентами, и кратко описывается в разделе "Другие COM-технологии" далее в этой главе.
Первый метод — это наиболее широко распространенный способ распределения COM-компонентов. Он работает с коклассами. Каждый кокласс имеет связанный объект, называемый объектом класса или фабрикой класса. Хотя и не в обязательном порядке, но этот объект предлагает интерфейс IClassFactory:
[ object,■
uuid(00000001-0000-0000-С000-000000000046), pointer_default(unique) J interface IClassFactory : IUnknown { HRESULT Createlnstance( [in, unique] IUnknown *pUnkOuter, [in] REFIID riid, [out, iid_is(riid)] void **ppvObject ); HRESULT LockServer( [in] BOOL bLock ); );
Метод LockServer() используется, чтобы предотвратить выгрузку сервера в то время, когда нет активных объектов. При этом объекты реализованы на сервере. Таким образом, запросы на создание генерируются позже и обслуживаются значительно быстрее. Все вызовы LockServer (TRUE) должны быть согласованы
Часть V
с соответствующим числом вызовов LockServer (FALSE) (в качестве истинного значения с AddRef() и Release() для интерфейсов). Перейдем к рассмотрению важного метода — Createlnstance().
Createlnstance() использует три параметра. Последние два из них передаются Querylnterface() и дают возможность клиенту непосредственно получить указатель для нужного интерфейса, причем не только ILnknow. Первый параметр — это новый lUnknow, управляющий объектом, который используется при агрегировании, как описано далее. Для обычных клиентов он всегда является NULL. Этот метод хорошо знаком с объектом, который должен создать.
Во время выполнения СОМ используются механизмы размещения фабрик класса, которые здесь не будут обсуждаться. Функция СОМ, которая делает это, — CoGetClassObject():
STDAPI CoGetClassObject(
REFCLSIDrclsid,
DWORDdwClsContext,
COSERVERINFO *pServerInfo,
REFIIDriid,
void**ppv ) ;
Последние два параметра — это те параметры, которые передаются методу Querylnterface() фабрики класса и представляют непосредственно запрошенный интерфейс из фабрики класса (обычно IClassFactory). Параметр pServerlnfo используется для описания компьютера и контекста защиты вызова. На одном и том же компьютере, использующем заданную по умолчанию защиту, pServerlnfo может иметь значение NULL. С помощью dwClsContext вызывающий субъект определяет, что нужно сделать, чтобы объект был создан.
Наиболее важным является первый параметр: REFCLSID является CLSID&, и CLSID — GU1D (это иллюстрация того, что мы столкнулись с большим количеством GUID). Каждый кокласс имеет CLSID, связанный с ним (каждый интерфейс, именованный с помощью IID). Таким образом, любые два кокласса могут в достаточной степени различаться. Следовательно, rclsid благополучно вызывает запрошенный кокласс. Это CLSID кокласса, который производит фабрика класса (фабрики класса не имеют CLSID). Если эта функция успешно выполняется, то вызывающий оператор удерживает указатель на фабрику класса для кокласса. представляющую интерес, и может создавать столько экземпляров, сколько нужно (в качестве интерфейса обычно используется IClassFactory). Когда работа с фабрикой класса завершается, клиент должен вызвать метод Release!) (как правило).
ПРИМЕЧАНИЕ
COM-объект может быть активизирован в трех различных контекстах класса: "в процессе", локально и удаленно. Контекст "в процессе" означает, что объект создан в адресном пространстве вызывающего процесса. Объектный сервер л размещается в DLL. На локальные и удаленные активизации ссылаются как на активизации внешних процессов. Объектный сервер постоянно находится в отдельной выполняемой программе, и объект создается в другом процессе. При этом определяется, находится ли постоянно объектный сервер на том же самом компьютере или на другом.
Если фабрика класса отображает IClassFactory (рекомендуется), библиотека СОМ времени выполнения поддерживает обертку, необходимую для создания единственного экземпляра кокласса:
STDAPI CoCreatelnstance( REFCLSID rclsid, IUnknown *pUnkOuter, DWORDdwClsContext,
REFIID riid,"
void**ppv ) ;
Эго несколько устаревшая функция, поскольку она не поддерживает параметр COSERVERINFO*. Однако для нашего обсуждения эта функция вполне удовлетворительна, rclsid и dwClsContext передаются CoGetClassObject() с тем, чтобы получить фабрику класса объекта (передаваемый 11D — IID_IClassFactory). Если фабрика класса получена, вызывается IClassFactory::CreateInstance(), передавая остающиеся три параметра. После этого фабрика класса освобождается. Выходной параметр содержит запрошенный указатель интерфейса, который был успешно вызван.
Технология СОМ имеет все характеристики, необходимые для того, чтобы рассматривать ее в качестве объектно-ориентированной системы: инкапсуляция, полиморфизм и возможность многократного использования. Отсутствует единственная возможность — повторное использование реализации (также называемая возможностью многократного использования). Для решения этой проблемы используется методика, называемая агрегацией. Благодаря агрегации один объект может непосредственно выставлять реализацию не-
Глава 21
скольких интерфейсов для другого объекта. Все кажется очень простым, пока не проявляются требования для IUnknown, и они не могут быть удовлетворены. Проблема заключается в том, что, когда мы получаем прямой указатель на агрегированный объект, это вовсе не означает, что он будет агрегирован. При этом не могут возвращаться интерфейсы для агрегированного объекта, когда клиент выполняет запрос с помощью одного из интерфейсов агрегированного объекта. Единственное решение заключается в том, чтобы объект получил информацию об агрегировании. После этого он может реагировать на вызовы Querylnterface(), делегируя их управляющему объекту реализации IUnknown. Кокласс имеет две реализации интерфейса IUnknown. Первая реализация используется, когда объект не агрегирован, и возвращается фабрикой класса в случае агрегирования (например, когда агрегирующий параметр pUnkOuter IClassFactory::CreateInstance() не NULL. Запрошенный IID должен быть IID_IUnknow). Вторая реализация разделяется всеми интерфейсами и делегируется первой реализации (если нет агрегирования) или контролирующему IUnknown. Очень важно обращать внимание на то, что агрегирование разрешается только тогда, когда клиент и объект постоянно находятся в одном и том же апартаменте (т.е. не используются прокси и заглушки). Другая методика для двоичного многократного использования, называемая ограничением, может использоваться во всех случаях. При использовании ограничения внешний объект реализует все желательные интерфейсы: для выполнения работы с некоторыми интерфейсами просто делегируются все обращения другому объекту. Ограничение образуется при создании объекта и освобождается при его разрушении. Включенный объект не обращает внимания на то, где он содержится (он не может делать этого).
Библиотеки типов
В программах, написанных на языках C++, IDL-компилятор обрабатывает ID описаний интерфейсов и генерирует header-файл с собственными определениями C++. Компилятор также генерирует файл, который хранит GUID для интерфейсов и коклассов. Это все хорошо подходит для C++; и читатели этой книги могут подумать, что это все, что необходимо. Однако вы, наверное, знаете, что это не совсем верно. Другие языки не понимают собственных заголовков C++ и нуждаются в другом способе описания интерфейсов и коклассов. Двоичный аналог заголовка C++ называется библиотекой типов.
Библиотека типов содержит намного больше информации, чем обычный заголовок C++. IDL включает в качестве подмножества другой язык (разработанный Microsoft), который называется языком описания объектов (DDL — Object Description Language).Обсуждение ODL находится вне контекста этой главы.Примите к сведению то, что IDL может включать определение библиотеки типа:
t
uuid(98178CD0-3467-lld2-914B-52544C004D83), version (1.0) , helpstring("This is type library")
1
library YourLib I
};
Обычно кокласс описывается в библиотеке типов:
[
uuid(55712EB0-3468-lld2-914B-52544C004D83), helpstring("This is coclass")
coclass YourClass (
[default] interface IMain; interface ISecond;
);
В дополнение к коклассам это определение влечет за собой определение двух интерфейсов: IMain и ISecond. Библиотека типов может также содержать определения типов, счетчики, структуры С, объединения и др.
Библиотека типов генерируется компилятором IDL или отдельным инструментальным средством. В результате создается двоичный файл, который может распределяться вместо исходного IDL. Затем пользователь применяет некоторый инструмент декодирования для преобразования библиотеки типов в собственный языковой формат. Эта тема будет рассмотрена далее в этой главе.
Технология СОМ определяет два интерфейса для непосредственного чтения содержимого библиотеки типов: ITypeLib и ITypelnfo. Хотя маловероятно, что обычная программа C++ использует библиотеку типов
18 Зак. 53
Часть V
непосредственно, можно выполнить ее обработку и сгенерировать соответствующие вызовы методов во время выполнения или соответствующие VTBL для выходных интерфейсов объекта (эта тема кратко рассматривается в следующем разделе).
Другие СОМ-технологии
В следующих разделах производится обзор некоторых технологий, основанных на использовании СОМ. Эти темы описываются очень кратко, поскольку эти технологии широко используются в большинстве СОМ- приложений. Многие завершенные СОМ-технологии формируются на этой базе.
Управление памятью
Если клиент и объект находятся в одном апартаменте, клиентские вызовы методов интерфейса используют непосредственный указатель таким образом, чтобы можно было осуществлять безопасную передачу указателей. Если подключаются прокси и заглушка, то это часто бывает необходимо для размещения данных, указывающих на параметр, находящийся между клиентом и объектом. Это делается для того, чтобк удовлетворить ожидание объекта на получение реальных данных в указанном месте. СОМ предоставляет разрешение параметрам [in] на поддержку со стороны клиента. Когда объект должен вернуть данные в параметре [out] метода интерфейса, он распределяет данные и клиент выполняет очистку. Для сохранения двоичной независимости необходим некий универсальный механизм для управления памятью. При этом используется интерфейс IMalloc:
I local, object, uuid(00000002-0000-0000-C000-000000000046) 1 interface IMalloc : IUnknown ( void *Alloc ( [in] ULONG cb ); void *Realloc ( [in] void *pv, [in] ULONG cb ) ; void Free ( [in] void *pv ) ; ULONG GetSize( [in] void *pv ); int DidAlloc ( void *pv ) ; void HeapMinimize () ;
Первые три метода осуществляют непосредственное обращение к функциям времени выполнения С malloc(), realloc() и free(). Интерфейс IMalloc реализуется на объекте, если СОМ использует функцию CoGetMalloc(). В целях обеспечения удобства работы программиста в СОМ поддерживаются три функции обертки, позволяющие обратиться к первым трем методам IMalloc — CoTaskMemAlloc(), CoTaskMemRealloc() и CoTaskMemFree(). Объектам предоставляется право распределения всех возвращенных данных, использующих IMalloc. При этом данные освобождаются с помощью заглушки; когда заглушка достигает прокси, она распределяется снова через IMalloc прокси. Затем клиент обрабатывает возвращенные данные и освобождает копию также с помощью IMalloc.
Подключаемые объекты
Объекты могут возвращать результаты клиенту при каждом вызове метода. Однако некоторые объекты могут пересылать информацию клиентам при возникновении определенных ситуаций — независимо от намерения клиента выполнить вызовы интерфейса. Одно возможное решение заключается в том, что, если клиент передает указатель интерфейса на объект, объект удерживает указатель (т.е. вызывает метод AddRef()) и использует его, когда необходимо. Если же клиент не хочет больше получать информацию о событиях, он останавливает этот процесс, вызывая тот же самый метод и передавая указатель NULL, который фактически разрывает соединение. Объект клиента, реализующий интерфейс, называется стоком. Интерфейс называется выходом объекта или источником, поскольку объект не реализует интерфейс, а просто использует его. Соединение, установленное между стоком клиента и объектом, называется консультативным.
Этот подход использовался для интерфейса lAdviseSink. При использовании подобного подхода возникают некоторые неудобства. Только один клиент может получать уведомления — удовлетворительная ситуация, если объект совместно используется некоторыми контекстами. Если объект возбуждает несколько наборов событий, то несколько методов могут быть выделены для прохождения стоков клиента. В связи с
Глава 21 тем что многим объектам придется возбуждать события, несколько интерфейсов будут совместно использовать один и тот же консультативный метод.
Для решения этих проблем определяются два главных и два вспомогательных интерфейса.
Метод IConnectionPoint обслуживает соединения одиночного выходного интерфейса для объекта. Этот метод не реализуется на основном объекте (Querylnteface() не обнаруживает его). Вместо этого каждая точка соединения выполняется на маленьком отдельном объекте. Для обращения к точкам соединения объект реализует интерфейс IConnectionPointContainer: [ object, uuid(B196B286-BAB4-101A-B69C-00AA00341D07), pointer_default(unique) ] interface IConnectionPoint : IUnknown { HRESULT GetConnectionlnterface( [out] IID * piid ); HRESULT GetConnectionPointContainer( [out] IConnectionPointContainer ** ppCPC ) ; HRESULT Advise( [in] IUnknown *pUnkSink, [out] DWORD *pdwCookie ); HRESULT Unadvise( [in] DWORD dwCookie ); HRESULT EnumConnections( [out] lEnumConnections **ppEnum ); } [ object, uuid(B196B284-BAB4-101A-B69C-00AA00341D07), pointer_default(unique) ] interface IConnectionPointContainer : IUnknown ( HRESULT EnumConnectionPoints( [out] lEnumConnectionPoints ** ppEnum ) ; HRESULT FindConnectionPoint( [in] REFIID riid, [out] IConnectionPoint ** ppCP ) ; }
Используя методы в интерфейсе IConnectionPointContainer, клиент может перечислять поддерживаемые выходные интерфейсы и присоединять стоки для тех точек соединения, которые его распознают, или могут производить непосредственный опрос об указанном выходном интерфейсе. Используя эти два способа, клиент завершает работу указателем на IConnectionPoint для объекта, который распознает указанный исходный интерфейс. Используя метод IConnectionPoint::Advise(), клиент присоединяет сток и получает дескриптор для консультативного соединения, называемого cookie. Позднее cookie используется для вызова IConnectionPoint::Unadvise(), чтобы прервать соединение. Существует метод перечисления стоков, подключенных к точке соединения.
Перечислители
Два интерфейса, используемые для перечисления точек соединения и соединений для указанной точки, — это часть семейства интерфейсов, называемых перечислителями. Общее определение перечислителя будет следующим:
interface lEnumXXX ( HRESULT Next( [in] ULONG nCount, [out, size_is(nCount), length_is(*pnFetched)] XXX *pXXX, [out] ULONG *pnFetched ) ; HRESULT Skip( [in] ULONG nCount ); HRESULT ResetO; HRESULT Clone( [out] lEnumXXX **ppEnum );
Часть V
Это несуществующий интерфейс. Для каждого типа, который перечисляется, определяется новый интерфейс, который использует четыре метода для частичного перечисления объектов. Метод Next() используется для выборки следующей части элементов. Метод Skip() выполняет игнорирование части элементов. Метод Reset() приводит к началу перечисления с самого начала, а С1опе() создает копию перечислителя объекта. В случае с точками соединения перечисляемые объекты являются указателями интерфейса. Они принимаются с ожидающей обработки ссылкой, поскольку клиент отвечает за освобождение каждого полученного указателя.
Структурная память и живучесть объектов
Если кокласс создается фабрикой класса, объект, как говорят, находится в неинициализированном состоянии. Одни объекты не нуждаются в дальнейшей инициализации и могут успешно действовать в этом состоянии. Другие объекты требуют инициализации из некоторого предварительно сохраненного состояния для собственной активизации. Эти объекты называются живучими. СОМ определяет два способа достижения живучести объектов: во-первых, обеспечивается стандартная модель памяти, называемая структурной памятью, а во-вторых, определяется модель живучести, которая может сопровождаться этими объектами.
Структурная память сформирована на базе двух интерфейсов: IStorage и IStream. СОМ во время выполнения включает готовую реализацию для обоих интерфейсов. Эти реализации помещаются в традиционном файле и в системной памяти. Другие реализации также могут быть выполнены (например, на записях базы данных и полях). Память — это коллекция потоков или других подсистем памяти типа каталога в традиционной файловой системе. Поток — это двоичная последовательность, аналогичная дисковому файлу. Поток управляется таким же способом, как файл операционной системы. Интерфейсы обеспечивают большую гибкость, чем файловая система, определяя операции транзакций и некоторые другие расширения.
Объекты соответствуют модели живучести СОМ и реализуют один или большее количество интерфейсов живучести: IPersistStorage, IPersistStream, IPersistStreamlinit или IPersistFile (другие интерфейсы живучести существуют, но являются более фундаментальными). Каждый интерфейс определяет специфическую модель живучести. IPersistStorage определяет постоянство на уровне памяти и предлагает наибольшую гибкость реализации, но при этом он является самым тяжелым в реализации. Объект может создавать потоки и дополнительные подсистемы памяти. Интерфейс IPersistStream определяет живучесть в одиночном потоке и является самым простым в реализации. Интерфейс IPersistStreamlnit идентичен IPersistStream, но содержит дополнительный метод для инициализации пустых объектов. Интерфейс IPersistFile используется для инициализации объекта из файла операционной системы. Все интерфейсы живучести являются порожденными от интерфейса IPersiet, который имеет единственный метод возврата объектного CLSID вызывающему оператору.
Автоматизация
В мире компонентов для среднего пользователя значительно упрощается выполнение некоторых общих задач путем использования макросов или сценариев на некотором языке высокого уровня. В целях облегчения этого процесса СОМ определяет стандарт для доступа к серверам объекта СОМ, называемый автоматизацией. Автоматизация определяет другой тип интерфейса, который будет называться интерфейсом диспетчеризации {dispinterface) (также на них иногда ссылаются как на интерфейсы автоматизации). Эти интерфейсы группируются вокруг объектов, реализующих интерфейс IDispatch:
[ object,.
uuid(00020400-0000-0000-C000-000000000046), pointer_default(unique)
J
interface IDidspatch : IUnknown
<
HRESULT GetTypelnfoCount( [out] UINT *pctinfo );
HRESULT GetTypelnfo(
[in] UINTiTInfo,
[in] LCIDIcid,
[out] ITypelnfo **ppTInfo );
HRESULT GetIDsOfNair.es ( [in] REFIID riid, [in, size_is(cNames)] LPOLESTR *rgszNames, [in] UINTcNames,
[in] LCIDIcid,
Глава 21
[out,
size_is(cNames)]
DISPID *rgDisp!d
HRESULT Invoke( [in] DISPID
dispIdMember,
[in]
REFIID
ri id,
[in]
LCID
laid,
[in]
WORD
wFlags,
[in,
out] DISPPARAMS <
'pDispParams,
[out]
VARIANT
♦pVarResult,
[out]
EXCEPINFO
♦pExcepInfo,
[out]
UINT
♦puArgErr );
};
Интерфейс диспетчеризации, в отличие от интерфейса СОМ, не включает представление размещения VTBL. Вместо этого все методы маркируются целочисленными идентификаторами, называемыми dispatch ID. В дополнение к методам эти интерфейсы вводят понятие свойств — абстракция для элементов данных структуры C++. Каждое свойство связано с методами get и put, которые осуществляют выборку и хранение значения, являющегося результатом двух различных методов, имеющих одинаковое значение dispatch ID. Методы (включая свойство accessors) вызываются в две стадии. Присваивая текстовые имена методам и их параметрам, вызывающий оператор получает соответствующий dispatch ID и подобные ID для параметра позиционирования, использующего IDispatch::GetIDsOfNames(). Затем выполняется вызов IDispatch::Invoke() со всеми параметрами, переданными в массиве pDispParams. Это процесс в целом является тяжелым в реализации. Результат возвращается в выходном параметре pVarResult. Вызов метода позволяет выполнить генерацию пересылающего исключения; это исключение сообщает о себе посредством pExcepInfo (это не исключение C++, а стандартный механизм для передачи обширной информации, касающейся исключений). Реализация IDispatch является весьма затруднительной. Существуют несколько функций, упрощающих реализацию. Они используют определение библиотеки типов интерфейса диспетчеризации. Для обращения к интерфейсу диспетчеризации из C++ необходим класс обертки для вызовов IDispatch::Invoke().
COM-интерфейсы используют раннюю схему связывания, поскольку клиент осуществляет привязку к интерфейсу VTBL во время компиляции. Этот процесс также называется связыванием VTBL. С другой стороны, Dispinterfaces предлагает схему позднего связывания, которая имеет две разновидности. Клиент знает о dispids заранее благодаря библиотеке типов и опускает вызовы IDispatch::GetIDsOfNames(). Этот подход используется при раннем связывании, поскольку клиент связывается с dispids во время компиляции. Конечно, связывание VTBL — это наиболее эффективная схема, но она не поддерживается всеми интерфейсами программирования: фактически она не поддерживается некоторыми широко распространенными языками написания сценариев (JavaScript и VBScript, используемыми при создании HTML-страниц в WWW). Для того чтобы интерфейс стал и эффективным, и широко доступным, была разработана общая схема. Интерфейс диспетчеризации был реализован как обычный интерфейс СОМ, происходящий от IDispatch. Реализация части IDispatch осуществляет вызовы только соответствующих методов в части VTBL интерфейса. Подобные интерфейсы называются двойными.
При автоматизации поддерживается только подмножество всех типов, доступных в IDL. Не поддерживаются наиболее важные составные типы данных типа структур, массивов и строк. Вместо этого автоматизация определяет новые типы: BSTR, SAFEARRAY и VARIANT. Структуры передаются с помощью реализации IDispatch для отдельного объекта, который отображает все поля, использующие свойства get и put. BSTR — это строка, содержащая информацию о длине непосредственно перед фактическим указателем на данные. SAFEARRAY — это многомерный массив, который фиксирует верхнюю и нижнюю границы для каждого из измерений.
Это довольно сложная структура. VARIANT — это структура, которая содержит объединение различных типов, доступных при автоматизации. Все три типа используют специальные функции сопровождения во время выполнения автоматизации.
Преимущество использования типов автоматизации заключается в том, что модуль времени выполнения имеет стандартную реализацию распределителя IDispatch, который может также использоваться для других интерфейсов. Единственное требование заключается в том, что эти интерфейсы могут использовать в своих методах только типы автоматизации и содержат ключевое слово oleautomation в описании интерфейса в IDL-файле. Двойные интерфейсы определяются по ключевому слову dual в IDL, которое также подразумевает использование oleautomation.
Использование COM-объектов в C++
Использование COM-объектов в C++ почти идентично использованию классов C++ посредством указателей. Различия начинаются лишь тогда, когда речь идет о времени жизни интерфейсов.
Часть V
Использование интерфейсов Raw
Создание объекта (который демонстрирует интерфейс) происходит в первую очередь. Существуют различия между классами C++ и интерфейсами. Каждый интерфейс реализован на некотором объекте. Объект также отображает другие интерфейсы. Для того чтобы получить другой интерфейс, клиент осуществляет запрос Queryinterface)), обращаясь к указателю интерфейса для получения указателя на другой интерфейс на том же самом объекте. Этот подход подобен реализации оператора dynamic_cast в языке C++. Другое различие состоит в механизме разрушения. Вместо использования оператора удаления клиент вызывает метод Release)), который передает инструкции объекту о его саморазрушении (некоторые классы C++ спроектированы таким же образом). Если объект находит, что это была последняя ожидающая обработки ссылка (для всех интерфейсов), он уничтожает себя. Ниже находится пример реализации этого механизма:
HRESULT hr;
ICar *pCar; lEngine *pEngine;
hr = Colntialize( NULL ); assert( SUCCEEDED( hr ) ) ; hr = CoCreatelnstance( CLSID_Car, NULL, CLSCTX_ALL, IID_ICar, (void**)SpCar ); if ( SUCCEEDED ( hr ) ) { hr = pCar->QueryInterface( IID_IEngine, (void**)SpEngine ); if ( SUCCEEDED ( hr ) ) ( pEngine->Start(); Drive( pCar ); pEngine->Stop(); pEngine->Release();
pCar->Release() ; } CoUnitialize();
Этот фрагмент программного кода инициализирует COM-библиотеку, создает объект Саг (Саг — это кокласс) и получает интерфейс ICar. Затем выдается запрос об интерфейсе lEngine, запускается двигатель (engine), вызывается некоторая функция для управления автомобилем и в конце останавливается двигатель. В заключение, когда и lEngine, и интерфейсы ICar освобождены, COM-библиотека также освобождается. Вторичное освобождение оставляет объект без ожидающих обработки ссылок для всех интерфейсов, вследствие чего объект самоуничтожается. Все это очень просто и не требует дальнейшего объяснения. В целях упрощения результаты, возвращенные метода, не проверяются. На практике же необходимо проверять эти результаты. Даже если гарантируется, что метод не потерпит неудачу, программный код мог бы претерпеть неудачу при реализации в силу разных обстоятельств (включая сетевые отказы).
Более интересным является то, что функция drive)) в соответствии с законами СОМ должна вызывать метод AddRefQ при получении указателя и Release)) — по окончании работы с ним. Ясно, что в этом нет необходимости, поскольку время существования параметра интерфейса указателя равно времени существования вызова функции и является подмножеством времени существования контекста вызова, который включает ссылку на интерфейс. Следовательно, во время выполнения функции интерфейс содержит положительную ссылку и объект не удалит себя. Благодаря этому может быть выполнена оптимизация и пара методов AddRef))/Release() может быть опущена. Во всех случаях, когда возможен выход за пределы логики программирования, переменная, которая получает копию интерфейса, имеет время существования, которое является подмножеством времени существования первоначальной переменной, хранящей ссылку интерфейса (время существования определяется временем инициализации, пока не вызывается заключительная функция Release))). При этом оптимизация разрешена. Если начальная переменная имеет более короткое время существования, то копия содержит недопустимый указатель и оптимизация не может быть выполнена. Обычно методы AddRef)) и Release)) очень просты в реализации, поэтому оптимизация необходима достаточно редко, за исключением тех случаев, когда быстродействие очень важно. При передаче локального указателя интерфейса другим функциям можно безболезненно опустить пару AddRef()/Release(). Если указатель интерфейса содержится в глобальной переменной и процесс имеет единственный поток выполнения, который использует эту переменную, то риска при этом также не возникает.
Глава 21
Использование интеллектуальных указателей
Время существования любого указателя интерфейса должно начинаться с AddRef)) — либо когда получена любая внешняя функция или метод, либо когда выполнена локальная копия (в явном виде). Время существования должно заканчиваться при вызове функции Release)). Можно легко забыть о необходимости вызова AddRef)) или Release)) или вызывать Release)) больше одного раза. Последствия этого часто бывают печальными, к тому же становятся затруднительными отладка и локализация ошибки. Поэтому лучше скрыть эти действия в классе обертки интерфейса. Этот класс в соответствующее время автоматически вызовет AddRef)) и Release)) для содержащего указатель интерфейса. Класс обертки в конце концов вызовет метод Release)) в деструкторе.
Класс этой категории — это интеллектуальный указатель класса. Интеллектуальный указатель для пользователя выглядит как и обычный указатель. Отличие заключается в возможности управления сроком службы содержащегося указателя интерфейса. Любой вызов метода, доступный через первоначальный указатель, доступен также через интеллектуальный указатель. Эта ситуация легко реализуется с помощью перекрытия оператора класса ->. Поскольку интеллектуальный указатель не может различать вызовы методов AddRef)) и Release)) через указатель, программист должен подчиняться некоторым правилам и не вызывать AddRef)) и Release)) непосредственно через содержащийся указатель. Вместо этого интеллектуальный указатель должен поддерживать методы AddRef)) и Release)), которые позволят программисту непосредственно управлять временем существования содержащегося указателя. В листинге 21.2 показан пример интеллектуального указателя класса.
Листинг 21.2. Простой интеллектуальный указатель класса
template<class Itf> class CSmartPtr
<
// Конструктор и деструктор public:
CSmartPtr() : m_pltf( null )
{
1
CSmartPtr ( Itf *pltf ) : m_pltf( null )
{'
Store( pltf );
}
-CSmartPtr()
(
// Освобождение указателя интерфейса, если он не пустой Release () ;
1
// Экстракторы public:
Н Получение базового указателя для вызовов методов
Itf* operator-:» ()
{
assert( m_pltf != null );
return m_pltf;
1
H Получение адреса основного указателя
Itf** operators() {
И Убедитесь, что предыдущий указатель освобожден
Release () ; return tm_pltf;
// Присваивание public:
Itf* operator=( Itf *pltf )
II Убедитесь, что предыдущий указатель освобожденII Убедитесь, что предыдущий указатель освобожден{
II Убедитесь, что предыдущий указатель освобожден
II Убедитесь, что предыдущий указатель освобожден
Распределенные вычисления Часть V
Release () ;
// Сохранение нового указателя
Store( pltf ); return m pltf; }~
// Тестирование контекста public:
bool operator! () {
return ( mjpltf == null ) ;
}
operator bool() {
return ( mjpltf != null ) ; )
// Реализация protected: void Release!) { if ( m_pltf != null ) { m_pItf->Release(); m_pltf = null;
} }
void Store( Itf *pltf ) (
// Сохранение нового указателя m_pltf = pltf;
H Call AddRef on the new pointer, check for null if ( m_pltf •= null ) { mjpItf->AddRef()•
} }
// Элементы реализации protected:
Itf *m_pltf;
Это самая простая форма интеллектуального указателя. Он обеспечивает присваивание из другого указателя того же самого типа и получение адреса содержащегося указателя для назначения необработанного указателя, возвращенного функцией или методом. При этом учитывается определение интеллектуального указателя, поскольку обработка AddRef() и Release() происходит автоматически. Кроме того, поддерживается оператор ->, необходимый для обеспечения доступа к методам в содержащемся указателе. Добавляются также операторы тестирования, определяющие, содержит ли класс указатель интерфейса. Конечно, при этом также может использоваться конструктор копии.
Проблема с интеллектуальным указателем, показанным в листинге 21.2, состоит в том, что он всегда нуждается в вызовах QuerylnterfaceO или CoCreatelnstanceQ (или некоторых других средств), необходимых для заполнения значений:
CSmartPtr<IMyInterfасе> pMylnterace;
HRESULThr;
hr = pSomeInterface->QueryInterface(
IID_IMyInterface, (void**)6pMyInterface );
Будет лучше, если интеллектуальный указатель также будет обрабатывать QuerylnterfaceO. Проблема заключается в том, что QuerylnterfaceO возвращает HRESULT в случае неудачи. Интеллектуальный указатель должен принять решение, как выйти из этой ситуации. Самое очевидное решение — использовать исключение, но при этом появляется проблема, связанная с тем, что семантика оператора = не подразумевает обработку катастрофического отказа. Лучше всего проигнорировать отказ и предоставить программисту воз-
Глава 21
можность проверки каждого присваивания с помощью operator! или оператора bool. Конечно, класс должен запоминать последний HRESULT, что позволит программисту идентифицировать причину отказа. В листинге 21.3 содержится предложенное добавление.
Листинг 21.3. Дополнения к интеллектуальному указателю класса /* в конструкторах */ CSmartPtr( IUnknown *pUnk ) : m_pltf( null ), m_hr( E_POINTER ) ( StoreUnk( pUnk ) ;
} /* в экстракторах */ operator IUnknown*() ( return m_pltf; } /* в присваивании */ Itf* operator=( IUnknown* pUnk ) { H Убедитесь, что предыдущий указатель освобожден Release () ; // Сохранение нового указателя StoreUnk( pUnk ) ; return m_pltf;
) /* при реализации */ void StoreUnk( IUnknown *pUnk ) { if ( pUnk ! = null ) (
m_hr = pUnk->QueryInterface( IID_##Itf, (void**)6m_pltf ) ; ) else ( m_hr = E_POINTER;
) }
/* в контексте тестирования */ HRESULT GetLastError () { return m_hr;
} /* в элементах реализации */ HRESULT m hr;
Другие конструкторы должны инициализировать элемент m_hr; другой оператор присвоения должен установить его. Оператор Store() заканчивается этой строкой:
m_hr = (m_pltf • = NULL ) ? S_OK : E_POINTER;
В целях непротиворечивости метод ReleaseO должен завершить оператор if следующей строкой:
m_hr = E_POINTER;
E_POINTER выделяется для того, чтобы указывать на ошибку, если содержащийся указатель будет иметь значение NULL.
Обратите внимание на то, что этот шаблон класса вызывает проблемы, когда выполняется образование для интерфейса IUnknown. Существуют два конструктора и два оператора присваивания, имеющие одни и те же параметры. Для разрешения этой неоднозначности должна быть исключена оптимизация AddRef(), и только для версии с IUnknown*, поскольку параметр должен оставаться. Шаблон может никогда не образовываться для IUnknown, однако он может быть приемлем.
Распределенные вычисления Часть V
Использование подобного механизма улучшило работу интеллектуального указателя. Фрагмент программного кода, описанный ранее, теперь выглядит следующим образом: HRESULT hr; CSmartPtr<ICar> pCar; CSmartPtr<IEngine> pEngine; hr = Colntialize( NULL ) ; assert( SUCCEEDED( hr ) ) ; hr = CoCreatelnstance( CLSID_Car, NULL, CLSCTX_ALL, IID_ICar, (void**)SpCar ) ; if ( (bool)pCar ) ( pEngine = pCar; H Queryinterface here if ( (bool)pEngine ) ( pEngine->Start(); Drive( pCar ); pEngine->Stop(); ) ) CoUninitialize();
Обратите внимание, что вызов CoCreatelnstance() не изменился. Это потенциально опасно, поскольку IID может быть неуместным. Для того чтобы избежать этой проблемы, можно добавить новый метод — Createlnstance(), как продемонстрировано в листинге 21.4.
Листинг 21.4. Интеллектуальный указатель обеспечивает создание объекта /* at assignment */ HRESULT Createlnstance( REFCLSID rclsid, DWORD dwClsContext = CLSCTX_ALL, IUnknown *pUnkOuter = NULL ) { Release() ; m_hr = ::CoCreatelnstance( rclsid, pUnkOuter, dwClsContext, IID_##Itf, (void**)6m_pltf ) ; return m_hr ; )
При использовании метода Createlnstance() фрагмент программного кода еще больше упрощается:
CSmartPtr<ICar> pCar;
CSmartPtr<IEngine> pEngine;
hr = Colntialize( NULL );
assert( SUCCEEDED( hr ) ) ;
pCar.Createlnstance( CLSID_Car ) ;
if ( (bool)pCar ) (
pEngine = pCar; // Здесь находится QueryInterface if ( (bool)pEngine ) {
pEngine->Start() ;
Drive( pCar );
pEngine->Stop();
}
}
CoUninitialize();
В этом фрагменте синтаксис, осуществляющий вызов метода Drive(), изменяется — передается интеллектуальный указатель вместо ожидаемого необработанного указателя. Конечно, программный код может
быть переписан для использования интеллектуальных указателей, но это не всегда возможно (например, если он был написан посторонним человеком и отсутствует текст исходного кода). Можно было бы сохранить параметр, используя Drive( *&рСаг ), но это выглядит неэстетично. Интеллектуальный указатель может быть достаточно хорош для представления экстрактора необработанного указателя интерфейса Itf*(). Действительно, интеллектуальные указатели поддерживают методы динамического присоединения и отсоединения необработанных указателей интерфейса.
Использование библиотек типов
Интеллектуальные указатели значительно упрощают поддержку времени существования указателей интерфейса СОМ. Правда, с их помощью невозможно удовлетворить потребность в непрерывной проверке результатов каждого обращения интерфейса к указателю. В целях упрощения во фрагменте программного кода, описанного в ходе дискуссии, посвященной интеллектуальным указателям, не проверяются результаты, возвращенные методами интерфейса. Однако в реальных программах это не может игнорироваться.
При обсуждении метода Querylnterface(), проведенном ранее, отмечалось, что семантика оператора назначения не позволяет использовать исключения. Это следовало из того, что, поскольку класс не должен вести себя подобно указателю, нельзя использовать исключения. Но так как был организован целый класс в целях генерирования исключений при возникновении ошибок, исключение в этом месте желательно. Конечно, исключения не могут генерироваться с помощью интеллектуальных указателей.
Для реализации семантики исключения должен быть разработан целый класс, обертывающий указатель. В листинге 21.5 определяется обертка для интерфейса lEngine.
Листинг 21.5. Простой класс обертки интерфейса
class CEnginePtr (
// Конструктор и деструктор
public:
CEnginePtr() : m_pEngine( null ) , m_hr( S_OK ) (
}
CEnginePtr( IUnknown* pUnk ) : m_pEngine( null ), m_hr( S_OK )
{
Store( pUnk );
}
-CEnginePtr()
{
Release () ;
}
И Экстракция
operator lEngine*()
<
CheckPointer();
return m_pEngine;
1
operator IUnknown*()
{
CheckPointer () ;
return m_pEngine;
}
И Информация о статусе (состоянии)
public:
HRESULT GetLastStatus ()
< return m_hr;
}
// Присваивание
public:
Часть V
void Createlnstance( REFCLSID rclsid, DWORD dwClsContext = CLSCTX_ALL, IUnknown *pUnkOuter = NULL )
(
Release();
CheckError( ::CoCreatelnstance( rclsid, pUnkOuter, dwClsContext, IID_IEngine, (void**)6m_pEngine ) ) ;
)
lEngine* operator=( IUnknown *pUnk ) (
// Убедитесь, что предыдущий указатель освобожден Release();
//- Сохранение нового указателя Store( pUnk ); return m_pEngine;
}
11 Методы интерфейса public:
void Start () {
CheckPointer () ;
CheckError( m_pEngine->Start() ) ; }
void Stop() (
CheckPointer(); CheckError( m_pEngine->Stop() ); }
// Реализация protected: void Store( IUnknown *pUnk ) (
Released ; if ( pUnk != null ) { CheckError( pUnk->QueryInterface( IID_IEngine, (void**)6m_pEngine ) ) ; ) else ( CheckError( E_POINTER );
) )
void Released ( if ( !IsEmpty() ) { m_pEngine->Release(); m__pEngine = null; m_hr = S_OK;
} )
bool IsEmpty() ( return ( m__pEngine = null ) ;
void CheckPointer() ( if ( IsEmptyO ) {
Глава 21
CheckError( E_POINTER );
void CheckError( HRESULT hr ) (
m_hr = hr ;
if ( FAILED( hr ) ) ( throw hr;
) }
// Элементы реализации
protected:
lEngine *m_pEngine;
HRESULT m hr;
Большая часть этого программного кода подобна реализации интеллектуального указателя с семантической разницей, когда используются исключения вместо обеспечения способа проверки успешного выполнения или неудачи. Метод GetLastStatus() необходим для того, чтобы различать большое количество кодов успешного завершения в том случае, когда не используется исключение. Как видно из примера программного кода, только небольшая его часть специфична для интерфейса. Оставшаяся часть стандартна и может быть создана с помощью шаблона. Исходя из предложения, что существует подобная обертка для ICar, фрагмент, использующий описанный механизм, будет выглядеть так:
CCarPtrCarPtr;
CEnginePtr EnginePtr;
HRESULThr;
hr = Colntialize( NULL );
assert( SUCCEEDED( hr ) ) ;
try (
CarPtr.Createlnstance( CLSID_Car );
EnginePtr = CarPtr; // здесь находится Queryinterface
EnginePtr.Start();
Drive( CarPtr ) ;
EnginePtr.Stop();
} catch ( HRESULT hr ) {
}
CoUninitialize();
Обертки могут быть выполнены вручную, но это утомительная и неблагодарная работа. Было бы лучше, если бы некоторый инструмент получал описание интерфейса и производил класс обертки. Поскольку IDL- файлы интерфейсов, определенных сторонними разработчиками, вообще недоступны и к тому же header- файлы C++ не содержат достаточной информации для описания всех параметров, необходимо предложить другой способ, помогающий обнаружить специальные сообщения интерфейса. В этом случае используются библиотеки типов. Библиотеки типов включают описание IDL интерфейса в двоичной форме — так чтобы они были меньше по объему и лучше распространялись. Еще одно преимущество библиотек типов заключается в том, что они описывают интерфейсы диспетчеризации, что позволяет также генерировать для них обертки.
Дальнейшее обсуждение специфично для компилятора Visual C++ 5.0. Компилятор реализует расширение Microsoft для препроцессора — директиву #import:
♦import <type_lib> no_namespace
Параметр no_namespace используется для того, чтобы подавить генерирование отдельного пространства имен для определений библиотеки типов. Несколько других опций управляют поведением директивы #import. Если препроцессор обнаруживает #import в исходном программном коде, он просматривает вызванную библиотеку типов и генерирует два файла с расширениями .tlh и .tli (эти расширения замещают заголовок библиотеки типов и реализацию библиотеки типов). Здесь же содержится заголовок для сгенерированного класса; последний содержит реализацию методов обертки. Любые другие детали лежат вне контекста этого обсуждения. Сгенерированные средства оберток в существующем шаблоне интеллектуального указателя имеют семантику исключения. Все оборачивающие методы также генерируют исключения при возникновении отказа.
Часть V
Создание COM-объектов в C++
Следующие разделы этой главы посвящены созданию объектов СОМ. Здесь не рассматривается программный код шаблона, необходимый для создания COM-сервера, который будет полностью работоспособным. Представлено несколько стратегий, которые могут быть использованы для поддержки объектов СОМ.
Множественное наследование
Компилятор IDL генерирует определения интерфейса, подобные классам C++, содержащим только чисто виртуальные методы. Естественно будет реализовать объекты СОМ с классами C++, которые наследуют определения отображаемых интерфейсов, как показано в листинге 21.6.
Листинг 21.6. Определение класса C++, реализующего объект СОМ с помощью метода множественного наследования из реализованных интерфейсов
class CCar : public ICar, public lEngine (
// Конструктор и деструктор
public:
CCar () ;
-CCar () ;
/ / Методы IUnknown
public:
STDMETHOD( Queryinterface ) ( REFIID riid, void **ppv ) ;
STDMETHOD_( ULONG, AddRef ) () ;
STDMETHOD_( ULONG, Release ) () ;
// Методы ICar
public:
STDMETHOD( SetSpeed ) ( long nSpeed ) ;
// Методы lEhgine
public:
STDMETHOD( Start ) () ;
STDMETHOD ( Stop )();
// Реализация специфики protected:
ULONG m_nRef;
bool m_bEngineStarted;
long m_nSpeed;
};
Макрос STDMETHOD дополняет соответствующее соглашение о вызовах и определяет HRESULT как возвращаемый тип. Макрос STDMETHOD_ подобен STDMETHOD и позволяет определять явный тип возвращаемого значения. Это необходимо только для некоторых старых интерфейсов (которые являются всегда локальными, т.е. они не могут распределяться), таких как IUnknown, IMalloc и др. Подобные макросы существуют для реализации метода — STDMETHODIMP и STDMETHODIMP_ (< тип) >. В листинге 21.7 показан пример реализации класса.
Листинг 21.7. Реализация класса C++, осуществляемая объектом СОМ с помощью множественного наследования из реализованных интерфейсов
CCar::CCar() :
m_nRef( 0 ) ,
m_bEngineStarted( false ) ,
m_nSpeed( 0 )
{ }
CCar::-CCar() { }
STDMETHODIMP CCar::Queryinterface( REFIID riid, void **ppv ) ; {
Глава 21
HRESULT hr = S_OK;
// Заметьте, что lEngine выделен для запросов IUnknown if ( IsEqualIID( riid, IID_IUnknown ) ) (
*ppv = (void*)static_cast<IEngine*>( this ) ;
} else if ( IsEqualIID( riid, IID_IEngine ) ) ( *ppv = (void*)static_cast<IEngine*>( this );
) else if ( IsEqualIID( riid, IID_ICar ) ) ( *ppv = (void*)static_cast<ICar*>( this ); ) else {
hr = E_NOINTERFACE;
*ppv = NULL ; )
// Заметьте, что мы вызываем AddRef для возвращаемого указателя if ( SUCCEEDED ( hr ) ) {
reinterpret_cast<IUnknown*>( *ppv )->AddRef();
return hr; }
STDMETHODIMP_(ULONG) CCar::AddRef() (
m_nRef++;
return m_nRef; }
STDMOETHODIMP_(ULONG) CCar::Release() (
m_nRef—;
if ( m_nRef = 0 ) { delete this;
}
return m_nRef; }
STDMETHODIMP CCar::SetSpeed( long nSpeed ) (
HRESULT hr = S_OK;
if ( m_bEngineStarted ) ( if ( nSpeed >= 0 ) ( m_nSpeed = nSpeed ;
} else ( hr = E_INVALIDARG;
}
) else { hr = CAR_E_POWER;
) return hr; }
STDMETHODIMP CCar::Start() {
HRESULT hr = S_OK;
if ( !m_bEngineStarted ) { m_bEngineStarted = true;■
m_nSpeed = 0;
} else ( hr = CAR_E_POWER;
) return hr; }
STDMETHODIMP CCar::Stop() (
HRESULT hr = S OK;
Часть V if ( m_bEngineStarted ) ( if ( m_nSpeed == 0 ) ( m_bEngineStarted = false; ) else {
hr = CAR_E_SPEED;
} } else { hr = CAR_E_POWER;
1 return hr;
)
При использовании метода множественного наследования все интерфейсы разделяют общий подсчет ссылок. Поскольку методы части IUnknown для всех интерфейсов реализованы в классе, все интерфейсы имеют единственную реализацию этих методов в своих VTBL. Когда все интерфейсы объекта освобождены, объект самоуничтожается. Реализация, показанная в листинге 21.7, не имеет безопасного потока. Если несколько потоков выполняют методы AddRef() и Release() одновременно, появляется элемент соревновательности и объект может быть разрушен преждевременно. Однако можно просто изменить эту реализацию, чтобы создать безопасный поток.
Модифицированное множественное наследование должно реализовывать специфические для интерфейса методы для всех интерфейсов на отдельных классах и иметь объектный класс, порожденный от классов реализации. Методы интерфейса IUnknown должны быть реализованы в объектном классе. Этот подход необходим, если два интерфейса совместно используют методы с одинаковым определением, но нуждаются в различной реализации. Недостаток множественного наследования заключается в том, невозможен подсчет ссылок для каждого интерфейса в отдельности. Положительный момент состоит в том, что необходимость в этом возникает редко.
Вложенные классы
При другом подходе реализация каждого интерфейса осуществляется в отдельном классе. Эти вспомогательные классы обычно определяются внутри объектного класса, который включает переменные-члены для каждого из вспомогательных классов. Объектный класс реализует методы IUnknown. При реализации вложенных классов в I Unknow выполняется простое делегирование к реализации объекта IUnknown. Каждый интерфейс может выполнять свой собственный подсчет ссылок. В листинге 21.8 показано определение объекта, использующего вложенные классы.
Листинг 21.8, Определение класса C++, реализующего объект СОМ с помощью вложенных классов
class CCar : public IUnknown {
protected:
// Реализация ICar
class XCar : public ICar (
// Конструктор и деструктор
public:
XCar( CCar *pOwner ) : m_pOwner( pOwner )
('
assert( pOwner •= null ); }
-XCar () { /
I/ Методы IUnknown
public:
STDMETHOD( Queryinterface )( REFIID riid, void **ppv ) ;
STDMETHOD_( ULONG, AddRef ) () ;
STDMETHOD__( ULONG, Release ) ();
// Методы ICar
public:
STDMETHOD( SetSpeed ) ( long nSpeed ) ;
Глава 21
// Реализация protected: CCar *m_pOwner;
);
// Реализация lEngine
// Конструктор и деструктор public:
CCar () ;
-CCar () ;
// Методы IUnknown public: STDMETHOD( Queryinterface )( REFIID riid, void **ppv ); STDMETHOD_( ULONG, AddRef ) () ; STDMETHOD_( ULONG, Release ) () ;
// Вложенные классы protected: XCar m_ICar; XEngine m_IEngine;
// Реализация protected: ULONG m_nRef; bool in_bEngineStarted; long m_nSpeed;
);
Каждый вложенный класс хранит указатель на экземпляр основного объекта и делегирует ему вызовы IUnknown. Данные, которые являются общими множественными интерфейсами, также размещены там. Наш пример не очень хорошо подходит для иллюстрирования вложенной реализации; он предлагает только объяснение управления вложенными классами. Реализация методов 1Саг и lEngine опущена. В листинге 21.9 показан пример реализации.
Листинг 21.9. Реализация класса C++ для объекта СОМ с помощью вложенных классов STDMETHODIMP CCar::XCar::Queryinterface( REFIID riid, void **ppv ) return m_pOwner->QueryInterface( riid, ppv );
} STDMETHODIMP_(ULONG) CCar::XCar::AddRef() ( return m_pOwner->AddRef(); }
STDMETHODIMP_(ULONG) CCar::XCar::Release() (
return m_pOwner->Release() ; )
CCar:: CCar () : m_ICar ( this ) , m_IEngine( this ), m_nRef( 0 ) , m_bEngineStarted( false ), m_nSpeed( 0 )
)
CCar: :~CCar () ( )
19 Зак. 53
Распределенные вычисления Часть V
STDMETHODIMP CCar::QueryInterfасе( REFIID riid, void **ppv ); (
HRESULT hr = S_OK;
И Обратите внимание, что lEngine выделен для запросов lUnknow if ( IsEqualllD( riid, IID_lUnknown ) ) {
*ppv = (void*)static_cast<IUnknown*>( this ) ;
) else if ( IsEqualllD( riid, IID_IEngine ) ) {
*ppv = (void*)static_cast<IEngine*>( Sm_IEngine ) ;
} else if ( IsEqualllD( riid, IID_ICar ) ) {
*ppv = (void*)static_cast<ICar*>( sm_ICar ); } else {
hr = E_NO INTERFACE;
*ppv = NULL;
)
// Обратите внимание, что вызывается AddRef для возвращенного указателя if ( SUCCEEDED ( hr ) ) (
reinterpret_cast<IUnknown*>( *ppv )->AddRef(); } return hr; }
STDMETHODIMP_(ULONG) CCar::AddRef() (
m_nRef++;
return m_nRef;
}
STDMOETHODIMP_(ULONG) CCar::Release() (
m_nRef—;
if ( m_nRef == 0 ) ( delete this;
) return m_nRef; )
Подход с использованием вложенных классов подобен модификации подхода с множественным наследованием, когда используются базовые классы реализации интерфейса. Основное различие заключается в том, что вложенные классы реализуют методы IUnknown, в то время как классы реализации этого не делают. Реализации вложенных классов методов IUnknown делегируют к реализации объекта IUnknown. Реализация Querylnterface() изменяется, чтобы осуществить поиск реализации интерфейса из переменных-членов, в то время как IUnknown имеет отдельную реализацию в базовом классе.
Можно объединить этот подход с методом множественного наследования и реализовать несколько интерфейсов на одном вложенном классе. Каждая группа интерфейсов вложенного класса будет иметь единственную реализацию IUnknown, делегирующую к основному объекту при использовании множественного наследования для реализации интерфейсов.
Использование классов tear off
Существуют случаи, когда некоторый интерфейс объекта редко используется, но потребляет большое количество ресурсов. Предпочтительно избегать пустой траты ресурсов, если интерфейс никогда не будет использован. Подходы к реализации, рассматриваемые до сих пор, не могут решать проблему непроизводительных затрат ресурсов, поскольку распределяются все объектные ресурсы. Решение заключается в использовании так называемого интерфейса tear-off. Если интерфейс запрашивается впервые, при реализации Querylnterface() динамически создает экземпляр класса, который реализует интерфейс. Элементы класса IUnknown делегируют к IUnknown основного объекта точно так же, как и в случае с вложенными классами. Поддерживается отдельный подсчет для интерфейса tear-off; когда интерфейс освобождается в последний раз, класс tear-off, в свою очередь, разрушается.
Реализация классов tear-off почти идентична реализации, используемой для вложенных классов, и здесь не рассматривается. Отличие заключается в том, что вместо элементов объектный класс хранит указатели
на классы tear-off и дополнительный подсчет ссылок на каждый элемент. Изменены методы QuerylnterfaceO и Release(). Другой подход должен позволить классу tear-off реализовать собственную ссылку и очистить объектный указатель в ходе выполнения операции удаления внутри собственной реализации Release().
Резюме
В этой главе были рассмотрены наиболее важные вопросы, касающиеся технологии СОМ. Многие незначительные детали были опущены. Книги, перечисленные в разделе "Дополнительная литература", помогут вам расширить свои познания о СОМ и дадут возможность рассмотреть широкий спектр технологий СОМ. Эта глава посвящена подробностям использования и реализации объектов СОМ. Даже если вы знали о СОМ ранее, автор надеется, что сможете найти кое-что новое на этих страницах.
Дополнительная литература
В качестве трамплина для перехода от C++ к СОМ, а также для того, чтобы узнать все подробности реализации ядра СОМ обратитесь к книге Дона Бокса (Don Box) Essential СОМ. Кроме того, первые сведения о СОМ вы можете почерпнуть из книги Дейла Роджерсона (Dale Rogerson) Inside СОМ. Для тех, кто знаком с основами СОМ, предназначена книга издательства Macmillan Publishing COM/DCOM Unleashed. Классическая книга по этой тематике — Inside OLE Крейга Брокшмидта (Kraig Brockschmidt). В ней рассматривается в деталях спецификация первой реализации СОМ: связывание и внедрение объектов (OLE).
Java и C++Java и C++
Java и C++
Java и C++
В ЭТОЙ ГЛАВЕ
Общие черты C++ и Java
Различия между C++ и Java
Объектно-ориентированные возможности Java
Глава 22
На первый взгляд может показаться, что нет особой необходимости включать в книгу о C++ отдельную главу о Java, но для программиста, работающего на языке C++, переход к изучению языка Java — это естественный процесс. Между языками Java и C++ имеется большое сходство. Каждый из языков обладает собственными уникальными преимуществами и недостатками. Поэтому программист, работающий на языке C++ и освоивший Java, сможет использовать этот язык для совершенствования своего мастерства программирования на C++. Многие программисты, работающие на C++, заинтересованы (и, возможно, немного побаиваются) языком Java. В этой главе показано, какие замечательные возможности открываются при использовании этих двух языков, и затем отмечаются их основные отличия. Таким образом, программисты, работающие на языке C++, могут быстро ознакомиться с важными аспектами Java.
Язык программирования Java оказал значительное влияние на компьютерную индустрию. В начальный период существования Java поставщики и разработчики объединяли свои усилия по реализации решений, основанных на использовании Java. Теперь же, когда ажиотаж утих, больше внимания снова уделяется C++. Прочитав эту главу, вы будете достаточно хорошо подготовлены, чтобы определить, каким инструментом пользоваться в каждой конкретной ситуации.
Общие черты C++ и Java
Общие черты C++ и Java рассматриваются, начиная от основ, и затем постепенно раскрываются расширенные возможности языка Java.
Комментарии
Язык программирования Java поддерживает комментарии C++: как блочную последовательность / * * /, так и одиночную строку, начинающуюся с символа / /.
При использовании языка Java комментарии несут дополнительную нагрузку. Используя специальные символьные знаки в файлах исходного программного кода и в утилите javadoc, можно формировать документацию для файлов исходного кода. Комментарии, относящиеся к документации, должны начинаться с последовательности символов / ** и заканчиваться последовательностью */• Специальные ключевые слова документа, которым предшествует символ @, используются для выделения разделов комментария. Можно также вставлять дескрипторы HTML внутри блока документа. Следующий пример иллюстрирует возможность комментирования содержимого документа:
/**
sleepMethod - sleeps a specified number of milliseconds
Sparam long millis - milliseconds to sleep
Sreturn void
@exception lOException
gauthor: Some Java Developer
gversion: 1.0
*/
public void sleepMethod( ) thows lOException {
//. . -
}
При выполнении утилиты javadoc необходимо указать один или большее количество файлов исходного программного кода для того, чтобы выполнить их анализ. Утилита javadoc приводит к отображению файлов HTML, содержащих документацию для анализируемых файлов исходного программного кода. Эта документация может затем просматриваться с помощью Web-броузера.
Типы данных
Java разделяет типы данных на две категории: примитивные и ссылочные типы. Java не располагает ни структурными, ни составными типами — они заменены классами. Java также не располагает указателями, которые более важны для программистов, работающих на языке C++. Все типы, не являющиеся примитивными, относятся к ссылочным; Java заботится об "уборке мусора" (очистка памяти) для пользователя.
Примитивные типы данных
Ниже приведены восемь примитивных типов данных, которые поддерживаются Java, — byte, boolean, char, short, int, long, float и double. В табл. 22.1 приведен список примитивных типов данных и их размерности.
Часть V
Таблица 22.1. Типы данных Java и их размерности
Тип
Размерность
byte boolean char short int long float double
1 байт (8 битов)
байт (8 битов)
байта Unicode (16 битов, целое)
байта (16 битов, целое)
байта (32 бита, целое)
байтов (64 бита, целое)
байта (32 бита, целое)
8 байтов (64 бита, целое)
Каждый тип примитивных данных в Java отнесен к одной из двух категорий — к числовой логической. Числовая категория состоит из целых типов и типов с плавающей точкой: byte, short, int, long, char, float и double. Логическая категория, конечно, состоит исключительно из типа данных boolean. Тип char представляет собой специальный примитивный тип для символьного представления. Каждый тип данных Java имеет определенную размерность для поддерживаемой платформы. Это позволяет поддерживать независимость платформы Java. Обратите внимание на то, что язык Java не поддерживает типы без знака.
Типы данных, приведенные в табл. 22.1, также рассматриваются как собственные типы данных. Язык Java также предлагает типы классов, определенные как Integer, Long и Character (обратите внимание на переход в верхний регистр). Собственные типы занимают меньший размер и более эффективны по сравнению с типами класса, но при передаче в методы и вовне методов по значению предпочтительнее, чем ссылки.
Тип данных char
Для представления символа в Java используются два байта и применяется международная кодировка Unicode. Следовательно, Java позволяет получить представления для большинства наборов интернациональных символов. Хотя тип данных char обеспечивает поддержку для различных наборов интернациональных символов, нельзя просто преобразовать и отображать символы, например, из английского языка в японский. Поддержка для Unicode должна быть обеспечена операционной системой.
Тип данных boolean
Тип данных boolean, поддерживаемый Java, является истинно логическим, а не только лишь целочисленным типом. Только два значения могут быть сохранены и восстановлены из boolean — true (истина) и false (ложь). В результате выполнения условного выражения должно отображаться логическое значение. Следующий фрагмент программного кода иллюстрирует допустимые использования логического типа, а также ограничения:
boolean result = false; int value - result; value = 1; result = value;boolean result = false; int value - result; value = 1; result = value;И допустимо — значение, принятое по умолчанию, ложно И неправильно! Java не позволяет этого // допустимо И этого также нельзя делать!
boolean result = false; int value - result; value = 1; result = value;
boolean result = false; int value - result; value = 1; result = value;
Нельзя упростить преобразование, используя явное приведение, как показано в этом примере:
result = (boolean) value; // это также недопустимо.
Тип данных byte
Тип данных byte в Java можно сравнить с типом данных char в C++, но полного сходства не существует. Байты в Java являются 8-битовыми целыми числами со знаком, значение которых может находиться в диапазоне от -128 до 127. Некоторые программисты, работающие на языке C++, будучи новичками в использовании Java, хотят использовать массив значений byte для моделирования строк C++ (и С). Java не поддерживает подобные массивы; библиотека Java предлагает класс String для этих целей.
Ссылочные типы данных
Существует три типа ссылок — класс, интерфейс и массив. Ссылка в Java очень похожа на ссылку в C++. Она является переменной, которая осуществляет связь с некоторым объектом, отличным от исходного.
Глава 22
Рассмотрим пример, написанный на языке C++:
int value = 5 ;
int S rv = value;
В этом примере rv фактически ссылается на value. Можно осуществить обращение к содержанию по имени value, на которое ссылается rv.
Нечто подобное происходит в языке Java. Следующий пример является примером ссылки Java. (Некоторые программисты, работающие на языке Java, называют ссылки дескрипторами.)
String str = new String () ;
System.out.printin (" str: " + str.length () + " characters long. ") ;
В C++ оператор new возвращает указатель. В Java оператор new возвращает ссылку на объект в куче, это единственный вид доступа к объекту. Не волнуйтесь по поводу освобождения памяти — Java заботится об этом. Этот метод является единственным для создания объектов в Java; все объекты распределяются динамически, и, как правило, ссылка на объект используется чаще, чем сам объект.
Операторы
Все арифметические, условные операторы и операторы отношений, доступные в языке C++, также имеются в Java. Оператор '+' не только используется в качестве символа арифметической операции, но и служит для указания конкатенации строк.
Вы будете приятно удивлены, обнаружив наличие операторов, выполняющих поразрядные действия. В Java добавляется еще один оператор такого типа: (беззнаковый) тройное смещение вправо. Оператор тройного смещения вправо, »>, является логическим оператором, используемым для смещения битов интегрального значения вправо. Поддерживаемый операнд определяет число смещаемых битов. При смещении битов с левой стороны добавляются нули. В следующем примере показано, как поделить число на 2 с помощью оператора тройного смещения вправо:
import java.io.* ;
public class Test
{
public static void main( String args[] ) (
int anlnt = 10 ;
System.out.printin( "anlnt before is:" + anlnt ) ;
anlnt = anlnt »> 1 ;
System.out.printin( "anlnt after is:" + anlnt ) ;
)
}
Результат выполнения этого приложения показан ниже:
anlnt before is: 10
anlnt after is: 5
Кроме того, все операторы присваивания C++ включены в Java . В целях совместимости также был добавлен оператор присваивания тройного смещения вправо, »>=. В следующем фрагменте программного кода демонстрируется пример использования этого оператора:
import java.io.* ;
public class Test
(
public static void main( String args[] ) {
int anlnt = 10 ;
System.out.println( "anlnt before is:" + anlnt ) ;
anlnt »>= 1 ;
System.out.printin( "anlnt after is:" + anlnt ) ;
}
}
Результат выполнения этого приложения показан ниже:
anlnt before is: 10
anlnt after is: 5
Часть V
Арифметические операторы перегрузки отсутствуют в Java. Оператор перегрузки может привести к появлению бессмысленного программного кода при неправильном применении. Существует одно исключение, позволяемое компилятором Java, — для класса String. Класс String может перегружать оператор +, используемый для конкатенации строк. Можно моделировать методы использования перегрузки оператора для представления арифметических операторов.
Операторы управления потоком
Все операторы, используемые для управления потоком в приложениях C++, также применяются в Java. При принятии решений в вашем распоряжении имеются операторы if-else и switch-case; при выполнении цикла применяются операторы for, while и do-while; при обработке исключений — try-catch-finally и throw; и наконец, в категории "разные" имеются операторы break, continue, label: и return. Обработка исключений описывается далее в этой главе.
Ключевое слово goto зарезервировано, но не поддерживается языком Java. Необходимо также знать, что результат выполнения выражений управления потоком будет логическим.
Различия между C++ и Java
Теперь, когда вы ознакомились с некоторыми общими чертами между Java и C++, рассмотрим некоторые различия между двумя языками.
Управление памятью
Сборщик мусора Java автоматически забирает обратно память, выделенную для объекта, если только все ссылки на этот объект были освобождены. Заметьте, что все ссылки на объект должны быть переназначены. Когда сборщик мусора запускается на выполнение, он находит все объекты, которые больше не упоминаются, и освобождает память.
Отсутствие указателей
В Java отсутствуют указатели. Из-за этого некоторые замысловатые технические приемы затруднительны в реализации, но программирование без указателей с использованием встроенного сборщика мусора в Java оказывается более быстрым и логичным, чем необходимость устранять проблемы управления памятью, возникающие при использовании указателей в C++.
Отсутствие препроцессора
Java не использует файлы include, директивы #define или typedefs и не имеет препроцессора.
Отсутствие деструктора
Java использует свойства сборщика мусора для утилизации памяти, используемой объектами, которые больше не вызываются. Хотя Java предлагает ключевое слово new для выделения объектов во время выполнения, не существует соответствующего ключевого слова delete, как в языке C++. Все, что нужно сделать в Java, — это выделить объект с помощью ключевого слова new; нет необходимости волноваться по поводу явного разрушения объекта.
Java предлагает метод finalize(), но он не очень похож на деструктор. Сборщик мусора может вызвать метод finalize() для объекта, который был определен как мусор (больше не требуется). Вы никогда не узнаете, когда выполняется метод finalize(); этот процесс не поддается непосредственному контролю.
Любые ресурсы, управляемые объектом, должны быть освобождены, когда объект выходит за пределы видимости. Если класс обладает ресурсами, которые должны быть освобождены, необходимо определить метод, используемый как деструктор. Вы могли бы назвать подобную функцию-член cleanup() или некоторым другим подобным именем. Необходимость в этом возникает редко, поскольку встроенного сборщика мусора должно быть достаточно в большинстве случаев.
Необходимо также привыкнуть устанавливать ссылку на объект в NULL, когда работа с объектом будет завершена. При этом сборщику мусора указывается, что с этим объектом работа закончена. Это, конечно, не означает, что сборщик мусора затребует объект; могут существовать другие ссылки, относящиеся к тому же самому объекту. В следующем примере продемонстрировано намерение освободить некий объект:
InterestingObject yoohoo = new InterestingObject( ) ;
yoohoo.yodel( ) ;
Глава 22
yoohoo.cleanup( ) ;
yoohoo = null ;
Вам необходимо привыкнуть выполнять это "замкнутое выражение" при работе с объектами. Это поможет сборщику мусора освободить память настолько быстро и эффективно, насколько это возможно.
Спецификаторы доступа
Спецификатор доступа позволяет управлять видимостью членов-функций и атрибутов. Язык Java предлагает те же самые ключевые слова доступа, что и C++, но с одной оговоркой: они являются частью синтаксиса объявления. Другими словами, необходимо явно определить видимость каждого индивидуального класса и классифицировать элемент. В языке C++ определяется доступ к элементам с блочной областью видимости. В Java необходимо потрудиться при вводе операторов больше, но у вас появится более тонкий контроль над видимостью (и размещением) отдельных элементов. Для уяснения вопросов, связанных с управлением доступом, рассмотрим следующий пример, показывающий определение видимости в C++.
class Goofy (
public:
Goofy ( ) ;
-Goofy( ) ;
int getValue( ) ;
protected:
int negotiateValue( const int valueln );
private:
bool verifyValue( const int valueln ) ; int value ;
} ;
В противоположность предыдущему примеру на C++, имеется подобное объявление в Java:
public class Goofy (
public Goofy( ) {/*...*/}
public int getValue( ) { return value ; }
protected int negotiateValue( int valueln ) { return value ; }
private boolean verifyValue( int valueln ) { return true ; } private int value ;
// ... и так далее
)
Если доступ явно не определяется, то элементом, принятым по умолчанию, будет пакет. Пакетный доступ указывает, что элемент доступен для других классов внутри того же самого пакета.
ПРИМЕЧАНИЕ
: В версии 1.0 Java существует пятый спецификатор доступа: private protected, который был позаимствован из версий
Java, последовавших за 1.0.■ • s
Доступ определяется не только к каждому индивидуальному элементу, но и для класса. Точка с запятой не ставится после заключительной фигурной скобки класса. В языке Java члены-функции всегда определяются в объявлении класса. Заметьте, что это вовсе не означает, что Java генерирует встроенные функции- члены.
Параметры метода
Другое отличие между Java и C++ заключается в том, что ключевое слово const не применяется к параметрам метода. Ключевое слово const (или его некоторая разновидность) не требуется в Java, поскольку этот язык использует "пересылку за значением" для примитивных параметров типа к методу. Это означает, что вы не можете изменять значение объектного параметра. Внутри функции-члена используется копия первоначального объекта, а не реальный объект, на который происходит ссылка по параметру. В Java, если передается объект (не примитив), то передается ссылка на объект, а не сам объект. Вы не можете изменять переданную ссылку, но можете обратиться к публичным методам и переменным экземпляра. Пример листинга 22.1 должен немного прояснить эту ситуацию.
Часть V
Листинг 22.1. Передача примитивного параметра в Java import, java.io.* ; public class Goofy { public Goofy( ) {/*...*/} public void doubleArg ( int value ) { value *= 2 ; } static public void main( String arg[] ) { int theValue = 5 ;
System.out.printin("1. theValue is: " + theValue ) ; Goofy g = new Goofy ( ) ; g. doubleArg( theValue ) ;
System.out.printin("2. theValue is: " + theValue ) ; }
)
В результате выполнения этого приложения выводится следующее:
theValue: 5 2. theValue : 5
Этот программный код показывает, что функция-член doubleArgO просто получает копию, а не ссылку на исходный объект theValue. Если вы передаете ссылку функции-члену, то фактически оперируете с объектом, на который выполняется ссылка. Пример в листинге 22.2 — это модификация примера из листинга 22.1, которая демонстрирует передачу объекта (не примитива).
Листинг 22.2. Передача объектного параметра в Java
import java.io.* ;
public class Int
( / / этот класс должен содержаться в своем собственном файле public Int() ( /* . .. */ } public int intValue = 5 ;
) public class Goofy { public Goofy( ) {/*...*/} public void doubleArg ( Int value ) { value. intValue *= 2 ; } static public void main( String arg[] ) ( Int theValue = new Int( ) ;
System.out.printin("1. theValue is: " + theValue.intValue ) ; Goofy g = new Goofy ( ) ; g.doubleArg( theValue ) ;
System.out.printin("2. theValue is: " + theValue.intValue ) ; }
Вот результат работы этого приложения:
theValue: 5
theValue: 10
В заключение — одно замечание: массивы являются объектами первого класса в Java. В предыдущем примере, если theValue — целочисленный массив, то метод doubleArgO заменит фактический параметр на аргумент.
Внешние функции
В Java каждая функция должна быть элементом класса. Нет каких-либо глобальных функций.
Перечислители
В языке C++ ключевое слово enum доступно при определении объекта, содержащего перечисляемые значения. Java не содержит ключевого слова enum, но можно его смоделировать, создавая класс, который
Глава 22
содержит только экземпляры переменных, которые являются static final. Объявляя экземпляры переменных как static final, на самом деле вы определяете их как константы. Следующий пример иллюстрирует процесс моделирования enum в Java:
public class DrawingColor <
public static final int red = 1;
public static final int yellow = 2;
public static final int green = 3; ) //...
pen.color( DrawingColor.red ) ;
//...
Имеет место некоторая особенность для enum в C++ (и С). Каждый элемент enum должен иметь уникальное имя по отношению к другим enum и переменным. Рассмотрите эквивалентное предыдущему примеру объявление в языке C++. Если нет какого-либо дополнительного enum под названием WindowColor, который содержит переменные-члены, именуемые red, yellow и green, то произойдет конфликт имен. Такого рода проблемы отсутствуют в Java.
Строки
В Java отсутствует какое-либо эквивалентное представление строки, как это имеет место в C++. Строки в C++ представлены как массив char (и оканчиваются символом NULL). В Java неизменный объект String доступен для представления строк и считается объектом первого класса. В Java можно создавать заключенные в кавычки (буквенные) строки точно так же, как это происходит в C++, но компилятор фактически преобразует это представление в объект String, например:
String str = "Это строка, заключенная в кавычки";
Компилятор Java создает объект String для строки, заключенной в кавычки, и затем назначает ее str. Объект String является неизменяемым: нельзя изменять его содержимое. Если необходимо изменить строку, то в Java предлагается класс StringBuffer.
Хотя в Java отсутствует перегрузка оператора, класс String предлагает оператор + для конкатенации объектов Strings. Не забывайте, что объекты String являются неизменяемыми, поэтому нельзя соединять String с другим объектом String; результатом конкатенации должен стать новый объект String. Класс String также предлагает функцию-член length(), позволяющую вычислить длину объекта String.
Массивы
Массивы в языке Java рассматриваются в качестве объектов первого класса. Доступ к элементам массива Java происходит таким же образом, как и обращение к элементам массива C++: используется индексация массива. Java предлагает проверку достижения границ массива во время выполнения. Массивы Java содержат экземпляр переменной length, который включает число элементов именованного массива (в зависимости от контекста). Скобки могут быть размещены слева или справа от имени массива.
В Java нельзя определить размерность массива в момент его объявления, как это делается в C++. Массивы в Java должны быть определены с помощью оператора new. Рассмотрим пример объявления и использования массива Java:
1: int arrayl[]-' П скобки справа
2: int [] array2 ; // скобки слева, обе расставлены правильно
3: int аггауЗ[10] ; // ОШИБКА, нельзя определять разиерность!
4: Object [] аггауЗ = new Object[10] ;
5: arrayl = new int[10] ;
6: array2 = new int[10] ;
7: arrayl [1] = 5 ;
8: arrayl[12] =5 ; // ОШИБКА, превышение границ
9: for( int i = 0; i < arrayl.length; i++ )
10:arrayl[i] = i ;
Первые два оператора просто объявляют ссылку на массив int; еще не определена размерность двух массивов. Третья строка генерирует ошибку, возникающую во время компиляции. В четвертой строке резервируется массив из 10 Object, а аггауЗ представляет собой ссылку на эти объекты. Пятая и шестая строки выделяют 10 ints и связывают их с именами массивов. Присвоение происходит в седьмой строке: значе-
Часть V
ние 5 присваивается второму элементу arrayl. Восьмая строка интересна тем, что, несмотря на то что компилятор допускает использование оператора, во время выполнения выводится диагностическое сообщение: java.lang. ArraylndexOutOfBoundsException. В девятой строке используется переменная-член length из arrayl для определения верхней границы массива.
Объектно-ориентированные возможности Java
После ознакомления с основными особенностями сходства и различия между Java и C++, заставившими нас немного отступить от темы изложения, приступим к исследованию более сложных свойств языка Java . Рассматривая различные возможности, изучим такие свойства, как наследование, инкапсуляция, абстрактные базовые классы и управление памятью. Начнем с обсуждения базового объектно-ориентированного элемента Java — класса.
Классы
Класс в Java имеет те же самые концептуальные и функциональные возможности, что и класс C++. Класс в Java содержит объявления — например, функций-членов и переменных.
Начнем рассмотрение с простого примера:
public class Goofy <
public GoofyO { /* . .. */ )
private bool interpret ( ) { return true ; } public int value ;
int noAccess ;
1
Сразу же необходимо сделать три замечания об особенностях этого примера: спецификатор доступа применяется к классу и элементам класса, функции определены внутри объявления класса, и нет точки с запятой после заключительной фигурной скобки.
Как упоминалось выше, можно применить четыре спецификатора доступа: public, protected, private и package. Фактически в явном виде не используется package, но этот спецификатор доступа задан по умолчанию, если он не определен. Спецификатор доступа package в Java поддерживает те же самые основные функциональные возможности, что и пространства имен в языке C++.
Ключевое слово static может быть применено к элементу класса. Эффект аналогичен тому, который реализуется при использовании static в C++. Если членом является переменная класса, то возможно лишь одно местонахождение для переменной для всех экземпляров класса, даже если количество экземпляров класса равно нулю. Переменные-члены, объявленные с помощью ключевого слова transient, используются таким образом, что не могут быть преобразованы в последовательную форму, т.е. переменные transient не могут храниться на диске или в базе данных.
Переменные класса и экземпляра могут быть объявлены как final. Класс final используется для достижения того же самого эффекта, что и const в C++. Переменная final должна быть инициализирована в момент объявления. Нельзя изменять значение переменной final. Если она представляет собой ссылку на объект, то можно оперировать с объектом непосредственно, но нельзя изменять ссылку при обращении к другому объекту. Подобным образом следует поступать и с массивами final, поскольку массивы являются объектами в Java.
Классы интерфейса
Java не поддерживает множественное наследование, но поддерживает интерфейсы, и класс может реализовать один или большее количество интерфейсов. Интерфейс определяет абстракцию, которая может быть реализована другими классами. Это похоже на типы абстрактных данных в языке C++.
В классе интерфейса функции-члены объявляются, но не определяются, и все переменные экземпляра в интерфейсе считаются конечными. Подобно абстрактному типу данных, нельзя инициализировать интерфейс: необходимо создать новый класс, который реализует интерфейс.
Класс реализации определяет методы, объявленные в интерфейсе. Ниже приведен небольшой пример класса interface:
public interface Silly
(
public void smile( long timespan ) ;
public void laugh( ) ;
Глава 22
public final long HowLong = 5000 ; // длинное целое }
В приведенном выше примере содержится объявление того, что Silly — класс интерфейса. Нельзя образовать объект Silly, как это сделано в следующем примере:
II...
Silly s = new Silly( );
Необходимо реализовать класс интерфейса, как это показано в следующем примере:
public class Goofy implements Silly (
public void smile( long timeSpan ) {/*...*/}
public void laugh( ) (/*...*/}
}
Нужно помнить, что следует реализовать функции членов, которые содержатся в классе интерфейса. Если вы забудете реализовать хотя бы одну функцию члена, то полученный класс также будет рассматриваться как абстрактный.
Теперь, когда в Goofy определены функции-члены, можно образовать объекты типа Goofy. Кроме того, объекты типа Goofy могут быть доступны (только для чтения) для константы Howlong. Рассмотрим полнофункциональное приложение.
public class Goofy implements Silly <
public void smile( long timeSpan ) {/*...*/}
public void laugh( ) {/*...*/}
public static void main( String args[] ) {
Goofy g = new Goofy ( ) ;
g.smile( g.HowLong ) ;
}
}
Используйте класс interface, если необходимо выразить проект как класс, но не обеспечить его реализацию.
Абстрактные классы
Java также содержит ключевое слово abstract для обозначения класса, представляющего абстрактный интерфейс. Класс, объявленный с ключевым словом abstract, не может быть образован. Для объявления класса в виде абстрактного, применяется ключевое слово abstract перед ключевым словом class, как показано в следующем примере:
abstract class Funny
{
public void smile( long timeSpan ) {/*...*/ I
abstract void laugh( ) ;
public final long HowLong = 5000 ; // длинное целое
)
Внутри абстрактного класса можно обеспечивать заданные по умолчанию реализации для функций-членов класса. В отличие от класса interface, нельзя поддерживать тело для функции-члена, которая объявлена внутри класса interface. Если наследование выполняется из абстрактного класса, то не обязательно реализовывать функции-члены, определенные внутри абстрактного класса. Необходимо определить все функции- члены, объявленные с помощью ключевого слова abstact.
Как и в случае с классом interface, можно определить постоянные значения в пределах абстрактного класса. Для формирования подкласса абстрактного класса применяется ключевое слово extends. Ниже приведен программный код, с помощью которого Joke объявляется подклассом Funny.
public class Joke extends Funny {
public void laugh( ) (/*...*/}
public static void main( String args[] ) {
Часть V
Joke f = new Joke ( ) ;
f.laugh( ) ;
f.smile ( f.HowLong ) ;
}
}
Обратите внимание на то, что нет необходимости в определении функции-члена smile(). В контексте класса Joke очевидно, что реализация smile() внутри Funny является адекватной. Можно, однако, перекрывать функции-члены, определенные внутри абстрактного класса. Следующий пример показывает, каким образом происходит перекрытие функции-члена:
public class Hilarious extends Funny
(
public void laugh( ) {/*...*/}
public void smile( long timeSpan ) (/*...*/}// перекрытие
public static void main( String args[] ) (
Hilarious f = new Hilarious ( ) ;
f.laugh( ) ;
f.smile( f.HowLong ) ;
}
)
В предшествующем примере перекрывается функция-член smile(). На этом этапе может возникнуть вопрос: "Каким образом можно вызвать функцию-член суперкласса?" Ответ можно получить с помощью ключевого слова super. Ниже приведена функция-член smile() (в Hilarious), переопределенная для вызова smile() в Funny:
public void smile( long timeSpan )
{
super.smile( super.HowLong ) ;
}
Обратите внимание на то, что ключевое слово super также применяется к параметру HowLong. Это не обязательно (выполняется в учебных целях).
Существенное различие между классом interface и классом abstract состоит в том, что наследование может выполняться лишь из одного класса abstract, в то время как можно объединять более одного класса implement. Для пояснения этого утверждения рассмотрим следующий пример:
public class Goofy implements Silly, Wacky
<
//...
}
Вы также можете предпринять этот шаг на следующем этапе, наследуя из Funny и реализуя Silly и Wacky. Следующий пример показывает, как это можно реализовать на практике:
public class Goofy extends Funny implements Silly, Wacky
{
//...
}■
Используйте abstract, если необходимо выразить проект как класс (подобно интерфейсу) и также включать некоторые заданные по умолчанию функциональные возможности. Это обеспечит шаблон для потенциальных пользователей, осуществляющих поиск специфических функциональных возможностей класса.
Инициализация члена
В языке Java инициализация члена гарантируется. В языке C++ локально или динамически выделенные объекты не инициализируются автоматически. Класс ссылается, если вы не инициализируете его, на значение NULL. Примитивные типы данных, если они существуют для класса, по умолчанию принимают нулевые значения. Подобная заданная по умолчанию инициализация также применяется по отношению к переменным-членам, объявленным как static и final. Конечно, в вашем распоряжении имеется опция для выполнения явной инициализации экземпляров переменных.
Возвращаясь к примеру Goofy, рассмотрим, как это применяется на практике:
Глава 22
public class Goofy
{
public void laugh( ) {/*...*/}
static final int value = 10 ;
private boolean notLaughing ;
private String name ;
)
В предшествующем примере переменная value не должна быть инициализирована вне класса, как требуется в C++. Следуя правилам языка Java, все должно существовать внутри класса. Экземпляр переменной notLaughing (boolean) по умолчанию устанавливается в значение false. Переменная-член паше объявляется как ссылка на String и инициализируется в значение NULL.
Ключевое слово this
Как и в C++, в Java поддерживается ключевое слово this. Оно позволяет получать доступ к члену класса с текущим диапазоном доступа. Однако заметьте, что в Java this — это ссылка на объект, а не указатель, и это положение не может быть изменено. В следующем примере демонстрируется использование ключевого слова this:
public class Goofy
(
public void laugh( boolean notLaughing ) {
this.notLaughing = notLaughing ;
}
static final int value = 10 ;
private boolean notLaughing ;
private String name ;
)
Ключевое слово this прежде всего используется для явной идентификации члена класса, если возникает неоднозначность имен. В предыдущем примере параметр notLaughing является тем же самым именем, что и экземпляр переменной для класса. Ключевое слово this применяется для разрешения переменной-члена.
Конструкторы
Основные концепции конструкторов в C++ также применимы к Java. Конструктор в Java имеет то же самое имя, что и класс, в котором он определен. Вы можете давать перегружаемые имена конструкторам внутри класса подобно тому, как это делается в C++. Заданное по умолчанию ограничение конструктора, существующее в C++, также справедливо для Java: если определяется конструктор, отличающийся от заданного по умолчанию, необходимо также определить заданный по умолчанию конструктор. Если конструктор для класса вообще не определяется, компилятор синтезирует заданный по умолчанию конструктор.
Существует опция, позволяющая применить спецификатор доступа к конструктору. Если доступ был не указан, используется пакет по умолчанию. Применение спецификатора доступа позволит определить объекты, которые могут создавать экземпляры определенного класса.
Вы можете вызывать суперкласс конструктора с помощью ключевого слова super. Просто используйте ключевое слово super и примените любые параметры, которые могут потребоваться. Эта методика сработает только тогда, когда суперкласс будет иметь соответствующий конструктор. В следующем примере иллюстрируется эта концепция:
public class Goofy extends Strange {
public void Goofy( )
{
super( "GoofBall" ) ; //применить идентифицированное имя }
//...
}
При использовании super необходимо помнить, что это должен быть первый вызов внутри конструктора. Если вы явно не вызываете конструктор базового класса внутри порожденного класса, используется заданный по умолчанию конструктор для создания части базового класса.
Это ключевое слово может использоваться (внутри конструктора) для вызова других конструкторов класса. В синтаксисе также используется ключевое слово super для вызова конструктора базового класса. Как и с
Распределенные вычисления Часть V
super, если вы используете ключевое слово this для вызова конструктора-близнеца, оно должно быть первым оператором внутри конструктора. Ниже приведен пример использования this при вызове конструктора:
public class Goofy { public Goofy( String name ) (/*...*/ } public Goofy( String name, boolean notLaughing ) ( this( name ) ; // позвольте указанному конструктору работать //... } //... }
Другое отличие между конструкторами Java и C++ заключается в том, что конструкторы Java не включают синтаксис для списка инициализатора. Инициализация переменной экземпляра класса выполняется одним из трех способов. Можно явно инициализировать переменную, объявив ее; можно явно инициализировать переменную внутри тела конструктора; или возможна неявная инициализация переменной с помощью компилятора: public class Strange ( Strange( ) ( ival3 = 10 ; } private intivall=10 ;//явно
private intival2;//гарантированная инициализация
private intival3;//выполняется в конструкторе
}
Метод finalize()
Java предлагает метод finalize(). Этот метод немного похож на деструктор C++, за исключением того, что поведение finalize() не столь предсказуемо, как поведение деструктора C++. В языке C++ всякий раз, когда вызывается деструктор, вы уже знаете, что объект будет разрушен, а память освобождена. Вы знаете, что это случится, когда вызывается деструктор, неявно или явно. В Java вы не знаете, когда вызывается метод finalize(). Он вызывается тогда, когда сборщик мусора возвращается к востребованию объекта.
Не слишком хорошим является решение освободить ресурсы внутри метода, поскольку никогда нельзя быть уверенным в том, когда будет вызываться finalize(). Если вы чувствуете, что нужно освободить ресурсы, которые выделены объекту, то необходимо выполнить функцию очистки. Когда вы заканчиваете работу с объектом, просто вызовите функцию очистки и затем установите объект в NULL. Установка объектной ссылки в NULL выдает подсказку сборщику мусора о том, что работа с указанным объектом закончена. Именно подсказка, поскольку сборщик мусора не обязан утилизировать объект именно в это время. Необходимо всегда помнить об этом. В следующем примере иллюстрируется идея очистки объекта после его использования.
Goofy g = new Goofy ( ) ;
g.doSomething( ) ; g.cleanup( ) ; g = null ;
Если вы определяете метод finalize() для своего класса, необходимо также убедиться в том, что был вызван метод finalize() для базового класса. К сожалению, Java не заботится об этом, в отличие от C++, где вызываются деструкторы. Что же произойдет, если вы не вызовите метод finalize() для базового класса? Совершенно очевидно, что он никогда не вызовется.
Если ваше приложение заканчивает работу, сборщик мусора будет собирать любые объекты, которые не были утилизированы.
Наследование
В ядре наследования Java заложена отдельно образованная иерархия. Все классы внутри иерархии Java наследуются из корневого класса Object. Если вы явно не устанавливаете базовый класс, то ваш класс неявно образуется из Object.
Глава 22
Вместо двоеточия, как в C++, в языке Java используется ключевое слово extends.
Не требуется выделять спецификатор для наследования в Java, поскольку нет приватного наследования. В следующем примере показано, как объявляется класс, порожденный от некоторого другого класса:
public class Strange
public void memberF( )
//...
}
public class Goofy extends Strange
private void memberF( ) И извините, нельзя изменять доступ
//...
}
В языке C++ функции-члены динамически вызываются во время выполнения только тогда, когда используется ключевое слово virtual. В Java все функции-члены динамически пересылаются во время выполнения. Ключевое слово final может применяться к функции-члену, с тем чтобы предотвратить перекрытие функции-члена в последующем производном классе. Раннее в этой главе уже было упомянуто о том, что компилятор Java не всегда работает со встроенными функциями. Применение ключевого слова final к функции-члену может происходить в случае, когда компилятор генерирует встроенную функцию. Если не происходит ничего другого, то функция-член final может быть статически связана. Компилятор не обязан выводить подсказку final; это только предложение.
В языке Java ссылка на базовый класс может использоваться для обращения к некоторому производному объекту класса. Оператор instanceof затем может использоваться для выполнения проверки того, обращается ли некоторая ссылка к некоторому производному объекту класса. Следующий пример демонстрирует использование instanceof:
Joke aJoke = new Joke ( ) ;
Laughter funny = aJoke ;
//...
if( funny instanceof Joke )
{
Joke j = (Joke)funny ;
//...,
)
Для любого запрещенного приведения во время выполнения генерируется исключение. Следующий пример демонстрирует возможное исключение:
Windows win = new Window ( . . . ) ;
Joke j = (Joke)win;
Правила для наследования элемента являются простыми. Любые переменные, объявленные в суперклассе и не переопределенные в подклассе, доступны подклассу. Для того чтобы подкласс мог обратиться к переменной, необходимо, чтобы переменная не была приватной в суперклассе. Любой метод, объявленный в суперклассе, который не перекрывается в подклассе, доступен подклассу, если доступ для базовой функции-члена не является приватным.
Объявление переменной с тем же самым именем в подклассе скрывает переменную, находящуюся в суперклассе. Тот же самое можно сказать о функциях-членах. В подклассе можно только вызывать функцию-член в непосредственном суперклассе.
Для вызова функции-члена суперкласса используется ключевое слово super. Конструкторы, поскольку они не являются элементами класса, не могут быть унаследованы порожденным классом. Если конструктор явно вызывается подклассом, то для вызова должно использоваться ключевое слово super. Конструктор, явно вызывающийся внутри того же самого класса подобно другому конструктору, должен вызываться с помощью этого ключевого слова. Следующий пример иллюстрирует эти правила:
public class Goofy
{
Goofy( ){/*...*/)
public void publicFunc( ) (/*...*/}
private void privateFunc( )(/*...*/}
public int publicValue ;
private int privatevalue ;
Распределенные вычисления Часть V
):
public class Silly extends Goofy ( Silly( ) { super( ) ; ) Silly( String name ) ( } Silly( String name, int value ) { this ( name ) ; } public void publicFunc( ) ( super.publicFunc( ) ; //... } public int publicvalue ; }
Функция-член publicFunc() в Goofy перекрывается в классе Silly. Экземпляр переменной publicValue в Silly скрывает экземпляр переменной с тем же самым именем в Goofy. Конструктор в Silly, который имеет два параметра (String и int), вызывает конструктор внутри Silly, который имеет единственный параметр String. Заданный по умолчанию конструктор в Silly вызывает заданный по умолчанию конструктор в базовом классе Goofy. Необходимо помнить, что, если вы используете ключевые слова super или this для вызова конструктора или функции-члена суперкласса, то эти ключевые слова должны быть первыми внутри конструктора или функции-члена.
Множественное наследование
В языке Java не существует множественного наследования, но его можно смоделировать, используя интерфейсы. Следующий пример показывает порожденный класс, реализующий несколько интерфейсов:
interface Setup { public void delivery( String text ) ; //. . . ) interface PunchLine < public void pause( ) ; //. . . public long timeToHesitate = 2000 ; } public class Joke implements Setup, PunchLine ( Joke( ){/*...*/} public void delivery( String text ) {/*...*/} public void pause( ) (/*...*/ } //... }
Можно также расширить единственный базовый класс в дополнение к реализации из нескольких интерфейсов, как показано в следующем фрагменте программного кода:
public class Joke extends Laughter implements Setup, PunchLine < Joke( )(/*...*/} public void delivery( String text ) {/*...*/ 1 public void pause( ) (/*...*/} //... 1
Глава 22
Обработка исключений
Механизм обработки исключений в Java очень похож на механизм, используемый в C++. В Java добавляется новое ключевое слово к средствам обработки исключений: finally. Блок finally всегда выполняется пост ле выполнения всех блоков try и catch. Любой объект, который попал в Java, должен быть унаследован от класса Throwable. Рассмотрим пример: //...'
public void func( ) throws lOException < ImportantObject iObj = new ImportantResource( ); try ( iObj.method( ) ; } catch( ImportantObjectException e) { // выполнить любу» требуемую обработку } catch( Throwable e) {"
// обработать исключение Throwable } finally { iObj.cleanup( ) ; // определенный метод очистки } ) Если исключение встроено внутри блока try, управление передается соответствующему блоку catch. После выполнения операторов catch управление передается блоку finally. Тем самым обеспечивается механизм, гарантирующий действительную очистку. Вы можете быть уверены в том, что управление в конечном счете перейдет к блоку finally.
Одна из отличительных возможностей обработки исключений в языке Java состоит в том, что проверка выполняется во время компиляции для соответствующих спецификаций исключения. В языке C++ невозможно обнаружить неподходящее использование исключения до времени выполнения. Кроме того, в Java любые функции-члены порожденного класса, которые перекрывают функцию базового класса, должны соответствовать спецификациям исключения базовой функции-члена.
В C++ многоточие (...) используется совместно с catch для создания фразы типа “любой catch". Следующий пример иллюстрирует эту концепцию:
try {
//... } catch( ... ){
// обрабатывать все случаи )
В Java перехватывается объект Throwable, как показано в следующем примере:
try (.
//... } catch( Throwable е ) //..."
catch (Th rowable) может использоваться как "весь catch"
Резюме
В этой главе представлен краткий обзор сходств и различий между языками C++ и Java.
Java поддерживает как блочный стиль, так и стиль единственной строки для комментариев, подобно тому как это производится в C++. Java предлагает дополнительные преимущества, заключающиеся в использовании документирующих комментариев. Применение утилиты javadoc позволит создавать документацию приложения на базе текста исходного программного кода, который документируется с помощью специальных маркерных ключевых слов.
Язык Java поддерживает примитивные типы данных: byte, boolean, char, short, int, long, float и double. Размерности этих типов данных являются непротиворечивыми для всех платформ, поддерживающих Java.
Часть V
Java не поддерживает struct или union; тип class заменяет эти типы. В Java нет указателей, но существуют ссылочные типы. В отличие от C++, ссылки в Java могут быть повторно связаны с другим объектом.
Язык Java поддерживает все арифметические, условные операторы и операторы отношений, как и C++, но также добавляет оператор тройного смещения вправо »>. Все операторы присваивания, имеющиеся в C++, присутствуют в Java. В Java также добавляется оператор присваивания тройного смещения вправо.
Перегрузка элементов поддерживается в Java, но перегрузки операторов, подобной в C++, не существует. Исключение для этого правила — класс Java String; оператор + является перегружаемым, что обеспечивает конкатенацию String.
Все операторы управления потоком данных и циклы, существующие в C++, также поддерживаются в Java. Ключевое слово goto зарезервировано в Java, но в настоящее время не поддерживается.
Язык Java использует возможности сборщика мусора для автоматической утилизации неиспользуемой памяти. Java не имеет деструкторов, подобно C++. Он поддерживает ключевое слово new, обеспечивающее создание объектов. Метод finalize() вызывается сборщиком мусора для объектов, которые не нужны; однако никогда нельзя знать, когда сборщик мусора вызовет метод finalize().
Java не использует файлы include, директивы #define или typedefs, поскольку отсутствует препроцессор.
Спецификаторы доступа public, protected и private поддерживаются в Java, как и в C++. Язык Java добавляет заданный по умолчанию спецификатор packet, если не определен никакой тип доступа. Ключевые слова спецификатора доступа используются при объявлении класса и элементов класса.
Примитивные типы данных в Java передаются по значению. Объекты в Java передаются по ссылке.
Java не поддерживает внешние (глобальные) функции, как это делается в C++. Все функции должны быть элементами некоторого класса Java.
В языке Java не существует понятия enum, но логика его действия может моделироваться с помощью переменных-членов как public static final.
Объект String в Java — объект первого класса, являющийся неизменяемым (т.е. вы не можете изменить содержимое). Java предлагает класс StringBuffer, если необходимо изменить содержимое строки.
Массивы в Java — это также объекты первого класса, включающие элемент lenght, который указывает число элементов в массиве. Язык Java также предлагает проверку на соответствие границам массива во время выполнения. Как и C++, Java поддерживает классы. Класс в Java может иметь спецификатор доступа, устанавливающий ограничение видимости. Ключевое слово static при использовании вместе с переменной- членом имеет то же самое значение в Java, что и в C++. Ключевое слово final, используемое с переменной класса, имеет тот же самый эффект, что и константы в C++.
Java не поддерживает множественное наследование, но зато обеспечивает реализацию множественных интерфейсов; интерфейс определяет абстракцию, подобную абстрактным типам данных в C++. Java также включает ключевое слово abstract, которое используется для определения класса, представляющего абстрактный интерфейс. Различие между interface и abstract заключается в том, что внутри класса interface вы можете обеспечивать заданные по умолчанию функциональные возможности методов. Можно также выполнять отдельное наследование из абстрактного класса. Java использует отдельно образованную иерархию наследования: корневой класс называется Object.
В Java гарантирована инициализация элемента. Примитивные данные по умолчанию обнуляются, и ссылки — устанавливаются в NULL.
Ключевое слово this, имеющееся в C++, также присутствует и в Java, но здесь оно является ссылкой, а не указателем.
Конструктор в Java имеет то же самое имя, что и класс, и может быть перегружаемым, как и в C++. Вы можете вызывать конструктор суперкласса, используя ключевое слово super (оно должно быть первым). Ключевое слово this может использоваться для вызовов других конструкторов внутри класса; оно должно быть первым вызовом внутри конструктора.
Обработка исключений в Java подобна аналогичному механизму C++. Java добавляет ключевое слово finally, определяющее блок программного кода, который будет всегда выполняться после выполнения всех блоков try и catch.
Содержимое CD-ROM
CD-ROM содержит программный код примеров, рассмотренных в книге, а также различные вспомогательные программные продукты.
Указания по установке в Windows NT 3.5.1
Вставьте CD-ROM в CD-ROM-дисковод.
В окне File Manager или Program Manager в меню File выберите команду Run.
В появившемся окне введите <диск>\8ТАВТ.ЕХЕ, где <диск> соответствует букве CD-ROM-дисковода, и нажмите клавишу Enter.
Следуйте появляющимся на экране монитора указаниям, чтобы завершить процесс установки.
Указания по установке в Windows 95/98 и NT4
Вставьте CD-ROM в CD-ROM-дисковод.
Дважды щелкните на пиктограмме Му Computer на рабочем столе.
Дважды щелкните на пиктограмме, соответствующей CD-ROM-дисководу.
В появившемся окне дважды щелкните на пиктограмме файла START.EXE.
Следуйте появляющимся на экране монитора указаниям, чтобы завершить процесс установки.
ПРИМЕЧАНИЕ
Для Windows 95/98 и NT4 можно активизировать функцию Ayto Play, что позволит запускать на выполнение START.EXE . после вставки CD-ROM в СО-ДОМ-дисковод.
Указания по установке в среде UNIX
Для пользователей UNIX предлагаются специфические указания по установке этой версии программы из CD-ROM. Обратитесь к администратору UNIX, чтобы получить помощь для применения команд по установке.
ПРИМЕЧАНИЕ
Перед подключением CD-ROM командой <mountpoint>, убедитесь в том, что она существует или работоспособна. Если операционная система работает, то подключение будет выполнено автоматически.
Для установки выполните следующие действия: 1. Вставьте CD-ROM в CD-ROM-дисковод.
Подключите CD-ROM, используя команду
mount [options] dev/cdrom /<mountpoints>
Создайте каталог, например cppunl вводом команды inkdir /cppunl
Перейдите в созданный каталог
cd /eppunl
Теперь вы готовы к копированию программного кода. Для копирования источников кода из CD-ROM в локальный каталог введите
ср -г /<mountpoints/source/*.
*ia = 5 * j++;
cout « "ListA" « "\n"; showList(ListA); // удалить первый элемент ListA.pop_front(); cout « "First element removed:\n"; showList(ListA);
11 вставить новый элемент в начало ListA.push_front(100);
cout « "Insert 100 at the beginning:\n"; showList(ListA);
return 0; }
// 11 Отобразить список элементов И template<class Т, class А> void showList(const list<T, A>s aList) ( cout « "sized = " « aList.sized « ":\t"; for (list<T, A>: :const_iterator i = aList.begin() ; i != aList.end() ; ++i) cout « *i « ", ";
Листинг 6.8. Слияние и сортировка списков
#include <iostream>
#include <list> using namespace std; typedef list<int>intList;
typedef list<int>:'.iterator intListltor;
templateCclass T, class A> void showList(const listCT, A>S aList);
int main() {
Вывод из листинга 6.8 показан ниже:
ListA sized = 5:0, 5, 10, 15, 20,
ListB sized = 6:20, 18, 16, 14, 12, 10,
Merge unsorted lists:
ListA sized = 11:0, 5, 10, 15, 20, 20, 18, 16, 14, 12, 10,