Текст
                    Дж.Либерти
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 Часть П. Вопросы реализации 89 Глава 4. Управление памятью 90 Управление памятью и указатели 91 Расход памяти 94 Распределение массивов 94 Паразитные, болтающиеся и дикие указатели 95 Указатели const 96 Указатели const и функции-члены const 96 Передача по ссылке 96 Передача указателя const 97 Возврат ссылки на объект, который не находится в области 97 видимости Указатель, указатель, указатель 99 Указатели и исключения 100 Использование автоуказателей 104 Подсчет ссылок 108 Резюме 118 Глава 5. Использование каркасов приложений 119 Microsoft Foundation Classes 120 Приступая к изучению 120
Другие мастера 122 В перспективе 122 Архитектура приложения 122 Многопоточность 122 Кооперативная многопоточность против вытесняющей 123 Проблемы вытесняющей многопоточности 123 Пример для изучения 124 Создание потоков 125 Пример 129 Служебные классы 137 Классы манипулирования строками 137 Классы времени 137 Документы и представления 138 Представления 139 Резюме 147 Глава 6. Контейнерные классы библиотеки STL 148 Определение и реализация шаблонов 149 Определение и реализация шаблонов функций 149 Определение и реализация шаблонов классов 149 Последовательные контейнеры 150 Контейнер-вектор 150 Контейнер-список 163 Контейнер-дека 172 Стеки 173 Очереди 175 Приоритетные очереди 177 Связанные контейнеры 178 Контейнер-запись 178 Множественные записи 187 Контейнер-набор 189 Множественные наборы 189 Вопросы производительности 190 Использование стандартной библиотеки C++ 190 Конструирование типов элементов 191 Резюме 192 Глава 7. Итераторы и алгоритмы STL 193 Классы итераторов 194 Позиция внутри контейнера 194 Типы итераторов контейнеров 194 Базовый класс итераторов 195 Вводные итераторы 196 Выводные итераторы 197 Пересылаемые итераторы 197 Двунаправленные итераторы 197
Итераторы произвольного доступа 197 Итераторные операции 197 Классы стандартных итераторов 199 Объекты-функции 200 Предикаты 201 Арифметические функции 202 Алгоритмы STL 203 Операции немутирующих последовательностей 203 Алгоритмы мутирующих последовательностей 211 Операции сортировки и связности последовательностей 221 Стандартные функции 238 Резюме 243 Глава 8. Исключение конфликтов имен 244 Функции и классы, разрешаемые по именам 245 Создание пространства имен 248 Использование пространства имен 250 Ключевое слово using 252 Объявление using 254 Псевдоним пространства имен 255 Неименованное пространство имен 255 Стандартное пространство имен 256 Резюме 257 Глава 9. Манипулирование типами объектов 258 Оператор typeid() 259 Класс typeinfo 259 Конструктор для класса typeinfo 260 Операторы сравнения 260 Функция-член name() 261 Функция-член before() 263 Оператор typeid() в конструкторах и деструкторах 264 Неправильное использование typeid() 264 Динамическое приведение объектов 265 Оператор dynamic_cast() 266 Операторы typeid() и dynamiccast 269 Другие операторы приведения 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 Компромиссы RTTI 285 Управление памятью для временных объектов 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-Toe 340 Применение альфа-бета-отсечений 340 Задача коммивояжера 341 Внешний поиск 342 Индексированный последовательный доступ 342 Двоичные деревья 342 Деревья 2-3-4 343 Резюме 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 Язык структурированных запросов SQL 420 Нормирование 420 Соединения 421 Живучесть для реляционной базы данных 421 Перемещения с идентификаторами объектов 422 Использование пятен 422 Скрытие деталей 423 Непосредственное сохранение объектов 423 Использование API БД 423 Доступ к источникам данных ODBC 423 Использование MFC 425 Операторы SQL 433 Установка характеристик БД 434 Резюме 434 Глава 17. Реализация живучести объектов с помощью реляционных 435 баз данных Объекты в Oracle 8 436 Типы объектов 436 Ссылки на объекты 437 Коллекции 438 Использование внешних процедур, разработанных на языке C++ 439 Отображение UML-диаграмм на объектно-реляционную базу данных 442 Проектирование базы данных 442 Генерирование классов на C++ 443 Генерирование сервера 445 Пример: Система заказа покупок 448 Описание системы 448 Глава 18. Объектно-ориентированные базы данных 451 Обзор объектно-ориентированных баз данных 452 Стандарт ODMG 453 Приложение на C++ для ведения счетов 453
Живучесть данных 466 Схемы базы данных и средства захвата этой схемы 468 Коллекции 469 Итераторы 471 Отношения 471 Базы данных и транзакции 475 Технические вопросы объектно-ориентированных баз данных 487 Архитектура клиент/сервер 487 Хранение данных и кластеризация объектов 487 Передача данных 488 Блокирование данных 488 Резюме 489 Глава 19. Защита приложений с помощью шифрования 490 Краткая история шифрования 491 Роль Национального бюро стандартов 491 Понятие шифрования 492 Коды 492 Шифры 493 Шифр Vernam 495 Криптография по частному ключу 497 Алгоритмы частного ключа 497 Механизмы шифрования по секретному ключу 498 Использование центров распространения ключей 500 Криптография по общему ключу 500 Метод головоломки Ральфа Меркле 501 Многопользовательские криптографические методы Диффи- 501 Хельмана Метод RSA 502 Использование Pretty Good Privacy 502 Выбор простых чисел в PGP 503 Использование случайных чисел в криптографии 504 Шифрование файлов с помощью PGP 504 Ограничения в криптографии 506 Юридические ограничения на криптографию 507 Криптографические атаки 507 Атака грубой силы 507 Криптоанализ 508 Взлом файла, зашифрованного программой PGP 508 Цифровые подписи 509 Public Key Cryptography Standard(PKCS) 509 Digital Signature Standard(DSS) 509 Неотрицание 510 Коммерческие криптографические продукты 510 Безопасные Web-клиенты 510
Безопасные почтовые клиенты 511 Продукты для защиты рабочего стола 512 Резюме 514 Часть V. Распределенные вычисления 515 Глава 20. CORBA 516 Теория и обоснование 517 Минимальная среда CORBА 518 Каркас объектной технологии 518 ПОР: объектное склеивание 519 Компонентная модель 520 IDL: соглашение связывания 520 Сравнение IDL с определением класса C++ 521 Брокер объектных запросов 522 Время жизни объектов 522 Среды разработки 523 Сравнение сред CORBA 523 Способность ORB к взаимодействию 523 Создание клиента С+ 524 Генерирование заглушки 524 Связь с ORB 525 Вызовы методов 525 Завершенное клиентское приложение C++ 526 Создание сервера C++ 526 Генерирование скелета 527 Реализация методов сервера 527 Подсоединение класса сервера 528 Загрузка BOA в ORB 528 Клиент Java 530 Генерирование заглушки 530 Запуск и программный код вызова метода 530 Стратеги и тестирования 531 Трассировка 531 Службы мониторинга и регистрации 531 Обработка исключений 531 Удаленная отладка 532 Служба имен и способность к взаимодействию 532 Interoperable Object Reference (IOR) 532 Именование контекстов 533 Проблемы взаимодействия 533 Производительность 533 Перерасход памяти со стороны ORB 534 Степень детализации интерфейса 534 Ссылки на передаваемый объект 534 Резюме 534
Глава 21. COM 535 Основы COM 536 Архитектура COM 537 Интерфейсы 537 Интерфейс IUnknown 541 СОМ-объекты 543 Библиотеки типов 545 Другие СОМ-технологии 546 Использование СОМ-объектов в C++ 549 Использование интерфейсов Raw 550 Использование интеллектуальных указателей 551 Использование библиотек типов 555 Создание СОМ-объектов в C++ 558 Множественное наследование 558 Вложенные классы 560 Использование классов tear off 562 Резюме 563 Дополнительная литература 563 Глава 22. Java и C++ 564 Общие черты C++ и Java 565 Комментарии 565 Типы данных 565 Операторы 567 Операторы управления потоком 568 Различия между C++ и Java 568 Управление памятью 568 Отсутствие указателей 568 Отсутствие препроцессора 568 Отсутствие деструктора 568 Спецификаторы доступа 569 Параметры метода 569 Внешние функции 570 Перечислители 570 Строки 571 Массивы 571 Объектно-ориентированные возможности Java 572 Классы 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 в Бангалоре Индия. Профессиональные интересы Вишваджита связаны с распределенны- распределенными вычислительными системами, исследованием и разработкой больших ЭВМ и изучением объектной тех- технологии. С ним можно связаться по адресу: vishwajH@tecbnologist.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 Pace) — один из соавгоров книги COBRA Unleashed. Он имеет 14-летний опыт разработки всевозможных видов современного программного обеспечения, написал множество компью- компьютерных программ — от интерактивных обучающих игр до систем управления большими предприятиями. Чарльз мечтает предоставить разработчикам такие возможности, которые позволили бы им создавать приложения, основанные на новейших методиках разработки программных средств. Магри Тхаккар (Meghraj Thakkar) — технический специалист компании Oracle Corp. Ему присвоена сте- степень магистра информационных технологий и степень бакалавра электроники. Он имеет несколько свиде- свидетельств, удостоверяющих его высокие профессиональные качества, в том числе диплом системного инженера высшей квалификации Microsoft, Novell, Oracle, диплом консультанта высшей квалификации Lotus и дру- другие. Магри прочитал несколько курсов в Калифорнийском университете в городе Ирвине. Он является со- соавтором нескольких книг, таких как Special Edition Using Oracle8, 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).
Объектно- ориентированное программирование ЧАСТЬ В ЭТОЙ ЧАСТИ Объектно-ориентированный анализ и проектирование Проектирование классов в C++ Наследование, полиморфизм и повторное использование программных кодов
Объектно-ориентированный анализ и проектирование В ЭТОЙ ГЛАВЕ Построение моделей Разработка программ: язык моделирования Разработка программ: процессы Замысел Анализ требований Проектирование
Объектно-ориентированный анализ и проектирование Глава 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.2. Изображение РИСУНОК 1.1. Обобщение/конкретизация. специализации в UML. В языке UML классы изображаются в виде прямоугольников, наследование — в виде линии со стрел- стрелкой. Интересно отметить, что на такой диаграмме стрелка направлена от специализированного класса в сторону более общего класса. Направление стрелки противоречит интуитивному представлению большин- большинства разработчиков, однако это не имеет никакого значения; если имеется соответствующее соглашение, система работает превосходно. Детали UML достаточно просты. Диаграммы нетрудно использовать или понять, автор будет давать со- соответствующие пояснения по мере изучения этой главы и книги, так что мы не будем изучать язык вне контекста. И хотя об языке UML можно написать целую книгу, все дело в том, что в основном вы будете пользоваться лишь небольшим подмножеством обозначений UML, и это подмножество нетрудно освоить по ходу изложения материала. Разработка программ: процессы Процесс объектно-ориентированного анализа и проектирования — намного более сложное и ответственное дело, чем язык моделирования. Тем не менее, о нем вы слышите гораздо реже. Это можно объяснить тем, что вопросы, касающиеся языков моделирования, более или менее успешно решены, выбрана также и область применения UML. В то же время дискуссии по поводу процесса в самом разгаре. Методолог — это специалист, который разрабатывает или изучает один или большее число методов. Обычно методологи разрабатывают и публикуют свои собственные методы. Метод представляет собой мо- моделирующий язык и процесс. Тремя ведущими методологами являются Грейди Буч (Grady Booch), автор метода Буча, Айвер Джекобсон (Ivar Jacobson), который разработал метод проектирования объектно-ори- объектно-ориентированного программного обеспечения, и Джеймс Рамбоф (James Rumbaugh), который разработал тех- технологию объектного моделирования (ОМТ — Object Modeling Technology). Эти три специалиста объединили свои усилия в целях создания Objectory — метода и коммерческого продукта компании Rational Software, Inc. Все три специалиста являются служащими компании Rational Software, где их коллеги любовно назы- называют их тремя мушкетерами.
Объектно-ориентированный анализ и проектирование РИСУНОК 1.3. Каскадный метод. Глава 1 В настоящей главе кратко описывается Objectory. Автор не приводит его точного описания, поскольку он не является сторонником слепой преданности умозрительной теории — он больше заинтересован в по- поставках готового продукта, чем в приверженности к какому-либо методу. Другие методы также имеют свои достоинства, а автор старается придерживаться эклектической точки зрения и собирать фрагменты по мере продвижения и объединять их в единую работоспособную конструкцию. Процесс проектирования программного обеспечения носит итеративный характер. Это означает, что по мере продвижения разработки ПО мы многократно проходим через весь процесс, чтобы проникнуться лучшим пониманием предъявляемых требований. Проект определяет направление реализации, но детали, обнаруженные во время реализации, вносят коррективы в проект в виде обратной связи. Самое главное то, что мы даже не пытаемся разрабатывать важный проект как единую упорядоченную последовательность действий; вместо этого мы неоднократно возвращаемся к конкретным фрагментам проекта, постоянно совершенствуя проект и улучшая его реализацию. Итеративная разработка отличается от каскадного проектирования. В условиях каскадного проектирования выход одной ступени становится входом для следующей, при этом возврат на предыдущие ступени ис- исключается (рис. 1.3), требования передаются разработчику, после чего разработчик создает проект (и следит за воплощением своего замысла) и передает его программисту, который и реализует его. В свою очередь, программист передает программные коды QA-персоналу (Question- Answer), который производит его тестирование и передает заказчику. Гладко в теории, стихийное бедствие на практике. В условиях итеративного проектирования некий мечтатель предлагает свою концепцию, после чего мы приступаем к воплощению замысла. По мере того как мы углубляемся в детали, концепция расширяется и углубляется. Если требования четко сформулированы, мы начинаем проектные работы, отчетливо сознавая, что все вопросы, которые возникнут в процессе проектных работ, могут потребовать уточнения. В процессе работы над проектом мы создаем прототипы, а затем воплощаем их в программный продукт. Проблемы, возникающие в процессе проектирования, приводят к внесению изменений в проект, они могут оказать влияние на наше понимание требований. Самое главное заключа- заключается в том, что мы каждый раз разрабатываем и реализуем только конкретные фрагменты полного про- программного продукта, многократно возвращаясь к фазам проектирования и реализации. Несмотря на то что этапы процесса многократно повторяются, их почти невозможно описать таким циклическим образом. Поэтому они могут быть описаны в такой последовательности: замысел, анализ, проект, реализация, тестирование, откат. На практике в процессе создания единого продукта мы много- многократно проходим через каждый из этих этапов. Итеративный процесс трудно представить и понять, если зацикливаться на каждом этапе; поэтому будем описывать их один за другим. В итеративном процессе разработки можно выделить следующие этапы: 1. Постановка задачи 2. Анализ 3. Проектирование 4. Реализация 5. Тестирование 6. Распространение Постановка задачи представляет собой формулировку замысла. В одном предложении излагается гранди- грандиозная идея. Анализ — это процесс понимания требований. Проектирование — процесс создания модели классов, из которых вы будете генерировать программный код. Реализация представляет собой написание программных кодов на языке C++; тестирование предназначено для того, чтобы убедиться в том, что вы все сделали правильно, а распространение — это доставка программного продукта заказчикам. Здесь указа- указано самое главное. Все остальное — это несущественные подробности. Бесконечные споры ведутся относительно того, что происходит на каждом этапе итеративного процесса проектирова- проектирования, спорят даже о том, как назвать эти этапы. На практике это не имеет никакого значения. Фактически основные этапы любого процесса заключаются в том, чтобы выяснить, что вы хотите построить, найти решение поставленной задачи и реализовать проект.
__ Объектно-ориентированное программирование Часть I Если группы новостей и списки рассылки объектной технологии мало чем отличаются друг от друга, то существенные особенности объектно-ориентированного анализа и проектирования ярко выражены. В этой главе автор предлагает практический подход к процессу, который послужит краеугольным камнем для построения архитектуры вашего прило- приложения. В последующих главах мы сосредоточимся на обсуждении деталей реализации проекта на языке C++. Цель всей этой работы состоит в том, чтобы написать такой программный код, который решает поставленную задачу, надежен, допускает расширения и удобен в эксплуатации. Но самое главное — это задача создания высококачествен- высококачественного программного кода в условиях ограничений по времени и по финансированию. Замысел Все крупные программные проекты начинаются с замысла. Конкретный индивидуум проникается мыс- мыслью, что неплохо было бы создать программный продукт, наделенный теми или иными свойствами. Редко, когда грандиозные замыслы становятся плодами коллективной мысли. Самая первая стадия объектно-ори- объектно-ориентированного анализа заключается в том, чтобы сформулировать этот замысел в виде отдельного предло- предложения (или максимум, в виде короткого абзаца). Замысел становится руководящим принципом разработки, и коллектив, который формируется для реализации этого замысла, может обращаться к нему — и при необходимости вносить в него изменения — по мере продвижения этих разработок. Даже если авторство формулировки замысла принадлежит отделу маркетинга, все равно должно быть назначено ответственное лицо, выполняющее роль постановщика. Именно в его обязанности входит хране- хранение священного огня. По мере продвижения к цели постановка задачи совершенствуется. Сроки разработки и выхода программного продукта на рынок могут повлиять на то, что вы намерены завершить на первой итерации, но постановщик должен следить за воплощением главной идеи, чтобы то, что создается, отра- отражало основной замысел с высокой точностью. Только твердая приверженность замыслу, неукоснительное выполнение обязательств приводят к успешному завершению проекта. Если вы измените замыслу, ваш проект обречен на неудачу. Анализ требований Фаза постановки задачи, в процессе которой формулируется замысел, весьма кратка. Это может быть всего лишь секундное озарение, плюс время, необходимое для записи на бумаге того, что имел в виду постановщик. Часто бывает так, что вы, будучи специалистом по ООП, подключаетесь к проекту уже пос- после того, как замысел сформулирован. Некоторые компании путают понятия замысла и постановку задачи с требованиями. Четкий замысел необходим, но недостаточен. Прежде чем перейти к анализу, вы должны четко представлять, как будет использован программный продукт и как он будет функционировать. Назначение фазы анализа состоит в том, чтобы определить и сформулировать соответствующие требования. Результатом фазы анализа является документ, содержащий требования. В первом разделе требований должен содержаться анализ примеров использования. Примеры использования Движущей силой анализа, проектирования и реализации являются примеры использования. Пример ис- использования есть не что иное, как высококачественное описание того, как будет использован программный продукт. Примеры использования приводят в движение не только анализ, но и проектирование, они помо- помогают определить классы, играют исключительно важную роль при тестировании программного продукта. Создание четко очерченного и исчерпывающего набора примеров использования может оказаться един- единственной важной задачей анализа. Именно в этой части вы в наибольшей степени зависите от экспертов в области применения; именно они обладают самой полной информацией о требованиях, предъявляемых коммерческой областью применения, которые вы пытаетесь собрать. Примеры использования практически не учитывают особенностей пользовательского интерфейса и со- совсем не учитывают внутренней структуры системы, которую вы создаете. Любая система или лицо, взаи- взаимодействующее с проектируемой системой, называется действующим субъектом. В качестве обобщения приведем некоторые определения: ¦ Пример применения: описание того, как будет использоваться ПО. ¦ Специалисты в предметной области: специалисты, имеющие опыт работы в конкретной области ком- коммерческой деятельности, для которой создается программный продукт. ¦ Действующий субъект: любое лицо или система, взаимодействующая с системой, которую вы разра- разрабатываете.
Объектно-ориентированный анализ и проектирование Глава 1 Пример использования — это описание взаимодействия между действующим объектом и самой систе- системой. Для целей анализа примеров применения система рассматривается как "черный ящик". Действующий субъект "посылает сообщение" системе, и непременно что-то случается: возвращается информация, меня- меняется состояние системы, космический корабль меняет курс или нечто подобное. Идентификация действующего субъекта Важно отметить, что не все действующие субъекты — люди. Системы, которые взаимодействуют с си- системой, которую вы строите, — также действующие субъекты. Следовательно, если мы создавали автома- автоматизированный кассовый аппарат, действующими субъектами могут быть как клиенты, так и банковские служащие — равно как и система контроля за ипотеками. Наиболее важными характеристиками действую- действующих субъектов являются следующие: ¦ Являются внешними по отношению к проектируемой системе ¦ Взаимодействуют с проектируемой системой Инициация часто бывает наиболее трудной фазой анализа примеров применения. Часто бывает так, что наилучшим способом начать анализ является "мозговой штурм". Просто начните составлять список людей и систем, которые будут взаимодействовать с вашей новой системой. Помните о том, что когда мы обсужда- обсуждаем людей, то имеем в виду роли — банковского служащего, руководителя, клиента и др. Одно лицо может исполнять несколько ролей. Что касается примера ATM-машины (Automated Teller Machine — автоматизированный кассовый аппа- аппарат), приведенного чуть выше, считаем, что такой список будет содержать следующие роли: ¦ Клиент ¦ Банковский персонал ¦ Учрежденческая система ¦ Лицо, которое наполняет ATM-машину деньгами и материалами На первых порах нет необходимости расширять этот очевидный список. Для того чтобы вы освоили операцию генерирования примеров применения, достаточно того, чтобы вы выполнили генерирование трех или четырех действующих субъектов. Каждый из этих субъектов взаимодействует с системой по-своему. Мы хотим интегрировать эти взаимодействия в наши примеры применения. Определение первых примеров применения Начнем с роли клиента. Для клиента можно применить процедуру мозгового штурма к следующим при- примерам использования: ¦ Клиент проверяет состояние собственного баланса ¦ Клиент кладет деньги на свой счет ¦ Клиент снимает деньги со своего счета ¦ Клиент переводит деньги с одного счета на другой ¦ Клиент открывает счет ¦ Клиент закрывает счет Должны ли мы проводить различие между "Клиент депонирует деньги на свой текущий счет" и "Клиент депонирует деньги на свой сберегательный счет" или мы должны объединить эти действия (что мы и сде- сделали в предшествующем списке) в операцию "Клиент вносит денежную сумму на свой счет"? Ответ на этот вопрос зависит от того, имеет ли значение такое различие в рассматриваемой области. Чтобы определить, являются ли эти действия одним или двумя примерами применения, вы должны получить ответ на такие вопросы: являются ли механизмы различными (существенно ли отличаются опе- операции клиента над счетами) и являются ли результаты этих операции различными (реагирует ли система на эти действия по-разному). Ответ на оба вопроса, касающихся вкладов, — "нет". Клиент откладывает де- денежные суммы на оба счета практически одинаково, и результаты его действий практически аналогичны; ATM-машина отреагирует на них тем, что увеличит остаток на соответствующем счете. При условии, что поведение и реакция действующего субъекта и системы в большей или меньшей сте- степени идентичны, независимо от того, сделан вклад на текущий или сберегательный счет, оба эти примера использования фактически являются одним и тем же примером. Позднее, когда мы будем создавать сцена-
Объектно-ориентированное программирование Часть! рии примера применения, мы можем выполнить два изменения и посмотреть, сможем ли мы вообще об- обнаружить какое-либо различие. В процессе изучения каждого действующего субъекта вы сможете обнаружить дополнительные примеры использования, пытаясь найти ответы на следующие вопросы: ¦ Почему этот действующий субъект пользуется этой системой? Клиент использует эту систему для получения наличных денег, чтобы положить деньги на хранение или проверить остаток на счете. ¦ Какой результат действующий субъект ожидает получить от каждого требования? Положить деньги на счет или получить наличные деньги, чтобы сделать покупку. ¦ Что заставило действующий субъект воспользоваться услугами этой системы в данный момент? Он получил денежные поступления либо намеревается сделать покупку. ¦ Что должен сделать действующий субъект, чтобы воспользоваться этой системой? Вставить карту ATM в приемное устройство машины. Нам нужен пример применения для регистрации клиента в системе. ¦ Какую информацию должен действующий субъект предоставить системе? Он должен ввести персональный идентификационный номер. Мы должны воспользоваться примерами применения для получения и корректировки данных персонально- персонального идентификационного номера. ¦ Какую информация надеется получить действующий субъект от системы? Состояние счета и прочее. Вы часто можете найти дополнительные примеры применения, сосредоточиваясь на атрибутах объектов области применения. У клиента есть имя — PIN (Personal Identification Number — Персональный иденти- идентификационный номер) — и номер счета, должны ли мы создавать примеры применения, чтобы иметь воз- возможность манипулировать этими объектами? Счет характеризуется номером, состоянием, данными о состоявшихся транзакциях, должны ли мы вводить эти элементы в примеры применения? После того как мы выполним детальный анализ примеров применения клиента, следующим шагом при воплощении списка примеров применения состоит в создании примеров использования для каждого из остальных действующих субъектов. Представленный ниже список представляет собой набор примеров при- применения для случая АТМ-машины: ¦ Клиент проверяет состояние собственного счета ¦ Клиент кладет деньги на свой счет ¦ Клиент снимает деньги со своего счета ¦ Клиент переводит деньги с одного счета на другой ¦ Клиент открывает счет ¦ Клиент закрывает счет ¦ Клиент вносит запись в свой счет ¦ Клиент просматривает последние транзакции ¦ Банковский служащий вносит запись в специальный доверительный счет ¦ Банковский служащий выполняет корректировку счета клиента ¦ Учрежденческая система выполняет обновление счета пользователя, отражающего внешнюю деятельность ¦ Изменения в счете пользователя отображаются в учрежденческой системе ¦ Машина ATM сигнализирует о том, что наличность для раздачи закончилась ¦ Банковский технический персонал загружает машину ATM наличностью и принадлежностями ¦ Создание модели области применения Как только вы получите в первом приближении набор примеров использования, можете начинать раз- разработку требований с использованием детализированной модели области применения. Модель области при- применения — это документ, в котором содержатся все имеющиеся у вас сведения об области применения. В рамках модели области применения вы создаете объекты области применения, описывающие все объекты,
Объектно-ориентированный анализ и проектирование Глава 1 ссылки на которые имеются в примерах использования. На этой стадии пример ATM включает такие объекты, как клиент, банковский персонал, учрежденческие системы, текущий счет, сберегательный счет и др. Для каждого из этих объектов области применения мы хотим собрать такие важные данные как имя объекта (например, имя клиента, счета и т.п.), независимо от того, является ли этот объект действующим субъектом или нет, главные атрибуты объекта и его поведение и т.д. Многие моделирующие средства под- поддерживают сбор этой информации в описаниях "класса". На рис. 1.4 показано, как производится сбор этой информации программой Rational Rose. Очень важно сознавать, что описываемые категории не являются объектами проекта. Это скорее описа- описание того, как устроен мир, но не описание работы системы. Мы можем отобразить схематично взаимоотношения между объектами в области применения примера с машиной ATM, воспользовавшись языком UML. Мы можем пользоваться одними и теми же средствами на любой стадии проекта. Например, можно зафиксировать тот факт, что текущий и сберегательный счет — суть специализации более общего понятия банковского счета на языке UML, касающегося классов и отношений обобщения (рис. 1.5). he cuttomei « вру patron Ы the bank who has one or «ив accounts The custom» taeitheATM lodeposf «nay. tramlM between accounts, check hts balance and ogetcattv Объект области ПиИ МСгЮп ИЯ Обобщение РИСУНОК 1.4. Сбор информации программой Rational Rose. РИСУНОК 1.5. Специализация. На этом риунке прямоугольники представляют собой различные объекты области применения, а линии со стрелкой обозначают обобщения. Язык UML определяет, что эти линии чертятся в направлении от спе- специализированного класса к более общему "базовому" классу. Таким образом, как Текущий счет, так и Сбе- Сберегательный счет указывают на Банковский счет, что означает, что каждый из них являются специализацией Банковского счета. ПРИМЕЧАНИЕ Нелишне подчеркнуть то, что мы показываем на данной стадии, — это отношения между объектами в области приме- применения. Позднее вы, возможно, решите ввести объект CheckingAccount в свой проект, равно как и объект Bankaccount, и сможете реализовать это отношение с помощью наследования, но это уже будут решения, относящиеся к стадии разработки. При анализе все, что от нас требуется, — это документирование восприятия этих объектов в области применения. UML — это язык моделирования с богатыми возможностями, среди которых имеется множество отно- отношений. Однако основные отношения, которые используется в анализе, — это обобщение (или специали- специализация), ограничение и сопоставление. Обобщение Обобщение часто отождествляют с наследованием, но при этом имеет место четко выраженное и суще- существенное различие между этими понятиями. Обобщение описывает отношение, наследование — это про- программная реализация обобщения, т.е. как мы представляем обобщение в программных кодах. Из определения обобщения следует, что производный объект есть подтип базового типа. Таким обра- образом, текущий счет есть банковский счет. Отношение обладает симметрией: банковский счет объединяет в себе одинаковые особенности поведения и общие атрибуты текущего и сберегательного счетов.
Объектно-ориентированное программирование Часть I Во время анализа области применения мы стремимся отобразить такого рода отношения в том виде, в каком они существуют в реальном мире. Включение Часто конкретный объект состоит из множества подобъектов. Например, в конструкцию автомобиля входят двигатель, руль управления, шины, двери, радиоприемник и др. Текущий счет состоит из сальдо, данных об операциях, идентификатора клиента и др. Мы утверждаем, что текущий счет содержит эти элементы; в моделях включения имеется это отношение. Язык UML отображает отношение включения с помощью линии с ромбом от содержащего объекта к включаемому объекту, как показано на рис. 1.6. Из рисунка видно, что текущий счет "имеет" сальдо. Вы можете использовать сочетание этих диаграмм, чтобы отобразить сложные наборы отношений (рис. 1.7). Checking Account (Текущий счет) Checking .Account ОекуиЗии счет) 1 1 Bank Ас (Банковсю (СбереЙ «Hint ш счет) s Account , ельныи счет) J -* Агрегирование РИСУНОК 1.6. Отношение включения. РИСУНОК 1.7. Отношения между объектами. Объеет А Объект В 4 Связь РИСУНОК 1.8. Связь. На рисунке показано, что и текущий счет и сберегательный счет являются банковскими счетами и что у всех банковских счетов имеется сальдо и данные об операциях. Связи Третье отношение, которое обычно фиксируется на стадии ана- анализа области применения, — простейшая связь (ассоциация). Связь предполагает, что два объекта знают друг о друге и тем или иным способом взаимодействуют. Это определение становится намного более точным на стадии проектирования, но для целей анализа пред- предполагаем, что только Объект А взаимодействует с Объектом В, но ни один из них не содержит другой и не является специализацией другого. В UML показывают эту связь в виде прямой линии между объектами (рис. 1.8). На этом рисунке показано, что Объект А связан в той или иной степени с Объектом В. Разработка сценариев Теперь, когда у нас есть предварительный набор примеров использования и средств для отображения на диаграммах отношений между объектами области применения, мы готовы формализовать примеры ис- использования и приступить к более глубокому их изучению. Каждый пример использования можно разбить на последовательности сценариев. Сценарий — это опи- описание специфического набора обстоятельств, которые выделяют его из различных взаимозависимых эле- элементов примера использования. Например, "Клиент снимает деньги со своего счета" может иметь такие сценарии: ¦ Клиент обращается с требованием снять со счета $300, получает эту сумму, затем помещает налич- наличность в приемное устройство, после чего система печатает квитанцию о получении. ¦ Клиент обращается с требованием снять со счета $300, однако остаток на его счету составляет всего лишь $200. Клиент получает информацию о том, что на его текущем счете недостаточно наличнос- наличности, чтобы совершить эту операцию.
Объектно-ориентированный анализ и проектирование Глава 1 ¦ Клиент обращается с требованием снять $300 с текущего счета, однако он уже снял сегодня со счета $100, а разрешается снять не более $300 в течение дня. Клиент получает информацию о возникшей проблеме, после чего принимает решение. ¦ Клиент обращается с требованием снять со счета $300, но в аппарате закончилась бумага, на кото- которой печатаются квитанции. Клиент получает информацию о возникшей проблеме, после чего при- принимает решение, получать или не получать деньги без квитанции. И так далее. Каждый сценарий исследует различные варианты в рамках исходного примера использова- использования. Часто такими вариантами являются исключительные ситуации (не счете нет требуемой суммы, в ма- машине нет нужной суммы и т.д.). Иногда исследуются нюансы принятия решения в самом примере использования (например, хочет ли клиент перевести деньги прежде, чем их снимать со счета). Не каждый возможный сценарий нужно исследовать. Мы ищем те сценарии, которые затрагивают требования системы или детали взаимодействия с действующим субъектом. Выработка руководящих принципов Для документирования каждого сценария вам придется вырабатывать руководящие принципы. Вы вклю- включаете эти руководящие принципы в постановку задачи. Позаботьтесь о том, чтобы в каждом сценарии были отражены следующие моменты: ¦ Предварительные условия — какие условия должны быть выполнены, прежде чем сценарий начнет работать ¦ Триггеры — механизм запуска сценария ¦ Какие действия выполняют действующие субъекты ¦ Какие результаты и изменения вызваны системой ¦ Какую обратную связь получают действующие субъекты ¦ Имеются ли повторяющиеся действия и что вызывает их завершение ¦ Описание логического течения сценария ¦ Что вызывает окончание сценария ¦ Последующие условия — какие условия должны быть выполнены по завершении сценария В дополнение к этому у вас появится необходимость присвоить имена каждому примеру использования и каждому сценарию. Таким образом, может возникнуть такая ситуация: Пример использования: Клиент снимает наличность со счета Сценарий: Успешное получение со счета наличных денег Предварительные условия: Клиент уже зарегистрирован в системе Триггер: Клиент запрашивает "снятие со счета" Описание: Клиент предпочитает снять сумму наличными с текущего счета. На счете достаточно денег, машина ATM заправлена бумагой, а сеть исправна и функционирует. Машина ATM просит указать сумму, снимаемую со счета, а клиент называет $300 — допустимую на текущий момент сумму. Машина выдает $300 и печатает квитанцию, а клиент получает деньги и квитанцию. Последующие условия: На дебет счета клиента относится сумма $300, а клиент получает наличными $300. Этот пример использования может быть представлен с помощью простой диаграммы, показанной на рис. 1.9. испой РИСУНОК 1.9. Диаграмма примера использования. Пример пользования Клиент ^ 4 Связь
Объектно-ориентированное программирование Часть I На этой диаграмме практически нет никакой другой информации, кроме описания взаимодействия между действующим субъектом (клиентом) и системой, которое выполнено на высоком уровне абстракции. Польза от этой диаграммы несколько увеличится, если вы покажете взаимодействие между примерами использо- использования. Автор говорит "польза несколько увеличится", поскольку в рассматриваемом случае возможны толь- только два взаимодействия: "использует" и "расширяет". Стереотип "использует" указывает на то, что один пример использования это подмножество другого. Например, нельзя снять сумму со счета, не зарегистрировавшись в системе. Мы можем показать это отношение с помощью диаграммы, представленной на рис. 1.10. На этом рисунке показано, что пример использования Снимаемая со счета сумма "использует" пример использования Регистрация и тем самым полностью реализует Регистрацию как часть Снимаемой со счета суммы. Пример использования "расширяет" предназначен для того, чтобы указать условное отношение и нечто такое, что напоминает наследование, однако что касается примеров использования "использует" и "расши- "расширяет", то в объектном моделировании еще так много путаницы, что некоторые разработчики просто отка- отказались от примера "расширяет", чувствуя, что его смысл недостаточно хорошо им понятен. Автор пользуется стереотипом "использует" в тех случаях, когда без него ему пришлось бы копировать и вставлять в нужное место весь пример использования, а стереотипом "расширяет" — только когда он пользуется примером использования при определенных условиях. Диаграммы взаимодействия Хотя диаграмма примера использования сама по себе может иметь всего лишь ограниченную ценность, тем не менее, существуют диаграммы, которые можно ассоциировать с этим примером использования, что самым кардинальным образом может улучшить документирование и понимание взаимодействий. На- Например, мы знаем, что сценарий Снимаемая со счета сумма представляет взаимодействия между следую- следующими объектами области применения: клиент, текущий счет и пользовательский интерфейс. Можно документально зафиксировать это взаимодействие с помощью диаграммы взаимодействий, представлен- представленной на рис. 1.11. Диаграмма взаимодействий, показанная на этом рисунке, отображает подробности сценария, которые могут оказаться далеко не очевидными при чтении текста. Объекты, которые взаимодействуют, — это объекты области применения, а вся конструкция ATM/UI (Automated Teller Machine/User interface) рассматрива- рассматривается как единый объект с одним конкретным банковским счетом, вызываемым с любой степенью детали- детализации. Этот сравнительно простой пример с машиной ATM показывает всего лишь воображаемую совокуп- совокупность взаимодействий, однако выявление специфики этих взаимодействий может оказать существенную помощь для понимания как самой проблемы, так и требований вашей новой системы. Клиент Пользовательский интерфейс Текущий счет Withdraw Cash Снимаемая со счета сумма) Клиент "использует"» 1: Отмена запроса 2: Указать возможные варианты 3: Указать сумму и счет 7: Наличность для распределения через автомат 8: Квитанция о получении запроса 9: Печать квитанции 4: Контрольный баланс, финансовое положение и 5: Разрешение на обратную операцию 6: Дебит $300 пр. РИСУНОК 1.10. Стереотип "использует". РИСУНОК 1.11. Диаграмма взаимодействий в языке UML.
Объектно-ориентированный анализ и проектирование Глава 1 Создание пакета Поскольку вы генерируете множество примеров использования для любой достаточно сложной пробле- проблемы, язык UML позволяет сгруппировать ваши примеры использования в пакеты. Пакет подобен каталогу или папке — он является совокупностью моделирующих объектов (классов, действующих субъектов и т.д.). Чтобы правильно ориентироваться в хитросплетениях примеров использова- использования, их можно группировать по любой характеристике, лишь бы это имело смысл для решения вашей проблемы. Таким образом, можно группировать примеры использования по типу счета (любой признак, характеризующий текущие и сберегательные счета), по кредиту или дебету, по типу клиента или любой другой характеристике, которая имеет для вас смысл. Что еще важнее, один и тот же пример использова- использования может фигурировать в нескольких различных пакетах, обеспечивая большую гибкость на стадии про- проектирования. Анализ приложения В дополнение к примерам использования документ с требованиями будет содержать предположения и ограничения вашего клиента, его требования к аппаратным средствам и операционным системам. Утверж- Утверждение требований приложения является прерогативой вашего заказчика — обычно вы формулируете их на стадии проектирования и реализации, но решающее слово в их принятии остается за заказчиком. Требования приложения чаще всего обусловливаются необходимостью стыковки с существующей сис- системой. В такого рода случаях понимание того, что делают существующие системы и как они работают, является немаловажной составляющей вашего анализа. В идеальном случае вы анализируете проблему, находите ее решение, а затем решаете, какая платфор- платформа и операционная система наилучшим образом подходят для реализации проекта. Такой сценарий настолько идеален, насколько и редок. Гораздо чаще заказчик должен нести определенные затраты на изготовление специальной операционной системы или аппаратной платформы. Коммерческие планы заказчика зависят от того, насколько хорошо работает ваше ПО в рамках существующей системы, вы должны выявить соот- соответствующие требования на достаточно ранней стадии проектирования и проводить дальнейшую разработ- разработку с учетом принятых решений. Анализ систем Некоторые программные продукты написаны из расчета работы в автономном режиме и обеспечивают взаимодействие только с конечным пользователем. Однако часто возникает необходимость интерфейса с существующей системой. Системный анализ — это процесс сбора всех подробностей о системах, с которы- которыми вы взаимодействуете. Будет ли ваша новая система сервером, предоставляющим услуги существующей системе, или клиентом? Будет ли у вас возможность согласовывать интерфейс между системами или вам необходимо приноравливаться к существующему стандарту? Будет ли другие системы работать стабильно или вам все время придется стрелять по движущейся мишени? На эти и на другие связанные с ними вопросы необходимо найти ответы на стадии анализа, прежде чем вы начнете проектировать новую систему. Кроме того, вы, возможно, захотите неявно выявить конк- конкретные ограничения, которые, по предположению, имеют место при взаимодействии с другими система- системами. Не увеличат ли они времени ответа вашей системы? Не приведет ли это к повышению требований к новой системе, к увеличению расхода ресурсов и машинного времени? Планирование выпуска документов Как только вы поймете, что должна делать ваша система и каким должно быть ее поведение, можете приступать к созданию документа, определяющего сроки выполнения и сметную стоимость проекта. Часто сроки исполнения в директивном порядке определяются заказчиком: "Вы должны закончить работы за 18 месяцев". В идеальном случае вы изучаете эти требования и определяете, какое время потребуется на раз- разработку проектора и реализацию проектных решений. Но это идеальный случай, на практике большая часть систем все-таки выпускается в установленные сроки и без превышения сметной стоимости, однако весь вопрос в том, какие функциональные средства реализованы в ней за указанные сроки и при выделенном уровне финансирования. При подсчете сметной стоимости проекта и разработке временного графика необходимо придерживать- придерживаться следующих принципов: ¦ Если устанавливается диапазон какого-либо показателя, то чаще всего его верхний предел выбира- выбирается с большой долей оптимизма.
Объектно-ориентированное программирование Часть! ¦ Закон Либерти (Liberty Law) утверждает, что все длится дольше, чем вы ожидаете, даже если вы делаете поправку на Закон Либерти. В условиях реальной жизни возникает необходимость ввести приоритеты в вашей работе, иначе вы ее не закончите — это же так просто. Очень важно, чтобы, когда истекают сроки, то, что у вас есть, работало и чтобы оно "тянуло" на первую версию продукта. Если вы строите мост и не успеваете сдать его в эксп- эксплуатацию в срок, то, если у вас нет шансов построить на нем дорожку для велосипедистов, это, конечно, очень плохо, но, тем не менее, вы все же можете открыть движение по мосту и начать взимать плату за проезд. Но если все сроки вышли, а мост построен только наполовину, это еще хуже. При подготовке плановых документов очень важно сознавать, что они никуда не годятся. На такой ран- ранней стадии фактически невозможно дать надежную оценку длительности процесса разработки проекта. Как только будет готов документ с требованиями, вы получаете в свое распоряжение удобный рычаг управле- управления длительностью разработки проекта, достаточно точный метод оценки длительности реализации и смо- сможете прикинуть, как долго будет длиться тестирование системы. Затем вы должны предусмотреть резерв времени, достигающий, по меньшей мере, от 20 до 25% от всех временных затрат, этот резерв можно сокращать по мере продвижения работ и накопления необходимых данных. ПРИМЕЧАНИЕ Включение резерва времени в плановый документ не является предлогом для отказа от плановых документов. Это скорее предостережение не слишком полагаться на них на достаточно ранней стадии. По мере реализации проекта ваше представление о том, как работает система будет становиться все более полным, а ваши оценки — все более точными. Иллюстративный материал Завершающим фрагментом постановки задачи является выбор иллюстративного материала. Под иллюс- иллюстративным материалом мы подразумеваем диаграммы, картинки, фототипы и любые другие визуальные отображения, призванные помочь облегчить понимание и разработку графического пользовательского ин- интерфейса вашего продукта. Для многих больших проектов можно разработать полный прототип, чтобы помочь представить себе (а также и заказчикам), каким будет поведение системы. Для некоторых коллективов разработчиков прототип становится "живой" постановкой задачи; "реальная" система разрабатывается в целях реализации функци- функциональных средств, продемонстрированных в прототипе. Артефакты В конце каждой фазы анализа и разработки вы будете подготавливать целый ряд документов или "арте- "артефактов". В табл. 1.1 представлены документы, подготавливаемые на стадии анализа. Они нужны заказчику для того, чтобы быть уверенным в том, что вы правильно понимаете, что от вас требуется, конечным пользователям — чтобы поддерживать обратную связь с проектом и устанавливать руководящие принципы проектирования, и коллективам разработчиков — для правильной разработки и реализации программных кодов. Многие из этих документов содержат материал, исключительно важный как для коллектива, осуще- осуществляющего документирование системы, так и для Службы обеспечения качества (Quality Assurance), ко- который позволяет им предвидеть поведение системы. Таблица 1.1. Документы, созданные на стадии анализа хода разработки проекта Документ Описание Сообщение о примере использования Документ, содержащий подробную информацию о примерах использования, сценариях, стереотипах, предварительных условиях и иллюстративных материалах Анализ области применения Документ и диаграммы, описывающие отношения между объектами области применения Диаграммы анализа совместной работы Диаграммы совместной работы описывают взаимодействия между объектами проблемной области Диаграмма анализа действий Диаграмма действий, описывающая взаимодействия между объектами проблемной области Анализ систем Отчет и диаграммы, описывающие системы низкого уровня и аппаратные системы, на основе которых строится проект
Объектно-ориентированный анализ и проектирование Глава 1 Документ Описание Документ с анализом приложения Отчет и диаграммы, описывающие требования пользователя, характерные для данного конкретного проекта Отчет об ограничениях на рабочие Отчет, описывающий рабочие характеристики и ограничения, накладываемые характеристики на них заказчиком Стоимостной и плановый документы Отчет, в котором используются диаграммы Ганта (Gantt) и Перта (Pert) и который отражает плановые сроки, этапы и соответствующие стоимости Проектирование Анализ базируется на понимании проблемной области, в то время как целью проектирования является получение решения. Проектирование — это процесс воплощения нашего понимания задачи в модель, ко- которая может быть реализована в виде программного продукта. Результат этого процесса материализуется в виде проектной документации. Проектная документация содержит два раздела: Проектирование классов и Архитектурные механизмы. В свою очередь, раздел Проектирование классов состоит из подразделов статического (в котором детализи- детализируются различные классы, их отношения и характеристики) и динамического проектирования (в котором рассматриваются подробности взаимодействия классов). В разделе Архитектурные механизмы проектной документации подробно определяется, как достигается устойчивость объектов, их параллельное функционирование, система распределенных объектов и др. В ос- оставшейся части этой главы рассматриваются вопросы отображения в проектной документации аспектов проектирования классов; в остальных главах книги приводятся пояснения того, как реализовать различные архитектурные механизмы. Что такое классы Будучи программистом, работающим на языке программирования C++, вы должны иметь определен- определенный опыт построения классов. Формальная методология проектирования требует, чтобы вы отделяли клас- классы C++ от классов, используемых на стадии проектирования, хотя между ними имеется тесная связь. Класс C++, который создается с помощью программных кодов, — это реализация класса, который вы спроек- спроектировали. Оба эти виды классов изоморфны: каждый класс проекта соответствует классу в программном коде, но не следует их путать. Естественно, классы проекта можно реализовать и в другом языке, однако синтаксис определений классов при этом может быть изменен. Сделав это замечание, в дальнейшем в большинстве случаев мы будем говорить об этих классах, не делая между ними различий, поскольку эти различия проявляются на уровне высокой абстракции. Когда вы говорите, что в вашей модели класс Cat содержит метод Meow(), это следует понимать так, что вы поместите в свой класс на языке C++ также и метод MeowQ. Вы отображаете классы этой модели в диаграммах 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. Предварительные классы. Checking Account Распределение Преобразования То, что вы начали делать в предыдущем разделе, недостаточно, чтобы выделить существительные из сценария и начать преобразования объектов, пригодных для анализа области применения, в объекты, ис- используемые на стадии проектирования. Но это лишь первый шаг в правильном направлении. Часто многие объекты области применения на стадии проектирования имеют заменители. Объект называют заменителем, чтобы отличать физическую сумму, фактически выдаваемую машиной 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 Обязанности: Отслеживание состояния текущего счета 2 Зак.53
Объектно-ориентированное программирование «Abstract» Account Checking Account Balance : int DaysATMWithdrawal : int GetBalanceO : int Deposit(int amount)!) : void Transfenn(int amount)() :bool TransferOutn : int WriteChecks(int amount)!) : bool РИСУНОК 1.13. Карточка CRC. Часть I Прием и перевод вкладов Фиксация операции Выдача наличности Отслеживание остатка наличности машины ATM на текущий день Совместно выполняемые работы: Другие счета Учрежденческие системы Аппарат выдачи наличных денег Отношения между классами После того как классы будут представлены в UML, можно постепенно переходить к изучению отношений между различными классами. Основными отношениями, которые вы должны смоделировать, являются следующие: ¦ Обобщение ¦ Связь ¦ Агрегирование ¦ Композиция Отношение обобщения реализовано в C++ через общее наследование. Однако в аспекте проектирова- проектирования мы больше внимания посвятим семантике в ущерб механизму обобщения: что из чего следует. Мы изучали отношение обобщения на стадии анализа, однако сейчас обратим внимание не только на объекты области применения, но и на объекты в нашем проекте. Попытаемся выявить общие функции в родственных классах и снабдить ими базовые классы, которые смогут инкапсулировать общие обязанности. Как только вы выявите общие функции, переносите их из специализированных классов в обобщающий их класс. Таким образом, как только текущему и сберегательному счетам требуется метод для перевода денег с одного счета на другой, автор перемещает метод TransferFunds() в базовый класс счета. Чем больше информации о производных классах вы получаете, тем более полиморфным будет ваш проект. Одна из возможностей, которая реализована в языке C++ и недоступна в Java, — множественное на- наследование (хотя в языке Java имеется похожая в первом приближении, хотя и ограниченная возможность со множественными интерфейсами). Множественное наследование позволяет классу наследовать свойства более чем одного базисного класса, накапливая в себе члены и методы двух и большего числа классов. Опыт показывает, что вы должны разумно использовать множественное наследование, поскольку это может существенно усложнить как проект, так и его реализацию. Многие из проблем, которые ранее ре- решались с помощью множественного наследования, сегодня решаются путем агрегирования. Короче говоря, множественное наследование — это мощное средство, но может случиться так, что ваш проект потребует создания отдельного класса, специализацией которого станет поведение двух или большего числа других классов. Множественное наследование против включения Является ли объект суммой своих частей? Есть ли смысл моделировать объект Саг (Автомобиль) как специализацию SteeringWheel (Руль), Door (Дверца) и Tire (Шина), как показано на рис. 1.14? РИСУНОК 1.14. Ложное наследование. Steering Wheel Door Tire Car Важно вернуться к основным понятиям. Общее наследование всегда должно моделировать обобщение. В общем виде эту мысль можно выразить следующим образом. Наследование должно моделировать отноше-
Объектно-ориентированный анализ и проектирование Глава 1 ние есть. Если вы захотите смоделировать отношение иметь (например, автомобиль имеет руль), то для этой цели необходимо использовать агрегирование, как показано на рис. 1.15. На этом рисунке показано, что у автомобиля имеется руль, четыре колеса и 2—5 дверей. Это одна из сравнительно точных моделей отношения между автомобилем и его составными частями. Обратите внима- внимание на тот факт, что ромбы на диаграмме не закрашены. Это означает, что мы моделируем отношение как агрегирование (группирование), но не как композицию. Композиция предусматривает управление на про- протяжении всего времени существования объекта. И хотя у автомобиля имеются шины и дверцы, и шины и дверцы могут существовать и до того, как они станут частями автомобиля, и после того, как они переста- перестанут быть его частями. На рис. 1.16 показано моделирование композиции. Модель отражает не только тот факт, что человечес- человеческое тело есть группирование головы, двух рук и двух ног, но и тот факт, что эти объекты (голова, руки, ноги) созданы тогда же, когда создано само тело, и исчезнут вместе с исчезновением тела. Иначе говоря, для них невозможно независимое существование; тело состоит из этих частей. Steering Wheel РИСУНОК 1.15. Агрегирование. РИСУНОК 1.16. Композиция. Дискриминаторы Как спроектировать классы, необходимые для отображения различных модельных линий типичного производителя автомобилей? Предположим, что вам поручили спроектировать систему для компании Acme Motors, которая одновременно выпускает пять моделей автомобилей: Плутон (Pluto — низкоскоростной миниатюрный автомобиль с двигателем малых размеров), Венера (Venus — седан с четырьмя дверями и двигателем средних размеров), Марс (Mars — спортивная модель с самым большим двигателем, выпуска- выпускаемым компанией и развивающим максимальную мощность), Юпитер (Jupiter — мини-фургон с таким же двигателем, что и спортивная модель, но рассчитанным на переключения на низких оборотах и на ис- использование его мощности для компенсации большего веса машины) и Земля (Earth — фургон с неболь- небольшим высокооборотным двигателем. Вы можете начать с создания подтипов класса Саг, которые отображают различные модели, а за- затем создать экземпляры каждой модели по мере того, как они сходят с конвейерной лини, как показано на рис 1.17. Чем отличаются между собой эти модели? Как нетрудно видеть, они отличаются друг от друга размерами двигателя, типом корпуса и рабочими ха- характеристиками. Эти отличающиеся между собой ха- характеристики можно комбинировать и подбирать РИСУНОК 1.17. Моделирование подтипов. таким образом, что будут получаться различные модели. Мы можем моделировать эти случаи в UML по- посредством стереотипа дискриминатора, как показано на рис 1.18. На этом рисунке показано, что классы могут быть порождены классом Саг путем комбинирования и подбора трех отличительных атрибутов. Размер двигателя соответствует мощности автомобиля, технические характеристики указывают на спортивную принадлежность автомобиля. Таким образом, можно иметь мощ- мощный спортивный фургон, семейный седан малой мощности и т.д. Каждый атрибут может быть реализован с помощью простого перечислителя. Таким образом, тип кор- корпуса может быть реализован следующим оператором, представленным в программных кодах: enum BodyType = { sedan, coupe, minivan, stationwagon } ; Однако может получиться так, что простого значения недостаточно, чтобы смоделировать конкретный дискриминатор. Например, технические характеристики могут быть довольно сложными. В этом случае мо-
Объектно-ориентированное программирование Car engine High Power performance body Sedan Coupe Family Car Часть! делью дискриминатора может быть класс, а сам диск- дискриминатор может быть инкапсулирован в экземпляре этого типа. Следовательно, класс Саг может моделировать тех- технические характеристики в типе performance, который содержит информацию о том, где происходит переклю- переключение двигателя и сколько оборотов в минуту он может развивать. Стереотип UML для класса, который инкап- инкапсулирует дискриминатор и который может быть исполь- использован для создания экземпляров класса (Саг), имеющих различные логические типы (например, SportsCar или „_„ _„ _ _„ LuxuryCar), - это тип мощности. В этом случае класс РИСУНОК 1.18. Моделирование дискриминатора. Performance — это тип мощности для автомобиля. Когда вы инициализируете экземпляр класса Саг, то одновременно генерируете экземпляр объекта Performance и сопоставляете с конкретным объектом Performance заданный класс Саг, как показано на рис. 1.19. Low Power SportsCar РИСУНОК 1.19. Дискриминатор как тип мощности. High Power Low Power engine Sedan Car • i performance:PerformanceCharacteristics body Coupe Family Car SportsCar «powertype» Performance Characteristics shift Point maxRPM accelerate t Family: PerformanceCharacteristics Sport: PerformanceCharacteristics Типы мощности позволяют создавать множество логических типов без использования свойства наследо- наследования. Таким образом, можно работать с большими и сложными наборами типов, не опасаясь "демографи- "демографического" взрыва, являющегося следствием применения средств комбинаторики, с которым вы можете столкнуться при использовании свойства наследования. В C++ обычный путь реализации типов мощности со- состоит в использовании указателей. В этом случае класс Саг содержит указатель на экземпляр класса PerformanceCharacte- PerformanceCharacteristics (рис. 1.20). Честолюбивый читатель может в качестве упражнения выполнить преобразование дискриминаторов корпуса и двигателя в типы мощностей. Class Car : public Vehicle { public: Car(); ~Car(); // Другие общедоступные методы не рассматриваются private: PerformanceCharacteristics * pPerformance; High Power engine Sedan Car t 1 body Performance Characteristics shift Point maxRPM accelerate Coupe Low Power РИСУНОК 1.20. Отношение между объектом Саг и его типом мощности. В качестве заключительного замечания отметим, что типы мощности позволяют создавать новые типы (не только экземпляры) во время выполнения программы. Поскольку каждый логический тип отличается только атрибутами сопоставленного ему типа мощности, эти атрибуты могут служить параметрами для конструктора типа мощности. Это означает, что вы можете создавать новые типы во время выполнения программ практически мгновенно, т.е. передавая типу мощности различные данные о размерах двигателя и точках переключения, можно эффективно создавать новые технические характеристики. Назначая такие характеристики различным автомобилям, вы можете эффективно расширять наборы типов во время вы- выполнения.
Объектно-ориентированный анализ и проектирование Глава 1 Динамическая модель Дополнительно к моделированию отношений между классами очень важно построить правильную мо- модель их взаимодействия. Например, классы CheckingAccount, ATM и Receipt могут взаимодействовать с классом Customer при выполнении примера использования "Снятие суммы со счета". С этой целью возвращаемся к диаграммам последовательности действий, которые впервые были использованы на стадии анализа, одна- однако сейчас они пополняются деталями на базе методов, которые мы разработали ранее для соответствую- соответствующих классов, как показано на рис. 1.21. Customer ATM Armiint Receipt РИСУНОК 1.21. Диаграмма последовательности действий. 1: Проверить остаток 2: Показать остаток 3: Отображение суммы остатка 4 : Снять сумму со счета 5 : Выдана остатка 6: Печать квитанции Эта простая диаграмма взаимодействий отображает взаимодействия между некоторыми проектными классами во времени. Предполагается, что класс ATM делегирует классу CheckingAccount все обязанности для работы с остатком, в то время как класс CheckingAccount передает классу ATM обязанность управлять отображением данных для пользователей. Диаграммы совместного выполнения работ существуют в двух разновидностях. Одна из этих разновидно- разновидностей, представленная на рис. 1.20, именуется диаграммой последовательности действий. Другое представление этой же информации предлагает диаграмма совместно выполняемых работ. Диаграмма последовательности действий показывает, в какие моменты времени происходят события; а диаграмма совместно выполняе- выполняемых работ показывает, как взаимодействует между собой классы. Диаграмму совместно выполняемых работ можно построить на базе диаграммы последовательности действий; инструментальные средства, подобные Rational Rose, доводят автоматизацию этих работ до такого уровня, что они могут выполняться по щелчку кнопки мыши (рис. 1.22). РИСУНОК 1.22. Диаграмма совместно выполняемых работ. Customer . >¦ ATM ^ч^4 : Снять сумму со счета 3: Отображение / */чЧЧц>чХч. суммы остатка / 5 : Выдач остатка\^ /2: Показать остаток Receipt Checking Armiint 6: Печать квитанции
Объектно-ориентированное программирование Start (Пуск) Сплошной маркер Переход в другое состояние Not Logged In (Клиент не зарегистрирован) Getting Account Info (Получение информации о состоянии счета) Состояние Защитная блокировка [Valid Account ID] Часть I Диаграммы состояний Чтобы понять, как осуществляется взаимодействие между объек- объектами, мы должны иметь правильное представление о возможных состояниях конкретных объектов. Мы можем моделировать перехо- переходы из одного состояния в другое с помощью диаграмм состояний (или диаграмм перехода). На рис. 1.23 показаны различные состоя- состояния класса CustomerAccount в момент регистрации клиента в сис- системе. Каждая диаграмма состояний начинается с единственного со- состояния пуска и заканчивается одним из нескольких состояний окончания или вообще без такового. Каждое состояние имеет имя, переходы могут быть помечены. Защитная блокировка указывает условие, которое должно быть выполнено, чтобы объект мог пе- перейти из одного состояния в другое. Суперсостояния Клиент в любой момент может раздумать и отказаться от реги- регистрации. Он может сделать это, когда вставит свою карту для иден- идентификации своего счета или после того, как введет свой пароль. В любом случае система должна принять его требование отмены се- сеанса и возвратиться в состояние "Клиент не зарегистрирован" (рис. 1.24). Вы видите, что в более сложной диаграмме состояние Отказ от операции быстро становится источни- источником путаницы. Это создает большие неудобства, так как отказ от операции представляет собой исключи- исключительную ситуацию, которая не должна занимать на диаграмме центрального места. Можно упростить эту диаграмму, вводя суперсостояние (рис. 1.25). Getting Password (Получение пароля) Logged In (Клиент зарегистрирован) Маркер в виде мишени = окончание РИСУНОК 1.23. Состояния счета клиента. 1 Start (Пуск) Not Logged In (Клиент не зарегистрирован' т Start (Пуск) Getting Account Info (Получение информации о состоянии счета) Getting Password (Получение пароля) Canceled (Отказ от операции) Canceled (Отказ от операции) Not Logged In (Клиент не зарегистрирован' > к Canceled (Отказ от операции) Cancelabte (Состояния допускающие on * Getting Account Info (Получение информации о состоянии счета) аз) 1 Getting Password (Получение пароля) Logged In (Клиент зарегистрирован) Logged In (Клиент зарегистрирован) I РИСУНОК 1.24. Пользователь может отменить транзакцию. РИСУНОК 1.25. Суперсостояние. Диаграмма на рис. 1.24 содержит ту же информацию, которая представлена на рис. 1.23, однако она значительно легче воспринимается. С момента начала регистрации в системе до завершения системой про- процесса регистрации вы можете отказаться от операции. Если вы отменяете операцию, вы возвращаетесь в состояние "клиент на зарегистрирован".
Объектно-ориентированный анализ и проектирование Глава 1 Резюме В этой главе в общих чертах описаны проблемы, сопровождающие объектно-ориентированный анализ и проектирование. Суть такого подхода заключается в анализе того, как ваша система будет использована и как она должна функционировать, и в последующем проектировании классов и моделировании их отно- отношения и взаимодействия. В старые добрые времена мы в общих чертах строили картину того, что мы намереваемся получить, и немедленно приступали к написанию программных кодов. Проблема заключалась в том, что сложные про- проекты никогда не завершались, а если и завершались, то полученный программный продукт оказывался ненадежным и хрупким. Выдвигая на передний план необходимость понимания требований и моделируя стадию проектирования, мы тем самым гарантируем успешное завершение создания программного про- продукта, который будет соответствовать предъявляемым требованиям. Большая часть последующего материала этой книги будет посвящена подробностям реализации. Проблемы, имеющие отношение к тестированию и распространению, выходят за рамки книги, поэто- поэтому они не рассматриваются.
Проектирование классов в C++ В ЭТОЙ ГЛАВЕ ¦ Перевод диаграмм классов в C++ Я Перевод диаграмм юаимодействия в C++ ¦ Перевод диаграмм состояний в C++ ¦ Перевод диаграмм активности в C++
Проектирование классов в C++ Глава 2 Прочитав главу 1, вы научились формулировать свои идеи, проблемы и решения в хорошо организо- организованной и понимаемой форме, воспользовавшись для этой цели универсальным языком моделирования UML. Теперь вы можете показывать проекты объектов любому заинтересованному лицу, имеющему отношение к отрасли программирования, не сомневаясь, что он поймет ваш замысел именно так, как вы себе его пред* ставляли. Следующий шаг состоит в воплощении ваших проектов в рабочие решения. Основным содержа- содержанием настоящей главы является преобразование различных моделей, полученных вами на стадии анализа. в соответствующие программные коды на языке C++. Если быть точнее, то в этой главе описывается пре- преобразование в коды C++ следующих диаграмм UML: ¦ Классов ¦ Взаимодействия (диаграммы совместно выполняемых работ) ¦ Перехода из одного состояния в другое ¦ Активности Перевод диаграмм классов в C++ Oalance: int DaysA TMVWthdrawal: int <4>GetBalance(): int: int ^> Deposit (amount: int): void "s^Transferln (amount: int): tool <^>TransferOutO: int ^WriteChecks (amount: intj: tool Диаграммы классов служат двум целям: представлению статических состояний классов и использованию отношений между ними. На ранних стадиях жизненного цикла разработки ПО важно отметить, что в мо- модели диаграммы класса делается попытка определить не только интерфейс общего пользования каждого класса, но и связи, агрегирование и обобщения, устанавливаемые между конкретными классами. Хотя определение конкретного набора классов имеет важное значение при построении основных инструментатьных средств, предназначенных для использования в законченном приложении, чтобы успешно продвигаться и направлении полного решения задачи не менее важно понимать, как каждый класс взаимодействует с другими классами. Далее в этой главе вы узнаете о том, как преобразовывать наиболее часто используемые элементы диаграмм классов в программные коды на языке C++. Стандартные классы Как уже было отмечено в главе 1, классы представляются в виде прямоу- гольников, разделенных на три части, в верхней части прямоугольника указано имя класса, в средней части — атрибуты, а в нижней части — операции. На рис. 2.1 показана типичная диаграмма класса. Преобразование диаграммы класса, представленной на рис. 2.1, в класс на C++ является процессом, состоящим из трех этапов: 1. Назначение классу имени, основой которого служит имя, указанное в верхнем прямоугольнике диаграммы класса: CheckingAccount. He забудь- забудьте включить в этот класс конструктор и деструктор. 2. Включение в класс атрибутов (из среднего прямоугольника диаграммы) РИСУНОК 2.1 в качестве приватных переменных-членов. И хотя такие переменные не класса текущего счета обязательно должны быть приватными, однако, если они будут приват- приватными, это существенно облегчит инкапсуляцию. В случае, представленном на рис. 2.1, такими атри- атрибутами являются Balance и DaysATMWithdrawal. 3. Включение операций (нижний прямоугольник диаграммы) в общедоступные функции-члены. Эти функции являются интерфейсами общего пользования такого класса. На рис. 2.1 такими операциями являются GetBalance(), Deposit(), Transferln(), TransferOut() и WriteChecks(). В листинге 2.1 представлено законченное объявление класса CheckingAccount. Листинг 2.1. Представление класса UML в C++ class CheckingAccount public: // Создание / Разрушение CheckingAccount () ; -CheckingAccount () ; // Операции общего пользования int GetBalance () ; void Deposit (int amount);
Объектно-ориентированное программирование Часть I BOOL Transferln (int amount); int TransferOut () ; BOOL WriteChecks (int amount); private: // Приватные атрибуты int Balance; int DaysATMWithdrawal; Шаблонные классы Шаблонные классы, которые также называют параметризованными, — это обобщающие классы, которы- которыми нельзя пользоваться до тех пор, пока они не инициализированы. Поскольку типы данных, используе- используемые в шаблонном классе, не определены до тех пор, пока этот класс не инициализирован, вы можете создавать обобщающий класс Stack только один раз; элементы, проталкиваемые в стек и выталкиваемые из него, могут отличаться от одного экземпляра класса к другому. Этот единственный в своем роде класс Stack может быть использован для хранения целых, значений с плавающей точкой или любых других пре- предопределенных или заданных пользователем типов. На диаграммах классов шаблонные классы представлены так же, как и Stack StackType %Рор(): <StackType> <§>Push (item: <StackType>): void обычные классы, однако в их верхнем правом углу имеется прямоугольник, обозначенный точками, в котором определены аргументы, предназначенные для использования в классе (рис. 2.2). Реализация шаблонного класса в языке C++ соответствует типу шаблон- шаблонного класса в C++. Чтобы преобразовать класс C++ для стандартного клас- са, выполните следующие действия: РИСУНОК 2.2. Шаблонная 1. Перед объявлением класса поместите шаблон из ключевых слов и оп- диаграмма класса Stack. ределите тип подстановки для класса: <класс StackTypeX 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 предусматривается их поддержка. Назначение служебного класса заключается в том, чтобы собрать воедино некоторые функциональные средства (такие, как тригономет-
Проектирование классов в C++ Глава 2 «Utility» TrigMathFunctions <^> Cosine (angle: float): float ^>Sine (angle: float): float <^> Tangent (angle: float): float РИСУНОК 2.З. Диаграмма служебного класса TrigMathFunctions. рические математические функции) и поместить их в отдельный класс. При этом отпадает необходимость создавать экземпляры этого класса, поскольку каждая функция этого класса объявляется как static: вы про- просто используете функции, содержащиеся в этом классе, называя точное имя нужной функции. Например, класс тригонометрических функций может содержать функции Cosine(), Sine() и Tangent(), которые могут быть вызваны в любой момент любым классом. В системе обозначений, используемой в диаграммах классов, именам служебных классов предше- предшествует конструкция <<Utility». Диаграмма класса TrigMathFunctions пред- представлена на рис. 2.3. Чтобы представить служебный класс в 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). Класс 1 Класс2 Person Reads Book РИСУНОК 2.4. Неименованная ассоциация классов. РИСУНОК 2.5. Именованная ассоциация классов. Вы отображаете ассоциацию 1:1 в коды C++ путем определения экземпляра ассоциированного класса внутри другого класса и предоставления интерфейса для доступа к этой информации. Листинг 2.4 содержит фрагменты из определения класса Person. Листинг 2.5 содержит фрагменты определения класса Book. Листинг 2.4. Объявление класса Person tinclude "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:
Объектно-ориентированное программирование Часть! 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; 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. Ассоциация классов с именованными ролями. Renter Tenant Dwelling Apartment Чтобы создать класс C++ для ассоциации 1:1 именованных ролей, в обоих классах этой ассоциации должны быть переменные типов другого класса. В листинге 2.6 в классе Renter имеется переменная-член Dwelling типа Apartment. Эта переменная представляет для квартиросъемщика квартиру, в которой он жи- живет. Более того, класс Renter обеспечивает интерфейс общего пользования для модификации своей пере- переменной Dwelling посредством двух общедоступных функции get_Dwelling() и set_Dwelling(). Аналогично в
Проектирование классов в C++ Глава 2 классе Apartment имеется приватная переменная-член Tenant типа Renter и две функции-члены, которые имеют доступ к переменной типа Renter класса Apartment: get_Renter() и set_Renter(). В листинге 2.6 пред- представлено объявление класса Renter, а в листинге 2.7 — объявление класса Apartment. Лиаинг 2.6. Объявление класса Renter tinclude "Apartment.h" class Renter { public: Renter () ; -Renter () ; //## Ассоциация: Unnamed //## Роль: Renter::Dwelling const Apartment get_Dwelling() const; void set_Dwelling(const Apartment value); private: Apartment Dwelling; Листинг 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; }; Ассоциации N:1 и 1:N Ассоциации N:l или 1:N, которые еще называются как отношение один-ко-многим, имеют место, когда один объект может существовать для многих экземпляров другого объекта, однако другой объект может существовать только для исходного объекта, и только один раз. Пример такого рода ассоциации имеет место между нанимателем и многоквартирным зданием, в котором находится квартира: для нанимателя суще- существует только одно многоквартирное здание, в то же время для одного многоквартирного здания может быть много нанимателей. Для обозначений на диаграмме классов ассоциации один-ко-многим характерно то, что имя роли ассоциации помещается над линией ассоциации, а список типов ассоциаций помещает- помещается ниже этой линии. В табл. 2.1 указано, какими способами обозначается множественность ассоциации. Таблица 2.1. Множественности ассоциации Обозначение Ассоциация 1 Только одна 0..1 Либо одна, либо ни одной 0..* Ни одной или большее число 1..* Одна или большее число * Ни одной или большее число M..N Многие-ко-многим
Объектно-ориентированное программирование Часть! На рис 2.7 представлена диаграммы ассоциации классов Tenant и ApartmentBuilding. РИСУНОК 2.7. Ассоциация классов один-ко-многим. Tenant Houses Lives at 0..* ApartmentBuilding Ассоциация классов 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 tinclude "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. Ассоциация классов многие-ко-многим. Contractor Employees Works for 0..* 0..* Company
Проектирование классов в C++ Глава 2 Реализация такой ассоциации в C++ подобна примеру с многоквартирным домом из предыдущего раз- раздела. Company содержит приватный набор подрядчиков Contractors под именем Employees, а каждый Contractor ведет перечень компаний Company под именем Works_for. Представленные ниже листинги рабо- работают с этими коллекциями, используя для этой цели шаблонный класс UnboundedSetByValue. В листинге 2.10 содержится объявление класса Contractor. В листинге 2.11 представлено объявление класса Company. Листинг 2.10. Объявление класса Contractor // класс Company tinclude "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; Листинг 2.11. Объявление класса Company // класс 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, в рамках которого класс BankAccount содержит классы Balance и TransactionHistory. Отношение поглощения изображается на диаграмме классов прямой линией, проведенной между двумя классами, с закрашенным ромбом со стороны класса, который содержит другой класс (рис. 2.9). Классы Balance и TransactionHistory хранят ссылки на свои классы BankAccounts, но сами содержатся в классе BankAccount; класс BankAccount создает приватные экземпляры классов Balance и TransactionHistory. Ли- Листинг 2.12 содержит объявление класса BankAccount. Класс BankAccount содержит приватные переменную-член the_Balance типа Balance и пере- менную-член the_TransactionHistory типа TransactionHistory. Листинг 2.13 РИСУНОК 2.9. Диаграмма композиции содержит объявление класса Balance, а в листинге 2.14 представлено классов. объявление класса TransactionHistory. BankAccount /А Balance TransactionHistory
Объектно-ориентированное программирование Часть I Листинг 2.12. Объявление класса BankAccount // класс TransactionHistory tinclude "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;
Проектирование классов в C++ Глава 2 Обобщение Обобщение используется для представления отношения от-общего-к-конк- ретному, связывающего два класса. В терминах C++ такое отношение называ- называется наследованием. Например, текущий счет и сберегательный счет — это конкретные типы банковских счетов. Обобщение обозначено на диаграмме классов стрелкой, направленной от специального класса к обобщающему классу (рис. 2.10). В C++ обобщение обозначается добавлением в объявление конкретного класса суффикса, состоящего из двоеточия, за которым следует имя обобща- обобщающего класса В листинге 2.10 класс Checking наследуется от класса Account в операторе class Checking: public Account. Класс Savings наследуется от класса Account / y Checking Savings РИСУНОК 2.10. Диаграмма обобщения классов. 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 Листинг 2.16. Объявление класса Savings // Account #include "Account.h" class Savings : public Account Диаграммы классов также поддерживают принцип множественного обобще- обобщения, или множественного наследования (по терминологии C++). Множествен- Множественное наследование представлено на диаграмме класса стрелкой, направленной от специализированного класса к каждому его обобщающему классу. Рассмот- Рассмотрим класс Derived, который пользуется атрибутами как класса BaseA, так и класса BaseB (рис. 2.11). На рис. 2.11 обобщающие стрелки проведены в направлении обоих базо- базовых классов. Чтобы представить множественное обобщение, или множествен- множественное наследование, в языке C++, оба базовых класса в операторе объявления класса разделены запятой, они расположены сразу после двоеточия, следую- следующего за объявлением порожденого класса. Класс Derived наследует как от класса BaseA BaseB \ / Derived РИСУНОК 2.11. Диаграмма множественного обобщающего класса. BaseA, так и от класса BaseB в операторе class Derived: public BaseA, public BaseB. В листинге 2.17 представ- представлен заголовок объявления класса Derived. Поскольку классы BaseA и BaseB не учитывают факт существова- существования класса Derived, их листинги здесь не приводятся. Листинг 2.17. Объявление класса Derived // BaseB #include "BaseB.h" // BaseA #include "BaseA.h" class Derived : public BaseA, public BaseB
Объектно-ориентированное программирование Часть I Перевод диаграмм взаимодействия в C++ Существует два вида диаграмм взаимодействия: диаграммы совместных работ и диаграммы последова- последовательности действий. Диаграммы совместных работ иллюстрируют взаимодействие объектов в организаци- организационном аспекте — они показывают, как упорядочены объекты по отношению друг к другу. Диаграммы последовательности действий отображают только взаимодействие объектов, не учитывая их упорядочен- упорядоченность. Как диаграммы совместных работ, так и диаграммы последовательности действий имеют свои достоин- достоинства и недостатки. Диаграмма совместных работ позволяет наглядно представлять классификацию объектов в их взаимодействии, но по мере увеличения числа взаимодействий она становится все более сложной и неудобной для чтения. Более того, такая модель теряет способность отображать естественным образом по- порядок взаимодействий, в силу чего взаимодействия должны быть пронумерованы. Диаграммы последовательности действий показывают взаимодействия объектов в упорядоченном виде и четко фиксируют относительный порядок взаимодействий, но при этом затрудняется восприятие проте- протекающего процесса, характерное для наглядного представления. В главе 1 описывается процесс построения диаграмм взаимодействия на основе анализа системы. После того как построение диаграмм завершено и объекты, указанные на диаграмме взаимодействия, ассоцииро- ассоциированы с классами, определенными на диаграммах классов, их реализация в C++ не представляет особых трудностей. Построение диаграмм совместной работы преследует четыре основные цели: ¦ Идентификация системных пользовательских интерфейсов н Совершенствование интерфейса общего пользования для каждого класса н Определение, с какими объектами взаимодействует каждый из объектов н Понимание и упорядочивание событий, которые происходят при возникновении соответствующих благоприятных условий. Во-первых, вы должны предусмотреть в пользовательском интерфейсе системы все допустимые взаимо- взаимодействия пользователя. Взаимодействия пользователя представляют действующие субъекты. Сообщения, которые действующие субъекты посылают системным объектам, должны быть взяты из пользовательского интерфейса и отправлены системным объектам; действующие субъекты обычно не взаимодействуют не- непосредственно с системными объектами. Во-вторых, следует сделать так, чтобы каждое сообщение, порожденное объектом, включенным в ди- диаграмму взаимодействия, соответствовало вытекающему из нее интерфейсу общего назначения этого объекта. Если объект А запрашивает услугу X от объекта В, то сервисная операция X объекта В должна быть дос- доступной и для объекта А. В-третьих, вы должны сделать так, чтобы объекты, взаимодействующие друг с другом, имели доступ друг к другу. Если объект С посылает сообщение объекту D, то объект С должен видеть объект D. Это осо- особенно важно при выборе пути реализации системы. И наконец, вы должны сделать так, чтобы каждая последовательность сообщений, которые появляются на объекте и передаются этим объектом, представляла событие, которое происходит в этом объекте. Если объект Е получает сообщение I и передает сообщение J, то служба объекта Е, которая получает сообще- сообщение I, должна передать сообщение J до того, как она окончит работу. Реализация диаграмм совместных работ и диаграмм последовательности действий в C++ Диаграммы совместных работ и диаграммы последовательности действий взаимозаменяемы. В качестве справки следует отметить, что такое инструментальное средство моделирования, как Rational Rose, позво- позволяет выполнять прямое преобразование одной диаграммы в другую. Вспомните пример операции снятия наличных средств с текущего счета с помощью машины ATM. Подробности разработки как диаграмм со- совместных работ, так и диаграмм последовательности действий уже рассматривались нами ранее, тем не менее, они показаны соответственно на рисунках 2.12 и 2.13. Прежде чем мы начнем построение классов для этих диаграмм, определим объекты, представленные на этих диаграммах. Ниже приводится список этих объектов: ш Машина (ATM) ¦ Текущий счет (CheckingAccount)
Проектирование классов в C++ Глава 2 Квитанция (Receipt) Пользовательский интерфейс (Userlnterface) РИСУНОК 2.12. Диаграмма совместных работ для операции снятия со счета наличных денег через машину А ТМ. 1. Check Balance 3. Display balance! \ \ 12. Get Balance 4. Withdrawal Cash \ V Checking Account: Checking Account 6. Print Person ATM: ATM Checking Account: Checking Account Receipt Check Balance Get Balance РИСУНОК 2.13. Диаграмма последовательности действий для операции снятия со счета наличных денег через машину ATM. Display Balance сг Withdrawal Cash Dispense D \\ U Print Объект Userlnterface предназначен для представления действий объекта Person, поскольку объект Person никогда непосредственно не взаимодействует с другими объектами. Прежде всего рассмотрим системный пользовательский интерфейс. Объект Person на рисунках 2.12 и 2.13 осуществляет взаимодействие с системными объектами: ¦ Выполняет действие CheckBalances (Проверить текущий остаток) с объектом ATM. ¦ Выполняет действие WithdrawCash (Снять со счета наличные деньги) с объектом CheckingAccount. ¦ Получает наличность, в то время как объект CheckingAccount выполняет действие Dispense cash (Выдача наличности) с объектом Person. Объект Userlnterface должен предоставлять объекту Person опции CheckBalance и WithdrawCash, а также обеспечить выдачу объекту Person наличности из объекта CheckingAccount. Во всех случаях мы прибегали к услугам ATM, так что пользовательский интерфейс, обеспечивающий получение наличных денег, нам известен.
Объектно-ориентированное программирование Часть I Объект Userlnterface может быть введен в действие через объявление класса, подобного представленно- представленному в листинге 2.18. Этот класс будет расширен при последующем рассмотрении, а сейчас введем интер- интерфейс общего доступа. Листинг 2.18. Объявление класса Userlnterface class Userlnterface < 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 (); Листинг 2.20. Общедоступный интерфейс для класса CheckingAccount class CheckingAccount < public: char * GetBalance () ; BOOL WithdrawCash (); Листинг 2.21. Общедоступный интерфейс для класса Receipt class Receipt { public: BOOL Print () ; Теперь, когда общедоступный интерфейс извлечен из диаграмм взаимодействия и включен в объявле- объявления классов, следующий шаг состоит в том, чтобы установить область видимости для конкретных классов. В нашем примере класс Userlnterface непосредственно сообщается с классами ATM и CheckingAccount. На этой стадии реализация зависит от приложения, а вам предоставляется следующий выбор: я Экземпляры классов ATM и CheckingAccount могут быть инициализированы с классом Userlnterface. Выбирайте этот вариант, если классу Userlnterface нужен непосредственный доступ как к классу ATM, так и к классу CheckingAccount.
Проектирование классов в C++ Глава 2 ¦ Экземпляры каждого класса могут быть порождены в пространстве глобальной системы, а указатели на каждый из них могут быть переданы в класс Userlnterface. Выбирайте этот вариант, если классы ATM и CheckingAccount требуют большей устойчивости, чем класс Userlnterface. ¦ Учитывая интересы классов, которые должны быть видимы из класса ATM, экземпляр объекта класса CheckingAccount может быть порожден с классом ATM и снабжен интерфейсом с классом CheckingAccount. На этой стадии нет однозначного решения по реализации, все зависит от других диаграмм взаимодей- взаимодействия и других ограничений на стадии проектирования, таких как механизм хранения класса CheckingAccosir.t (например, класс CheckingAccounts может храниться в базе данных, а в пределах объекта ATM может быть видимым и доступным только один его экземпляр). В этом конкретном примере CheckingAccount — это объект, экземпляр которого будет порожден в объс.1- ATM, а экземпляр объекта ATM будет порожден в объекте Userlnterface. В листинге 2.22 представлено объе- объедение класса Userlnterface с атрибутом ATM, включенным в качестве приватного элемента данных the_Alivi. Листинг 2.22. Объявление класса Userlnterface со включенным объектом ATM class Userlnterface { public: // Общедоступный интерфейс char * CheckBalance () ; BOOL WithdrawCash (); private: // Приватные атрибуты ATM the_ATM; ); Далее, объект ATM взаимодействует только с объектом CheckingAccount и, как уже отмечалось выше, будет содержать только экземпляр объекта CheckingAccount. Поскольку он содержит объект CheckingAccount, в его обязанность входит поддержание связи с CheckingAccount в интересах класса Userlnterface. В листин- листинге 2.23 представлено объявление класса ATM с включенным в него объектом: the_CheckingAccount. Оно также содержит метод WithdrawCashFromCheckingAccount() (снятие суммы с текущего счета), который взаимо- взаимодействует с объектом CheckingAccount класса Userlnterface. Листинг 2.23. Объявление класса ATM, содержащего объект CheckingAccount class ATM { public: // Общедоступный интерфейс char * CheckBalance () ; char * DisplayBalance (); // Интерфейс класса CheckingAccount BOOL WithdrawCashFromCheckingAccount (); private: // Приватные атрибуты CheckingAccount the_CheckingAccount; >; Класс CheckingAccount не содержит собственных классов. Класс Receipt может содержаться в классе CheckingAccount, поскольку с ним установлена связь, тем не менее, это не имеет смысла, ибо создание объекта Receipt в CheckingAccount логически не обосновано. Это еще одно решение, которое принимается на стадии реализации, оно оправдано тем, что объект Receipt не характерен для объекта CheckingAccount, в силу чего он не может стать частью объекта CheckingAccount. Класс Receipt не должен устанавливать связь с каким-либо классом, представленным на диаграмме взаимодействия, так что в область его видимости никакие классы не попадают. Завершающий шаг состоит в том, чтобы выявить все операции, посылающие сообщения, получающие сообщения и выдающие наличность. Наиболее простой путь решения данной задачи — проследовать в на- направлении стрелок на диаграмме взаимодействия. Первое из сообщений, CheckBalance, посылается с объекта Userlnterface на объект ATM. Следующее сообщение, GetBalance, посылается с объекта ATM на объект CheckingAccount. Тем самым накладывается следующее ограничение: функция ATM::CheckBalace() должна
Объектно-ориентированное программирование Часть I вызвать функцию ChcckingAccount::GetBalance(), прежде чем завершит свою работу. Реализация функции ATM::CheckBa!ance() представлена в листинге 2.24. Листинг 2,24. Реализация функции CheckBalanceQ в классе ATM char * ATM::CheckBalance { // Выполнение других функций, отслеживающих состояние счета // Вызов функции CheckingAccount::GetBalance szBalance = the_CheckingAccount.GetBalance () ; // Возврат данных о состоянии счета return szBalance; } Следующее сообщение, Display Baiance, направлено от объекта CheckingAccount к объекту ATM. В силу этого объект CheckingAccount должен вызвать метод ATM::DisplayBalance() в свой метод GetBaIance(). В листинге 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. Функция Userinterface::WithdrawCash() Function BOOL Userlnterface::WithdrawCash<) { // Выдать значение, которое возвращает ATM::WithdrawCashFromCheckingAccount return the_ATM.WithdrawCashFrcmCheckingAccount(); } В листинге 2.27 представлена реализация функции CheckingAccount::WithdrawCash(). Изучение диаграмм взаимодействия показывает, что эта функция генерирует два события: UserInterface::Dispense() и Receipt::Print(). Функция UserInterface::Dispense() выдает клиенту наличность из машины ATM, а функция Receipt::Print() печатает квитанцию для клиента. В этом листинге предполагается, что GetParent() — это функция, которая существует в каждом классе, и что она возвращает указатель своему родителю. Более того, предполагается, что имеется также функция GetReceipt(), существующая в объекте Userlnterface, которая возвращает системный объект Receipt. На реализацию этих функциональных средств ограничения не накладываются, с этой целью разработчик может выбрать любой приемлемый (однако функциональ- функциональный) способ. Листинг 2.27. Функция CheckingAccount::WithdrawCash() BOOL CheckingAccount::WithdrawCash() { // Выполнить функции, реализующие снятие со счета наличных денег // Послать сообщение Dispense объекту Userlnterface GetParent()->GetParent()->Dispense // Послать сообщение Print объекту Receipt GetParentO -XSetParent () - >GetReceipt () ->Print() ; // Возвратить результат if (fSuccess)
Проектирование классов в C++ Глава 2 return TRUE; else return FALSE; Перевод диаграмм состояний в C++ Диаграммы взаимодействий отображают взаимодействие между переменными объектами, в то время как диаграммы состояний отображают те изменения, которые происходят с отдельным объектом. Диаграммы состояний показывают все состояния внутри объекта и переходы, которые существуют между состояниями. Диаграммы перехода должны быть созданы для всех объектов, имеющих следующие характеристики: я Множество состояний, в которых может находиться объект я Конкретные действия, которые должны быть осуществлены при переходе из одного состояния в другое Если при переходе объекта из одного состояния в другое должно произойти какое-либо событие, диаг- диаграммы состояний могут оказать существенную помощь для правильной реализации проектных решений. В диаграммах состояний состояние Start означает начало перехода, обычно представленное началом фун- функции-члена, в которой содержится переход. Состояние End означает завершение перехода из одного состо- состояния в другое и может соответствовать завершению этой функции-члена. Состояния в объекте представлены переменными, характерными для конкретных приложений. Например, объект светофор принимает три состояния (зеленый, желтый и красный), которые могут быть представленные целым значением состоя- состояния. И хотя реализация фактического перехода из одного состояния в другое зависит от конкретного при- приложения, как только инициировано состояние Start, действия, которые должны быть выполнены, должны быть четко определены, а их реализация должна быть простой. Возвратимся к примеру класса CustomerAccount из главы 1. На рис. 2.14 показана диаграмма состояний для этого класса. Эта диаграмма дает представление о том, что объект (или в случае с языком C++ — класс) должен обеспечить интерфейс, позволяющий пользователю зарегистрироваться в системе. Кон- Конкретнее, класс должен каким-то образом запросить у пользова- пользователя информацию о его счете (GettingAccountlnfoO), пригласить ввести пароль (GettingPassword()) и изменить состояние объекта с NotLoggedln на Loggedln — и все это с сохранением возможно- возможности отмены пользователем операции в любой момент (Canceled). Эти шаги могут быть реализованы в одном методе или в наборе функций, эта реализация зависит от приложения и от разработ- разработчика. Для достижения этой цели в данном примере использует- используется метод Login. Класс CustomerAccount поддерживает булевскую приватную переменную-член m_fLoggedIn, которая содержит информацию о состоянии клиента в момент регистрации в системе. Эта пере- переменная проверяется всеми методами, которые выполняют какие- либо действия, требующие связи пользователя с системой (например, GetBalanceO или WithdrawCash()). Объявление класса CustomerAccount представлено в листинге 2.28. Листинг 2.28. Объявление класса CustomerAccount 1 Mot Lagged In (Клиент не зарегистрирован) Canceled (Отказ от операции) Состояния допускающие отказ Getting Account Into (Получение информации о состоянии счета) Getting Password (Получение пароля) Logged In (Клиент зарегистрирован) РИСУНОК 2.14. Диаграмма перехода объекта CustomerAccount. class CustomerAccount { public: // Создание / Разрушение CustomerAccount () ; "CustomerAccount(); // Общедоступный интерфейс BOOL Login(); BOOL Logout(); char *GetBalance() ; private:
Объектно-ориентированное программирование U Часть I // Приватные данные о состоянии BOOL m_fLoggedIn=FALSE; // Приватные данные char *m szBalance; Конструктор класса CustomerAccount инициализирует переменную-член m_fLoggedIn со значением false, так что ни одна из функций не попытается выполнить неавтономные действия, будучи в автономном ре- режиме. Деструктор проверяет состояние регистрации и при необходимости выводит клиента из системы, перед тем как завершить работу. Функция LogOut() просто присваивает переменной m_fLoggedIn значение false. Функция Login выполняет действия, уточненные на диаграмме состояний: сначала она запрашивает у пользователя номер счета и сохраняет его в переменной szAccountNumber. На практике реализация этого объекта требует использования более совершенных методологий отказа, однако, чтобы привлечь ваше вни- внимание к тому факту, что это действие может быть прекращено, номер счета сравнивается со значением Cancel, прежде чем работа будет продолжена. Затем клиент получает просьбу указать свой пароль. И снова ответ клиента сравнивается со значением Cancel. Если клиент ввел и номер счета, и пароль, проверка этих значений выполняется другой функцией, а именно функцией ValidPassword(), которая, по всей видимос- видимости, выполняет несколько просмотров базы данных и операций сравнения. После того как все эти действия будут выполнены, значение переменной mfLoggedln меняется на true. Если какое-либо событие отмене- отменено, значение переменной m_fLoggedIn остается false и переход не происходит. В листинге 2.29 показана реализация класса CustomerAccount. Листинг 2.29. Реализация класса CustomerAccount tinclude "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); // Событие может быть отменено в любой момент if (Istrcmp (szAccountNumber, "Cancel")) return FALSE; // Получение пароля printf ("Enter Password:"); scanf ("%s", szPassword); // Событие может быть отменено в любой момент if (Jstrcmp (szPassword, "Cancel")) return FALSE; // Некоторые вспомогательные функции, выполняющие проверку пароля if (!ValidPassword (szAccountNumber, szPassword)) return FALSE; else // Пароль действительный — Зарегистрировать пользователя в системе m_f Loggedln = TRUE ; // Проверка закончилась успешно return TRUE;
Проектирование классов в C++ Глава 2 BOOL CustomerAccount::LogOut() { m_fLoggedln = FALSE; } char *CustomerAccount::GetBalance() < II Проверка состояния регистрации if(!m_fLoggedIn) return NULL; // Возвратить данные об остатке return m szBalance; Перевод диаграмм активности в C++ Диаграмма активности — это специальный тип диаграммы состояний, в которой представлены состоя- состояния действия и транзакции, имеющие место по завершении операций. Диаграммы активности могут быть использованы для представления диаграмм перехода в синхронные состояния, на которых все или боль- большая часть событий на диаграмме представляют собой завершение операций внутреннего происхождения. Когда возникают асинхронные события, используйте диаграммы переходов для описания модели. Реализация диаграмм активности во многом напоминает реализацию диаграмм перехода: состояние Start представляет собой начало операции, а состояние End — ее завершение. Поскольку диаграммы активности — синхронные диаграммы, то в момент, когда операции запускаются на выполнение, все действия одно- однозначно определены. Рассмотрим диаграмму активности, представленную на рис. 2.15. Customer (Клиент) т РИСУНОК 2.15. Диаграмма активности для операции снятия со счета денег с помощью машины ATM. WithdrawCash 1 (Снять со счета) 1 Authorize Transaction {Разрешить транзакцию] Receive Cash (Получить наличные деньги) ATM (Машина ATM) Process Request (Запрос процессов) Debit Account {Счет с дебетовым сальдо) CheckingAccount (Текущий счет) I Funds Available \ (Имеющиеся в наличии I денежные средства) Диаграмма, изображенная на рис. 2.15, представляет три объекта, взаимодействующих друг с другом: Customer, ATM и CheckingAccount. Диаграмма запускается в момент, когда Customer предпринимает по- попытку выполнить WithdrawCash (Снять сумму со счета). Активность WithdrawCash запускает две другие од- одновременные активности: AuthorizeTransaction и ProcessRequest. ProcessRequest осуществляет переход к активности FundsAvailable объекта CheckingAccount. Оба перехода — AuthorizeTransaction и FundsAvailable —
Объектно-ориентированное программирование Часть! ведут к активности 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(); // Общедоступный интерфейс BOOL FundsAvailable();
Проектирование классов в C++ Глава 2 Листинг 2.33. Реализация функций WithdrawCashTransactionQ и ProcessRequestQ void WithdrawCashTransaction { // Активные классы Customer myCustomer; ATM myATM; CheckingAccount myCheckingACcount; // * Состояние пуска // Запуск первой активности myCustomer.WithdrawCash(); // Теперь, когда первая транзакция завершена, запускайте две следующие if (myCustomer.AuthorizeTransaction() SS ProcessRequest()) i II Теперь, когда оба действия завершены, продолжайте обработку myATM.DebitAccount(); myCustomer.ReceiveCash{); } // * Состояние завершения } BOOL ProcessRequest () < myATM.ProcessRequest(); return myCheckingAccount.FundsAvailable(); } Резюме В этой главе рассматривалась реализация в C++ некоторых наиболее популярных моделей UML, полу- полученных на стадии проектирования. Вы узнали о диаграммах классов, диаграммах взаимодействий (о диаг- диаграммах совместных работ и диаграммах последовательности действий), диаграммах состояний и диаграммах активности. Универсальный язык моделирования LJML обладает достаточно мощными инструментальными средствами, которые оказывают специалистам по программному обеспечению неоценимую помощь как на стадии анализа, так и на стадии проектирования. В одних случаях перевод таких проектных решений в про- программные коды C++ производится по формальным правилам, в других случаях следует учитывать особен- особенности приложения, однако, так или иначе, успех проекта возможен только при наличии мощной базы методов реализации проектных решений. Существуют целый ряд инструментальных средств, которые об- легчающт задачу разработчиков, моделирующих соответствующий процесс. Инструментальные средства, разработанные компанией Rational Rose, могут генерировать для некоторых диаграмм программные коды C++ и даже переводить диаграммы из одной формы в другую. Завершив изучение этой главы, иы должны поупражняться с некоторыми из этих автоматических инструментальных средств, чтобы убедиться в том, что они генерируют те программные коды C++, на которые вы рассчитываете.
Наследование, полиморфизм и повторное использование программных кодов В ЭТОЙ ГЛАВЕ и Преимущества наследования Объектно-ориентированные связанные списки Абстрактные классы Виртуальные деструкторы Полиморфизм, реализованный путем перегрузки методов класса Управление памятью Проблемы при перегрузке других операторов Множественное наследование
iiuj-iedoeanue, полиморфизм и повторное использование программных кодов Глава 3 Способность обнаруживать шаблоны и отношения между предметами в нашей жизни — это именно то, что нам приходится делать. Мы унаследовали эту способность от наших далеких предков, которые пользо- пользовались ею для предсказания поведения опасных животных. Если они обнаруживали, что животные с ко- короткой шерстью, длинными клыками и крупными зубами пожирают своих соседей, они остерегались их. Одним из основных отношений, которое мы открываем в возрасте примерно трех лет является отношение это есть: "Смотри, мама, это (есть) фрукт. Это (есть) машина". Мы делаем такие открытия задолго до того, как овладеваем языком, чтобы сформулировать его; трехлетний мальчик, увидев пожарную машину, объявил: "Большая красная машина". Величайшей заслугой объектно-ориентированного анализа является то, что благодаря ему стало воз- возможным практическое использование феноменологии. Подобно Гегелю и Канту, мы может спросить: "Что такое автомобиль? Что отличает его от грузовой машины, от человека, от скалы?" С одной точки зрения, автомобиль представляет собой совокупность определенных частей: рулевого колеса, тормозов, сидений, фар. С другой точки зрения, также имеющей право на жизнь, Автомобиль — не роскошь, а Средство пе- передвижения. Утверждая, что автомобиль средство передвижения, мы используем богатое смыслом сокра- сокращение. Поскольку автомобиль есть средство передвижения, он перемещается и перевозит вещи. В этом и состо- состоит предназначение Средства передвижения. Автомобили наследуют характеристики "перемещается" и "пе- "перевозит вещи" от своего родительского типа: Средства передвижения. Мы также знаем, что Автомобили суть специализация Средств передвижения. Они представляют собой специальный вид Средств передвиже- передвижения, а именно тот, который соответствует техническим характеристикам Автомобилей. Моделируя это отношение, мы пользуемся приемом обобщения, а при реализации в языке C++ — наследованием. Преимущества наследования Использование свойства наследования влечет за собой два преимущества. Во-первых, мы можем конк- конкретизировать существующие классы (типы) и писать только те программные коды, которые претерпели изменения. Таким образом, мы можем повторно воспользоваться существующими классами (в качестве базовых классов) и извлечь для себя выгоду из работы, которая когда-то уже была проделана. Теперь нам не обязательно "копировать и вставлять" программные коды. Копирование и вставка особо проблематич- проблематичны, поскольку изменения, внесенные в один раздел программного кода, влекут за собой соотвествующие изменения в другом разделе — это весьма опасная, чреватая отказами практика. Используя вместо этого специализацию, мы можем подвергать изменениям базовый класс, который автоматически вносит нужные изменения во все производные классы. Вторым существенным преимуществом наследования является то, что оно позволяет подходить к про- производным объектам с позиций полиморфизма. Поли означает много, морф означает форму, полиморфизм означает способность принимать многие формы. Пожалуй, наиболее широкое применение полиморфизм находит в приложениях, интенсивно использу- использующих окна. В этом отношении библиотека Microsoft Foundation Classes могут служить прекрасным приме- примером. Фактически все "экранные окна" происходят непосредственно или косвенно от CWindow, который инкапсулирует общедоступный интерфейс функциональных средства для всех объектов на экране. CWindow предлагает такие методы, как UpdateWindow(), BeginPaint(), GetWindowText(), и многие другие. Каждый из этих методов может быть перекрыт в любом из классов, которые происходят от CWindow. В число таких производных классов входят CButton, CKadioButton, CListBox и др. Когда вы вызываете UpdateWindow() на CButton, она воспроизводит себя. Когда вы вызываете UpdateWindow() на CListBox, окно списка также воспроизводит себя. Подробности того, что делают CButton и CListBox, когда они вос- воспроизводят себя, существенно отличаются, однако эти подробности инкапсулированы интерфейсом UpdateWindowQ, представленным CWindow. Объектно-ориентированные связанные списки Чтобы продемонстрировать могущество полиморфизма, подробно ознакомимся с объектно-ориентиро- объектно-ориентированным связанным списком. Как вам уже, возможно, известно, связанный список — это структура дан- данных, разработанная с таким расчетом, чтобы хранить неопределенное число объектов. Вы можете, естественно, хранить объекты только в массивах, но массивы имеют фиксированные размеры. Если вы заранее не знаете, какое число потребуется, то массив — не самый лучший выбор. Если вы сделаете массив слиш- слишком большим, это чревато излишней тратой памяти, если он будет слишком мал, быстро возникнет не-
Объектно-ориентированное программирование Часть! хватка памяти. Все, что нужно в подобного рода случаях, — это массив, способный расширяться; связанный список — это подходящая отправная точка. Разработка связанного списка Связанный список обычно реализуется в виде последовательности узлов. Каждый узел указывает на один объект (данные), а также, возможно, на следующий узел в списке. Если следующий узел отсутствует в списке, узел указывает на NULL (рис. 3.1). Узел Узел Узел Нет узлов РИСУНОК 3.1. Связанный список. Данные Мы хотим, чтобы каждый узел выполнял специальные функции, таким образом, мы создаем три спе- специализированных типа: LinkedList (связанный список), TailNode (хвостовой узел) и InternalNode (внутрен- (внутренний узел). LinkedList предоставляет клиенту точку входа в список. TailNode действует как часовой, он отмечает конец списка. И наконец, узлы InternalNode содержат фактические данные. Разложим обычное поведение этих трех типов в базовый класс Node, который поддерживает два мето- метода: Insert() и Show(). Метод Insert() берет объект и помещает его в список, а метод Show() отображает значения данных в списке. Отношения между этими объектами показано на рис. 3.2. РИСУНОК 3.2. Иерархия наследования узлов. Unk edlist •myNext: Node Node +lnsert() +Show( :Node* : void IntemalNode -myNext: Node -myData : Data* 1 TailNode В узлах LinkedList и InternalNode имеется переменная-член Node * под именем myNext. Этот указатель используется для поиска следующего узла в списке. Это, как легко видеть, односвязный список, т.е. каж- каждый узел указывает на следующий узел в списке, но не указывает на предыдущий. Чтобы что-то поместить в список, создадим класс Data. Все, что потребуется от класса Data, — это иметь некоторое значение (мы воспользуемся целыми значениями) и метод, обеспечивающий двум объек- объектам Data возможность сравниваться и решать, какой из них "больше", что позволило бы их упорядочивать. Мы бы также предпочли, чтобы объект Data был способен отображать себя, так чтобы мы могли знать его значение. ПРИМЕЧАНИЕ В рассматриваемом случае утверждаем, что LinkedList — это фактически особый тип узла, обеспечивающий предоставле- предоставление интерфейса клиентам связанного списка. Не нарушает ли это мандат, согласно которому наследование представляет отношение это есть1? Вовсе нет; LinkedList — специальный узел, который обозначает начало списка. Сам список — это виртуальное построение, абстракция. Объект LinkedList, по существу, представляет собой дескриптор списка. Реализация связанного списка Когда создается узел LinkedList, он немедленно строит узел TailNode: LinkedList::LinkedList()
Наследование, полиморфизм и повторное использование программных кодов Глава 3 nextNode = new TailNode; Таким образом, пустой список состоит из двух таких объектов: Linked List и TailNode, а узлы InternalNodes, содержащие Data, отсутствуют (рис. 3.3). Когда вы вводите данные в LinkedList, узел LinkedList передает данные туда, куда показывает его соб- собственный указатель nextNode. Изначально это TailNode: Node * LinkedList::Insert(Data * theData < nextNode = nextNode->Insert(theData); return this; } Когда в узел TailNode вызывается Insert(), TailNode знает, что передаваемый в него объект — это наи- наименьший объект в списке. Таким образом, TailNode вставляет новый узел в список непосредственно перед собой. Если новый объект — первый объект, включаемый в список, то он по определению является наимень- наименьшим объектом в списке, однако любой объект, появляющийся в хвосте, должен быть наименьшим, в противном случае он уже должен быть включен в список другим узлом, находящимся ближе к началу списка. Как узел TailNode вводит новый объект данных? Он порождает новый экземпляр узла InternalNode. Для построения нового InternalNode конструктору требуется два аргумента: указатель на данные и указатель на узел, который его создал (в рассматриваемом случае — TailNode). Затем новый узел присваивает свой указа- указатель данным, а также свой указатель nextNode узлу, которому он был дан, а именно: TailNode::Insert(Data * theData) Node { InternalNode * dataNode = new InternalNode(theData, this); return dataNode; Новый экземпляр InternalNode указывает на данные, а также на TailNode. Узел TailNode возвращает вызывающему объекту указатель на новый, созданный им узел. В первом случае TailNode возвращает указа- указатель на новый InternalNode узлу LinkedList. Узел LinkedList назначает собственный указатель nextNode на возвращаемое значение из nextNode->Insert(). Таким образом, теперь LinkedList указывает на новый InternalNode, как показано на рис. 3.4. РИСУНОК 3.3. Пустой список. РИСУНОК 3.4. Список после включения в него узла. В следующий раз, однако, LinkedList будет передавать данные тому объекту, на который указывает его указатель nextNode, но в этом случае указатель указывает не на TailNode, а на InternalNode. Когда InternalNode получает объект Data, он сравнивает собственные данные с данными из нового узла, например, таким образом: Node * InternalNode::Insert(Data * theData) int result = myData->Compare(*theData); Новый объект либо больше существующего узла, либо меньше. Если он меньше, то InternalNode пере- передает объект Data туда, куда указывает его собственный элемент данных NextNode, т.е. новый объект Data передается следующему узлу в списке: case JcIsSmaller: nextNode = nextNode->Insert(theData); return this; В свою очередь, каждый InternalNode анализирует данные. Если новые данные являются наименьшими в списке, они в конечном итоге переместятся в TailNode и будут включены в список в качестве последнего
Объектно-ориентированное программирование Часть I узла. С другой стороны, если новый объект Data попадает в узел InternalNode, содержащий данные, кото- которые меньше,.чем новые данные, то этот InternalNode включает новые данные в список. В этом случае InternalNode делает то, что делал TailNode: он создает новый узел InternalNode и дает новому ItiternalNode инструкции указывать на него (т.е. новый узел InternalNode указывает на InternalNode, который он создал). Таким образом, он фактически включает новый объект в список непосредственно перед текущим InternalNode. Затем текущий узел возвращает указатель на новый узел, так что, кто бы ни вызвал Insert(), он теперь может установить связь с новым InternalNode. case klsSame: // неудача case JclsLarger: // новые данные помещаются передо мной { InternalNode * dataNode = new InternalNode(theData, this); return dataNode; ПРИМЕЧАНИЕ В этом примере программного кода реализуется проектное правило, которое заключается в том, что объекты того же размера, что и текущий объект, рассматриваются как объекты большего размера. Вы легко можете изменить этот код, чтобы исключить дубликаты или обращаться с ними особым способом. После того как данные оказались в списке, мы запрашиваем у пользователя новый объект. Программа продолжается до тех пор, пока пользователь не укажет, что он закончил ввод данных. На этой стадии потребуем, чтобы Linked List отобразил эти данные. Узел LinkedList пересылает эту команду туда, куда ука- указывает его элемент данных NextNode: virtual void Show() { nextNode->Show() ; } если список пуст, LinkedList указывает на TailNode. Метод Show() узла TailNode никаких действий не совершает, соответственно и отображать нечего. Более обычным является ситуация, когда LinkedList ука- указывает на InternalNode. Для метода Show() узла InternalNode характерно действие virtual void Show() { myData->Show() ; nextNode->Show() ; } Каждый InternalNode требует от своего объекта данных печатать его значение, а затем посылает коман- команду тому узлу, который стоит следующим в связанном списке. Таким образом, метод Show() вызывается по очереди для каждого объекта данных Data. В конечном итоге доходит очередь и до метода Show() узла TailNode, на чем и заканчивается отображение. Когда возникает необходимость удалить список, клиенту достаточно удалить только узел LinkedList. В деструкторе LinkedList мы удаляем следующий узел списка таким образом: -LinkedListQ { delete nextNode; } Каждый узел, в свою очередь, удаляет следующий за ним в списке узел, пока не дойдет очередь до ухи TailNode, на этом, собственно говоря, удаления и заканчиваются. Обратите внимание на тот факт, что совсем необязательно знать, сколько узлов содержится в списке Удаление узла 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 & theOtherData) { if (dataValue < theOtherData.dataValue) return JcSmaller; if (dataValue > theOtherData.dataValue) return JcLarger; 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 JcLarger: // отображаются новые данные { InternalNode * dataNode = new InternalNode(theData, this) return dataNode; } case kSmaller: nextNode = nextNode->Insert(theData); return this; } return this; } class TailNode : public Node { public: TailNode () {} -TailNode () {} virtual Node * Insert(Data * theData); virtual void Show() { } 3 Зак.53
Объектно-ориентированное программирование Часть I private: }; Node * TailNode::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 TailNode; 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? @ 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 и TailNode, чтобы строить в высшей степени децентрализованные приложения. Это позволит повысить уровень инкап- инкапсуляции, благодаря чему с программными кодами легче будет работать. Наиболее важной частью этого кода с точки зрения понимания полиморфизма., является точка, в ко- которой каждый Node вызывает nextNode->Insert(). Каждый узел, отличный от TailNode, знает только, что у него имеется указатель на следующий узел списка, но не знает, является ли этот узел InternalNode или TailNode, однако различие между ними определяет статический тип. Проанализируем функции InternalNode::Insert() и TailNode::Insert() — они существенно отличаются друг от друга. Объект, вызывающий их, не знает, какую из них он вызывает, однако все идет как надо. Аналогично, когда вы требуете от узлов проявить себя, все происходит должным образом, т.е. LinkedList требует от следующего узла проявить себя, каждый узел InternalNode отображает содержащиеся в нем дан-
Наследование, полиморфизм и повторное использование программных кодов Глава 3 ные, а затем посылает эту команду следующему за ним узлу; узел TailNode, завершая цепь, действует как часовой. Абстрактные классы Класс Data, с которым мы работали в первой части этой главы, достаточно прост. Может случиться так, что приложение потребует несколько различных, но в то же время связанных между собой классов данных. При этом может оказаться полезным иметь возможность полиморфного подхода к этим объектам, позволяя связанному списку манипулировать классами Data без учета требований специфики отдельных производных типов. С этой целью вы можете принять решение присвоить классу данных Data тип абстрактных данных (ADT — Abstract Data Type). Чтобы сделать это, вы должны добавить этому классу одну или большее число чисто виртуальных функций, как показано в листинге 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)rmyVal(val){} virtual ~Data(){) int Compare(const Data &) ; 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)
Наследование, полиморфизм и повторное использование программных кодЬв 10 Глава 3 return kSmaller; if (myVal > theOtherData.myVal) return kLarger; else return kSame; } class Node // тип абстрактных данных { public: Node()U 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: // отображаются новые данные { InternalNode * dataNode = new InternalNode(theData, this) return dataNode; } case kSmaller: nextNode = nextNode->Insert(theData); return this; ) return this; } class TailNode : public Node { public: TailNode(){} ~TailNode() {} virtual Node * Insert(Data * theData); virtual void Show() { ) private: Node * TailNode::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,: :LinkadList() { nextNode = new TailNode; } Nods * LinkedList::Insert(Data * theData) { nextNode -- next.Node->Insert (theData) ; return this; } int main О { Data * pData; int val; int whichData; LinkedList 11, for (;.-) { cout « "[1] Integer, [2] Written, [0] Quit: "; cin » whichData; if CwhichDatH) break; coat « "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"; retiirn 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 () t 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 представлен простой набор классов.
Объектно-ориентированное программирование Часть I Листинг 3.5. Перегрузка членов ¦include <iostream.h> class MyPoint public: MyPoint (int x. -MyPoint (){ } int void int void private: int int GetX() const SetX(int x) GetY() const SetY(int y) myX; myY; int { { { { y):myX(x), : return myX = return myY = myX; x; } myY; y; ) { } class MyRectangle public: MyRectangle(MyPoint upperLeft, MyPoint lowerRight): myUpperLeft(upperLeft), myLowerRight(lowerRight) -MyRectangle () { } int GetWidth() { return myLowerRight.GetX() - myOpperLeft.GetX(); } int GetHeightO { return myLowerRight.GetY() - myUpperLeft.GetY(); } private: MyPoint myUpperLeft; MyPoint myLowerRight; int main О MyPoint ul@,0) ; MyPoint lwB0,30); MyRectangle myRect(ul,lw); 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) U 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; int main 0 Point ul@,0); Point lwB0,30); Rectangle myRect(ul,lw); Rectangle otherRect@,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)),
Объектно-ориентированное программирование Часть! myLowerRight(new Point(lowerRightX,lowerRightY)) U -Rectangle(){ delete myUpperLeft; delete myLowerRight;} int GetWidthO { return myLowerRight->GetX() - myUpperLef t->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 & 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 GetHeightO { return myLowerRight->GetY() - myUpperLeft->GetY() ; \ II приватные: Point * myUpperLeft; Point * myLowerRight; }; void SomeFunction( Rectangle ) ; int main() { Point ul@,0); Point lwB0,30) ; Rectangle myRect(ul,lw); cout « "myRect measures "; cout « myRect.GetWidth(); cout « " by " « myRect.GetHeight() « 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.GetHeightO « endl; cout « "r address: " « Sr « endl; cout « "r->myUpperLeft: " « r.myUpperLeft « endl; cout « "sr .myOpperLef t: " « Sr .myUpperLeft « endl; cout « "Returning from SomeFunction!"; Выходные данные программы, представленной в листинге 3.8: myRect measures 20 by 30 myRect address: Ox0012FF6C myRect->myUpperLeft: 0x00421180
Объектно-ориентированное программирование Часть! tonyRect.myUpperLeft: 0x0012FF6C In Rectangle's copy constructor... r measures 20 by 30 r address: 0x0012FF48 r->myUpperLeft: 0x00421180 Sr.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 иллюстрируется возникшая проблема. 0x00421180 РИСУНОК 3.5. Проблема использования двумя объектами одной области памяти, возникшая в результате поверхностного копирования. Rectangle Свободная память Когда удаляется копия, память помечается как свободная. Когда удаляется исходный объект, вы удаля- удаляете уже удаленный указатель. Если вам повезет, программа сразу же завершится аварийно. Это не очень хорошо. Выход из этой ситуации следует искать в построении конструктора копий, который не только копирует указатель, но и объект, на который он указывает. На рис. 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, myUpperLeft(new Point(upperLeftX,upperLeftY)), myLowerRight(new Point(lowerRightX,lowerRightY)) int lowerRightY) : Rectangle( const Rectangle & 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 GetHeightf) { return myLowerRight->GetY() - myUpperLeft->GetY(); } приватные: Point * myUpperLeft; Point * myLowerRight; 0x00421180 РИСУНОК 3.6. Глубокая копия. Rectangle -myUpperteft: Point * -myUiwerflight: Point' Некоторый другой адрес Объект Point -mvUppenjft: Point * ¦тЛонеЯШ: Point* Свободная память Ниже приведены выходные данные программы, представленной в листинге 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 Sr.myUpperLeft: OxOO12FF34 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 & 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 & 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; 1; 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 & 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 & rhs); int GetWidthO { return myLowerRight->GetX() - myUpperLeft->GetX(); } int GetHeightO { return myLowerRight->GetY() - myUpperLeft->GetY(); } //приватные: Point * myUpperLeft; Point * myLowerRight; }; Rectangle & Rectangle::operator=(const Rectangle & rhs) < if ( this == Srhs ) // защита от случая а = a return *this; delete myUpperLeft; delete myLowerRight; myUpperLeft = new Point(*rhs.myUpperLeft); myLowerRight= new Point(*rhs.myLowerRight); return *this; ) int main() { Point ul@,0); Point lwB0,30); Rectangle myRect(ul,lw); Rectangle otherRect@,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 « "otherRect->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 a=0, Ь=5; а = Ь++; После того как эти операторы будут выполнены, а будет 5, a b — б, т.е. а будет присвоено значение b E) и после этого значение b будет увеличено на 1. Чтобы обеспечить это, оператор увеличения должен быть готов возвратить исходное значение, которое имела переменная Ь, и в то же время установить значе- значение b на 1 больше. Это легко проследить на примере. Во-первых, мы должны решить, имеет ли смысл оператор увеличе- увеличения для нашего класса. Что значит увеличить Rectangle на 1? Конечно, мы можем присвоить любое значе- значение, какое пожелаем, — можем использовать оператор увеличения, чтобы возвратить положение координат верхнего левого угла: Point upperLeft = myRect++; // возвратить текущие координаты верхнего левого угла с // помощью оператора ++ Это вполне допустимо в C++. Если бы вы работали у автора, то это была бы последняя строка C++, какую вы написали бы в его фирме. Для этого языка справедлива старая истина: в C++ трудно выстрелить в ступню, но, если вы умудрились сделать это, вам оторвет всю ногу. Естественно, оператор увеличения (++) следует использовать для увеличения значений на 1. Тем не менее, далеко не очевидно, что значит "увеличить прямоугольник на 1", и автор не намерен использовать оператор увеличения таким образом. Какой смысл имеет выражение "увеличить точку на 1"? Это граничное условие; вы можете решить, что увеличение точки на 1 означает, что увеличивают на 1 обе координаты х и у, но это означает растянуть Point. В листинге 3.11 показано, как вы можете построить такой оператор увеличения. Листинг 3.11. Оператор увеличения ¦include <iostream.h> class Point
Наследование, полиморфизм и повторное использование программных кодов Глава 3 public: Point (int x, int y):myX(x), myY(у) {} Point (const Point S 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 s operator++(); Point operator++(int); const Point & operator—(); Point operator--(int); private: int myX; int myY; const Point S 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(upperLef t)), 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; delete myUpperLeft; delete my LowerRight; } Rectangle & operator=(const Rectangle & rhs); void Expand() { — (*myUpperLeft) ; ++(*myLowerRight) ; } int GetWidth() { return myLowerRight->GetX () - myUpperLef t->GetX () : } int GetHeightf) { return myLowerRight->GetY() - myUpperLeft->GetY(); } private: Point * myUpperLeft; Point * my LowerRight; }; Rectangle & Rectangle::operator=(const Rectangle s rhs) i if ( this == Srhs ) // защита от случая а = a return *this; delete myUpperLeft; delete myLowerRight; myUpperLeft = new Point(*rhs.myUpperLeft); myLowerRight= new Point(*rhs.myLowerRight); return *this; } int main() { Point ulA0,10) ; Point lwB0,30) ; Rectangle myRect(ul,lw); 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). В рамках этой простой иллюстрации класс Griffin (Гриф) наследует ха- характеристики и поведение от Lion (Лев) и от Eagle (Орел). Следовательно, Griffin может EatMeat() (Есть мясо), Roar() (Рычать), Squawk() (Клеко- (Клекотать) и Fly() (Летать). Это реализуется на языке C++ путем формирования списков базовых классов, отделенных друг от друга запятыми. В листин- листинге 3.12 демонстрируется реализация классов, модели которых представле- представлены на рис. 3.7. Листинг 3.12. Пример реализации множественного наследования #include <iostream.h> class Lion lion EatMeatO Roar() Eagle Squawk) FtyO i t Griffin РИСУНОК 3.7. Простое множественное наследование. public:
Объектно-ориентированное программирование Часть I void EatMeatO ; void Roar () ; protected: private: }; class Eagle { public: void Squawk(); void Fly<) ; >; class Griffin : public Lion, public Eagle Этот программный код сделан намеренно кратким — из него исключены конструкторы, деструкторы и прочее для упрощения и наглядности. Класс 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(l) {} void Sleep(){} int GetAge() const private: int age; { return age; } Animal age : int G*Age<) > Lion Animal age : int itAgeO Eagle Squawk,, Griffin class Lion : public Animal { public: void EatMeatO О void Roar(){} protected: private: ); class Eagle : public Animal { public: void Squawk () {} void Fly (){} РИСУНОК З.8. Два класса совместно используют общий базовый класс. class Griffin : public Lion, public Eagle
Наследование, полиморфизм и повторное использование программных кодов Глава 3 }; int main () I Griffin g; cout « g.GetAge(); // Неоднозначность! Не следует выполнять компоновку! return 0 ; } Этот общий базовый класс Animal содержит переменную-член, которую класс Griffin наследует дважды. Когда вы запрашиваете переменную-член age (возраст), компилятор не обязательно будет знать, возраст какого объекта вы имеете в виду, поэтому он сообщит об ошибке в этих строках: 'Griffin::беtAge' 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 A) { } 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() { } >; 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. Инициализация базовых классов с виртуальным наследованием tinclude <iostream.h> class Animal { public: Animal(int theAge):age(theAge){} void Sleep () {} int GetAgeO const { return age; } private: int age. Animal Age : int * {Virtual} Lion EatMeat( Roar() t Eagle РйГ4 Griffin РИСУНОК 3.3. Моделирование виртуального наследования. class Lion : virtual public Animal { public: Lion(int theAge, int howManyCubs):Animal(theAge), numCubs(howManyCubs){} void EatMeat(){} void Roar(J{} 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, Lion(theAge, theWeight), Eagle(theAge, howManyCubs), Animal(theAge){} int howManyCubs): // инициализация базового класса Lion // инициализация базового класса Eagle // инициализация виртуального базового класса Animal! int main() int hisAge = 5; int hisWeight = 7; int litterSize = 4; Griffin g(hisAge, hisWeight, litterSize) ;
Наследование, полиморфизм и повторное использование программных кодов Глава 3 cout « g.GetAge(); return 0; Множественное наследование и включение Как узнать, когда использовать множественное наследование, а когда его следует избегать? Должен чи автомобиль наследовать что-либо от руля или шины? Вы должны реализовать класс Саг, как показано ниже: class SteeringWheel class Door class Tire class Car : public SteeringWheel, public Door, public Tire Хотя такой профаммный код поддается компилированию, тем не менее, модель, которую он реализует, плохо структурирова- структурирована, что следует из рис. 3.10. Полезно возвратиться к базовым понятиям: общедоступное наследование всегда должно моделировать конкретные предметы. Такое положение можно выразить следующей формулой: насле- наследование должно моделировать отношение есть. Если вы хотите моделировать отношение имеет (например, автомобиль имеет руль), то для этой цели можно использовать агрегирование. В C++ агрегирование реализуется с помощью переменных-членов, т.е. вы придаете классу Саг переменную-член SteeringWheel: class SteeringWheel SteeringWheel i, Dooi Tire > t Car РИСУНОК 3.10. Неудачная попытка построения модели с наследованием. class Door { >; class Tire { }; class Car < public: private: SteeringWheel s; Door d[2]; Tire t[4]; }; Вопрос заключается в том, является ли автомобиль рулем или он имеет руль? Вы можете возразить. что автомобиль есть совокупность руля и шин, однако наследование не моделирует этого обстоятслье o;i. Тем не менее, автомобиль не является конкретизацией этих предметов — это агрегация этих предмекш Автомобиль имеет руль, дверцы и шины. Вы изображаете эти отношения на диаграмме на языке UML. используя символ агрегации, показанный на рис. 3.11. Незаштрихованный ромб класса Саг указывает на то, что объект Саг имеет то, что показано на дру- другом конце соединяющей линии. В рассматриваемом случае Саг имеет, по меньшей мере, один руль, дьер
Объектно-ориентированное программирование Часть I цы и шины. Можно уточнить эту диаграмму, добавив свойство множественности, т.е. указать, сколько объек- объектов различного типа может иметь объект Саг (рис. 3.12). Саг Саг SteeringWheel Door Tire SteeringWheel Door Tire РИСУНОК 3.11. Моделирование с агрегацией. РИСУНОК 3.12. Обозначение множественности в языке UML. На рис. 3.12 показано, что Саг имеет один SteeringWheel (Руль управления), четыре шины (Vehicle (Транс- (Транспортное средство) с другим числом шин не являются Саг (Автомобилем)) и от двух до пяти дверец. Резюме Полиморфизм — это мощное инструментальное средство, предоставленное в распоряжение программи- программистов, работающих на языке C++. Оно может быть реализовано путем перегрузки методов и операторов, что предоставляет клиенту большую гибкость в его взаимодействии с серверным объектом. Еще более мощная реализация полиморфизма может быть достигнута путем применения виртуальных методов перекрытия в порожденных классах. Может быть построена полная иерархия классов, каждый из которых специализиру- специализируется на реализации виртуальных методов. Множественная реализация и включение представляют собой инструментальные средства, предназна- предназначенные для построения мощных полиморфных моделей, однако важно сосредоточить внимание не столько на реализации, сколько на проектировании и семантике своей модели. Если вы понимаете суть проекта своего класса, реализация вытекает из этого естественным путем.
Вопросы реализации В ЭТОЙ ЧАСТИ Управление памятью Использование каркасов приложений Контейнерные классы библиотеки STL Итераторы и алгоритмы STL Исключение конфликтов имен Манипулирование типами объектов Настройка производительности приложений ЧАСТЬ
! , Управление памятью 1 / . 1 4, В ЭТОЙ ГЛАВЕ ¦ Управление памятью и указатели ¦ Указатели и исключения ¦•.. >
Управление памятью Глава 4 Опыт преподавания автором языка C++ показывает, что управление памятью является труднейшей темой. Даже опытные программисты, работающие с C++, путаются в понимании указателей и ссылок. Одной из основных причин такой путаницы является частое механическое преподавание темы указателей ("вот как их использовать"), а не концептуальное ("вот что это такое"). После того как вы поймете, что указатель представляет собой не что иное, как переменную, которая содержит адрес, все остальное уже не покажется для вас очень сложным. Основы такого понимания пере- перечислены ниже: ¦ Указатель является переменной и потому имеет собственный адрес. ¦ Указатель содержит адрес объекта, в котором вы заинтересованы. ¦ Вы получаете интересующий вас объект, разадресовывая указатель. ¦ Оператор new возвращает адрес. Указатель представляет собой место для хранения адреса. Все сказанное несколько усложняется, когда добавляются ссылки, поскольку они, по сути, являются автоматически разадресованными указателями. Трудность со ссылками состоит в том, что они всегда ука- указывают на объект и никогда не указывают на самих себя. Ниже приведен краткий тест на то, понимаете ли вы ссылки: #include <iostream.h> int main () { int x = 7; int & ref = ж; ref = 8; int у = 10; ref = y; cout « "x = " « x; cout « " у = " « у; cout « " ref = " « ref « endl; return 0; } Вопрос заключается в том, что будет напечатано? Прекратите чтение, пока не укажете ответ. Ответили ли вы так: ж = 8, у = 10, ref = 10 Если да, то это самая широко распространенная ошибка (не огорчайтесь, вопрос трудный). Обратите внимание на оператор: ref = у; Данный оператор не заставлял ref указывать на у, как было бы, если бы было записано: ж = у; Таким образом, правильный ответ таков: х = 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 S rhs):myValue(rhs.myValue) < cout « "In myClass copy constructor\n"; > -myClass() { cout « "In myClass Destructor\n"; } int GetValueO const { return myValue; } private: int myValue; }; int somePunction(myClass c) ; int main() { cout « "In main, ready to create objectC)...\n"« endl; myClass object C); 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.GetValueO « endl; cout « "Exiting someFunction...\n" « endl; return с.GetValue() ; ) Ниже приведен вывод программы в листинге 4.1: In main, ready to create objectC)... 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() создается объект в стеке и затем передается по значению someFunctionO. В результате передачи по значению в области видимости функции someFunctionO создается копия. Когда копия покидает область видимости, вызывается деструктор. Когда объект в 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 objectC)..."« endl; myClass * pObject = new myClassC); cout « "In main, pObject's value is: " « pObject-XSetValue () « 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 с.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 C)... 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 myClassC); Как результат вызывается конструктор. Мы передаем объект по значению (разадресовывая указатель) функции SomeFuiictionQ с помощью оператора: someFunction(*pObject);
Вопросы реализации Часть II Затем вызывается конструктор копии. Теперь объект создается из стека и разрушается, если функция возвращает управление (обратите внимание на вызов деструктора). Затем мы передаем объект перегруженной функции SomeFunction() по ссылке, как показано: someFunction(pObject); В этот раз конструктор копии не вызывается потому, что копия не создается. Объект, на который про- происходит ссылка в данной функции, — это тот же самый объект, на который имеется ссылка в mainQ. Когда метод возвращает управление, то деструктор не вызывается. В данный момент мы готовы покинуть main(), но обязаны явно вызвать оператор delete. Каждое обращение к оператору new должно сопровождаться вы- вызовом оператора delete, в противном случае потребуется расход памяти. Расход памяти Термин расход (утечка) памяти распространен в индустрии программного обеспечения, но объяснение ему дается не часто. Вот идея: если память распределяется оператором new и не востребуется обратно опе- оператором delete, то она безвозвратно теряется до самого конца программы. Дело обстоит так, будто память "утекает" из программы. Если такую ошибку сделать в функции, которая распределяет много памяти (несколько больших объек- объектов или множество маленьких), и вы повторно вызовете данную функцию, то вполне возможно разрушить программу (или замедлить ее работу), поскольку появится недостаток в доступной памяти. В C++, в отли- отличие от Java, нет автоматического возврата памяти ("сборки мусора"). Вы должны востребовать память само- самостоятельно. Распределение массивов Если распределяется массив памяти, то следует использовать специальный оператор delete[]. Если его не использовать, а использовать просто delete (без квадратных скобок), то высвободится только первый объект, а остаток памяти будет утерян. В листинге 4.3 демонстрируется применение оператора delete[]. Листинг 4.3. Использование delete[] #include <iostream.h> class myClass { public: myClass(int val=O):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 GetValueO 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]r 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 <iostream.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 myClassE); cout « "The value of the object is " « pc->GetValue () « endl;
Вопросы реализации Часть II delete pc; рс = 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 * pi; // указываемая переменная int является хонстантой Если справа от ключевого слова const находится переменная, то постоянным является сам указатель: int * const p2; // р2 является хоястантой и не может указывать на что-либо еще Указатели const и функции-члены const Если типом const объявляется функция-член, то компилятор отмечает как ошибку любую попытку этой функции изменить данные в объекте, которому функция принадлежит. При объявлении объекта типа const вы, по сути, объявляете, что указатель this является указателем на объект const. Указатель const this может использоваться только с функцией-членом const. Передача по ссылке При каждой передаче объекта в функцию по значению создается копия этого объекта. Каждый раз, когда объект возвращается из функции по значению, создается другая копия. Эти объекты создаются в стеке. Их копирование занимает время и память. Для маленьких объектов, подобных встроенным целым значениям, это незначительные затраты.
Управление памятью Глава 4 Однако для больших объектов, созданных пользователем, затраты возрастают. Размер созданного пользо- пользователем в стеке объекта — это сумма всех его переменных-членов. Они, в свою очередь, могут оказаться созданными пользователем объектами и передача такой огромной структуры путем копирования ее в стеке может оказаться очень дорогостоящим удовольствием в смысле производительности и расхода памяти. Если временный объект разрушается, а это происходит тогда, когда функция возвращает управление, то вызывается деструктор объекта. Если объект возвращается по значению, то будет создаваться и разру- разрушаться и копия данного объекта. Передача указателя const Хотя передача указателя более эффективна по сравнению с передачей объекта по значению, но она и более опасна, поскольку позволяет изменить объект и обесценивает защиту, которая обеспечивается при передаче по значению. Передача по значению похожа на то, как если бы вы дали музею фотографию произведения искусства, а не реальную вещь. Если вандалы повредят фотографию, то с оригиналом ничего не произойдет. Передача по ссылке похожа на то, как если бы вы передали музею свой домашний адрес и предложили бы гостям прийти и посмотреть на реальную вещь. Решение заключается в передаче указателя const. Такой прием предотвратит вызов какого-либо метода, отличного от const, и тем самым обеспечит защиту, предлагаемую передачей по значению. В то же время будет сохранена эффективность передачи по ссылке. Гости опять приглашаются в дом, но произведение искусства защищено пуленепробиваемым стеклом. Передача постоянной ссылки предоставляет такую же эффективность, не заставляя вас использовать довольно неуклюжий синтаксис разадресации указателей. Постоянная ссылка на самом деле является ссылкой на постоянный объект (сами по себе ссылки всегда постоянны). Передавая ссылку на постоянный объект, его нельзя изменить, но при этом конструктор копии не вызывается. Вы можете передать постоянную ссылку только тогда, когда знаете, что объект не является NULL. Помните, что, хотя возможно создать ссылку на объект NULL, однако это сделает программу некоррект- некорректной. Она может разрушиться, может работать вполне хорошо, но она незаконна с точки зрения C++, и результаты ее непредсказуемы. Возврат ссылки на объект, который не находится в области видимости После того как программисты, работающие с C++, обучатся передавать параметры по ссылке, будет не исключена тенденция увеличения расхода памяти. Однако такую тенденцию можно преодолеть. Помните о том, что ссылка — это всегда псевдоним какого-то другого объекта. Если вы передаете ссылку в функ- функцию или из нее, то обязательно спросите себя: "Какому объекту дать псевдоним и будет ли он существо- существовать всякий раз, когда его используют?" Листинг 4.5 иллюстрирует возвращение ссылки на временный объект. Обратите внимание, что некото- некоторые компиляторы этого не допустят. В любом случае данная программа почти наверняка разрушится при выполнении. Она некорректна. Листинг 4.5. Возвращение ссылки на временный объект tinclude <iostream.h> class myClass { public: myClass(int val=0):myValue(val) { cout « "in myClass constructor\n"; } myClass(const myClass & rhs):myValu*(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 & WorkFunctionO { myClass meE); return me; } void SomeFunction () { myClass srC = WorkFunctionO; int value = rC.GetValue() ; cout « "rC's value: " « value « endl; Ниже показан вывод из программы листинга 4.5: In myClass constructor In myClass destructor rC's value: 1245036 Функция WorkFunction() создает объект в стеке и возвращает ссылку на него, которая присваивается локальной ссылке (гс) в функции SomeFunction(). К сожалению, теперь гс ссылается на объект, который покинул область видимости. Таким образом, ссылка недействительна, и вся программа некорректна. Может появиться искушение решить проблему, заставив WorkFunctionO создать объект из кучи. Когда функция возвратит управление, объект все еще будет существовать: #include <iostream.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(); int main () { SomeFunction () ; return 0; } myClass S WorkFunctionO { myClass * pC = new myClass E); return *pC; } void SomeFunction() { myClass SrC = WorkFunctionO; int value = rC.GetValue(); cout « "rC's value: " « value « endl;
Управление памятью Глава 4 Ниже следует вывод: In myClass constructor rC's value: 5 Этот пересмотренный код решает проблему, представленную в листинге 4.5, но за счет расхода памяти. Как восстановить память, распределенную в функции WorkFunction()? Конечно, можно удалить ее в SomeFunction(), как показано в следующем примере: iinclude <iostream.h> class myClass { public: myClass(int val=0):myValue(val) { cout « "In myClass constructor\n"; B} myClass(const myClass s rhs):myValue(rhs.myValue) { cout « "In myClass copy constructor\n"; ) ~myClass() { cout « "In myClass D*structor\n"; } int GetValueO const { return myValue; } void SetValue(int theVal) { myValue = theVal; } private: int myValue; }; void SomeFunction{); myClass SWorkFunction(); int main() { SomeFunction () ; return 0; } myClass SWorkFunction() { myClass * pC = new myClassE); return *pC; ) void SomeFunction () { myClass SrC = WorkFunction() ; int value = rC.GetValueO ; 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 S rhs) : myX(rhs.myX), myY(rhs.myY){ cout « "Point copy constructor called" ~Point(){ cout « "Point destructor called" « endl;} int GetX() const { return myX; } void SetX(int x) { myX = x; ) int GetY() const { return myY; J 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 * pOpperLeft, Point * pLowerRight): myOpperLeft ( new Point(*pOpperLeft)), myLowerRight(new Point(*pLowerRight)) « endl;} Rectangle(int upperLeftX, int upperLeftY, int lowerRightX, int lowerRightY): myOpperLeft(new Point(upperLeftX,upperLeftY)), myLowerRight(new Point(lowerRightX,lowerRightY)) U
Управление памятью Глава 4 Rectangle( const Rectangle & rhs ) : myUpperLeft(new Point(*myUpperLeft)) , myLowerRight(new Point(*myLowerRight)) {} -Rectangle() { cout « "In Rectangle's destructor" « endl; delete myUpperLeft; delete myLowerRight; int GetWidthO { return myLowerRight->GetX () - myUpperLef t->GetX() ; } int GetHeightO { 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); Point * pLR = new PointB0,30) ; Rectangle myRectangle(pUL, pLR); int w = myRectangle.GetWidthO ; int h = myRectangle.GetHeight(); cout « "the Rectangle is " « w « " by " « h « endl; delete pUL; delete pLR; catch ( myException & e ) cout « "caught exception: " « e.errorMsgO « "\n\n" « endl; } try cout « "Begin round 2..." « endl; Point * pUL = new Point @,0); Point * pLR = new PointB0,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 & 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 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 * pOL » 0; Point * pLR » 0; try { cout « "Begin round 1..." « endl ; pUL ж new Point@,0); pLR » new Point B0,30) ; Rectangle myRectangle(pUL, pLR); int w = myRectangle.GetHidthO ; int h = myRectangle.GetBeightO ; cout « "the Rectangle is " « w « " by " « h « endl; delete pUL; delete pLR; ) catch ( myException ( e ) { cout « "caught exception: " « e.errorMsgO « "\n\n" « endl; try cout « "Begin round 2..." « endl; pUL = new Point@,0) ; pLR = new PointB0,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 & 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; pOL = new Point@,0); pLR = new PointB0,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 & e ) < cout « "caught exception: " « e.errorMsgO « "\n\n" « endl; } try { cout « "Begin round 2..." « endl; pUL = new Point @,0) ; pLR = new PointB0,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 & 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 call«d"« 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; }
Управление памятью Глава 4 void SetY(int у) { туУ = у; ) private: int туХ; int туУ; }; class Rectangle < public: Rectangle( Point upperLaft, Point lowerRight ): myOpperLeft(new Point(upparLeft)), myLowerRight(new Point(lowerRight)) О Rectangle( auto_ptr<Point> pOpperLeft, auto_ptr<Point> pLowarRight ): myUpperLeft (new Point(*pUpparLaft)), myLowerRight(new Point(*pLowerRight)) О Rectangle( int upperLeftX, int upperLeftY, int lowerRightX, int lowerRightY ): myUpperLeft(naw Point(upperLeftX,upperLeftY)), myLowerRight(new Point(lowerRightX,lowerRightY)) {} Rectangle( const Rectangle ? rhs ) : myOpperLeft(new Point(*myOpperLeft)) , myLowerRight(naw Point(*myLowerRight)) О -Rectangle(){ cout « "In Rectangle's destructor" « andl; } int GetWidthO { return myLowerRight->GetX() - myOpperLeft->GetX(); } int GetBeight() { return myLowerRight->GetY() - myOpperLeft->GetY(); } void DangerousMethodO { throw myException(); } private: auto_ptr<Point> myOpperLeft; auto_ptr<Point> myLowerRight; ); int main() { try { cout « "Begin round 1..." « endl; auto_ptr<Point> pUL(new Point@,0)); auto_ptr<Point> pLR(naw PointB0,30)); Rectangle myRectangle(pOL, pLR); int w = myRectangla.Getwidth(); int h = myRectangle.GetHeight(); cout « "the Rectangle is " « w « " by " « h « endl; } catch ( myException & e ) { cout « "caught exception: " « e.errorMsgO « "\n\n" « endl; try cout « "Begin round 2..." « endl ; auto_ptr<Point> pOL(new Point@,0));
Вопросы реализации Часть II auto_ptr<Point> pLR(new Point B0,30)); Rectangle myRectangle(pOL, pLR); int w = myRectangle. GetWidth (); int h = myRectangle.GetHeight(); cout « "the Rectangle is " « w « " by " « h « endl; myRectangle.DangerousMethod(); } catch ( myException & e ) { cout « "caught exception: " « e.errorMsg() « "\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() - myUpperLeft->GetX() ; } Нет зависимости от того, как вызывается метод GetX(): используется оператор указания на auto_ptr, как если бы это был обычный указатель. Копирование autoptr Что произойдет, если делать копию 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 () { try { cout « "Begin round 1..." « endl ; auto_ptr<Point> pUL(new Point@,0)) ; auto_ptr<Point> pLR(new PointB0,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-XJetX() « 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>S pUpperLeft, auto_ptr<Point>S 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 & 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 & String::operator[](int index) { if ( rcString->isShared())
Вопросы реализации Часть II { rcString->removeRef(); rcString = new countedString(rcString->cString); ) rcString->marlcNotShareable () ; return rcString->cString[index]; } Если строка помечена как монопольная, то она никогда уже не будет помечена как используемая со- совместно. Общеиспользуемого метода MarkShareableO не существует. Мы никогда не будем знать, что дан- данную память безопасно использовать совместно, поэтому не будем использовать ее совместно. Ключевыми методами являются AddRef() и RemoveRef(). Кто-то должен вызывать эти методы каждый раз при создании копии. Мы предпочитаем не беспокоить String такой обязанностью и потому создаем интеллектуальный указатель для объектов с подсчетом ссылок (RCSmartPointer), которому принадлежит эта ответственность: template<clasa T> class RCSmartPointer < public: RCSmartPointer(T* ptrToRC = 0) ; RCSmartPointer(const RCSmartPointers rha); -RCSmartPointer{); RCSmartPointers operator»(const RCSmartPointerfi rhs); T* operator->() const; T& operator*() const; private: T * pRC; void initialize(); }; Интеллектуальный указатель инициализируется указателем на любой объект с подсчетом ссылок — в нашем случае это String. Дело интеллектуального указателя, наращивать счетчик ссылок, когда добавляется новая ссылка, и уменьшать счетчик ссылок, когда указатель удаляется. Поскольку интеллектуальный указа- указатель должен наращивать указатель для обоих своих конструкторов, то он делает это с помощью метода initialize(): template<class T> void RCSmartPointer<T>::initialize() { if ( pRC « 0 ) return; if ( pRC->isShareable() = false ) { pRC = new T(*pRC); ) pRC->addRef () ; ) Здесь представлена суть объектно-ориентированной конструкции: делегирование ответственности и по- повторное использование. В листинге 4.9 представлена вся программа. Листинг 4.9. Делегирование ответственности ¦include <iostream.h> ¦include <string.h> template<dass T> class RCSmartPointer { public: RCSmartPointer(T* ptrToRC = 0) ; RCSmartPointer(const RCSmartPointers rhs); -RCSmartPointer () ;
Управление памятью Глава 4 RCSmartPointer& operator^(const RCSmartPointerS rhs) ; T* operator-:»() const; T& 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 () ; } template<class T> RCSmartPointer<T>::RCSmartPointer(T* ptrToRC): pRC (ptrToRC) { initialize () ; } template<class T> RCSmartPointer<T>::RCSmartPointer(const RCSmartPointer& rhs): pRC(rhs.pRC) { initialize () ; ) template<class T> RCSmartPointer<T>::-RCSmartPointer() { if ( PRC ) pRC->removeRef(); > template<class T> RCSmartPointer<T>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 addReft) { ++referenceCount; ) void removeRef() { if ( --referenceCount = 0 ) delete this; void markHotShareable() ( shareable = false; ) bool isShareable() { return shareable; } bool isShared() { return referenceCount > 1; } protected: ReferenceCounted():referenceCount@), shareable(true) (} ReferenceCounted(const ReferenceCountedi rhs) : referenceCount(O) , shareable(true) О ReferenceCountedS operator^(const ReferenceCountedS rhs) { return *this; } virtual ~ReferenceCounted(){) private: int referenceCount; bool shareable; class String public: String(const char * cString = ""):rcString(new 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 ReferenceCounted char * cString; countedstring( const char * initialCString) initialize(initialCString); countedstring( const countedstring S rhs) { initialize(rhs.cString); } -countedstring() delete [] cString; void initialize(const char * initialCString) cString = new char[strlen(initialCString) +1]; strcpy(cString,initialCString); void ShowAddress() const { cout « ScString; ) friend class RCSmartPointer<countedString>; RCSmartPointer<countedString> rcString; char s String::operator[](int index) if ( rcString->isShared())
Управление памятью Глава 4 rcString->removeRef(); rcString = new countedString(rcString->cString), > rcString->mar)cKotShareable() ; return rcString->cString[index]; int main() String si("hello world"); String s2 = si; // конструктор копии String s3("bye"); e3 = 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 0x0042118C s2: hello world 0x0042118C S3: hello world 0x0042118C Как видно, все три строки имеют в памяти один и тот же адрес. Вот как все это работает. В первой строке main() создается объект String. Поместив код в свой отладчик, вы обнаружите, что эта безобидная строка переносит вас в сердце всех обсуждаемых классов. Самое инте- интересное заключается в том, что вы увидите, как счетчики ссылок вызываются автоматически. Вот этапы работы: String si("hello world"); В стеке создается объект String. Дальнейший шаг переносит нас в конструктор String: String(const char * oString = ""):rcString(new countedString(cString)){ } При инициализации переменной-члена rcString мы входим в оператор new и затем в конструктор для countedString: countedString( const char * initialCString) { initialize(initialCString); } Прежде чем войти в тело конструктора, мы входим в инициализацию базового класса (неявную), в которой переменная-член ReferenceCounted устанавливается в 0: ReferenceCounted():referenceCount@), 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 template<class T> RCSmartPointer<T>::RCSmartPointer(T* ptrToRC): pRC(ptrToRC) initialize () ; Метод initialize() проверяет, установлен ли указатель в значение NULL. Если да, то никаких действий не требуется. В противном случае метод проверяет, является ли объект ReferenceCounted совместно исполь- используемым. Если нет, то распределяется новая память. В противном случае увеличивается счетчик ссылок: if ( pRC->isShareable() == false ) pRC = new T(*pRC) ; pRC->addRef () ; Это приводит к вызову метода addRef() базового класса: void addRef() { ++referenceCount; } В завершение описанной последовательности событий распределяется память в куче и эта память управ- управляется объектом с подсчетом ссылок, после того как инициализируется счетчик ссылок. То, что выглядит как стек-базированный объект, на самом деле управляет памятью для подсчета ссылок в куче. А ГДЕ КОНСТРУКТОР КОПИИ И ОПЕРАТОР ПРИСВАИВАНИЯ? Итак, только что мы занимались копированием объектов String, которые имеют указатели. Нуждаемся ли мы в конст- конструкторе копии и операторе присваивания? Хорошо, у нас есть один — сгенерированный компилятором по умолчанию. Хотя конструктор копии по умолчанию порождает поразрядную копию; это хорошо. Когда он копирует член rcString, то вызывается конструктор копии RCSmartPolnter или оператор присваивания и это гарантирует, что будет сделана глубокая копия — та, что использует подсчет ссылок. Объект 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."; } template<class T> class RCSmartPointer { public: RCSmartPointer(T* ptrToRC = 0) ; RCSmartPointer(const RCSmartPointer& rhs) ; ~RCSmartPointer(); RCSmartPointerS operator»(const RCSmartPointerS rhs) T* operator->() const; Ts operator*() const; private:
Управление памятью Глава 4 Т * pRC; void initialize(); ь- template<class 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 () ; } template<class T> RCSmartPointer<T>::RCSmartPointer(const RCSmartPointerS rhs) : pRC(rhs.pRC) { initialize () ; } template<class T> RCSmartPointer<T>::-RCSmartPointer() { if ( pRC ) pRC->removeRef(); } template<class T> RCSmartPointer<Т>Ь 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 { public: void addRef() { ++referenceCount; } void removeRef() { if ( —referenceCount = 0 )
Вопросы реализации Часть II delete this; } void markNotShareable() { shareable = false; } bool isShareable() { return shareable; } bool isShared() { return referenceCount > 1; } protected: ReferenceCounted():referenceCount@), shareable(true) {} ReferenceCounted(const ReferenceCountedS rhs): referenceCount@), 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 & 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 GetWidth() { return rcRect->myLowerRight->GetX() - rcRect->myUpperLeft->GetX();
Управление памятью Глава 4 int GetHeightO return rcRect->myLowerRight->GetY() - rcRect->myUpperLeft->GetY(); void DangerousMethod() { throw myException(); } private: struct countedRect : public ReferenceCounted char * cString; Point * myUpperLeft; Point * myLowerRight; countedRect( const Point * ul, const Point * lr) { initialize(ul,lr); } countedRect( Point ul, Point lr ) { initialize(Sul, Sir); } countedRect( const countedRect & rhs) initialize(rhs.myUpperLeft, rhs.myLowerRight); -countedRect () de le te my Uppe r Le ft; delete myLowerRight; void initialize(const Point * ul, const Point * lr) I myUpperLeft = new Point(*ul); myLowerRight = new Point(*lr); friend class RCSmartPointer<countedRect>; RCSmartPointer<countedRect> rcRect; int main() try cout « "Begin round 1..." « endl; Point pUL(O,O); Point pLRB0,30) ; Rectangle myRectangle(pUL, pLR) ; int w = myRectangle.GetWidthf) ; int h = myRectangle.GetHeight(); cout « "the Rectangle is " « w « " by " « h « endl; myRectangle.DangerousMethod(); cout « "You never get here.\n" « endl; catch ( myException & 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() возбуждает исключение, стек раскручивается, но память не теряется — интеллектуальные указатели уменьшаются и распределенная память разрушается. Резюме Хитрость в управлении памятью заключается в отчетливом понимании того, какую память вы распреде- распределяете и где находится указатель на эту память. Необходимо либо следить за указателями на память и обес- обеспечивать возврат памяти по окончании использования, либо обеспечивать сборку мусора в форме интеллектуальных указателей. Хотя интеллектуальные указатели имеют свои ограничения, они помогают защитить код от расхода памяти и особенно полезны из-за исключений. Чтобы быть действенной, программа должна хорошо управлять ресурсами и не допускать утечки памяти.
Использование каркасов приложений В ЭТОЙ ГЛАВЕ ¦ Microsoft Foundation Classes ¦ В перспективе ¦ Проблемы вытесняющей многопоточности ¦ Пример для изучения ¦ Служебные классы ¦ Документы и представления
Вопросы реализации Часть II Прекрасной, но не реализованной возможностью ООП является повторное использование программно- программного кода. Идея ООП состояла в создании классов, которые могли бы служить в качестве фундамента пост- построения новых программ. Компоненты можно было бы добавлять в новые архитектуры подобно тому, как электронные чипы размещаются на платах. Однако до сих пор это не реализуется — за исключением биб- библиотек каркасов приложений. Множество библиотек каркасов приложений сопровождают создание приложений в оконных средах, таких как Windows NT или X Window. До сих пор наиболее распространенной библиотекой остается Microsoft Foundation Classes (MFC). Эта глава посвящена MFC как примеру того, что могут дать библиотеки карка- каркасов приложений. ПРИМЕЧАНИЕ Библиотека Microsoft Foundation CSasses является большой, мощной и сложной конструкцией. Ни в одном исчерпыва- исчерпывающем учебном пособии нельзя полностью представить ее функциональность, не говоря уже об отдельной главе. Цель этой главы заключается не в том, чтобы научить вас использовать 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) (многодокументный интерфейс) либо просто нуждаетесь в диалоговом интерфейсе? РИСУНОК 5.1. Application Wizard. РИСУНОК 5.2. Выбор типа интерфейса. В последующих окнах мастера приложений можно выбирать среди различных опций: поддержку базы данных, поддержку СОМ, поддержку доступа к Internet и т.п. После запуска Application Wizard создается несколько классов, которые составляют начальный каркас приложения (рис. 5.3).
Использование каркасов пршюжений Обратите внимание на то, что каждый класс содержит несколько стандартных методов. Часто эти методы предлагают просто актуальный программный код и затем указывают, куда добавить пользовательский код, как показано в этом простом примере вывода из Application Wizard: BOOL CUnleashedDoc::OnNewDocument() if (!CDocument::OnNewDocument()) return FALSE; // TODO: здесь добавить программный код повторной // инициализации // (SDI-документы будут повторно использовать данный // документ) return TRUE; Иногда программный код, предлагаемый мастером, расширен. Он может сэкономить много времени при разработке и упростить взаимо- взаимодействие с более сложными элементами программирования Windows, такими как настройка СОМ для межпроцессного взаимодействия. В листинге 5.1 представлен метод Initlnstance, сгенерированный мастером. Листинг 5.1. Код, сгенерированный мастером. BOOL CUnleashedApp::Initlnstance() AfxEnableControlContainer(); // Стандартная инициализация // Если вы не используете эти возможности и хотите // уменьшить размер окончательного исполняемого модуля, то // удалите из следующего текста подпрограммы специальной // инициализации, которые не нужны. #ifdef _AFXDLL Enable3dControls(); // Вызывайте данный модуль, // когда используете MFC в // совместно используемой // библиотеке DLL // Вызывайте данный модуль, // когда связываетесь с MFC // статически #else Enable3dControlsStatic(); #endif // Измените ключ реестра под которым запомнены наши // параметры. // Вы должны изменить данную строку во что-то подходящее, // например, имя своей компании или организации. SetRegistryKey(_T("Local AppWizard-Generated Applications")); Глава 5 ^Unleashed classes ] »CAboutDIg - §CAboutDlg() V$ DoDataExchange(CDataExhange*p ^AssertValidQ § CChildFrame § "CChildFrame § Dump(CDumpContext & dc) §, PreCreateWindow(CRBATESTRUCT rrj. -*T? CMainFrame §AsserrValid() \-- $ CMainFrame ^ "CMainFrame _ i Dump(CDumpContext & dc) г - -9$ OnCreate(LPCREATESTRUCT ipCrea $ PreCreateWndowiCREATESTRUCT y§ mjmdStatusBar $§ m_wndToolBar p}- -*"d* CUnleashedApp $ CUnleashedApp § Initlnstancef) ^OnAppAboutO 1 CUnleashedDoc - §AssertValid() -$$CUnleashedDoc() - $~CUnleashedDoc() - § DumpfCDumpContext & dc) - § OnNewOocumentO - § SerializefCArchive & ar) ' CUnleashedView - $AssertValid() -9$CUnleashedView() - $~CUnleashedViewO - ^ Dump(CDumpContext & dc) - $GetDocument() \ - -$$ OnBeginPrinting(CDC*pDC, CPrintlnfo '-- ^OnDraw(CDC*pDC) --9$OnEndPrinting(CDC*pDC, CPrintlnfo - - V^ OnPreparePrinting(CPrintlnfo*plnfo) -- ^PreCreateWindow(CREATESTHUCT РИСУНОК 5.З. Классы, созданные мастером. LoadStdProfi.leSetti.ngs () ; // Загрузить опции стандартного файла INI (включая MRU) // Зарегистрируйте шаблоны документов приложения. Шаблоны документов / / служат как соединение между документами, окнами и представлениями. CMultiDocTemplate* pDocTemplate; pDocTemplate = new CMultiDocTemplate( IDR_HNLEASTYPE, RHNTIME_CLASS(CUnleashedDoc), RHNTIME_CLASS(CChildFrame), // пользовательский интерфейс MDI RHNTIME_CLASS(CUnleashedView)); AddDocTemplate(pDocTemplate); // создать главное MDI окно-рамку CMainFrame* pMainFrame = new CMainFrame; if (!pMainFrame->LoadFrame(IDR_MAINFRAME))
Вопросы реализации Часть II return FALSE; m_pMainWnd = pMainFrame; // Синтаксический разбор командной строки для стандартных команд оболочки, DDE, // открытия файла CCommandLinelnfо cmdlnfо; ParseCommandLine(cmdlnfo); // Диспетчерские команды, указанные в командной строке if (IProcessShellCommand(cmdlnfo)) return FALSE; // Главное окно инициализировано, поэтому покажите и обновите его. pMainFrame->ShowWindow(m_nCmdShow); pMai nFrarae-MJpdate Window () ; 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, очень просто и целенаправленно: программа пишется так, будто она обладает неоспоримым доступом к процессору, жес- жесткому диску, базе данных и т.д. Обо всем остальном должна позаботиться операционная система — вы просто идете вперед так, будто ваша программа выполняется в гордом одиночестве. Написание многопоточного приложения более сложно. На самом деле это гораздо более сложно, по- поскольку ваш код неожиданно может прерываться в любое время (что всегда и происходит). Еще важнее то, что более одного потока может войти в ваш программный код так, что один и тот же код может выпол- выполняться в двух разных потоках. Такое может произойти, если каждая функция в двух разных потоках вызы- вызывает ваш код. Многопоточные приложения могут быть обманчивыми. Если ваш код непотокобезопасен, то один поток может тщательно устанавливать значения переменных для базы данных, а другой может перекрыть те же самые значения, делая переменные бессмысленными. Это плохо и может оказаться одной из самых труд- трудноуловимых ошибок.
I pLU,lU3UHUU Часть II Для примера предположим, что есть функция, которая получает запись из базы данных, обновляет ее и записывает обратно. Ниже приведен предполагаемый программный код: int UpdaterO { getARecord() ; updateltO ; writeItBack() ; } Предположим, что есть функция fund в потоке 1 и другая функция, func2, в потоке 2 и обе вызывают Updater(). Выполняется funcl() и вызывает Updater(). UpdaterO начинает выполняться в контексте потока 1 и получает запись. Но прежде чем запись успевает обновиться, поток прерывается и запускается поток 2. Теперь func2() вызывает функцию UpdaterO, которая выполняется снова, на этот раз в контексте по- потока 2. Она получает ту же самую запись (случайно), обновляет ее, прежде чем прерывается. Затем возобновляется поток 1, и функция UpdaterO продолжает выполнение с того места, где она была прервана, с записью, которая была изначально считана из базы данных. Запись будет обновлена и записа- записана обратно. При этом будут искажены обновления, произведенные в потоке 2. Это один пример из многих, когда многопоточность может исказить данные, если применяемые мето- методы не являются потокобезопасными. Краткий совет по поводу многопоточности: если можно избежать написания многопоточного кода, то избегайте его. Не используйте многопоточность до тех пор, пока есть иной выбор. Многопоточность делает код сложным и гораздо более трудным для поддержки. Она существенно увеличивает вероятность ошибок и чаще всего снижает производительность. В конце концов, она занимает время на обработку контекста потока и переключение потоков. Добавьте достаточно потоков, и процессор будет тратить значительную часть времени, просто переключаясь между ними, и пропорционально этому меньше времени будет тра- тратиться на выполнение полезной работы! Предположим, что имеется программа, которая принимает много данных, высчитывает сумму и затем использует эту сумму во втором уравнении. "Ага! — скажете вы. — Две задачи! Я создам два потока". Про- Проблема в том, что второй поток не может начинаться до тех пор, пока не закончит работу первый. Даже если вы можете вычислять их в одно и то же время и если нельзя использовать данные до тех пор, пока оба не завершатся, то почему бы не выполнять их последовательно, а не параллельно? Выполнение задач вместе не даст ничего, а цена переключения между задачами может быть слишком высока. Итак, когда же использовать многопоточность? Классический сценарий — это когда одна задача зани- занимает много времени, а вторая задача не зависит от первой. Предположим, что необходимо сохранить дан- данные на жестком диске и также отобразить их на экране. Вам не нравится ждать, пока данные будут сохраняться на жестком диске и лишь потом появятся на экране, — запись на диск может продлиться долго. Почему бы не поместить задачу "запись на диск" в отдельный поток и не запустить более важную задачу взаимодействия с пользователем? Действительно, почему бы нет? Пример для изучения Автор написал приложение, которое "звонит" по телефону. Это напоминает те раздражающие телемар- телемаркетинговые звонки, которые звучат, когда вы сидите за ужином. Разница заключается в том, что эта про- программа "звонит" только тем, кто желает принимать звонки. Программа полностью автоматизирована и "не звонит" во время ужина. Компьютер может звонить 72 разным людям одновременно. После каждого звонка программа сохраняет результаты вызова и периодически обновляет базу данных полученными результатами. Одновременно она хранит кэш номеров для вызова, так чтобы никогда не простаивать. Данный проект связан с многопоточ- ностью. Конструкция проекта довольно сложна, но наша цель — прийти к следующему: автор запускает один поток для каждой линии вызова (CCaller) для 72 потоков, если все 72 линии активны. Автор также создал один поток для управления отчетом о функционировании (Creporter) и еще один поток для управления набором звонков, ожидающих освобождения линии (CLocalCallQueue). Наконец, есть главный поток, ко- который управляет всеми другими потоками и который владеет пользовательским интерфейсом (панелью управления наблюдением за звонками). Таким образом, приложение выполняет 75 потоков! Это большое число потоков, но при наличии мощного компьютера (Windows NT Workstation, выпол- выполняющейся на компьютере с процессором Pentium PC, и 128 Мб оперативной памяти) они работают дос-
Использование каркасов приложений Глава 5 таточно быстро, создавая иллюзию одновременности работы. Кроме того, приложение само не только многопоточное, но и многопроцессное, поскольку существуют дополнительные вызывающие устройства и дополнительные компоненты в других процессах (и на других устройствах), ответственные за планирова- планирование заданий и управление базой данных. Теперь рассмотрим проблемы, которые возникают в подобной среде. Создание потоков Первая задача состоит в создании потока. Фактически существует два разных способа создания и управ- управления потоками. Первый подходит для использования с элементами (окнами), которые должны выпол- выполняться как поток. Представьте объект, который содержит методы, но оперирует ими независимо от всех других потоков. Это первый тип потока, и он порождается от класса CWinThread. Второй тип потока более подходит для использования в том случае, когда необходима функция, вы- выполняющаяся в собственном потоке. На такой тип потока ссылаются как на рабочий. Использование объектов CWinThread Объект CWinThread создается точно так же, как любой другой объект: вы порождаете его от самого CWinThread или от какого-то класса, который порожден от CWinThread. Когда реализуется CWinThread- порожденный объект, то поток не создается. Для создания потока необходимо вызвать метод CreateThread() данного объекта. Создание объекта CWinThread вполне целенаправленно. Объявляется класс, порожденный (прямо или косвенно) от CWinThread, и реализуется объект данного типа в куче. Затем вызывается CreateThreadQ данного объекта и с помощью метода обеспечивается возврат значения 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, lParam); wParam и lParam являются устаревшим наследием, оставшимся от 16-разрядного программирования. В 16-разрядном Windows lParam является 32-разрядным, но wParam — только 16-разрядным (w — это обо- обозначение 16 разрядов в 16-разрядной операционной системе). Сегодня Windows — это настоящая 32-раз- 32-разрядная система, и оба параметра являются 32-разрядными (как и int). Первый параметр является либо системно-определенной, либо определенной пользователем констан- константой. Отображение между идентификатором данного сообщения и вызываемым методом обрабатывается функцией OnThreadMessage(), которая, в свою очередь, определяется в маршруте сообщения для объекта CWinThread. В многопоточном приложении, обсуждавшемся ранее, каждый вызывающий поток порожден от CWinThread:
Вопросы реализации Часть II class CCaller : public CWinThread { public: bool CallCompleted(bool isConnected = true) bool RestoreRoute () ; int void // время жизни virtual BOOL virtual BOOL void void Call HangUp Exitlnstance Initlnstance MakeCalls ReceiveCalls 0; 0; 0 0; 0; protected: DECLARE_MESSAGE_MAP () DECLARE_DISPATCH_MAP() DECLARE_INTERFACE_MAP () IVoice* injpVoicel ; private: //... Здесь пропущена большая часть объявления данного класса, с тем чтобы показать только наиболее от- относящиеся к делу методы. Макрос DECLARE_MESSAGE_MAP устанавливает маршрут для диспетчериза- диспетчеризации сообщения. В файле реализации мы используем еще несколько макросов для привязки сообщений к методам: BEGIH_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 = MM_0SER + 1; const int MM_RECEIVE_CALLS = WM_0SER + 2; Детали макросов отображения сообщений выходят за рамки настоящего обсуждения, но вы видите, что создать объект CWinThread, настроить его в собственном потоке и затем использовать его методы прямо (из его собственного потока) или косвенно (посылая ему сообщения) достаточно просто. Теперь другие объекты, имеющие указатель на ваш объект, могут посылать ему сообщения и получать эти сообщения обработанными в фоновом режиме. Управление окнами Объект CWinThread, кроме всего прочего, предназначен и для управления окнами (и другими произ- производными элементами), которые требуют собственных потоков. Связывание окна с объектом CWinThread достигается просто: заставьте initlnstance() создать окно (или элемент) и сохраните указатель на него. За- Затем объект CWinThread становится оберткой для окна.
Использование каркасов приложений Глава 5 Для того чтобы поддержать процесс, MFC предоставляет возможность присваивать окно переменной- члену mjpMainWnd объекта CWinThread. Это приводит к тому, что код диспетчеризации сообщения в CWinThread управляет исключительно данным окном. Обратите внимание, что теперь имеется три объекта, за которыми нужно следить: ¦ Объект CWinThread ¦ Сам поток ¦ Окно, которым управляет CWinThread Помните, что при создании объекта CWinThread поток не создается. Поток создается путем обращения к CreateThread(). Подобно этому, когда поток разрушается, то объект CWinThread продолжает жить. Такое положение вещей можно изменить, установив переменную-член mJbAutoDelete в значение true (по умолча- умолчанию — false). Такая установка указывает Windows разрушить объект CWinThread при разрушении его потока. Альтернатива: рабочие потоки Если вы просто хотите, чтобы метод одного из классов работал в фоновом режиме, т.е. в собственном потоке, то можете создать рабочий поток. Для этого используйте функцию AfxBeginThreadQ, которая на самом деле перегружена так, чтобы ее можно было использовать для этой цели. Кроме того, ее можно использовать для управления потоками с классами, порожденными от CWinThread. В этом случае нас ин- интересует только первая перегрузка AfxBeginThread(): CWinThread * AfxBeginThread() ( AFXJTHREADPROC pSomeThreadFunction, LPVOID pParamter, int nPriority ¦ THREAD_PRIORITY_NORMAL, UINT nStackSize = 0, DWORD dwCreateFlags = 0, LPSECURITY_ATTRIBUTES lpSecurityAttributee = NOLL ) Поскольку наибольшее значение имеют первые два параметра, то они будут рассматриваться в после- последнюю очередь. 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 и передайте всю структуру в управляющую функцию, используя AfxBeginThreadQ.
Вопросы реализации Часть II В некоторых книгах советуют сделать управляющую функцию глобальной. Автор не согласен с этим. Вы достигнете лучшей инкапсуляции (и тем самым более просто поддерживаемого кода), если сделаете дан- данную функцию статической функцией-членом конкретного класса. Обратите внимание, что в статической функции-члене нет указателя this, поэтому необходимо переда- передавать указатель this как параметр. Например, можно объявить такой метод-член: class myClass < public: static OINT myThreadFunc(LPVOID pMe); // ... }; И написать реализацию, подобную следующей: 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. Та- Таким образом Critical Section синхронизирует доступ к указанной области программного кода. Ниже показа- показано, как можно использовать критический раздел для изоляции кода, который изменяет значение: m_CS.Lock() ; s.Format("Current value: %d" , m_Value); m_Value++; m_CS. Unlock () ; Объект имеет две переменные: m_Value (целую) и in_CS (CriticalSection). Критический раздел блокиру- блокируется, значение возрастает, и критический раздел разблокируется. Мютексы Для многих целей критические разделы представляют собой именно то, что надо. Они быстры, легко- легковесны и просты в применении. Но, к сожалению, они ограничены. Самое существенное ограничение со- состоит в том, что они видимы только одному процессу, и в том, что не могут ждать указанное время до разблокирования. Более интеллектуальным синхронизирующим устройством является мютекс. Слово mutex происходит от слов mutuaHy exclusive (взаимно исключающий) с намеком на то, что если два (или более) объекта раз- разделяют мютекс, то они обладают взаимно исключающим доступом к тому, чем управляет мютекс.
Использование каркасов приложений Глава 5 Мютекс очень похож на критический раздел: либо вы владеете им (и можете получить доступ к области защищаемого им программного кода), либо вы им не владеете (и должны ждать). Однако, в отличие от критического раздела, мютекс может совместно использоваться несколькими процессами. Программный код также может указывать, пока мютекс станет доступным, прежде чем наступит "тайм-аут". Вот как можно использовать мютекс для управления доступом к коду, который меняет значение: CSingleLock theLock ( Sm_myMutex ); if ( theLoek.Lock(WAIT_TIHE) ) < s.Format("Current value: %d", m_Value); m_Value++; theLock.Unlock() ; } else { A?xMessageBox("Lock on Mutex failed. No increment!"); > break; Мютекс работает так. Вы начинаете с инициализации объекта Lock. Если вы ожидаете только один объект (например, мютекс), то можно использовать CSingleLock. В ожидании доступности более чем одного объек- объекта используется CMuMLock. Когда вы готовы получить доступ к защищаемому коду, то блокируете объект Lock и передаете, как долго собираетесь ждать, указывая время в миллисекундах. Значение, принятое по умолчанию, INFINITE, означает, что вы будете ждать до скончания века. События Объекты событий полезны в ожидании какого-либо события. Объекты событий не сигналят до тех пор, пока какой-то другой объект не заставит их делать это. Потоки могут ждать, когда событие засигналит, предоставляя вам возможность сказать потоку: "Начинай выполняться, когда произойдет данное событие". Обычно событие создается в несигналящем состоянии и затем указывается одному или нескольким потокам ждать, когда данное событие засигналит. Вы можете установить такое событие для уведомления потоков о завершении печати, о разгрузке файла, о сообщении в очереди и т.п. События имеют переменную-член bManualReset, которая определяет, что произойдет, когда будет выз- вызвана функция SetEvent() для объекта Event. Если bManualReset имеет значение false, то все ожидающие потоки при вызове SetEvent() будут освобождены. Если вы хотите, чтобы событие было сброшено (пере- (перестало сигналить), то должны вызвать функцию ResetEvent(). Если bManualReset имеет значение true, то освобождается только один ожидающий поток и Event еще раз автоматически устанавливается в несигналящее состояние. Для события (bManualReset установлено в true), очищаемого вручную, все ожидающие потоки можно освободить, вызвав PuIseEvent(). Такое действие также автоматически очистит событие. Следовательно, PulseEvent() позволяет обрабатывать вручную очищаемое событие, как если бы оно было автоочищаемым событием. Пример Программа в листинге 5.1 использует несколько синхронизирующих устройств, описанных в предыду- предыдущих разделах. Эта программа преднамеренно упрощена, чтобы сконцентрироваться на том, как взаимодейству- взаимодействуют объекты синхронизации. Это приложение MFC рисует простое диалоговое окно, показанное на рис. 5.4. При щелчке на кнопке Start Thread 1 порождается рабочий поток, который увеличивает и отображает значение, хранимое в диалоговом окне. (Обычно такие значения хранятся в документе, но в диалоговом приложении MFC нет объекта Document.) Затем данный поток ждет некоторое время (до одной секунды) и повторяется. Документы и представления детально рассматриваются далее в этой главе. При щелчке на кнопке Start Thread 2 в диалоговом окне порождается второй рабочий поток, который также увеличивает и отображает то же самое значение, что сохранено в диалоговом окне. Если позволить этим потокам какое-то время выполняться, то станет видно, что числа беспорядочно добавляются к спис- списку в окне, поскольку потоки "воюют" за один и тот же ресурс. При щелчке на кнопке Start Thread 3 в диалоговом окне создается новый CWinThread-производный поток. Затем диалоговое окно посылает этому новому объекту сообщение с указанием о начале работы, и поток также начинает увеличивать значение, хранящееся в диалоговом окне. Теперь все три потока увеличивают одно и то же значение (рис. 5.5). 53ак.53
Вопросы реализации Часть II . 1 Ml ¦Jit 1 Ml -J era r-i Л ft Si* ¦»¦¦ «Й nil or :*J.. « .¦ r *'!•»! aMVWa; Cunent value-63 Cunent vak*; 82 Cunenl vakje: 85 Current vafcje. 84 Cwent value. 86 Cunent value: 87 Cunent value 83 Current value: 88 Cunent value 31 Cunent value; 90 ТЭ ErxtSep17l9981i:53t44AM ТЭ - Duafion 5 *eeond* T3 Current value: 74 РИСУНОК 5.4. Простое диалоговое окно. РИСУНОК 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.7. Диалоговое окно мастера приложений.
Использование каркасов приложений Глава 5 Построение диалог-базированного приложения означает, что имеется более простое приложение, но отсутствует класс CDocument. Конструктор для CDocument предназначен инициализировать переменные состояния: CThreadsUnleashedDlg::CThreadsUnleashedDlg(CWnd* pParent /*=NULL*/) : m_Value(O) , m_Paused(false), m_MTC(O), m_numThreads @), m_AHRunningEvent (false, true,NULL,NULL) , CDialog (CThreadsUnleashedDlg: :IDD, pParent) { //{ {AFX_DATA_INIT (CThreadsUnleashedDlg) m_RadioButtonValue = 0; //) }AFX_DATA_INIT // Заметьте, что Loadlcon не требует в Win32 последующе* Destroylcon m_hlcon = AfxGetAppf)->LoadIcon(IDR_MAINFRAME) ; > В данном коде m_VaIue представляет значение, которое будет возрастать, m_Paused является флагом, указывающим, что пользователь щелкнул на кнопке Pause, m_MTC является указателем на объект MyThreadClass, который создается, когда пользователь щелкает на кнопке Start Thread 3. m_NumThreads хранит счетчик того, как много затребованных пользователем потоков выполняется. m_AllRunningEvent является объектом Event, который сигналит, когда выполняются все три затребованных пользователем потока. ПРИМЕЧАНИЕ Часть программного кода выделена полужирным шрифтом, чтобы привлечь внимание к особенно интересным аспек- аспектам или показать, что добавлено или изменилось. Оставшаяся часть метода создана Application Wizard. Автор создает новый поток в OnInitDialog(), как показано ниже: BOOL CThreadsUnleashedDlg::OnInitDialog() { CDialog::OnInitDialog(); // ... неважные детали опущены . . . AfxBeginThread (ThreadsNotification, (LPVOID) this) ; return TRUE; // возвратить значение TRUE, если только фокус ие установлен иа элемент // управления } Вызов в AfxBeginThread(ThreadsNotification, (LPVOID)this) порождает рабочий поток, который реализу- реализуется в методе ThreadsNotification(). Остаток кода Application Wizard опущен, поскольку очевиден. Метод ThreadsNotification() выглядит следующим образом: UINT CThreadsUnleashedDlg::ThreadsNotification(LPVOID pParam) { CThreadsUnleashedDlg * pThis = (CThreadsUnleashedDlg*) pParam; WaitForSingleObject (pThis->m_AllRunningEvent, INFINITE) ; AfxMessageBox("All three threads are now running"); return 0; } С помощью этого очень простого метода распаковывается параметр и преобразуется в указатель на объект CThreadsUnleasedDlg, который вызывает данный метод. Такой подход необходим, поскольку функция ра- рабочего потока должна быть статической. Она не может иметь указатель this. Затем мы используем указатель this для доступа к объекту CEvent, передаваемого WaitForSingleObject(), и указываем коду ждать вечно. Такая инструкция блокирует поток и освобождает его кванты времени. Если событие засигналит, данный поток освободится и отобразит окно сообщения. Последняя строка return 0 требует от функции возвратить управ- управление и покинуть поток.
Вопросы реализации Часть II Теперь диалоговое окно отображается и "засыпает", пока не будет выполнен щелчок на кнопке. Когда пользователь щелкает на кнопке Start Thread 1, диалоговому окну посылается сообщение, которое пере- перехватывается в карте сообщения: ON_BN_CLICKED(IDC_BUTTOH_START_T1, OnButtonStartTl) Здесь вызывается метод OnButtonStartTl(): void CThreadsUnleashedDlg::OnButtonStartTl() { CButton * pB = (CButton*)GetDlgItem(IDC_BUTTOH_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 I running. В кон- конце вызывается CountThreads(). Вскоре мы исследуем, что происходит в теле нового потока. Но прежде рассмотрим CountThreads(): void CThreadsUnleashedDlg::CountThreads() i if ( Interlockedlncrement(&m_numThreads) >= 3) m_AHRunningEvent. SetEvent () ; } Цель данного метода состоит в увеличении счетчика выполняющихся потоков. Когда счетчик достига- достигает 3, то устанавливается событие AllRunningEvent, так что поток уведомления может быть освобожден. По- Поскольку мы хотим убедиться, что переменная mnumThreads находится под синхронизирующим управлением, то можем воспользоваться преимуществом метода Interlockedlncrement(), который принимает указатель на длинное целое D байта) и увеличивает его под управлением потока. ПРИМЕЧАНИЕ Под управлением Windows NT данный подход надежно возвращает вновь увеличенное значение. Однако под управле- * нием Windows 95 этого не происходит. Таким образом, представленный программный код специфичен для Windows NT. Если вы должны выполнять данный код в Windows 95, то увеличьте значение и протестируйте код отдельно. Рабочий поток выполняет простую работу по увеличению члена m_Va!ue диалогового окна: UINT CThreadsUnleashedDlg::ThreadFunction(LPVOID pParam) { CListBox * pLB = ((CListBox*)(pThis-X3etDlgItem(IDC_LIST_OUTPUT))); CString s; int offset; srand( (unsigned)time( NULL ) ); for ( ;; ) ( if ( pThis->m_Paused ) < SleepE00); 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 sMOTEX: // Мыотекс CSingleLock theLock ( &(pThis->m_myMutex) ) ; if ( theLock.Lock(WAITJTIME) ) { 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->SetTopIndex( offset ) ; ) return 0; } Первая работа данного раздела кода заключается в приведении указателя this: CThreadsUnleashedDlg * pThis = (CThreadsUnleashedDlg*) pParam; Поскольку поток реализован в статической функции-члене, то вы должны передать копию указателя this, который должен использоваться (явно) для доступа к членам. Однако поскольку это метод-член, то он имеет доступ к приватным переменным-членам класса. Мы немедленно входим в бесконечный цикл, берем значение и увеличиваем его до тех пор, пока поток не разрушится. Каждый раз в цикле проверяем значение переменной-члена in_Paused — должны ли мы выдержать паузу, чтобы пользователь рассмотрел, что происходит: if ( pThis->m_Paused ) < SleepE00); continue; } Теперь рассмотрим кнопки-переключатели. Обратите внимание, что мы исследуем не элемент управле- управления, а переменную-член mRadioButtonValue: 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(IDCJRADIO_MUTEX, OnRadioMutex) ON_BH~CLICKED(IDC~RADIO_CRITICAL_SECTION, OnRadioCriticalSection) Например, когда пользователь щелкает на кнопке Mutex (IDC_RADIO_MUTEX), вызывается метод OnRadioMutex(), как показано ниже: void CThreadsUnleaahedDlg:: OnRadioMutex () { UpdateData() ; CStatic * pStatic = ( (CStatic *) (GetDlgItem(IDC_STATIC_STATUS))); pStatic->SetWindowText{"Using Mutex"); } Первое, что делает данный метод, — обновляет связанную переменную (m_RadioButtonNone) и печата- печатает сообщение состояния на строке состояния. Вернемся к ThreadFunction(). После того как мы определили, что не выдерживается пауза, переключа- переключаемся на значение m_RadioButtonNone, которое, как вы теперь знаете, соответствует переключателю, выб- выбранному пользователем. Для проверки оператора switch используем перечисляемую константу: •num 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 CThreadsOnleashedDlg::OnButtonStartT3() { CButton * рВ = (CButton*)GetDlgItem(IDC_BUTTON_START_T3) ; pB->EnableWindow (false) ; if ( ! m_MTC ) { m_MTC - new CMyThreadClass(this); BOOL bRc = m MTC->CreateThread() ; if ( ! bRc )~ { 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 Создание потока представляет собой двухэтапный процесс. На первом этапе реализуется объект потока: m_MTC = new CMyThreadClass(this); На втором этапе создается сам поток: BOOL bRc = m_MTC->CreateThread(); Обратите внимание на разницу. Когда мы создаем экземпляр, используя оператор new, то создаем объект, но не создаем поток Windows. Поток создается путем обращения к CreateThread(). Мы говорим об объекте и о потоке так, будто это одно и то же, но это разные вещи. После того как поток запущен, мы хотим, чтобы он начал работать, но не хотим вызывать его метод StartWork() прямо. Если так сделать, то поток зависнет вплоть до возврата методом управления (что ни- никогда не произойдет). Вместо этого мы хотим асинхронно послать сообщение данному потоку и указать ему начать работу. Такое указание посылается методом PostThreadMessage(), который возвращается немедленно: m_MTC->PostThreadMessage( MM_START_WORK, 0, 0 ); Этот оператор посылает потоку определенное пользователем сообщение со значением WM_START_WORK. Данное значение также определяется в файле constants.h, как показано ниже: const int MM_START_WORK = WM_USER + 1; Когда поток обнаруживает это сообщение, то отображает его на метод-член класса диспетчерской кар- картой: BEGIN_MESSAGE_MAP(CMyThreadClass, CWinThread) //{{ AFX_MSG_MAP(CMyThreadClass) // Примечание — будет здесь добавлять и удалять отображение макросов. //}} AFX_MSG_MAP ON_THREAD_MESSAGE (MM_START_WORK, OnStartWork END_MESSAGE_MAP () Обратите внимание, что автор добавил свою строку ON_THREAD_MESSAGE после защищенного кода AFX_MSG_MAP, но перед макросом END_MESSAGE_MAP(). Все, что находится между этими двумя стро- строками, может быть переписано мастерами, но код, который добавил автор, находится в безопасности: // {{ AFX_MSG_MAP(CMyThreadClass) AFXMSGMAP Это отображение просто диктует, чтобы при приеме сообщения WM_START_WORK вызывался метод OnStartWork(): void CMyThreadClass::OnStartWork() { CStatic * pStatic = ( (CStatic *) (m_pDlg->GetDlgItem(IDC_STATIC_STATUS))); pStatic->SetWindowText("Thread 3 running"); CListBox * pLB = ((CListBox*)(m_pDlg->GetDlgItem(IDC_LIST_OUTPUT)));
Вопросы реализации Часть II srand( (unsigned)time( NULL ) ); CString s; CString status; for ( ;; ) if ( m_pDlg->IsPaused() ) SleepE00) ; continue; int val; switch ( m_pDlg->m_RadioButtonValue ) case sNONE: // Нет val = m_pDlg-X3etValue() ; s.Format("T3 Current value: %d",val); Sleep (rand() % 1000); m_pDlg->SetValue(++val); break; case sCRITICAL_SECTION: // критический раздел m_pDlg->m_CS.Lock(); // ожидание невозможно val = m_pDlg-X3etValue() ; 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(HAIT_TIHE) ) val = m_pDlg-X3etValue() ; s.Format("T3 Current value: %d",val); Sleep(rand() % 1000); m_pDlg->SetValue(++val); theLock.Unlock() ; else AfxMessageBox("Lock on Mutex failed. No increment!") break; int offset = pLB->AddString(s) ; pLB->SetTopIndex( offset ) ; Неудивительно, что данный метод очень похож на ThreadFunction(), который мы только что исследова- исследовали. Значительная разница заключается в том, что это не статический метод класса диалогов, а простой метод-член класса потоков. Класс потоков может поддерживать свое состояние и отвечать на сообщения потоков. Для того чтобы гарантировать правильную синхронизацию, этот поток должен использовать тот же мютекс, который использует поток класса диалогов. Помните, что CMutex можно именовать. Для этого мы инициализируем мютекс в конструкторе диалога: CThreadsUnleashedDlg::CThreadsUnleashedDlg(CWnd* pParent /*=NULL*/) : m_value@), m_Paused(false), m_MTC@),
Использование каркасов приложений Глава 5 m_numThreads@) , m_AllRunningEvent(false,true,NULL,NULL), m_myMutex(false,"ml"), CDialog(CThreadsUnleashedDlg::IDD, pParent) Затем мы создаем другой мютекс как член потока: class CMyThreadClass : public CHinThread private: CThreadsUnleashedDlg * m_pDig; CMutex m_Mutex; Затем инициализируем данный мютекс тем же самым именем: CMyThreadClass::CMyThreadClass(CThreadsUnleashedDlg * pDlg): mj?Dlg (pDlg) , m_Mutex(false,"ml") Сейчас можно изменить программный код в потоке для ссылки на собственный мютекс, который яв- является ссылкой на объект-мютекс, хранимый диалогом: CSingleLoek theLock ( Sm_Mutex ); if ( theLock.Lock(WAITJTIME) ) { II... Служебные классы Библиотека MFC предоставляет набор служебных классов, часть из которых мы уже видели в действии. Служебные классы добавлены компанией Microsoft к базовым классам для того, чтобы упростить програм- программирование, — они освобождают вас от написания части кода. Наиболее важными и полезными из служебных классов являются CString (для манипулирования сим- символьными строками) и CTime (для манипулирования датами, временем и промежутками времени). Классы манипулирования строками Возможно, тем служебным классом, который используется наиболее часто, является CString. Этот про- простой строковый класс помогает использовать текстовые строки: создание, форматирование, копирование, передачу в качестве параметров и т.п. Конструктор CString перегружен так, чтобы принимать параметры (конструктор по умолчанию) либо существующий объект CString (конструктор копии), либо несколько альтернатив, возможно, самыми по- полезными из которых являются строки С-стиля и NULL-определенные строки. После того как объект CString создан, его можно объединять с другими CString. Следующий код ини- инициализирует s3 строкой "Hello world": CString CString CString el("Hello"); s2 (" world") s3 = sl+s2; Класс CString можно использовать для осмысленных манипуляций строками, например, поиском под- подстрок. Одним из самых мощных методов является Format(), который принимает строку, подобную той, что принимает printf(), и который использует те же спецификации, что и printf(). Вы увидите CString.Format() в действии в этой и других главах книги. Классы времени Другой популярный набор служебных классов MFC помогает манипулировать временем. Стандарт ANSI обеспечивает тип данных time_t, который MFC оборачивает в класс CTime. Данный класс представляет абсолютное время и дату в диапазоне между 1 января 1970 года и 18 января 2038 года. Класс CTime вклю-
Вопросы реализации feii Часть II чает служебные функции для преобразования дат Грегорианского календаря. Дополнительные методы по- помогают вычитать год, месяц, день, часы, минуты из заданного значения времени. Класс CTimeSpan обеспечивает относительные значения времени, т.е. диапазоны времени. Когда один объект CTime вычитается из другого, результатом будет объект CTimeSpan. Вы можете добавлять и вычи- вычитать объекты CTimeSpan из объектов CTime для создания новых объектов CTimeSpan. Таким образом мож- можно манипулировать диапазоном дат в 68 лет. Этот класс можно использовать для измерения интервалов между двумя событиями. Изменим класс CMyThreadCIass, показанный ранее, так, чтобы тогда, когда он не находился под синхронизирующим управлением, он приостанавливался на случайные интервалы времени — от 1 до 5 тыс. миллисекунд E секунд). Мы можем измерять длительность (в секундах), учитывая текущее время начала работы и время конца работы и измеряя разницу между ними: CTime start, end; CTimeSpan len; start = CTime::GetCurrentTime(); rightNow = start.Format("T3 Start: %b %d %Y %I:%M%p"); offset = pLB->AddString(rightNow); pLB->SetTopIndex( 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->SetTopIndex( offset ) ; len = end - start; duration.Format("T3 - Duration %d seconds",len.GetSeconds()); offset = pLB->AddString(duration); pLB->SetTopIndex( 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 является базовым для всех представлений, включая диалоговые окна, класс CFraineWnd управляет рамкой окна и класс CMDIChildWnd управляет многодокументным оконным интерфейсом. Класс CWnd также является базовым классом для CView, а равно и для всех элементов управления, таких как CButton, Clistbox и т.д. Классы CDocument и CView инкапсулируют шаблон конструктора документ/представление и работают с CWnd-производными классами над отображением содержимого CDocument. Класс CView можно расши- расширить многими способами, и MFC обеспечивает вас семейством порожденных классов, включающих CScrollView (для прокрутки больших документов), CEditView (для построения текстовых документов), CRichEditView (для создания документов с расширенным текстом с форматированием полужирным, кур- курсивным и другими шрифтами). CFormView (для создания экранных форм), CRecordView (для работы с база- базами данных), CTreeView (для создания структурированных представлений иерархических данных). CView является родительским классом всех перечисленных порожденных представлений и обеспечивает несколько виртуальных функций для перекрытия производными классами. Вероятно, наиболее важные функции CView — это OnInitialUpdate(), OnUpdateQ и OnDraw(). OnInitialUpdate() вызывается, когда представление первый раз обновляет себя. Каждый раз, когда до- документ изменяется, он уведомляет все текущие прикрепленные (вложенные) документы. MFC вызывает OnInitialUpdate() первый раз и OnUpdateQ все последующие разы. Поведение, предусмотренное по умолчанию, для методов обновления заключается в лишении окна законной силы и вызове OnDrawQ. Такой подход обеспечивает гибкость в том, как визуализировать пред- представление, но, кроме того, предоставляет актуальный программный код, который можно использовать для реализации функций рисования по умолчанию. Представление форм Одним очень распространенным примером использования MFC является создание сложных форм для сбора информации от пользователя. Существует два общих метода для реализации подобных приложений: как простое диалоговое окно и с новым классом CFormView, который объединяет архитектуру документ/ представление с простотой использования диалогового окна. Здесь мы исследуем представление форм до- достаточно глубоко для того, чтобы рассмотреть общий подход построения подобных приложений с исполь- использованием фактически любой библиотеки каркасов приложений. Пример программы, которую мы будем реализовывать, выглядит так, как показано на рис. 5.8. Это приложение представляет форму для использования в кредитном агентстве. Оно собирает информа- информацию от пользователя с помощью нескольких элементов управления. Среди них мы исследуем поля для редактируемых данных (например, Last Name), переключатели (например, Spending), поля со списком (например, Salary Range), ползунок (например, Credit Limit) и счетчик (например, Member Since). Кроме того, исследуем вкладки свойств, которые вызываются здесь с помощью кнопки Profile, но которые в приложениях часто применяются как устройства для сбора пользовательских настроек. В качестве примера вкладок свойств на рис. 5.9 показана страница свойств текстового редактора Microsoft Word.
Вопросы реализации Часть II РИСУНОК 5.8. Пример приложения с представлением формы. 1 иигМошйп ] ГопраШ* J H. ^ ч| e* I net I s». | Л" *•№*»* Га~. % 'Л Л1. ^li- U РИСУНОК 5.9. Вкладки свойств Microsoft Word. Как видно, вкладка свойств является способом управления последовательностью диалог-базированных форм, предоставляющим пользователю свободу перемещения между вкладками по мере необходимости Та вкладка свойств, которую мы реализуем в примере, выглядит так, как та, которая изображена на рис 5 10 Она несколько проще, чем вкладка свойств Microsoft Word, но предоставляет возможность в деталях ис- исследовать, как реализуются вкладки свойств. Приступая к изучению представлений форм Для начала запустим новый проект SDI и вызовем мастер обычным способом. Как раз перед подтвер- подтверждением имен файлов выделим представление (CControlsUnleashedView) и изменим его тип (базовый класс) с CView на CFormView, как показано на рис. 5.11. •SB "¦''-¦ tax t i*>ll ¦* J *2 4i (*'¦' 1 tfk da ¦r 'I г e 1 1 _.^L 1 J РИСУНОК 5.10. Пример вкладки свойств. РИСУНОК 5.11. Изменение представления на Form view. После того как приложение будет построено, откроем редактор ресурсов и приступим к построению начальной формы. Каждому элементу управления присваивается имя, и вызывается мастер Class Wizard для присваивания переменных, соответствующих значению каждого элемента управления, как показано на рис, з.1^_. Для некоторых элементов управления, таких как IDC_COMBOSalaryRange, создаются две переменные- одна содержит значение (m_SalaryRangeValue), а другая предоставляет сам элемент управления (m_ComboSalaryRange) Имея переменную, которая содержит сам элемент управления, можно написать такой простой оператор: m_ComboSalaryRange.GetCount();
Использование каркасов приложений Глава 5 Если бы такой переменной не было, то пришлось бы писать: ССошЬоВох * рВох = (CComboBox *) GetDlgItem(IDC_COMBOSalaryRange); pBox->GetCount() ; Когда вы ожидаете получения доступа к самому элементу управления, то использование сгенерирован- сгенерированной мастером Class Wizard переменной проще и легче поддерживается. После того как все элементы управления формы получили свои значения, Class Wizard генерирует код, гарантирующий, что эти переменные будут обновлять- обновляться вместе со значениями в элементах управления. Ис- Исследуем, как работает такой механизм. При этом обратите внимание на то, что эти переменные являются членами CFormView, а не документа. Если вы хотите обеспечить, чтобы данные были помещены в централизованное хра- хранилище (а именно это и требуется, когда данные бу- будет отображать более чем одно представление), то необходимо создать соответствующие переменные в до- документе и управлять передачей данных между представ- представлением и документом вручную. РИСУНОК 5.12. Присваивание переменных каждому элементу управления. Управление данными Для приема данных в переменные и последующего извлечения их из переменных вызывается UpdateData(). Если вы передаете параметр false, то инициализируете элементы управления данными в представлении. Если вы не передаете параметр, то значение true, принятое по умолчанию, указывает, что необходимо обновить переменные содержимым элементов управления. Метод UpdateData() в конце концов вызовет DoDataExchangeO (не вызывайте данный метод прямо!), ко- который был создан Class Wizard (хотя можно при желании создать его самостоятельно). Метод DoDataExchangeO выглядит следующим образом: void CControlsUnleashedView::DoDataExchange(CDataExchange* pDX) CFormView::DoDataExchange(pDX); //{{AFX_DATA_MAP(CControlsUnleashedView) DDX_Control(pDX, IDC_COMBOSalaryRange, m_ComboSalaryRange); IDC~SPIN_MEMBER_SINCE, m_MemberSinceControl) IDC_STATICVISAPic, m_VisaPic); IDC_STATICMCPic, m_MasterCardPic); IDC_SLIDERCredit, m_CreditSlider); IDC_RADIOVisa, m_VisaButton); IDC~BUTTONProfile, m_ButtonProfile) ; IDC~EDITCreditLimit, m_CreditLimitControl); IDC~COMBOOptions, m_CCOptions); IDC~CHECKVisaClub, m_CheckVisaClub); IDC_COMBOOptions, m_CCOptionValue); IDC_COMBOState, m_State); _EDITAddress, m_Address); IDC_EDITCity, m_City); IDC_EDITFirstName, m_FirstName); IDC_EDITLastName, m_LastName); IDC_RADIONoProfile, m_RadioProfile) ; IDC_RADIOVisa, m_VisaButtonValue); IDC_Zip, m_Zip); IDC_CHECKAdv, m_CheckAdWalue); IDC_CHECKCheckWriting, m_CheckWritingValue); IDC_EDIT_MEMBER_SINCE, m_EditSince); DDV_MinMaxInt(pDx7 m_EditSince, 1990, 2005); DDX_Text(pDX, IDC_EDITCreditLimit, m_EditCreditLimitValue); DDX_CBIndex(pDX, IDC_COMBOSalaryRange, m SalaryRangeValue); //}}AFX DATA MAP ~ DDX_Control(pDX, DDX_Control (pDX, DDX_ContrOl(pDX, DDX_Control(pDX, DDX_Control(pDX, DDX~Control (pDX, DDX_Control (pDX, DDX_Control{pDX, DDX_Control(pDX, DDX_CBIndex(pDX, DDX_CBString(pDX, DDX_Text(pDX, IDC DDX_Text(pDX, DDX_Text(pDX, DDX~Text(pDX, DDX_RadiO(pDX, DDX_Radio(pDX, DDX_Text(pDX, DDX_Check(pDX, DDX_Check(pDX, DDX_Text(pDX,
Вопросы реализации Часть II Каждый из макросов DDX_ гарантирует, что данные будут взяты из элементов управления и вставлены в пе- переменные в правильном формате (например, преобразо- преобразованы из строк в целые там, где это необходимо). Кроме того, макросы предоставляют возможность автоматической проверки диапазона. Например, переменная m_EditSince ус- устанавливается для проверки целого значения между 1990 и 2005. Когда вы исследуете данную переменную в Class Wizard, то получите возможность установить указанные ограничения (рис. 5.13). Если пользователь делает попытку покинуть диалого- диалоговое окно, а выбранное значение не находится в заданных пределах, то выдается информация об ошибке и проис- происходит возврат в соответствующее поле для выполнения исправления (рис. 5.14). Проверка выполняется успешно тогда, когда пользо- пользователь щелкает на кнопке ОК, но автор модифицировал программный код так, чтобы обеспечить выпол- выполнение проверки после того, как пользователь покинет поле. Он сделал это, указав Class Wizard создать метод для сообщения EN_KILL_FOCUS, который вызывается, когда вы покидаете поле (рис. 5.15). IDCCHECXVusCU] IDC_COHB00pto« IDC COHBOOpHorn IDC_COMBGS«l«iflmge ЮС С0МВ05Ы»1«»О5 IDC COMBO5W» РИСУНОК 5.13. Мастер Class Wizard. 1 | HnataVarin 3 Г-- "~3". ч.".-„ РИСУНОК 5.14. Проверка значений. РИСУНОК 5.15. Проверка значений при покидании поля. В реализации данного метода автор просто вызывает метод UpdateDataO, который выполняет проверку данных: void CControlsUnleashedView::OnKillfocusEditMemberSince() UpdateDataO ; Программирование, управляемое событиями Конечно, наиболее часто приходится отвечать на сообщения о щелчках на переключателях, которые управляются в основном так же, как и сообщения, сгенерированные другими элементами управления. Например, если щелкнуть на переключателе Visa или Master Card, то будет изменяться отображаемый рисунок и флажки доступных возможностей. Мы создаем метод OnRADIOVisa(), который будет вызываться при нажатии кнопки: void CControlsUnleashedView::OnRADIOVisa() UpdateDataO ; if( m_VisaButtonValue=0 ) m_CCOptions.ResetContent(); m_CCOptions.AddString("Visa Card");
m_CCOptions.AddString("Visa Card Gold"); m__CCOptions.AddStr ing ("Visa Card Platinum") ; m__CCOptions . SetCurSel @) ; m_CheckVisaClub.EnableWindow(true); m_VisaPic.ShowWindow(SW_SHOW); nTMasterCardPic.ShowWindow(SW_HIDE); Использование каркасов приложений Глава 5 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@); m_CheckVisaClub.EnableWindow(false); m_VisaPic.ShowWindow(SW_HIDE); m_MasterCardPic.ShowWindow(SW_SHOW); Как видно, этот метод используется для проверки значения выбранного переключателя и затем соот- соответственно для установки содержимое поля со списком. Метод также скрывает рисунок Master Card и ото- отображает рисунок Visa (или наоборот). Метод OnRADIOMasterCardO вызывает OnRADIOVisa: void CControlsUnleashedView::OnRADIOMasterCardO { 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 CSliderCtrl\n"); else < int value=p_Slider->GetPos(); CString temp; temp.Format("%ld",value * 1000); (GetDlgItem(IDC_EDITCreditLimit))->SetWindowText(temp); CFormView::OnHScroll(nSBCode, nPos, pScrollBar);
Вопросы реализации Часть It Обратите внимание на то, что прежде всего необходимо гарантировать прием представлением уведом- уведомления о горизонтальной прокрутке от самого ползунка. Внутрь передается указатель на класс CScrollBar. Поскольку класс CSliderCtrl происходит от CScrollBar, можно использовать новый оператор ANSI dynamic_cast для гарантии, что у нас имеется действительный объект CSliderCtrl, до того, как реализовы- вать обновление. Наконец, мы связываем поле со списком Salary Range. Изменение информации о зарплате должно ус- установить в поле Credit Limit по умолчанию и это, в свою очередь, позволит установить положение ползунка: void CControlsUnleashedView::OnCloseupCOMBOSalaryRange() { UpdateDataO ; m_EditCreditLimitValue = m_ComboSalaryRange.GetItemData(m_SalaryRangeValue); m_CreditSlider.SetPos( m_EditCreditLimitValue / 1000 ); UpdateData(false); ~ I Этот программный код имеет небольшую хитрость. Мы устанавливаем m_EditCreditLimitValue в элемент данных, связанный с полем, в котором находится список. Windows не обеспечивает данной связи. Мы делаем это, инициализируя диалоговое окно: // связать начальное ограничение кредита с каждым диапазоном зарплаты for(int offset-0; offset<=m_ComboSalaryRange.GetCount(); offset++) { switch (offset) { case 0: crLimit=5000; break; case 1: crLimit=15000; break; case 2: crLimit=40000; break; case 3: crLimit=80000; break; case 4: crLimit=l00000; break; default: crLimit=0; > m_ComboSalaryRange.SetltemData(offset,crLimit); } Данный код связывает значение 5000 с первым входом в окне, значение 15000 — со вторым входом и т.д. Возвращаясь к OnCloseupCOMBOSalaryRange(), когда пользователь выбирает Salary Range, мы осуще- осуществляем выбор, используем его для извлечения связанного элемента данных и затем помещаем это значе- значение в поле для редактирования. Наконец, мы используем значение поля для установки ползунка. Пользовательский интерфейс становится динамичным и интегрированным: размер зарплаты устанавливает ограничение кредита по умолчанию. Переключатели Переключатели в группе распознаются Windows как связанная группа. Вы должны помочь Windows, гарантировав, что порядок табуляции правилен. Суть в том, что кнопки располагаются в последовательном порядке, и первая кнопка та, для которой установлен стиль Group. Кроме того, первый элемент управле- управления, не принадлежащий группе, должен иметь стиль Group, установленный так, чтобы дать Windows сиг- сигнал о том, что достигнут конец группы переключателей (рис 5.16).
Использование каркасов приложений Глава 5 ПРИМЕЧАНИЕ Порядок табуляции управляет последовательностью, в которой пользователь будет перемещаться от одного элемента управления к другому. Порядок табуляции устанавливается в редакторе ресурсов нажатием клавиш Ctri+D и затем щелчком на каждом элементе управления в том порядке, в котором они должны вызываться. Обратите внимание, что кнопки в группе Credit Card Type располагаются последовательно. У Visa уста- установлен атрибут Group и рисунок Visa (IDC_STATICMCPic) — следующий в порядке табуляции — имеет свой атрибут, который установлен так, чтобы сообщать об окончании группы переключателей. РИСУНОК 5.16. Установка стиля Group. U f j4m \ 'MM | t<rwJi1 ii рийосоиа—3 -^ г • :. «1 1 - ^' Счетчик MFC предоставляет возможность объединить поле для редактирования со счет- Member since: |1997{§§ РИСУНОК 5.17. Счетчик. чиком, как видно из Member Since на рис. 5.17. Если исследовать элементы управления в редакторе ресурсов, то выяснится, что они достаточно своеобразны (рис. 5.18). Установкой стиля Autobuddy вы связываете счетчик с полем сразу перед ним в последовательности переходов. (Еще одна причина проверить порядок переходов!) Если вы хотите, чтобы поле управлялось счетчиком (как показано на рис. 5.19), то следует также установить флажок Setbuddyinteger. РИСУНОК 5.18. Редактор ресурсов. РИСУНОК 5.19. Установка нужных флажков. Наконец, необходимо установить диапазон счетчика, что делается в методе OnInitialUpdate(): m_MemberSinceControl.SetRangeA990,2005); Вкладки свойств К этому моменту почти все элементы управления для примера приложения выбраны. Последний шаг заключается в создании вкладок свойств, которые появляются, когда пользователь щелкает на кнопке Profile. Вкладки свойств — это, по сути, связанные диалоговые окна, каждое из которых представляет tabSheet (рис. 5.20). Простая вклада свойств состоит из контейнера (класс СРгоШе : 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.21. Выбор шаблона. РИСУНОК 5.22. Добавление элементов управления во вкладку свойств. РИСУНОК 5.20. Вкладки свойств. Следующий шаг заключается в добавлении всех элементов управле- управления к каждой вкладки свойств, как показано на рис. 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(tworkPage); theProfile.AddPage(SincomePage); theProfile.AddPage(bcreditHi«toryPage); // здесь начальные значения диалога if ( theProf ile. DoModal () = IDOK ) m_SalaryRangeValue ¦ incomePag«.m_SalaryComboValue; m_EditCreditLimitValue = m_ComboSalaryRange.GetItemData(m_SalaryRangeValue); m_CreditSlider.SetPo«( m_BditCreditLimitValue / 1000 );
Использование каркасов приложений Глава 5 } UpdateData(false); ) Затем вкладка свойств вызывается как окно модального диалога через обращение к методу DoModal(). Если мы вернемся к ШОК (пользователь щелкнул на кнопке ОК), то сможем предпринять любое соответ- соответствующее действие. То, что показано в программном коде, — это обновление единичного элемента управ- управления (Salary Range). В коммерческом приложении вы, вероятно, обновили бы документ новыми данными, а затем обновили бы все представления так, чтобы они отражали новые данные. Резюме Вы увидели, что библиотека Microsoft Foundation Classes предлагает мощный набор служебных и вспо- вспомогательных классов, необходимых при создании приложений Windows. Каждый поставщик может предло- предложить свои каркасы приложений. Поставщики усиленно работают над построением актуального базового программного кода, позволяя вам сконцентрироваться на семантике конкретной проблемы, а не на дета- деталях операционной системы. Поскольку библиотеки каркасов приложений отличаются друг от друга, у программистов будет доволь- довольно широкий выбор. Следовательно, делать его нужно осмысленно. Идеальный каркас приложения — это тот, который хорошо написан, инкапсулирован, объектно-ориентирован, действен, расширяем и надежен. Он также должен поддерживаться и эволюционировать вместе с базовой операционной системы.
Контейнерные классы библиотеки STL В ЭТОЙ ГЛАВЕ Определение и реализация шаблонов Последовательные контейнеры Стеки Очереди Связанные контейнеры Вопросы производительности Использование стандартной библиотеки C++ Конструирование типов элементов
Контейнерные классы библиотеки STL Глава 6 Контейнер является объектом, который содержит другие объекты. Стандартная библиотека C++ (Standard C++ Library) содержит несколько контейнерных классов, которые представляют собой мощные инстру- инструменты, помогающие разработчикам справиться с распространенными проблемами программирования. Су- Существует два типа контейнерных классов Standard Template Library (STL): последовательные и связанные. Последовательные контейнеры сконструированы для обеспечения последовательного и случайного доступа к их членам, или элементам. Связанные (ассоциативные) контейнеры оптимизированы для доступа к сво- своим элементам по ключу. Как и другие компоненты Standard C++ Library, библиотека STL переносима между различными операционными системами. Все контейнерные классы STL определены в namespace std. Определение и реализация шаблонов Прежде чем углубиться в обсуждение контейнерных классов Standard Template Library, кратко рассмот- рассмотрим концепцию шаблонов. Шаблоны C++ позволяют типам данных передаваться в качестве параметров в функции и определения классов. Одна и та же функция или класс может использоваться для множества объектных типов. Определение и реализация шаблонов функций Шаблон функций предоставляет возможность конструировать функцию, которую затем можно использо- использовать для обработки различных типов объектов. Например, можно указать, что аргумент будет объектом разных типов. Типы возвращаемого значения также могут изменяться в зависимости от параметров шаблона. Следующий фрагмент кода показывает определение шаблона функций: template<dass ClassX, class ClassY>void Square2(ClassXS x, ClassYS y) x *= x; у *= у; Функция Square2() может использоваться для обработки разных типов объектов, если эти объекты реализуют оператор перегрузки *=. Например, можно вызвать следующую функцию для вычисления квад- квадратов alnt и afloat, двух объектов разных типов: Square2(int alnt, float aFloat); Шаблон функций реализуется путем генерирования функции из функции и параметров template. На- Например, для шаблона функций Square2() рассмотрим данный оператор: Square2(int alnt, float aFloat); Приведенный оператор реализует следующую функцию: Square2(int, float); ИЗ teroplate<class ClassX, class ClassY>void Square2(ClassXt x, ClassYS y) и параметров template ClassX и ClassY. Действительная реализация выполняется компилятором на основе определенного шаблона. Определение и реализация шаблонов классов Шаблон классов обеспечивает основу для конструирования и реализации классов с различными типами их членов. Вы определяете шаблон классов способом, похожим на тот, которым конструируется обычный, нешаблонный класс. Разница заключается в том, что члены шаблона классов можно параметризовать. Ниже приведен пример определения шаблона классов: template<class T> class MyClass { public: // конструкторы и деструктор MyClass(); // конструктор по умолчанию MyClass(T? newVal); // конструктор копии ~MyClass(); // деструктор // функции доступа
Вопросы реализации Часть II void SetVar(TS newVal); Т? GetVar() const; // дружественные функции ostreamS operator«(ostreamS os, const МуС1а«з<Т>Ь c) ; private: T mVar; ) ; template<class T>MyClass<T>::MyClass() О template<class T>MyClass<T>:.MyClaea(TS newVal): mVar(newVal) U template<class T>MyClass<T>::~MyClase() {> template<class T> void MyClass<T>::SetVar(TS newVal) { mVar = newVal; } template<class T> T? MyClass<T>::GetVal() const « return mVar; } template<class T> ostreamS MyClass<T>::operator«(ostreamS os, const MyClass<T>S c) { ¦, os « "member variable mVar = " « c.mVar « "\n"; return os; ) Сравните это с обычным определением класса. Существенная разница заключается в том, что теперь члены класса имеют параметризованный тип. Когда член определяется вне определения класса, то вы дол- должны добавлять к определению функции следующий префикс: template<class T> Вы также должны параметризовать MyClass следующим образом: MyClass<T> Реализация шаблона классов происходит путем генерирования определения класса из шаблона классов и параметров. Следующий пример показывает реализацию двух классов, MyCIass<int> и MyClass<string>: int main () . { ^И> MyClass<int> intObj ; Щщ>1?, intObj . SetVar E) ; cout « "intObj.GetVar = " « intObj .GetVar () « "\n"; cout « intObj; MyClass<string> strObj("This is a string"); cout « "strObj. GetVar * " « strObj. GetVar () « "\n"; cout « strObj; return 0; Последовательные контейнеры ' - Последовательные контейнеры библиотеки STL обеспечивают эффективный последовательный доступ к списку объектов. Библиотека Standard C++ Library предлагает три последовательных контейнера: vector, list и deque. Контейнер-вектор Для хранения и доступа к нескольким элементам часто используются массивы. Элементы в массиве имеют один и тот же тип и получают доступ по индексу. Библиотека STL обеспечивает контейнерный класс vector,
Контейнерные классы библиотеки STL Глава 6 который ведет себя подобно массиву, но более силен и безопасен в использовании по сравнению со стан- стандартным массивом C++. Vector представляет собой контейнер, оптимизированный для обеспечения быстрого доступа к своим элементам по индексу. Контейнерный класс vector определен в файле заголовка <vector> в namespace stf (см. главу 8). При необходимости класс vector может наращивать себя. Предположим, есть контейнер-век- контейнер-вектор для хранения десяти элементов. После заполнения контейнера-вектора десяти объектами он полон. Если добавить к нему еще один объект, то контейнер-вектор автоматически увеличит свою емкость так, чтобы принять 11-й объект. Так определяется класс vector: template <class T, class A = allocator<T» class vector { // члены класса }; Первый аргумент (class T) — это тип элементов в контейнере-векторе. Второй аргумент (class A) — это класс распределителя. Распределители представляют собой диспетчеры памяти, отвечающие за распределе- распределение и высвобождение памяти для элементов в контейнере. По умолчанию элементы создаются оператором new() и освобождаются оператором delete(), т.е. конструктор по умолчанию класса Т вызывается для со- создания нового элемента. Векторы для хранения целых и плавающих можно определить следующим образом: vector<int> vlnts; // вектор содержит целые элементы vector<float> vFloats; // вехтор содержит элементы с плавающей точкой Конструкторы Для использования вектора его необходимо прежде всего создать. При создании объекта вектора от операционной системы получается блок памяти. Размер блока, по крайней мере, достаточно велик, чтобы вместить весь объект vector. Точный размер (называемый емкостью) блока памяти может быть больше, чем тот, что требуется для хранения вектора. При реализации важно решить, исходя из требуемой производи- производительности, какая стратегия распределения памяти лучше подходит для вектора. Как показано ниже, контейнер vector предлагает несколько конструкторов: template <class T, class A = allocator<T» class vector { public: // типы typedef typename A::size_type size_type; // смотри Примечание // конструкторы explicit vector(const AS = A()); explicit vector(size_type n, const TS val = T(), const AS => A()); vector(const vectors v); ПРИМЕЧАНИЕ .«.-, ¦ Ключевое слово typename используется для утверждения, что за ним следует тип. Хотя А является параметром TJ* во время компиляции компилятор не знает, что такое А. Точное значение A::alze_type не прояснится до техпор, А не реализуется класс шаблонов. Следовательно, компилятор не знает, является ли A::size_type типом-или Г ' ной-членом. Квалифицируя имя типа ключевым словом typename, вы указываете, что это имя являетсяд A::size_type представляет собой просто имя для беззнакового целого. Оно используется для индексиро- индексирования элементов контейнера. Вектор можно создать различными способами. Например, этот оператор со- создает вектор целых без элементов: vector<int> vlntl; Следующий оператор создает вектор из 100 целых, каждое из которых инициализируется с помощью МО; vector<int> vInt2A00); Можно также инициализировать вектор содержимым другого вектора, например: vector<int> vlnt3(vlnt2); Конструктор копии vector копирует все элементы из vlnt2 в vlnt3. Показанный оператор может пагубно отразиться на производительности, если работа выполняется с очень большими по размеру векторами.
Вопросы реализации Часть II Размер и емкость Вектор вмещает ограниченное количество элементов. Класс vector содержит функции-члены, которые заботятся о получении памяти так, чтобы векторы могли расти, когда это нужно. Такие функции получе- получения памяти объявляются следующим образом: template <class T, class A = allocator<T» class vector { public: // размер и емкость size_type max_size() const; // максимальное число элементов size_type size() const; // число элементов в векторе bool empty () const; // size = 0; size_type capacity() const; // размер распределенной памяти void reserve(size_type n) ; // резервирование пространства памяти для // п элементов // увеличить размер вектора и добавить элемент void resize (size_type n, T element = T()); } Функция-член max_size() возвращает количество элементов наибольшего возможного вектора. Такая информация полезна, если следует убедиться, что можно создать большой вектор. Вспомните, что под вектор распределяется блок памяти, который может быть больше действительного размера, требуемого создаваемому вектору. Функция capacity() указывает общее количество элементов, которые могут содержаться в распределенной памяти. Если capacity() больше size(), то к вектору можно добавить capacity()—size() элементов без получения дополнительной памяти. Функцию-член reserve(n) можно вызвать для запроса памяти для п элементов. Операция reserve(n) рас- распределяет блок памяти, достаточно большой, чтобы вместить п элементов. Если затребованный размер памяти меньше или равен текущей емкости вектора, то перераспределения памяти не происходит и емкость век- вектора не изменяется. В противном случае распределяется новый блок памяти, способный принять возрос- возросший размер вектора, и емкость вектора устанавливается в п. Когда создается вектор, можно указать количество элементов. Это количество называется размером век- вектора. Размер вектора можно выяснить с помощью функции-члена size(). Значение размера для вектора можно изменить в течение жизни вектора. Функция resize(n) изменяет размер вектора к п элементам. Когда п больше чем capacity(), то компилятор должен распределить больше памяти, чтобы принять вектор. В листинге 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 vlnt2C); // определить вектор целых с тремя элементами cout « "vlnt2C)" « "\n"; ShowVector(vlnt2); vlnt2.resizeE, 100); // увеличить размер vlnt2 до 5 // и добавить значение 100 в хонец cout « "vlnt2 after resizeE, 100)\n"; ShowVector(vlnt2); vlnt2.reserveA0); // зарезервировать память для 10 элементов cout « "vlnt2 after reserveA0)\n"; ShowVector(vlnt2);
Контейнерные классы библиотеки STL Глава 6 return 0; // Отобразить свойства вектора // template<class T, class A> void ShowVector(const vector<T, A>S 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() = 1073741823 size() = 0 vlntC) max_size() = 1073741823 size() = 3 vlnt after resizeE, 100) max_size() - 1073741823 size() = 5 vlnt after reserveA0) max_size() = 1073741823 size() = 5 empty capacity () = 0 not empty not empty capacity () capacity () = 10 i not empty capacity () При определении пустого вектора vlntl память для его потенциальных элементов конструктором по умолчанию не резервируется. Когда определяется вектор vlnt2 для хранения трех целых, то конструктор распределяет память, как раз достаточную для этих трех элементов. Затем размер vlnt2 меняется для хране- хранения пяти элементов. Посмотрите на вывод и обратите внимание, что компилятор увеличил размер вектора до шести элементов. У вас значение размера может оказаться другим, если у компилятора другая стратегия распределения памяти. Позже вызывается функция reserve(), чтобы распределить для vlnt2 больше памяти. В результате этого вызова увеличивается размер vlnt2 до десяти элементов, но размер сохраняется равным пяти. Дополнительное пространство для пяти элементов буквально зарезервировано для будущего исполь- использования. ПРИМЕЧАНИЕ Функция ShowVectorO определена как функция шаблона, чтобы ее можно было использовать для отображения любо- любого типа вектора. Вообще, если функция предназначена для использования различных типов объектов, то ее следует определять как функцию шаблона. При вызове функции reserve(n) или resize(n) (где п > capacity()) в целях получения большого объема памяти для вектора должен распределиться новый блок памяти. Тот факт, что память, распределенная для вектора, должна быть непрерывной, подразумевает, что вновь полученный блок должен идти сразу же после предыдущего распределенного блока. Такое бывает, возможно, не всегда, поскольку память, следующая за первым распределенным блоком, уже может оказаться распределенной под другие объекты. В такой ситуа- ситуации где-то должен распределиться новый блок, который по размеру, по меньшей мере, равен объединен- объединенному размеру исходного и дополнительного блоков. Элементы вектора, включая новый элемент, копируются в новую память. В конце всей операции освобождается исходный блок. Функцию resize() можно также использовать для сокращения размера вектора. Однако функция resize() не влияет на емкость вектора. Доступ к элементам От контейнера будет мало проку, если не обеспечить доступ к его элементам. Контейнеры vector скон- сконструированы для обеспечения быстрого доступа к элементам, выполняемого с помощью индекса. Следую- Следующий программный код показывает функции доступа к элементам, доступные в классе vector: template <class T, class A = allocator<T» class vector
Вопросы реализации Часть II public: // типы typedef typename A::reference reference; typedef typename А::const_reference const_reference; // доступ x элементу reference operator!](size_type n) ; // n-й элемент const_reference operator!](size_type n) const; reference at(size_type n) ; // n-й элемент с conat_reference at(size_type n) const; // проверкой диапазона reference front(); // первый элемент conat_reference front() const; reference back(); // последний элемент const reference back() const; > Обратите внимание, что первые строки создают два typedef: reference и const_reference. По умолчанию ftference является ссылкой на объект класса Т, a const_reference является const T&. Перегруженный оператор индексации [] обеспечивает доступ к элементам по индексам — точно так же, как получается доступ к элементам в массивах. Оператор не проверяет граничных значений, поэтому за правильность индекса несут ответственность программисты. Попытки получить доступ к элементам с ис- использованием индекса, выходящего за границы допустимого, приведут к непредсказуемым результатам. Однако функцию at() можно использовать для проверки того, находится ли индекс внутри допустимого диапазона, и для возбуждения исключения out_of_range, чтобы использовать блок try/catch для перехвата исключения. Исключение out_of_range определено в <stdexcept>, как #included в <vector>. Листинг 6.2 де- демонстрирует применение оператора индексирования и функции-члена at() для доступа к элементам век- вектора. Листинг 6.2. Доступ к членам вектора •Include <io«tream> •include <vector> «Ming namespace std; vector<int> intVector; taaplateKclass T, class A> void ShowVector(const vector<T, A>? v) ; iat maln() I intVector vlntC); // определить вектор целых с тремя элементами eout « "vlntC)n « "\n"; // присвоить значение элементу, используя индексы for (vector<int>: :size_type i = 0; i < vlnt. size (); ++i) vlnt[i] - 5 * i; // присвоить значение элементу, используя функцию at() try { vlnt.at((intVector::size_typeL) = 50; // вне диапазона! } catch(out_of_range) { cout « "Index out of range" « endl; ) ShowVactor(vInt); Vint.resizeE, 100); // увеличить размер vlnt2 до 5 // и добавить значение 100 в хонец cout « "vlnt after resizeE, 100)\n"; // теперь попробовать доступ х 4-му элементу вновь, используя функцию at() try { vlnt.at((intVector::size_typeL) = 50; // теперь в диапазоне
Контейнерные классы библиотеки STL Глава б catch(out_of_range) { cout « "Index out of range" « endl; } ShowVector(vlnt); return 0; } // // Отобразить свойства вектора // template<class T, class A> void ShowVector(const vector<T, A>? v) { // переместиться по вахтеру, используя индексы cout « "\nelements:\n"; for (vector<T, A>: :siz«_typ* i ¦ 0; i < v.siz«(); cout « v[i] « ", ¦¦; cout « "\nfront() = " « v.front(); cout « "\tback() = " « v.back(); cout « "\n\n"; Вывод из листинга показан ниже: vlntC) Index out of range elements: 0, 5, 10, front() = 0 back() vlnt after resizeE, 100) elements: 0, 5, 10, 100, 50, front() = 0 back() = 10 50 Прежде всего создается вектор vlnt с тремя целыми элементами. Значения элементам можно присвоить, используя оператор индексирования. При попытке использовать функцию at() для присваивания значения несуществующему элементу блок try выполняет проверку диапазона и возбуждает исключение out_of_rang?, если позиция недействительна. Блок try/catch используется для перехвата этого исключения дважды. Ис- Исключение появляется в момент незаконного доступа к несуществующему элементу. После изменения раз- размера вектора до пяти элементов второе обращение к функции at() будет успешным. Функция шаблона ShowVector() изменена так, чтобы отображать все элементы в векторе, используя оператор индексирова- индексирования []. Итераторы В предыдущем разделе доступ к элементам вектора выполнялся с использованием оператора индексиро- индексирования. Однако оператор индексирования может не подходить для определенных контейнеров, таких как list, когда доступ к элементам по индексу неэффективен. STL предоставляет другой метод доступа: итераторы. Итераторы обеспечивают стандартную модель доступа к данным так, чтобы эти контейнеры не должны были прибегать к расширенным операциям доступа к элементам. Каждый контейнер STL использует итера- торный класс, который наиболее удобен для оптимизированных операций доступа к элементам. Например, контейнерный класс <vector> определяет несколько функций-членов для поддержки применения итерато- итераторов: template <class T, class A - allocator<T» class vector public: // типы typedef (implementation defined) typedef (implementation defined) iterator; const_iterator;
Вопросы реализации Часть II typedef std::reverse_iterator<iterator> reverse_iterator; typedef std::reverse_iterator<const_iterator> const_reverse_iterator; // итераторы iterator begin(); // указывает на первый элемент const_iterator begin() const; iterator end(); // указывает на (последний + 1) элемент const_iterator end() const; // указывает на последний элемент реверсированной последовательности reverse_iterator rbegin(); const_reverse_iterator rbegin() const; // указывает на (последний + 1) элемент реверсированной последовательности reverse__iterator rend() ; 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; template<class T, class A> void ShowVector(const vector<T, A>S v) ; int mainQ { intVector vlntC); // определить вектор целых с тремя элементами cout « "vlntC)" « "\n"; // присвоить значение элементу в vlnt, используя итератор int i = 0; for (intVector::iterator itor = vlnt.begin() ; itor != vlnt.end() ; ++itor) *itor = 5 * i++; cout « *++itor; ShowVector(vlnt); return 0; // Отобразить свойства вехтора // template<class T, class A> void ShowVector(const vector<T, A>? v)
Контейнерные классы библиотеки STL Глава 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"; // перемещаться по вектору, используя реверсированный итератор 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: vintp) 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, A>::const_iterator itor = v.begin(); itor != v.end(); ++itor) Однако для реверсных итераторов используется более простой оператор (<): for (vector<T, A>::const_reverse_iterator r itor = v.rbegin(); r_itor < v.rend(); ++r_itor) Такая непоследовательность указывает на разницу между пересылаемым и реверсным итераторами. Со- Согласно стандарту C++, реализация оператора < не требуется для пересылаемых итераторов, но требуется для реверсных итераторов, хотя поставщики STL могут реализовывать оператор для обоих типов итерато- итераторов. Хотя ваш компилятор может поддерживать этот оператор для пересылаемого итератора, его использо- использование сделает программный код менее переносимым, поскольку оператор не является частью стандарта языка C++. Модификаторы Элементы можно вставлять или удалять из любой позиции вектора. Функции вставки и удаления назы- называются модификаторами, поскольку они изменяют содержимое вектора. Контейнерным классом <vector> обеспечиваются следующие модификаторы: template <class T, class A = allocator<T» class vector { public: // вставка и удаление // вставить t перед роз и возвратить итератор, указывающий на // вновь вставленный элемент iterator insert(iterator pos, const Ts t) ; // вставить п копий t перед pos void insert (iterator pos, size_type n, const T? t) // вставить диапазон элементов перед pos void insert(iterator pos, const_iterator i, const_iterator j); void puah_back (const T? t); // добавить t в конец void pop back()• // удалить последний элемент
Вопросы реализации Часть II // удалить элемент в позиции pos iterator erase(iterator pos); // удалить элементы от первого до предпоследнего iterator erase(iterator first, iterator last); // удалить все элементы void clear(); ) Когда в конец вектора добавляется новый элемент, может произойти одно из двух. Если емкость векто- вектора больше чем его размер, то распределения памяти не происходит. Новый элемент просто добавляется в конец вектора. Если емкость вектора такая же, как и его размер, то распределяется новый блок памяти, принимающий новый элемент. Как уже говорилось при рассмотрении функции reserve(), получение нового блока памяти может вызвать три затратных с точки зрения времени процесса: ¦ Распределение нового блока памяти, достаточно большого, чтобы принять весь вектор, включая вновь добавленный элемент ¦ Копирование всех элементов в новые позиции памяти ¦ Освобождение старого блока памяти Для большого вектора эти операции могут занять много процессорного времени и памяти — потенци- потенциально дорогостоящее упражнение. Другая дорогостоящая операция контейнера vector — это вставка и удаление из середины вектора. Ког- Когда новый элемент вставляется в середину вектора, все элементы, следующие за этим элементом, должны переместиться, предоставляя ему место. Точно так же, когда элемент удаляется из середины вектора, то все последующие элементы должны подвинуться, чтобы заполнить возникший промежуток. Если ожидает- ожидается частая вставка или удаление из середины, то рассмотрите возможность использования другого класса, такого как list (описанного в этой главе далее). Листинг 6.4 показывает вставку и удаление элементов век- векторов. Листинг 6.4. Добавление и удаление элементов #include <iostream> #include <vector> using namespace std; typedef vector<int> intVector; typedef vector<int>::iterator ivltor; template<class T, class A> void ShowVector(const vector<T, A>S v) ; int main() { intVector vlntE); // определить вектор целых с пятью элементами cout « "vlntE)" « "\n"; // присвоить значение каждому элементу, используя индексы for (vector<int>: :size_type i = 0; i < vlnt. size() ; 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"; // вставить 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);
Контейнерные классы библиотеки STL Глава б // удалить три элемента иа vlnt cout « "vlnt after erase three element\n"; vlnt. erase (vlnt.begin () + 3, vlnt.begin() + 6) ; ShowVector(vlnt); // вставить несколько элементов иа другого вектора intVector vlnt2B, 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); // добавить элемент в конец vlnt2 cout « "vlnt2 after push_baclt () \n" ; vInt2.push_backA00); ShowVector(vlnt2); // удалить элемент из конца vlnt2 cout « "vlnt2 after pop_back() \n" ; vlnt2.pop_back(); ShowVector(vlnt2); II очистить vlnt2 cout « "vlnt2 cleared\n"; vlnt2. clear () ; ShowVector(vlnt2); return 0; // Отобразить свойства вектора // template<class T, class A> void ShowVector(const vector<T, A>S v) { cout « "size() = " « v.sizeO « "\tcapacity() ¦ " « v.capacityO « "\n" // пройтись по вектору используя индексы cout « "elements:\t"; for (vector<T, A>: :size_type i » 0; i < v.«ize(); cout « v[i] « ", "; cout « "\n\n"; Вывод из программы показан ниже: vlntE) size () = 5 capacity () = 5 elements: 0, 5, 10, 15, 20, vlnt after insert(vlnt.begin() + 1, SO) size() = 6 capacity () = 10 elements: 0, 50, 5, 10, 15, 20, Current element is 50 vlnt after insert(vlnt.end(), 5, 30) size() = 11 capacity () ° 12 elements: 0, 50, 5, 10, 15, 20, 30, 30, 30, 30, 30, vlnt after erase one element size() = 10 capacity () = 12 elements: 0, 50, 5, 15, 20, 30, 30, 30, 30, 30, vlnt after erase three element size() = 7 capacity () ¦ 12 elements: 0, 50, 5, 30, 30, 30, 30,
Вопросы реализации Часть II vlnt2 size () = 2 capacity () = 2 elements: О, О, vlnt2 after insert from vlnt size() = 4 capacity () = 4 elements: 0, 50, 5, 0, vlnt2 after push_back () size() = 5 capacity () = 8 elements: 0, 50, 5, 0, 100, vlnt2 after pop_back() size() = 4 capacity () = 8 elements: 0, 50, 5, 0, vlnt2 cleared size() = 0 capacity () = 8 elements: Прежде всего вставляется элемент перед вторым элементом в векторе, что является потенциально до- дорогим удовольствием. Функция insert() возвращает итератор, указывающий на вновь добавленный элемент. Этот результирующий итератор можно разадресовать для отображения нового элемента. Затем в конец век- вектора добавляются пять элементов, каждый из которых имеет значение 30. Функция end() возвращает пос- последний элемент плюс 1 так, чтобы вставка элемента перед end() эффективно добавляла его после последнего элемента. Затем дважды вызывается функция erase(). При первом вызове из вектора удаляется один эле- элемент (четвертый элемент со значением 10), при втором — три элемента (с четвертого по шестой). Затем определяется другой вектор с двумя целыми элементами vlntl Два элемента из vlnt вставляются перед вторым элементом vlnt2. Элементы можно также добавлять или удалять, используя функции push_back() и pop_back(), как это делается с vlnt2. Когда очищен весь vlnt2, размер вектора сокращается. Однако это не уменьшает вектор, его размер остается неизменным. Векторные операции Библиотека STL определяет перегруженные операторы сравнения таким образом, чтобы можно было сравнить друг с другом два вектора одного и того же типа. Перегруженные операторы сравнения часто используются в стандартных алгоритмах. Операторы сравнения также используются в дополнительной век- векторной операции Swap(), которая применяется для обмена значениями элементов в двух векторах. В кон- контейнерном классе <vector> обеспечены следующие операторы сравнения: template <class T, class A = allocator<T» class vector { public: // операции над векторами void swap(vectors v) ; // обмен элементами между техущим вектором и V template <class T, class A> void swap(const vector<T, A>Svl, const vector <T, A>?v2) { vl.swap(v2); } // операторы сравнения template<class T, class A> bool operator==(const vector<T, A>S vl, const vector<T, A>S v2) ; template<class T, class A> bool operator's(const vector<T, A>S vl, const vector<T, A>S v2) ; template<class T class A> bool operator<( const vector<T, A>S vl, const vector<T, A>S v2) ; template<class T, class A> bool operator<=(const vector<T, A>S vl, const vector<T, A>S v2) ; template<class T, class A> bool operator>( const vector<T, A>S vl, const vector<T, A>S v2) ; template<class T, class A> bool operator>=(const vector<T, A>s vl, const vector<T, A>S v2) ;
Контейнерные классы библиотеки STL Глава 6 Два вектора, vl и v2, равны (==), если vl.size() == v2.size() и vl[n] == v2[n], где п имеет значение от О до vl.size()-l. Псевдо реализация оператора == показана ниже: template<class T, class A> bool operator==(const vector<T, A>S 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; i Вектор vl меньше вектора v2, если vl лексикографически меньше чем v2. Фраза "лексикографически меньше чем v2" означает, что правильно одно из следующих утверждений: ¦ Первый vl[n], который не равен v2[n], меньше чем v2[n] ¦ Все vl[n] = v2[n], где п = 0, 1, ..., vl.size-1, но vl.size() < v2.size() Оператор < можно продемонстрировать в следующем фрагменте программного кода: template<class T, class A> 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 T, class A> bool operator!=(const vector<T, A>S vl, const vector<T, A>S v2) < return ! (vl = v2) ; } template<clasa 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
Вопросы реализации Часть И Листинг 6.5. Сравнение векторов #include <iostream> tinclude <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 vector<T, A>S vl, const vector<T, A>S v2); int main() { intVector vlntlE); // определить вектор целых с пятью элементами cout « "vlntl E)" « "\п"; // присвоить значение каждому элементу в vlnt, используя индексы for (vector<int>: :size_type i = 0; i < vlntl .size() ; ++i) { vlntl[i] = 5 * i; ) ShowVector(vlntl); intVector vlnt2 = vlntl; // определить вектор vlnt2 и // скопировать элементы из vlntl cout « "vlnt2E)" « "\n"; ShowVector(vlnt2); // сравнить vlnt и vlnt2 compareVectors(vlntl, vlnt2); // добавить элемент в конец vlnt2 cout « "vlnt2 after pushjback()\n"; vlnt2.push_backA00) ; 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; // Отобразить свойства вектора * // template<class T, class A> void ShowVector(const vector<T, A>S v) { cout « "size() = " « v.size() « "\tcapacity() = " « v.capacityO « "\n" // отобразить элементы вектора, используя индексы cout « "elements:\t" ; for (vector<T, A>: :size_type i = 0; i < v.size(); cout « v[i] « ", "; cout « "\n\n"; // сравнить два вектора // template<class T, class A> void compareVectors(const vector<T, A>S vl, const vector<T, A>S v2)
Контейнерные классы библиотеки STL Глава 6 if (vl = v2) { cout « "vl == v2"; } else if (vl < v2) { cout « "vl < v2"; } else { cout « "vl > v2"; } oout « "\n\n"; Ниже представлен вывод из программы: vlntl E) size () = elements: vlnt E) size() = elements: vl == v2 5 5 capacity () 0, 5, 10, capacity () 0, 5, 10, 15 15 5 , 20, 5 , 20, vlnt2 after push_back() size() = 6 capacity () = 10 elements: 0, 5, 10, 15, 20, 100, vl v2 vlntl after swap size () = 6 capacity () = 10 elements: 0, 5, 10, 15, 20, 100, vlnt2 after swap size() = 5 capacity () = 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, за исключением ин- индексируемых capacity() и reserve(). В следующем листинге перечислены контейнерные члены, предоставляемые классом list: template <class T, class A = allocator<T» class list public: // типы typedef typename A::size_type size_type; // размер типа
Вопрись реилизацш I! типь. итераторо! v с:сшт» t.ypttatif i.mjL-.emer'.tiXi.oi aei-nec! : ter* bui V}rpecLtit _mj.-.emt!iitct:*.o5 at!f:;.iied const-tei.i'.toi t.ypetief Ftc reverst_j №rstoi,<.; tertuir/ reversit . ttretoi v.ypeatii s1.: i.evt;j.ssfc_- t:t;?.'c.t;oj.-<const_. tert'toi.v const revers€_:tbr«'.toi v.ypbabt v.yptsuanifc i reference rcferenct tn'peatii r.ypename i constreterenct const / ' KOHCWpyKTOpi eJ^Jj.l-CJ t *._S*t 'COriSt i\l - J- "¦ ' ci,t j.i.sjt ',s:.s!fc_tvpt' r const TL vs.. = n , con?t i.l = r ' COIIS1 ...,?'t-4 V t..st_v.yp( maj_sb.j:t cons;t- максимально* числе элементе» i;_i:fc_r.yp€ s:..!:t ; const числе алеивито! ь списке doc. empri coi4sst s:..3.t == I , . увеличите jJuSiwej списке f. добавит» злшеш voi-0 res;i.se \S5j.zet.ype r 1 e-ement = ? c'.t ,tj:.ze tjTat r. i'~i элемена с corisj'._rt!terent:t ni ,ii..s:t_typt t, сочгч проверко* диапазона i:i:ont первык аиеиенч cons;i._rei:t;i.ent:t f:i:oiit const back ' посяедго» :t!i.enat Ьас* cons<t - terator becj_i '_ укааьшаеэ нг первы» элемена cons:'.. . tertv'-oi beg:.ii j const . t;ei:cit;'ji eno [ укааывг1е^ нг kпоследний ¦* i • элемент cons4*. . ttri'xoi enc ' const . lit первый элемент реаерсированног последоьап^льност». revemt - t:t!j.?,t;oi rbeg^r '. cons;-- г<л%;.'8(_,1ег?го1 rbega.1 const укаыыьае? иг .наследии»- -< . ¦ зленени реверсироваимог последовательност!- revurat . teri>voi renc consit_i.evt;j.s<t_.xtreitoi renc ' con^t I вставке а удаление / ' вищвиш t пере; pos v возвратить HTepawoj ' ' указывающий нг uuobi вставленный алемем : teravoi „nsitirt .-teratoi pos: cons!t 7 i t. ' вставит* j копит t пере; pot \'o_; д.паег! ,i teriitoi pos ?i..2€_ f.ype i const T( t.. ' / вставите диапаао! элементов пере; pos vo_c -.nsiert. ,i.t:fej.4».t:oj pos cons't _ teiirc'.tx)* _ cons!t_ _ teratoi 3) • t ' операци* с окончание* массиве vti..t pusi_bac* const ?.'{ t: добавит! t 1 KoHeL vji.c pof_bac) |- удалив последит элемент / ' удалил, алемена и! поэицш- pos - teratoj eirasit-,; teratoj pot: / ' удалит* элемента с первого пс предпоследнегс .. t«?:at:o? f...r&t : tercto: ..ast /,/ удалит» act алементь VO.C C-.Oill ' ' списковые операци» vo_c awaj ,....S!t i . помекят! элемент межд; текут» списков к booi opera. tor== ^cons.t .._?!t<? iv>{ 1 гл.. const ...S!t<7 Ai -t listfi
Контейнерные классы библиотеки STL Глава 6 bool operator> =(const iist-CT A>& listl. const list<T, A>6 Iist2); bool operator<; const liSt<T, A>i listl, const list<T, A>6 Iist2); bool operator<=(const iist<T, A>t listl, const list<T, A>6 Iist2); bool operator>, const i^sKl A>S listl. const list<T, A>6 Iist2); bool operator>=(const l^st<T, A>i listl, const list<T, A>6 Iist2); Распределение памяти для контейнерного класса list более динамично, чем в случае с контейнерным классом vector. Помните чтс вектор использует непрерывный блок памяти для приема своих элементов. Если элемент удаляется или вставляется в середин> вектора, то это оказывает влияние на всю занимаемую память. В контейнере list этг проблеме смягчена. Благодаря своей расширенной гибкости по управлению памятью, класс list может обеспечить несколь- несколько дополнительных операций вклейки, операции с начальными элементами, сортировку и слияние. В сле- следующих нескольких раздела? эти операции рассматриваются более подробно. Операции вклейки Операции вклейки перераспределяют элементы из одного списка в другой. Контейнерный класс list обеспечивает три операции вклейки tempiate<clase Т, class h = aiiocator<T» class list public, // вставить все элемента x nepes *pos в текущем списке void splice Сiterator pos xistt x) : // вставить все элемент* *i ие списка х перед *pos в текущем списке void splice (iterator pos. list*, x. iterator i) ; // вставить элемента оч * first до *(last-I) иа списка х перед *pos //' ? текущем списке, voxc splice Uteratoг pos, list» x, iterator first, iterator last) ; Функция spliceO удаляет указанные элементы из списка х и вставляет их в текущий список в заданную позицию. Этот процесс просто выполняет операции над указателями, а элементы не копируются. Рассмот- Рассмотрим, что происходит "за кулисами' Предположим, что создань. две списка, ListA и ListB, как показано на рис. 6.1. После выполнения опе- операции c.splice(pos, х. first, last) две списка распределяются, как показано на рис. 6.2. РИСУНОК 6.1. Два списке перед операцией jlSicf*- 'beginoj *begin() РИСУНОК 6.2, Эти же списки после операции вклейки. ¦¦**¦ *|fBM) •(IW-1) •(pos-1) *(endO- *(end() - Порядок существующи> элементов гарантируется операцией вклейки неизменным, а распределения (или перераспределения! памяти ь результате вклейки не происходит. В листинге 6.6 показано использование операции вклейки.
Вопросы реализации III Часть II Листинг 6.6. Пример операции вклейки #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>& aList); int main() { // определить список целых с пятью элементами intList ListAE); int j = 0; for (intListltor ia = ListA.begin(); ia != ListA.end(); ++ia) *ia = 5 * j++; cout « "ListA" « "\n"; ShowList(ListA) ; // определить список целых с шестью элементами intList ListBF); j = 0; for (intListltor ib = ListB.begin(); ib != ListB.end(); ++ib) *ib = 100 * j++; cout « "ListB" « "\n"; ShowList(ListB); // Объединить данные! cout « "Splice:\n"; ListA.splice(++ListA.begin(), ListB, ++(++ListB.begin()) , -ListB.end()) cout « "ListA" « "\n"; showList(ListA) ; cout « "ListB" « "\n"; ShowList(ListB); return 0; } // // отобразить список элементов // template<class T, class A> 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() 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.
Контейнерные классы библиотеки STL Глава 6 Функция splice() проста, но есть один нюанс, который достоин упоминания. Исследуем следующий оператор: ListA.splice (++ListA.begin () , ListB, ++(++ListB.begin ()), —ListB.endO ) ; Этот оператор говорит, что вы будете вклеивать в ListA часть ListB, начиная с третьего элемента в ListB и заканчивая предпоследним элементом ListB. Вы используете два оператора приращения для ListB.begin(), чтобы достигнуть третий элемент ListB. Почему нельзя вместо этого просто использовать ListB.begin + 2? К сожалению, потому, что стандарт C++ не требует реализации оператора + классами итераторов. Операции с начальными элементами В отличие от векторов, списки эффективно выполняют вставку и удаление первого элемента. Операции с начальными элементами предоставляют возможность добавлять элементы в список более эффективно, поскольку список не надо пересекать. Данный факт ведет к включению двух операций с начальными эле- элементами — одна добавляет первый элемент, а другая удаляет его: template(class T, class A = allocator<T» class list { public: void push_front (const It t); // добавить t в начало void pop_front(); // удалить первый элемент }; Использование обеих операций с начальными элементами показано в листинге 6.7. Листинг 6.7. Операции с начальными элементами списка #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>& aList); int main() { // определить список целых с пятью элементами intList ListAE); int j = 0; for (intListltor ia = ListA.begin (); ia != ListA.end() ; ++ia) *ia = 5 * j++; cout « "ListA" « "\n"; showList(ListA); // удалить первый элемент ListA.pop_front(); cout « "First element removed:\n"; showList(ListA); // вставить новый элемент в начало ListA.push_frontA00); cout « "Insert 100 at the beginning: \n" ; showList(ListA); return 0; // Отобразить список элементов // template<class T, class A> void showList(const list<T, A>& aList) { cout « "size() = " « aList. size () « ":\t"; for (list<T, A>: :const_iterator i = aList .begin (); i != aList.end() cout « *i « ", ";
Вопросы реализации Часть II oout « "\n\n"; Вывод из листинга 6.7 представлен ниже: ListA size() = 5: 0, 5, 10, 15, 20, First element removed: size () =4: 5, 10, 15, 20, Insert 100 at the beginning: sizet) = 5: 100, 5, 10, 15, 20, Первый элемент ListA удаляется функцией pop_front(). После того как новый элемент вставляется в начало функцией push_front(), новый элемент становится первым в списке. Операции с начальными элементами стабильны, т.е. порядок и позиции существующих элементов в списке не изменяются. Операция push_front() должна распределить память для нового элемента, операция pop_front() должна освободить память, занимаемую удаляемым элементом. Операции sortQ и merge() Бывают случаи, когда список требуется отсортировать. Контейнерный класс list обеспечивает следую- следующие две операции sort(): template<class T, class A = allocator<X» class list { public: // алгоритмы сортировки void sort () ; template<class Compare> void sort(Compare); // реверсировать порядок элементов void reverse(); // слить два отсортированных списка 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.8. Слияние и сортировка списков #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>& aList); int main()
Контейнерные классы библиотеки STL Глава 6 // определить список целых с пятью элементами intList ListAE) ; int j = 0; for (intListltor ia = ListA.begin() ; ia != ListA.end() ; ++ia) *ia = 5 * j++; cout « "ListA" « "\n"; showList(ListA); // определить список целых с шестью элементами intList ListBF); j = 10; for (intListltor ib = ListB.begin(); ib != ListB.endO; ++ib) *ib = 2 * j-; cout « "ListB" « "\n"; ShowList(ListB); intList ListC = ListA; intList ListD = ListB; // слить первый pas без сортировки 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 0; showList(ListA) ; // сортировать и слить cout « "Sort and Merge :\n"; ListC.sort(); ListD.sort() ; ListC.merge(ListD); cout « "ListC" « "\n"; showList(ListC); cout « "ListD" « "\n"; showList(ListD); return 0; // Отобразить список элементов // template<class T, class A> void showList(const list<T, A>& aList) { cout « "size() = " « aList. size () « ":\t"; for (list<T, A>: :const_iterator i = aList .begin (); i != aList. end () cout « *i « ", " ; cout « "\n\n"; Вывод из листинга 6.8 показан ниже: ListA size() = 5: 0, 5, 10, 15, 20, ListB size() = 6: 20, 18, 16, 14, 12, 10, Merge unsorted lists: ListA size() = 11: 0, 5, 10, 15, 20, 20, 18, 16, 14, 12, 10,
Вопросы реализации Часть II ListB size () = 0: Reverse ListA size() = 11: 10, 12, 14, 16, 18, 20, 20, 15, 10, 5, 0, Sort and Merge: ListC size() = 11: 0, 5, 10, 10, 12, 14, 15, 16, 18, 20, 20, ListD size() = 0: Когда сливаются два несортированных списка — ListA и ListB, то элементы из ListB просто добавляют- добавляются в конец ListA и результирующий ListA получается неотсортированным. Когда список ListA реверсирует- реверсируется, то результат все равно остается неотсортированным. Затем вы сортируете ListC и ListD. При слиянии двух сортированных списков — ListC и ListD — результат получается также отсортированным. Операции по удалению элементов Элементы из списка можно удалять. Например, приложение по обслуживанию новостей в Internet мо- может отображать списки новостей на основе профилей пользователей. Когда пользователь меняет свой про- профиль, то определенные элементы должны из списка удаляться. Контейнерный класс list обеспечивает следующие четыре операции для удаления элементов из списков: template<class T, class A = allocator<T» class list < public: // удалить элемент из списка void remove(const TS t) ; template<class Predicate> void remove_if(Predicate p); // удалить дублирующиеся элементы void unique(); template<class Predicate> void unique(Predicate p); }; Обычная функция remove() удаляет элементы, которые равны t. Для этого используется перегруженный T::operator==. Функция шаблона remove_if() удаляет элементы, у которых предикат р оценивается как true. Обычная функция unique() удаляет из списка все дублирующиеся элементы. Элементы ет, ет+1, ..., еп, где е, обозначает i-й элемент в списке, считаются дублирующимися, если они идут в списке последовательной группой и если em == em+1 ==...==en. Функция шаблона unique() делает то же самое, за исключением использования предиката р вместо оператора ==. В листинге 6.9 показаны различные операции удаления из списка. Листинг 6.9. Удаление элементов из списков tinclude <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>& aList); int main() { // определить список целых с пять» элементами intList ListA E); int j = 0; for (intListltor ia = ListA.begin () ; ia != ListA.end() ; ++ia) *ia = 5 * j++; cout « "ListA" « "\n"; ShowList(ListA);
Контейнерные классы библиотеки STL Глава 6 // удалить элемент иа списка ListA ListA.removeE); cout «  removed from ListA:\n"; ahowList(ListA); // определить список целых с шесть» элементами intList ListBF); j = Ю; for (intListltor ib = LiatB.begin(); ib != ListB.end(); ++ib) *ib = 2 * j—; cout « "ListB" « "\n"; ahowList(ListB); // слить ListA & ListB и добавить элемент 10 в начало ListA cout « "splice ListA & ListB and add 10 to the beginning: \n" ; ListA.splice(++(++ListA.begin()), ListB); ListA.push_frontA0); cout « "ListA" « "\n"; showList(ListA); // сначала отсортировать ListA ListA.sort(); // так, чтобы элементы с одинаковыми значениями были сгруппированы вместе. cout « "ListA sorted:\n"; showList(ListA); // удалить дубликаты иа ListA ListA.unique () ; cout « "Duplicates in ListA removed:\n"; showList(LiatA); return 0; ) // // Отобразить список элементов // template<class T, class A> void ahowList(const list<T, A>& aList) { cout « "size() = " « aList. size () « ":\t"; for (list<T, A>: :const_iterator i = aList.begin() ; i != aList.end(); ++i) cout « *i « ", " ; cout « "\n\n"; } Вывод из листинга 6.9 показан ниже: LiatA size() = 5: 0, 5, 10, 15, 20, 5 removed from ListA: size() = 4: 0, 10, 15, 20, ListB aize() = 6: 20, 18, 16, 14, 12, 10, splice ListA & ListB and add 10 to the beginning: ListA aize() = 11: 10, 0, 10, 20, 18, 16, 14, 12, 10, 15, 20, LiatA sorted: aize() = 11: 0, 10, 10, 10, 12, 14, 15, 16, 18, 20, 20, Duplicates in ListA removed: aize() = 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 A = allocator<T» class deque public: // типы typedef typename A::aize_type size_type; // размер типа // типы итераторов и ссылок typedef (implementation defined, may be T*} iterator; typedef (implementation defined, may be const T*) const_iterator; typedef std::reverse_iterator<iterator> reverse_iterator; typedef std::reverse_iterator<const_iterator> const_reverse_iterator; typedef typename A::reference reference; typedef typename A::const_reference const_reference // конструкторы explicit deque (const AS =• A()) ; explicit deque(size_type n, const TS val = T(), const hi = *,;.; vector(const deques dq) ; // функции размера size_type max_size() const; // максимальное число элементов size_type aize() const; // число элементов в чеке bool empty () const; // size О = 0; // увеличить размер деки и добавить элемент void resize(size_type n, T element = TO); // доступ к элементам reference at(size_type n) ; // n-й элемент z const_reference at(size_type n) const; // проверкой диапазона reference front О; // первый элемент const_reference front() const; reference back(); // последний элемент const_reference back О const; // итераторы iterator begin(); // указывает на первый элемент const_iterator begin() const; iterator end О; // указывает на (последний + 1) элемент const_iterator end О const; // указывает на первый элемент реверсированной последовательности reverse_iterator rbegin(); const_reverse_iterator rbegin() const; // указывает на (последний + 1) элемент реверсированной последовательности reverse_iterator rend(); const_reverse_iterator rend 0 const; // вставка и удаление // вставить t перед pos и возвратить итератор, // указывающий на вновь вставленный элемент iterator insert(iterator pos, const T& t) ;
Контейнерные классы библиотеки STL Глава б // вставить п копий t перед pos void insert(iterator pos, size_type a. const rs t) // вставить диапазон элементов перед pos void insert(iterator pos, const_i'erator i, const_iterator j) ; // операции void void void void с началом и окончанием push_front(const IS к, pop_front(); push_back(const I& t; pop_back(), // добавить t з начало II удалить первый элемент // добавить t з конец // удалить последний элемент // удалить элемент из позиции pos iterator erase(iterator pos) • // удалить элементы с первого яо 'Предпоследний iterator erase(iterator first, iterator last), // удалить все элементы void clear () ; // операции деки void swap(deque S dq); bool operator= (const bool operator!=(const bool operator<( const bool operator<=(const bool operator>( const bool operator>=(const // поменять элементы между текущей декой и deque<T, A>S dql, deque<T, A>& dql, deque<T, A>& dql, deque<T, A>s dql, deque<T, A>& dql, deque<T. A>S dql, const deque<T, A>« dq2) . const deque<T, A>& dq2) ; const deque<T, A>& dq2) , const deque<T, A>S dq21 ; const deque<T A>& dq2) const deque<T. A>S dq2); Стеки Одной из наиболее распространенных :труктур данных в программировании является стек. Однако стек не реализован как независимый контейнерный класс. Вместо этого он реализован как обертка контейнера. Шаблонный класс stack определен з файле <stack> з namespace std. Стек — это непрерывно распределенный блок, который увеличивается или уменьшается с конца. Эле- Элементы в стеке доступны или удаляются только с конца. Вы видели подобные характеристики в последова- последовательных контейнерах, векторах и деках. Фактически любой последовательный контейнер, который поддерживает операции back(), push backQ л pop_back(), яожет использоваться для реализации стека. Боль- Большинство других контейнерных методов для стека не требуются и, следовательно, стеком не выставляются. Шаблонный класс библиотеки STL stack сконструирован для хранения объектов любого типа. Единствен- Единственное ограничение заключается в том, что зсе объекты должны быть одного л того же типа. Стек является структурой LIFO (last in, flnt out — последним вошел — первым вышел). Это как перепол- переполненный лифт: первый вошедший человек продвигается к стене, а последний зошедший стоит рядом с дверью. Когда лифт достигает назначенного этажа, то последний вошедший выходит первым. Если кто-то хочет покинуть лифт ранее, то все люди между ним л дверью должны уступить ему дорог/, вероятно, зыйдя из лифта и затем вернувшись обратно. По соглашению открытый конец стека часто называют вершиной стека, i операции, проводимые над стеком, часто называют проталкиванием л выталкиванием. Класс stack наследует эти распространенные термины. ПРИМЕЧАНИЕ ¦-;:' Класс библиотеки STL stack — это не то же самое, что механизм стека, используемый компиляторами и" опера Щгръмил системами, в которых стеки могут содержать объекты разных типов. Однако базовая функциональность! ^•похожа. Интерфейс стека очень прост: template <claas T, class Container class «tack deque<T» public:
Вопросы реализации Часть II typedef typename Container::value_type value_type; typedef typename Container: :size__type size_type; typedef typename Container container_type; protected: Container с; public: explicit stack(const Containers = Container()); bool empty() const { return c.empty(); } size_type sizeQ const { return c.sizeO; } value_typeS top() { return c.bac)c(); } const value_type& top() const { return c.back(); } void push(const value_typeS 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<class T, class C> void ShowStack(stack<T, C>& aStack); int main() < // создать стек целых stack<int> slnt; cout « "Stack slnt created:\n"; ShowStack(slnt); // протолкнуть элемент в стек for (unsigned int i = 0; i < 5; ++i) slnt.push(i * 2) ; cout « "slnt:\n"; ShowStack (slnt); // изменить верхний элемент 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; // Отобразить свойства стека // template<class T, class О void ShowStack(stack<T, OS aStack) { cout « "size = " « aStack.size() ;
Контейнерные классы библиотеки STL Глава б if (!aStack.empty ()) cout « "\ttop = oout « "\n\n"; « aStack, top () ; Вывод из листинга 6.10 показан ниже: Stack slnt created: size = 0 slnt: size ¦¦ top = 8 top element size = 5 modified: top = 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 T, class Container> bool operator==(const stack<T, Container>& x, template <class T, class Container> bool operator< (const stack<T, Container>S x, template <class T, class Container> bool operator!=(const stack<T, Container>& x, template <class T, class Container> bool operator> (const stack<T, Container>& x, template <class T, class Container> bool operator>=(const stack<T, Container>& x, template <class T, class Container> bool operator<=(const stack<T, Container>& x, const const const const const const stack<T, stack<T, stack<T, stacker, stack<T, stack<T, Container>& Container>& Container>& Container>& Container>& Container>& У) У) У) У) У) У) Очереди Очередь — это еще одна широко используемая структура в программировании. Элементы добавляются в очередь с одной стороны, а извлекаются с другой. Классическая аналогия такова: стек — это как стопка подносов с салатом в буфете. Вы добавляете в стек, помещая поднос на вершину (проталкивая стек вниз) и берете из стека, выталкивая поднос сверху (тот, что был добавлен совсем недавно). Очередь — это как очередь в театр. Вы становитесь в конец очереди и уходите из очереди в ее начале. Такая структура известна под названием 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 T, class Container = deque<T> > class queue { public: typedef typename Container::value_type value_type; typedef typename Container::size_type sizetype; typedef typename Container container_type; protected: Container с; public: explicit queue(const Containers = Container ()); bool empty() const { return c.empty () ; } size_type size() const { return c.sizeO; } value_typeS front() { return c.front(); } const value_typeS front() const { return c.front(); } value_types back() { return c.back() ; } const value_type& back() const { return c.bac)c(); } void push(const value_type& x) { c.push_back(x); } void pop() { c.pop_front() ; } }; Этот код показывает сходство между шаблонными классами queue и stack. Обычные операции push() и рор() используются для обертки контейнерных операций push_back() и pop_front(). В листинге 6.11 демон- демонстрируются базовые операции очереди. Листинг 6.11. Базовые операции очереди #include <iostream> tinclude <queue> using namespace std; template<class T, class C> void ShowQueue(const queue<T, C>& 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); // удалить из очереди первый элемент qlnt.pop () ; cout « "After one pop() operation\n"; ShowQueue(qlnt);
Контейнерные классы библиотеки STL Глава б return 0; // Отобразить элементы очереди template<class T, class О void ShowQueue(const queue<T, OS aQueue) cout « "size() = " « aQueue. size () ; if (!aQueue.empty()) cout « "\tfront!) = " « aQueue.front(); cout « "\tback() = " « aQueue. back (); cout « "\n\n"; Вывод из листинга 6.11 показан ниже: Queue qlnt created: size () = О qlnt: size () = 4 front () = 2 back () The first and last elements modified: size () = 4 front () = 20 back() = 30 After one pop() operation size() = 3 front () = 4 back() = 30 Элементы проталкиваются в очередь. Как и стек, очередь может получать память при увеличении своего размера. Вы можете получать доступ к первому и последнему элементам в очереди, используя операции frontO и back() После того как первый (верхний) элемент из очереди удален с помощью функции рор(), следующий элемент становится новым первым элементом. И вновь для сравнения двух очередей определяются перегруженные операторы сравнения: template <class bool operator== template <class bool operator< template <class bool operator ! = template <class bool operator> template <class bool operator>= template <class bool operator<= T, class Container> (const queue<T, Container>& x, T, class Container> (const queue<T, Container>& x, T, class Container> (const queue<T, Container>& x, T, class Container> (const queue<T, Container>S x, T, class Container> (const queue<T, Container>& x, T, class Container> (const queue<T, Container>S x, const queue<T, const queue<T, const queue<T, const queue<T, const queue<T, const queue<T, Container>s y); Container>& y), Container>S y); Container>& y), Containers y) ; Container>S y); ПРИМЕЧАНИЕ Как и класс стека библиотеки STL, класс очереди — это не то же самое, что механизм очереди, используемый компи- компиляторами и операционными системами, где очереди могут содержать различные типы объектов. Однако базовая фун- функциональность очень похожа. Приоритетные очереди Приоритетная очередь — это очередь, элементам которой присвоен приоритет. Шаблонный класс priority_queue также определен в файле <queue> в namespace std. Элемент с высшим приоритетом находит- находится в начале очереди. Интерфейс для шаблонного класса priority_queue выглядит следующим образом: template <class T, class Container = vector<T>, class Compare = less<Container::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 с; Compare comp; public: explicit priority_queue(const Compares x = Compare(), const Containers = Container()) : с(Container), comp(Compare) (}; template <class Inputlterator> priority_queue(lnputlterator first, Inputlterator 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):
Контейнерные классы библиотеки STL Глава 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 GetPriceO 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: string mName; int mStockLevel; double mPrice; int main() Product Pen("Pen", 5.99, 58); Product TableLamp("Table Lamp", 28.49, 24); Product Speaker("Speaker", 21.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: Pen Product: Table Lamp Product: Speaker Speaker's price is 24.95 Используя эту программу, вы можете напечатать прайс-лист продукции для всего склада. Но что, если нужно знать только цену динамиков? Для получения такой информации необходимо написать код, подоб- подобный следующему: for (int j = 0; j < 3; ++j) if (productArray[j] .GetNameO = "Speaker") cout « "Speaker's price is " « productArray [j] .GetPrice () « "\ n"; Такой процесс довольно неэффективен и запутан. Не лучше ли сделать так? cout « "Speaker's price is " « productArray["Speaker"].GetPrice() « "\ n" ; Очевидно, что с обычными массивами такого сделать нельзя. К счастью, это можно сделать с контей- контейнерные классом тар библиотеки STL. Изменим листинг 6.12 так, чтобы он хранил списки продуктов в тар и с его помощью можно было получить цену динамиков. Такой цели достигает программа в листин- листинге 6.13. Price: Price: Price: 5. 28 24 99 .49 .95 Stock Stock Stock on on on hand: hand: hand: 58 24 40
Вопросы реализации Часть \\ Листинг 5.13. Использование тар для хранения перечня продуктов ^include <iostream> #include <string> Kinclude <map> using namespace з ".d class Product { public: Product". raName'"New Product"), mStockLevel@), mPrioe(O) {} Product; (string newName, double newPrice = 0, int newStockLevel = 0): mName{newName), mPrice(newPrice), mStockLevel(newStockLevel) {} void SetName(string newName) void SetName(int newStockLevel) void SetPrice(double newPrice) string GetName () const int 5etStockLevel () const double "SetPrice О const {mName = newName; } {mStockLevel = newStockLevel;} {mPrice = newPrice; У {return mName;} {return mStockLevel;} {return mPrice; friend ostreams operator« 'ostreamS os, Products p) I os « "Product: " « p.mName « "\tPrice: " « p.mPrice « "\tStock on hand: " « p.mStockLevel; return os, private. string int double } г int main G nName. oStockLevel, «Price: Product ?en{"Pen", 5 39, 58), Product ГаЫеЬатр ("Table Lamp", 28.49, 24); Product Speaker('Speaker", 24 95, 40); map<sftr.ing, Product> productMap ; productMap f Pen GetNameO] = Pen; productMap[TableLamp.GetName()] = TableLamp; productMap[Speaker.GetName О i = Speaker; cout « 'Speaker s price is return 0, « productMap ["Speaker"] .GetPriceO « '\nfl Вывод из листинга 6 t.3 показан ниже: Speaker's price is 24 95 В листинге 6.13 показана важная характеристика контейнерного класса тар: доступ к элементам записи можно получить по ключевым значениям. Это подразумевает, что контейнер тар должен иметь класс Key. Контейнерный класс тар должен также уметь сравнивать ключевые значения, используя либо встроенные операторы сравнения класса Key, либо внешний объект сравнения. Как и ожидается, контейнер тар дей- действительно имеет класс Key, который обеспечивает данный механизм: template<class Key, class T, class Compare = less<Key>, class A = Allocator<T» class map // типы typedef Compare typedef A typedef typename A: :deference_type typedef typename A::reference key_compare; allocator_type; difference_type; reference;
Контейнерные классы библиотеки STL Глава 6 cypeder" cypename A: : const_reference const_reference. Первый аргумент Key является гипом ключей для записи, второй — типом тачения элемента. В приме- примере записи Product ключом является идентификатор продукта типа string, а значением элемента — тип Product. Третий аргумент является классом Compare, который можно использовать для сравнения значений двух ключей. Четвертый аргумент А — это распределитель, который заботится об управлении памятью контей- контейнеров. В большинстве случаев он должен быть адекватным Allocator<T> ло умолчанию. Map содержит пос- последовательность лар ключ/значение, где каждый ключ уникален. Это позволяет осуществить точное распределение элементов в записи. Кроме того, вы должны уметь отсортировать элементы в записи соглас- согласно л% ключевым значениям. Если не указывать класс Compare, го будет использоваться функция сравнения <. Оператор < можно перегрузить для сравнения двух объектов вашего класса. В примере Product для срав- г!ения ядентификаторов продуктов используется operators (string, string). Если вы решите упорядочить спи- список продуктов по наименованиям, то можете определить класс Compare л обеспечить его записью Product. Более подробно определение класса сравнения описано в главе 7. Конструкторы и деструктор Следующий фрагмент программного кода показывает конструкторы и деструктор для контейнерного класса тар: •;empia?e<cias3 Key, class T, class Compare = 1езз<Кеу>, class A = Aiiocator<T» class map ( // конструкторы и деструктор 11 создать аустую запись ascpi^cit napvconst Compare» cmp = Compare О , const hi = A ) , , ' создать мар и копировать с первого по предпоследний элементы из I.nputlterator lemplar.e<ciass Inputlterator> napflnputlterator first, Inputlterator Last, const Compares cmp = Compare () , const Ai ¦= A;. » /,' конструктор копии map (const map4 m) , '' деструктор -map!', ¦ оператор присваивания map* operator= (const шар»; } ¦ Конструктор-член создает объект map с исходным классом Compare и, или классом распределителя. (Сейчас мы не будем затрагивать шаблонный конструктор, поскольку он вовлекает вводной итераторный класс. Итераторы рассматриваются в этой главе далее.) Конструктор копии, деструктор и оператор при- присваивания являются стандартными. Размеры Как и последовательные контейнеры, класс тар обеспечивает несколько функции-членов, касающих- касающихся определения размеров записи: template<class Key, class T, class Compare = less<Key>, class A = Allocator<T» class map ( public: // типы typedef typename A::size_type size_type; // размеры size_type sizeO const; // количество элементов size_type max_size() const; // размер наибольшего bool empty () const { return (siza() == 0): } Все эти операции аналогичны операциям в классе vector
Вопросы реализации Часть II Итераторы Как и другие стандартные контейнеры, записи можно пересекать, используя итераторы: template<class Key, class T, class Compare = less<Key>, class A = Allocator<T» class map { public: // типы typedef Key key_type; typedef T mapped_type; typedef (implementation defined) iterator; typedef (implementation defined) const_iterator; typedef std::reverse_iterator<iterator> reverse_iterator; typedef std::reverse_iterator<const_iterator> const_reverse_iterator; // итераторы iterator begin(); // указывает на первый элемент const_iterator begin() const; iterator end(); // указывает на (последний + 1) элемент const_iterator end() const; // указывает на первый элемент реверсированной последовательности reverse_iterator rbegin(); const_reverse_iterator rbegin() const; // указывает на (последний + 1) элемент реверсированной последовательности reverse_iterator rend(); 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 First first; Second second; }; В примере записи Product можно изменить цену любого продукта и напечатать прайс-лист, как показа- показано в листинге 6.14. Листинг 6.14. Доступ к элементам записи ((include <iostream> jfinclude <string> #include <map> using namespace std; class Product { public: Product():mName("New Product"), mStockLevel@), 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 GetNameO const { return mName; } int GetStockLevel() const ( return mStockLevel; } double GetPrice() const { return mPrice; } friend ostreamS operator«(ostreamS os, const Products p) { os « "Product: " « p.mName « "\tPrice: " « p.mPrice « "\tStock on hand: " « p.mStockLevel; return os;
Контейнерные классы библиотеки STL Глава 6 private: string int double mName; mStockLevel; mPrice; // шаблонная функция для отображения всех элементов записи template<class T, class A> void ShowMap (const mapKT, A>& 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 T, class A> 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: Lamp Product: Lamp Pen Product: Pen Speaker Product: Speaker Price: 28.49 Stock on hand: 24 Price: 5.99 Stock on hand: 58 Price: 24.95 Stock on hand: 40 У вас есть определенный по умолчанию конструктор для класса Product. Это не строго необходимо, но требуется для конструирования productMap, поскольку productMap использует для распределения памяти под свои элементы конструктор по умолчанию Product. В productMap элементам присваиваются три пары ключ/значение. Шаблон функций ShowMapO представляет собой функцию общего назначения, которую можно использовать для отображения всех элементов для любой записи. Для доступа к элементам здесь используется итератор. Поскольку элементы в записи являются парами, то first и second используются для возврата их пар ключ/значение. Доступ к элементу Контейнерный класс мар предлагает несколько функций, которые можно использовать для доступа к элементам по их ключевым значениям. Для прямого получения элемента можно использовать оператор индексирования [] или можно использовать функцию-член find() для получения указателя (итератора) на желаемый элемент. Следующий программный код показывает функции доступа, обеспечиваемые контей- контейнерным классом мар: template<class Key, class T, class Compare = less<Key>, class A = Allocator<T» class map { public: // индексация
Вопросы реализации Часть II mapped_types operator[](const key_typeS к); // Другие операции // число элементов с ключом к, возвратить 0, если не найдены size_type count(const key_typeS к); // найти элемент с ключом к, возвратить мар:end(), если не найдены iterator find(const key_type& к) ; const_iterator find(const key_typeb k) ; // найти первый элемент с ключевым значением большим или равным ключу к iterator lower_bound(const key_type& к) ; const_iterator lower_bound(const key_typeS k) const; // найти первый элемент с ключом большим к iterator upper_bound(const key_type& к) ; const_iterator upper_bound(const key_typei k) const; // найти все элементы с ключом к pair(iterator, iterator) equal_range(const key_type& k); pair(const_iterator, const_iterator) equal_range(const key_types k) const; }; Вы уже видели, что можете получить доступ к элементу по его ключу. Что произойдет, если указать ключевое значение, которое не существует? Рассмотрим пример: 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"].GetPrice() * 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, Products-: : iterator ci = productMap.lower_bound("Pen"); cout « "First Pen is " « ci->second « "\n";
Контейнерные классы библиотеки STL Глава 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; Вывод из измененного программного кода представлен ниже: Stock on hand: 58 24.95 Price: Stock on hand: 40 28.49 Stock on hand: 24 6. 24 589 .95 Price: Price: Price: Stock Stock 0 24.95 24.95 on on hand: hand: Stock Stock Stock 58 40 on on on hand: hand: hand: 0 40 40 Map elements: Pen Product: Pen Price: 5.99 Speaker Product: Speaker Price: Table Lamp Product: Table Lamp Speaker's price is 24.95 Pen's price has been changed to 6.589 Number of Pens is 1 First Pen is Product: Pen Price: Next to Pen is Product: Speaker Price: Try Rubber: Product: New Product Number of Red Pens is 0 First Red Pen is Product: Speaker Next to Red Pen is Product: Speaker Элементы записи можно изменить с помощью оператора индексирования []. Например, цена ручек величивается на 10%, если используется функция SetPrice() класса Product. Вы показываете количество лементов с ключевым значением "Реп", используя функцию count() Здесь имеется только один элемент, вы знаете, что никогда не будете иметь более одного элемента с одним и тем же ключевым значением, функция lower_bound() ищет продукт "Реп", и функция upper_bound() дает нам элемент с ключевым зна- ением, превышающим значение "Реп". Это динамик. Затем вы пытаетесь найти продукт с ключевым значением "Rubber". Он не существует, поэтому созда- гся новый продукт. Для этого используется конструктор по умолчанию класса Product. Функция count() ля несуществующего ключевого значения возвращает 0, указывая, что продукта с таким ключевым зна- ением не существует. Обратите внимание, что здесь функция count() не создает новый элемент в записи, функция lower_bound() для данного несуществующего ключа возвращает элемент, который имеет следую- iee ключевое значение (в данном случае динамик). Функция upper bound() работает точно так же. Может возникнуть ощущение, что функции Iower_bound(), upper_bound() и equal_range() в данном слу- ае имеют довольно ограниченное применение. Помните, что контейнерный класс мар не допускает дуб- ирующихся ключевых значений. Подробнее об этом будет рассказано, когда будет представлен класс lultimap, где дублирующиеся ключи допустимы. Вставки и удаления Элементы можно удалять из записи или вставлять в запись. Класс мар обеспечивает несколько функций ставки и удаления: template<class Key, class T, class Compare = less<Key>, class A = Allocator<T» class map public: // типы typedef pair<const Key, T> value_type; // вставка и удаление pair<iterator, bool> insert(const value_types val); iterator insert(iterator pos, const value_typeS val) // вставить пару <key, T>
Вопросы реализации Часть II template<class Inputlterator> void insert(Inputlterator first, Inputlterator last); void erase(iterator pos); size_type erase(const key_value* k); void erase(iterator first, iterator last) ; void clear(); ); Операции вставки и удаления аналогичны подобным операциям в векторах. Рассмотрим следующую функцию: iterator insert(iterator pos, const value_type& val) ; Итератор pos не влияет на то, где вставляется val, поскольку позиция элемента определяется только после вставки пары <кеу, Т>. В листинге 6.16 показано, как добавлять и удалять элементы из контейнерного класса мар. Замените функцию main() в листинге 6.14 на программный код из листинга 6.16 и получите всю программу. Листинг 6.16. Добавление и удаление элементов typedef map<string, Product> PRODUCT_MAP; int mainQ { 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"; else cout « "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"; else cout « "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: Speaker Price: 24.95 Stock on hand: 40 Add a new element: New element added! Map elements:
Контейнерные классы библиотеки STL Глава 6 Lamp Product: Lamp Price: 28.49 Stock on hand: 24 Pen Product: Pen Price: 5.99 Stock on hand: 58 Speaker Product: Speaker Price: 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: Speaker Price: 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: Speaker Price: 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 T, class Compare - less<Key>, class A = Allocator<T» class map { public: // операции над записями void swap(maps m) ; // поменять местами элементы между текущим шар и m 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); 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); 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 Вспомните синтакси; функции insert{i p классе мар: pair<iterator bool>:.nsert {const value_type& val) ; В классе мар второй элемент i вс!звращаемой паре используется для указания того, была ли вставка успешной Второй элемен- ял; класса multimap избыточен, поскольку он всегда имееп значение true Таким образом, функция insert'* реализована в классе multimap следующим образом: iterator insert, icoiift value_type* val): Функции lower_bounfl(j. uptie?_bound() и range_check() являются основными средствами доступа к эле- элементам множественной записи с указанными ключевыми значениями. Эти три функции определены i классе multimap следующим образом template<clasr Key Ci-as; 5 ;.±asj Compare = less<Key>, class к = allocat.or<T>> class multimap i public• // другие члень классе. /' / найти первый элеменч с клочок большим или. равным ключу >. iterator j.ower_bound (const key_typet k.> . const_: teratoi lt)ne:_houncf'const key_typeS k; const // найти первь» элемент с ключом большик к iterator uppei_bound(const key_typeS kj ; const_iterator uppej_bound(const key_types k) const // найти все элементь о ключок к pair(iterator, iterator) equal_range(const key_typeS k> pair (const; teretoj const iterator) equal_range (const ke',_typeS k; const }; Функции loweF_bound(,' и upper_boundO кратко уже описывались, когда вы приступили i; знакомству с классом мар. Теперь рассмотрим функцию equai_range(). Функция equal_range( s возвращаем пару итераторов, содержащих первый элемент с ключом к и первый элемент с ключевьш значением превышающим к Функция equai_range(> действует каг комбинации фун- функций lower_bound() и upper_bound{>. собранных в одной функции Это уменьшает время выполнения и упрощает программ^ г смысле доступа ь диапазону элементов. В листинге 6.1" пoкaзa^! пример использова- использования класса multimap Листинг 6.17. Использование класса multimap #include <iostream^ #include <string> ¦include <map> using namespace std template<class Kev class T> void ShowMultimap(const mu1txmap<Key T>S m) : template<class Key, class T> void ShowMultimapRangetconst mujtimap<Key T>4 m, const Keys k) ; int main() { multimap<string, string> stateMap; stateMap insert (make_paxr i (string) "USA1', (string) "California")) . stateMap.insert(make_pair4 (string)"USA", (string)"New York")); stateMap. insert (make_pair ; (string) "USA'', (string) "Washington") ) ; stateMap insert(make_pair,istringVAustralia", (string)"New South Wales");. stateMap.insert(make_paxr'(string!"Vatican City", (string)"Vatican City")): ShowMultimap(stateMap) ShowMultimapRange'.stateMai (string! "USA") ; return 0,
Контейнерные классь: библиотеки STL Главе 6 tempiate<class Key class T> voig ShowMuItimap(const mu3timap<Key, T>( m) t typedef multimap<Key, T>;¦const_iterator Itor; for (Itor i = m.begin',) ; i != m.end(); cout «: i->first « "\t" « i->second « "\n"; cout « "\n"; template<class Key, class T> voic ShowMultimapRange (const multimapKKey, T>4 n,. const Keys k typedef multimajKKey, T>- const_iterator Itor, pair<ltor, Itor> p = it. equal _range (k/ for (Itor » = p first; i '=• j. seconu ++i. cout <<¦ i->first « "\f « i->second « "\t>" cout << "\i>.": Вывод программы из листинга 6.17 показан ниже: Australia New South Wales US* California USA New York USA Washington Vatican City Vatican City USA California USA New York USA Washington В программе создается множественная запись, которая содержит пары страна/штат. Эти пары добавля- добавляются к множественной записи с помощью функции insert(). Функция ShowMultimap() подобна функции ShowMapO, использовавшейся ранее. Функция ShowMultimapRange() использует функцию equa!_range() для получения первого и последнего элементов, которые имеют одинаковые ключевые значения Все квалифи- квалифицированные элементы отображаются с помощью итератора. Контейнер-набор Набор также подобен записи. Разница заключается в том, что в наборе нет пар: ключ/значение. Вместо этого в наборе элемент содержит только ключ. Шаблонный класс set определен в файле <set> ь namespace std. Оператор индексирования [] здесь неуместен. Если вы знаете ключ, то уже знаете к значение. Класс set имеет функции-члены, почти идентичные тем, которые имеются м в классе мар, за исключением того, что в классе set нет оператора индексирования []. Тип значения — это просто сам ключ, а значение compare — это просто класс Compare. Следующий программный код представляет функции доступные t контейнер- контейнерном классе set template<cias$ Key, class Compare = less<Key>, class A = Allocator<Key» class set { public: // другие функции-члены typedef Key value_type; // в противоположность паре <const Key T> typedef Compare value_compare; Множественные наборы Множественный набор представляет собой набор, который допускает использование дублирующихся ключей. Он также определен в файле <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 types v) ; Вопросы производительности Контейнеры библиотеки STL сконструированы так, чтобы удовлетворять различным требованиям раз- разработки приложений. Последовательные контейнеры лучше всего подходят для последовательного и произ- произвольного доступа к элементам, выполняемого с использованием оператора индексирования [] и/или итераторов. Связанные контейнеры оптимизированы для обеспечения произвольного доступа к их элемен- элементам по ключевым значениям. В этом разделе представлен краткий обзор производительности некоторых стан- стандартных контейнерных операций. В табл. 6.1 показаны некоторые распространенные контейнерные операции и их сложности. Таблица 6.1. Стандартные контейнерные операции Операция vector list deque тар multimap set multiset Конструкторы Пустой контейнер с элементами Деструктор begin(), end(), rbegin(), rend() Операции с начальными элементами Операции с конечными элементами Модификаторы П at() Сравнение 0A) 0(п) 0A) 0A) 0A) 0A) 0A) 0A) 0A) 0A) 0(п) 0A) 0A) 0(п) 0(п) 0A) 0(log(n)) 0(Юд(п)) 0(п) 0(log(n)) 0(log(n)) 0(Юд(п)) Производительность операции часто определяется ее сложностью, т.е. время выполнения сравнимо с числом элементов, вовлеченных в операцию. Постоянная сложность, обозначенная в табл. 6.1 как 0A), означает, что время выполнения не зависит от количества вовлеченных элементов. Логарифмическая слож- сложность, обозначенная как 0(log(n)), означает, что время выполнения пропорционально логарифму значе- значения числа вовлеченных элементов. Линейная сложность, обозначенная как 0(п), означает, что время выполнения пропорционально количеству вовлеченных элементов. При возникновении потребности в стандартном контейнерном классе необходимо принять решение о том, какой из доступных контейнеров лучше подходит для приложения. При этом следует задаться двумя вопросами: ¦ Какие контейнеры предлагают требуемую функциональность? Например, если доступ к элементам должен производиться по ключу, то можно воспользоваться лю- любым из связанных контейнеров. ¦ Какой из подходящих контейнеров наиболее эффективен? Если доступ к элементам всегда последователен, то последовательные контейнеры имеют преимуще- преимущество перед связанными. Более того, если ожидается частая вставка в середину последовательности элемен- элементов, то лучшим выбором является контейнер list. Использование стандартной библиотеки C++ Контейнерные классы библиотеки STL являются частью Standard C++ Library. Они определены в namespace std. Программы должны ссылаться на эти классы одним из двух способов. Первый способ ссылки на контейнерные классы STL заключается в использовании директивы using namespace для указания намерения применять библиотеку Standard C++ Library. В программе должен при- присутствовать оператор:
Контейнерные классы библиотеки STL Глава 6 using namespace std; Этот оператор разрешает компилятору использование имен классов и методов, выполняя поиск в namespace std. Все примеры программ используют этот метод. Второй метод ссылки на классы STL заключается в явной квалификации класса ключевым словом std, как показано в следующем примере: std::vector<int> vlnt; Вы должны также использовать директиву #include для включения в программу уместного header-файла. Например, для использования контейнерного класса vector необходим следующий оператор: #include <vector> В табл. 6.2 приведены header-файлы для каждого контейнерного класса. Таблица 6.2. Заглавные файлы контейнерных классов. Класс Header-файл vector list deque stack queue priorityqueue map multimap set multiset <vector> <list> <deque> <stack> <queue> <queue> <map> <map> <set> <set> Конструирование типов элементов Контейнерные классы библиотеки STL сконструированы для хранения любых объектов. Вы видели, что многие методы контейнерных классов STL требуют, чтобы элементы могли выполнять конкретные опера- операции: ¦ Класс элемента должен иметь конструктор по умолчанию. Конструктор по умолчанию вызывается тогда, когда объект контейнера создается в следующей форме: vector<MyClass> vA00); Этот оператор создает вектор, содержащий 100 объектов MyClass, созданных с помощью MyClass::MyClass(). ¦ Класс элемента должен иметь конструктор копии. Конструктор копии вызывается тогда, когда к контейнеру добавляются новые элементы, как пока- показано в следующем примере: MyClass myObject; vector<MyClass> vA00, myObject); // вызывается конструктор копии vector<MyClass> v; v.push_back(myObject); // вызывается конструктор копии ¦ Класс элемента должен иметь оператор присваивания (=). Оператор присваивания вызывается тогда, когда элементу присваивается новое значение, как пока- показано в следующем примере: MyClass myObject; vector<MyClass> vA00); v[l] = myObject // вызывается оператор присваивания При использовании контейнерных классов, которые требуют сравнения значений своих элементов, следует определить перегруженные операторы сравнения. Если их не определить, то нужно исполь- использовать специализированный класс сравнения и явно указать его в определении контейнера. Основ-
Вопросы реализации Часть II ными операторами сравнения являются == и <. Эти операторы можно использовать для определе- определения других операторов, включая <=, >=, !=, что было показано при рассмотрении контейнерного класса vector. После того как перегруженные операторы == и < определены, можно предпринять шаги по порожде- порождению остальных операторов сравнения, используя файл <utility> библиотеки Standard C++ Library, кото- который обеспечивает реализацию шаблонного оператора сравнения на базе операторов == и <. ПРИМЕЧАНИЕ О реализации компилятором библиотеки STL можно узнать из документации по компилятору. Резюме Библиотека Standard Template Library (STL) предлагает несколько контейнерных классов, которые по- помогают программистам выполнять распространенные функции по управлению структурами данных. Реали- Реализация стандартных контейнеров компилятором гарантированно будет удовлетворять стандарту C++. Стандартные контейнеры обеспечивают переносимость приложений и повышают вероятность привлечения наибольшего возможного числа пользователей. Для многих приложений контейнерные классы библиотеки STL обеспечивают удовлетворительную функциональность и сокращают затраты на кодирование и отладку. В целях обеспечения данного преимущества следует корректно использовать контейнерные классы. Контей- Контейнер необходимо выбрать так, чтобы он лучше удовлетворял требованиям приложения. В общем случае кон- контейнеры обеспечивают оптимальный последовательный и произвольный доступ к элементам с помощью индексирования и итераторов. Связанные контейнеры обеспечивают прямой доступ к элементам по ключе- ключевым значениям. Вы обнаружите, что часто нуждаетесь в использовании стандартных контейнеров для хранения своих объектов. Такие объекты должны удовлетворять ряду требований и ограничений, которые обусловливаются выбранным контейнером. Функции-члены, такие как операторы сравнения == и <, могут оказать большое влияние на производительность выбранного контейнера и самого приложения. Убедитесь в том, что реали- реализуете контейнер самым эффективным и надежным способом.
Итераторы и алгоритмы STL В ЭТОЙ ГЛАВЕ Классы итераторов Объекты-функции Алгоритмы STL
Вопросы реализации Часть! Классы итераторов При обсуждении контейнерных классов STL в главе 6 мы использовали итераторы так, как будто это указатели на элементы контейнерных классов STL, и обращали внимание на то, что итераторы можно разадресовать, чтобы оценить элементы. В этой главе мы предпримем попытку оценить классы итераторов (итераторные классы). Классы итераторов определены в файле <iterator> в namespace std. Итераторы представляют собой абст- абстракции или обобщения указателей, т.е. они реализуют все операции указателей. Все итераторные операции имеют тот же эффект, что и соответствующие операции с указателями. Позиция внутри контейнера Контейнер хранит коллекцию объектов одного типа. Как именно хранятся элементы, зависит от кон- контейнера. Например, элементы в векторе хранятся в последовательных блоках памяти. Контейнер-список хранит элементы в любой доступной памяти. Каждый стандартный контейнер организовывает свои элементы та- таким образом, чтобы элементы можно было получить от начала коллекции к концу. На рис. 7.1 показаны позиции элементов в контейнере. РИСУНОК 7.1. Элементы в контейнере. begin() end() Element 0 м Bement 1 Element 2 Bement size_of sequence ¦ 1 pass_the_word Доступ к элементам происходит через итератор. Если итератор указывает на элемент п, то его можно разадресовать, чтобы получить элемент п. Итератор можно нарастить для указания на элемент п+1 или уменьшить для указания на элемент п-1. Когда итератор оценивается функцией end(), то он не указывает ни на какой элемент. Для итератора неприменимо значение NULL. Говорят, что итератор действителен, если он указывает на элемент или на конец последовательности (end()). Итератор недействителен, если он не инициализирован или если последовательность изменила размеры. Типы итераторов контейнеров Существует пять категорий итераторов. Они представлены в иерархии на рис. 7.2. Этот рисунок не показывает диаграмму наследования классов — он представляет уровень функциональ- функциональности, обеспечиваемый разными категориями итераторов. Вводной и выводной итераторы обеспечивают наиболее ограниченную функциональность. Они могут использоваться только для пересечения последова- последовательностей в один проход в направлении от начала к концу. Проход нельзя повторить, т.е., если пройти через последовательность второй раз, используя вводной или выводной итератор, результат будет, скорее всего, иным. РИСУНОК 7.2. Иерархия итераторов. Вводной (Input) Выводной (Output) <—1 Пересылаемый (Forward) ^ Двунаправленный (Bidirectional) Произвольного доступа (RandomAccess) Пересылаемый итератор обеспечивает все операции вводного и выводного итераторов и ослабляет не- некоторые ограничения, налагаемые ими. Наиболее примечательное отличие заключается в том, что повтор- повторный проход через ту же самую последовательность с помощью пересылаемого итератора породит те же самые результаты. Подобным же образом двунаправленный итератор обеспечивает все пересылаемые опера- операции. Он также предоставляет возможность пересекать последовательность в обратном порядке. И наконец, итератор произвольного доступа обеспечивает все операции двунаправленного итератора и другие методы, характерные для случайного метода доступа. В табл. 7.1 представлены операции, предлагаемые итераторами.
Итераторы и алгоритмы STL Глава 7 Таблица 7.1. Итераторные операции Категория Вводной Выводной Пересылаемый Двунаправленный Произвольного доступа Элемент -> -> -> ->, [] Доступ Чтение = *i = *i = *i = *i Запись *i= *i= *i= *i = Итерации ++ ++ ++ ++, — ,--, +, -, +=, -= Сравнение ==, != ==, != ==, != ==, !=, <, <=, >, >= Базовый класс итераторов Библиотека Standard C++ содержит базовый тип, который можно использовать для создания собствен- собственных итераторных классов: template<class Category, class T, class Distance = ptrdiff_t, class Pointer = T*, class Reference = T?> struct iterator { typedef Category iterator_category; typedef T value_type; typedef Distance di ferrence_type; typedef Pointer pointer; typedef Reference reference; }; Класс 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), поскольку используются только как ярлыки для представления итераторных категорий. На самом деле они не используются ни в одной стандартной итераторной функции-члене. С другой стороны, можно использовать наследование итераторных ярлыков для написания общих алго- алгоритмов итераторов. Например, если функция ожидает вводной итератор, то мы можем передать ей пересы- пересылаемый итератор. Функция должна выполняться без проблем, поскольку этот итератор без проблем выполнит все операции вводного итератора. С учетом важности использования наследования итераторных ярлыков можно ожидать, что forward_iterator_tag будет порожден от input_iterator_tag и the output_iterator_tag, как показано на рис. 7.2. STL, однако, по- порождает forward_iterator_tag только от input_iterator_tag. Причина в том, что выводной и пересылаемый итераторы используются для записи вывода в различные типы контейнеров. Выводные итераторы часто используются для записи значений в неограниченные контейнеры, такие как стандартный вывод. Когда мы пишем в стандартный вывод, то можем выводить любое значение, кото- которое должно быть записано. Нет необходимости проверять, примет ли стандартный вывод то, что мы выво- выводим. Хотя пересылаемые итераторы обеспечивают все выводные итераторные операции, они работают только с ограниченными контейнерами. Пересылаемый итератор должен записать элемент в контейнер. Любая попытка перешагнуть границу генерирует либо ошибку времени компиляции, либо ошибку времени вы- выполнения. Точно так же forward_iterator_tag не порожден от output_iterator_tag. В любом случае на практике редко появляется необходимость писать алгоритмы, которые используют выводные итераторы. Выводные итераторы не имеют типа значения, поскольку невозможно получить зна- значение от выводного итератора. Через него можно только записывать. Кроме того, выводные итераторы не имеют типа дистанции, поскольку невозможно найти дистанцию от одного выводного итератора к другому. Поиск дистанции требует сравнения на равенство, а выводные итераторы не поддерживают оператор ==. Таким образом, существует мало алгоритмов, которые работают на выводных итераторах. Фактически ите- итераторные операции STL distanceQ и advance() вообще непригодны для работы с выводными итераторами.
Вопросы реализации Часть II Следующий тип в итераторном шаблонном классе Т представляет собой тип объекта, указанного итера- итератором. Distance — это знаковый целый тип, представляющий дистанцию между двумя итераторами. По умол- умолчанию это тип ptrdiff_t, определенный в <cstddef>. Указатель является типом указателя, который указывает на value_type итератора. Этот тип является обобщенным типом указателя, возвращаемого перегруженным оператором ->. Ссылка является ссылкой на value_type итератора. Она возвращается перегруженным опера- оператором разадресации *. Пересылаемый итератор можно породить от итератора шаблонного класса: template <class T, class Distance = ptrdiff_t> class forward_iterator : public iterator<forward_iterator_tag, T, Distance> { public: const Ti operator*() const; const T* operator->() const; iterators operator++(); // префикс-инкремент iterator operator++(int); // постфикс-инкремент // другие члены }; Все итераторы, порожденные от базового итераторного шаблонного класса, обладают общим набором атрибутов, включающим iterator_category, value_type и т.д. Библиотека STL обеспечивает iterator_traits шаблонного класса: template<class Itor>struct iterator_traits { typedef typename Itor::iterator_category iterator_category; typedef typename Itor::value_type value_type; typedef typename Itor::difference_type difference_type; typedef typename Itor::pointer pointer; typedef typename Itor::reference reference; >; Использование характерных черт итераторов в итераторных операциях демонстрируется на конкретных примерах далее в этой главе. Вводные итераторы Вводной итератор представляет собой итератор, который должен удовлетворять следующим требованиям: ¦ Быть конструируемым по умолчанию: итератор должен обладать конструктором по умолчанию, так чтобы мог создаваться без инициализации в какое-либо конкретное значение. Когда вводной итера- итератор создается, используя свой конструктор по умолчанию, то это не действительный итератор. Он остается недействительным до тех пор, пока ему не будет присвоено значение. ¦ Быть присваиваемым: итератор должен иметь конструктор копии и перегруженный оператор присваива- присваивания. Эта характеристика предоставляет возможность копировать и присваивать значения итераторам. ¦ Быть сравниваемым на равенство: итератор должен иметь перегруженный оператор равенства == и оператор неравенства !=. Данные операторы позволяют сравнивать два итератора. Вводной итератор представляет собой итератор, который можно разадресовать, когда он указывает на элемент в последовательности. Кроме того, итератор может указывать на конец последовательности. Когда итератор указывает на конец последовательности, то он фактически указывает на место как раз после последнего элемента в последовательности и о нем говорят, что он "past the end". Итератор past-the-end нельзя разадресовать. Вводной оператор действителен, если его можно разадресовать в действительный объект или он past- the-end. Вводной итератор гарантирует доступ на чтение к указываемому объекту. Например, если И являет- является вводным итератором, у которого value_type — это Т, то доступ к указываемому объекту получают путем разадресации: Т t = *ii; // правильно Поскольку вводные итераторы не допускают доступ на запись к указываемым элементам, то следующие операторы содержат ошибку: т t; *ii = t; // ошибка
Итераторы и алгоритмы STL Глава 7 Вводной итератор можно нарастить для указания на следующий элемент. Должны быть определены как префиксный оператор приращения (++И), так и постфиксный оператор приращения (Н++). Вводной ите- итератор можно наращивать, если ++Н имеет место. Итераторы past-the-end наращивать нельзя. Выводные итераторы Выводной итератор должен быть по умолчанию конструируемым и присваиваемым. Выводной итератор гарантирует доступ к записи, но не доступ для чтения. Рассмотрим следующие операторы: т t; *oi = t; // правильно t = *oi; // ошибка Кроме того, выводной итератор определяет и префиксный, и постфиксный операторы приращения. Пересылаемые итераторы Пересылаемый итератор похож на видеоленту — его можно проигрывать в одном направлении, но зато многократно. Он обладает всеми характеристиками вводного итератора, т.е. он по умолчанию конструиру- конструируемый, присваиваемый и сравниваемый на равенство. Кроме того, пересылаемый итератор можно разадре- совывать и наращивать. Пересылаемые итераторы обеспечивают доступ к указываемым объектам как на чтение, так и на запись. Двунаправленные итераторы Двунаправленный итератор похож на пересылаемый, за исключением того, что он может и уменьшаться. Итераторы произвольного доступа Итератор произвольного доступа реализует все операции двунаправленного итератора и добавляет мето- методы для перемещения от одного элемента к другому за постоянное время @A)). Дистанция между исходным элементом и элементом места назначения не важна, т.е. итератор произвольного доступа так же быстр при переходе от первого элемента к тридцать седьмому, как и при переходе от первого элемента ко второму. Поскольку итераторы произвольного метода доступа могут перемещаться более чем на один шаг, то для них определены операторы сложения (+ и +=) и вычитания (- и -=). Итераторы произвольного дос- доступа также определяют оператор индексирования []. Если ri является итератором произвольного доступа, то ri[n] возвращает n-й элемент в последовательности. Итератор произвольного доступа сравним не только на равенство, но и по величине. Следовательно, он определяет операторы <, >, <= и >=. Фундаментальным из них является только оператор <. Все остальные операторы можно породить от него. Итераторные операции В дополнение к перегруженным операторам библиотека STL обеспечивает две функции, которые воз- возвращают количество элементов между двумя элементами и которые "прыгают" с одного элемента на лю- любой другой элемент в контейнере. Функция distance() Функция distance находит дистанцию между текущими позициями двух итераторов, т.е. если первый итератор указывает на элемент 12 в последовательности, а второй на элемент 47 в той же последователь- последовательности, то дистанция будет равна 35. template<class Inputlterator> iterator_traits<lnputlterator>::difference_type distance(Inputlterator first, Inputlterator last) ; Как вычисляется дистанция между двумя итераторами? Для итераторов с произвольным доступом это легко, поскольку они обеспечивают операторы вычитания. Мы можем просто вычесть первый итератор из последнего и получить дистанцию: template<class RandomAccessIterator> i terator_trai ts<RandomAccessIterator>::di fference_type distance(RandomAccessIterator first, RandomAccessIterator last) ( return last — first;
Вопросы реализации Часть II Для других итераторов это несколько усложнятся, поскольку для них не определен оператор вычитания. Необходимо пройти по последовательности от первого итератора до последнего и зарегистрировать коли- количество шагов: template<class lnputlterator> iterator_traits<lnputlterator>::difference_type distance(Inputlterator first, Inputlterator last) { iterator_traits<lnputlterator>::difference_type n = 0; while (first++ != last) ++n; return n; ) Благодаря наследованию итераторных ярлыков, вторая версия функции distance() работает для ввод- вводных, пересылаемых и двунаправленных итераторов. Поскольку выводные итераторы не имеют оператора сравнения !=, то невозможно проверить, являются ли два выводных итератора равными. Поэтому для вы- выводных итераторов функция distance() не определена. Сложность (время выполнения в сравнении с количеством элементов, вовлеченных в операцию) фун- функции distance() зависит от категории итератора. Для итераторов с произвольным доступом это постоянное время @A)), поскольку операция вычитания для итераторов произвольного доступа имеет сложность с постоянным временем. Для других итераторов сложность линейна @(п)). Функция advance() До сих пор мы видели, как перемещать итераторы вперед и назад, используя операторы приращения и уменьшения соответственно. Итераторы произвольного доступа также можно перемещать сразу на несколь- несколько шагов, используя функции сложения и вычитания. Другие типы итераторов, однако, не имеют функ- функций сложения и вычитания. Для перемещения любого итератора, кроме выводных, на несколько шагов одновременно STL предоставляет функцию advance(). template<class Inputlterator, class Distance> void advance(InputlteratorS ii. Distances n) ; В данном синтаксисе Distance представляет собой знаковый целый тип. Дистанция для перемещения п может быть как положительной, так и отрицательной. Если п положительна, то итератор перемещается вперед, в противном случае итератор перемещается назад. Если п равна нулю, то функция никакого эф- эффекта не имеет и перемещения не происходит. Значение п может иметь отрицательное значение только в случае двунаправленного итератора или итератора произвольного доступа. Существует значительная разница между разными категориями итераторов: операции сложения и вычи- вычитания обеспечивает только итератор произвольного доступа. Поэтому функцию advance() можно перегру- перегрузить для итераторов разных категорий: template<class Inputlterator, class Distance> void advance(Inputlterator& ii, Distances n) { while (n—) ++ii; > template<class Bidirectionallterator, class Distance> void advance(Bidirectionallterator & bi, Distances n) { if (n >= 0) while (n—) ++bi; else while (n++) —bi; } template<claas RandomAccessIterator, class Distance> void advance(RandomAccessIteratorS ri. Distances n) { ri += n ; } Поскольку выводные итераторы предоставляют только возможность записи в их типовые значения, то должен иметь место пошаговый проход их. Оператор приращения предназначен именно для этой цели. Для выводных итераторов не существует необходимости в функции advance(). Подобно функции distance(), сложность advance() зависит от категории итератора. Функция advance() имеет постоянное время для итераторов с произвольным доступом и линейную сложность для других ите- итераторов.
Итераторы и алгоритмы STL Глава 7 Классы стандартных итераторов Библиотека STL содержит множество классов итераторов, которые выполняют определенные общие итеративные операции. Данные классы описаны в последующих разделах этой главы. Класс istreamjterator Шаблонный класс istream_iterator определен в <iterator> и является вводным итератором, который выполняет форматированный ввод объектов заданного типа из istream. Класс istream_iterator можно опре- определить следующим образом: template <class T, class Distance = ptrdiff_t> class istream_iterator : public input_iterator<T, Distance> { public: typedef input_iterator_tag iterator_category; typedef T value_type; typedef Distance difference_type; typedef const T* pointer; typedef const TS reference; const TS const T* istream_iteratorS istream_iterator // другие члены operator*{) const; operator-:» () const; operator++() ; // префикс-инкремент operator++(int); // постфикс-инкремент ПРИМЕЧАНИЕ Приведенное определение не является стандартным определением шаблонного класса 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> int il = *ii; cout « "il = " « il « "\n"; ii(cin) cout « "Ready for another integer: int i2 = *++ii; cout « "i2 = " « i2 « "\n"; return 0; Ниже приведен вывод, сгенерированный программой из листинга 7.1: Enter an integer: 6 11 = 6 Ready for another integer: 2 12 = 2 Итератор istream ii создается для приема ввода из стандартного ввода cin. Второй шаблонный параметр — это базовый тип istream. Первое введенное целое присваивается il с помощью оператора разадресации *. Для приема следующего ввода итератор должен наращиваться.
Вопросы реализации Часть II Класс ostreamjterator Шаблонный класс ostream_iterator также определен в <iterator> и является выводным итератором, который выполняет форматированный вывод объектов заданного типа в ostream. Класс ostream_iterator можно определить следующим образом: template <class T> class ostream_iterator : public output_iterator { public: typedef output_iterator_tag iterator_category; typedef void value_type; typedef void difference_type; typedef void pointer; typedef void reference; ostream_iteratorS operator*(); ostream_iterator& opera tor++(); ostream_iteratorS operator++(int); ostream_iterator& operator=(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. В манере, сходной с той, как работает istream_iterator, второй параметр указывает базовый тип 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; template<class T> class Print {
Итераторы и алгоритмы STL Глава 7 public: void operator () (TS t) { cout « t « "\n"; } int main() Print<int> DoPrint; for (int i = 0; i < 5; ++i) return 0; DoPrint (i); Ниже приведен вывод программы из листинга 7.3: О 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 Arg argument_type; typedef Result result_type; template<class Argl, class Arg2, class Result> struct binary_function { typedef Argl first_argument_type; typedef Arg2 second_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 T> class Print: public unary_function<T, void> { Предикаты Когда тип возвращаемого значения унарного объекта-функции — bool, то функция называется унарным предикатом. Бинарный (двоичный) объект-функция, который возвращает значение boo!, называется би-
Вопросы реализации equaljo not_equal_to greater greaterequal less less_equal logical_and logical_or logical not двоичный двоичный двоичный двоичный двоичный двоичный двоичный двоичный унарный Часть II парным предикатом. Библиотека Standard C++ Library определяет в <functional> несколько распространен- распространенных предикатов. Эти предикаты перечислены в табл. 7.2. Таблица 7.2. Предикаты, определенные в <functional> Функция Тип Описание arg1 == arg2 arg1 != arg2 arg1 > arg2 arg1 >= arg2 arg1 < arg2 argi <= arg2 arg1 && arg2 argi 11 arg2 !arg1 Предикат equal_to определяется следующим образом: template<class T> struct equal_to : binary_funotion<T, T, bool> { bool operator<)(T4 argi, T6 arg2) const { return argi == arg2; } }; Для сравнения двух значений типа Т следует определить перегруженный оператор ==, используя пре- предикат equal_to. Оператор == можно определить либо как унарную функцию-член класса Т, либо как пере- перегруженный двоичный оператор-функцию, как показано ниже: bool Т::operator==()(Т4 t) // функция-член класса Т template<class T> // общий оператор сравнения bool operator==(T4 argi, T4 arg2); Все другие предикаты определяются подобным образом и имеют сходные требования. Арифметические функции Арифметические функции выполняют гнкции, определенные Standard C++ L: Таблица 7.3. Арифметические функции, определенные в <functional> Функция Тип Описание plus двоичный argi + arg2 minus двоичный argi — arg2 multiplies двоичный argi * arg2 divides двоичный argi / arg2 modulus двоичный argi % arg2 negate унарный -argi Двоичные арифметические функции принимают два аргумента одного типа и возвращают значение та- такого же типа. Например, функция plus() определена следующим образом: template <class T> struct plus : binary_function<T,T,T> { T operator()(const T4 x, const T4 у) const; }; Функция negate() определена как унарная: template <class T> struct negate : unary_function<T,T> Арифметические функции выполняют над объектами арифметические операции. В табл. 7.3 перечислены функции, определенные Standard C++ Library в <functional>.
Итераторы и алгоритмы STL Глава 7 Т operator()(const Ts x) const; Алгоритмы STL Контейнер представляет собой полезное место для хранения последовательности элементов. Все стан- стандартные контейнеры определяют операции, которые манипулируют контейнерами и их элементами. Одна- Однако реализация всех этих операций в ваших последовательностях требует скрупулезной работы и часто чревата ошибками. Поскольку большинство таких операций для множества последовательностей, скорее всего, будут одинаковы, набор общих алгоритмов может сократить потребность в написании собственных операций для каждого контейнера. Библиотека Standard C++ Library представляет около 60 стандартных алгоритмов, которые выполняют большинство базовых и наиболее часто применяемых операций для контейнеров. Стандартные алгоритмы определены в <algorithm> в namespace std. Операции немутирующих последовательностей Операции немутирующих последовательностей используются для извлечения значений или позиций элементов в последовательном контейнере. Последовательность идентифицируется парой итераторов (first, last). First указывает на первый элемент в последовательности, и last является итератором past-the-end. Операция for each() Операция for_each() выполняет унарную операцию над каждым элементом в последовательности. template<class Inputlterator, class UnaryFunction> FunctionObject for_each(Inputlterator first, Inputlterator last, UnaryFunction f); Функция f должна быть унарной и выполнять доступ к элементам только на чтение. Операция find() Операция find() проверяет последовательность с целью найти ее первый элемент, который равен за- заданному значению. й,. template<class Inputlterator, class T> Inputlterator find(Inputlterator first, Inputlterator last, const Ts value); Если элемент найден, то find() возвращает итератор, указывающий на элемент. В противном случае возвращается итератор last. Операция find_if() Операция find_if() проверяет последовательность с целью найти ее первый элемент, который при пере- передаче предикату оценивает предикат как true. template<class Inputlterator, class UnaryPredicate> Inputlterator find_if(Inputlterator first, Inputlterator last, UnaryPredicate pred); Подобно find(), операция find_if() возвращает итератор, указывающий на найденный элемент. Если элемент не найден, то возвращается итератор last. Операция count() С помощью операции count() подсчитывается количество элементов в последовательности, которые равны заданному значению. template<class Inputlterator, class T> iterator_traits<lnputlterator>::difference_type count(Inputlterator first, Inputlterator last, const TS value); Элементы в последовательности сравниваются с заданным значением с помощью перегруженного опе- оператора сравнения ==. Возвращается количество квалифицированных элементов. Операция count_if() С помощью операции count_if() подсчитывается количество элементов в последовательности, которые равны заданному значению. Проверка на равенство использует внешний двоичный предикат.
Вопросы реализации Часть II template<class Inputlterator, class BinaryPredicate> iterator_traits<lnputlterator>::difference_type count_if(Inputlterator first, Inputlterator last, BinaryPredicate pred) ; Подобно count(), операция count_if() возвращает количество квалифицированных элементов. В листинге 7.4 демонстрируются операции find() и count(). Лиаинг 7.4. Использование операций findQ и 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_funotion<T, bool> { public: bool operator()(T6 argl) { return (argl > 2) ; template<class Container, class Iterator> void ShowElement(Container4 c, Iterators itor); int main() Print<int> DoPrint; vector<int> vlnt(VectorSize); typedef vector<int>::iterator Itor; for (int x = 0; i < VectorSize; ++i) vlnt[i] = i; Itor first = vlnt.begin(); Itor last = 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";
Итераторы и алгоритмы STL Глава 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() определена для печати элемента, указываемого итератором itor в последовательности с. Если итератор является past-the-end, то он отображает строку last. В функции main() определен объект класса 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_flnd(). Оператор первой версии сравнивает элементы с помощью перегруженного оператора ==: template<class Forwardlterator> Forwardlterator adjacent_find(ForwardIterator first, Forwardlterator last); Оператор второй версии adjacent_find() сравнивает элементы с помощью внешнего двоичного предика- предиката. Если предикат оценивает значением true оба смежных элемента, то они считаются равными: template <class Forwardlterator , class...BinaryPredicate> Forwardlterator adjacent_find(Forwardlterator first, Forwardlterator last, BinaryPredicate binary_pred);
Вопросы реализации Часть II Операторы обеих версий возвращают итератор, указывающий на первый элемент в паре, если такая пара элементов найдена. В противном случае возвращается итератор last. Операция search_n() Операция search_n() выполняется для поиска заданного количества последовательных элементов пос- последовательности. Все элементы должны быть равны заданному значению. Операция имеет две версии, ко- которые отличаются тем, как проверяется равенство элементов: template<class Forwardlterator, class Size, class T> Forwardlterator search_n(Forwardlterator first, Forwardlterator last, Size count, const T& value); template<class Forwardlterator, class Size, class T, с1азз BinaryPredicate> Forwardlterator search_n(Forwardlterator first, Forwardlterator last, Size count, const T4 value, BinaryPredicate pred); Если search_n() находит количество count последовательных элементов в последовательности [first, last], то она возвращает итератор, указывающий на первый элемент. В противном случае возвращается итератор last. В листинге 7.5 показано применение операций adjacent_find() и search_n(). Листинг 7.5. Использование операций adjacentfindQ и searchnQ #include <iostream> #include <vector> ¦include <iterator> #include <functional> #include <algorithm> using namespace std; const int VectorSize = 10; template<class T> class EqualToThree: public binary_function<T, T, bool> { public: bool operator () (T& argl, T4 arg2) { return (argl == arg2) 44 (argl == 3) ; } >; template<class Container, class Iterator> void ShowElement(Containers c, Iterators itor); int main() { vector<int> vlnt(VectorSize); typedef vector<int>::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.endQ ; Itor retltor = adjacent_find(first, last); cout « "adjacent_find(first, last) = "; ShowElement(vlnt, retltor); cout « "\n"; EqualToThree<int> IsEqualToThree;
Итераторы и алгоритмы STL Глава 7 retltor = adjacent_find(first, last, IsEqualToThree) cout « "adjacent_find(first, last, IsEqualToThree) : ShowElement(vInt, retltor); cout « "\n"; retltor = search_n(first, last, 3, 4); cout « "search_n(first, last, 3, 4) = "; ShowElement(vInt, retltor); oout « "\tprevious element is "; ShowElement(vlnt, —retltor); cout « "\n"; return 0; template<class Container, class Iterator> void ShowElement(Containers c, Iterators itor) { if (itor != o.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() С помощью операции fmd_first_of() проверяется последовательность с целью найти первое появление элемента, идентичного любому из элементов в другой последовательности. Существует две версии find_first_of(): при выполнении операции первой версии для проверки элементов на равенство использует- используется перегруженный оператор ==, а при выполнении второй — внешний двоичный предикат: template <class Inputlterator, class Forwardlterator> Inputlterator find_first_of(Inputlterator firstl, Inputlterator lastl, Forwardlterator first2, Forwardlterator Iast2); template <class Inputlterator, class Forwardlterator, class BinaryPredicate> Inputlterator find_first_of(Inputlterator firstl, Inputlterator lastl, Forwardlterator first2, Forwardlterator Iast2, BinaryPredicate comp); Если такая субпоследовательность найдена, то возвращается итератор, указывающий на первый эле- элемент в субпоследовательности. Если при выполнении операции find_first_of() не может быть найдена суб- субпоследовательность, то возвращается итератор lastl. Операция search() Существует две перегруженные операции search(), которые проверяют последовательность [firstl, lastl) для поиска первого появления субпоследовательности, которая идентична другой последовательности [first2, last2). Как и две версии find_flrst_of(), две версии search() используют для проверки элементов на равен- равенство либо перегруженный оператор ==, либо внешний двоичный предикат. template <class Inputlterator, class Forwardlterator> Inputlterator search(Inputlterator firstl, Inputlterator lastl,
Вопросы реализации Часть II Forwardlterator first2, Forwardlterator Iast2); template <class Inputlterator, class Forwardlterator, class BinaryPredicate> Inputlterator search(Inputlterator firstl, Inputlterator lastl, Forwardlterator first2, Forwardlterator Iast2, BinaryPredicate comp); Если нужная субпоследовательность найдена, то возвращается итератор, указывающий на первый эле- элемент в субпоследовательности. Если при выполнении операции find_flrst_of() не может быть найдена нуж- нужная субпоследовательность, то возвращается итератор lastl. Операция find_end() Операция find_end(), наверное, названа так ошибочно. Она больше похожа на операции search(), чем на операцию find(). Как и search(), fmd_end() проверяет последовательность [firstI, lastl) в поиске суб- субпоследовательности, которая идентична другой последовательности [first2, last2). Разница заключается в том, что find_end() ищет последнее появление субпоследовательности. template<class Forwardlteratorl, class Forwardlterator2> Forwardlteratorl find_end(Forwardlteratorl firstl, Forwardlteratorl lastl, Forwardlterator2 first2, Forwardlterator2 Iast2); template<class Forwardlteratorl, class Forwardlterator2, class BinaryPredicate> Forwardlteratorl find_end(Forwardlteratorl firstl, Forwardlteratorl lastl, Forwardlterator2 first2, ForwardIterator2 Iast2, BinaryPredicate pred); Как и версии search(), две версии find_end() различаются тем, как они сравнивают элементы в после- последовательностях. В листинге 7.6 показано, как работают функции поиска. Листинг 7.6. Использование других функций поиска #include <iostream> tinclude <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> vlntlA0); vIntl[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> vlnt2C); vInt2[0] = 1; vlnt2[l] = 2; vlnt2[2] = 3; Itor firstl = vlntl. begin (); Itor lastl = vlntl.end() ; Itor first2 = vlnt2.begin() ; Itor Iast2 = vlnt2.end(); Itor retltor = find_first_of(firstl, lastl, first2, Iast2); cout « "find_first_of(firstl, lastl, first2, Iast2) = " ; ShowElement(vlntl, retltor);
Итераторы и алгоритмы STL Глава 7 oout « "\п"; retltor = search(firstl, lastl, first2, Iast2); oout « "search(firstl, lastl, first2, Iast2) = ShowElement(vlntl, retltor); cout « "\n"; retltor = find_end(firstl, lastl, first2, Iast2); cout « "find_end(firstl, lastl, first2, Iast2) = ShowElement(vlntl, retltor); cout « "\n"; return 0; } template<class 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, Iast2) = 1 the previous element is 0 search(firstl, lastl, first2, Iast2) = 1 the previous element is 1 find_end(firstl, lastl, first2, Iast2) = 1 the previous element is 4 Шаблонная функция ShowElement() здесь немного изменена, с тем чтобы показать не только текущий элемент, но и предыдущий, что позволит увидеть позицию элемента в последовательности с помощью операции find_first_jf() ищется второй элемент в векторе vlntl. С помощью операции search() ищется тре- третий элемент, поскольку это первый элемент первой субпоследовательности A, 2, 3) в vlntl. При выполнении операции find_end() находят седьмой элемент, который является первым в последней субпоследовательно- субпоследовательности A, 2. 3) в vlntl. Операция equalQ Операция 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); Вторая последовательность начинается с 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(Container4 c, Iterator& itor); int main() { typedef vector<int>::iterator Itor; vector<int> vlntl D); vIntl[0] = 1; vlntl[l] = 2; vlntl[2] = 3; vlntl[3] = 4 vector<int> vlnt2C) 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:
Итераторы и алгоритмы STL Глава 7 vlntl != vlnt2 First mismatch element in vlntl = 4 the previous element is 3 First mismatch element in vlnt2 = last В векторе vlntl есть четыре элемента, в vlnt2 — три элемента. Поскольку их размеры не совпадают, то операцией equelQ возвращается значение false. При выполнении операции mismatch() возвращается пара первых несовпадающих элементов в обоих векторах. Алгоритмы мутирующих последовательностей С помощью операций мутирующих последовательностей выполняются действия, которые меняют эле- элементы в последовательности. Операция fi!l() Операция ffll() обеспечивает присваивание значения каждому элементу в последовательности: template<class Forwardlterator, class T> void fill(Forwardlterator first, Forwardlterator last, const Ts value); Функцию Ш1() можно определить следующим образом: for (Forwardlterator fi = first; fi != last; ++fi) { *fi = value; > Таким образом, класс Т должен быть преобразуем в тип значения Forwardlterator, с тем чтобы опера- оператор присваивания был действительным. Операция fiil_n() Операция flll_n() похожа на fill(), за исключением того, что в результате ее выполнения присваивается значение только первым п элементам в последовательности [first, last): template<class Outputlterator, class Size, class T> void fill_n(Outputlterator first. Size n, const T6 value); Поскольку операцией fill() выполняется точно n присваиваний, то [first, first + n) должна быть дей- действительной последовательностью. Операция generate() Операция generate() похожа на операцию fill(), но при ее выполнении каждому элементу в последова- последовательности присваивается результат генератора, а не значение. template<class Forwardlterator, class 6enerator> void generate(Forwardlterator first, Forwardlterator last, Generator f) ; Возвращаемое значение функции f должно быть преобразуемо в тип элемента. Операция generate_n() Операция generate__n() подобна операции fill_n(), но при ее выполнении первым п элементам последо- последовательности присваивается результат генератора, а не значение. template<class Outputlterator, class Size, class Generator> void generate(Outputlterator 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 () (TS argl) { cout « argl « " " ; int main() Print<int> typedef typedef Vectorlnt Itor Itor DoPrint; vector<int> Vectorlnt:: vlntlA0) firstl = lastl = Vectorlnt; iterator Itor; vlntl.begin(); 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): 8 8 8 8 8 8 8 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. Операция partitionQ Операция partition() обеспечивает реорганизацию порядка элементов в последовательности так, чтобы элементы, которые удовлетворяют унарному предикату, размещались перед теми элементами, которые не удовлетворяют предикату. template<class Bidirectionallterator, class UnaryPredicate> Bidirectionallterator partition(Bidirectionallterator first,
Итераторы и алгоритмы STL Глава 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_shuffle() случайно реорганизует порядок элементов в последовательности: template<class RandomAccessIterator> void random_shuffle(RandomAccessIterator first, RandomAccessIterator last); template<class RandomAccessIterator, class RandomNumber6enerator> void random_shuffie(RandomAccessIterator first, RandomAccessIterator last, RandomNumberGeneratori 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 () (TS argl) < cout « argl « " " ; ) }; template<class T> class PartitionPredicate: public unary_function<T, bool> I public: bool operator () (TS argl) < return (argl < 8) ; int main () Print<int> typedef typedef DoPrint; vector<int> Vectorlnt::iterator VectorInt; I tor;
Вопросы реализации Часть II Vectorlnt vlntl A0); for (int i = 0; i < 10; i++) vlntl[i] = i; Itor firstl = vlntl. begin (); Itor lastl = vlntl.end() ; random_shuffie(firstl, lastl); cout « "vlntl after random_shuffie(firstl, lastl):\n"; for_each(firstl, lastl, DoPrint); cout « "\n\n"; Vectorlnt vlnt2 = vlntl; Itor first2 = vlnt2 .begin (); Itor Iast2 = vlnt2.end(); PartitionPredioate<int> pred; partition(firstl, lastl, pred); cout « "vlntl after partition(firstl, lastl, pred):\n"; for_eaoh(firstl, lastl, DoPrint); oout « "\n\n"; stable_partition(first2, Iast2, pred) ; oout « "vlnt2 after stable_partition(first2, Iast2, pred):\n"; for_each(first2, Iast2, 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, Iast2, pred): 4302675189 Унарный предикат объекта-функции класса Partition Predicate определяется для того, чтобы проверить, меньше ли аргумент значения 8. Этот объект-функция используется операциями partition() в целях органи- организации элементов вектора. Функция random shuffle() вызывается для переупорядочивания элементов векто- вектора. Результирующий вектор копируется в vlnt2, так чтобы мы могли сравнить результаты операций partition() и stable_partition(). Когда для vlntl выполняется операция partition(), исходный порядок элементов вектора не сохраняется. С другой стороны, операция stable_partition() не изменяет упорядоченность элементов в каждой группе. Операция transform() Операция transform() выполняется над каждым элементом в последовательности, а результат копирует- копируется в другую последовательность. Существует две перегруженные функции transform(). Функция первой вер- версии выполняет унарную операцию: template<class Inputlterator, class Outputlterator, class UnaryOperation> Outputlterator transform(Inputlterator first, Inputlterator last, Outputlterator result, UnaryOperation f); Функция второй версии выполняет бинарную операцию над двумя последовательностями и копирует результат в третью: template<class Inputlteratorl, class Inputlterator2, class Outputlterator, class BinaryOperation> Outputlterator transform(Inputlteratorl firstl, Inputlteratorl lastl, Inputlterator2 first2, Outputlterator result, BinaryOperation binary_op); В обеих версиях элементы исходной последовательности не изменяются.
Итераторы и алгоритмы STL Глава 7 Операция сору() С помощью операции сору() элементы копируются из одной последовательности в другую. template<class Inputlterator, class Outputlterator> Outputlterator copy(Inputlterator first, Inputlterator last, Outputlterator dest); Элементы в последовательности [first, last) копируются в последовательность, начинающуюся в dest. Элемент *first копируется в *dest, *(flrst+l) to *(dest+l) и т.д. При выполнении операции сору() возвра- возвращается итератор dest+(last-flrst), при этом значение dest не должно быть в диапазоне [flrst,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> Outputlterator reverse_copy(Bidirectionallterator first, Bidirectionallterator last, Outputlterator result); После операции исходная последовательность остается неизменной. Операция rotate() Операция rotate() обеспечивает прокрутку элементов в последовательности. template<class Forwardlterator> void rotate(Forwardlterator first, Forwardlterator middle, Forwardlterator last); После выполнения этой операции все элементы "протягиваются" вперед в начало последовательности на middle-first позиций. Элементы, выталкиваемые с первых позиций, проталкиваются в конец последова- последовательности. Операция rotate_copy() Операция rotate_copy() обеспечивает прокрутку элементов в одной последовательности и копирование результата в новую последовательность. template<class Forwardlterator, class Outputlterator> Outputlterator rotate_copy(Forwardlterator first, Forwardlterator middle, Forwardlterator last, Outputlterator result); Исходная последовательность после операции остается неизменной. В листинге 7.10 проиллюстрированы некоторые из операций копирования.
Вопросы реализации Часть II Листинг 7.10. Копирование последовательности элементов, путем выполнения операций копирования #include <iostream> ¦include <veotor> ¦include <functional> ¦include <algorithm> using namespace std; template<class T> class Print: public unary_function<T, void> { public: void operator () (T& argl) { cout « argl « " " ; template<class T> class Square: public unary_function<T, T> { public: T operator () (TS argl) { return argl * argl; template<class Container> void ShowElements(Container& c, char* text) ; int main() { typedef vector<int> Vectorlnt; typedef Vectorlnt::iterator Itor; Square<int> Sqr; // создайте вектор целых Vectorlnt vlntl E); Itor firstl = vlntl. begin (); Itor lastl = vlntl.end(); for (int i = 0; i < 5; ++i) vlntl [i] = 3 * i; ShowElements(vlntl, "vlntl created"); // создайте вектор для хранения результатов Vectorlnt vlnt2G); Itor first2 = vlnt2. begin (); Itor Iast2 = vlnt2.end() ; fill(first2, Iast2, -1); ShowElements(vlnt2, "vlnt2 created"); // трансформируйте vlntl в vlnt2 transform(firstl, lastl, first2 + 1, Sqr); ShowElements(vlnt2, "Transformed vlntl to vlnt2"); // копируйте Vlntl обратно в vlnt2 fill(first2, Iast2, -1) ; copy_backward(firstl, lastl, Iast2 - 1) ; ShowElemants(vlnt2, "vlntl copied backwards to vlnt2"); // переупорядочивание vlntl в vlnt2 fill(first2, Iast2, -1) ; rotate_copy(firstl, firstl + 1, lastl, first2 + 1) ; ShowElements(vlnt2, "rotate_copy vlntl to vlnt2"); // обращение vlnt2 reverse(first2, Iast2); ShowElements(vlnt2, "reversed vlnt2");
Итераторы и алгоритмы STL Глава 7 return 0; } teraplate<class Container> void ShowElements(Containers с, 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. Результат копируется в vlnt2, начиная со второго элемента. Затем выполняется обратная копия элементов из vlntl в vlnt2. Элементы копируются в vlnt2 со второго элемента last. Затем элементы в vlntl прокручиваются и копируются в vlnt2, начиная со второго элемента. Точка раз- разрыва установлена во второй элемент со значением 3. Он становится первым элементом в последовательно- последовательности, и все следующие элементы перемещаются влево на одно место. Теперь первый элемент становится последним. Наконец, порядок элементов в vlnt2 реверсируется. Операция replace() С помощью операции replace() на новое значение заменяются все элементы последовательности, кото- которые равны указанному значению. template<class Forwardlterator, class T> void replace(Forwardlterator first, Forwardlterator last, const TS old_value, const TS new_value); Операция replace() реализована следующим образом: for (Forwardlterator fi = first; fi != last; ++fi) { if (*fi == old_value) *fi = new_value; } Операция replace_if() Операция replace_if() похожа на операцию replace(), но при ее выполнении не проверяется каждый элемент на значение, а проверяется, заставляет ли элемент унарный предикат вернуть значение true. template<class Forwardlterator, class Predicate, class T> void replace_if(Forwardlterator first, Forwardlterator last. Predicate pred, const TS new_ value); Операция replace_if() может быть реализована следующим образом:
Вопросы реализации 1У Часть II for (Forwardlterator fi = first; fi != last; ++fi) { if (pred(*fi) == true) *fi = new_value; } Операция replace_copy() При выполнении операции replace_copy() все элементы копируются из одной последовательности в другую и выполняется операция replace() над результирующей последовательностью. template<class Inputlterator, class Outputlterator, class T> Outputlterator replace_copy(Inputlterator first, Inputlterator last, Outputlterator result, const TS old_value, const TS new_value); Операция replace_copy_if() Операция replace_copy_if() обеспечивает копирование всех элементов из одной последовательности в другую и выполнение над результирующей последовательностью операции replace_if(). template<class Iterator, class Outputlterator, class Predicate, class T> Outputlterator replace_copy_if(Iterator first. Iterator last, Outputlterator result. Predicate pred, const T4 new_value); Операция remove() Операция remove() удаляет те элементы из последовательности, которые равны указанному значению. template<class Forwardlterator, class T> Forwardlterator remove(Forwardlterator first, Forwardlterator last, const T& value); Если удаляются п элементов, то remove() возвращает итератор last-n. Размер последовательности не меняется. Последние п элементов все еще можно разадресовать, но их значения не определены. Операция remove_if() С помощью операции remove_if() удаляются все элементы в последовательности, которые заставляют унарный предикат возвращать значение true. template<class Forwardlterator, class Predicate> Forwardlterator remove_if(Forwardlterator first, Forwardlterator last, Predicate pred); Подобно remove(), при выполнении операции remove_if() возвращается итератор last-n. Последние n элементов в последовательности содержат неопределенные значения. Операция remove_copy() Операция remove_copy() обеспечивает копирование всех элементов из одной последовательности в дру- другую и выполнение над результирующей последовательностью операции remove(). template<class Inputlterator, class Outputlterator, class T> Outputlterator remove_copy(Inputlterator first, Inputlterator last, Outputlterator result, const TS value); Операция remove_copy_if() Операция remove_copy_if() обеспечивает копирование всех элементов из одной последовательности в другую и выполнение над результирующей последовательностью операции remove_if(). template<class Inputlterator, class Outputlterator, class Predicate> Outputlterator remove_copy_if(Inputlterator first, Inputlterator last, Outputlterator result, Predicate pred); Операция unique() С помощью операции unique() проверяется последовательность для поиска элементов, имеющих одно и то же значение, и удаляется все, кроме первого элемента. Существует две версии функции unique(): одна
Итераторы и алгоритмы STL Глава 7 проверяет элемент на значение, а другая проверяет, заставляет ли элемент двоичный предикат возвратить значение true. teraplate<class Forwardlterator> Forwardlterator unique(Forwardlterator first, Forwardlterator last); template<class Forwardlterator, class EinaryPredicate> Forwardlterator unique{Forwardlterator first, Forwardlterator last, BinaryPredicate pred); Функции обеих версий возвращают итератор, указывающий на элемент past-the-end в результирующей последовательности. Подобно remove(), операция unique() не изменяет размер последовательности. Если удаляется п элементов, то последние п элементов в последовательности будут содержать неопределенные значения. Операция unique_copy() Операция unique_copy() обеспечивает копирование элементов из одной последовательности в другую и выполнение над новой последовательностью операции unique(). template<class Inputlterator, class Outputlterator> Outputlterator unique_copy(Inputlterator first, Inputlterator last, Outputlterator result); template<class Inputlterator, class Outputlterator, class BinaryPredicate> Outputlterator unique_copy(Inputlterator first, Inputlterator last, Outputlterator result, BinaryPredicate pred); Обе версии возвращают итератор, указывающий на элемент past-the-end в результирующей последова- последовательности. Если из результирующей последовательности удаляются п элементов, то последние п элементов будут иметь неопределенные значения. Операция swap() С помощью операции swap() меняются местами два элемента. template<class T> void swap(TS a, TS Ь) ; Операция 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() меняются местами элементы в последовательностях [firstI,lastl) и [first2, first2+(lastl-flrstl)). Будет возвращен итератор 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 template<class T> class Print: public unary function<T, void> < public: void operator () (T4 argl) { cout « argl « " " ; // Унарная функция для проверки, является ли целое четный числом. // bool IsEven(int var); // // Отобразить все элементы в контейнере // template<class Container> void ShowElements(Containers с, char* text); int main О < typedef vector<int> Vectorlnt; typedef Vectorlnt::iterator Itor; // создайте вектор целых Vectorlnt vlntlG); Itor firstl = vlntl. begin (); Itor lastl = vlntl.end() ; for (int i = 0; i < 7; ++i) vlntl [i] = 3 * i; ShowElements(vlntl, "vlntl created"); // создайте вектор для хранения результатов Vectorlnt vlnt2G); Itor first2 = vlnt2.begin () ; Itor Iast2 = vlnt2.end() ; fill(first2, Iast2, -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, Iast2); ShowElements(vlnt2, "removed duplicate values from vlnt2"); // удалите в vlntl все четные числа и скопируйте результат в vlnt2 fill(first2, Iast2, -1) ; remove_copy_if(firstl, lastl, first2, IsEven); ShowElements(vlnt2, "removed all even numbers"); // поменяйте местами второй и третий элементы в 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 ; // Унарная функция для проверки, является ли целое число четным. // bool IsEven(int var)
Итераторы и алгоритмы STL Глава 7 { return ((var % 2) =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.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() обеспечивает сортировку элементов в последовательности. template<class 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; // // Унарная функция для вывода аргументов в cout // template<class T> class Print: public unary_function<T, void> { public: void operator () (T& argl) { cout « argl « " " ; // Функция сравнения // bool ReverseCompare(int argl, int arg2); // // Отобразить все элементы в контейнере // template<class Container> void ShowElements(Containers c, char* text); int mainQ < typedef vector<int> VectorInt; typedef Vectorlnt::iterator Itor; // создать вектор целых Vectorlnt vlntl G); Itor firstl = vlntl. begin (); Itor lastl = vlntl.end() ; generate(firstl, lastl, rand) ; ShowElements(vlntl, "Random number sequence"); // отсортировать последовательность по убыванию sort(firstl, lastl, ReverseCompare); ShowElements(vlntl, "Sorted in descending order") return 0;
Итераторы и алгоритмы STL Глава 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" ; f or_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() и stable() #include <iostream> #include <vector> #include <functional>
Вопросы реализации Часть II #include <algorithm> using namespace std; // Унарная функция для вывода аргументов в cout // template<class T> class Print: public unary_function<T, void> { public: void operator()(T& argl) { cout « argl « " " ; // Функция сравнения // bool ReverseCompare(int argl, int arg2); // // Отобразить все элементы в контейнере // template<class Container> void ShowElements(Containers c, char* text); int main() { typedef vector<int> VectorInt; typedef VectorInt::iterator Itor; // создать вектор целых Vectorlnt vlntlG); Itor firstl = vlntl. begin (); Itor lastl = 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; // Функция сравнения bool ReverseCompare(int argl, int arg2) return (argl > arg2); // Отобразить все элементы в контейнере template<class Container> void ShowElements(Containers с, char* text) Print<Container::value_type> DoPrint; cout « text « ":\n"; for_each(c.begin() , c.end() , DoPrint) ; cout « "\n\n"; Ниже приведен вывод, сгенерированный программой из листинга 7.13:
Итераторы и алгоритмы STL Глава 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 Inputlterator, class RandomAccessIterator> RandomAccessIterator partial_sort_copy(Inputlterator first, Inputlterator last, RandomAccessIterator result_first, RandomAccessIterator result_last); template<class Inputlterator, class RandomAccessIterator, class Compare> RandomAccessIterator partial_sort_copy(Inputlterator first, Inputlterator last, RandomAccessIterator result_first, RandomAccessIterator result_last, Compare comp); Эта операция обеспечивает сортировку самых меньших п элементов, где n=min(last-first, result_Iast- result_first), в последовательности [first,last), а результат копируется в [result_flrst, result_first+n). Будет возвращено min(result_first+n, result_Iast). Операция nth_element() С помощью операции nth_element() реорганизуется порядок элементов в последовательности так, что- чтобы элементы, которые меньше или равны п, помещались перед элементом n-m. Элементы в данной группе сортируются. Остальные элементы помещаются после элемента п, и их сортировка не гарантирована. template<class RandomAccessIterator> void nth_element(RandomAccessIterator first, RandomAccessIterator nth, RandomAccessIterator last); template<class RandomAccessIterator, class Compare> void nth_element(RandomAccessIterator first, RandomAccessIterator nth, RandomAccessIterator last. Compare comp); Операция nth_element() показана в листинге 7.14. Листинг 7.14. Упорядочение последовательности с применением операции nthelementQ #include <iostream> #include <vector> #include <functional> #include <algorithm> using namespace std; // // Унарная функция для вывода аргументов в cout // template<class T> class Print: public unary_function<T, void> { public: void operator () (TS argl) { cout « argl « " " ; // Функция сравнения // bool ReverseCompare(int argl, int arg2); 8 Зак. 53
Вопросы реализации Часть II // Отобразить все элементы в контейнере // t«mplate<class Container> void ShowElements(Containers с, char* text); int main() { typedef vector<int> Vectorlnt; typedef Vectorlnt::iterator Itor; // создать вектор целых Vectorlnt vlntl G); Itor firstl = vlntl. begin (); Itor lastl = vlntl. end (); generate(firstl, lastl, rand); ShowElements(vlntl, "Random number sequence"); // отсортировать последовательность по убыванию nth_element(firstl, firstl + 1, lastl, ReverseCompare) ShowElements(vlntl, "After nth_element()"); return 0; > II II функция сравнения // bool ReverseCompare(int argl, int arg2) { return (argl > arg2); // Отобразить все элементы в контейнере // tanplate<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() проверяется отсортированная последовательность с целью найти первую позицию, в которой можно вставить значение без нарушения порядка сортировки последователь- последовательности.
Итераторы и алгоритмы STL Глава 7 template<class Forwardlterator, class T> Forwardlterator lower_bound(Forwardlterator first, Forwardlterator last, const T& value) ; template<class Forwardlterator, class T, class Compare> Forwardlterator lower_bound(Forwardlterator first, Forwardlterator last, const T& value, Compare comp); Возвращается итератор last, если не будет найдена подходящая позиция для вставки. Операция upper_bound() При выполнении операции upper_bound() проверяется отсортированная последовательность с целью найти последнюю позицию от начала, в которой можно вставить значение без нарушения порядка сортировки последовательности. template<class Forwardlterator, class T> Forwardlterator upper_bound(Forwardlterator first, Forwardlterator last, const T& value); template<class Forwardlterator, class T, class Compare> Forwardlterator upper_bound(Forwardlterator first, Forwardlterator last, const TS value, Compare comp); Подобно lower_b°und, операция upper_bound() также возвращает итератор last, если не может найти подходящую позицию для вставки. Операция equal_range() С помощью операции equal_range() ищется диапазон, в котором можно вставить значение без наруше- нарушения порядка сортировки последовательности. template<class Forwardlterator, class T> pair<ForwardIterator, ForwardIterator> equal_range(Forwardlterator first, Forwardlterator last, const TS value); template<class Forwardlterator, class T, class Compare> pair<ForwardIterator, ForwardIterator> equal_range(Forwardlterator first, Forwardlterator last, const TS value, Compare comp); Будет возвращена пара итераторов, указывающих на первую и последнюю позиции, в которых можно вставить значение. Операция binary searchf) Операция обеспечивает binary_search() выполнение поиска двоичного значения в последовательности. template<class Forwardlterator, class T> bool binary_search(Forwardlterator first, Forwardlterator last, const T5 value); template<class Forwardlterator, class T, class Compare> bool binary_search(Forwardlterator first, Forwardlterator last, const TS value, Compare comp); Будет возвращено true, если значение найдено, и false, если значение не найдено. Операция тегде() Операция merge() обеспечивает объединение двух отсортированных последовательностей. template<class Inputlteratorl, class Inputlterator2, class Outputlterator> Outputlterator merge(Inputlteratorl firstl, Inputlteratorl lastl, Inputlterator2 first2, Inputlterator2 Iast2, Outputlterator result) ; template<class Inputlteratorl, class Inputlterator2, class Outputlterator, class Compare> Outputlterator merge(Inputlteratorl firstl, Inputlteratorl lastl, Inputlterator2 first2, Inputlterator2 Iast2, Outputlterator result, Compare comp);
Вопросы реализации Часть» Если вводные последовательности содержат равные элементы, то элементы из первой последовательно- последовательности помещаются первыми. В результате выполнения операции возвращается итератор past-the-end в резуль- результирующей последовательности. Результирующая последовательность не должна перекрываться вводными последовательностями. В противном случае результат будет не определен. Операция inplace_merge() С помощью операции inplace_merge() объединяются две последовательности, а результат помещается в исходную последовательность. tamplate<class Bidirectionallterator> void inplace_merge(Bidirectionallterator first, Bidirectionallterator middle, Bidirectionallterator last); tamplate<class Bidirectionallterator, class Compare> void inplaca_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 // template<class T> class Print: public unary_function<T, void> { public: void operator () (T& argl) ( cout « argl « " "; // Отобразить все элементы в контейнере // template<class Container> void ShowElements(Container& c, char* text); int main() < typedef vector<int> VectorInt; typedef Vectorlnt::iterator Itor; // создать вектор целых Vectorlnt vlntl E); Itor firstl = vlntl. begin (); Itor lastl = vlntl.end(); vlntl [0] = 1; vlntl[1] = 2; vlntl[2] = 2; vlntl[3] = 4; vlntl[4] = 5; ShowElements(vlntl, "vlntl"); // найти первую и последнюю позицию для вставки целого 2 Itor 1Ь = lower_bound(firstl, lastl, 2) ; Itor ub = upper_bound(firstl, lastl, 2) ; cout « "The first position to insert 2 is between " « *(lb - 1)
Итераторы и алгоритмы STL Глава 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(firstl, 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"; else cout « "Can't find 4. \n\n" ; // переприсвоить элементы 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"); // переприсваивание элементов 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> DoPxint; cout « text « " : \n"; for_each(c.begin() , c.end() , DoPrint) ; cout « "\n\n"; } Ниже приведен вывод из программы и листинге 7.15: vlntl: 12 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
Вопросы реализации Часть I! Found 4. vlntl reassigned: 14 2 2 5 After inplace merge: 12 2 4 5 vlntl reassigned: 14 2 2 5 After inplace merge on unsorted sequences: 14 2 2 5 При выполнении операции equal_range() ищется первая и последняя позиции, куда можно вставить значение. Это предпочтительная операция для использования, если необходимо найти обе позиции. Опера- Операция inplace_merge() требует, чтобы обе последовательности были отсортированы до ее вызова. Второе об- обращение к операции показывает, что inplace_merge() не влияет на две несортированные последовательности. Операции над наборами Набор представляет собой коллекцию объектов. Последовательность — это набор, в котором к элемен- элементам можно получить доступ с помощью итераторов. В STL определены операции над наборами для после- последовательностей, как описано далее в этой главе. Операция include^ С помощью операции inc!ude() проверяется, все ли элементы имеются в одной и других последователь- последовательностях. template<class Inputlteratorl, class Inputlterator2> bool includes(Inputlteratorl firstl, Inputlteratorl lastl, Inputlterator2 first2, Inputlterator2 Iast2); template<class Inputlteratorl, class Inputlterator2, class Compare> bool includes(Inputlteratorl firstl, Inputlteratorl lastl, Inputlterator2 first2, Inputlterator2 Iast2, Compare comp); Если каждый элемент последовательности [first2,Iast2) является также и членом последовательности [firstl,Iastl), то при выполнении операции include() возвращается значение true. В противном случае воз- возвращается значение false. Операция set_union() С помощью операции set_union() две отсортированные последовательности объединяются в третью. template<class Inputlteratorl, class Inputlterator2, class Outputlterator> Outputlterator set_union(Inputlteratorl firstl, Inputlteratorl lastl, Inputlterator2 first2, Inputlterator2 Iast2, Outputlterator result); template<class Inputlteratorl, class Inputlterator2, class Outputlterator, class Compare> Outputlterator set union (Inputlteratorl firstl, Inputlteratorl lastl, Inputlterator2 first2, Inputlterator2 Iast2, Outputlterator result, Compare comp); При выполнении операции set_union() сортируется результирующая последовательность и возвращает- возвращается итератор past-the-end результирующей последовательности. Операция setjntersectionQ С помощью операции set__intersection() копируются элементы, общие для двух последовательностей, в третью последовательность. template<class Inputlteratorl, class Inputlterator2, class OutputIterator> Outputlterator set_intersection(Inputlteratorl firstl, Inputlteratorl lastl, Inputlterator2 first2, Inputlterator2 Iast2, Outputlterator result);
Итераторы и алгоритмы STL Глава 7 template<class Inputlteratorl, class Inputlterator2, class Outputlterator, class Compare> Outputlterator set_intersection(Inputlteratorl firstl, Inputlteratorl lastl, Inputlterator2 first2, Inputlterator2 Iast2, Outputlterator result, Compare comp); При выполнении операции set_intersection() сортируется результирующая последовательность и возвра- возвращается итератор past-the-end результирующей последовательности. Операция set_difference() Операция set_difference() обеспечивает копирование элементов из одной последовательности, отсутству- отсутствующих в другой, в третью. template<class Inputlteratorl, class Inputlterator2, class Outputlterator> Outputlterator set_difference(Inputlteratorl firstl, Inputlteratorl lastl, Inputlterator2 first2, Inputlterator2 Iast2, Outputlterator result); template<class Inputlteratorl, class Inputlterator2, class Outputlterator, class Compare> Outputlterator set_difference(Inputlteratorl firstl, Inputlteratorl lastl, Inputlterator2 first2, Inputlterator2 Iast2, Outputlterator result, Compare comp); При выполнении операции set_difference() в последовательность [firstl, lastl) копируются элементы, отсутствующие в [first2, Iast2). Результирующая последовательность является отсортированной. Итератор past- the-end возвращается результирующей последовательности. Операция set_symmetric_difference() С помощью операции set_symmetric_difference() создается новая последовательность, содержащая эле- элементы, не являющиеся членами обеих вводных последовательностей. template<class Inputlteratorl, class Inputlterator2, class Outputlterator> Outputlterator set_syinmetric_dif ference (Inputlteratorl firstl, Inputlteratorl lastl, Inputlterator2 first2, Inputlterator2 Iast2, Outputlterator -result) ; template<class Inputlteratorl, class Inputlterator2, class Outputlterator, class Compare> Outputlterator set_symmetric_difference(Inputlteratorl firstl, Inputlteratorl lastl, Inputlterator2 first2, Inputlterator2 Iast2, Outputlterator result, Compare comp); При выполнении операции set_symmetric_difference() элементы копируются в последовательность [firstl, lastl), но не в [first2, Iast2) и элементы в [first2, Iast2), но не в [firstl, lastl). Результирующая последова- последовательность является отсортированной. Возвращается итератор past-the-end результирующей последовательности. В листинге 7.16 демонстрируются операции, выполняемые над наборами. Листинг 7.16. Обработка последовательностей с помощью операций, выполняемых над наборами #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 « " " ; int main()
Вопросы реализации Часть II typedef vector<int> Vectorlnt; typedef Vectorlnt:-.iterator Itor; Print<int> DoPrint; int i = 0; // создать первый вектор целых Vectorlnt vlntlA0); Itor f irstl = vlntl. begin (); Itor lastl = vlntl.end() ; for (i = 0; i < 10; ++i) vlntl [i] =2 * i; // создать второй вектор целых Vectorlnt vlnt2(8); Itor first2 = vlnt2.begin() ; Itor Iast2 = vlnt2.end(); for (i = 0; i < 8; ++i) vlnt2[i] = 3 * i; // создать третий вектор целых для хранения операций set Vectorlnt vInt3B0, -1) ; Itor first3 = vlnt3.begin(); Itor Iast3 = vlnt3.end(); // копировать объединение vlntl и vlnt2 в vlnt3 set_union(firstl, lastl, first2, Iast2, first3); cout « "Onion: elements in either vlntl or vlnt2:\n"; for_each(first3, Iast3, DoPrint); cout « "\n\n"; // случайное переупорядочивание элементов в vlnt3 random_shuffle(first3, Iast3); cout « "Random Shuffle: el3ments in vlnt3 reordered:\n"; for_each(first3, Iast3, DoPrint); cout « "\n\n"; // сортировка vlnt3 sort(first3, Iast3); cout « "Sorted:\n"; for_each(first3, Iast3, DoPrint); cout « "\n\n"; // копировать пересечение vlntl и vlnt2 в vlnt3 fill(first3, Iast3, -1); set_intersection(firstl, lastl, first2, Iast2, first3); cout « "Intersection: elements in both vlntl and vlnt2\n"; for_each(first3, Iast3, DoPrint); cout « "\n\n"; // копировать элементы vlntl, которых нет в vlnt2 в vlnt3 fill(first3, Iast3, -1); set_difference(firstl, lastl, first2, Iast2, first3); cout « "Difference: elements in vlntl but not in vlnt2\n"; for_each(first3, Iast3, DoPrint); cout « "\n\n"; // копировать элементы, которые имеются только в vlntl или только в vlnt2 в vlnt3 fill(first3, Iast3, -1); set_symmetric_difference(firstl, lastl, first2, Iast2, first3); cout « "Symmetric Difference: elements not in both vlntl and vlnt2\n"; for_each(first3, Iast3, DoPrint); cout « "\n\n"; return 0; Ниже приведен вывод программного кода из листинга 7.16: Onion: 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-10-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() обеспечивает преобразование последовательности в кучу. template<class RandomAccessIterator> void make_heap(RandomAccessIterator first, RandomAccessIterator last) ; tempiate<class RandomAccessIterator, class Compare> void make_heap(RandomAccessIterator first, RandomAccessIterator last. Compare comp); При выполнении этой операции последовательность [first, last) преобразуется в кучу [first, last). Операция sort heap() Операция sort_heap() позволяет преобразовать кучу в последовательность, сортируя ее элементы. template<class RandomAccessIterator> void sort_heap(RandomAccessIterator first, RandomAccessIterator last) ; template<class RandomAccessIterator, class Compare> void sort_heap(RandomAccessIterator first, RandomAccessIterator last, Compare comp); Результирующая последовательность отсортирована. Операция push_heap() Операция push_heap() позволяет добавлять в кучу новый элемент. template<class RandomAccessIterator> void push_heap(RandomAccessIterator first, RandomAccessIterator last) ; template<class RandomAccessIterator, class Compare> void push_heap(RandomAccessIterator first, RandomAccessIterator last, Compare comp); Операция push_heap() принимает последовательность [first, last). Она предполагает, что диапазон [first, last-1) является кучей, и проталкивает элемент *(last-l) в кучу. После выполнения операции [first, last) становится кучей. Операция рор_пеар() При выполнении операции рор_пеар() из кучи удаляется верхний элемент. template<class RandomAccessIterator> void pop_heap(RandomAccessIterator first, RandomAccessIterator last); template<class RandomAccessIterator, class Compare> void pop_heap(RandomAccessIterator first, RandomAccessIterator last, Compare comp);
Вопросы реализации Часть II С помощью операции pop_heap() меняются местами первый и предпоследний элементы в куче [first, last). Результирующая последовательность [first, last) больше не является кучей, поскольку наибольший элемент теперь указывается итератором last-1. При выполнении операции pop_heap() последовательность [first, last-1) преобразуется обратно в кучу. Прежний самый большой элемент теперь доступен по итерато- итератору *(last-l). В листинге 7.17 демонстрируются операции над кучей. Листинг 7.17. Операции над кучей tinclude <iostream> #include <vector> tinclude <functional> tinclude <algorithm> using namespace std; // // Унарная функция для вывода аргументов в cout // template<class T> class Print: public unary_function<T, void> < public: void operator()(TS argl) { cout « argl « " " ; // Отобразить все элементы в контейнере // template<class Container> void ShowElements(Containers c, char* text); int main() { typedef vector<int> VectorInt; typedef Vectorlnt::iterator Itor; // создать вектор целых Vectorlnt vlntlE); Itor firstl = vlntl.begin() ; Itor lastl = 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() ; make_heap(firstl, lastl); ShowElements(vlntl, "vlntl is now a heap again"); // операция pop firstl = vlntl.begin(); lastl = vlntl.end() ; pop_heap(firstl, lastl); cout « * (lastl - 1) « " popped, "; ShowElements(vlntl, "vlntl is no longer a heap");
Итераторы и алгоритмы STL Глава 7 // операция push firstl = vlntl.begin() ; lastl = vlntl.endO; *(lastl - 1) = 32000; ShowElements(vlntl, "vlntl is ready for a push"); push_heap(firstl, lastl); ShowElements(vlntl, "New value pushed into vlntl"); 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.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.beginQ, vlntl.endO) больше не явля- является кучей. Однако первые четыре элемента [vlntl.begin(), vlntl.end()-l) — все еще из кучи. Когда мы про- проталкиваем большее значение в кучу, то оно становится новой вершиной кучи. Обратите внимание на то, что поскольку с помощью операции над кучей обычно реорганизуются элементы в последовательности, то прежде полученные итераторы могут после таких операций стать недействительными. Тем не менее, в этом листинге мы переприсваиваем итераторам firstl и lastl новые значения begin() и end(), перед тем как ис- использовать их для определения новой последовательности. Операции минимизации и максимизации Библиотека STL содержит функции, которые можно использовать для поиска максимального или ми- минимального значений объектов. Операция min() С помощью операции min() возвращается наименьший из двух элементов. template<class T> const TS min(const TS a, const TS Ь) ; template<class T, class Compare> const Т& min (const TS a, const T& b, Compare comp) ;
Вопросы реализации Часть II Операция тах() При выполнении операции гаах() возвращается наибольший из двух элементов. template<class T> const T& max(const T& a, const T& Ь) ; template<class T, class Compare> const T& max (const T& a, const T& b, Compare comp) ; Операция min_element() При выполнении операции min_element() возвращается итератор, указывающий на наименьший эле- элемент в последовательности. template<class Forwardlterator> Forwardlterator min_element (Forwardlterat-.or first, Forwardlterator last); template<class Forwardlterator, class Compare> Forwardlterator min_element(Forwardlterator first, Forwardlterator last, Compare comp); Операция maxjelementf) С помощью операции max_element() возвращается итератор, указывающий на наибольший элемент в последовательности. template<class Forwardlterator> Forwardlterator max_element(Forwardlterator first, Forwardlterator last); template<class Forwardlterator, class Compare> Forwardlterator max_element(Forwardlterator first, Forwardlterator last, Compare comp); Операция lexicographical_compare() При выполнении операции lexicographical_compare() сравниваются две последовательности в лексиког- лексикографическом порядке. template<class Inputlteratorl, class Inputlterator2> bool lexicographical_compare(Inputlteratorl firstl, Inputlteratorl lastl, Inputlterator2 first2, Inputlterator2 Iast2); template<class Inputlteratorl, class Inputlterator2, class Compare> bool lexicographical_compare(Inputlteratorl firstl. Inputlteratorl lastl, Inputlterator2 first2, Inputlterator2 Iast2, Compare comp); Эту операцию лучше объяснить, используя следующий псевдокод: template<class Inputlteratorl, class Inputlterator2> bool lexicographical_compare(Inputlteratorl firstl, Inputlteratorl lastl, Inputlterator2 first2, Inputlterator2 Iast2) ( // пройти по двум последовательностям до тех пор, пока одна не будет исчерпана while ((firstl != lastl) && (first2 != Iast2)) I // сравнить каждую пару элементов if (*firstl < *first2) return true; if (*firstl > *first2) return false; // если *firstl == *first2, то перейти к следующей паре элементов ++firstl; ++first2; } if ((firstl == lastl) && (first2 != Iast2)) { // первая последовательность короче второй return true; > else
Итераторы и алгоритмы STL Глава 7 return false; В листинге 7.18 демонстрируется операция lexicographical compare(). Листинг 7.18. Сравнение последовательностей с помощью lexicographicalcompareQ #include <iostream> #include <string> tinclude <algorithm> using namespace std; int main() string si = "abed"; string s2 = "abd"; if (lexicographical_compare(sl.begin() , sl.endf), s2.begin(), s2.end())) cout « "[" « si « "] < [" « s2 « "]\n"; else cout « "[" « si « "] >= [" « s2 « "]\n"; si = "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() для сравнения двух строк с разными значениями. Генераторы перестановок Последовательность из п элементов можно упорядочить n!=n*(n - 1)*(п - 2)*.. .*2*1 способами. Каждая из упорядоченностей будет числом перестановок последовательности. Часто бывает полезно знать следующую или предыдущую перестановку по сравнению с текущей упорядоченностью последовательности. В библиотеке STL содержится два генератора перестановок, каждый из которых описан в следующих разделах. Операция next_permutation() С помощью операции next_permutation() генерируется следующая перестановка последовательности. template<class Bidirectionallterator> bool nextjpermutation(Bidirectionallterator first, Bidirectionallterator last); template<class Bidirectionallterator, class Compare> bool nextjpermutation(Bidirectionallterator first, Bidirectionallterator last, Compare comp); Если при выполнении этой операции находится следующая перестановка, то реорганизуется последо- последовательность к найденной перестановке и возвращается значение true. В противном случае упорядочивается последовательность к первой перестановке и возвращается значение false.
Вопросы реализации Часть li Операция prev_permutation() Операция prev_permutation() позволяет генерировать предыдущую перестановку последовательности. template<class 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 (prevjpermutation (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 (neKt_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 permutationO позволяет найти предыдущую перестановку последовательности abdc. Слу- Случилось так, что это первая перестановка последовательности из четырех символов а, Ь, с, d. Поскольку это первая перестановка, то предыдущая отсутствует. Следующая операция prev_permutation() позволяет реор- реорганизовать строку к deba, которая является последней перестановкой последовательности. Это приводит к тому, что первая операция next jermutation() терпит неудачу в поиске следующей перестановки и при этом строка реорганизуется к abed, первой перестановке последовательности. Последняя операция next_perniutation() обеспечивает поиск следующей перестановки и реорганизацию строки к найденной перестановке. Стандартные функции Объекты-функции часто используются стандартными алгоритмами как аргументы. Например, стандарт- стандартный алгоритм for_each() принимает унарный объект-функцию в качестве своего третьего аргумента: for_each(Iterator first, Iterator last, FunctionObject f)
Итераторы и алгоритмы STL Глава 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 определяет базовый класс для функции bind 1st: template<class BinaryOperation> class binderlst : public unary_function<BinaryOperation::second_argument_type, BinaryOperation::result_type> { public: binderlst(const BinaryOperation & x, const BinaryOperation::first_arguraent_type& const_argl) : op(x), value (const_argl) {} result_type operator()(const second_argument_type& arg2) const { return BinaryOperation(value, arg2); > protected: BinaryOperation op; BinaryOperation::first_argument_type value; }; Конструктор класса binderlst принимает два аргумента: объект операции класса и постоянное значение для первого аргумента. Такая организация эффективно связывает новый объект binderlst с операцией клас- класса и первым аргументом. Мы можем использовать этот базовый класс для создания функции компоновки, которая сравнивает объект и значение: template<class BinaryOperation, class T> binderlst<BinaryOperation> bindlst(const BinaryOperationS op, const T& argl); Перегруженный оператор () класса bindenrlst можно использовать для выполнения бинарной операции: BinaryFunction<T>::result_type bindlst(TS arg2) В листинге 7.20 демонстрируется использование функции bindlst() для разделения строчных и пропис- прописных букв в строке. Листинг 7.20. Применение функции bindistQ #include <iostream> tinclude <string> #include <functional> tinclude <algorithm> using namespace std; int main() { string s = "aBCdefGH"; partition(s.begin(), s.end(), bindls t(greater<char>() , ' a')) ; cout « "s = " « s « "\n"; return 0;
^^ Вопросы реализации Часть II Ниже приведен вывод программного кода в листинге 7.20: s = HBCGefda Мы использовали функцию bindlst() для связывания первого аргумента функции greater() с буквой а. Поскольку строчная а больше, чем все прописные буквы, то функция bind 1st(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) ; }; template<class S, class T> mem_fun t<S, T> mem_fun(S (T::*f)()) { return mem_fun_t<S, T>(f); ) В листинге 7.21 показано применение адаптеров функций-членов для того, чтобы предоставить возмож- возможность алгоритмам вызывать функции-члены. Листинг 7.21. Передача функций-членов алгоритмам с помощью адаптеров функций-членов tinclude <iostream> tinclude <list> tinclude <?unctional> tinclude <algorithm> using namespace std; typedef unsigned int OINT; class MyWindow { public: MyWindow(DINT newID = 0): mlD(newID) {} void Show() const { cout « "Showing window " « mID « "\n";
Итераторы и алгоритмы STL Глава 7 > // MSVC++ 5 version // // int Show () // { // cout « "Showing window " « mID « "\n"; // return 0; // ) private: UINT mID; int main{) // создать и добавить элементы х списку указателей на оконные объекты MyWindow* pWin; list<MyWindow*> winPtrList; for (OINT winID = 0; winID < 5; ++winID) pWin = new MyWindow (winID) ; winPtrList.push_back(pWin); pWin = 0; // показать каждое окно в списке for_each(winPtrList.begin(), winPtrList.end(), mem_fun(SMyWindow::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> tinclude <list> tinclude <functional> tinclude <algorithm> uaing namespace std; typedef unsigned int UINT;
Вопросы реализации Часть II class MyWindow ( public: MyWindow(OINT newID = 0): mlD(newID) {} DINT GetID() const { return mID; } private: UIHT mID; }; void ShowWindowUnary(const MyWindowS win); int main() ( // создать и добавить элементы к списку указателей на оконные объекты MyWindow* pWin; liat<MyWindow> winList; UINT winID; for (winID = 0; winID < 5; ++winID) < pWin = new MyWindow(winID) ; 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 MyWindowfc win) { cout « "Showing window " « win.GetlDO « ".\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. Мы определили унарную функцию для печати переменной-члена шГО объекта MyWindow. Эта функция вызывается операцией for_each() для печати themID для каждого объекта 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 arguraent_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 operatorO (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, not20ess_equal<int>())), который делает то же самое, что и пользовательский предикат ReverseCompare(). Резюме Библиотека Standard C++ Library предлагает иерархию итераторов, которые можно применять для дос- доступа к последовательностям. Для выполнения распространенных операций, таких как сортировка и пересе- пересечение последовательностей, в Standard C++ Library определено множество стандартных алгоритмов. Поставщики компиляторов часто реализуют стандартные алгоритмы в наиболее эффективной и надеж- надежной форме для конкретной операционной системы. Использование стандартных алгоритмов в общем слу- случае предпочтительнее для функций, написанных вручную и выполняющих аналогичные операции.
Исключение конфликтов имен В ЭТОЙ ГЛАВЕ Функции и классы, разрешаемые по именам Создание пространства имен Использование пространства имен Ключевое слово using Псевдоним пространства имен Неименованное пространство имен Стандартное пространство имен
Исключение конфликтов имен Глава 8 Конфликты имен являются источником раздражения для разработчиков, использующих как язык С, так и C++ уже много лет. Тем более странно, что Комитет по стандартизации C++ лишь недавно уделил внимание данной проблеме, представив механизм пространства имен. По мнению автора, тема конфлик- конфликтов имен должна была решаться много лет назад. Поскольку Комитет по стандартизации до недавнего времени эту проблему не затрагивал, некоторые поставщики не включали в текущие версии своих компиляторов поддержку пространств имен. Тем не ме- менее, автор рекомендует разобраться с пространствами имен сейчас, даже если ваш компилятор не вклю- включает поддержку этого механизма. Конфликт имен происходит тогда, когда в двух разных транслирующихся модулях встречаются дублиру- дублирующиеся имена с совпадающей областью видимости. Наиболее часто такое происходит в двух библиотечных пакетах. Например, библиотека контейнерных классов объявляет и реализует класс List. He удивительно потому, что класс List используется и в библиотеке окон. Предположим, что для своего приложения вы хотите организовать поддержку списка окон, а также используете для этого возможности класса List, най- найденного в библиотеке контейнерных классов. Таким образом, вы объявляете экземпляр List для хранения группы окон. К вашему разочарованию обнаруживается, что те функции-члены, которые вы хотели выз- вызвать, недоступны. В чем дело? Очевидно, что компилятор предпочел объявление List из контейнера List, но то, что вам требуется, — это List в библиотеке окон. Пространства имен используются для разделения глобального пространства имен и устранения или, по крайней мере, уменьшения количества конфликтов имен. Пространства имен похожи на классы и структу- структуры. Синтаксис очень сходен. Элементы, объявленные внутри пространства имен, принадлежат простран- пространству имен. Все элементы внутри пространства имен обладают общедоступной видимостью. Одни Пространства имен могут быть вложены в другие пространства имен. Функции можно объявлять как внутри тела про- пространства имен, так и вне его. Если функция определена вне тела пространства имен, то она должна ква- квалифицироваться именем пространства. Здесь обсуждаются эти и другие детали во всех подробностях. Но прежде всего рассмотрим несколько базовых правил, касающихся разрешения имен, требований к уникальности имен, и другие вопросы про- пространства имен. Функции и классы, разрешаемые по именам В ходе анализа исходного программного кода и построения списка функций и имен переменных компи- компилятор проверяет, не вступают ли имена в конфликты. Конечно, компилятор не может разрешить все кон- конфликты имен. В тех случаях, когда он не может разрешить конфликты, в дело вступает компоновщик. Компилятор не может проверить имена в кэш-памяти транслируемых модулей. Если бы компилятор мог это сделать, то он мог бы (потенциально) принять ответственность компоновщика на себя. Вы, наверное, видели следующее сообщение об ошибке, выдаваемое компоновщиком: Identifier multiply defined (Identifier, конечно, это какой-то именованный тип). Это сообщение компоновщика появляется в том случае, если одно и то же имя определено в одной и той же области видимости в разных транслирующихся модулях. Ошибка компилятора возникает тогда, когда имя переопределяется в пределах одного файла, имеющего ту же самую область видимости. В следующем примере при компилировании и редактировании будет выдано сообщение об ошибке компоновщика: // файл first.срр int integerValue = 0 ; int main( ) { int integerValue = 0 ; // • - . return 0 ; ) ; // файл second, срр int integerValue = 0 ; // конец second.срр Компоновщик поставит следующий диагноз: 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" // . . . return 0 ; } ; // файл second, срр int integerValue = 0 ; // конец second.срр Проблема с двумя глобальными целыми, определенными вне какой-либо функции, заключается в том, что они имеют одинаковые имена и видимость. Автор использует термин видимость для обозначения области видимости определенного объекта, неза- независимо от того, является ли он переменной, классом или функцией. Например, переменная, объявленная и определенная вне любой функции, имеет файловую или глобальную область видимости. Видимость такой переменной распространяется от точки ее определения до конца файла. Переменная имеет блоковую или локальную область видимости, если находится внутри блоковой структуры. Наиболее распространенными являются примеры, когда переменные определены внутри функций. В следующем примере показана об- область видимости переменных: 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.epp extern int externalInt ; int anExternallnt = 10 ; const int j = 10 ; Переменная externallnt, определенная в файле first.epp, обладает внешней связываемостью. Хотя она определена в first.ерр, файл second.ерр также имеет доступ к ней. Две переменные js, имеющиеся в обоих файлах, являются const, что по умолчанию придает им внутреннюю связываемость. Умолчание const мож- можно переопределить, обеспечив явное объявление, как показано ниже: // файл: first.ерр extern const int j = 10 ; // файл: second.epp 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 x, int у) ; // объявления void resize( int x, int у ) ; II... другие объявления и определения переменных. void move ( int x, int у ) { if( X < MAX_SCREEN_X SS x > 0 ) if( у < MAX_SCREEN_Y fifi у > 0 ) platform.move( x, у ) ; // специфическая подпрограмма } void resize( int x, int у ) { if( x < MAX_SIZE_X fifi x > 0 ) if ( у <" MAX~SIZE_Y fifi у > 0 ) platform.resize( x, у ) ,11 специфическая подпрограмма } // . . . продолжение определений } Видите, как быстро фрагментируется пространство имен! Приведенный пример занимает приблизительно 20 строк. Вообразите, что было бы, если бы пространство имен было в 4 раза длиннее. В следующем разде- разделе описано, как определить функции вне пространства имен. Такой прием помогает уменьшить фрагменти- рованность пространства имен.
Исключение конфликтов имен Глава 8 Определение функций вне пространства имен Пространство имен функций следует определять вне тела пространства имен. Это четко будет иллюст- иллюстрировать разделение объявления функций и их определения, а также сохранит тело пространства имен нефрагментированным. Разделение определения функций и пространства имен также предоставляет воз- возможность поместить пространство имен и заключенные в нем объявления в header-файл. Определения можно поместить в блок реализации. Ниже представлен краткий пример: // файл header.h namespace Window { void move) int x, int y) ; // другие объявления ... > // файл impl.cpp void Window: :move( int x, int у ) { // программный ход для перемещения охна > Добавление новых членов Новые члены можно добавить к пространству имен только внутрь его тела. Нельзя определить новые члены, используя синтаксис с квалификаторами. Самое большее, что можно ожидать в последнем случае, — это "жалоба" компилятора. Пример ниже демонстрирует подобную ошибку: namespace Window { // множество объявлений } // некоторый программный ход int Window::newIntegerlnNamespace ; // извините, этого делать нельзя Предыдущая строка программного кода незаконна. Ваш компилятор выдаст сообщение об ошибке. Для исправления ошибки или ее обхода просто переместите объявление внутрь тела пространства имен. Подчеркнем еще одно различие между пространством имен и классом. Если внутри объявления класса определяется функция-член, то она подразумевает применение 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( void move ( void show{ private: static int int x ; int у ; ; ; int X, int x, ) ; cnt ; int у ) int у ) int Window::Pane::cnt = 0 ; Window: : Pane: : Pane () : x@), y@) { } Window::Pane::~Pane() { ) void Window: : Pane :: size ( int x, int у ) { if( x < Window: :MAX_X ?S x > 0 ) Pane: :x = x ; if( у < Window: :MAX_Y && у > 0 ) Pane: :y = у ; } void Window:: Pane:: move ( int x, int у ) { iff x < Window: :MAX_X ?? x > 0 ) Pane: : x = x ; if{ у < Window::MAX_Y && у > 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. Статическая переменная cnt, которая объявлена в Рапе, определяется обычным способом. Заметьте, что внутри функции Pane::size() MAX_X и MAX_Y полностью квалифицированы. Это потому, что Рапе явля- является областью видимости. В противном случае компилятор сообщил бы об ошибке. То же самое справедли- справедливо и для функции Pane::move(). Интересно также квалифицирование Рапе::х и Рапе::у внутри обоих определений функций. Почему именно так? Если бы функция Pane::move() была написана так, то возникла бы проблема: void Window. : Pane::move ( int x, int у ) { if( x < Window::MAX_X && x > 0 ) x = x ; iff у < Window: :MAX_Y 66 у > 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 size( int x, int у ) ; void move( int x, int у ) ;
Вопросы реализации Часть II void show( ) ; private: static int cnt ; int x ; int у ; > ; > using namespace Window ; int Pane::cnt = 0 ; Pane:: Pane () : xA0), yA0) { } Pane: :~Pane() { } void Pane: :size( int x, int у ) { if( x < MAX_X && x > 0 ) Pane::x = x ; if( у < МАХ_У ?& у > 0 ) Pane: :y = у ; ) void Pane: : move ( int x, int у ) { if( x < MAX_X SS x > 0 ) x = x ; if( у < MAX_Y && у > 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 должна быть квалифицирована, чтобы на нее сослаться. Пере- Переменная value2 не требует квалификации, поскольку директива представляет в текущую область видимости все имена в пространстве имен. Директиву using можно использовать на любом уровне области разрешения. Это предоставляет возмож- возможность применять директиву внутри области блока. Когда этот блок выходит из области видимости, то же самое происходит со всеми именами внутри пространства имен. В следующем примере показано такое по- поведение: namespace Window { int valuel = 20 ; int value2 =40 ; void f () using namespace Window ; value2 = 30 ; value2 =20 ; //ошибка! Последняя строка кода в f(), va!ue2=30; ошибочна, поскольку имя value2 не определено. Это имя дос- доступно в предыдущем блоке, поскольку директива перенесла имя в него. Как только блок вышел из области видимости, то же самое произошло и с именами в пространстве имен Window. Имена переменных, объявленные внутри локальной области, закрывают любые имена пространства имен, введенных в данной области видимости. Такое поведение похоже на то, как локальная переменная засло- заслоняет глобальную. Даже если пространство имен представлено после локальной переменной, эта локальная переменная будет заслонять имя пространства. Следующий пример иллюстрирует сказанное: namespace Window { int valuel = 20 ; int value2 =40 ; void f() int value2 =10 ; using namespace Window ; std: :cout « value2 « std: :endl Вывод этой функции 10, а не 40. Такой вывод подтверждает факт, что value2 в пространстве имен Window закрыта переменной value2 в f(). Если надо использовать имя в пространстве имен, то его следует квали- квалифицировать именем пространства. Неоднозначность может появиться и при использовании имени, которое определено как глобально, так и внутри пространства имен. Неоднозначность проявляется только при использовании имени, а не просто когда представляется пространство имен. Сказанное иллюстрируется следующим фрагментом программно- программного кода: namespace Window { int valuel =20 ; using namespace Window ; int valuel = 10 ; void f( }
Вопросы реализации Часть II value1 =10 Неоднозначность проявляется внутри функции f(). Директива переносит Window::value 1 в глобальное пространство имен. Поскольку уже есть глобально определенная valuel, то применение value 1 в 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 ; } //• • • 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() заслоняет value2, определенную в глобальном пространстве имен. Как уже упоминалось, объявление using предоставляет более тонкий уровень управления именами из пространства имен. Директива using переносит в текущую область видимости все имена из пространства имен. Использование объявления предпочтительнее использования директивы, поскольку директива, по сути, сводит на нет назначение механизма пространства имен. Объявление более избирательно, поскольку явно указывает имена, которые необходимо представить в области видимости. Объявление using не засоряет гло- глобального пространства имен, как это происходит в случае директивы using (конечно, если вы не объявля- объявляете всех имен, имеющихся в пространстве). Благодаря использованию объявления using маскирование имен, засорение глобального пространства имен и неоднозначность сокращаются. Псевдоним пространства имен Псевдоним пространства имен сконструирован в целях обеспечения для пространства имен другого име- имени. Псевдоним — сокращенный термин, который используется при ссылке на пространство имен. Сказан- Сказанное особенно справедливо, если имя пространства имен очень длинное. Рассмотрим пример: namespace the_software_coinpany { int value ; // - . . } the_software_company:rvalue = 10 ; namespace TSC = the software_company ; TSC:rvalue =20 ; Недостаток, конечно, в том, что псевдоним может вступить в конфликт с существующим именем. В таком случае компилятор распознает конфликт и вы сможете решить проблему, изменив псевдоним. Неименованное пространство имен Неименованное пространство имен — это просто пространство имен, не имеющее имени. Неименован- Неименованные пространства принято использовать для защиты глобальных данных от потенциальных конфликтов имен между транслируемыми модулями. Каждый транслируемый модуль обладает собственным уникальным не- неименованным пространством имен. На все имена, определенные внутри неименованного пространства (внутри каждого транслируемого модуля), можно сослаться без явного квалифицирования. Ниже приведен пример двух неименованных пространств имен, находящихся в двух разных файлах: // файл: опе.срр namespace { int value ; char p ( char *p ) ; // файл: two.cpp namespace { int value ; char p( char *p ) ; //. - . } int main( ) { char с = p ( ptr ) ; } Каждое из имен, value и р(), определены для соответствующего файла. Для ссылки на (неименованное пространство) имя внутри транслируемого модуля просто используйте имя без квалификации. Такое при- применение демонстрировалось в предыдущем примере при обращении к функции р(). Подобное применение подразумевает директиву 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() для приведения указателя (ссылки) базового класса к указателю (ссылку) производного класса. Оператор typeidQ Оператор 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 { private: // Присваивание type_info не поддерживается type_infofi operator= (const type_infofi); type_info (const type_infofi); protected: type_info (const char *n) : _name (n) { } const char *_name; public: // деструктор virtual ~type_info (); bool operator== (const type_infofi arg) const; bool operator!= (const type infos arg) const; const char* name () const { return name; } bool before (const type_infofi arg) const;
Вопросы реализации Часть II Конструктор для класса typejnfo Класс type_info не предоставляет общедоступных конструкторов. По этой причине вы не можете прямо инициализировать объекты typejnfo в программе. Ссылки typejnfo генерируются оператором typeid(). На- Например, с учетом выражения ехрг следующее обращение генерирует const typejnfo& для ехрг: type id (ехрг) Поскольку конструктор копии и оператор присваивания для typejnfo приватны для класса, то объекты данного типа нельзя скопировать. Оператор typeid() работает подобно оператору sizeof(): он может принять или выражение, или сам тип. Например, задан тип Т и объект типа t, тогда действительно применение typeid(): typeid(T); typeid(t); Операторы сравнения Следующие операторы обеспечивают сравнение ссылок typejnfo: bool operator== (const type_info6 arg) const; bool operator!= (const type_infoS arg) const; Чтобы понять, как использовать typeid() и класс typejnfo, рассмотрим пример типичной иерархии классов графического интерфейса GUI, в которых класс Window служит базовым для всей иерархии GUI. Предполо- Предположим, что DialogBox является производным классом, который обеспечивает функцию repaintControis(). Функция 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() void paint(Window* pw) I if (typeid(*pw)==typeid(DialogBox)) < cout«"DialogBox"«endl; DialogBox* pd=(DialogBox*)pw; pd->repaintControls(); ) else cout«"Non-Dialog Object Type"«endl; } 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) ) В идеале виртуальные функции являются лучшим выбором для иерархических типов. Использование typeid() для замены виртуальных функций является определенно неэффективным механизмом. Это поло- положение рассматривается далее в этой главе. Обратите внимание на то, что программа в листинге 9.1 использует выражение приведения в функции paint(): DialogBox* pd=(DialogBox*)pw; Подобные выражения не считаются в C++ хорошей практикой, так как этот язык программирования обеспечивает лучший способ выполнения приведения (читайте раздел "Оператор dynamic_cast()" далее в этой главе). Функция-член name() Функция-член namc() возвращает строку, которая идентифицирует тип имени операнда для typeid. Вывод typeid().name() зависит от реализации. В листинге 9.2 демонстрируется использование функции name(). Листинг 9.2. Функция name() finclude <typeinfo.h> finclude <iostream.h> class Window //тип полиморфного класса. { public: virtual void show() (}; class Button {/*...*/>; int f() public Window Button b; Button* pb; pb=Sb; if (typeid(*pb)==typeid(Button)) cout«"Name is " <<typeid(*pb) .name () «endl; if (typeid(*pb)!=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; int v;
Вопросы реализации Часть 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() При запуске этой программы на 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 Программа демонстрирует применение не только функции пашс(), но и операторов сравнения типов двух объектов (*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()=19 builtin_type_info Как уже говорилось, typeid() обеспечивает информацию о типе во время выполнения в том случае, если операнд typeid() является разадресованным указателем на полиморфный тип, например: #include <iostream.h> # include <type infо.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(). Эта функция должна выполняться, в про- противном случае динамический тип возвращаемого из f() значения нельзя будет определить. Функция-член before() Функция-член before() используется для сравнения упорядоченности типов (не путать с порядком объяв- объявления или иерархическим порядком). Вывод из функции зависит от реализации. В листинге 9.3 показано применение функции before(). Листинг 9.3. Функция-член beforeQ iinclude <iostream.h> iinclude <typeinfo.h> class Window //тип полиморфного класса { public: virtual void show(){); class DialogBox:public Window { public: virtual void show(){}; template <class T,class U> void f(T t,U u) { cout«typeid(t) .name ()«" before "« typeid(u) .name ()«" : "« typeid(t) .before (typeid(u) ) «endu- «endure turn; } 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 ?(рс[О],рс); //во время компиляции Window* pw=new Window; Window* pd=new DialogBox; g(*pw,*pd); //во время выполнения return 0; В Visual C++ 5 этот пример породит следующий вывод: double before int:l 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 <typeinfо.h> class Window { public: Window() i 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. Неправильное использование typeidQ Хотя оператор typeid() представляет собой полезный механизм, его следует применять только при не- необходимости. Когда проверка типа времени выполнения не является реально необходимой, нужно выпол- выполнить проверку типа времени компиляции, поскольку она более эффективна. Например, когда речь не идет о полиморфных типах, то применение typeidQ абсолютно не требуется. Кроме того, только потому, что
Манипулирование типами объектов Глава 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 <Type>(Object) Аргумент Туре — это тип, к которому выполняется приведение. Аргумент Object — это преобразуемый объект. Туре должен быть указателем или ссылкой на тип определенного класса. Аргумент Object должен быть выражением для указателя или ссылки. В случае успеха dynamic_cast преобразует Object к желаемому типу. Если приведение указателя неудачно, то возвращаемый указатель будет иметь значение 0. Если неудачно приведение ссылки, то возбуждается исключение bad_cast. Поскольку может быть возбуждено исключение, то оператор dynamic_cast гораздо безопаснее по сравнению с традиционным приведением. Когда терпит неудачу обычное приведение, то оно не указывает на ошибку — конечно, до тех пор, пока программа не будет запущена и не выявит множество ошибок. Использование оператора dynamic_cast также называется безопасным приведением типа. Преобразование указателя или ссылки порожденного класса в указатель или ссылку базового класса выполняется во время компиляции, а результат направляется в указатель или ссылку на подобъект базово- базового класса. Такое преобразование называется преобразованием вверх (upcast). В листинге 9.5 приведен пример приложений, которые используют dynamic_cast для выполнения приве- приведения вниз (downcast). Листинг также показывает пример межиерархического приведения с использованием dynamic_cast. Листинг 9.5. Безопасное приведение типа с использованием dynamiccast #include <typeinfо.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; //попытка приведения BitMapS rb=dynamic_cast<BitMapS>(*pc);
Манипулирование типами объектов Глава 9 cout« "The resulting reference's type: "«typeid(rb) .name ()«endl; //попытка приведения через иерархию Pictures rp=dynamic_cast<Picture&>(*pc); cout« "The resulting pointer's type: "«typeid(rp) .name ()«endl; } catch (const bad_cast&) { cout«"dynamic_cast () f ailed"«endl ; return 1 ; } return 0; } int main() В Visual C++ программа породит следующий вывод: The resulting reference's type:class BitMap The resulting pointer's type:class Picture Здесь указатель *рс фактически указывает на объект типа 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 //тип полиморфного класса. { public: virtual void show() {/*...*/} class DialogBox:public Window { public: virtual void repaintControls() < cout«typeid(*this) . name () «endl ;
Вопросы реализации Часть 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); //правильно //. . . ) В этом примере dynamic_cast() применяется к указателю рс для Control с целью привести его к указа- указателю BitMap. Конечно, это работает только в том случае, если нет неоднозначности при преобразовании от Control к BitMap. Следующий оператор будет забракован при компиляции, поскольку Control является виртуальным ба- базовым классом класса BitMap: BitMap* pbl=(BitMap*)pc; //ошибка Использование dynamic_cast() в конструкторах и деструкторах Оператор dynamic_cast() можно использовать при конструировании и разрушении объектов. Если опе- операнд dynamic_cast() является объектом конструирования или разрушения, то он не может выполнить при- приведения. В листинге 9.7 показан пример применения dynamic_cast() в конструкторе.
Манипулирование типами объектов Глава 9 Листинг 9.7. Использование dynamic_cast() в конарукторе #include <iostream.h> iinclude <typeinfо.h> class base { public: base() ; virtual void f(); class derived : public base { public: derived(){} }; base::base () { try < dynamic_cast<derived?>(*this); //сбой } catch(const bad_cast&) { cout«"bad_cast in base: :base() "«endl; ) ) void base: :f () { try { dynamic_cast<derivedS>(*this); cout«"successful dynamic_cast in base: : f () "«endl; } catch(const bad_castS) { cout«"bad_cast in base: : f () "«endl; } } int main () { derived d; Эта программа сгенерирует следующий вывод: bad_cast in base::base() successful dynamic_cast in base::f() Во время конструирования объекта d в main() первый раз подобъект base конструируется с помощью конструктора по умолчанию basc::basc(). Таким образом, во время конструирования base информация о порожденном объекте во время выполнения внутри basc::basc() недоступна. В результате обращение к dynamic_cast() в конструкторе потерпит неудачу. С другой стороны, внутри функции base::f() обращение к dynamic_cast() будет успешным, поскольку порожденный объект d к этому моменту полностью сконстру- сконструирован. Операторы typeidQ и dynamiccast Операторы typeid() и dynamic_cast() весьма похожи и применяются во многих похожих ситуациях. Со- Совместно они обеспечивают манипулирование объектами во время выполнения. Тем не менее, между ними существует несколько различий: ¦ Оператор dynamic_cast() не работает с неклассовыми типами, в отличие от оператора typeid().
^^ Вопросы реализации Часть II Для использования оператора dynamic_cast() указанный класс должен быть полиморфным, т.е. он должен содержать как минимум одну виртуальную функцию-член. С оператором typeid() операнд нео- необязательно должен быть полиморфным. Оператор typeid() может сказать только о типе объекта. Оператор dynamic_cast() может сказать, яв- является ли объект указанным классом или классом, порожденным от указанного класса. Другие операторы приведения В последующих разделах описаны три оператора приведения, не выполняющие проверок времени вы- выполнения и не ограниченные базовым или порожденными классами в одной и той же иерархии полимор- полиморфных классов. Хотя в центре внимания этой главы находится тема манипулирования объектами во время выполнения, последующее обсуждение важно, если вы желаете иметь полное представление об операторах приведения типа в языке C++. Оператор static_cast() Оператор static_cast() применяется для определенных пользователем, стандартных или неявных преоб- преобразований типов. В отличие от dynaraic_cast(), оператор static_cast() используется в контексте типа объекта времени компиляции. Синтаксис оператора static_cast() подобен синтаксису оператора dynamic_cast(): static_cast<Type>(Object); Здесь и Туре, и Object должны быть известны во время компиляции. Если один тип можно преобразо- преобразовать в другой каким-то методом, который уже обеспечивается языком C++, то выполнение такого преоб- преобразования с помощью static_cast() будет аналогичным. Ниже приведен пример, не требующий пояснений: void f() ( 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() j Basel* pbl=new DerivedClass; DerivedClass* pd=static_cast<DerivedClass*>(pbl) ; if (Pd) { cout «"The resulting pointer's type:["« typeid(pd) . name {)«"]" 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 value3 : "<<value3«endl; //... } 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(), staticcastQ или 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() iinclude <iostream.h> #include <typeinfo.h> class myClass ( public: myClass(int v) :_val(v) ,_count@) { } ; -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 A О; class В { public: virtual void f()O ); class С : private В {>; int main() { A* pa=new A; В* рЫ=(В*)ра; //нет ошибок времени компиляции В* pb2=static_cast<B*>(pa); //ошибка времени компиляции С* pc=new С; В* рЬЗ=(В*)рс; //нет ошибок времени компиляции BS pb4=dynamic_cast<B&>(*pc); //ошибка времени компиляции ) В этом примере следующий оператор будет компилироваться успешно, несмотря на то, что он преобра- преобразует класс А к несвязанному классу В: В* рЫ=(В*)ра; //нет ошибок времени компиляции Такое приведение может быть во время выполнения по настоящему опасно. Кроме того, такую ошибку нелегко обнаружить при чтении кода. Точно так же данная строка не будет отмечена как ошибочная даже в том случае, если программа по- попытается получить доступ к приватной базе В или С: В* рЬЗ=(В*)рс; //нет ошибок времени компиляции Как видно, такие неправильные преобразования "отлавливаются" при использовании операторов при- приведения нового стиля. Резюме Неявное приведение производного указателя на базовый класс является одним из наиболее часто упот- употребляемых средств языка C++. Однако иногда вы будете попадать в ситуацию, в которой почувствуете, что могли бы написать эффективную программу, если бы знали (во время выполнения) точный тип объекта, указываемого базовым указателем. RTTI предоставляет подобную информацию и доказала свою полезность многим программистам. Фактически большинство коммерческих библиотек реализуют ту или иную форму виртуальной функционально-базированной RTTI. Хотя RTTI представляет собой большую услугу, ее часто используют неправильно, как новички, так и опытные программисты. Замена виртуальных функций опе- оператором rypeid() или dynamic_cast() — является самым распространенным случаем ошибочного примене- применения RTTI. Как и большинство механизмов в языке C++, RTTI не имеет каких-либо механизмов самозащиты от неправильного употребления, и если вы определенно желаете сделать ошибку, то это вполне возможно.
Настройка производительности приложения В ЭТОЙ ГЛАВЕ Функции inline вне определений классов Как избежать раскрытия программного кода реализации в распространяемых header-файлах Анализ стоимости виртуальных функций и виртуальных базовых классов Компромиссы RTTI Управление памятью для временных объектов
Настройка производительности приложения Глава 10 Среди разработчиков программ нет единого мнения относительно настройки приложений. Одни, учи- учитывая мощь современных компьютеров, полагают, что настройка не нужна. Эта группа специалистов также полагает, что всю возможную оптимизацию программы должен выполнять компилятор, другие считают, что конструирование важно для повышения производительности приложений. Когда мы реализуем конст- конструкцию, то выбираем оптимальные алгоритмы и используем механизмы производительности языка, по- поскольку не можем предсказать конфигурацию компьютеров потенциальных пользователей и поскольку компилятор не всегда знает, как выполнять оптимизацию программы. В этой главе описываются функции inline, реализация программного кода в header-файлах, виртуальные функции и виртуальные базовые классы, информация о типе времени выполнения и временные объекты. Функции inline вне определений классов Ключевое слово inline подразумевает, что компилятор должен расширять тело функции всякий раз, когда он сталкивается с обращением к такой функции. Расширение уменьшает накладные расходы на вызов функции, поскольку вызова как такового и нет. Обратите внимание, на слово "подразумевает". Компилято- Компилятору может быть неудобно вставлять функцию в строку. Почему компилятор может не вставить функцию, мы обсудим в этой главе далее. Принятый стиль диктует, что вы должны обеспечить определение функции-члена inline вне объявления класса. Такой подход предохраняет определение класса от фрагментирования и облегчает чтение. Нет необ- необходимости применять ключевое слово inline к объявлению функции-члена. Однако обязательно применяйте ключевое слово inline к определению функции-члена. Следующий фрагмент кода демонстрирует стиль: class Object { public: inline void f ( ) ; // нет в объявлении класса! }; Этот пример поднимает вопрос, касающийся расположения определения функции-члена inline. Где его разместить? Хорошо, компилятор может вставить в строку функцию только в том случае, если он видит ее определение. Наиболее логичным местом ддя размещения определения является header-файл, содержащий определение класса. Автор помещает определения функций inline в отдельные header-файлы. Они не обяза- обязательно являются header-файлами в буквальном смысле, а реально являются реализацией файлов, заданных расширениями .срр, Лес или л. Вы можете использовать любое расширение. Только обязательно используй- используйте стандартное название. Такие header-файлы inline затем вставляются в конец header-файла, содержащего определение класса. Это хороший стиль. Следование такой практике разделяет интерфейс и реализацию. Это же облегчает поддержку. Если вы решили не использовать header-файлов inline, то поместите функ- функции-члены inline после объявления класса. И вновь, такой подход обеспечивает чистое разделение объяв- объявления класса и его реализации. Вот простой пример: // вводной файл: Object.h class Object. { public: void f( ) , } ; #include "object.i" // вводной файл: Object.i inline void Object::f( ) С целью расширить пример также используются макросы для включения и отключения вставки функ- функций в строку. В своем header-файле автор сделал следующее: // вводной файл: Object.fa class Object { public: void f( ) ; } ; fifdef DO INLIHE
Вопросы реализации Часть II #include "object.i" iendif Затем осталось лишь возбудить переключатель -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)+l] ) { 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 { i ?( 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 idefine DOUBLE_IT(a) (a * а) //Если вы сделаете это, int i = 5 ; int j = DOUBLE_IT(i) ; //препроцессор расширит его int j = E * 5) ; Здесь нет ничего удивительного. А теперь рассмотрим другой пример: int j = DOUBLE_ITA0 + 10) ; //препроцессор расширит макрос int j = A0+10 * 10+10) ; Это не то, что мы хотели. Вот еще один оператор, который породит интересные результаты: int j = DOUBLE_IT(++i) ; Данный оператор расширяется в следующий: int j = (++i * ++i) ; И это не то, что мы хотели. Если же в обеих ситуациях использовать функцию inline, то результаты будут такими, ка- какими мы и предполагали. Используйте макросы только для простых подстановок. Еще лучше, используйте 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. Вызовы виртуальной и невиртуальной функций tinclude <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" void nvf( ) { std: :cout « "Derived: :nvf ) ; int main( ) { Base aBase ; Derived aDerived ; Base *basePtrToDerived = SaDerived ; basePtrToDerived->vf( ) ; basePtrToDerived->nvf( ) ; return 0 ; « std: -.endl ; } « std:.endl ; } При выполнении программы из листинга 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 void vfuncOne( ) { } ; virtual void vfuncTwo( ) { } ; virtual void vfuncThree( ) { } ; private: int value ; } ; class Derived : public Base public: virtual void vf uncOne (){}>' virtual void vfuncTwo( ) { } ; // виртуальная функция vfuncThree не перекрывается private: int value ; В этом примере одна VTBL — для Base и одна — для Derived. Таблицы VTBL для Base и Derived распределяют про- пространство для хранения трех указателей. Вы можете сказать: "Да, но функция vfuncTree не была переопределена в Derived". Это правда, но указатель все равно существует и указывает на vfuncTree Base. Обратите внимание, что каждый класс имеет только один объявленный атрибут и что тело каждой функ- функции пусто. Предположим, что во время выполнения вы реа- реализовали по одному объекту для каждого класса. В результате этого будут добавлены 8 дополнительных байтов (для VPTR) для двух объектов. Накладные расходы на пространство могут быть ощутимы, если, например, вы строите размещенную систему или другое критичное к пространству приложение. Тем не менее, выгоды от использования виртуального механизма должны превзойти затраты. На рис. 10.1 показано графичес- графическое представление двух классов. Имейте в виду, что для невиртуальных функций в табли- таблице VTBL входов указателя нет. Конструкторы также не имеют входов в VTBL. Помните, если виртуальная функция не пе- перекрывается в порожденном классе, то указатель функции будет указывать на виртуальную функцию в базовом классе. Объекты типа Object Объекты типа Derived указатель на объект type info указатель на виртуальные функции-члены указатель на объект type info указатель на виртуальные функции-члены РИСУНОК 10.1. Графическое описание виртуальной таблицы и указателя.
Настройка производительности приложения Глава 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. Вновь компилятор не в состоянии определить правильную версию са11Ме() для вызова. Во избежание нео- неоднозначности и здесь придется использовать оператор разрешения области видимости. В объявление са11Ме() и в OneDerivied, и в TwoDerived вовлечен базовый вопрос конструкции. Если бы функциональность, обеспечиваемая функцией са11Ме(), могла использоваться совместно, то объявление и реализацию следовало бы перенести в Base. Другой путь решения проблемы заключается в объявлении и определении функции са11Ме() в классе UltimatelyDerived. Такой подход позволит спрятать версии са11Ме() и в 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 Появилось по 4 лишних байта в OneDerivied и TwoDerived. Дополнительные байты появились потому, что классы OneDeriried и 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 используется для получения информации об объекте во время выполнения программы. Иденти- Идентификационный механи-j.v. времени выполнения реализуется с учетом полиморфных типов. Это подразумева- подразумевает, что fc.iai.с должен содержать как минимум одну виртуальную функцию. Обсуждение виртуальных функций,
Вопросы реализации Часть 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); // предотвращение копирования type_info & operator=(const type_infoS) ; // предотвращение присваивания }; Функция-член before используется для раскрытия места объекта в последовательности. Эта последова- последовательность не раскрывает информации об иерархии наследования, поэтому вы не должны использовать ее для исследования отношений между объектами. Упорядоченная последовательность также зависит от реа- реализации, поэтому не считайте, что результаты будут одинаковыми при использовании продуктов разных поставщиков. Получение имени объекта может быть полезным для целей отладки. Атрибут name хранится как NULL- определенная строка и возвращается функцией-членом name. Функции-члены 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 ) ) // ходит как обезьяна else if( typeid( animal ) == typeid( Elephant ) ) // ходит как слон } Как упоминалось ранее, этот программный код будет выглядеть знакомым тем, кто работал на языках Smalltalk или Pascal. Автору не нравится подобное использование RTTI, и вы не должны им злоупотреблять. Виртуальные функции представляют собой более элегантную логику времени выполнения такого типа. Существует и другая веская причина, по которой вы должны избегать механизма RTTI: поддержка про- программного кода. Рассмотрим, что потребовалось бы, если бы к иерархии добавился еще один тип Animal. Пришлось бы пересмотреть всю логику RTTI, сделать соответствующие расширения и затем перекомпили- перекомпилировать весь код. Издержки, связанные с производительностью и надежностью для использования операто- операторов if...else, также высоки. RTTI обеспечивает безопасное приведение для полиморфных типов. Однако для неполиморфных объек- объектов поведение не определено. Реально вы, скорее всего, получите информацию статического типа для объек- объекта, переданного typeid. Вот пример программного кода, который использует иерархию неполиморфных классов:
Настройка производительности приложения Глава 10 Animal *pa = new Tiger ; const typeid fetheType = typeid( *pa ) ; if( theType -- typeid( Animal ) ) // строка 3 cout « "It's only an Animal!" ; else if( theTyp-: -- typeid ( Tiger ) ) cout <.< "It's a Tiger, run!" ; // никогда не выполняется! Оператор if v, третьей строке всегда разрешается в значение true. Оператор typeid работает для встроенных типов языка. Хотя автор не видит реальной пользы от его применения, но вы. тем не менее, можете его использовать. В следующем примере все операторы if будут выполнять соемвекчвующие операторы: char aChar = ' с' ; if ( typeid ( aChar ) == typeid ( char ) ) cout « "It's e char!" ; iff typeid( fiaChar ) == typeid( char * ) ) cout << "It's a char pointer!" ; if( typeid( ' e' ) —— typeid( char ) ) cout « "Guess what, another char!" ; Возможно, вы знакомы с оператором dynamic_cast, который определяет, безопасно ли приведение. Рассмотрим следующий пример, считая, что класс Animal и его порожденные классы являются полимор- полиморфными: Animal *pa = 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"; Аргумент для dynamiccast в угловых скобках представляет собой тип, к которому вы пытаетесь выпол- выполнить приведение. Если приведение успешно, то будет получен требуемый тип. Если приведение неудачно, то возвращается указатель NULL. И в этом случае автор предпочитает использовать виртуальные функции вместо dynamiccast. При работе с виртуальными функциями не требуется проверять, опасно ли приведе- приведение, — обращение всегда разрешается в какую-нибудь виртуальную функцию в иерархии. Используйте механизм RTT! экономно. Важно знать характеристики RTTI, чтобы правильно его приме- применять. Следует иметь представление и о недостатках RTTI и знать, когда правильно использовать виртуаль- виртуальные функции, виртуальные базовые классы и RTTI. Кроме того, полезно уметь оценить затраты, связанные с использованием каждого механизма. Управление памятью для временных объектов Компилятор может преподнести сюрприз многими способами. Сюрприз, который получал любой про- программист, заключается в том, что компиляторы имеют тенденцию создавать временные конструкции. Вы даже можете и не знать, что компилятор создал что-то временное. Две наиболее распространенные ситуа- ситуации — это когда функция должна возвратить объект и когда объект передается функции. Мы также иссле- исследуем и несколько других причин, по которым компиляторы создают временные объекты. Одно из свойств временного объекта заключается в том, что его нельзя видеть невооруженным глазом. Вы не всегда можече увидеть временный объект, глядя на исходный программный код. Однако концепту- концептуально временный объект можно видеть, если понимать, как их создает компилятор. Временный объект всегда невидим в программном коде. Временные объекты рассматриваются как неименованные. Другими словами, имя явно им никогда не дается. Компилятор генерирует имя для временного объекта "за кулисами". Само собой разумеется, что вы не должны о них беспокоиться, поскольку обо всем заботится компилятор. Вре- Временные объекты не создаются из кучи, если, конечно, вы закодировали конструктор, распределяющий память динамически. Необходимо также следить за распределением памяти в функциях-операторах. И пос- последнее, что необходимо пояснить, прежде чем двигаться дальше, — это то, что мы не говорим о времен- временных переменных, коюрые создаются внутри функции или блока программного кода. Теперь создадим класс Integer и рабочий нршраммный код: class Integer ( public. Integer( )
Вопросы реализации Часть II Integer( const Integer & ) ; Integer( int valueln ) ; Integer( int valueln, int addedValue ) ; integer operator+( const Integer Slhs, const Integer Srhs ) void operator=( const Integer 6 ) ; Integer value = 2 ; // первая строка (работающий программный код) for( int i = 0; i < 100; i++ ) value = value + 2 ; Последние три строки программного кода совершенно очевидны? С помощью первой строки констру- конструируется Integer и используется оператор присваивания, который позволяет поместить в объект значение 2. Однако сначала компилятор должен преобразовать константу 2 в объект Integer. Компилятор делает это, используя конструктор, принимающий целый аргумент. Нагрузку на компилятор можно сократить таким образом: Integer valueB) ; Теперь посмотрим на цикл. Именно выражение цикла и представляет собой узкое место. Разделим выра- выражение и фрагмент за фрагментом разберем, что же происходит. Прежде всего, существует постоянное выражение с правой стороны 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 Slhs, const Integer Srhs ) { Integer temp = Ihs.value ( ) + rhs.value ( ) ; return temp ; } Этот код можно рассматривать как очень типичную реализацию. Создается экземпляр локального объекта Integer для хранения результата сложения, и затем экземпляр используется в операторе return. Если бы заменить Integer, например, родным типом int, то результирующий исходный программный код выглядел бы действительно очень естественно. Конечно, код, сгенерированный компилятором, был бы незначительным. К сожалению, код, сгенерированный для Integer::operator+, совершенно не такой. Вам требуется сокра- сократить издержки времени исполнения на создание временных объектов. Для этого необходимо использовать явный конструктор класса. Член operator+ будет выглядеть следующим образом: Integer Integer::operator-)-( const Integer Slhs, const Integer Srhs ) { return Integer( Ihs.value( ), rhs.value( ) ) ;
Настройка производительности приложения Глава 10 Скорее всего, вы думаете, что данный программный код по-прежнему генерирует временный объект для возврата. Отличие при использовании именованного конструктора состоит в том, что компилятор мо- может оптимизировать временные объекты, т.е. делать то, что вы определенно от него хотите. Знание подоб- подобного обстоятельства позволяет вам усилить контроль за созданием временных объектов. (В данном случае следовало бы говорить за устранением, а не за созданием.) По сути, компилятор строит объект для возвра- возврата по месту. Поставщик компилятора может реализовать, а может и не реализовать оптимизацию имено- именованного возвращаемого значения. Подробности — в документации. Теперь, когда вы понимаете, что такое временные объекты, следует быть настороже относительно про- программного кода, который передает данные функциям. Следует повторно изучить код, который возвращает объекты. Скорее всего, компилятор создает временные объекты, если вы не использовали технику оптими- оптимизации именованного возвращаемого значения. Рассмотрите такое положение: если вы можете обеспечить более эффективную реализацию, то компилятор отблагодарит вас, породив более эффективный программный код. Резюме В этой главе были исследованы функции inline, виртуальные функции, виртуальные базовые классы, RTTI и временные объекты. Теперь вы можете оценить затраты, связанные с каждой из перечисленных конструкций. Ключевое слово inline является только намеком компилятору. Вы знаете несколько причин, по которым компилятор может отказаться встраивать функцию в строку. Кроме того, было исследовано несколько по- побочных эффектов встраивания функций в строку. Вы теперь имеете представление об эффектах раскрытия кода реализации в header-файлах. Подумайте о том, как реализован код в ваших программах и о том, как его можно усовершенствовать. В этой главе вы узнали, как используются виртуальные функции и виртуальные базовые классы, и о том, какое влияние оказывают виртуальные функции на таблицу VTBL. Теперь вы знаете, как виртуальные базовые классы могут устранить копии базового класса в случае применения множественного наследова- наследования. RTTI представляет собой механизм, предназначенный для специальных программных ситуаций, но вы не должны его применять для замены виртуального механизма. Помните, что RTTI подрывает полимор- полиморфизм и свидетельствует о плохом конструировании. Не забывайте о временных объектах и о том, что компилятор молча создает эти объекты. Выясните, поддерживает ли ваш компилятор механизм оптимизации именованных возвращаемых значений. Если этот механизм поддерживается, то используйте его всегда, когда это возможно. 10 Зак. 53
Обработка данных ЧАСТЬ ?&¦•..¦.:! k ¦ В ЭТОЙ ЧАСТИ Рекурсия и рекурсивные структуры данных Использование методов сортировки Алгоритмы поиска данных Хеширование и синтаксический анализ
Рекурсия и рекурсивные структуры данных В ЭТОЙ ГЛАВЕ Что такое рекурсия Рекурсивные структуры Обход рекурсивной структуры с помощью рекурсивной функции Цикл и хвостовая рекурсия Непрямая рекурсия Рекурсия и стек Отладка рекурсивных функций
Рекурсия и рекурсивные структуры данных Глава 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"; re turn 0;
Обработка данных Часть III Ниже показан пример работы программы, в котором текст, вводимый пользователем, выделен полу- полужирным курсивным шрифтом: Which Fibonacci number do you want? 14 Fibonacci A4) is 377 Which Fibonacci number do you want? 40 Fibonacci D0) is 102334155 ПРИМЕЧАНИЕ В программе, показанной в листинге 11,1, не рекомендуется вводить номера больше 40, потому что время обработки :> возрастает экспоненциально. Вычисление числа Фибоначчи с номером 40 на компьютере автора требует 10 секунд.;-;: Вычисление числа Фибоначчи номер 41 выполняется почти вдвое дольше, и с каждым последующим увеличением номера на единицу время вычисления почти удваивается. •¦',.' ¦. ;?- Определение функции fib() очень похоже на математическое определение этой операции. Кого-то может насторожить то, что функция вызывает саму себя. Как она узнает, что возвращать? Компьютер решает эту задачу, создавая новую копию параметра п для каждого вызова функции. Вместо функции, вызывающей саму себя, просто представьте себе, что вам нужно написать функцию, которая принимает один параметр, назовем его п. Если п меньше или равняется 2, то функция возвращает 1. Если п больше чем 2, то функция возвращает сумму sqrt(n-l)+sqrt(n-2), где sqrt() — функция извлечения квадратного корня. Это не вызовет никаких проблем, вам не нужно беспокоиться о том, что происходит внутри функции sqrt. Это просто функция из библиотеки math, и вы верите, что она правильно выполняет свою задачу. Написание рекурсивной функции выполняется точно так же. Если требуется выполнить рекурсивный вызов, забудьте об этам\ Не беспокойтесь о том, что вы выполняете вызов той же самой функции, над которой работаете и написание которой еще не завершено. Просто поверьте в то, что эта функция выпол- выполнит свою задачу. Когда вы напишете и отладите эту функцию, ваше доверие будет вознаграждено. Остановка рекурсии Это одно из главных требований для написания рекурсивной функции. В какой-то момент следует оста- остановить рекурсию, ибо в противном случае она приведет к тому же эффекту, что и бесконечный цикл, по крайней мере, до переполнения стека. (Стек — это специальная область памяти, в которой компьютер хра- хранит свою позицию на каждом уровне рекурсии. Стек будет рассмотрен более подробно далее в этой главе.) Чтобы гарантировать остановку рекурсии, необходимо выполнить два требования. Во-первых, в функции должна быть часть, которая завершает ее без каких-либо рекурсивных вызовов. Условия, при которых вы- выполняется эта часть функции, называются условиями завершения. Второе требование, необходимое для оста- остановки рекурсии, состоит в том, что в каждом последующем рекурсивном вызове программа некоторым образом приближается к условиям завершения. Если есть какой-либо способ прекращения рекурсивных вызовов и каждый последующий рекурсивный вызов приближает вас к условиям завершения, то рекурсия не сможет продолжаться бесконечно. В примере с числами Фибоначчи, когда параметр меньше или равен 2, функция просто возвращает 1 без рекурсивных вызовов. Таким образом, (п <= 2) — это условие завершения. Если п больше 2, то при каждом последующем рекурсивном вызове используется параметр, который меньше текущего значения п. Дру- Другими словами, каждый вызов приближает нас к условию завершения. Поскольку два описанных выше требова- требования выполняются, можно быть уверенным в том, что рекурсия Фибоначчи в конце концов завершится. Рекурсивные структуры Рекурсивная структура — это такая структура, которая указывает на подоб- подобную себе структуру. Хорошим примером служит иерархия каталогов на компью- компьютере: каждый каталог содержит список файлов, и одним из частных случаев файла является каталог. Диаграмма классов этого отношения показана на рис. 11.1. Это запись на универсальном языке моделирования (Universal Modeling Language) для двух классов: File и Directory. Стрелка показывает, что Directory является дочерним классом File, отношением это есть. Стрелка с ромбом пока- показывает отношение один-ко-многим между Directory и File (ромб означает агре- агрегирование или принадлежность). Другими словами, Directory владеет нулем или РИСУНОК 11.1. Диаграмма классов файлов и каталогов.
Рекурсия и рекурсивные структуры данных Глава 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 & name) : m_name(name) {} const string & getNameQ const { return m_name; } void setName(const string & 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 *> mJFileList; typedef list<File *>::iterator Filelter; }; Directory::-Directory() { while (!m_FileLiSt.empty()) { delete mJFileList. 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 & name) : m_name(name) {} -File О О const string & getName() const { return m_name; } void setName(const string & name) { m_name = name; ) virtual void Display(ostream & os, int level = 1, const string & prefix = ""); private: string m_name; }; class Directory : public File ( public: Directory(const string & name) : File(name) (} -Directory () ; virtual void Display(ostream & os, int level = 1, const string & 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 & os, int level, const string & prefix) File::Display(os, level, prefix); string newPrefix = prefix + getName() + ":"; for (Filelter iter = mJFileList.begin (); iter != mJFileList. end (); iter++) (*iter)->Display(os, level + 1, newPrefix); Directory: :-Directory() while (! mJFileList. empty ()) delete mJFileList. front (); mJFileList.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 l: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 3. TheDir:SubDir 2:Sub File 6 2. TheDir:File 3 2. TheDir-.File 4 Здесь к двум классам, показанным в начале этой главы, добавлен метод DisplayO. Поскольку это вир- виртуальный метод, можно проходить по списку указателей File и вызывать DisplayO Для каждого из них, зная, что будет вызван правильный метод DisplayO. Функция DisplayO в этом примере использует три параметра: выходной поток, уровень рекурсии и префикс имени файла. Префикс — это накопленное множество имен каталогов, через которые мы прошли перед вызовом данной функции. Только не думайте об этих именах рекурсивно. Просто предположите, что функция, которую вы пишете, получает правильное значение в своем параметре и передает правильное значение в следующем вызове функции. В функции File::Display() необходимо просто отобразить в выходном потоке номер рекурсии, префикс и имя файла. Функция Directory::Display() более сложная. Чтобы отобразить каталог, сначала его следует отобразить как файл, а затем вызвать DisplayO Для всех файлов, содержащихся в этом каталоге. Таким образом, для файлов, которые в действительности являются вложенными каталогами, эта функция вызывает сама себя. Но запомните первый принцип написания рекурсивных функций: не думайте при выполнении этой задачи о нескольких уровнях сразу; это простая функция DisplayO, которая выполняет определенную операцию для всех вложенных файлов, и следует поверить в то, что эта операция работает. Посмотрите на однострочное тело цикла for в Directory::Display(). (Здесь выражение (*iter) возвращает текущий элемент списка в соответствии с текущей позицией счетчика. Это выражение возвращает File *, потому что список определен как lisKFile *>.) В цикле for выполняется шаг рекурсии. Первый параметр достаточно простой: в нем просто передается поток ostream, в который следует направлять вывод. Во вто- втором параметре передается уровень рекурсии, который мы уменьшаем на единицу при переходе на следую- следующий уровень. При вызове рекурсивной функции для следующего уровня она снова уменьшит значение level, но нам не нужно об этом беспокоиться. Сейчас достаточно знать только то, что следующий уровень рекур- рекурсии будет на единицу больше, чем текущий уровень. Последний параметр — это префикс. Мы знаем, что в этом параметре передается объединенный список имен каталогов, которые мы прошли до текущей позиции. Не думайте о том, как был построен этот спи- список, просто поверьте в то, что он правильный. Чтобы передать правильный префикс во все вложенные файлы, следует построить новый префикс, объединив текущий префикс с текущим именем. Таким обра- образом, выполняется корректное накопление префиксов: каждый уровень получает правильный префикс и передает дальше также правильный префикс. В этом рассуждении недостает только одного шага: необходимо обеспечить передачу правильных значе- значений на самом первом уровне. С этой целью мы определяем для параметров функции значения по умолча- умолчанию, соответствующие правильным значениям для первого уровня. В данном случае это значения level = 1 и prefix = "", как можно увидеть в объявлении функции DisplayO для обоих классов, 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 & data = "", LLNode * subList = NULL) : myData(data), mySubList(subList) {} -LLNode() {delete mySubList,} const string & GetFirstData() const {return myData;} void Sortedlnsert(const string & data); bool isInList(const string & data) const; int Count() const; void Display (ostream & os) const; void ReverseDisplay(ostream & os) const; private: string myData; LLNode * mySubList; }; void LLNode::Sortedlnsert(const string & data) { if (NULL == mySubList II data <= mySubList-XSetFirstData ()) mySubList = new LLNode(data, mySubList); else mySubList->SortedInsert(data);
Обработка данных Часть III bool LLNode:risInList(const string & 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); } void LLNode::ReverseDisplay(oatream & 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("Hilma"); 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". Если данные не должны находиться перед подсписком, то их следует вставить где-то внутри подсписка. На этом этапе мы просто возлагаем эту зада чу на подсписок и предполагаем, что подсписок сделает все правильно. Теперь, когда эта функция написана, следует немного подумать о рекурсии. Во-первых, выполняется ли условие наличия в функции пути, не содержащего рекурсивных вызовов? Да, если имеет место тести- тестирование. Во-вторых, всегда ли мы приближаемся к этому условию завершения? Да, потому что известно, что мы всегда достигнем узла, не содержащего подсписка. Наконец, правильно ли начинается рекурсия'' Это проблематичный вопрос, если следует вставить элемент перед самым первым узлом. В связи с этим предположим, что первый узел в каждом полном списке является пустым узлом, не содержащим никаких данных. Благодаря этому можно исключить ситуацию, когда новые данные нужно вставить перед первым узлом. Когда клиент хочет создать один из таких связанных списков, он создает отдельный специальный узел без данных. Обратите внимание на то, что при вставке нового узла создается полностью новый под- подсписок. Новый узел содержит данные и старый подсписок, благодаря чему он становится полноценным списком, который может использоваться в качестве подсписка. Теперь рассмотрим две функции Display() и обратим внимание на простое отличие между ними. Первые проверки предназначены для обхода ложного узла, который мы добавили для упрощения процесса вставки. Изучите эти функции и воспринимайте их не как отображение данных списка, а как отображение данных Node и указание подсписку Node отобразить самого себя. Хвостовая рекурсия В листинге 11.4 содержится хороший пример хвостовой рекурсии в функции LLNode::DispIay() (но не в функции ReverseDisplayO). В этой функции рекурсивный вызов является самым последним оператором перед возвратом функции. Это пример хвостовой рекурсии. Хвостовая рекурсия важна для разработчиков компи- компиляторов, потому что любую хвостовую рекурсию можно легко заменить циклом. Эта замена приводи! к заметной оптимизации, так как экономится время для помещения аргументов в стек и выполнения вызова функции. Компиляторы могут уверенно выполнить такую оптимизацию, потому что функция уже не будет использовать свои переменные после выполнения рекурсивного вызова. Нет необходимости в создании новых копий всех локальных переменных, если известно, что они уже не потребуются. Ниже показан эквивалент C++ для программного кода, сгенерированного оптимизирующим компиля- компилятором для функции LLNode::Display(): void LLNode::Display(ostream & os) const { const LLNode * p = this; TailRecursionLabell: if ("" != p->myData) oa « p->myData « endl; if (NULL != p->mySubList) { p = p->mySubList; goto TailRecursionLabell; ПРИМЕЧАНИЕ He присылайте автору письма с замечаниями об операторе goto. Первоначально автор использовал здесь цикл do. .while с оператором break, но показанный пример намного ближе к тому, что в действительности создает компилятор. От- Откомпилированная версия любого существенного фрагмента программного кода содержит сотни операторов goto.
Обработка данных Часть III Когда вам следует думать о хвостовой рекурсии? Вероятно, никогда, если только вы не работаете в компании, которая создает компиляторы. При составлении функции Count() автор намеренно написал ее так, чтобы компилятор смог ее оптимизировать. Однако если вы случайно бросите фразу хвостовая рекур- рекурсия в разговоре на работе, то вы будете выглядеть знатоком. Непрямая рекурсия Когда функция не вызывает саму себя, а вызывает другую функцию, которая затем может вызвать пер- первую функцию, это называется непрямой рекурсией. В примере File/Directory в начале этой главы можно было создать метод Display Children () для класса Directory. Затем можно было изменить метод Directory::Display() и вызывать в нем File::Display() и DisplayChildren(). В листинге 11.5 показано новое объявление класса и два его метода. Как видно, метод DisplayQ не вы- вызывает сам себя, но он вызывает DisplayChildrenQ. Метод DisplayChildren() не вызывает сам себя, но вызы- вызывает Display(). Это пример непрямой рекурсии. Заметьте, это не полный листинг, в нем показаны только фрагменты, интересующие нас в данный момент. Листинг 11.5. Классы file и Directory, использующие непрямую рекурсию class Directory : public File < public: Directory(const string & name) : File(name) {} ~Directory () ; virtual void Display(ostream & os, int level = 1, const string fi prefix = ""); void DisplayChildren(ostream & os, int level, const string & 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 & os, int level, const string & prefix) { File::Display(os, level, prefix); string newPrefix = prefix + getName() + " : "; DisplayChildren(os, level, newPrefix); ) void Directory: -.DisplayChildren (ostream & os , int level, const string & prefix) { for (Filelter iter = m_FileList.begin() ; iter != m_FileList.end(); iter++) (*iter)->Display(os, level + 1, prefix); Рекурсия и стек При рассмотрении функции Фибоначчи мы отметили, что компьютер без проблем отслеживает теку- текущий уровень рекурсии и создает новые копии всех переменных для каждого рекурсивного уровня. Для это- этого компьютер использует стек. Стек — это блок памяти, выделенной компилятором для хранения функции, когда она вызывает другую функцию. Стек можно сравнить со стопкой тарелок, в которой можно добавить тарелку только на вершину стопки и можно снять тарелку только с вершины стопки. Нельзя вставить или удалить что-либо с середины такой стопки или стека.
Рекурсия и рекурсивные структуры данных Глава 11 При выполнении вызова функции компьютер помещает в стек (на его вершину, конечно) четыре пе- перечисленных ниже элемента. Заметьте, этот процесс справедлив для вызовов всех функций, а не только для рекурсивных: 1. (в исходной функции) Параметры, передаваемые в функцию. 2. (при переходе к другой функции) Положение в программном коде для возврата после завершения вы- вызываемой функции. 3. (в вызываемой функции) Ваш "базовый указатель", который можно будет восстановить после заверше- завершения функции. После этого функция устанавливает базовый указатель в текущее положение в стеке для своего собственного использования. Базовый указатель используется для нахождения параметров, переданных в функцию, а также локальных переменных, для которых функция будет выделять па- память в стеке. 4. (в вызываемой функции) Пространство для локальных переменных, необходимых функции. Естественно, ваша функция при своем вызове выполняет точно такие же шаги. Каждый раз, когда функция обращается к одному из переданных ей параметров или к одной из своих локальных переменных, она получает их значения из памяти по адресу, определенному базовым указателем, плюс или минус не- некоторое известное значение. При завершении функции эти шаги выполняются в обратном порядке: снача- сначала функция освобождает память для локальных переменных, затем восстанавливает базовый указатель вызвавшей функции и возвращается к сохраненному адресу кода (одновременно выталкивая этот адрес из стека). Наконец, функция освобождает память для переданных параметров. Когда функция вызывает сама себя, этот процесс не изменяется. Фактически компилятор даже не знает ни о каком отличии. Но мы видим, что этот процесс приводит к нужному эффекту. Каждый раз, когда рекурсивная функция вызывает саму себя, в стеке сохраняются новые копии параметров, адрес возврата, если мы находимся в середине вычисления выражения (как в функции Фибоначчи), и новая функция выделяет свое собственное пространство для всех своих локальных переменных. Например, в функции Directory"DispIay() память для переменной newPrefix вьщеляется заново при каждом вызове функции. При этом новые экземпляры этой переменной не влияют на другие экземпляры, которые остаются доступными в предыдущих вызовах рекурсивной функции. Теперь можно представить эффект бесконечной рекурсии, т.е. рекурсии, для которой разработчик не обеспечил условия завершения. При каждом очередном рекурсивном вызове расходуется некоторое про- пространство в стеке. Даже если создать рекурсивную функцию без параметров и без локальных переменных, все равно нельзя избежать необходимости хранить базовый указатель и адрес возврата. Если функция не прекратит рекурсию, то используемый объем в стеке будет увеличиваться, пока не израсходуется вся вы- выделенная для него память. Обычно пространство стека примыкает к глобальной памяти, в которую может вторгнуться компьютер в отчаянной попытке сохранить базовый указатель и адрес возврата. В конце кон- концов, компьютер перезапишет какие-либо критичные данные или вызовет ошибку доступа к памяти. В лю- любом случае ваша программа остановится (если не зависнет компьютер). Поскольку это обычно не является желаемым результатом, следует очень тщательно проверять условия завершения. К аналогичному результату можно прийти и при правильной реализации условия завершения. Выполне- Выполнение программы или работа компьютера может остановиться, если рекурсия потребует больше стекового пространства, чем выделено. Если на каждом уровне рекурсии необходимо выделять память для больших переменных, то имеет смысл размещать их в куче, а в локальных переменных хранить только указатели. Если вам нужно подсчитать требования к памяти, то запомните, что имеет значение только максимальная глубина рекурсии, а не общее количество вызовов рекурсивной функции. В частности, рассмотрим пример Directory/File с рекурсивной функцией Directory"Display(). Хотя эта функция вызывается для каждого су- существующего каталога, каталоги SubDir 1 и SubDir 2 никогда не будут храниться в стеке одновременно. Программа сначала полностью обработает каталог SubDir 1, потом освободит используемую память в стеке и только после этого перейдет к каталогу SubDir 2. Отладка рекурсивных функций Теперь мы научились забывать о рекурсии при написании рекурсивных функций и структур. К сожале- сожалению, при отладке придется снова вспомнить о рекурсии, потому что не так легко установить точку оста- останова внутри одной функции, не указав точку останова внутри вызываемой функции. Нельзя даже выполнить шаг через вызов функции, потому что отладчик незаметно устанавливает временную точку останова при шаге через функцию. Эта точка останова активизируется в последующем рекурсивном вызове и собьет вас с толку.
Обработка данных Часть III Эту проблему можно обойти, воспользовавшись несколькими механизмами. В одном из этих механизмов используется дополнительный параметр рекурсивной функции, который позволяет всегда знать, где вы находитесь. Можно даже создать условные точки останова (если отладчик их поддерживает), которые акти- активизируются в определенной строке программного кода, когда переменная level принимает какое-то опре- определенное значение. Другой механизм, позволяющий обойти проблему с указателями, состоит в создании полной копии рекурсивной функции, которая будет вызываться извне. Эту функцию можно безопасно трассировать, по- потому что она не является по-настоящему рекурсивной. Конечно, при использовании такого подхода необ- необходимо тщательно следить за полным соответствием двух копий функции, а также необходимо избавиться от дополнительной функции после завершения отладки. Резюме Рекурсия долгое время служила источником страха и беспокойства для многих разработчиков программ- программного обеспечения. Однако если вы начинаете забывать о рекурсии при написании класса или функции, то разработка становится проще. Несмотря на необходимость проверки условия завершения рекурсии и не- небольшие хлопоты с отладкой, в целом в рекурсии нет ничего страшного.
Использование методов сортировки В ЭТОЙ ГЛАВЕ Анализ производительности алгоритмов Пузырьковая сортировка Сортировка вставками Сортировка выбором Быстрая сортировка Сортировка слиянием Сортировка по методу Шелла Пирамидальная сортировка Выбор метода сортировки Генерирование тестовых данных
Обработка данных Часть III Сортировка является одной из наиболее важных функций во многих приложениях. В этой главе рассмат- рассматривается несколько методов, используемых для сортировки данных, а также сравнивается объем работы, необходимый при использовании каждого из этих методов. По мере чтения главы вы поймете, что эффек- эффективность определенного метода зависит от нескольких факторов — реализации программного кода и сор- сортируемых данных. Перед выбором конкретного метода сортировки следует протестировать его на различных исходных данных, в частности, на примере данных, которые будут сортироваться на практике. При сортировке определенного набора данных (записей) используется ключевое значение, содержащее- содержащееся в каждой записи. Это ключевое значение может быть просто отдельным элементом записи или строится на основе нескольких элементов записи. Однако применительно к процессу сортировки вся запись пред- представляется только своим ключевым значением. Сортировка позволяет в конечном счете переупорядочить записи на основе их ключевых значений. Следовательно, сортировка может выполняться для повышения производительности методов поиска. Зависимость эффективности сортировки от входных данных станет более понятной при рассмотрении различных реализаций. Для определенной реализации метода сортировки худший и лучший случаи опреде- определяются природой входных данных. Например, для некоторых методов сортировки объем выполненной ра- работы для уже отсортированных данных является линейным, но этот же алгоритм может выполнять работу порядка п2 для записей, отсортированных в обратном порядке. Анализ производительности алгоритмов В отличие от оценки производительности аппаратного обеспечения, производительность алгоритмов для сортировки и поиска не измеряется по времени выполнения, потому что эти алгоритмы зависят от данных. Такие управляемые данными алгоритмы оцениваются в отношении размера входных данных с использова- использованием символа О. Эта запись называется О-записью. В зависимости от типа алгоритма выполненная работа (и, следовательно, производительность алгоритма) определяется различными характеристиками. Например, для таких алгоритмов сортировки, как пузырьковая сортировка, очень важным показателем является коли- количество сравнений, необходимых для перемещения элементов. При сортировке набора из N элементов алго- алгоритм выполняет множество сравнений и меняет элементы местами, если они расположены не в том порядке. Производительность пузырьковой сортировки определяется как О((п2-п)/2). При сортировке списка, состо- состоящего из N элементов, программа обходит список по одному разу для каждого элемента списка; после каждого обхода один из элементов помещается на свое правильное место и не учитывается при последую- последующих обходах. Поиск алгоритма с лучшей производительностью равнозначен поиску алгоритма с лучшей оценкой Big-O. Алгоритмы с производительностью, худшей, чем п2, считаются непригодными к использованию; следует стремиться к О(п). Необходимо понять, что степень п (размер входных данных) важнее, чем константа перед п. Другими словами, можно сказать, что О(п) не намного хуже, чем ОCп), но намного хуже, чем О(п2). В этом можно легко убедиться, подставив определенные значения п. Компьютер, который выполняет одну операцию за 1 не, может потребовать 90 лет для выполнения алгоритма с оценкой О(п3), но этот же компьютер может потребовать только пару часов для выполнения алгоритма с оценкой ОA5000000п). В связи с этим в О-записи константа не учитывается, а учитывается только степень. Следовательно, про- производительность О(п) сравнима с производительностью ОDп). Сравнение среднего, худшего и лучшего случаев Следует быть очень осторожным, чтобы не попасть в ловушку О-записи. Имейте в виду, что производи- производительность зависит от самих данных, а не только от их размера. Это означает, что алгоритм может иметь один порядок на одном типе входных данных и другой порядок — на другом типе входных данных. Всегда следует оценивать алгоритм, рассматривая его поведение в среднем — в сценарии лучшего и худшего слу- случаев. Эти различные сценарии основаны на различных типах данных, которые заставляют выполнять мень- меньше или больше работы для достижения желаемого результата. Не удивляйтесь, если встретите алгоритм, который выполняется очень хорошо в среднем случае, но настолько плохо работает в худшем случае, что приходится выбрать другой алгоритм, который хуже выполняется в среднем, но и в худшем случае рабо- работает не так плохо. В табл. 12.1 показано сравнение двух алгоритмов для различных значений п.
Использование методов сортировки Глава 12 Таблица 12.1. Сравнение NLOGN и W Количество элементов (п) Средний случай: nlogn Худший случай: пг 64 2048 24 384 22528 64 4096 4194304 шшшшш Производительность лучшего случая обычно не анализируется, потому что она не таит потенциальной опасности для ••-.. алгоритма. Однако все же следует рассмотреть сценарий лучшего случая, потому что лучший случай может помочь v проникнуть в суть алгоритма, чтобы извлечь из него максимальные преимущества. Например, если в методе пузырь- : ковой сортировки входные данные уже отсортированы (лучший случай), то сортировка выполняется с производитель- \ ностью О(п}; однако в среднем пузырьковая сортировка выполняется с производительностью О(п2). Вполне возможно, ' что тип данных, которые будут обрабатываться алгоритмом, известен заранее. В таком случае можно выбрать алго- алгоритм, для которого эти данные представят лучший случай. Оптимизация алгоритма — это попытка сократить время, необходимое для выполнения алгоритмом наи- наиболее частых операций. Для алгоритмов сортировки оптимизация дает возможность сократить время, необ- необходимое для выполнения сравнения и перестановки. Этого можно добиться, выполняя такие операции в памяти, а не на жестком диске. Легко увидеть, что реализация кода играет очень важную роль в оптими- оптимизации алгоритма. Однако общее поведение алгоритма не изменяется с реализацией. Другими словами, не- независимо то того, как реализован алгоритм пузырьковой сортировки, его производительность все равно составляет О(п2). Запись Big-О позволяет сравнить алгоритмы, предположив, что все аспекты реализованы одинаково. Необходимо понимать, что Big-О имеет значение только для достаточно больших значений п, потому что для малых значений п константа может оказать существенное влияние на эффективность метода. Напри- Например, если А,(п)=20п, а А2(п)=3000п, то оба алгоритма имеют оценку О(п), но в действительности А, в 150 раз быстрее, чем Aj. Следовательно, можно заключить, что при прочих равных условиях алгоритм О(п) всегда превосходит алгоритм О(п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 // Цель: Сортировка массива, состоящего иэ п элементов, расположенных в порядке возрастания, // с использованием метода пузырьковой сортировки Sinclude <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"; 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 && swapped == 1; i++) < // Если в конце итерации не нужно выполнять никаких перестановок, // это означает, что список уже отсортирован и цикл можно завершить, 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+1] = temp; swapped = 1; show_list("List sorted in ascending order using bubble sort"); > //main( ) : Тест пузырьковой сортировки void main(void) ( // Создание нового объекта с помощью метода bubble_sort sort sort_objE); static int unsorted_list[] = E4,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++) { = 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("List 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 ) && ( j >= 0))
Использование методов сортировки Глава 12 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_objE); static int unsorted_list[] = {54,6,26,73,1); sort_obj.insertion_sort(unsorted_list); ) Обратите внимание, в отсортированной части списка создается "дырка". Эта "дырка" образуется при копировании данных в позицию temp и при копировании S[j] в S[j+1], если S[j] > temp. "Дырка" переме- перемещается в обратном направлении по отсортированному списку элементов до тех пор, пока не найдет пра- правильную позицию для нового элемента. После этого цикл while завершается, и элемент из временной переменной вставляется в свою правильную позицию. Анализ сортировки вставками Сортировка вставками обладает следующими характеристиками: ¦ После каждой итерации только один элемент данных помещается в свою правильную позицию. ¦ При сортировке вставками выполняется меньше перестановок, чем в пузырьковой сортировке. Это очевидно, потому что в методе пузырьковой сортировки наибольший элемент всегда "всплывает" к вершине; сортировка вставками перемещает "дырку" через уже отсортированные элементы и в боль- большинстве случаев может сместить только половину отсортированных элементов перед тем, как найти место для новой вставки. ¦ Наихудший случай — когда все элементы данных отсортированы в обратном порядке. ¦ Наилучший случай — когда элементы почти отсортированы в правильном порядке. ¦ Сортировка вставками легко реализуется. Очень важным вопросом проектирования сортировки вставками является выбор направления просмот- просмотра отсортированных записей. Другими словами, просматривать ли отсортированные записи от первой к последней или наоборот? Это решение имеет большое значение; правильный ответ зависит от самих дан- данных, которые нужно отсортировать. Чтобы понять важность направления просмотра, предположим, что список отсортирован в обратном порядке {5, 4, 3, 2, 1}. Если просматривать отсортированные записи в направлении от последней к первой, то можно увидеть, что на каждой итерации следующий элемент для вставки меньше уже отсортированных элементов и сортировка требует меньшего количества сравнений, чем при просмотре от первой к последней записи. С другой стороны, если список почти отсортирован, то просмотр в направлении от первой к последней записи выполнится быстрее и потребует меньшего коли- количества сравнений, чем при просмотре от последней к первой записи. Сортировка выбором Алгоритм сортировки выбором основан на использовании элементов в качестве ключей для сравнения, при этом в конце каждого просмотра только один элемент помещается в свою правильную позицию. Этот алгоритм простой, но очень неэффективный, потому что он не учитывает частично или полностью отсор- отсортированных списков. Другими словами, если список уже полностью или частично отсортирован, то при сортировке выбором будет выполнено то же количество сравнений, что и в случайном списке, при этом такая ситуация не послужит для повышения производительности (как могло быть при пузырьковой сорти- сортировке). В результате для метода сортировки выбором не существует сценария лучшего случая. Для списка из
Обработка данных Часть III п элементов при сортировке выбором всегда выполняется (п-1) итерация. Ниже показан псевдокод для алгоритма сортировки выбором: Для j от 0 до (п-1) выполнить следующие шаги: 1. Для элементов от ХЦ+1] до Х[п-1] выполнить сравнение по ключам и найти наименьший элемент; назовем этот элемент XflowerJ. 2. Поменять местами X[lower] и X[n-1]. X[lower] теперь находится в своей отсортированной позиции. После завершения этого цикла список будет отсортирован. Реализация сортировки выбором на языке C++ показана в листинге 12.3. Листинг 12.3. Реализация сортировки выбором // Программа: selection_sort.cpp // Автор: 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 ahow_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_liat(input); show_liat("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 1c = j+1; 1c < n ; lc++) { if (X[k] < X[lowest]) 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 E); static int unsorted_list[] = E4,6,26,73,1); sort_obj.selection_sort(unsorted_list); Анализ сортировки выбором В листинге 12.3 можно увидеть, что для каждой итерации внешнего цикла внутренний цикл выполняет не более (n-j) сравнений и внешний цикл выполняется (п-1) раз. Общее количество сравнений для сорти- сортировки выбором можно вычислить так: (п-1) + (п-2) + (п-З) +...+ [п-(п-1)] = п(п-1)/2 = п72 - п/2 = О(п2) . Таким образом, это сценарий худшего случая. Сценарий лучшего случая в действительности требует такого же количества сравнений, потому что сортировка выбором не учитывает частичной сортировки, которая может существовать в исходных данных. Быстрая сортировка Быстрая сортировка — это наиболее эффективный алгоритм внутренней сортировки. Его производи- производительность сильно зависит от выбора точки разбиения. При быстрой сортировке используются три стратегии: 1. Массив разбивается на меньшие подмассивы. 2. Подмассивы сортируются. 3. Отсортированные подмассивы объединяются. Быструю сортировку можно реализовать несколькими способами, но цель каждого подхода заключается в выборе элемента данных и помещении его в правильную позицию (этот элемент называется точкой раз- разбиения), таким образом, чтобы все элементы слева от точки разбиения оказались меньше (или предше- предшествовали) точки разбиения, а все элементы справа от точки разбиения оказались больше (или следовали за) точки разбиения. Выбор точки разбиения и метод, используемый для разбиения массива, оказывают большое влияние на общую производительность реализации. Остановимся на рекурсивной реализации бы- быстрой сортировки. Псевдокод этой реализации можно записать следующим образом: 1. Выбрать элемент данных и сделать его точкой разбиения таким образом, чтобы он разбивал массив на левый и правый подмассивы, как было описано выше. 2. Применить быструю сортировку к левому подмассиву. 3. Применить быструю сортировку к правому подмассиву. Выбор точки разбиения имеет критическое значение. В эффективном методе разбиения может использо- использоваться следующая стратегия: 1. Выбрать ключ первого элемента данных как точку разбиения. Другими словами, Pivot = X[first]. 2. Инициализировать два указателя поиска, i и j, чтобы i = first (наименьший индекс подмассива), а j = last (наибольший индекс подмассива). 3. Используя указатель поиска i, найти, начиная слева, элемент данных, который больше или равен точке разбиения. Это можно выполнить, используя следующий псевдокод: Пока A[i] <= Pivot и i < last, продолжать увеличение i на 1 иначе прекратить увеличение i
Обработка данных Часть III 4. Используя указатель поиска j, найти, начиная справа, элемент данных, который меньше или равен точке разбиения. Это можно выполнить, используя следующий псевдокод". Пока A[j] >= Pivot и j > last, продолжать уменьшение j на 1 иначе прекратить уменьшение j 5. Если i < j, то поменять местами A[i] и АЦ]. 6. Повторить пункты 2-4, пока не выполнится условие i > j. 7. Поменять местами точку разбиения и АЦ]. После выполнения п.7 точка разбиения будет расположена в своей правильной позиции. В листинге 12.4 показана реализация этого кода на языке C++. Листинг 12.4. Реализация быстрой сортировки // Программа: quiclc_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++) = 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[firat]; //Переменная i используется для просмотра слева. int i = first; //Переменная j используется для просмотра справа. int j = last; while (i < j) < // Поиск элемента, который больше или равен выбранной точке // разбиения. Поиск слева. while (X[i] <= pivot ?& i < last)
Использование методов сортировки Глава 12 i += 1; // Поиск элемента, который меньше или равен выбранной // точке разбиения. Поиск справа. while (X[j] >= pivot SS 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); quicksort (j+1, last); //main( ) : Тест быстрой сортировки void main(void) { sort sort_obj E); static int unsorted_listt] = E4,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@,4); sort_obj.show_list("List sorted in ascending order using quick sort"); Анализ быстрой сортировки Быструю сортировку следует рассмотреть одной из первых при выборе метода внутренней сортировки. Этот алгоритм содержит сложную фазу разбиения и простую фазу слияния. В лучшем случае выполняется работа порядка nlogjii; в худшем случае выполненная работа эквивалентна работе при сортировке выбо- выбором, т.е. О(п2). Производительность быстрой сортировки сильно зависит от выбора точки разбиения. Сортировка слиянием Сортировка слиянием — очень эффективный метод внешней сортировки, когда все сортируемые эле- элементы не вмещаются в доступной памяти и для выполнения сортировки приходится использовать жесткий диск. При сортировке слиянием используется та же стратегия, что и при быстрой сортировке: 1. Разбить файл на меньшие файлы. 2. Отсортировать меньшие файлы. 3. Слить отсортированные файлы. При быстрой сортировке разбиение является сложным шагом, а слияние — простым; при сортировке слиянием разбиение представляет собой простой шаг, а слияние — более сложный. Существует несколько разновидностей сортировки слиянием, основанных на стратегии, используемой для фаз разбиения и сли- слияния. Мы остановимся на сортировке слиянием, использующей итеративный метод. Этот метод состоит из следующих этапов: 1. Открыть файл для сортировки (to_sort) в режиме чтения/записи, а также открыть два временных файла для записи. 2. Фаза разбиения: Скопировать элементы файла to_sort попеременно во временные файлы. 3. Фаза слияния: Сравнить каждый элемент из двух временных файлов и записать сначала меньший, а затем больший из двух элементов обратно в файл to_sort.
Обработка данных Часть III 4. Фаза разбиения: Скопировать элементы из файла to_sort попеременно по два элемента во временные файлы. 5. Фаза слияния: Сравнить каждую группу из двух элементов из временных файлов и записать сначала меньшую из двух групп элементов, а затем — большую обратно в файл to_sort. 6. Повторить фазы разбиения и слияния с размером групп 21 при I = 2,3,4,...log2n. В результате получим отсортированный файл to_sort. Процесс сортировки слиянием проиллюстрирован на рис. 12.1. Ш [Ю [I] ГТЦП ГП [Г! РИСУНОК 12.1. Сортировка слиянием. 4 5 | File.sorted ИЩИ] Реализация сортировки слиянием на языке C++ показана в листинге 12.5. Листинг 12.5. Реализация сортировки слиянием //Программа: merge_sort.срр //Автор: Megh Thakkar //Цель: Сортировка массива с использованием сортировки слиянием #include <stdio.h> #include <stdlib.h> #include <iostream.h> #define MIN(x,y) ( (x <= y) ? x : у ) enum STATUS {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 ("subfiIe2.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; int curr_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", &data_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); 1 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", &read_from_filel) status = SORTED; status_filel = END_OF FILE; EOF) { if (fscanf (sub_file2, "%d", &read_from_file2) status = SORTED; status_file2 = END_OF FILE; EOF) { last_considered = MIN (read_f roin_f ilel, read_froin_f ile2) ; while (status_filel != END_OF_FILE ?? status_file2 != END_OF_FILE) { if (read_from_filel <= read_from_file2 ?& 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) status_filel = END_OF_FILE; ) else if (read_from_file2 <= read_from_filel && read_from_file2 >= last_considered) { // Запись значений из sub_file2 fprintf (sorted file, "%d ", read_from_file2);
Обработка данных Часть III last_considered = read_from_file2; if (fscanf (sub_file2, "%d", &read_from_file2) == EOF) status_file2 = END_OF_FILE; } else if (read_from_filel >= last_considered) { // Запись значений иэ subfilel fprintf (sorted__file, "%d ", read_from_filel); last_considered = read_from_filel; if (fscanf (sub_filel, "%d", SreadjEromJEilel) == EOF) status__filel = 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 != EKD_OF_FILE) { //Теперь можно записать остаток sub_file2 fprintf (sorted_file, "%d ", read_from_file2); if (fscanf (sub_file2, "%d", Sread_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 элементов. Легко увидеть, что, поскольку список разбивается пополам, мы получаем не более Iog2n подсписков для списка из п элементов. В худшем случае сортировка слиянием имеет производительность порядка nlog2n — намного лучше, чем большинство других методов. Сортировка по методу Шелла Сортировка по методу Шелла является разновидностью сортировки вставками. Сортировка вставкой обладает ограничением, потому что позволяет сравнивать только смежные элементы, в результате при каждой перестановке элемент перемещается только на одну позицию. Элементы, далеко удаленные от своих пра- правильных позиций, требуют многих проходов для попадания на свои правильные позиции. Сортировка по методу Шелла допускает "скачки" в порядке ее выполнения. Для этого записи делятся на чередующиеся
Использование методов сортировки Глава 12 группы, и каждая группа сортируется с использованием метода сортировки вставками. Это разделение выполняется с использование значения приращения (назовем его h); значение приращения делит первона- первоначальный массив на h подмассивов. Затем эти подмассивы сортируются с использованием метода сортиров- сортировки вставками или пузырьковой сортировки. Этот шаг называется h-сортировкой. Процесс h-сортировки повторяется с использованием уменьшающегося значения h, пока это значение не окажется равным 1. Во время последнего прохода список уже почти отсортирован, и сортировка вставками на финальном проходе завершает этот процесс, образуя отсортированный список. В связи с этим сортировка по методу Шелла называется также сортировкой уменьшающегося приращения. Выбор правильной последовательности значений для h очень важен. Проведено множество исследова- исследований выбора значений для h без четкого указания, как осуществлять этот выбор. В реализации на C++ в листинге 12.6 используется следующая стратегия для выбора значения h. Эта стратегия считается эффек- эффективной: 1. Пусть h, = 1 и п равняется количеству сортируемых элементов. 2. Пусть hs+1 = 3 * hs + 1. Остановиться, когда h > n/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[ J ) ; 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] = inputfi]; } 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 { int j; temp = X[i]; for ( j = i-h ; j >«0 ; j — h ) { if (temp < X[j]) { Xtj+h] = x[j]; } else break; } X[j+h] = temp; } } show_list("List sorted using shell sort: "); } //main( ) : Тест сортировки по методу Шелла void main(void) { // Создание нового объекта класса sort sort sort_objA0); // Список для сортировки static int unsorted_list[] = {54,6,26,73,1,43,51,83,5,28}; // Вызов метода сортировки по методу Шелла sort_obj.shell_sort(unsorted_list); Анализ сортировки по методу Шелла Сортировке по методу Шелла посвящено множество исследований, и показано, что производительность худшего случая находится в интервале от п1М до 1.бп|И. Эффективность этого метода сильно зависит от выбора последовательности значений для h. Как уже отмечалось ранее, не существует идеальной формулы для выбора этой последовательности, но хорошо подобранные последовательности показывают произво- производительность сортировки по методу Шелла порядка пAо^пJ. Сортировка по методу Шелла практически нечувствительна к исходным данным и показывает худшую производительность, чем пузырьковая сорти- сортировка и сортировка вставками, когда исходные данные почти отсортированы. Однако для случайных набо- наборов данных сортировку Шелла следует рассматривать в числе первых. Пирамидальная сортировка При пирамидальной сортировке массив рассматривается как двоичное дерево с определенными характе- характеристиками. Этот метод, по существу, переупорядочивает элементы дерева, чтобы значение каждого узла было больше или равным значениям дочерних узлов. Имейте в виду, что узлы в пирамиде не упорядочены. Однако условие того, что любой родительский узел содержит большие элементы данных, чем все дочер- дочерние узлы, гарантирует, что самый большой элемент находится на вершине пирамиды. Если затем удалить этот элемент из вершины и поменять его местами с последним элементом массива, то этот элемент попа- попадет на свою правильную отсортированную позицию. После этого можно повторно заставить выполниться условие пирамиды: найти наибольший элемент данных среди оставшихся элементов. Повторяя эту проце- процедуру, мы, в конце концов, отсортируем массив. Реализация пирамидальной сортировки на языке 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::Beap (int n) { X = пен int tn + 1] ; heap_size = n; void Heap::load_list(int input[ ]) for (int i = 1; i <= 10; = 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) && (done ==0)) { //Поиск максимального элемента между левым и правым дочерними элементами if ((j < limit) SS (X[j] < X[j + 1])) j++; //Сравнение найденного максимального элемента с наибольшим значением //Если наибольший элемент имеет максимальное значение, то условие пирамиды выполнено //и нужно выйти. if (biggest >= X[j]) done= 1; else { j 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] и Х[1] int temp = X[i + 1] ; X[l] = temp; // Поместить корень в отсортированную позицию inputti] = X[i + 1] ; establish_heap_property A, i) ; ПЗак. 53
Обработка данных Часть II } input[O] } 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_objA0); // Массив элементов данных для сортировки 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: ") , Анализ пирамидальной сортировки Метод пирамидальной сортировки сильно зависит от функции bui!d_max_heaP()- Эта функция использу- используется для построения пирамиды и помещения максимального значения на вершину, чтобы его можно было удалить из дальнейшего рассмотрения. Эта функция выполняет работу О(п). Эффективная реализация этого метода дает оценку O(nlog2n). Выбор метода сортировки В этой главе было рассмотрено несколько алгоритмов для реализации сортировки. Каждый из этих алго- алгоритмов представляет собой определенный компромисс, потому что сценарии лучшего и худшего случаев для каждого из них зависят от самих сортируемых данных. Однако некоторые алгоритмы в целом лучше других, потому что они минимизируют количество сравнений, количество перестановок или количество просмотров. Вообще, при небольшом количестве сортируемых записей (меньше 1000) можно выбрать лю- любой из улучшенных методов (таких, как быстрая сортировка или пирамидальная сортировка и сортировка по методу Шелла), потому что, как показано в табл. 12.2, производительность О-записи для быстрой и пирамидальной сортировки составляет O(nlog2n), а для сортировки по методу Шелла — О(п'-25). Эти оцен- оценки производительности не сильно отличаются для небольших значений п. Следует также иметь в виду, что эффективность любого метода сортировки зависит от его конкретной реализации. В табл. 12.3 приведены сравнительные характеристики различных видов техники сортировки. Например, для сортировки вставками сценарий лучшего случая (уже отсортированные записи) легко мо- может стать сценарием худшего случая при плохой реализации (при просмотре от последней записи к пер- первой, а не наоборот). Пирамидальная сортировка и сортировка по методу Шелла выгодны тем, что они не зависят от исходных данных и для них фактически нет худших случаев. Продуманная реализация быстрой сортировки часто может оказаться намного быстрее других улучшенных методов. Таблица 12.2. Сравнение методов сортировки Метод сортировки Худший случай Лучший случай Пузырьковая сортировка О(п2) О(п) Сортировка вставками О(п2) О(п) Сортировка выбором О(п2) О(п2)
Использование методов сортировки Глава 12 Метод сортировки Худший случай Лучший случай Пирамидальная сортировка Сортировка слиянием Быстрая сортировка Сортировка по методу Шелла O(nlog2n) O(nlog2n) O(n2) O(n(log2nJ) O(nlog2n) O(nlog2n) O(nlog2n) Для большого количества элементов данных (больших значений п) рост nlog2n меньше, чем п2. Следо- Следовательно, очень эффективны такие методы сортировки, как пирамидальная сортировка, сортировка слия- слиянием и быстрая сортировка. С другой стороны, для малого количества элементов данных (малых значений п) достаточно эффективными оказываются такие методы, как пузырьковая сортировка, сортировка вставка- вставками и сортировка выбором (см. табл. 12.3). Таблица 12.3. Сравнение характеристик методов сортировки Метод сортировки Преимущества Недостатки Сортировка вставками Пирамидальная сортировка Быстрая сортировка Простой код Стабильная сортировка Сортировка массивов по месту О(п) сравнений в лучшем случае Сортировка массивов по месту Всегда производительность O(nlog2n) и относительно быстрое выполнение Самая быстрая в среднем случае Сортировка выбором Сортировка по методу Шелла Простой код Стабильная сортировка Сортировка массивов по месту Количество перестановок О(п) Простой код Сортировка массивов по месту Производительность худшего случая лучше других методов О(п'-5) О(п2) сравнений в среднем Нестабильная сортировка Сложная сортировка Сложный код Очень плохая производительность в худшем случае Необходимо дополнительное стековое пространство O(logn) Нестабильная сортировка Среднее количество сравнений О(п2) Нестабильная сортировка Пирамидальная сортировка и быстрая сортировка в большинстве случаев лучше сортировки Шелла Генерирование тестовых данных Перед выбором определенной реализации сортировки следует проверить ее на различных исходных дан- данных, чтобы убедиться в оптимальной работе этой реализации при всех условиях. Следует, по меньшей мере, проверить реализацию на данных, отсортированных в прямом порядке, в обратном порядке, в возраста- возрастании-убывании и с дублирующимися записями. Кроме того, следует использовать примерные данные, ко- которые предполагается сортировать на практике. Для генерирования различных типов тестовых данных можно использовать программу, показанную в листинге 12.8. Листинг 12.8. Генерирование тестовых данных для методов сортировки // Программа: test_data.cpp // Автор: 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 = пен 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 ) = 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: oin » n; cout « "\n We will generate numbers in the following orders: \n" « " A) Forward-ordered\n" « " B) Reverse-ordered\n" « " C) Duplicate records\n" « " D) Random ordered records\n"; // Создание объекта для хранения сгенерированных данных test_data test_obj(n); test_obj.forward_ord(n); test_obj.reverse_ord(n); test_obj.duplicate_ord(n); test obj.random ord(n); Резюме Эта глава поможет вам выбрать лучший метод сортировки данных. Следует запомнить, что эффектив- эффективность любого метода сортировки сильно зависит от реализации этого метода и сортируемых данных. Луч- Лучший способ выбрать правильный метод сортировки для определенной задачи — протестировать каждый метод на различных типах исходных данных, в частности, на примере данных, которые будут сортировать- сортироваться на практике.
Алгоритмы поиска данных В ЭТОЙ ГЛАВЕ Линейный поиск Сопоставление с образцом Алгоритмы поиска на графе Внешний поиск
Алгоритмы поиска данных Глава 13 Поиск — это процесс нахождения данных среди набора элементов, который удовлетворяет определен- определенному критерию. Приемлемость общей производительности многих приложений зависит от эффективности используемых методов поиска данных. Конечная цель всех методов поиска одна и та же: найти запрашиваемую информацию. Однако пригод- пригодность определенного метода поиска в конкретной ситуации определяется несколькими факторами, такими как реализация и логика, используемая в этом методе. При сравнении методов поиска необходимо учесть следующие критерии: ¦ Время подготовки. Некоторые методы требуют значительного времени для подготовки среды поиска перед началом самого поиска. Если планируется выполнять поиск в небольшом наборе данных, то время подготовки становится очень важным фактором при выборе метода. в Время поиска. Это время, необходимое для выполнения алгоритма поиска. Основная часть алгорит- алгоритмов имеет оценку О(п), где п — это количество элементов данных, среди которых выполняется поиск. Это линейное время обычно включает в себя время подготовки (х) и время поиска (у). Другими словами, общее время равняется х+у; цель состоит в минимизации х и у. В этом выражении время подготовки равняется х, а время поиска у равняется О(п). При возрастании п время подготовки ста- становится менее значительным фактором при подсчете общего времени. ¦ Необходимость возврата. Одни алгоритмы поиска выполняют простой линейный просмотр элементов данных; другие переходят вперед и назад по набору данных. Этот критерий также следует учесть при выборе алгоритма поиска. Линейный поиск Линейный поиск — это самый простой тип поиска, потому что при его выполнении просто просмат- просматривается множество элементов данных и эти элементы сравниваются с искомыми данными, пока не обна- обнаружится совпадение. Таким образом, линейный поиск прост в реализации, но не всегда эффективен. Он не требует времени на подготовку. Предположим, что нужно найти определенный элемент данных S в неот- неотсортированном массиве X, состоящем из N целочисленных элементов. Этого можно достичь с помощью следующего псевдокода: Пусть i = О Сравнить X[i] с S. Если они совпадают, возвратить 1, иначе увеличить 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 A0); static int list_to_search[] = {54, 6,26,73,1,100,36,41,2,83); cout « " \n" « "C++ Implementation 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_Xey); if (result != -1) cout « "\n" « "Search Result: " « "X[" « result « "] = " « search_Jcey ; 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 > XtN-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 A0); 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 = 0; m = strlen(s) ; n = strlen(t) ; while ( j < m ss i < n) { if ( t[i] != s[j] ) { i -= j-l; j = -i; if (j == m) return (i - m) ; else return i; } Объяснение грубого алгоритма В представленном выше псевдокоде указатель i указывает на символы в тексте; указатель j указывает на символы в образце, который нужно найти. После инициализации эти указатели указывают на начало тек- текста и образца. Переменные шип используются для хранения длины двух строк. Указатели увеличиваются до тех пор, пока они указывают на совпадающие символы и пока не достигнуты концы строк. Если сим- символы не совпадают, то указатели переустанавливаются таким образом, чтобы указатель j указывал на на- начало образца, a i возвратился на следующую позицию после первого символа, совпавшего с образцом. После этого можно найти возможные совпадения в оставшемся тексте. Если достигнут конец образца (j == in), то получаем совпадение, при этом образец начинается в позиции t[i-m]. Если достигнут конец текста без совпадения с образцом (j < га && i < n), значит, совпадений не существует. Анализ грубого алгоритма В сценарии худшего случая все символы в образце (га) проверяются для всех возможных позиций об- образца в тексте (n-m+1). Таким образом, выполняется работа порядка O(m(n-m+l)) = O(mn). Представление образца Образцы могут быть представлены как символы, связанные с помощью следующих основных операций: ¦ Соединение (конкатенация). Два символа считаются соединенными, если они примыкают друг к дру- другу. Совпадение существует только в том случае, если эти два символа в тексте также примыкают друг к другу. Например, АВ означает, что в образце сразу за А следует В. ¦ OR. OR представляется знаком плюс (+). Если два символа объединены операцией OR, то они рас- расцениваются как альтернативы. Другими словами, А+В означает, что в данной позиции находится либо А, либо В. ¦ Замыкание. Замыкание представляется символом звездочки (*); если за символом следует знак замы- замыкания, это означает нуль или более вхождений этого символа.
Обработка данных Часть III Рассмотрим несколько примеров: ¦ (ABC)* означает повторяющиеся образцы ABC. Другими словами, это может быть АВСАВСАВСАВС. Это видно из представления замыкания, которое означает повторение образца ABC. ¦ A(B+C)D означает, что допустимыми образцами являются ABD и ACD. Это следует из применения записи OR, которая означает, что за А следует В или С, а затем следует D. Поиск совпадения с образцом можно упростить с помощью алгоритмов, реализующих конструкцию конечных автоматов, которые могут искать ключевые слова (ключевые слова означают возможные образ- образцы, которые нужно найти). Построение конечных автоматов Образец для поиска можно использовать для построения конечных автоматов. Такие автоматы графи- графически представляются как сеть узлов, в которой каждый узел указывает определенное состояние. Узлы связаны друг с другом в соответствии с образцом. Входной символ, соответствующий переходу, указывается на дуге между узлами. Конструкцию и применение конечных автоматов можно проиллюстрировать на следующих нескольких примерах. Сначала рассмотрим, как можно представить основные операции, а затем увидим, как можно составить целый образец, построив конкретные автоматы, которые можно объединить для об- образования более крупных автоматов: ¦ Распознать один символ — А. Это можно представить с помощью автомата с двумя состояниями: начальным и конечным, переход в конечное состояние при этом осуществляется, когда встречается символ А (рис. 13.1). ¦ Объединить два автомата — Ml и М2. Это можно выполнить, объединив конечное состояние перво- первого автомата и начальное состояние второго (рис. 13.2). -(А)- -М {Ml) and (M2) = >- -{М1}- -{М2}- РИСУНОК 13.1. Автомат с двумя состояниями. РИСУНОК 13.2. Конечный автомат, представляющий конкатенацию. Выполнить операцию OR для двух автоматов — Ml и М2. Операция OR изображается введением но- нового пустого состояния как начального состояния для обоих автоматов и конечного состояния одно- одного автомата, которое также является конечным состоянием второго автомата (рис. 13.3). Выполнить операцию замыкания. Для этого конечное состояние замыкается на начальное состояние (рис. 13.4). -11 -<мц- РИСУНОК 13.3. Конечный автомат, представляющий операцию OR. ПРИМЕЧАНИЕ РИСУНОК 13.4. Конечный автомат, представляющий операцию замыкания. На рисунках 13.1-13.4 используются следующие обозначения: • () —начальное состояние. s • [] — конечное состояние. • (X) — переход, если встречается символ X. • {М}—конечный автомат. Строго следуя этим правилам, можно построить конечный автомат для любого образца. Например, построим конечный автомат для следующего образца: A(BC+DE)F (рис. 13.5). ( ) (А) A) (В) B) (С) C) (F)- РИСУНОК 13.5. Конечный автомат, представляющий A(BC+DE)F. -E) -(D)- -D)- -№)- Переходы, не определенные на рис. 13.5, ведут в состояние FAIL_STATE. Можно также создать таблицу переходов на основе этой диаграммы (табл. 13.1).
Алгоритмы поиска данных Глава 13 Таблица 13.1. Таблица переходов для A(BC+DE)F Состояние Входной символ Следующее_состояние1 Следующее_состояиие2 0 А 1 1 1 2 4 2 С 3 3 3 F 5 5 4 Е 3 3 5 0 0 Эта таблица переходов интерпретируется следующим образом: если автомат находится в состоянии X и читает символ Y, то он переходит в состояние Следующее_состояние1 или Следующее_состояние2. В этой таблице переходов представлен недетерминированный автомат, потому что, когда он находится в состоянии 1, он может перейти в состояние 2 или 4 в зависимости от прочитанного символа. Другими словами, он содержит более одного состояния FAIL_STATE для состояния 1. Можно построить результирующую функцию, которая ищет успешные совпадения, определяемые ко- конечным состоянием в конечном автомате. Конечные автоматы могут быть детерминированными или недетерминированными. В детерминированных автоматах переход из любого состояния можно легко определить на основе следующего входного символа. Недетерминированные автоматы, с другой стороны, обычно представлены операциями OR или замыкани- замыканиями, потому что на основе простого сравнения с одним символом нельзя определить, возможно ли совпа- совпадение с образцом на данном этапе. Псевдокод для реализации такого конечного автомата можно описать следующим образом: State = 0; While ((с = getchar( ) EOF ) if (transition (state, с) IS NOT NOLL) state = transition(state,c) ; if (result(state) IS NOT NOLL) //Совпадение найдено printf("Match found."); В этом псевдокоде используется несколько функций: функция transition(state,c) исследует таблицу пе- переходов и определяет, возможен ли переход, на основе текущего состояния и следующего прочитанного символа. Функция result(state) определяет, указывает ли текущее состояние на достижение совпадения и, следовательно, завершение алгоритма. Этот программный код очень обобщен; его реальная реализация определяет производительность поиска. Алгоритмы поиска на графе Графы можно использовать для представления некоторых повседневных ситуаций. Например, вы запла- запланировали поездку из Киева в Симферополь. Можно поставить следующий вопрос: Какой воздушный марш- маршрут следует избрать, чтобы добраться из Киева в Симферополь быстрее всех? Еще одним примером служит составление расписания работ, когда есть множество задач и множество зависимостей между ними, со- согласно которым выполнение определенной задачи можно начать только после завершения одной или не- нескольких других задач. Конечные автоматы, рассмотренные в предыдущем разделе, также можно считать примерами графов. Рассмотрим несколько простых определений, связанных с графами: ¦ Граф. Это совокупность узлов и соединений между ними. ¦ Узлы. Это объекты, с которыми могут быть связаны имена и другие свойства. ¦ Связи (или дуги). Это соединение между двумя узлами. ¦ Путь. Путь из узла А в узел В — это список узлов, через которые необходимо пройти при переходе из узла А в узел В. Например, на рис. 13.6 ABCD и ACD — это два пути из узла А в узел D. РИСУНОК 13.6. Графические представление задачи определения пути между узлами. (А)
Обработка данных Часть ill н Связный граф. Граф называется связным, если существует путь из одного узла в любой другой узел в этом графе. ш Простой путь. Путь между двумя узлами является простым, если ни один узел в нем не повторяется. Другими словами, такой путь ни через один узел не проходит больше одного раза. и Цикл. Это простой путь, в котором первый и последний узлы совпадают. ¦ Дерево. Это граф, который не содержит циклов. и Двоичное дерево. Это дерево, в котором каждый узел соединен только с тремя другими узлами. Один из этих узлов является родителем данного узла, а два других являются дочерними узлами. Дерево можно легко преобразовать в цикл, добавив к нему только одно дополнительное соединение (это соединение завершит цикл). Граф обладает следующими свойствами: ¦ Дерево с N узлами содержит N-1 соединений. о Граф с N узлами, но менее чем с N-1 соединениями не может быть связными. и Граф с N узлами и более чем с N-1 соединениями обязательно содержит цикл. ¦ Граф с N узлами и с N-1 соединениями не обязательно является деревом. и Граф с N узлами может содержать соединения в количестве от 0 до N(N-l)/2. И Разреженный граф — граф с очень маленьким количеством соединений. в Плотный граф — граф с большим количеством соединений. в Полный граф — граф, который содержит N(N-l)/2 соединений. и Неориентированный граф — граф, в котором соединения между узлами двунаправленны. и В ориентированном графе соединения между узлами однонаправленны. ш Взвешенный граф — граф, в котором соединениям между узлами назначены веса. Вес можно предста- представить как стоимость перехода из узла А в узел В. ш Два узла называются смежными или соседними, если между ними существует соединение. Соедине- Соединение, или ребро, называется инцидентным с узлами А и В. а Степень узла А — это количество инцидентных с ним узлов. и Узел называется изолированным, если его степень равна нулю. Алгоритмы, которые работают с графами, не должны посещать один и тот же узел более одного раза, чтобы увеличить свою эффективность. Для обхода графа широко используются два метода — поиск в глу- глубину и поиск в ширину. Поиск в глубину Поиск в глубину называется также поиском с возвратом. Это название станет понятным из принципа работы этого метода. Рассмотрим неориентированный граф, который начинает свой обход с узла А. Предпо- ложим, что степень этого узла равна d. Это означает, что к узлу А прилегают узлы А,, где i=l,2,3,...,d. Основной принцип этого механизма состоит в пометке узлов, которые уже просмотрены. Поиск в глубину начинается с просмотра узла А, а затем со всех непросмотренных узлов, смежных с узлом А, например, А,. Остальные смежные узлы хранятся в стеке, чтобы их можно было просмотреть позже. После того как будут просмотрены все узлы, смежные с узлами, которые, в свою очередь, являются смежными с узлом At, выполняется возврат назад и просмотр всех остальных непросмотренных смежных узлов А,, где i=2,3,...,d. Этот процесс продолжается до тех пор, пока не будут просмотрены все узлы графа. Поиск в глубину явля- является примером полного перебора, потому что он позволяет исследовать все узлы графа для определения самого лучшего решения задачи. Поиск в глубину можно реализовать как рекурсивно, так и нерекурсивно. Имейте г. виду, что поиск в глубину, по существу, ведется вниз по графу, пока не будет достигнут тупик или узел, который не содержит ни одного непросмотренного смежного узла; после этого он начинает ве- вестись снова вверх, чтобы можно было просмотреть другие дочерние (смежные) узлы родителя. В следующем фрагменте программного кода C++ показана рекурсивная реализация поиска в глубину: void Dept.h_first search (int n ) { int 3. ; 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[i] == FALSE) check(i); 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 ^(?>^ (?) Иллюстрация метода поиска в глубину. На рис. 13.7 поиск в глубину начинается с просмотра и печати узла А. С узлом А смежны узлы В и D, следовательно, теперь можно просмотреть любой из этих узлов. Предположим, что выбран узел В. При поиске в глубину после просмотра и печати узла В другие узлы, смежные с узлом А (в данном случае — D) ос- оставляются для просмотра позже. С узлом В смежны узлы А, С, D, Е и F. Узел А уже просмотрен, поэтому допустим, что выбран и просмотрен узел С. С узлом С смежны узлы В и F. Поскольку узел В уже просмот- просмотрен, то просматривается узел F. После этого просматривается узел Е и наконец, узел D. Таким образом, узлы будут просмотрены в следующем порядке: ABCFED. Путь, пройденный при поиске в глубину, не является уникальным. Если в начале поиска выбрать дру- другой смежный узел, то будет пройден совсем другой путь. Поиск в ширину Поиск в ширину является альтернативным методом обхода графов. Это также метод полного перебора, т.е. обходятся все узлы графа. В отличие от поиска в глубину, при поиске в ширину сначала просматривают все смежные узлы A, (i=l,2,3,...,d) данного узла А, прежде чем перейти к узлам, смежным с узлами А,. Этот метод также помечает просмотренные узлы для повторного просмотра. Псевдокод поиска в ширину: Создать объект queue_obj, который может функционировать как очередь. Инициализировать этот объект очереди пустым значением. Инициализировать массив checked[], чтобы все его элементы были равны FALSE. Начнем с узла А. Присвоить checked [A] = TRUE. Добавить узел А к началу очереди. Выполнить следующие действия, пока объект очереди не пустой: Присвоить current_node = head(queue_obj). Просмотреть все непросмотренные узлы, смежные с current_node. Добавить все узлы, смежные с current_node, в очередь, чтобы их смежные узлы можно было просмотреть позже. Освободить очередь queue_obj. Этот метод можно проиллюстрировать на той же диаграмме, которая использовалась для объяснения работы поиска в глубину (см. рис. 13.7). Начнем, как и прежде, с узла А. После просмотра узла А просмот- просмотрим все его смежные узлы (в данном случае узлы В и D). Теперь можно просмотреть узлы, смежные с узлом В или с узлом D. Например, выберем узел D. После просмотра узлов, смежных с D (в данном случае узла Е), просмотрим узлы, смежные с В. В результате получится путь поиска ADBEFC. Как можно понять из псевдокода и рис. 13.7, в этом методе путь поиска также не уникален, а зависит от выбора следующего узла во время обхода.
Обработка данных Часть III Сравнение поиска в глубину и поиска в ширину Метод поиска в глубину прост для реализации и может быстро привести к хорошему решению. Недо- Недостаток этого метода в том, что можно потратить много времени на просмотр путей, которые не приведут к успеху. Простейшей альтернативой поиску в глубину является поиск в ширину, который позволяет га- гарантированно найти наилучший путь к решению (если оно существует), если количество ветвей является конечным. Чтобы понять это, предположим, что есть путь длины р от исходного узла к конечному. При поиске в ширину сначала будут найдены все узлы на уровне 1, затем все узлы на уровне 2 и т.д. Наконец, будут просмотрены все узлы на уровне р. Таким образом, будет найден не просто путь, а лучший путь. Поиск в ширину обладает двумя недостатками: ¦ Он требует много памяти, поскольку количество узлов возрастает экспоненциально при просмотре последующих уровней. ¦ Если кратчайший путь длинный, то будет просмотрено очень много уровней, что потребует больше времени, чем при поиске в глубину, поскольку поиск в ширину должен проверить все узлы на оп- определенном уровне перед тем, как перейти к следующему уровню. Поиск в глубину лучше, чем поиск в ширину, в тех ситуациях, когда существует множество путей из исходного узла в конечный, но каждый из этих путей очень длинный. Поиск по первому наилучшему совпадению Метод поиска по первому наилучшему совпадению — это интеллектуальный алгоритм, объединяющий поиск в глубину и поиск в ширину в одном методе. При поиске по первому наилучшему совпадению ис- используется лучшее из обоих алгоритмов, он называется так потому, что предлагает исследовать путь, кото- который кажется наиболее многообещающим. Этот алгоритм можно реализовать, выполнив такие действия: 1. Найти узлы, доступные из текущего узла. 2. Применить эвристическую функцию к каждому из этих узлов. Эвристическая функция — это, по суще- существу, определенная пользователем функция, которая на основе исходных данных определяет важ- важность этого узла. Например, каковы шансы найти решение, находясь в этом узле? 3. Выбрать следующий узел, который обладает наилучшими шансами (по результату эвристической фун- функции) быстрее достичь решения. 4. Если решение найдено, можно выйти из алгоритма; иначе сохранить оставшиеся узлы для рассмотре- рассмотрения позже. 5. Присвоить current_node = best_next_node и повторять пункты 1—4 до тех пор, пока не будет найдено решение. ПРИМЕЧАНИЕ Эвристическая функция — это определенная пользователем функция, которая на основе исходных данных определяет важность этого узла. Например, "Каковы шансы выиграть, находясь в этом узле?" или "Каковы шансы найти решение, выйдя из этого узла?" ¦ .'¦ При реализации алгоритма поиска по первому наилучшему совпадению частично выполняется поиск в глубину, пока не будет найден узел, который выглядит достаточно многообещающим. При этом исследу- исследуются остальные смежные узлы по методу поиска в ширину. Выбрав наиболее перспективный узел, снова выполняется поиск в глубину, пока не будет найдено решение. Этот подход в конечном счете позволяет найти решение меньшими усилиями, чем поиск в глубину или в ширину. Рассмотрим граф, представленный на рис. 13.8. Первоначально есть только один узел (узел А), но он связан с четырьмя другими узлами. После применения эвристической функции к этим узлам узел С выби- выбирается как наиболее многообещающий. Узел С связан с двумя другими узлами. Применим эвристическую функцию и к этим узлам. Видно, что узел Е не такой многообещающий, как узел В. Следовательно, при- применим эвристическую функцию к узлам, связанным с узлом В. Этот процесс продолжается, пока не будет найдено решение.
Алгоритмы поиска данных Глава 13 V=1 V=9 V=10 РИСУНОК 13.8. Пример, иллюстрирующий поиск по первому наилучшему совпадению. V=10 \M2 Реализация объектов графов Как известно, граф состоит из множества узлов, связей между этими узлами и стоимостей, сопостав- сопоставленных этим связям. Существует несколько способов реализации таких структур: ¦ Матрица смежности ¦ Массив указателей на одно- или двусвязные списки смежных узлов ¦ Связный список указателей на одно- или двусвязные списки смежных узлов ¦ Массив указателей на одно- или двусвязные списки связей ¦ Связный список указателей на одно- или двусвязные списки связей Наиболее часто используемой структурой среди этого списка является матрица смежности; она обычно представляется как двумерный массив. Деикстра (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 //Цель: Реализация взвешенного ориентированного графа // и алгоритма кратчайшего пути Деикстры. «include <stdio.h> «include <string.h> «include <stdlib.h> «include <iostream.h> class graph {
Обработка данных Часть III private: int adj_matrix[100][100] ; //хранит стоимости, введенные пользователями int 8hortest_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_nodee) < init_adj_matrix(num_nodes); //инициализация матрицы смежности нулями 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; { //Ввод пользователем значений для матрицы смежности
Алгоритмы поиска данных Глава 13 cout « "Is there a direct path from Node " « i « " to Node " « j « " ? : " ; cin » direct; if ( ( direct[0] == 'y' ) II (direct[O] == 'Y')) { 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; // очень большая стоимость void graph::load_shortest_path_matrix( int num_nodes) { int i, j, k; const int LARGE_COST = 999999; for ( j = 1; j <= num_nodes; j++) { for ( i = 1; i <= num_nodes; i++) < for (k = 1; к <= num_nodes; k++) { if ( (shortest_j3ath_matrix[j, k] > 0 ) ) { //Если существует непряной путь между двумя узлами, //который короче найденного ранее пути, то //он становится кратчайшим путем между этими узлами. if ( (shortestj>ath_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] = shortestj>ath_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 можно представить как структуру, содержащую девятиэлементный вектор, который представляет позиции на игровом поле. Кроме того, следует хранить список позиций, которые достижимы из данной позиции, и оценочные значения, которые отражают вероятность возможного выигрыша. Алгоритм состоит из просмотра возможных следующих ходов и выбора наилучшего. Лучший ход опреде- определяет следующим образом: 1. Если следующий ход приводит к победе, то это лучший ход. 2. Если ни один из возможных ходов не приводит к победе, то для каждого из возможных ходов требу- требуется рассмотреть возможные ответные ходы противника. Имейте в виду, что цель противника состоит в поиске хода, который максимально снизит вероятность его выигрыша. Следовательно, оценкой нашего следующего хода является наихудшая оценка всех дочерних узлов. 3. Лучшим узлом (т.е. лучшим следующим ходом) для нас является узел с наибольшей оценкой. Эта процедура называется минимаксной, потому что на чередующихся уровнях дерева цель состоит в попеременной максимизации и минимизации шансов выигрыша. Применение альфа-бета-отсечений Минимаксная процедура является примером поиска в глубину, потому что она сначала как можно глубже исследует один путь и применяет оценочную функцию к последнему узлу в пути. Затем она поднимается на один уровень вверх и т.д. Подобные процедуры поиска в глубину можно улучшить, используя такие мето- методы, как методы ветвей и границ, описанные выше. Эффективность поиска в глубину можно улучшить, отказавшись от просмотра путей, которые заведомо хуже наилучшего найденного до сих пор пути. В то же время очень важно понять, что на общую производительность этого метода очень сильно влияет порядок анализа путей. Это связано с тем, что если начать анализ с худшего пути, то это сведет на нет всю идею этого метода. Эту технику поиска лучше всего описать на примере, в котором используется минимаксная процедура в игре Tic-Tac-Toe. При альфа-бета-отсечении используются два пороговых значения: альфа и бета. Значение альфа — это нижняя граница значения, которое можно присвоить узлу максимизации. Значение бета — это верхняя граница значения, которое можно присвоить узлу минимизации. Рассмотрим на примере (рис. 13.9). Предположим, что мы выполняем поиск в глубину: поиск выполнен во всем поддереве узла В, и результаты применения оценочной функции показали, что узлу D соответ- соответствует оценка 6, а узлу Е — оценка 9. Таким образом, можно гарантировать, что узлу А соответствует оценка
Алгоритмы поиска данных не менее 6. Это значение становится значением альфа для узла А, и его можно использовать для исключения некоторых ветвей все- всего дерева из просмотра. После анализа узла К мы видим, что его оценка равна 0. Таким образом, узел I содержит оценку не более 0. Значит, уже не нужно анализировать остальные поддеревья узла I, потому что путь из узла С в узел I заведомо невыгоден, так как переход в узел В дает оценку 6. Теперь предположим, что для узла J получена оценка 10. Таким образом, оценка узла С будет не более 10. Это бета-значение. Рассмотрим использование бета-значения, которое является /^ верхней границей узла минимизации. Предположим, что оценка в \н\ ^~~^ узле М равняется 15; это больше, чем значение бета для узла С. Проще говоря, если мы выберем из узла С узел G, то получим рисунок 13-9- Пример, иллюстрирующий оценку 15, которая больше 10 (оценка узла F). Однако в узле С альфа-бета-отсечения. выполняется минимизация оценок, поэтому нет смысла выбирать узел G (оценка которого не меньше оценки узла М, поскольку в узле G выполняется максимизация). В результате можно отказаться от анализа всех остальных поддеревьев узла G. В очень больших деревьях использование альфа-бета-отсечений может при- привести к значительному сокращению путей поиска и к повышению общей производительности. При использовании альфа-бета-отсечений на уровне максимизации можно исключить некоторый путь, если становится понятно, что его оценка меньше значения альфа для родительского узла; на уровне мини- минимизации можно исключить путь, если становится понятно, что его оценка больше значения бета для ро- родительского узла. Как уже упоминалось, эффективность альфа-бета-отсечений зависит от порядка обхода путей. Можно доказать, что при существовании идеального порядка обхода количество просмотренных конечных узлов с применением альфа-бета-отсечений и глубине просмотра d примерно вдвое больше количества просмот- просмотренных конечных узлов без использования альфа-бета-отсечений и с глубиной просмотра d/2. Дальнейшая модификация альфа-бета-отсечений, называемая отсечениями пустот, может обеспечить значительное улучшение производительности. Идея этого вида отсечений состоит в том, что можно исклю- исключить дополнительные пути, если ожидаемое улучшение минимально по сравнению с наилучшим найден- найденным в данный момент путем. Смысл в том, что можно потратить больше времени на анализ других путей дерева в надежде найти лучший путь. Задача коммивояжера Изложение методов поиска не было бы полным без упоминания задачи коммивояжера. Эта задача зак- заключается в поиске ответа на вопрос среди очень большого количества возможных решений. Для таких задач поиска неприемлем линейный поиск, используемый в других типах задач, потому что задача коммивояже- коммивояжера потребовала бы огромного количества времени для выполнения линейного поиска. Определение задачи. Дано множество из N городов. Найти кратчайший маршрут, соединяющий все города без посещения одного и того же города более одного раза. Проведено множество исследований задач такого рода; на данный момент не существует эффективного метода их решения с использованием полного перебора, потому что необходимо искать решение среди большого количества маршрутов, каждый из которых может быть очень длинный в зависимости от значе- значения N. Если предположить, что коммивояжер может перемещаться только между определенными парами горо- городов, то эту задачу можно представить с помощью графа. Тогда задача будет состоять в поиске цикла. Суще- Существует несколько способов решения этой задачи, но ни один из них не является достаточно эффективным. Первый метод заключается в применении полного перебора. Полный перебор можно выполнить с помо- помощью разновидности поиска в глубину, если не просто помечать просмотренные узлы, но и снимать помет- пометки с узлов, если путь привел в тупик. Другими словами, выполняется поиск в глубину, и на просмотренных узлах ставятся пометки (чтобы не просматривать их повторно). Если текущий путь приводит в тупик, необ- необходимо выполнить возврат и попробовать другой маршрут. Такой возврат требует снятия пометок с узлов, которые уже были просмотрены на маршруте. После этого можно попробовать другую ветвь. Такой полный перебор может потребовать очень много времени, особенно на полном графе. Для сокращения количества путей используется множество различных методов. Они основаны на при- применении определенных проверок на узлах в целях исключения тех ветвей, которые заведомо не могут при- привести к правильному решению. В таких методах применяются различные формы возврата.
Обработка данных Часть III При попытке найти простой цикл в таких задачах можно исключить некоторые альтернативы, осознав, что циклы являются симметричными, т.е. всегда существует два пути, представляющих один и тот же мар- маршрут. Это можно определить, применив ограничение, согласно которому три узла должны появляться в определенном порядке. Другими словами, из узлов А, В и С узел В должен идти после А, но перед С. При этом узел С будет проверяться только после проверки узла В. Задачи поиска пути с наименьшей стоимостью можно выполнить более эффективно, если не продол- продолжать просматривать путь, когда стоимость его части становится выше стоимости наилучшего найденного на данный момент. Поиск решения будет более эффективным, если проверять дочерние узлы данного узла в порядке возрастания стоимостей, чтобы найти оптимальное решение первым. Применяя возвраты и про- продуманные методы отсечения, можно получить эффективные результаты поиска. Внешний поиск Поиск — это очень важная и часто используемая операция, применяемая к дисковым накопителям. На дисках хранятся файлы, и для доступа к ним требуется эффективный способ. В большинстве приложений, использующих дисковый ввод/вывод, скорость доступа к диску является самым критичным элементом, поэтому это время необходимо сократить до минимума. Индексированный последовательный доступ Основные методы поиска можно применить и к дискам. Однако методы последовательного доступа, рассмотренные в этой главе, недостаточно эффективны. Методы линейного поиска позволяют просто про- просматривать ключи, пока не будет найдено совпадение или достигнут конец списка. Можно улучшить по- поиск, используя индекс, чтобы отследить, какие ключи принадлежат каким страницам на диске. Первая страница каждого диска может быть его индексной страницей. Этот подход можно улучшить, используя главный индекс, чтобы страница главного индекса содержала информацию о том, на каких дисках находятся раз- различные ключи. Например, главный ключ может указывать, что ключи на первом диске меньше, чем D, ключи на втором диске находятся в интервале от Е до К и т.д. Главный индекс может быть достаточно маленьким и размещаться в памяти, позволяя очень быстро обратиться к записи, выполняя для этого только два вида поиска — по главному индексу, чтобы найти диск, содержащий ключ, и по индексной странице на диске, чтобы найти, где на диске хранится запись. Этот метод объединяет методы индексирования с последовательным доступом и поэтому называется индексированным последовательным доступом. Недостаток этого метода заключается в том, что при большом количестве обновлений индекса (при добавлении и удалении записей) для обслуживания индексов может потребоваться много времени. Двоичные деревья Для улучшения индексного просмотра можно использовать двоичные деревья, помещая ключи в струк- структуру дерева, чтобы ключи в левом поддереве любого узла следовали перед этим узлом, а ключи в правом поддереве — после этого узла. Существует три метода обхода, которые можно использовать с объектом binary_tree: до упорядочивания, при упорядочивании и после упорядочивания. Хотя просмотр можно улуч- улучшить, используя любой метод обхода, особенно эффективным является обход при упорядочивании. Ниже показан псевдокод для каждого их этих трех методов. Основное отличие между методами заключается в порядке сравнения узлов с ключом поиска. Следующие фрагменты программного кода иллюстрируют пред- представленные методы обхода. Метод обхода до упорядочивания: preorder_traversal (,iode 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 Дерево 2-3-4 РИСУНОК 13.10. Преобразование дерева 2-3-4 в горизонтальное/вертикальное дерево и в красно-черное дерево. Гор» e/taepi Красно-черное дерево Резюме Поиск — это важная задача для многих приложений. Обычно информация разделяется на записи, иден- идентифицируемые с помощью ключей. Процесс поиска, как правило, включает в себя не только поиск клю- ключа, но и доступ к информации в соответствующих записях. Для решения задач поиска может использоваться несколько алгоритмов; выбор эффективного алгоритма зависит от представления данных. Для определения объема работы алгоритма поиска может использоваться О-запись. Вообще, алгоритмы поиска подразумевают просмотр набора данных. Этот процесс можно улучшить, используя информацию о представлении данных. При методе бинарного поиска используется информация о том, что данные предварительно отсортированы. Задачу сопоставления с образцом можно представить как конечный автомат. В этой главе также рас- рассмотрено построение конечных автоматов на основе образца для поиска. Множество практических задач можно представить с помощью графа; для решения задач, которые могут быть представлены на графе, разработано большое количество алгоритмов, таких как алгоритм Деикстры (Dijkstra). Идеального метода поиска не существует, поэтому всегда следует творчески подходить к реали- реализации метода поиска.
Хеширование и синтаксический анализ В ЭТОЙ ГЛАВЕ Сравнение поиска и хеширования Функции хеширования Разрешение конфликтов Синтаксический разбор
Обработка данных Часть III Сравнение поиска и хеширования При использовании различных методов поиска, рассмотренных в главе 13, для нахождения нужных данных выполнялось сравнение с ключом. При последовательном поиске данные искались в таблице, сравниваясь с ключом, чтобы определить имеется ли ключ поиска в этой таблице. При двоичном поиске сравнение с ключом необходимо для разбиения таблицы на две части и определения, в какой из половин может нахо- находиться ключ поиска. Что же касается хеширования, — это способ, который подразумевает использование значения ключа поиска для определения его позиции в таблице без сравнения с ключом. При хешировании используется произвольная функция отображения для определения местоположения любого элемента данных и обеспе- обеспечения приблизительно равномерного распределения элементов данных в памяти, выделенной для выпол- выполнения хеширования. В идеальном случае расположение элементов определяется с помощью функции хеширования, приме- примененной для ключа поиска, при этом выполняется работа независимо от фактического количества элемен- элементов данных. Однако в действительности метод хеширования только приблизительно соответствуют этому расчету и приводит к конфликтам, когда одна и та же функция хеширования, примененная к двум или более различным клкйам поиска, может указать на одну и ту же позицию. Поэтому очень важно уметь разрешать конфликты. Методы хеширования сильно зависят от выбора функции хеширования. Цель хеширования состоит в нахождении функции хеширования h, которая при применении к ключу поиска К преобразует этот ключ в индекс таблицы, содержащей набор данных. Требуется идеальная функция хеширования, которая не приводит к конфликтам. Можно создать идеальную функцию хеширования, если таблица содержит как минимум столько же позиций, сколько имеется элементов. Однако мы не всегда знаем количество элемен- элементов заранее, кроме того, может не оказаться достаточно пространства для хранения всех элементов. Слож- Сложные функции хеширования могут избежать конфликтов, но могут также потребовать много времени для выполнения. Поэтому при использовании метода хеширования нужно найти компромисс между размером и временем. Во многих приложениях предпочтительнее использовать хеширование, а не методы двоичного поиска, описанные в главе 13. Хеширование — относительно дешевый способ реализации (в отношении скорости выполнения и используемого пространства), который к тому же обеспечивает очень быстрый поиск, осо- особенно если можно выделить достаточно много пространства для хранения большой таблицы хеширования. С другой стороны, двоичный поиск обладает несколькими преимуществами: ¦ Двоичный поиск является динамическим и не зависит от предварительной информации о количе- количестве элементов данных. ¦ Производительность худшего случая для двоичного поиска вполне приемлема. Худший случай при хешировании — это когда все ключи указывают на одну и ту же область; разрешение конфликтов при этом может превратиться в настоящее бедствие. ¦ При двоичном поиске могут выполняться дополнительные операции, такие как сортировка. Функции хеширования Функция хеширования является важной частью процесса хеширования. Эта функция используется для преобразования ключей в адреса таблицы. Она должна легко вычисляться и преобразовывать ключи (обыч- (обычно целочисленные или строковые значения) в целые числа в интервале от 0 до TR-1 (где TR — это коли- количество записей, которое может вместиться в таблице). Поскольку большинство распространенных функций основано на арифметических операциях, то нужно уметь преобразовывать ключи в числа, над которыми можно выполнять арифметические операции. Рас- Рассмотрим несколько функций хеширования, которые можно легко вычислить: ¦ Деление. Если нет информации о характеристиках ключей, то деление является наилучшей функци- функцией хеширования. Предположим, что TR — это количество записей в таблице, а К — это ключ поис- поиска; тогда h(K) = К mod TR Эта функция лучше всего работает, когда TR — простое число. Если TR — не простое число, то можно использовать функцию h(K) = (К mod p) mod TR, где р — простое число, большее, чем TR. Важно, чтобы функция хеширования возвращала значение, меньшее, чем TR; для этого выполняется деление по модулю. Для ключей, представленных строками, число К можно получить, проанализировав двоичное представление символов строки и преобразовав его в десятич- десятичное значение. Имейте в виду, что этот метод может привести к конфликтам.
Хеширование и синтаксический анализ Глава 14 ¦ Свертывание. В основе методов свертывания лежит разбиение ключей на подключи с последующим выполнением некоторых арифметических операций над ними. Предположим, что ключом для хеши- хеширования является телефонный номер. Каждый номер можно разбить на пять частей, которые можно сложить, а результат разделить по модулю. Например, телефонный номер D07) 555-1212 можно разбить на пять частей: 40, 75, 55, 12, 12. В результате сложения этих чисел получаем значение 194. Результат хеширования: 194 mod TR. Различные вариации только что описанного метода свертывания отлича- отличаются по способу разбиения ключей и по операциям, выполняемым над подключами. Строки можно разбить на подстроки, а затем применить к подстрокам операцию XOR, как показано в следующем примере: h("xyzw")="xy" XOR "zw". и Средняя площадь. В некоторых случаях очень эффективным оказывается метод средней площади. Сна- Сначала используется один из методов свертывания, а затем результат возводится в квадрат. Адрес в таблице извлекается как средняя часть полученного значения. В предыдущем примере с телефонным номером в результате свертывания получено значение 194. Теперь возведем его в квадрат: A94J = 37636. Для таблицы из 1000 записей можно использовать адрес 763. Таким образом, hD07-555-1212) = 763. Теперь вы знакомы с основными методами создания функций хеширования. Как можно догадаться, при реализации этих методов потребуется приложить немалые усилия для разрешения конфликтов. Разрешение конфликтов В практических ситуациях сложно реализовать идеальные функции хеширования. Таблицы хеширования должны обладать возможностью разрешения конфликтов, возникающих в тех случаях, когда функция хе- хеширования возвращает один и гот же результат при применении к двум различным элементам данных. В последующих разделах рассматриваются несколько вариантов решения этой задачи. Линейное повторное хеширование Линейное повторное хеширование — это простейший метод разрешения конфликтов. При обнаружении конфликта алгоритм проверяет таблицу в поисках пустой ячейки, а затем помещает элемент в эту ячейку. Когда нужно найти элемент, функция хеширования указывает на положение в таблице и обнаруживается конфликт (на него указывает no_match). Затем выполняется просмотр таблицы до тех пор, пока не возник- возникнет одна из следующих ситуаций: Н Будет найден элемент. ш Будет просмотрена вся таблица. и Будет найдена пустая ячейка в таблице, означающая, что элемент не найден. При пошаговом просмотре таблицы вовсе необязательно выполнять единичные шаги — лучше переша- перешагивать через несколько записей. Величина шага должна быть простым числом и не являться делителем размера таблицы, чтобы обеспечить обход всех записей. Единичный шаг не следует выполнять, чтобы избежать перегруженности, вызванной конфликтами в какой-либо области таблицы. Линейному повторному хешированию свойственны следующие недостатки: ¦ Элементы нельзя удалять из таблицы. При удалении элементов следует помечать ячейки как недей- недействительные, в противном случае можно достичь пустой ячейки и сделать ошибочный вывод о том, что искомый элемент отсутствует в таблице. Предположим, что элементы Mike, Michael и Mikhael при хешировании указывают на одну и ту же позицию в таблице. Если удалить элемент Michael и не пометить ячейку как недействительную, то образуется пустая ячейка. Если затем попытаться найти элемент Mikhael, то поиск может завершиться в пустой ячейке в предположении, что элемент Mickhael не существует. При нахождении ячейки, помеченной как недействительная, поиск продолжится. ¦ Производительность иногда может оказаться низкой, поскольку этот метод позволяет просматривать оставшуюся часть таблицы от точки возникновения конфликта и, таким образом, перебирать мно- множество несовпадающих записей. Нелинейное повторное хеширование При линейном повторном хешировании выполняется последовательный просмотр от точки конфликта к пустой ячейке. Нелинейное повторное хеширование подразумевает повторное хеширование ключа поиска для определения нового значения. Если обнаружится еще один конфликт, то повторное хеширование будет выполняться до тех пор, пока не будет найдена пустая ячейка. Если таблица почти полная, то, возможно,
Обработка данных Часть III потребуется выполнить хеширование несколько раз. Одна из разновидностей такого подхода называется двойным хешированием. Этот метод, в частности, решает задачу кластеризации. Используя этот метод, мы применяем фиксированный "шаг" для последовательного просмотра вместо просмотра каждой записи. Для определения "шага" применяется вторая хеш-функция. Выбор хеш-функции важен, потому что неподходя- неподходящее значение может привести к неправильной работе программы. В эффективной реализации используются следующие ограничения для второй хеш-функции. Предположим, что h2 = hash2(K) и таблица содержит TR ячеек. ¦ Значение h2 не должно равняться нулю, в противном случае программа "зациклится". ¦ h2 и 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 97 250 241 500 499 1000 997 1500 1499 2000 1999 5000 4999 8000 7999 10000 9973 Хеширование строк символов Хотя существуют приложения с числовыми ключами, часто применяются ключи, являющиеся строка- строками символов (такими, как имена, адреса и ключевые слова в тексте). Один из подходов к хешированию строк состоит в сложении ASCII-значений символов и делении по модулю на TR, где TR — это количе- количество ячеек в таблице хеширования. В следующем фрагменте профаммного кода показана простая и эффек- эффективная функция хеширования, которая в качестве исходных данных принимает ключ и информацию о количестве ячеек TR и возвращает адрес области памяти: int smpl_hash ( char * key, int TR) { int bucket = 0; while (*key) bucket += *key++; bucket = bucket % TR; return bucket; } Очень сложно найти функцию хеширования общего назначения, которая хорошо подходит для всех типов строк, но следующая функция Elfflash() очень эффективна. Эта функция является частью 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 250 500 750 1000 Линейное хеширование 43 51 842 1983 531 Повторное хеширование 35 44 186 641 962 Простое связывание в цепочку 15 32 134 299 4971 Связывание в группу цепочек 24 36 182 504 64,594 Открытая адресация Метод открытой адресации работает лучше, если есть достаточно четкое представление о количестве элементов. В каждой области памяти хранится только один элемент, и функция хеширования применяется к ключу для определения его позиции в таблице. Если область памяти пустая, то ключ копируется в эту область; в противном случае возникает конфликт и для определения положения используется одна из не- нескольких стратегий, рассмотренных ранее в этой главе. Каждая из стратегий, по существу, предполагает использование предопределенной последовательности, с помощью которой выполняется поиск пустой области; затем элемент вставляется в эту область. Если пустая область не найдена, значит, таблица полно- полностью заполнена. Из этого описания следует, что последовательность проверок должна, в конце концов, охватить всю таблицу. Имейте в виду, что та же последовательность проверок выполняется при поиске ключа (вместо вставки). Поиск продолжается до тех лор, пока не будет найдено совпадение или пустая область. Если найдена пустая область, значит ключ отсутствует в таблице. Метод открытой адресации содержит две трудности: ¦ Сложно определить, когда встречается пустая область. Вообще, каждая область использует некото- некоторый флаг, который указывает, пустая ли эта область. ¦ Сложно выбрать лучшую тестовую последовательность. Это, вероятно, самое важное, решение, пото- потому что необходимо убедиться в том, что с помощью этой последовательности при необходимости проверяется вся таблица, а также можно минимизировать группирование и исключить конфликты. Предположим, что тестовой последовательностью является р0, р,, р2, ... ps,, где р0 — это результат фун- функции хеширования, а оставшаяся часть последовательности образуется перестановкой чисел от 0 до s-1. Линейное тестирование В линейном тестировании используется следующая тестовая последовательность (последовательность проб): р0, ро+1, Р0+2, ... po+TR-l, с применением деления по модулю на TR. Деление по модулю обеспечивает переход к началу таблицы при достижении ее конца. Эта последовательность легко представляется с помо- помощью следующих равенств: р0 = hash (key) PL = (P0+i) mo TR
Обработка данных Часть III В этих равенствах р, — это i-й тестовый элемент. Пример. Используем линейное тестирование для вставки следующих телефонных номеров в таблицу хеширования: 8881234, 8882345, 8883456, 8884321, 8886543. Мы будем использовать функцию хеширования hash(key) = key mod 11 и вставлять ключи в таблицу размером 11. Это упражнение состоит из следующих шагов: 1. hash (8881234) = 8881234 mod 11 = 10 2. hash (8882345) = 8882345 mod 11 = 10 (конфликт) Этот конфликт разрешается с помощью следующей тестовой последовательности: Pi = (Po+1) mod 1:L = 11 mod 11 = 0 3. hash (8883456) = 8883456 mod 11 = 10 (конфликт) Этот конфликт разрешается с помощью следующей тестовой последовательности: р1 = (po+l) mod 11 = 11 mod 11 = 0 Этот конфликт, в свою очередь, разрешается с помощью последовательности: р2 = (ро+2) mod 11 = 12 mod 11 = 1 4. hash(8884321) = 8884321 mod 11 = б 5. hash(8885432) = 8885432 mod 11 = б (конфликт) Этот конфликт разрешается с помощью следующей тестовой последовательности: Pi = (Po+1) mod 11 = 7 niod 11 = 7 6. hash (8886543) = 8886543 mod 11 = б (конфликт) Этот конфликт разрешается с помощью следующей тестовой последовательности: pt = (po+l) mod 11 = 7 mod 11 = 7 Этот конфликт разрешается с помощью последовательности: р2 = (р„+2) mod 11 = 8 mod 11 = 8 В результате телефонные номера будут вставлены в таблицу следующим образом: Индекс Значение 0 8882345 1 8883456 2 3 4 5 6 8884321 7 8885432 8 8886543 9 10 8881234 Любое тестирование может привести к последующим конфликтам, но при линейном тестировании си- ситуация особенно невыгодная. При выполнении линейного тестирования элементы начинают группировать- группироваться, и чем больше группа, тем больше вероятность последующего конфликта. Группирование, вызванное линейным тестированием, называется первичным. Среднее количество проб, необходимое для успешного поиска (нахождения совпадения) и неуспешно- неуспешного поиска (нахождения пустой ячейки при вставке элемента данных в таблицу хеширования), оценивается с помощью следующих равенств: S (alpha) = 1/2 + 1/2 A/A-alpha)) , U (alpha) = 1/2 + 1/2 A/A-alpha) J
Хеширование и синтаксический анализ Глава 14 В этой последовательности alpha — это коэффициент загрузки. Эти формулы оценивают среднее количество попыток для успешного и неуспешного поиска. Эти фор- формулы сформулированы Дональдом Кнутом (Donald Knuth) и подробно описаны в его книге The Art of Computer Programming. По мере заполнения таблицы оба типа поиска — Л(alpha) и U(alpha) — требуют все больше и больше времени. Когда таблица почти заполнена, количество проб достигает O(TR). Поиск, выполняемый при хешировании, зависит от коэффициента загрузки и не обязательно от количества элементов. Квадратичное тестирование Проблема производительности при линейном тестировании вызвана образованием групп в результате использования линейной тестовой последовательности. При квадратичном тестировании используется дру- другая последовательность, позволяющая избежать группировки. Эта последовательность представляется с по- помощью следующих равенств: р0 = hash (key) pt = (po+ia) mod TR В этой последовательности р. — это i-я проверка, a TR — количество элементов в таблице. Квадратичное тестирование переходит через большее количество ячеек по сравнению с линейным тес- тестированием и, следовательно, приводит к меньшему группированию. Однако для всех ключей, которые отображаются в одну и ту же ячейку, используется одна и та же тестовая последовательность. Это приво- приводит к ситуации, которая называется вторичным группированием. Самый большой недостаток квадратичного тестирования состоит в том, что, после того как просмотрена примерно половина ячеек, последователь- последовательность начинает повторяться. В результате пустые ячейки пропускаются. Для улучшения ситуации можно ис- использовать разновидность квадратичного тестирования: р„ = hash (key) pt = (po+i2) mod TR Однородное тестирование — это теоретическая тестовая последовательность, которая, по существу, яв- является случайной и в которой каждая ячейка может быть выбрана с одинаковой вероятностью. При таком тестировании не возникает ни первичного, ни вторичного группирования. Среднее количество проб для такой последовательности можно выразить с помощью следующих равенств: S (alpha) = 1/а1рЬа*1яA/A-а1рЬа)) U(alpha) = l/(l-alpha) Здесь alpha — это коэффициент загрузки. В результате будет получено приблизительное число попыток для успешного и неуспешного поиска. Эти формулы написаны Дональдом Кнутом (Donald Knuth) и подробно рассмотрены в его книге The Art of Computer Programming. На практике не существует методов генерации такой последовательности. Однако очень похожих резуль- результатов можно достичь с помощью метода двойного хеширования. Тестирование двойного хеширования При двойном хешировании используются две функции хеширования. Первая функция применяется к ключу. В случае возникновения конфликта к результату первой функции применяется вторая функция и ее результат используется как смещение для нахождения новой ячейки. При этом используются следующие равенства: р0 = hashl(key) offset = hash2(key) Pi = (Pi.j + с) mod TR Здесь TR — это количество ячеек. Для гарантии проверки всей таблицы числа с и TR должны быть взаимно простыми. Очень важно обеспечить, чтобы функции hashl и hash2 не зависели друг от друга. Если hashl приводит к конфликту, то hash2 не должна приводить к другому конфликту. В свой книге Mathematics for the Analysis of Algorithms Дональд Кнут предлагает функции, помогающие избежать конфликтов: 12 Зак. 53
Обработка данных Часть III hashl (key) = key mod TR 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 3. hashl (8883456) = 8883456 mod 11 = 10 (конфликт) Этот конфликт разрешается с помощью следующей последовательности: hash2(8883456) = 8883456 mod 7=1 4. hashl (8884321) = 8884321 mod 11 = 6 5. hashl (8885432) = 8885432 mod 11 = 6 (конфликт) Этот конфликт разрешается с помощью следующей последовательности: hash2(8885432) = 8885432 mod 7=3 (конфликт) Теперь можно использовать линейное тестирование; следующая пустая ячейка — 4. 6. hashl (8886543) = 8886543 mod 11 = 6 (конфликт) Этот конфликт разрешается с помощью следующей последовательности: hash2(8886543) = 8886543 mod 7=1 (конфликт) Используя линейное тестирование, получим следующую пустую ячейку с индексом 2. В результате телефонные номера будут вставлены в таблицу следующим образом: Индекс Значение 0 1 8883456 2 8886543 3 8882345 4 8885432 5 6 8884321 7 8 9 10 8881234 Как видно, метод двойного хеширования не устраняет конфликтов и группирования, но минимизирует их число. В листингах 14.2-4.6 показана реализация связанного списка и его операций previous_node(), current_node(), insert() и removeQ.
Хеширование и синтаксический анализ Глава 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; }; Листинг 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; Листинги 14.7-14.11 содержат определение таблицы openjiash и реализацию ее функций для вставки, поиска и удаления ключей.
Хеширование и синтаксический анализ Глава 14 Листинг 14.7. Определение класса таблицы openhash //Класс open_hash_table можно определить следующим образом, template <class Element_Type> class open_hash_table { private: unsigned int table_size; linked_list<Element_Type> *lists; open_hash_table( open_hash_table & 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 & key ) ; void insert( const Element_Type & key ); Листинг 14.8. Функция инициализации класса openhash //Эта функция инициализирует 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
Обработка данных it] Часть 111 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. Функция вставки указанного ключа в таблицу opcnhash //Эта функция может использоваться для вставки указанного //ключа в таблицу открытого хеширования. //Она использует класс 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 + 4 54 + 6* G + 4) 674 + * G + 8) * B - 9 ) + 1 7 8 + 2 9-* 1 + 2 * ( C + 1) * 6) 2 3 1+6** Далее показан алгоритм, используемый при работе со стеками и постфиксными выражениями: 1. Просмотреть выражение слева направо. 2. Если встретился операнд, то поместить его на вершину стека. 3. Если встретился оператор, то вытолкнуть из стека два верхних элемента и применить к ним соответ- соответствующую операцию; результат поместить на вершину стека. В листинге 14.13 показана реализация разбора числовых выражений с использованием класса stack. Листинг 14.13. Реализация синтаксического разбора числовых выражений char с ; stack num_stackA00); int sum; int operl, oper2; while ( cin.get(c) )
Обработка данных Часть III sum = 0; while ( с == ' ') cin.get(c); if (c == ¦ + • ) operl = num_stack.pop( ) ; oper2 = num_stack.pop( ) , sum = operl + oper2; if (c == •*¦ ) operl = num_stack.pop( ) ; oper2 = num_stack.pop ( ) ; sum = operl * oper2 ; if (c == '-' ) operl = num_stack.pop( ) ; oper2 - num_stack.pop ( ) ; sum = oper2 - operl ; if (c == •/' ) operl = num_stack.pop( ) ; oper2 - num_stack.pop ( ); sum = oper2 / operl; while (c >='0' SS c<= '9') sum = 10*sum + (c-'O1); cin.get(c); num_stack.push(sum); cout « "The result of the expression is " & sum Синтаксический разбор строковых выражений Синтаксический разбор строковых выражений — более сложный, чем /*^\ разбор числовых выражений, и может быть представлен с помощью дерева /ч~ч разбора. Для выражения дерево разбора можно построить, используя следу- / \ ющее простое рекурсивное правило: сделать оператор корнем; дерево, coot- \+J (A) ветствующее левому операнду (выражению), будет левой ветвью, а дерево, / Ч соответствующее правому операнду (выражению), — правой ветвью. Приме- s~\ fn\ няя это простое правило, можно построить двоичное дерево разбора. На \~s ^-J рис. 14.1 показано дерево разбора для выражения A*((B+C)+D). / >ч Можно также создать дерево разбора для более сложных выражений, та- AГ) (с) ких как выражения английского языка. В таком синтаксическом анализе строк ..„^..и^^ .*. т, * _ РИСУНОК 14.1. Дерево разбора используются более сложные методы, рассмотрение которых выходит за рамки , i*//n_i_«-.\_i_iC\ „__ . , „ _ для выражения A*((B+C)+D). этой книги. Объект stack, который использовался для разбора числовых вы- выражений, можно также использовать для анализа строк; отличие в том, что вместо промежуточных резуль- результатов мы сохраняем в стеке промежуточные деревья выражений. В листинге 14.14 показана реализация для A*((B+C)+D). Сначала мы преобразуем это выражение в его постфиксный эквивалент: А В С + D + *. Здесь используется постфиксный эквивалент выражения, потому что его проще реализовать в объекте stack. Листинг 14.14. Реализация для А«((В+С)+Р) с использованием класса stack struct node { char data;
Хеширование и синтаксический анализ Глава 14 struct node *left, *right; stack string_stack{100) ; char с ; 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== '*¦ ) { x->right = stack.pop( ); x->left = stack.pop( ); stack.push(x); Контекстно-свободная грамматика и синтаксический анализ Контекстно-свободная грамматика часто используется для определения правильных конструкций. Она может использоваться для определения того, является ли данная строка символов правильной в некотором языке. Это может быть язык программирования или естественный язык (такой, как английский). Напри- Например, следующая запись может использоваться как контекстно-свободная грамматика, определяющая мно- множество всех допустимых регулярных выражений: <expression> : := <term> | <term> + <expression> <term> : := <factor> | <factor> <term> <factor> : := (<expression>) | с | (<expression>) * | c* Каждая строка в описании грамматики называется порождающим правилом или правилом замены. Порож- Порождающее правило, или правило замены, состоит из следующих элементов: ¦ Терминальные символы, такие как (, ), + и -, которые используются в языке. ¦ Нетерминальные символы, такие как <expression>, <term> и <factor>, которые используются в грамматике. ¦ Специальный символ "с", который представляет букву или цифру. ¦ Метасимволы, такие как ::= (означает "это есть") и | (означает "или"), которые используются в грам- грамматике. Можно использовать дерево разбора для описания строковых выражений, применяя эти порождающие правила. Можно выполнить синтаксический разбор для определения того, можно ли получить данную строку, применив порождающие правила. Если строку нельзя породить, то она недопустима в языке, представлен- представленном грамматикой. Пример дерева разбора для выражения А*В+АС показан на рис. 14.2. Выполнение нисходящего синтаксического разбора для проверки правильности регулярных выражений В методе нисходящего синтаксического разбора для распознавания строк используется рекурсия. В этом методе используется процедура для представления каждого из порождающих правил, определенных в грам- грамматике. Таким образом, нетерминальные символы в левой части правил становятся именами процедур, а нетерминальные символы в правой части становятся рекурсивными вызовами. Терминальные символы ис- используются для просмотра строки в процессе получения дерева разбора. Чтобы подробнее узнать о генера- генерации компиляторов для таких регулярных выражений, обратитесь к книге Седгевика (Sedgewick) An introduction to the Analysis of Algorithms.
Обработка данных Часть III expression expression terfn РИСУНОК 14.2. I Дерево разбора для выражения ^^ А*В+АС. factor term &&* temi factor л ***» A * I \ I С В Анализируемое регулярное выражение можно представить в массиве р; индекс i будет представлять те- текущий анализируемый символ. Идея состоит в том, чтобы начать с i = 0; если при выполнении програм- программы, реализующей анализатор, мы достигнем i = strlen(p), значит, строка допустима в языке. Программа должна быть способной обработать ошибки, а также предотвратить бесконечные циклы, вызванные непра- неправильной рекурсией. Реализация этой идеи показана в следующем псевдокоде: for(i=0;i<strlen(p);i++){ // Здесь следует программный код, реализующий синтаксический раэбор } Резюме Выбор наилучшего метода хеширования для конкретного приложения может оказаться очень сложным. Однако, когда вопрос объема памяти не стоит остро, вы обнаружите, что все методы хеширования, рас- рассмотренные в этой главе, работают примерно с одинаковой производительностью. Если вы знаете размер множества ключей, то мы предлагаем использовать метод двойного хеширования. В противном случае луч- лучше использовать метод связывания в цепочку. Имейте в виду, что следует также принять во внимание ко- коэффициент загрузки, который является важной частью метода хеширования. Используемый метод проходит настоящую проверку только при больших коэффициентах загрузки; следует также тщательно обдумать раз- размер связей, используемых в методе открытой адресации. Вообще, хеширование лучше двоичного поиска, потому что хеширование проще реализовать и оно может обеспечить минимальное и почти постоянное время поиска (при наличии достаточного объема памяти для хранения большой таблицы хеширования). Метод двоичного поиска лучше использовать, если важен воп- вопрос объема памяти или если размер множества ключей неизвестен заранее, потому что для двоичного поиска можно предсказать производительность худшего случая. Методы синтаксического анализа широко используются компиляторами для интерпретации входных данных и преобразования их в другую форму, которая может интерпретироваться компьютером. При син- синтаксическом разборе используется нисходящий или восходящий подход; выбор подхода обычно зависит от конкретной грамматики. Числовые выражения обычно анализируются с использованием постфиксного представления; при анализе строк обычно используется дерево разбора для представления анализируемого выражения. Метод синтаксического разбора широко используется в искусственном интеллекте для интер- интерпретации языков. Используя порождающие правила и грамматику, для конкретного языка можно создать дерево разбора выражений.
Живучесть объектов и шифрование В ЭТОЙ ЧАСТИ Живучесть объектов Реляционные базы данных и живучесть Реализация живучести объектов с использованием реляционных баз данных Объектно-ориентированные базы данных Защита приложений с помощью шифрования ЧАСТЬ
Живучесть объектов В ЭТОЙ ГЛАВЕ Создание хранимых объектов * Кэширование Подкачка данных на жесткий диск
Живучесть объектов Глава 15 Используя язык C++, можно манипулировать объектами в памяти. Временами, впрочем, будет появ- появляться необходимость в сохранении этих объектов на жестком диске и восстановлении их в оперативной памяти позже. Сохранение объектов на жестком диске позволяет манипулировать большим количеством объектов, которые могут находиться и в оперативной памяти одновременно; также оно позволяет закрыть программу и продолжить ее работу в дальнейшем. Производитель компилятора предоставляет объект ofstream, реализующий основные механизмы мани- манипуляции файлами. В разрабатываемой вами программе может быть создан объект fstream, с помощью кото- которого будут открываться файлы для чтения и записи данных в них. Впрочем, объекты fstream ничего не знают о ваших данных. Они слишком громоздки для чтения потоков символов, но ваши объекты часто бывают более сложными. Вашей задачей является научить классы направлять данные на жесткий диск. Реализации этого механиз- механизма может быть осуществлена несколькими способами. Итак, во-первых, уточните, будете ли вы в своих файлах сохранять объекты многих видов. Если вам придется считывать запись, не зная, что за тип объекта в ней содержится, то в таком случае в файле не- необходимо сохранить информации больше, чем это было бы в том случае, когда все записи в файле были бы одного типа. Во-вторых, решите, собираетесь ли вы сохранять записи фиксированной длины. Если вы знаете, что следующий считываемый объект занимает например, 20 байтов, то у вас задача проще, чем если бы вы не знали точного размера объекта. В последнем случае вместе с объектом должна сохраняться и его длина. В листинге 15.1 показана очень простая программа, которая открывает файл, сохраняет в нем некото- некоторый текст, а затем опять открывает файл и считывает из него этот текст. Листинг 15.1 Простая программа чтения/записи ¦include <fstream.h> void main () 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.ignoreA, '\n') ; // "съесть" строку после имени файла 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(); // всегда закрывать по окончании работы В этой программе ясно показаны основы механизма записи текста в файл. Вы создаете имя файла, от- открываете объект 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; }; class Reader { public: virtual Readers operator»(ints) ; virtual Readers operator»(char*s) ; Reader(char «fileName):fin(fileName,ios:rbinary) (} -Reader()(fin.close();} private: ifstream fin; }; Задача Reader и Writer состоит в инкапсуляции ответственности за считывание и запись элементарных данных (таких, как целые, строки символов и др.) в место их долговременного хранения. Здесь мы реали- реализовали только механизмы хранения int и char*. Возможно добавление double, float и других типов. Чтобы увидеть эти классы в работе, сохраним на диске некоторые объекты, а затем считаем их. Начнем с создания класса Note, порожденного от Storable, как показано в листинге 15.2. Класс Note сохраняет некоторый текст, его длину и некоторую дополнительную информацию. Сейчас в качестве этой дополни- дополнительной информации мы сохраним два целых (reserved 1 и reserved2). В коммерческих приложениях эти пе- переменные могут хранить дату создания и другие приемлемые данные. Листинг 15.2. Note.h class Note : public Storable < public: Note(const Strings text): itsText(text), reservedl@L), reserved2@L) U Note(const char* text): itsText(text) , reservedl@L), reservtd2@L) U Note::Note(Readers rdr) :itsText(rdr)
Живучесть объектов Глава 15 rdr » reservedl; rdr » reserved2 ; } -Note(){} const Strings GetTextOconst { return itsText; } II... void Write(Writers wrtr) { itsText.Write (wrtr) ; wrtr « reservedl; wrtr « reserved2; } private: String itsText; int reservedl; int reserved2; Текст, сохраняемый Note (itsText), имеет тип String. Это пользовательский тип, управляющий строками символов. Класс Note возлагает на Reader ответственность за создание reservedl и reserved2, которые мы передадим прямо в конструктор. Reader отвечает за reservedl и reserved2, поскольку те являются элемен- элементарными типами данных (целыми). Впрочем, класс Note просит объект String сконструироваться самосто- самостоятельно, поскольку тот является пользовательским типом. Для этого при инициализации класс Note передает объект Reader конструктору String: Note::Note(Readers rdr) : itsText(rdr) Поскольку класс Note унаследован от Storable, он имеет конструктор, принимающий объект Reader, a также должен переопределить метод 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 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; bool operator!=(const Strings rhs)const; // дружественные функции 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 <?stream.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.ignoreA,'\n'); // пропускается строка после имени файла Writer * writer = new Writer(fileName); Note* theNote; for (long i = 0; i<howMany; 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 « theNota->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) , reserved!. @L) , reserved2 @L) О Текст инициализируется введенным пользователем словом, а целые переменные reservedl и reserved2 устанавливаются нулевыми значениями. 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 *& data) < int len; fin.read((char*) Slen,szlnt); data = new char [len+1] ; 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 же самое происходит и с reserved2. В конце выводится Note и уничтожается Reader. Когда Reader и Writer будут полностью разработаны для поддержки всех встроенных типов, реализа- реализация сериализации ваших классов будет несложной. Для этого необходимо будет выполнить следующее: 1. Унаследовать от класса Storable. 2. Реализовать метод Write() и конструктор, принимающий Reader. 3. Возложить ответственность за элементарные типы на Reader и Writer. 4. Возложить ответственность за считывание и запись включаемых объектов 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). Первое слово — Four — добавляется в новую страницу, как показано на рис. 15.2. Эта новая страница является листовой, и индекс Four указывает на реальные данные. Корень - Корень—>¦ ¦*• 1 four 1111 страница 1 (Данные) РИСУНОК 15.1. Пустое В-дерево. РИСУНОК 15.2. Листовая страница. Слова добавляются к странице до тех пор, пока их количество не станет равным порядку страницы (в этом случае четырем), к этому моменту страница будет заполнена (рис. 15.3). РИСУНОК 15.3. Заполненная листовая страница. Корень- *¦ | and | four I score | seven [страница 1 Когда придет время добавить слово years, страницу необходимо будет разбить для выделения места. Алгоритм следующий: 1. Разбить страницу пополам. 2. Добавить новое слово в подходящую позицию (в этом случае по алфавиту). 3. Если новое слово меньше первого (и, следовательно, должно быть добавлено в первую позицию), установить соответственно указатель. 4. Вернуть указатель на новую страницу. 5. Если корень определит, что требуется новая верхняя (узловая) страница, создать ее. 6. В новую верхнюю страницу добавить запись, на которую будет указывать корень. 7. В новой верхней странице добавить запись для возвращаемого значения шага 4.
Живучесть объектов Глава 15 8. Указать корню на новую верхнюю страницу. В случае, показанном на рис. 15.4, добавляется слово years. Для этого страница должна быть разбита. Возвращается новая страница, и корневой указатель распознает, что требуется новая верхняя страница. В новую страницу вносится индекс, указывающий на запись (and), которую myRoot будет использовать для обращения к странице, и вторая запись, указывающая на новую страницу. Затем myRoot ссылается на эту новую узловую страницу. Корень - РИСУНОК 15.4. Разбиение страницы. and | four | | | | score | seven | years j (Данные) (Данные) Следующим добавляемым словом является слово ago. Поскольку в алфавитном порядке оно выше слова and, то добавляется перед листовой страницей and, и узловая страница "исправляется", чтобы указывать на этот более ранний индекс, как показано на рис. 15.5. РИСУНОК 15.5. Исправление узла. Корень- ago I and I four I our | | score | seven | years | Когда страница заполнена (рис. 15.6), она разбивается, как показано на рис. 15.7, и к корневому указа- указателю добавляется новый. Это продолжается до тех пор, пока не заполнится узловая страница (рис. 15.8). РИСУНОК 15.6. Готовность к разбиению страницы. Корень ago | score ago | and | four | our | | score | seven | years | | Корень ago | four [ score | | РИСУНОК 15.7. После разбиения страницы. ago I and | fathers | | (Данные) [ four | our (Данные) score I seven | years | (Данные) Корень—-» РИСУНОК 15.8. Дерево с заполненной узловой страницей. | score | seven | this | years | Добавление следующего слова new представляет проблему. Должна быть разбита первая страница, и, когда это происходит, она передает запись своему родительскому узлу. Этот узел, впрочем, заполнен и
Живучесть объектов и шифрование Часть IV также должен быть разбит. В такой ситуации корневой указатель должен распознать, что требуется новый узел (рис. 15.9). РИСУНОК 15.9. Третий ярус | а | ago j and ч\/ (Данные) IrH Ч I10?1! —V J V | а | brought| | •f | |brought|continent| fathers | (Данные) 1 1 | | forth | on / | | forth | four (Данные) с | score | ¦^= new | on our У (Данные) Zl | \ | score | seven | this | years J 4\/X (Данные) 1 1 Запись В-дерева на диск Общее предназначение В-дерева состоит в сохранении данных на диске. Каким-то образом В-дерево должно сохранить на диске страницы, индексы и данные. Одной из задач является кэширование. Немного отклонимся от темы, чтобы поговорить о создании базы данных в реальном мире. По всей видимости, вот как вы будете программировать свою следующую базу данных: возьмете телефон, позвони- позвоните по 800 номерам магазинов "Товары-почтой" с названиями типа Programmer Credit-Card Heaven и спро- спросите, есть ли у них самая быстрая и максимально надежная система баз данных, отвечающая вашим спецификациям. Интерфейс вы напишете в MFC, ASP или какой-либо другой технологии, свяжете ваше приложение с базой данных через СОМ, обернув его в ODBC, DAO или другую технологию доступа к базам данных. И только в самых редких случаях вы будете на самом деле "раскручивать" свою собственную базу дан- данных "с нуля". В наших целях создадим малый PIM (Personal Information Manager — персональный информационный диспетчер), который позволит создавать отметки (строки или символы) и сохранять их в базе данных. Несколько позже вы сможете запросить базу данных на наличие строки и найти каждый объект Note, со- содержащий такую строку. Эти объекты будут различной длины. В каждом из них мы сохраним дату, длину и набор символов, представляющих данные. ПРИМЕЧАНИЕ В этой упрощенной версии объектом Note будет строка символов. В более сложной реализации вы могли бы исполь- использовать класс Note для хранения этих строк и другой дополнительной информации о состоянии. ;' ¦¦ Индексный файл (WORD.IDX) будет состоять из страниц индексов, ведущих в конечном счете к лис- листовой странице с индексом на искомое слово. Эта запись все еще не указывает на данные. Как-никак, одно слово может находиться во многих объектах Note. Мы создадим индексный файл, который будет содержать список указателей на записи в самой базе данных. Все это основано на приложении PIM, которое автор создал для редкой сейчас книги 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 индексов на страницу. 10 уровней страниц с 16 индексами на страницу предоставляют триллион ключей. 10 страниц, впрочем, занимают всего лишь 2 Кб памяти. A6 байтов на индекс умножить на 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 pIndex, bool XsLeaf) { 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 LeaflndexCtr; static int NodePageCtr; static int LeafPageCtr; static int NodeIndexPerPage[Order+l]; static int LeafIndexPerPage[Order+l] ; private: int myRoot; >; Задача ВТгее — выступать в качестве точки входа в дерево и хранить некоторую статистическую инфор- информацию о дереве. Работа выполняется с помощью страницы и индекса, как показано в листинге 15.6. Листинг 15.6. Объявление Page class Page { public:
Живучесть объектов Глава 15 // конструктор и деструктор Page <); Page (char*) ; Page(Indexs,bool); Page(Index*, int, bool, int) ; ~Page(){) // операции вставки и поиска 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(IndexS,int offset=O, bool=true); // функции доступа Index GetFirstIndex() { return myKeys[0]; } bool GetIsLeaf() const { return myVars.IsLeaf; } int GetCount() const { return myVars.myCount; } void SetCount(int cnt) { myVars.myCount=cnt; } time_t GetTimeO { return myTime; } bool GetLockedt) { return myVars.IsLocked; } void SetLocked (bool state) { myVars.IsLocked = state; ) // манипуляция страницей int GetPageNumber() const { return myVars.myPageNumber; } Page* GetPagejint 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 cnt) { return BTree::theDiskManager.NewPage(arr,off,isL, cnt); } // служебные функции void Nullify(int offset); void Print(); fstreams Write (f streams) ; 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 существует несколько конструкторов; мы их увидим в работе при подробном рассмот- рассмотрении примера. Методы Jnsert(), Find() и другие предназначены для помещения индексов на страницы и для нахождения их позже. Отметим, что GetPage() является inline-функцией; она просто передает полно- полномочия классу DiskManager, который обеспечивает быстрое считывание и чистую инкапсуляцию работы по кэшированию страниц в оперативной памяти, а также получения их с жесткого диска.
Живучесть объектов и шифрование Часть IV Внимательно рассмотрите раздел private. Мы имеем объединение массива символов и структуры myVars, которая включает в себя переменную-счетчик, флаг (указывающий, листовая ли это страница), номер страницы, флаг блокировки и массив индексов. Индексы являются пользовательскими объектами, конст- конструкторы которых принимают параметр. Впрочем, при создании массива пользовательских объектов каждый объект должен создаваться с помощью стандартного конструктора. Что же мы делаем? Во-первых, хотим прочитать все эти индексы страницы за один раз и не можем использовать стандартный конструктор. Во-вторых, мы должны обмануть компилятор, сделав вид, будто вовсе не загружаем массив индексов, а просто загружаем массив символов. Вот почему шк объявляется как массив символов. Размер массива равен порядку Order C2), умноженному на размер одного индекса Index A6). Мы также объявляем указатель на индекс: myKeys. Для указателя не нужен конструктор. Нехороший трюк, о котором упоминалось несколько ранее, появляется в конструкторе Page. Мы уста- устанавливаем указатель myKeys на первый байт mk. Для этого необходимо привести myVars.mk для указания на Index: Раде::Page(IndexS index, bool bLeaf): myKeys((Index*)myVars.mk) У нас есть указатель на массив индексов, и, как вы знаете, указатель может использоваться в точности как массив, т.е. позже можно написать следующее: myKeys[i].PrintKey(); Это классический пример обмена безопасности типов на производительность. Вы не будете делать это так часто. Попробуйте-ка сделать так в Java! В листинге 15.7 приведено объявление класса Index. Листинг 15.7. Объявление класса Index class Index { public: // конструктор и деструктор Index () ; Index(char *) ; Index (char*, int) ; Index(IndexS); -Index (){> / / функции доступа 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 Index& rhs) (return strcmp(myData,rhs.GetData())>=0;) public:
Живучесть объектов Глава 15 int myPointer ; char myData[SizeItem - SizePointer]; Заметьте, вы можете сравнивать два индекса, в результате чего получаете сравнение данных, которым соответствуют эти индексы. Эта возможность позволяет поддерживать упорядоченность объектов. Более обоб- обобщенный подход состоит в хранении данных не как массива символов, а скорее, как параметризированного типа. В дальнейшем лучше было бы возложить ответственность за сравнение на сами объекты данных. Автор не сделал этого здесь, чтобы сохранить пример как можно более простым. Обратите внимание также на то, что Sizeltem является константой, равной 16. (Прочитайте раздел "Оп- "Определение размера страницы", чтобы увидеть, почему размер каждого элемента равен 16 байтам.) Мы вычитаем из него размер указателя (учитывая размер переменной-члена myPointer) для того, чтобы размер Index остался равным значению Sizeltem — 16. В этой программе имеется три дополнительных класса: Data File, DiskManager и WNJFile. Первый класс представляет собой реальную базу данных объектов Note. Каждый Note последовательно добавляется в дис- дисковый файл, и при этом сохраняется его смещение: class DataFile ( public: // конструктор и деструктор DataFile(); -DataFile () {} // управление файлами void Closed ; void Create () ; void GetRecord(int, char*, ints, time_t?); 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(IndexS, bool); int NewPage(Index *array, int offset, bool leaf, int count); private: Page* myPages[MaxPages]; fstream myFile; int myCount; }; Класс 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 I int myints[5 ] ; char myArray[5*szlnt]; }; }; Здесь вы видите массив пяти целых, о котором говорилось ранее. Первые четыре представляют смеще- смещение в файле данных, а пятый (если используется) является смещением следующего массива в WNJFile. В последующих листингах приведен полный программный код этого примера, который позже будет рассмотрен более подробно. В листинге 15.8 приведен BTree.hpp, единственный header-файл для всей про- программы. Он содержит объявления всех классов, а также значения констант, используемых в программе. Листинг 15.8. Из header-файла BTree.hpp #i?nde? BTREE_HPP // защита от повторного включения #define BTREE_HPP #include <time.h> #include <string.h> ¦include <fstream.h> const int SIZE_TIME = 4; const int SIZE_INT = 4; const int Order = 31; // 31 индекс и 1 заголовок const int dataLen = 11; // длина ключа const int MaxPages = 20; // больше, чем нам нужно const int Sizeltem = 16; //Datalen + NULL const int SizePointer = SIZE_INT; // размер смещения const int PageSize = (Order+1) * Sizeltem; 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: // конструктор и деструктор DiskManagerO ; -DiskManagerO {} // управление файлами void Close(int); int Create () ; // манипуляция страницей Page* GetPage(int); void SetPage(Page*); void Insert(Page * newPage); void Save(Page* pg); int NewPage(IndexS, 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*, intfi); static void GetStats () ; 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 (){} // функции доступа const char * GetDataO 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 PrintKeyf) ; void PrintPageO; int Insert(IndexS ref,bool findOnly = false); // перегруженные операторы int operator==(const IndexS rhs); int operator < (const IndexS rhs) {return strcmp(myData,rhs.GetDataO)<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[Sizeltem - SizePointer]; }; // страницы состоят из заголовка и массива индексов class Page { public: // конструктор и деструктор
Живучесть объектов Глава 15 Page () ; 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(IndexS,bool findOnly); int InsertNode(IndexS,bool findOnly = false); void Push(IndexS,int offset=0, bool=true); // функции доступа Index GetFirstIndex() { return myKeys[0]; } bool GetIsLeaf() const { return myVars.IsLeaf; ) int GetCount() const ( return myVars.myCount; ) void SetCountfint cnt) { 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: : theDislcManager .GetPage (page) ; ) int NewPage(IndexS rlndex, bool IsLeaf) { return BTree::theDiskManager.NewPage(rlndex,false) ; } int NewPage (Index* arr, int off,bool isL, int cnt) ( return BTree::theDiskManager.NewPage(arr,off,isL, cnt); } // служебные функции 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*sizeo?(Index)]; // массив индексов }myVars; ); // только в памяти static int gPage; time_t myTime; // для очереди LIFO (последним вошел — первым вышел) }; «endif Программа драйвера и вспомогательные функции находятся в файле Persist.срр, приведенном в лис- листинге 15.9.
Живучесть объектов и шифрование Часть IV Листинг 15.9. Persist.cpp ((include "btree.hpp" #include <Windows.h> #include <iostream.h> #include <stdlib.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: : NodelndexPerPage [Order-t-1] ; int BTree: :Leaf IndexPerPage [Order-t-1] ; / / прототипы void par seCorratiandLines (char *buffer,int argc,char **argv) void ShowMenu(long*); void DoFind(char*, BTreeS); void ParseFile(BTreeS); // программа драйвера int main() { BTree myTree; for (int i = 0; i < Order +1; i++) { BTree::NodelndexPerPage[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 'F1 : case ' f' : ParseFile(myTree); break; case '0 ' : fQuit = true; break; else { if ( myTree.Insert(buffer) )
Живучесть объектов Глава 15 cout « "Inserted.\n" ; buffer[0] = '\0'; return 0; } // Ищем совпадения, показываем меню найденных вариантов, // где каждая запись пронумерована и датирована void ShowMenu(int *list) { int j=0; char buffer[PageSize+1] ; time_t theTime; int len; char listBuff[256]; struct tm * ts; int dispSize; while (list[j] SS 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_niday « "/"; cout « ts->tm_year « " " ; cout « listBuff « endl; // обрабатываем команду -? // ищем совпадения, показываем меню, запрашиваем выбор, // выводим запись и опять выводим меню void DoFind(char * searchString, BTreeS myTree) // создаем массив всего набора смещений WNJ. // Это будет использоваться для вывода выборов и // для нахождения самого текста int list[PageSize]; // заполняем массив нулями for (int i = 0; i<PageSize; list[i] = 0; int k = 0; char * pi = searchString; while (pi [01 == ' ') int offset = myTree.Find (pi) ; if (offset) { // получаем массив смещений из WNJFile int *found = BTree::theWNJFile.Find(offset) int j = 0; 13 Зак. 53
Живучесть объектов и шифрование Часть IV // добавляем те, которых у яас пока нет for (;k < PageSize && 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 @ 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 (!len) break;
Живучесть объектов Глава 15 theString[len]='\O'; char *pl, *р2, *рО; рО = pi = р2 = theString; while (pl[0] 4S (pl[O] = Чп" || pl[0] =• '\г")) P2 = pi; while (p2[0] 44 p2[0] != "Xn1 44 p2[0] != '\r') p2++; int bufferLen = p2 - pi; int totalLen = p2 - pO; if ('bufferLen) continue; // lstrcpyn(buffer,pi,bufferLen); strncpy(buffer,pi,bufferLen); buffer[bufferLen]='\0'; // 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 tinclude "btree.hpp" #include <ctype.h> «include <stdlib.h> // создаем дерево // устанавливаем указатель myRoot в NULL // создаем либо считываем индексный файл BTree: :BTree() :myRoot @) { myRoot = theDiskManager.Create 0: theDataFile.Create(); theWNJFile.Create(); } // записываем индексный файл BTree: : -BTree () { theDiskManager.Close(myRoot); theDataFile.Close() ; theWNJFile.Close() : } II находим суцествупцую запись int BTree::Find(char * str) { Index index(str); if (! myRoot) return OL; else return GetPage(myRoot)->Find(ind*x); ) 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; // создаем Index, где str — в верхнем регистре, // а смещение записано в Index.myPointer Index index(str,offset); if ('myRoot) myRoot = theDiskManager.NewPage (index,true); else Page * pPage = GetPage(myRoot); retVal = pPage->Insert(index); 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 на новую вершину myRoot = PageNumber; } > } void BTree::PrintTree() NodePageCtr = 0; LeafPageCtr = 0;
Живучесть объектов Глава 15 NodelndexCtr = 0; LeaflndexCtr = 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: " « LeaflndexCtr « endl; for (i = 0; i < Order + 1; i++) { if (NodelndexPerPage[i]) cout « "Pages with " « i « " nodes: "; cout « NodelndexPerPage[i] « endl; ) i f (LeafIndexPerPage[i]) { cout « "Pages with " « i « " leaves: "; cout « Leaf IndexPerPage [i] « endl; } } } bool BTree::GetWord(char* string, char* word, ints wordOffset) int i; if (!string[wordOffset]) return false; char *pl, *p2; pi = p2 = string+wordOffset; // "съедаем" ведущие пробелы for ( i = 0; i<(int)strlen(pl) && !BTree::myAlNum(pi[0]); // смотрим, есть ли у нас слово if (!BTree::myAlNum(pl[0])) return false; p2 = pi; // указатель на начало слова // р2 направляем в конец слова while (BTree::myAlNum(p2[0])) р2++ ; int len = int (p2 - pi) ; int pgSize = PageSize; #if defined(_MSVC_16BIT) || defined(_MSVC_32BIT) len = min(len,(int)PageSize); } «else len = min(len,(int)PageSize); «endif strncpy (word,pi,len); word[len]='\0'; for (i = int(p2-string) ;
Живучесть объектов и шифрование Часть IV i< (int) strlen (string) 44 ! BTree: :myJUNum <p2 [0]) р2++ ; wordOffset = int(p2-string) ; return true; } int BTree::myAlNum(char ch) { return isalnum(ch) || ch == • - • || ch = ' \' ' II ch == • (' II ch == •) • ; Листинг 15.11. Index.cpp #include "btree.hpp" #include <ctype.h> Index::Index(char* str):myPointerA) { strncpy(myData,str,dataLen); myData[dataLen]=¦\0•; for (size_t i = 0; i < strlen(myData) 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); myData[i] = toupper(myData[i]); Index::Index(IndexS rhs): myPointer(rhs.GetPointer()) { strcpy(myData, rhs.GetData()); for (size_t i = 0; i< strlen(myData) myData[i] - toupper(myData[i ]) ; Index::Index():myPointer@) void Index::PrintKey() { cout « " " « myData ; } void Index::PrintPage() { cout « myData « ": " ; BTree::theDiskManager.GetPage(myPointer)->Print(); } int Index::Insert(IndexS ref, bool findOnly) { return BTree::theDiskManager.GetPage(myPointer)->Insert(ref,findOnly) , } int Index::operator==(const IndexS rhs)
Живучесть объектов Глава 15 { return (strcmp(myData,rhs.GetData()) == 0); // не учитывая регистр } Листинг 15.12. DataFile.cpp #include "btree.hpp" #include <assert.h> // при конструировании пытаемся открыть файл, если он существует DataFile::DataFile(): myFile("RACHEL.DAT", ios: .-binary | ios::in | ios::out | ios: : nocreate | iosr.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@); myFile.write(Header,SIZE_INT); return; } // мы открыли файл, он уже существует // необходимо получить числа int MagicNumber; myFile.seekg(O); myFile.read((char *) SMagicNumber,SIZE_INT); // проверяем магическое число. Если оно неправильно, // то файл поврежден или это не индексный файл if (MagicNumber != 1234) { // заменить на исключение!! cout « "DataFile Magic number failed!"; } return; } // записать числа, которые понадобятся в следующий раз 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); myFile.clear(); myFile.flushf) ; myFile.seekp@,ios::end); int offset = (int) myFile.tellp(); myFile.write(buffer,fullLen); myFile . f lush () ; 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(StheTime,tmpBuff+SIZE_INT,SIZE_TIME); strncpy(buffer,tmpBuff+SIZE_INT+SIZE_TIME,len); buffer[len] = '\0'; > Листинг 15.13. Page.cpp tinclude "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); // Если это лист, то это первый индекс первой страницы. // Установите его указатель, основанный на // создании нового wnj. В ином случае, // если вы создаете здесь новый узел, не устанавливайте // указатель, он уже установлен. if (bLeaf) { int indexPointer = index.GetPointer(); int appendResult = BTree::theWNJFile.Append(indexPointer) index.SetPointer(appendResult); } myKeys[0]=index; KiyVars.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<Order SS i < count; i++, j++) { myKeys[i]= array[j]; myVars.myCount++; } myVars.myPageNumber = gPage++; SetLocked(false); myTime = time(NOLL); ) void Page::Nullify(int offset) { for (int i = offset; i<Order; { myKeys[i].SetPointer@); myVars.myCount—; // решить какой лист или узел, а // передать этот индекс правой функции. // Если findOnly истинно, то правильный возврат // номера страницы не вставляется int Page::Insert(IndexS rindex, bool findOnly) { int result,- if (myVars.IsLeaf) { SetLocked(true); result = FindLeaf(rindex,findOnly) ,• SetLocked(false); return result; else SetLocked(true); result = InsertNode(rindex,findOnly); SetLocked(false); return result; // найти правую страницу для этого индекса int Page::InsertNode(IndexS rindex, bool findOnly) { int retVal =0; bool inserted = false; int i,j; assert(myVars.myCount>0); // узлы имеют как минимум 1 assert(myKeys[0].GetPointer()); // должна быть корректной // Выше ли она моей первой записи? if (rindex < myKeys[0]) { if (findOnly) return 0L; // не найдено myKeys[0].SetData(rindex); retVal=myKeys[0].Insert(rindex);
Живучесть объектов и шифрование Часть 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; // определить, где она должна находиться if (!inserted) for (j = 0; j<i && j+1 < myVars.myCount; { assert (myKeys [j+1] .GetPointer ()) ; if (rlndex < myKeys[j+1]) { retVal=myKeys[j].Insert(rlndex,findOnly); inserted = true; break; assert(inserted); // заменить на исключение в случае неудачи! // если вы имеете разделение if (retVal && !findOnly) // получить обратно указатель на новую страницу { Index * plndex = new Index(GetPage(retVal)->GetFirstIndex()); pIndex->SetPointer(retVal); retVal = InsertLeaf(*plndex); } return retVal; } // вызывается, если текущая страница листовая int Page::FindLeaf(Indexs rlndex, bool findOnly) { int result = 0; // без дубликатов! 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; // Этот элемент индекса пока не существует, // прежде чем поместить его в индекс, // поместите запись в wnj.idx // и установите индекс на эту запись. rlndex.SetPointer(BTree::theWNJFile.Append(rlndex.GetPointer())); return InsertLeaf(rlndex);
Живучесть объектов Глава 15 int Page::InsertLeaf(IndexS 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 4S rlndex <= myKeys[Order/2-1]) Push(rlndex); else // поместить в нового соседа Sibling->Push(rlndex); result = NewPg; // разбиваем страницу return result; // помещаем новый индекс на эту страницу (в порядке) void Page::Push(IndexS rlndex,int offset,bool first) bool inserted = false; assert(myVars.myCount < Order); for (int i-offset; i<Order SS 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::LeaflndexPerPage[myVars.myCount]++; BTree::LeafIndexCtr+=myVars.myCount; for (int i = 0; i<Order 44 i < myVars.myCount; i++) assert(myKeys[i].GetPointer()); if (myVars.IsLeaf)
Живучесть объектов и шифрование Часть IV cout « "\nPage " « myVars.myPageNumber myKeys[i] . PrintKeyO ; else cout « "\nPage " « myVars.myPageNumber « ": "; myKeysfi] .PrintPageO ; // записываем всю страницу в виде блока fstreams 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" tinclude <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 (!myFile) // сбой nocreate, первое создание файла { // открываем файл и создаем его myFile.clear(); myFile.open("RACHELWNJ.IDX", ios::binary I ios::in I ios::out); MagicNumber - 1234; memcpy(Header,SMagicNumber,SIZE_INT); memcpy(Header+SIZE_INT,Szero,SIZE_INT); myFile.seekp@); myFile.write(Header,2*SIZE_INT); myFile.flush(); return; } // мы отхрыли файл, он уже существует // получить необходимые нам числа myFile.seekg(O); myFile.read(Header,2*SIZE_INT); memcpy(SMagicNumber,Header,SIZE_INT); memcpy(SmyCount,Header+SIZE_INT,SIZE_INT); // проверяем магическое число. Если оно неправильно, // то файл поврежден или это не индексный файл
Живучесть объектов Глава 15 if (MagicNumber != 1234) // заменить на исключение! 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) // начальный заголовок + 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_SIZE) { if (intsli]) { if (i = N_DATA_SETS-1) { myFile.seekg(ints[DATA_SET_POINTER_OFFSET]); myFile.read((char*)ints,H_DATA_SETS*SIZE_INT); i = 0; continue; > results[j++] = } else break; } return results; Листинг 15.15. DiskManager.cpp #include "btree.hpp" #include <assert.h> // при конструировании пытаемся открыть файл, если тот существует DiskManager::DiskManager(): myFile("RACHEL.IDX",ios::binary | ios::in | ioa::out I ios::nocreate) < // установить указатель в пустое значение for (int i - 0; i< MaxPages; i++) myPages[i] = 0; myCount = 0; } // вызывается конструктором btree // если файл открыт, прочитать необходимые числа, // в ином случае создать новый файл int DiskManager::Create() { if (!myFile) // сбой nocreate, первое создание файла { // открываем файл и создаем его myFile.open("RACHEL.IDX",ios::binary | ios::in | ios::out); char Header[PageSize]; int MagicNumber = 1234; // число, которое мы можем проверить memcpy(Header,fcMagicNumber,SIZE_INT);
Живучесть объектов Глава 15 int NextPage = 1; memcpy(Header+SIZE_INT,SNextPage,SIZE_INT); memcpy(Header+B*SIZE_INT),SNextPage,SIZE_INT); Page::SetgPageNumber(NextPage); char title[]="RACHEL.IDX. Ver 1.01"; memcpy(Header+C*SIZE_INT),title,strlen(title)); myFile.flush(); myFile.clear(); myFile.seekp@); myFile.write(Header,PageSize); return 0; // мы открыли файл, он уже существует // получить необходимые числа int MagicNumber; myFile.seekg(O) ; myFile.read((char *) SMagicNumber,SIZE_INT); // проверяем волшебное число. Если оно неправильно, то файл // поврежден или это не индексный файл 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.seekgB*SIZE_INT); myFile.read((char*) SFirstPage,SIZE_INT); const int room = PageSize - C*SIZE_INT) ; char buffer[room]; myFile.read(buffer,room); buffer[20]='\0'; // cout « buffer « endl; // считать все страницы 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; // записать числа, которые понадобятся в следующий раз void DiskManager::Close(int theRoot) for (int i = 0; i< MaxPages; 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.seekpB*SIZE INT);
Живучесть объектов и шифрование Часть IV myFile.write((char*) StheRoot,SIZE_INT); myFile.close(); ) // функция оболочки int DiskManager::NewPage(Index& 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->GetPageHumber(); > 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]-X3etTime () < myPages [lowest] -X3etTime () && 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; 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 [1] 7/6/98 We hold these truths to be se.. [2] 7/6/98 I regret that I have but one ... [3] 7/6/98 a search, a bang forces a rep... [4] 7/6/98 extending this so that the us... [5] 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 [1] 7/6/98 We hold these truths to be se... [2] 7/6/98 I regret that I have but one ... [3] 7/6/98 a search, a bang forces a rep... [4] 7/6/98 extending this so that the us... [5] 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 [1] 7/6/98 We hold these truths to be se... [2] 7/6/98 I regret that I have but one ... [3] 7/6/98 a search, a bang forces a rep... [4] 7/6/98 extending this so that the us... [5] 7/6/98 extending this so that the us... Choice <0 to stop>: Ясно видно, что профамма работает хорошо. Найдено пять объектов Note со словом that, и мы можем просмотреть их один за другим. Взглянем на структуру самой базы данных: Page 2: Page 2: Page 2: Page 2: Page 2: Page 3: Page Page Page Page Page Page Page Page Page Page Page Page 4: Page 4: Page 4: Page 4: Page 4: Page 4: Page 3: Page 6: Page 6: Page 6: Page 6: Page 6: Page 6 : Page 6: Page 6: Page 6: Page 6: Page 6: Page 6: Page 6: Page 6: Page 6: Page 6: Page 6: PROGRAM PURPOSES QUESTION READ REASONABLE REGRET REGRET REPORT REPRESENT SEARCH SEE SELF-EVIDENT SIMPLE SINGLE SPECIAL STARTING STORED STRUCTURE SYSTEM TAKES TEXT THAT THE THESE: THESE THIS THREE TREATED TREATS TREE TRUTHS TURNED TYPES USED> USER WANTS WHEN WITH WNJFILE WORD 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]; bool 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 ' 0 ' : fQuit = true; break; I else { if ( myTree.Insert(buffer) ) cout « "Inserted.\n"; buffer[0] = '\0'; > } return 0; } Функция main() создает дерево и ожидает ввода пользователя. Если пользователь введет флаг, вызыва- вызывается соответствующий (глобальный) метод (doFind() или ParseTreeO). Если введен флаг с восклицатель- восклицательным знаком, то дереву посылается команда распечатать себя (таким образом выводится статистика). Первая исполняемая строка программы такова: BTree myTree ; Здесь, конечно, вызывается конструктор ВТгее: ВТгее::ВТгее():myRoot@) { 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 (!myFile) // сбой nocreate, первое создание файла { // открываем файл и создаем его myFile.open("RACHEL.IDX",ios::binary | ios::in | ios::out); char Header[PageSize]; int MagicNumber = 1234; // число, которое мы можем проверить memcpy(Header,SMagicNumber,SIZE_IHT);
Живучесть объектов Глава 15 int NextPage = 1; memcpy(Header+SIZE_INT,SNextPage,SIZE_IHT); memcpy(Header+B*SIZE_INT),SNextPage,SIZE_INT); Page::SetgPageNumber(NextPage); char title[]="RACHEL.IDX. Ver 1.01"; memcpy(Header+C*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) // сбой nocreate, первое создание файла { // открываем файл и создаем его myFile. clear () ; myFile.open ("RACHEL.DAT"Доз::binary | ios::in | ios::out I 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; Затем то же самое мы делаем с 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(bu?f,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,slen,SIZE_INT); memcpy(buffer+SI2E_INT,StheTime,SIZEJTIME); memcpy(buffer+SIZE_INT+SIZE_TIME,newNote,len); myFile.clear(); myFile.flush(); myFile.seekp@,ios::end); int offset = (int) myFile.tellpO ; 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 индекс, который, в свою очередь, будет указывать на смещение в файле данных. Вскоре мы увидим, как это работает. Заметьте так-
Живучесть объектов ¦rrr Глава 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]); } После возвращения в 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->GetPageNumber() ; Новая страница создана, помечена как листовая, вставлена в массив страниц DiskManager и записана на диск. Номер новой страницы возвращается классом BTree, и это значение присваивается переменной BTree::myRoot. Рассмотрим этапы создания, вставки и записи новой страницы. Первым этапом является создание но- новой страницы: Раде::Раде(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);
Живучесть объектов и шифрование U Часть 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++] = 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() && myPages[i]->GetLocked() == false) lowest = i; assert(myPages[lowest]); Save(myPages[lowest]); delete myPages[lowest];
Живучесть объектов Глава 15 myPages[lowest] = newPage; Новая страница вставляется в массив страниц DiskManager, если для нее есть место в оперативной памяти. В ином случае дольше остальных не используемая страница удаляется из памяти, если она не заблокирована. Заключительный этап: страница записывается на жесткий диск: void DiskManager::Save(Page* pg) i pg->Write(myFile); Когда ключ добавлен, BTree::AddKey() не увидит нуля в значении корня (myRoot) — тот будет указы- указывать на уже созданную страницу: if ('myRoot) myRoot = theDiskManager.NewPage (index,true); else Page * pPage = GetPage(myRoot); retVal = pPage->Insert(index); if (retVal) // разделить корень { } Корневая страница извлекается с помощью GetPage(), и ей сообщается о вставке нового индекса. От- Ответственность за получение страницы BTree::GetPage() возлагает на DiskManager: { return theDiskmanager.GetPage(page); ) DiskManager, возможно, уже содержит эту страницу в памяти: в ином случае он берет ее с жесткого диска и заносит в свой массив страниц. ПРИМЕЧАНИЕ Несколько слов о терминологии. Объект, который мы назвали Index, должен, по всем соображениям, называться key — файлы, хранящие ключи и связывающие их с записями, должны называться Index. Впрочем, страницы вызывают ин- индексы ключей, что приводит к нескончаемой путанице в именах. Далее Index в методе BTree::AddKey() добавляется к странице при вызове Page::Insert(), которой пере- передается новый Index. Page::Insert() принимает два параметра, где второй по умолчанию установлен в значе- значение false и показывает, что мы не ищем записи с переданным ключом, а добавляем ключ к индексу: int Page::Insert(Indexs 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;
Живучесть объектов и шифрование Часть IV Вид страницы (узловая или листовая) определяется переменной myVars. Если страница листовая, ин- индекс добавляет указатель на данные. Страница заблокирована (чтобы не выйти из памяти!) и выполняется функцией FindLeaf(), которой передается индекс и флаг, указывающий, что это не запрос, а вставка: int Page::FindLeaf(IndexS rlndex, bool findOnly) { int result = 0; // нет дубликатов! 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; // Этот элемент индекса пока не существует, // прежде чем поместить его в индекс, // поместите запись в 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(Indexs 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-l && rlndex <= myKeys[Order/2-1]) Push(rlndex); else Sibling->Push(rlndex); result = NewPg;
Живучесть объектов Глава 15 В двух словах, если мы должны разбить страницу, то мы берем правую половину страницы, помещаем ее в новую страницу и возвращаем результат, который является номером новой страницы. В ином случае мы возвращаем нуль, указывая, что разбиения не было. В этом конкретном случае наша страница пока не заполнена, поэтому мы помещаем в нее новый ин- индекс: void Page::Push(IndexS rlndex,int offset,bool first) { bool inserted = false; assert(myVars.myCount < Order); for (int i=offset; i<Order && i<myVars.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() функции ВТгее::AddKey(), должно быть ненулевым. Ненулевой результат указывает, что разбита корневая страница, в результате чего создается наша первая узловая страница: if (retVal) // корневая страница разбита { рРаде = GetPage(myRoot); Index index (pPage-XSetFirstlndex ()) ; 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); 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->InsertLea?(Sib); Затем myRoot мы устанавливаем в PageNumber новой корневой страницы. Сделаем краткий обзор. Прежде чем мы разобьем страницу, наше дерево будет выглядеть, как показано на рис. 15.10. РИСУНОК 15.10. Перед разбиением страницы. Корень - four | score | seven |страница 1 7 (Данные) Корневая страница была листовой, и все ее индексы указывали на данные. После разбиения наше де- дерево выглядит, как показано на рис. 15.11. Корень- РИСУНОК 15.11. После разбиения страницы. | and | score [ | |страница 2 страница 3 score [ seven | years | | \1 (Данные) Создана новая страница (страница 2), которая имеет два индекса. Первый указывает на старую страни- страницу 1, а второй — на новосозданную страницу 3. Как же теперь добавляются данные? Вызов функции BTree::AddKey() проходит как обычно, и она вызывает, в свою очередь, Page::Insert() корневой страницы. Корневая страница, теперь распознающая себя узлом (а на листом), вызывает функ- функцию InsertNode(), передавая ей новый Index: int Page::InsertNode(IndexS rlndex, bool findOnly) Здесь, опять же, retVal (возвращаемое значение) устанавливается в нуль, и локальный флаг inserted остается установленным в значение false. Прежде всего проверяется, выше ли индекс первой записи в узле. Если так, то мы устанавливаем данные первого ключа (MyKeys[0]) так, чтобы те указывали на новые данные из Index, и вставляем Index в страницу, на которую указывает myKeys[0]:
Живучесть объектов Глава 15 if (rlndex < myKeys[O]) { 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 && !findOnly) // вернуть указатель на новую страницу < Index * plndex = new Index(GetPage(retVal)->GetFirstIndex()); pIndex->SetPointer(retVal); retVal = InsertLeaf(*plndex); ) Вот полный метод InsertNode(): int Page::InsertNode(IndexS rlndex, bool findOnly) { int retVal =0; bool inserted = false; int i,j; assert(myVars.myCount>0); // узлы имеют как минимум 1 assert(myKeys[0].GetPointer()); // должен быть корректен // идет ли он перед моей первой записью? 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[ij) { retVal=myKeys[i].Insert(rlndex,findOnly); inserted = true; break;
Живучесть объектов и шифрование Часть IV // определить, где она должна находиться if ("inserted) for (j = 0; j<i && j+1 < myVars.myCount; { assert (myKeys[ j+1] .Get.Pointer () ) ; if (rlndex < myKeys[j+l]) { retVal=myKeys[j].Insert(rlndex,findOnly) inserted = true; break; assert(inserted); // заменить на исключение в случае неудачи! // если вы имеете разделение if (retVal && 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. В результате получите следующий вывод: ?: -?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 Не 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 » He hold these truths to be self-evident that all men are created equal [1] 7/7/98 He 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, BTreeS myTree) { int list[PageSize]; for (int i = 0; KPageSize; i++) list[i] = 0; int k = 0; char * pi = searchString; while (pl[0] == ' ') int offset = myTree.Find(pi); Функция BTree::Find() очень проста. Если страница назначена корню, она создает индекс из буфера и вызывает функцию Find() на корневой странице, передавая ей новосозданный индекс: int BTree::Find(char * str) < Index index(str); if ('myRoot) return 0L; else return GetPage(myRoot)->Find(index) ; } Функция GetPage() просто возвращает страницу. Page::Find() — это inline-функция, которая вызывает Insert(), передавая ей булевый флаг true и указывая, что это не настоящая вставка, а поиск: int Find(lndexs idx) {return Insert(idx, true);} Такой вариант повторного использования программного кода элегантен, но соглашение об именах, вероятно, не совсем удачно. Когда вы вызываете функцию Insert() со вторым параметром, установленным в значение true, то вовсе не вставляете, а просто ищете данные. Функция Page::Insert() остается такой же, как была: она определяет, какая это страница — листовая или узловая, и вызывает либо InsertLeaf(), либо InsertNode() соответственно. Во всяком случае, она также передает флаг, указывающий, что мы выполняем поиск, а не вставку. Такая же логика, которая ранее использовалась для выявления "копий" ключей, применяется здесь для поиска смещения этого ключа для WNJFUe. Как вы могли догадаться, возвращаемое значение в конечном счете будет содержать смещение в WNJFile. Это значение передается обратно вверх к функции DoFind(). Затем оно используется в качестве параметра для вызова WNJFile::Find(): int offset = myTree. Find (pi); if (offset) { int *found = BTree:-.theWNJFile. Find (off set) ; Функция 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*SI2E_INT); while (j < MAX_ARRAY_SIZE) { if (ints[i]) { if (i = N_DATA_SETS-1) { myFile.seekg(ints[DATA_SETJPOINTER_OFFSET]); myFile.read((char*)ints,N_DATA_SETS*SI2E_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. Реляционные базы данных организованы в записи и поля. Author John Doe Jane Writer Jack Sprat Address 100 Main Street 50 Apple Way 100 Candlestick City Anytown SomeCity HotTown State AnyState ST XY Zip 01320 09939 99939 Записи Мощь реляционных БД проявляется в том случае, когда вы создаете две таблицы и связываете их. Та- Такую связь можно создать при условии, что для каждой записи существует совместно используемый уни- уникальный идентификатор. Например, если одна таблица содержит список авторов, пишущих для вашего издательства, а вторая — список публикуемых книг, то две эти таблицы можно связать по автору. Один из способов, позволяющий сделать это, — задать каждому автору уникальный идентификатор authorlD и до- добавить поле таких идентификаторов в таблицу Titles. Из рис. 16.2 видно, что каждый автор имеет уникальный идентификатор. Этот идентификатор появляет- появляется также и в таблице Titles, связывая обе эти таблицы и позволяя увидеть, что книги What A Wonderful Program и Another Wonderful Program были написаны автором, номер которого 102. Автором с номером 102 является Jane Writer, который живет по улице 50 Apple Way в SomeCity, ST. Первичный ключ РИСУНОК 16.2. Каждый автор имеет уникальный идентификатор. Первичный ключ AuthorlD 101 102 103 Author John Doe Jane Writer Jack Sprat Address 100 Main Street 50 Apple Way 100 Candlestick City Anytown SomeCity HotTown State AnyState ST XY Zip 01320 09939 99939 Соединение - Внешний ключ Title ID 1001 1002 1003 1004 Title Teach Yourself C++ In 21 Days What A Wonderful Program Another Wonderful Program You Gotta' Be Quick Description Primer on C++ Intro to programming Sequel to What A Wonderful... Nursery rhymes for programmers AuthorlD 101 - 102 102 103 Поле AuthorlD уникально идентифицирует каждого автора в таблице Author и поэтому является первич- первичным ключом этой таблицы. Когда ключ появляется в таблице Titles, он создает связь между таблицами, таким образом, он является внешним ключом таблицы Titles. Внешний ключ — это просто ключ, который служит первичным ключом в какой-либо другой таблице. Это прекрасная идея, к которой мы вернемся вскоре. Такие таблицы, как мы могли предположить, могут быть довольно большими. Поиск отдельного автора будет занимать некоторое время. Для ускорения поиска столбцы могут быть проиндексированы. При создании индекса система создает второй файл, сорти- сортируемый по индексируемому значению. Второй файл предоставляет указатель на оригинальный файл. В боль- большинстве случаев этот указатель является обычным смещением записи в файле, измеряемым в количестве записей или в байтах.
Реляционные базы данных и живучесть Глава 16 Таблица всегда индексируется по своему первичному ключу. Вы также могли бы проиндексировать любое другое поле, чтобы ускорить поиск записи, основанный на информации, содержащейся в этом поле. На- Например, вы могли бы проиндексировать поле не только по Author ID (первичный ключ), но и по имени автора. Проиндексировав таблицу по имени автора, вы сможете пройти по индексу Author Name, выиски- выискивая нужную запись, а затем использовать смещение для перехода к этой записи в файле данных: AuthorW (Первичный ключ) Author Address City State Zip 101 102 103 104 105 Author John Doe Jane Writer Jack Sprat Jesse Liberty Ernest Hemming 100 Main Street 50 Apple Way 100 Candlestick 1 Fake Street 50 Famous Dr. Anytown SomeCity HotTown Boston NY AnyState ST XY MA NY 01320 09939 99939 01297 11209 Offset John Doe Ernest Hemming Jesse Liberty Jack Sprat Jane Writer 0 4 3 2 1 Использовать индексы очень удобно, поскольку индекс может быть найден очень быстро путем бинар- бинарного поиска (который описан в главе 13). С увеличением файла данных количество необходимых записей увеличивается очень медленно. Чтобы найти запись в файле, содержащем п записей, необходимо просмот- просмотреть в среднем log 2(n) записей. Если, с другой стороны, просматривать каждую запись в оригинальной БД, потребуется просмотреть в среднем п/2 записей. В базе данных, содержащей 65536 (п) записей, последовательный поиск приведет к просмотру в сред- среднем 32768 записей (п/2), тогда как при бинарном поиске будет просмотрено всего лишь 16 записей (log 2(n))! Учтите также, что поиск каждой из 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 * Первая нормальная форма ANF) предписывает, что каждое поле (часто называемое атрибутом) хра- хранит только одно значение (в отличие от списка значений). Вторая нормальная форма BNF) вносит требо- требование, чтобы каждая строка (запись) была уникальна. В целях выполнения этого требования для каждой строки создается уникальный идентификатор, называемый опознавательным столбцом или опознаватель- опознавательным полем. Если у вас нет других значений для индексации, вы могли бы сделать опознавательный столбец первичным ключом, но это не обязательно. Опознавательный столбец создается только в целях сохранения уникальности. Третья нормальная форма CNF) — это наиболее популярный компромисс между слишком слабым и слишком сильным нормированием. В 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 ; >; При отображении его в реляционную БД мы отображаем указатель на идентификатор объекта (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 Я Текстовые файлы Источники данных состоят из нескольких компонентов, полностью описывающих данные для доступа. Эти компоненты включают следующие элементы: ¦ Особый набор данных н Информацию о соединении, необходимую для доступа к данным ¦ Информацию о местонахождении источника данных Источники данных должны конфигурироваться с помощью администратора ODBC. Полный набор фун- функциональных возможностей зависит от установленного драйвера ODBC и его возможностей. Администратор ODBC используется для управления источниками данных. В системном реестре Windows он хранит информацию об источниках данных и их соединениях. С помощью администратора ODBC можно выполнять следующее: ¦ Добавлять и удалять драйверы ODBC ¦ Добавлять, изменять и удалять источники данных Модель курсора ODBC Большинство систем управления БД предоставляют простой метод получения данных из запроса. Стро- Строки возвращаются приложению по одной до тех пор, пока не будет возвращена последняя строка результи- результирующего набора. Вернуться обратно к строке, не вызвав запрос еще раз, невозможно. С другой стороны, интерактивные приложения обычно позволяют пользователю перемещаться вперед и назад по набору дан- данных с помощью клавиш со стрелками или клавиш Page Up и Page Down. Курсор — это механизм, позво- позволяющий обрабатывать одновременно отдельные строки, возвращаемые в результате запроса; курсор указывает на текущую строку в наборе данных. Курсор, предоставляющий возможность перемещаться вперед и назад по результирующему набору, называется прокручиваемым курсором. Курсор, позволяющий изменять и уда- удалять полученные данные, называется прокручиваемым, обновляемым курсором. При использовании курсоров большую роль играет контроль параллелизма (особенно при работе с транзакциями). ODBC предоставляет гри следующих типа курсоров: ¦ Статические курсоры. В статическом курсоре принадлежность, расположение и значения результиру- результирующего набора неизменны при открытом курсоре. Эти элементы остаются неизменными до тех пор, пока курсор не закроется. Такой тип курсора особо полезен для приложений типа "только-для-чте- ния". Статический курсор предоставляет очень стойкое представление данных. Большим недостатком этой модели является тот факт, что изменения, сделанные другими транзакциями при открытом курсоре, невидимы для такого курсора. ¦ Курсоры, управляемые с клавиатуры. В курсоре, управляемом с клавиатуры, принадлежность и рас- расположение результирующего набора неизменны, тогда как значения при открытом курсоре можно изменять. ¦ Динамические курсоры. В динамическом курсоре изменения, внесенные кем-либо, а также незавер- незавершенные изменения, вносимые владельцем курсора, видимы для него. Изменения, внесенные кем- либо, могут повлиять на принадлежность и расположение результирующего набора: 7мл курсора Точность Постоянство Параллелизм Статический Низкая Высокое Средний Управляемый с клавиатуры Средняя Среднее Средний Динамический Высокая Низкое Высокий Контроль параллелизма с помощью 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_REIATIVE: получить набор строк, находящийся на расстоянии в п строк от текущего набора строк (п может быть как положительным, так и отрицательным числом) ¦ 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 AuthorlD Author Name Author Notes t AutoNumber Text "Text Jj ID AuthorlD BookID AutoNumber Number : Number РИСУНОК 16.3. Таблица Book. РИСУНОК 16.4. Таблица AuthorBookJoin. РИСУНОК 16.5. Пример данных. :т tar»» 1 Jesse Liberty 2 Dostoyevsky 3 Powers 4 Thompson 5 Caleb Can Б Erich Gamma 7 Richard Helm 8 Ralph Johnson 9 John Vftssides 10 Orson Scott Cai 1 Teach Yourself C++In 21 Days2E 2 Teach Yourself C++ In 24 Hours 3 Beginning Object Onerrted Analysts And Design 4 Clouds To Code 5 C++Unleashed Б Crime 3. Punishment 7 Galatea 2.2 8 Fear and Loathing in Las Vegas 9 Alienist ID Design Patterns 11 Brothers Karamazov Fiction Fiction Fiction Programming Fiction 5 Excellent tour del 4 Hunter Thompaoi 3 Great mystery, wf 5 Classic 7 All time favorite n Заметьте, что каждая их этих таблиц была создана в одной базе данных 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.7. Обзор классов в мастере. Заметим, что классом представления в этом случае является CRecordView, как и ожидалось. При сборке приложения мастер обеспечил нас всех необходимым, но MFC ожидает от создания объекта диалогового окна для представления, в котором будет отображаться содержимое таблицы. Поэтому предварительное диалоговое окно, показанное на рис. 16.8, выглядит таким спартанским. Для вывода полей таблицы установим на форме несколько элементов управления (рис. 16.9). РИСУНОК 16.8. Предварительное диалоговое окно для примера приложения. РИСУНОК 16.9. Элементы управления для вывода полей. Затем эти элементы управления необходимо подключить к содержимому базы данных. Для этого nepeftv дем к функции 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); 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_jsSet) ; 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, IDCJTITLE, m_pSet->m_Book_Title, m_pSet); //}>AFX DATA MAP В результате сборки и запуска этого кода получится приложение, которое фактически напрямую читает из БД, как показано на рис. 16.11. tlb \. :.--..,хфъряц |Wfto«IC»l..^ '- .„, . ... РИСУНОК 16.10. Создание переменных-членов. РИСУНОК 16.11. Приложение прочитало запись из базы данных. Видно, что приложение работает, но как? Вся суть заключается в переменной-члене данных m_pSet класса CRecordView. Эта переменная является указателем на объект CBookCoIlectionUnleashedSet. И сам класс, и переменная-член были созданы и добавлены мастером приложений (Application Wizard). Так что же такое CBookCoIlectionUnleashedSet? Этот тип был создан мастером приложений и унаследо- унаследован от класса CRecordSet: class CBookCoIlectionUnleashedSet : public CRecordset { public: CBookCoIlectionUnleashedSet(CDatabase* pDatabase = NULL); DECLARE_DYNAMIC(CBookCoIlectionUnleashedSet) // Поля/Param Data //{(AFX_FIELD(CBookCoIlectionUnleashedSet, CRecordset) long m_Book_ID; CString m_Book_Title; CString m_Category; long m_Rating; CString m_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 _DEBHG virtual void AssertValid() const; virtual void Dump(CDuinpContext& dc) const; «endif }; CRecordSet, no существу, является итератором в вашей таблице. Он имеет одну переменную-член для каждого из полей выбранной таблицы, и использовать вы их можете для обмена данными с самой табли- таблицей. Более подробно мы изучим объекты 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; CString m_Author Name; CString m_Author_Notes; //)}AFX_FIELD // Перекрытия // Мастер ClassWizard сгенерировал перекрытие виртуальной функции //{{AFX_VIRTUAL(AuthorRecordSet) public: virtual CString GetDefaultConnect(); // Строка соединения по умолчанию virtual CString GetDefaultSQL(); //По умолчанию SQL для набора записей virtual void DoFieldExchange(CFieldExchange* pFX); // Поддержка RFX // } }AFX_VIRTUAL // Реализация #ifdef _DEBUG virtual void AssertValidO const; virtual void Dump(CDumpContextS dc) const; #endif }; class BookAuthorRecordSet : public CRecordset { public: BookAuthorRecordSet(CDatabase* pDatabase = NULL); DECLARE_DYNAMIC(BookAuthorRecordSet) // Поля/Param Data //{{AFX_FIELD(BookAuthorRecordSet, CRecordset) long m_ID; long m_AuthorID;
Живучесть объектов и шифрование Часть IV long mJBookID; //))AFX_FIELD // Перекрытия // Мастер ClassWizard сгенерировал перекрытие виртуальной функции //{{AFX_VIRTUAL(BookAuthorRecordSet) public: virtual CString GetDefaultConnect(); // Строка соединения по умолчанию virtual CString GetDefaultSQL(); // По умолчанию SQL для набора записей virtual void DoFieldExchange(CFieldExchange* pFX); // Поддержка RFX //})AFX_VIRTHAL // Реализация #ifdef _DEBOG virtual void AssertValidQ const; virtual void Dump(CDumpContextS dc) const; «endif Здесь приведен программный код, сгенерированный 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, IDCJSATING, m_pSet->m_Rating, m_pSet); DDX~FieldText(pDX, IDCJTITLE, m_pSet->m_Book_Title, m_pSet) ; //)Tafx_data_map OpdateAuthor(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_AuthorID); ars.m_strFilter = authorSQL; if ( ars.IsOpen() ) ars.Requery(); else ars. Open () ; display = ars.m_Author ^Name; pos = m_Author_Control.AddString(display); bars .MoveNext () ; return true;
Реляционные базы данных и живучесть Глава 16 Суть UpdateAuthor в том, что мы передаем ему идентификатор выбранной в данным момент книги, полученный из объекта CBookCollectionUnleashedSet, подключенного к RecordView. После входа в функ- функцию UpdateAuthorO мы просим окно списка самостоятельно очиститься (m_Author_Control.ResetContent();) и настраиваем запрос в БД на записи в таблице AuthorBookJoin, соответствующие этому идентификатору BooklD. Рассмотрим этот программный код шаг за шагом: 1. Установить строку поиска, которая станет конструкцией WHERE оператора SQL SELECT: booJcAuthorSQL. Format ("BooklD = %d" , id) ; bars.m_strFilter = boolcAuthorlD; Автор делает это в два этапа. Сначала он создает объект CString, а затем присваивает эту строку переменной-члену m_strFilter класса CRecordSet. В результате CRecordSet при каждом открытии и повторном запросе на данные будет возвращать набор соответствующих данных. (Обратите внимание на то, как это делается: CRecordSet добавляет к запросу конструкцию WHERE и вставляет строку, содержащуюся в m_strFilter.) 2. Открыть набор данных: bars.Open(); 3. Провести итерацию по записям: while ( ! bars.IsEOFO ) 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_Author Name; pos = m_Author_Control.AddString(display); По завершении мы сможем выводить имена авто- авторов, связанных с каждой из записей, как показано на рис. 16.12. TU> um FM4 tola' R»ae> I-'ЧГ 1- I» H* 1«I»I»!J i« erg I >-ra»i t p il . ;..' .. 1 s' *' ' "»Ml.J. '\f ¦ ¦ .» .,¦ ¦ -i. X\ •:* Пи ¦.- t * РИСУНОК 16.12. Вывод имен авторов. Редактирование Попробуйте изменить рейтинг или категорию ка- какой-либо из книг. Вот это да! БД обновляется при пере- переключении к другой записи. Это волшебство происходит в методе класса представления DoDataExchange(), который вызывается автоматически при смене текущей записи. Метод DoDataExchange() вызывается функцией CWnd::UpdateData, принимающей параметр bSavaAndVa- lidate — булевый флаг, по умолчанию установленный в значение true. При переходе от одной записи к другой старая запись обновляется и значения в элементах управления диалогового окна записываются об- обратно в БД с помощью механизма DDX_FieldText. Немедленные обновления Но что делать в том случае, когда вы хотите немедленно получить воздействие? Добавим кнопку Update, обновляющую текущую запись на месте. Программный код довольно прост. 8 Class Wizard создается обра-
Живучесть объектов и шифрование Часть IV ботчик, а затем остается перенести данные из элементов управления в переменные CRecordSet и обновить БД: void CBookCollectionOnleashedView::OnBookUpdate() m_pSet->Edit() ; UpdateData() ; m_pSet->Hpdate(); AfxMessageBox("Updated",MB_ICONEXCLAMATION | MB_OK); К сожалению, только что описанный подход редактирования неприменим для поля авторов, поскольку оно является коллекцией. С этим полем нам придется немного потрудиться. Когда функция UpdateData() вызывает функцию DoDataExchange(), она передает ей объект CDataExchange. Одним из членов объекта CDataExchange является mJbSaveAndValidate — флаг, указывающий направление обмена данными (если флаг установлен в значение false, данные из БД передаются в элементы управления; если флаг установлен в значение true, мы записываем значения элементов управления в БД). Эту информацию можно передать методу UpdateAuthor(), но это нам не очень поможет в нашем прило- приложении, поскольку само по себе окно списка используется только для чтения. Для редактирования списка авторов нам необходимо обновить таблицу BookAuthorJoin, проведя нетривиальное редактирование. Что нам может понадобиться? Мы можем при желании добавить автора — в случае чего можем доба- добавить автора, уже существующего в таблице Author, или можем обновить эту таблицу. В дальнейшем нам может понадобиться отредактировать список авторов в этом списке, взятом из таблицы BookAuthorJoin. Завершая описание пользовательского интерфейса, необходимо четко понимать, чего мы пытаемся достигнуть, и написать соответствующий программный код. Добавление записей Чтобы добавить новую книгу в систему, необходимо со- создать новую запись. Для начала добавим кнопку Add Book и в Class Wizard создадим код обработки этой кнопки. Затем создадим новое диалоговое окно для ввода данных (рис. 16.13). Пользователь заполняет поля и щелкает на кнопке ОК (чтобы убедиться, что пустых полей нет, запрещаем исполь- использовать кнопку ОК до тех пор, пока все поля не будут запол- заполнены). Затем мы извлекаем данные из полей, создаем новую запись и вставляем ее в БД, как показано в листинге 16.1. Листинг 16.1. OnBookAdd void CBookCollectionUnleashedView::OnBookAdd() РИСУНОК 16.13. Обработка ввода данных. CAddBookDialog dig; if ( dlg.DoModalO 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 | MB_OK ) ; catch (...) CString s2; s2.Format("Unknown Error in OnBookAdd!"); AfxMessageBox("s2",MB_IC0NST0P | MB_OK ); else AfxMessageBox("No Record Added",MB_ICONEXCLAMATION I MB_OK ) ; Локальный экземпляр CAddBookDialog создается в стеке: CAddBookDlg 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. Сделать это можно, если вызвать функцию ExecuteSQL() любого объекта CDataBase. Типичное применение этого метода состоит в вызове параметризированной хранимой процедуры: CString sqlCmd; sqlCmd.Format("exec usp_SomeProcedure \'%s\', \'%s\', %ld, \'%s\', \'%s\'", Stringl, String2, String3,
Живучесть объектов и шифрование Часть IV String4, Strings) ; try { CDatabase * pDB = new CDatabase; pDB->OpenEx("DSN=MySystemDSN"); pDB->ExecuteSQL( sqlCmd ) ; // ... В этом программном коде мы создаем CString, который вызывает хранимую процедуру. Затем создаем объект CDatabase, используем его для открытия БД, передав имя DSN, и вызываем функцию ExecuteSQL(), передав ей объект CString. Установка характеристик БД При использовании объекта CDatabase фактически можно установить LoginTimeout и QueryTimeout, чтобы отрегулировать промежуток времени, в течение которого мы будем ожидать ответа от БД, прежде чем об- обработать сбой: pDB->SetLoginTimeout(loginTimeOut); pDB->SetQueryTimeout(queryTimeOut); Резюме В этой главе были рассмотрены фундаментальные понятия технологии реляционных БД и было показа- показано, как реляционные БД могут использоваться для хранения объектов. Отображение между реляционной БД и вашими объектами может быть обработано на различных уровнях абстракции. Многие поставщики предоставляют стандартизированные технологии (например, ODBC), упрощающие создание интерфейсно- интерфейсного слоя между вашей моделью объектов и самой реляционной БД. Создание итераторов по таблицам и отображение полей в БД на переменные-члены ваших объектов может стать намного проще, если в библиотеках каркасов приложений предоставлены классы поддержки. MFC — наиболее популярная библиотека такого типа — предоставляет два критических класса для работы с БД с помощью ODBC: CDataBase и CRecordSet. В этой главе было рассмотрено применение этих классов для работы с реляционными БД.
Реализация живучести объектов с помощью реляционных баз данных В ЭТОЙ ГЛАВЕ Объекты в Oracle 8 Использование внешних процедур, разработанных на языке C++ Отображение UML-диаграмм на объектно- реляционные базы данных Пример: система заказа покупок
Живучесть объектов и шифрование Часть IV Появившись более десяти лет назад, реляционные базы данных (БД) стали очень популярны. Приложе- Приложения создавались на языке третьего поколения CGL), таком как C++, чтобы получить доступ к данным, хранимым в реляционных БД, с помощью операторов SQL или какого-то другого средства разработки при- приложений. Реляционные БД очень хорошо подходят для хранения данных приложения, хотя не способны охватить его логику. Завершенное приложение должно содержать логику, а соблюдение этого условия часто приводит к проблемам при попытках согласования модели приложения с моделью данных. Объектно-реля- Объектно-реляционная БД сохраняет не только данные, но и логику. Такой результат достигается благодаря использова- использованию объектов для захвата атрибутов и функциональных возможностей, связанных с конкретными сущностями. С ростом конкуренции в торговле корпорации пытаются создавать эффективные приложения, простые для поддержки, а также рентабельные с точки зрения стоимости. Кроме того, необходимо, чтобы прило- приложения полностью соответствовали отдельным моделям и процессам бизнеса. Приложения могут быть напи- написаны для имитации различных состояний и процессов, например, сборочной линии на производстве. Стало обычным делом создавать такие приложения и использовать объекты для представления различных сущно- сущностей сборочной линии. Такие системы требуют многого от используемых ими приложений. Стоимость про- проектирования, разработки и поддержки этих приложений должна быть сведена к минимуму. Другим фактором серьезного обсуждения является тип данных, используемых в БД. Революция Internet и World Wide Web привела к повышению сложности используемых данных. Вместе с обычными скалярны- скалярными данными стали широко использоваться гипертекст, рисунки и аудиоданные. И поскольку большая часть современных приложений работает с реляционными БД, вы должны гладко (бесшовно) соединить объек- объектную технологию с существующей реляционной. Огас1е8 — пример популярной объектно-реляционной БД. Кроме своих встроенных типов, Oracle предо- предоставляет набор объектов для создания типов объектов и манипулирования ими. Типы объектов дают гиб- гибкость в моделировании сущностей реального мира вместе с операциями, которые могут выполняться над этими сущностями. OracleS позволяет получать доступ к хранимым данным из сред 3GL и предоставляет ряд возможностей, таких как кэширование объектов на клиенте и одновременное получение связанных объектов, что повышает производительность приложений, основанных на объектах. Из числа других объек- объектно-реляционных БД можно выделить IBM DB2 и Sybase System 10. Традиционные приложения реляционных БД могут извлечь выгоду из моделирования объектов и воз- возможностей мультимедиа, предоставляемых объектно-реляционными БД. Вы можете написать приложение на языке C++, которое получит выгоду из таких объектно-реляционных БД. Объекты в Oracle8 Система типов Огас1е8 была расширена для поддержки не только объектов, но и их коллекций. Вот несколько ключевых возможностей Oracle8, которые могут использоваться в объектно-реляционной пара- парадигме: ¦ Тип 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 ); / С помощью этой команды 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_no CHARE), accounts account_array); / /* Создать таблицу с использованием этого типа объекта */ CREATE TABLE customers OF customer_type; Create type ACCOUNTS as object ( ACCTNO number,
Живучесть объектов и шифрование HacrblV ACCTYPE charD), BALANCE number, CUST ref CUSTOMERS ПРИМЕЧАНИЕ Ссылки могут создаваться только на объекты, хранящиеся в таблице объектов. Оператор REF С помощью оператора REF можно получить ссылку на объект в таблице объектов. Следующий оператор показывает, как вставить объект в таблицу объектов, а также как получить ссылку на объект. В следующем примере предположим, что Person является таблицей объектов: /* Объявить переменную как ссылку на таблицу объектов*/ DECLARE reftojane REF Person; /* Оператор REF возвращает ссылку на таблицу 'pref */ /* и эта ссылка сохраняется в переменной 'reftojane' */ BEGIN INSERT INTO people pref VALUES (Person('Jane1,'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 (projno NUMBERE),~ projname CHARC0), projlocation CHARC0)); / /* Создать таблицу проектов с использованием типа объекта */
Реализация живучести объектов с помощью реляционных баз данных Глава 17 CREATE TABLE orl_projects OF project_type; /* Вставить записи в таблицу объектов */ INSERT INTO orl_projects VALUES A11, 'venus','orlando'); INSERT INTO orl_projects VALUES A12, 'saturn','miami'); /* Создать тип для использования в качестве вложенной таблицы */ CREATE TYPE project_table AS TABLE OF project_type; / /* Создать таблицу, использующую вложенную таблицу */ CREATE TABLE employees (empno NUMBERE), empname CHARB0), empprojs project_table) NESTED TABLE empprojs STORE AS nested_proj_table; /* Вставить в таблицу объектов */ INSERT INTO employees VALUES A2345, 'MIKE', project_table(project_typeE5555,'honda','orlando'), project_typeF6666,'toyota','miami1))); В этом примере показано, как создавать VARRAY с помощью операторов SQL: /* Создать тип объекта */ CREATE TYPE account_type as OBJECT (accoun t_no INT, account_type CHARB), balance DECA0,2)); / /* Создать VARRAY с использованием типа объекта */ CREATE TYPE account_array AS VARRAYA0) OF account_type; / /* Создать тип объекта */ CREATE TYPE customer_type AS OBJECT (cust_no CHARE), accounts account_array); / /* Создать таблицу с использованием типа объекта */ CREATE TABLE customers OF customer_type; /* Вставить в таблицу объектов */ INSERT INTO customers VALUES E5555, account_array(account_typeA1,¦С',1000.00) , account_typeB2,'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'; 2. Объявить прототип функции 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 С with context parameters ( context, data OCILOBLOCATOR >; end; / create or replace type ext_objtype as object ( data CLOB, member function ext_objtype_func return binary_integer ); / 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; / Прототип внешней функции может быть следующим: tlnclude <oci.h> #define DLLEXPORT declspec(dllexport) cdecl int DLLEXPORT c_func (OCIExtProcContext *ctx, OCILobLocator *lobl); int c_func (OCIExtProcContext *ctx, OCILobLocator *1оЫ) { /*3десь разместить программный код функции */ return 0 ; > Внешний вызов может быть протестирован с помощью PL/SQL: declare i binary_integer; x ext_objtype; begin x : = ext_obj type(EMPTY_CLOB()); i := x.ext_objtype_func(); DBMS_OUTPUT.PUT_LINEAext_objtype_func() returned ' I I i) ; end; Если процесс загружает DLL без символьной отладочной информации, то вы можете воспользоваться отладчиком Visual C++ для установки точек останова и выявления ошибки. При реализации методов объектов в виде внешних процедур, написанных на языке C++, слушатель вызывает процедуру по требованию; сле- следовательно, вы не сможете отладить процедуру с помощью стандартных методов. Для отладки в таких си- ситуациях в начало внешней процедуры можно поместить вызов Win32 API DebugBreak(). Рассмотрим объект DataStore, который может сохранять набор данных в символьном типе Oracle8 LOB (называемом CLOB). Над хранимыми данными можно проводить некоторые манипуляции, например, най- найти минимум, максимум, выполнить регрессию данных и т.п. Вот главные действия, которые необходимо выполнить для такой реализации: 1. Создать тип объекта для представления DataStore. Атрибуты и методы этого объекта должны отражать хранимые данные и функциональные возможности этого объекта. Объявим методы как external, по- поскольку тип требуемой обработки лучше реализуется с помощью языка C++:
Реализация живучести объектов с помощью реляционных баз данных Глава 17 create or replace type DataStore as object ( pid integer, name varchar2A0), 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)); 2. Объявить пакет, который будет использоваться для хранения всех внешних процедур: 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" (т.е. не изменять переменные пакета). 3. Реализовать тело типа объекта 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; / 4. Предоставить имя PL/SQL библиотеке, содержащей реализацию внешней процедуры: create or replace library datastore_lib as '<directory_of_library> / libdatastore.so' 5. Объявить тело пакета и определить связи между функциями пакета и ЗОЬ-функциями библиотеки: Create or replace package body DataStore_package as function datastore_findmin(data clob) return integer is external name "c_minimum" library datastore_lib language с with context,- function datastore_f indmax (data clob) return integer is external name "c_maximum" library datastore_lib language с with context; end; 6. Реализовать внешние процедуры на C++. Внешняя процедура принимает параметр CLOB и использу- использует его при вызове БД в качестве локатора LOB: #Include <oci.h> int c_minimum (OCIExtProcContext *ctx, OCILobLocator *lobl) { ubl bufptMAXBUFLEN]; 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, выполните следующие действия: 1. Создайте в БД таблицу, указав функцию OCI OCILobWriteO для загрузки атрибута CLOB объекта DataStore. 2. Поместите в DataStore_table запись: Insert into DataStore_table values A, 'testl', to_date('03-28-1998', 'MM-DD-YYYY'), EMPTY_CLOB() ) ; commit; 3. Используйте OCI-программу, использующую функцию OCI OCILobWriteO для загрузки атрибута CLOB объекта DataStore. 4. Откомпилируйте внешние процедуры (cminimum и c_maximum) и поместите их в библиотеку. 5. Вызовите методы объекта, использующие процедуры PL/SQL: Select d.DataMinimum(), d.DataMaximum() from DataStore_table d; Отображение UML-диаграмм на объектно-реляционную базу данных Чтобы понять, как UML-диаграммы можно отобразить на объектно-реляционную БД, используем пример приложения, показанного на рис. 17.1 и представляющего простую банковс- банковскую систему, содержащую сущности CUSTOMERS, ACCOUNTS и ADDRESS. Взаимосвязь между сущностями может быть опи- описана следующим образом: CUSTOMERS ACCOUNTS : CUSTOMERS : ACCOUNTS ADDRESS = : ADDRESS = один • один : =один : : мя один один UML-спецификации для этого сценария включают следую- следующие стадии: ¦ Проектирование базы данных. Определяется, как UML- спецификации будут представлены с использованием ти- типов объектов. ¦ Генерирование на C++. Создаются заголовочные и исход- исходные файлы для набора типов, определенных во время РИСУНОК 17.1. Проект простого банковского проектирования базы данных. приложения. ¦ Генерирование сервера. Генерируется DLL, используемые для реализации проекта базы данных на сервере. Для иллюстрирования процедуры на примере приложения используем Oracle8. Customers Custno Custom AddCustomer {) DetetoCustomer () GedDj) SedD GetNamaM SetNamef GetAddress () GetAccountj) 1 T/ 1 Accounts Account no Account spec Balance' UpdateBahnce () AddAccountl) RGfnmeAccount {) Address Address! Address2 City State Zip \ Проектирование базы данных Каждый тип, определенный в UML, отображается на тип данных Oracle8. Затем этот тип может исполь- использоваться при определении таблиц; взаимосвязь типов реализуется в Огас1е8 с помощью типа REF. Впро- Впрочем, поскольку ADDRESS смоделирован по строгому агрегированию, этот тип внедряемый. Отображение типа на экземпляры нескольких таблиц устанавливается с помощью идентификатора зоны (Object Database Designer — ODD — особый тип, используемый при генерировании класса C++):
Реализация живучести объектов с помощью реляционных баз данных Глава 17 create type address_type as object ( addressl varchar2A00), address2 varchar2A00), city varchar2D0), state char B) , zip varchar2A0)); / create type account_type as object account_no account_spec balance customer address NUMBERA0), CHARE), DECIMALA0,2), REF customer_type, address_type Генерирование классов на C++ Object Database Designer (ODD — конструктор объектных баз данных) — это новый продукт из набора продуктов Designer/2000. Он предоставляет эффективный доступ из программ на языке C++ к базам дан- данных Огас1е8. Этот продукт может использоваться на всех стадиях проектирования, создания и доступа к объектно-реляционной БД. ODD имеет несколько ключевых возможностей: ¦ ODD моделирует абстрактные типы с помощью UML. ¦ ODD трансформирует модель абстрактного типа в проект БД. ¦ Моделирование схемы БД Огас1е8 может использоваться для определения физических объектов Oracle8. ¦ Генерирование SQL может выполняться для транслирования проектов объектно-реляционных БД в SQL DLL. ¦ Генератор C++ создает объявления классов C++ и реализует UML-объекты. Сложности и тонкости доступа к БД скрыты от пользователя. В результате сгенерированные классы C++ могут трактоваться как и обычные. Процесс разработки Генератор объектного слоя C++ в Designer/2000 позволяет программистам C++ использовать Oracle в качестве долговременного хранилища. Кроме того, он предоставляет прозрачный способ использования хранилища. При этом требуется создать: ¦ Модель взаимосвязи сущностей для представления хранимых классов ¦ Реляционную схему, представляющую постоянное хранилище ¦ Отображение между двумя предыдущими компонентами С использованием этих определений генератор сервера и генератор объектного слоя C++ в составе Designer/2000 создадут следующие компоненты кли- клиента и сервера: ¦ Обычные классы C++ с общедоступными ме- методами для реализации живучести ¦ Код библиотеки объектного слоя C++ для про- прозрачной обработки процессов клиента-сервера ¦ Определения БД Oracle для сохранения экзем- экземпляров объектов На рис. 17.2 показаны компоненты ODD, исполь- используемые для генерирования программного кода C++. Программисты могут контролировать процесс гене- генерирования кода C++, разбивая исходный код на от- отдельные исходные файлы — по одному файлу на класс. РИСУНОК 17.2. Компоненты Object Database Designer.
Живучесть объектов и шифрование Часть IV Генерирование программного кода C++ Далее описано, как сгенерировать классы C++ с помощью генератора программного кода C++: 1. Запустите C++ Object Layer Generator. 2. Загрузите набор классов. 3. Исправьте ошибки в модели. 4. Выберите цель генерирования. 5. Установите классы, которые должны быть сгенерированы. 6. Убедитесь, что были заданы исходные файлы, в которых будет размещен генерируемый и глобаль- глобальный программный код. 7. При необходимости измените стандартные установки генерирования во вкладке Options. 8. Перейдите во вкладку Generate. 9. Если программный код должен быть создан для файлов объявлений или реализаций классов модели приложения, установите соответствующие флажки. 10. Если должны быть сгенерированы новые разделы кода, установите флажок Generate Classes. 11. Если должны быть обновлены разделы существующего кода, установите флажок Regenerate Classes. 12. Щелкните на кнопке 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 &GetID() const; void SetID(const OLNumber SID); OLString SGetNameO const; void SetName(const OLString &Name); 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/tiger8ORCL") != true) { // Ошибка! Соединение установить не удалось. } Запрос объекта Объект C++ может быть восстановлен из долговременного хранилища пользовательским приложением, выполняющим запрос с предикатом, который определяет требуемый объект. В следующем примере преди- предикат является частью конструкции SQL WHERE, которая отбирает набор ссылок на заказчиков с иденти- идентификатором 25. Корневой объект пользовательское приложение получает с помощью метода Query(), как показано в следующем коде: COLCustomersRefSet Custrs; COLCustomers::Query(OLZONEID::Default, custrs, "ID = 25"); Коллекции с итераторами Коллекциям итераторы необходимы для того, чтобы управлять относящимися к этим коллекциям объек- объектами. В результате разыменования итератора представляется ссылка C++ на генерируемый объект. В следу- следующем фрагменте кода показано, как итераторы могут быть созданы для коллекции заказчиков, которая получается в результате вызова QueryQ над таблицей Customers: for (COLCustomers iter(custrs); iter; iter++) { COLCustomersRef Customers = *iter; OLString name = customers->GetName();
Живучесть объектов и шифрование Часть IV Перемещение по связям Связь между сгенерированными классами С+-' представляется с помощью REF или SET, в зависимо- зависимости от моши соединения. Операторы REF и SET могут использоваться для перемещения по связям между классами. Библиотека времени выполнения C++ должна убедиться, что объект Accounts уже восстановлен из долговременного хранилища; в ином случае REF будет некорректен. Подобным образом набор объектов восстанавливается только тогда, когда SET итерируется и разыименовывается. В следующем фрагменте программного кода показано движение по связям: COLAccountBef Accounts = *iter; COLCustomersRef Customers = Accounts-XSetCustomer(); Изменение хранимых объектов Над хранимыми объектами может быть выполнена модификация трех видов: ¦ Создание ¦ Обновление ¦ Удаление По завершению изменений пользовательское приложение должно пометить объект C++ как модифици- модифицированный. Классы C++ могут быть созданы с методами get или set для каждого атрибута или без них; следовательно, библиотека времени выполнения C++ не может определить, изменен ли объект, если при- приложение не сообщит библиотеке о произведенном им изменении. В следующем фрагменте программного кода показано, как можно изменить адрес и пометить объект как модифицированный: COLAddressItef address = Customers->GetAddress () ; address->Set3tieerl("Wisconsin Avenue."); address-^'arkModified {) ; По завершении текущей транзакции объект записывается в долговременное хранилище. После со^дммр с t " i «тематически помечается как созданный. Удаление хранимою объекта также требует отдельного рассмотрения. Хотя вы можете использовать опе- оператор C++ delete для удаления объекта из памяти, однако с его помощью вы не сможете удалить объект из долговременного хранилища. В следующем (,)'>• гменте кода показано, как Account может быть помечен как удаленный: Rfcf "coounts = *iter; Dui!t.ruy () ; Ниже показано, как новый объект Customer может быть создан с использованием стандартной зоны: COLCustomorsRet 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); По завершению работы с объектом приложение должно снять блокировку. Блокировка может быть снята в результате транзакции. Важно понимать, что метод Refresh() охраняет целостность и постоянство БД. Если объект не заблоки- заблокирован, то любой объект в памяти может выйти из синхронизации с долговременным хранилищем. 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((rows); Здесь создается объект SQL, устанавливается оператор SQL SELECT, связывается переменная и затем выполняется оператор. Интерфейс CURSOR для вложенных таблиц Oracle8 предоставляет новую конструкцию 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(с.accounts)) FROM customers c; EXEC SQL OPEN custaccount_cur; EXEC SQL ALLOCATE :account_cur; EXEC SQL ALLOCATE :account_ref_p; whileA){
Живучесть объектов и шифрование Ш Часть IV EXEC SQL FETCH custaccount_cur INTO :custid, :account_cur; cout « "Customer ID : " « custid « " has the following accounts : \n \n"; whileA){ 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"; } } EXEC SQL CLOSE custaccount_cur; EXEC SQL CLOSE :account_cur; EXEC SQL FREE : account_cur ; EXEC SQL FREE : account_ref_j>; } Функция select_customer_accounts() используется для вывода счетов всех заказчиков с помощью атри- атрибута вложенной таблицы accounts в объекте customers. В листинге 17.1 также используется два цикла: один для открытия курсора custaccount_cur и получения объекта каждого заказчика, а второй — для прохожде- прохождения по всем счетам конкретного заказчика. Конструкция CURSOR (SELECT * FROM TABLE(c.accounts)) определяет курсор результирующего набора по счетам; курсор custaccountcur используется для получения курсора результирующего набора. Доступ к атрибутам объектов Атрибуты объекта не могут изменяться обычными функциями и присваиваниями C++, поскольку те представлены непрозрачными типами OCI8. Pro С C++ предоставляет механизм преобразования OCIString к типу С char и OONumber к типам С 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 ( custno custname addressl address2 city state zip phone1 phone2 primary key number, varchar2{50), varchar2A00) , varchar2A00), varchar2D0), charB), varchar2A0), varchar2B0), varchar2B0), (custno)); create table purchase order ( pono custno orderdate shiptoaddressl shiptoaddress2 shiptocity shiptostate shiptozip primary key number, number references customers(custno) date, varchar2A00), varchar2A00), varchar2D0), charB), varchar2A0), (pono)) ; create table stocks ( s tockno number, stockname varchar2A00) , price number, primary key (stockno)) ; create table line_items ( lineitemno pono stockno quantity primary key number, number references number references number, (pono,lineitemno)); purchase_order(pono) stocks(stockno), custno custname addressl address2 city state Zip phone 1 phone2 1 2 pono John Doe Mike Jordan custno 2 Foothill Dr. 36 College Park Avenue orderdate NULL NULL shiptoaddressl Orlando FL R. Shores CA shiptoaddress2 32817 95054 shipetty 407-555-1296 415-555-7620 shipstate NULL NULL shipzip 101 102 15 Зак. 53 sysdate sysdate 18 Madison St. 5 Church St. NULL NULL NY NY Miami FL 92312 39815
Живучесть объектов и шифрование Часть IV stock.no stockname price 1001 1002 1003 lineitemno 01 02 01 02 Intel Pentium NC-workstation Sun Ultra pono 101 101 102 102 2100.00 220.50 6900,00 stockno 1001 1002 1002 1003 quantity 25 50 20 5 Хотя вы можете писать приложения в среде 3GL типа C++, предыдущая модель для получения желае- желаемого результата требует нескольких сложных соединений. Впрочем, эта система может быть упрощена с помощью объектного подхода, как показано в следующем программном коде: ПРИМЕЧАНИЕ Этот пример показывает, как преобразовать модель взаимоотношений сущностей в объектно-реляционный проект. Действительные объекты создаются с помощью SQL. Хотя приложения C++ могут быть написаны как с реляционным, так и с объектно-реляционным подходом, второй упрощает работу над такими приложениями. create type stocks_t as object ( stockno number, stockname varchar2A00), price number); / create type address_t as object ( addressl varchar2A00) , address2 varchar2 A00) , city varchar2 D0) , state charB), zip varchar2A0)) ; / create type phone_t as varrayA0) of varchar2B0); create or replace type customer_t as object ( custno custname address phone_list po_list number, varchar2E0), address_t, phone_t, po reflist t, member function add_po(poref REF purchase_order_t) return po_reflist_t) create type line_item_t as object ( lineitemno number, stockref ref stocks_t, quantity number); / create type line_item_list_t as table of line_item_t; / create or replace type purchase_order_t as object ( pono number, custref ref customers_t, orderdate date, line_item_list line_item_list_t, shipto_addr address_t); create type po_ref_list as table of REF purchase_order_t;
Объектно-ориентированные базы данных В ЭТОЙ ГЛАВЕ Обзор объектно-ориентированных баз данных Стандарт 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 ¦ Гибкие объявления типов данных. ООБД поддерживают создание пользовательских типов данных и манипуляцию ими. Они способны непосредственно сохранять экземпляры типов (классов) приложе- приложения, определенных в обычном языке программирования. ш Автоматические сети поддержки объектов. ООБД поддерживают хранение, получение и перемещение объектов в сетях. Каждый объект в сети связан с одним или несколькими другими объектами. Отно- Отношения могут иметь типы, один-к-одному, один-ко-многим или многие-ко-многим. Каждый производитель БД предоставляет покупателю такую реализацию СУООБД, которая лучшим образом подходи! для его бизнеса. Такая классификация обеспечивает тесное моделирование областей при- применения приложения в реализации БД; она также позволяет покупателям разрабатывать лучшие приложе- приложения, способные использовать все преимушества СУООБД. Побочный эффект, конечно, состоит в том, что пользователь остается привязанным к конкретному производителю. Перенос приложений из одной реали- реализации СУООБД r другую чрезвычайно сложен, если вообще возможен. Покупатели часто озабочены тем, что им приходится ограничиваться рамками частной реализации особого продукта. Поскольку компании — производители СУООБД относительно малы, и, следовательно, их деятельность в большой степени зави- зависит от изменений рынка, надежда на отдельный продукт — далеко не лучшее вложение капитала. Без на- наличия стандартов невозможно надеяться на признание СУООБД на рынке. it ODIVib Исходя из необходимости в стандарте ODBMS, в 1991 г. была сформирована группа управления объек- объектными Gysa.Mp ачных (Object Database Management Group (ODMG)) для определения и продвижения стан- стандарта хранения объектов. В состав ее членов в настоящее время входят главные производители объектных БД, а также друпк; заинтересованные лица. Стандарт ODMG определил связь БД с тремя наиболее попу- популярными оГн.ек то-ориентированными языками программирования: C++, Java и Smalltalk. Используя связь с языками программирования, разработчики могут включать в приложения возможность манипулирования объектами на родном синтаксисе языка. В следующие р !зде;пх мы реализуем очень простую программу ведения счетов, позволяющую управлять ассортиментом товаров и "оздавать счета. Начнем с обычного приложения C++, не реализовав в нем хра- нимости данных; затем улучшим его, добавив возможность сохранять и получать данные из объектной БД с помощью ОDMCi-стандарта. Приложение на C++ для ведения счетов В листингах 18.1 — 18.6 приведен полный программный код приложения для ведения счетов. В этом при- приложении автор сделал три отклонения от обычной практики кодирования. Во-первых, все члены классов в этом примере объявлены общедоступными, поэтому программный код проще в отслеживании. В приложе- приложениях реального мира переменные-члены обычно объявляются частными, а для доступа к ним реализуется набор функций. Во-вторых, приложение использует генерируемые компилятором копии конструктора и операторов присваивания, поскольку в предоставлении наших собственных нет необходимости. Хорошей практикой также является явное определение этих функций. Последнее отклонение от стандартной практи- практики кодирования состоит в очень незначительном коде обработки ошибок. Пример приложения предполо- предположительно менее устойчив без возможностей исчерпывающей обработки ошибок. Единственная причина таких отклонений от общепринятой практики программирования в этом примере заключается в уменьшении объема программного кода для лучшей демонстрации основной функциональности этого приложения. Приложение разделено на шесть листингов, поэтому описание каждого из компонентов мы можем поместить рядом с программным кодом. Для компиляции приложения в проект должны быть добавлены все файлы. Сначала рассмотрим класс Product, приведенный в листинге 18.1. Листинг 18.1. Приложение ведения счетов: класс Product // Product.h - Объявление класса Product tfifndef Product_H Sdefine Product_H #include <iostream> Sinclude <string> using namespace std;
Живучесть обьектов и шифрование Часть IV class Product { public: // Конструктор и деструктор Product(); Product(const strings newID, const strings newName, const int newPrice) virtual ~Product() ; // Переменные-члены string mlD; string mName; int mPrice; // Перегруженный оператор вставки friend ostrearns operator«(ostrearns os, const Product* const aProduct); }; #endif * *__ _____ // Product.cpp - Реализация класса Product Sinclude "Product.h" // Конструктор и деструктор Product::Product() : mID(""), mName (""), mPrice(O) U Product::Product(const strings newID, const strings newName, const int newPrice) : mlD(newID), mName(newName), mPrice(newPrice) О Product::~Product() {} // Перегруженный оператор вставки ostrearns 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. Счет обычно состоит из заголовка и списка элементов. Каждый элемент определяет продаваемый про- продукт и его количество. Класс Invoiceltem приведен в листинге 18.2. Листинг 18.2. Приложение ведения счетов: класс Invoiceltem //==___=_______===_____=_==_==____=_=_: // Invoiceltem.h - Объявление класса Invoiceltem //_______=__=_==_=__=______=_________==______=_____
Объектно-ориентированные базы данных Глава 18 #ifndef Sdefine InvoiceItem_H InvoiceItemH #include <iostream> using namespace std; Sinclude "Product.h" class Invoice; class InvoiceItem { public: // Конструкторы и деструктор Invoiceltem() ; Invoiceltem(Product* pProduct, const int newQuantity); virtual ~lnvoiceltem(); // Переменные-члены Invoice* mpInvoice; Product* mpProduct; int mQuantity; // Указатель на связанный объект счета // Указатель на объект продукта // Продаваемое количество // Другие функции-члены // Получить количество элементов int GetTotalO const; // Перегруженный оператор вставки friend ostreams operator« (ostreams os, const InvoiceltemS anlnvoiceltem) }; #endif // Invoiceltem.cpp - Реализация класса Invoiceltem #include "Invoiceltem.h" // Конструкторы и деструктор Invoiceltem::Invoiceltem() : mpProduct@), mQuantity@) U Invoiceltem::Invoiceltem(Product* pProduct, const int newQuantity) : mpProduct(pProduct), mQuantity(newQuantity) О // Деструктор // He удалять объекты продуктов Invoiceltem::~lnvoiceltem() {} // Получить количество элементов int Invoiceltem::GetTotal() const < return mpProduct->mPrice * mQuantity; } // Перегруженный оператор вставки ostrearns operator« (ostreams os, const InvoiceItems anlnvoiceltem) < os « anlnvoiceltem.mpProduct « "\t" « anlnvoiceltem.mQuantity « "\t" « anlnvoiceltem.GetTotal(); return os; Класс Invoiceltem имеет три переменных-члена. Первая является указателем на родительский объект Invoice, как мы и обсудили ранее. Вторая является указателем на продукт, а третья — количеством единиц продаваемого продукта. Вместо указателя мы могли бы поместить в класс и сам объект продукта — оба метода корректны. Преимущество хранения указателя состоит в том, что указатель позволяет ссылаться на продукт в списке продуктов и устанавливает связь между элементом счета и продуктом. Если мы решим
Живучесть объектов и шифрование Часть IV изменить имя продукта, элемент счета автоматически получит обновленное имя продукта. Это поможет сохранить целостность данных. Сохранение объекта, напротив, не поддерживает связи между продаваемым продуктом и списком дос- доступных продуктов. Следовательно, любые изменения в продукте не скажутся на элементе счета. ПРИМЕЧАНИЕ Часто предпочтительнее создавать снимок информации о продукте во время создания счета и сохранить этот снимок вместе со счетом. Если позже мы выведем счет, то увидим информацию с продукте, которая была представлена на время его продажи. В этом примере, впрочем, мы более заинтересованы в поддержании связи между элементом сче- счета и продуктом. Стандартный конструктор также определяется явно (хотя и не используется в проекте). Этот конструк- конструктор пригодится при создании списка пустых элементов счетов (стандартный контейнер C++ для выделе- выделения памяти под элементы вызывает их стандартные конструкторы). Деструктор выполняет обычную очистку; нам не нужно удалять объект продукта при удалении счета. Функция-член GetTotal() предназначена для вычисления полной стоимости элемента. Для хранения этого значения мы могли бы создать переменную-член, но в нашем случае это излишне. Кроме того, это могло бы привести к противоречивости данных при изменении стоимости продукта. Элемент счета может быть выведен с помощью перегруженного оператора вставки. Этот оператор выводит информацию о стоимости продукта, количестве продуктов и полной стоимости. Полностью определив класс Invoiceltem, можно переключить внимание на класс Invoice. В листинге 18.3 приведены объявление и реализация класса Invoice. Листинг 18.3. Приложение ведения счетов: класс Invoice // Invoice.h - Объявление класса Invoice //= #ifndef tdefine Invoice_H InvoiceH tinclude <iostream> tinclude <string> #include <list> using namespace std; #include "Invoiceltem.h" typedef list<lnvoiceltem *> typedef InvoiceltemList::const_iterator typedef InvoiceltemList::iterator class Invoice InvoiceltemList; IIL_CItor; IIL_Itor; public: // Конструкторы и деструктор Invoice(); Invoice(const long mID, const strings newDate); virtual -Invoice (); // Переменные-члены long mID; string mDate; InvoiceltemList mlterns; void Addltem(Invoiceltem* pitem); int GetTotal() const; // Идентификатор счета // Дата создания счета // Список указателей на элементы счетов // Добавить элемент в список счетов // Полная стоимость счета // Перегруженный оператор вставки friend ostreamS operator«(ostrearns os, const Invoices anlnvoice); «endif // Invoice.cpp - Реализация класса Invoice
Объектно-ориентированные базы данных Глава 18 #include "Invoice.h" // Конструкторы и деструктор Invoice::Invoice() : mID@), mDateC") Invoice::Invoice(const long newID, const strings newDate) : mID(newID), mDate(newDate) Invoice::-Invoice() { // Удалить все элементы счета for (IIL_Itor i = ml terns .begin {); i ! delete *i; mltems .end () ; ++i) // Добавить элемент в список счетов void Invoice::AddItem(InvoiceItem* pltem) mlterns.push_back(pltem); pltem->mplnvoice = this; // Используем стандартную функцию списка push_back() // Связываем элемент счета с текущим счетом // Получить полное количество счетов int Invoice::GetTotal() const { int intTotal = 0; // Итерация по списку элементов и вычисление полной стоимости for (IIL_CItor i = ml terns, begin () ; i != mltems. end() ; ++i) intTotal += (*i)->GetTotal() ; return intTotal; } // Перегруженный оператор вставки ostreams operator«(ostrearns os, const Invoices anlnvoice) { // Вывести номер счета os « "Invoice: " « anlnvoice.mID « "\n" « "Date : " « anlnvoice.mDate « "\n"; // Вывести все элементы счета for (IIL_CItor ci = anlnvoice.mlterns.begin(); ci != anlnvoice.mlterns.end(); ++ci) os « **ci « "\n"; // Вывести полную стоимость os « "Total: " « anlnvoice. GetTotal () « "\n"; return os; Счет имеет номер, или идентификатор, уникально определяющий счет. Другими переменными-членами являются дата создания счета и список элементов. Имеется два способа привязки элементов к счетам: мы могли бы сохранить сами объекты элементов или указатели на внешние объекты элементов. С точки зрения связи классов большого различия между этими способами не существует, поскольку элементы счета не могут существовать не связанными со счетом. На практике внешнее сохранение элементов упрощает созда- создание запросов. Например, если нам необходим отчет о движении товара, мы можем просмотреть все эле- элементы счета для получения списка элементов счета, связанных с конкретным продуктом. В этом случае в счете мы сохраняем список указателей на элементы, позволяя тем самым объектам элементов находиться за пределами класса Invoice. При создании счета создается пустой список счетов с использованием стандартного конструктора стан- стандартного класса списка. При удалении счета удаляются все его элементы, поскольку элементы не могут существовать без самого счета.
Живучесть объектов и шифрование Часть IV Функция-член Addltem() просто добавляет указатель на элемент счета в список элементов, т.е. элемент связывается со счетом. Кроме того, эта функция устанавливает переменную-член mpInvoice элемента в указатель на счет. Функция GetTotal() просто подсчитывает стоимость всех элементов и возвращает резуль- результат. Перегруженный оператор вставки выводит идентификатор счета, дату создания счета и все его элементы. К этому моменту определены все элементы нашего приложения ведения счетов. Впрочем, наше прило- приложение — это не просто коллекция несвязанных продуктов и счетов. Оно должно организовать их так, что- чтобы объекты были легко доступными. Например, при создании счета мы должны знать, существует ли продукт и где его искать. Для этого необходимо создать список всех доступных продуктов. Желательно, чтобы к продукту можно быть получить доступ по его идентификатору. В листинге 18.4 определен список продуктов. Листинг 18.4. Приложение ведения счетов: карта продуктов // ProductMap.h - Объявление класса ProductMap // Этот класс инкапсулирует список продуктов и связанные // с ним функции. tifndef ProductMap_H ¦define ProductMap_H ¦include <iostream> tinclude <string> tinclude <map> using namespace std; tinclude "Product.h" typedef map<string, Product *> typedef ProductPtrMap::const_iterator typedef ProductPtrMap::iterator class ProductMap { public: // Конструктор и деструктор ProductMap(); virtual -ProductMap(); // Функции-члены void Add(); void Edit(); void Delete (); Product* Find(const strings id); bool IsEmptyO const; ProductPtrMap mProducts; // Перегруженный оператор вставки friend ostrearns operator«(ostreams os , ProductPtrMap; PPM_CItor; PPM I tor; // Добавить продукты в список // Отредактировать продукт // Удалить продукты // Вайти продукт с заданным идентификатором // Проверить, пуста ли карта // Карта указателей на продукты const ProductMapS aProductMap) ; tendif // ProductMap.cpp - Реализация класса ProductMap tinclude <iostream> using namespace std; tinclude "ProductMap.h" // Конструктор ProductMap::ProductMap() // Заполнить список продуктами mProductst"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); // Деструктор ProductMap::-ProductMap() // Удалить все продукты, на которые указывают элементы карты for (PPM__Itor i = mProducts .begin () ; i != mProducts. end (); delete i->second; // Интерактивное добавление продуктов в список void ProductMap::Add() string string int strlD; strName; intPrice cout « "Add Products:\n" ; whileA) { cout « "ID (Type 0 to finish): "; getline(cin, strlD); if (strlD == ") 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; string strlD; string strName; int intPrice; // Использовать стандартную функцию класса карты find() // и изменить продукт, если тот существует cout « "Edit Products:\п"; whileA) { cout « "ID (Type 0 to finish) : "; getlinefcin, strlD); if (strlD == ") 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() { string strlD; // Использовать стандартную функцию класса карты find() // и изменить продукт, если тот существует cout « "Delete Products:\n"; whileA) { cout « "ID (Type 0 to finish): " ; getlxne(cin, strlD); if (strlD == ") 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; > // Проверить, пуста ли карта bool ProductMap::IsEmpty() const { return mProducts.empty(); > // Перегруженный оператор вставки ostreamS operatoг«(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() и DeleteQ могут использоваться для изменения объектов продуктов в карте и для удаления их карты соответственно. Функцию find() можно использовать для полу- получения указателя на продукт с заданным идентификатором. Функция IsEmpty() возвращает булево значе- значение, указывающее, существует ли продукт в карте. В заключение перегруженный оператор вставки выводит все продукты карты.
Объектно-ориентированные базы данных Глава 18 Теперь определим список счетов, чтобы можно было получать доступ к счетам. В листинге 18.5 показан список счетов для нашего приложения. Листинг 18.5. Приложение ведения счетов: класс InvoiceList // 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_iterator IPL_CItor; typedef InvoicePtrList::iterator IPL_Itor; class InvoiceList public: // Деструктор virtual -InvoiceList() ; // Интерактивный ввод счетов. Ссылка на карту продуктов передается, чтобы связать // элементы счета с продуктами. void Add(ProductMaps PMap); // Список счета реализуется со стандартным списком InvoicePtrList mlnvoices; // Перегруженный оператор вставки friend ostream* operator«(ostream* os, const InvoiceListS anlnvoiceList); private: // Интерактивный ввод элементов в счет, ссылка на карту продухтов передается, // чтобы проверить элементы счета. void Addlterns(ProductMapS PMap, Invoice* plnvoice); #endif // InvoiceList.cpp - реализация класса InvoiceList #include <iostream> using namespace std; #include "InvoiceList.h" // Деструктор InvoiceList::~InvoiceList() { // Удалить все счета, на которые указывают элементы списка for (IPL_Itor i = mlnvoices.begin() ; i != mlnvoices.end(); delete *i; } II Интерактивный ввод счетов void InvoiceList::Add(ProductMap& PMap) { if (PMap.IsEmpty ()) { cerr « "No product available, " « "please enter products and try again!" « endl;
Живучесть объектов и шифрование Часть IV return; Invoice* plnvoice; Long lnglD; string strDate; cout « "Add Invoices :\n"; whileA) cout « "ID (Type 0 to finish): "; cin » lnglD; cin.ignore() ; if (lnglD = 0) break; cout « "Date : " ; getline(cin, strDate); plnvoice = new Invoice(lnglD, strDate); mlnvoices.push_baclc(plnvoice) ; Addlterns(PMap, plnvoice); cout « "Invoice added to the list\n\n"; // Интерактивный ввод элементов в счет // Ввод: РМар — список (реализуемый картой) доступных продухто», // plnvoice — указатель на счет void InvoiceList::Addlterns(ProductMapS PMap, Invoice* plnvoice) I Invoiceltem* pltem; Product* pProduct; string strProductID; int intQty; cout « "Add Invoice Items:\n"; while(l) { do ( cout « "Product ID (Type 0 to finish): "; getline(cin, strProductID); if (strProductID = ") return; } while ((pProduct = PMap.Find(strProductID)) =¦ 0) ; cout « "Quantity : " ; cin » intQty; cin. ignore() ; pltem = new Invoiceltem(pProduct, intQty); pInvoice->AddItern(pltem); cout « "Item added\n"; ) } // Перегруженный оператор вставки ostreams operator«(ostreamS os, const InvoiceListS anlnvoiceList) { // Вывести все счета, на которые указывают элементы списка os « "Invoice Listing:\n"; for (IPL_CItor ci = anlnvoiceList.mlnvoices.begin(); ci != anlnvoiceList.mlnvoices.end(); ++ci) os « **ci « "\n"; return os; } Обычно мы вводим новые счета, изменяем существующие и ищем счета по их идентификаторам. Фун- Функции этого класса практически идентичны с соответствующими функциями класса ProdnctMap. Чтобы
Объектно-ориентированные базы данных Глава 18 упростить этот пример, автор реализовал только один конструктор. Для сохранения списка счетов выбран стандартный контейнер списка C++. Функция Add() позволяет вводить счета и добавлять их в список счетов. Она принимает ссылку на ProductMap, чтобы мы могли связать элементы счета с объектами продуктов. Перегруженный оператор вставки выводит все счета в списке. Деструктор заботится об удалении всех объектов счетов. До сих пор мы реализовали практически все возможности нашей довольно маленькой программы. Пос- Последняя задача заключается в создании программы драйвера, которая свяжет их вместе. В листинге 18.6 при- приведена главная функция нашего приложения. Листинг 18.6. Приложение ведения счетов: главная программа // main.cpp - программа драйвера для приложения * /======== #include <iostream> using namespace std; #include "ProductMap.h" #include "InvoiceList.h" int main() { int intChoice = 0; ProductMap Products; // создать пустую карту продуктов InvoiceList Invoices; // создать пустой список счетов do { cout « "\nA Trival Invoice Entry Program\n\n"; cout cout cout cout cout cout cout cout cin cin. cout « « « « « « « « » 1. " 2. 11 3. 4. 5. 6. 9. Enter invoices\n"; Print invoice list\n"; Add products\n"; Edit product\n"; Delete products\n"; Print product list\n"; Exit\n\n"; "Enter your choice: "; intChoice; ignore () ; « endl ; switch(intChoice) < case 1: Invoices.Add(Products); break; case 2: cout « Invoices « endl; break; case 3: Products.Add(); break; case 4: Products.Edit(); break; case 5: Products.Delete(); break; case 6: cout « Products « endl; break ; case 9: 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 1. Enter invoices 2. Print invoice list 3. Add products 4. Edit product 5. Delete products 6. Print product list
Живучесть объектов и шифрование Часть IV 9. Exit Enter your choice: 3 Add Products: ID (Type 0 to finish) Наше Price Product added to the list ID (Type 0 to finish) Name Price Product added to the list ID (Type 0 to finish) Compiler-MSVCS Microsoft Visual C++ 5.0 500 Compiler-BCB Borland C++ Builder 520 0 A Trival Invoice Entry Program 1. Enter invoices 2. Print invoice list 3. Add products 4. Edit product 5. Delete products 6. Print product list 9. Exit Enter your choice: € Product Listing: Compiler-BCB Borland C++ Builder 520 Compiler-MSVC5 Microsoft Visual C++ 5.0 500 OS-Linux Linux 20 0S-Mac0S8 MacOS 8.0 300 OS-Win98 Windows 98 89 OS-WinHT4 Windows NT 4.0 500 A Trival Invoice Entry Program 1. Enter invoices 2. Print invoice list 3. Add products 4. Edit product 5. Delete products 6. Print product list 9. Exit Enter your choice: 4 Edit Products: ID (Type 0 to finish): Compiler-BCB Editing product: Compiler-BCB Borland C++ Builder 520 Hew 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 1. Enter invoices 2. Print invoice list 3. Add products 4. Edit product 5. Delete products 6. Print product list 9. 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 Program 1. Enter invoices 2. Print invoice list 3. Add products 4. Edit product 5. Delete products 6. Print product list 9. Exit Enter your choice: Product Listing: Compiler-MSVC5 OS-Linux OS-MacOS8 OS-Win98 OS-WinNT4 Microsoft Visual C++ 5.0 Linux 20 MacOS 8.0 300 Windows 98 89 Windows NT 4.0 500 500 A Trival Invoice Entry Program 1. Enter invoices 2. Print invoice list 3. Add products 4. Edit product 5. Delete products 6. Print product list 9. Exit Enter your choice: 1 Add Invoices: ID (Type 0 to finish) : 1 Date : 07/24/98 Add Invoice Items: Product ID (Type 0 to finish): OS-Linux Quantity : 1 Item added Product ID (Type 0 to finish): OS-MacOSB Quantity : 10 Item added Product ID (Type 0 to finish): OS-Win98 Quantity : 6 Item added Product ID (Type 0 to finish): 0 Invoice added to the list ID (Type 0 to finish) : 2 Date : 07/25/98 Add Invoice Items: Product ID (Type 0 to finish) Quantity Item added Product ID (Type 0 to finish) Quantity Item added Product ID (Type 0 to finish) Quantity Item added Product ID (Type 0 to finish) Quantity Item added Product ID (Type 0 to finish) Invoice added to the list ID (Type 0 to finish) : 0 A Trival Invoice Entry Program 1. Enter invoices OS-WinNT4 2 OS-Win98 50 Compilar-MSVC5 5 OS-MacOsa 3
Живучесть объектов и шифрование 1 300 89 20 10 6 3000 534 2 50 3 1000 4450 500 900 Часть IV 2. Print invoice list 3. Add products 4. Edit product 5. Delete products 6. Print product list 9. Exit Enter your choice: 2 Invoice Listing: Invoice: 1 Date : 07/24/98 OS-Linux Linux 20 OS-MacOS8 MacOS 8.0 OS-Win98 Windows 98 Total: 3554 Invoice: 2 Date : 07/25/98 OS-WinNT4 Windows NT 4.0 500 OS-Win98 Windows 98 89 Compiler-MSVC5 Microsoft Visual C++ 5.0 500 5 2500 OS-MacOS8 MacOS 8.0 300 Total: 8850 A Trival Invoice Entry Program 1. Enter invoices 2. Print invoice list 3. Add products 4. Edit product 5. Delete products 6. Print product list 9. Exit Enter your choice: 9 Bye! Сначала в нашу карту продуктов мы добавили два компилятора. Затем изменили продукты и удалили их. После просмотра нашей карты продуктов мы создали два счета и вывели список счетов. Наше маленькое приложение делает все, что нам нужно, за исключением одного: все продукты и счета при выходе из программы разрушаются. Такое приложение не нашло бы широкого применения. Необходи- Необходимо добавить возможность сохранения данных в этом приложении постоянными. Живучесть данных Чтобы сделать объект живучим, его необходимо унаследовать от класса d_Object: class MyClass : public d_Object О ODMG предоставляет подмножество элементарных типов C++ (табл. 18.1). Таблица 18.1. Элементарные типы ODMG Тип ODMG Размер Описание d_Char 8 битов ASCII-символ dBoolean Зависит от реализации d_True или dFalse d_Short 16 битов Краткое целое со знаком d_UShort 16 битов Краткое целое без знака d_Long 32 бита Длинное целое со знаком d_ULong 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 // Product.h - Объявление хранимого класса Product «ifndef «define Product_H Product H «include <iostream.h> «include <odmg.h> // header-файл OOMG «include <d_String.h> // header-файл ODMG для типа d_String class Product : public d_Object public: // Конструкторы и деструктор Product () ; Product(const d_StringS newID, const d_String& newName, const d_Long newPrice); virtual -Product (); // Переменные-члены d_String mID; d_String mName; d OShort mPrice; // идентификатор продукта // название продукта // стоимость продукта }; «endif // Перегруженный оператор вставки friend ostreamS operator«(ostreams os, const Product* const BaProduct) Во-первых, мы больше не используем namespace std, поскольку ODMG пока не поддерживает концеп- концепций пространства имен C++. Теперь класс Product унаследован от класса ODMG d_Object. Для использо- использования специфических возможностей ODMG необходимо включить поставляемый производителем файл odmg.li. ПРИМЕЧАНИЕ Имена 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++. Затем компоновщик связывает все объек- объектные файлы и библиотеки баз данных, предоставляемые производителем, и создает исполняемый файл приложения. C++ header-файлы для хранимых классов Исходные файлы C++, поддерживающие манипуляции хранимыми данными Расширенные header-файлы C++ для хранимых классов header-файлы C++ для нехранимых классов Исходные файлы C++ РИСУНОК 18.1. Процедура создания приложения. Библиотеки БД, предоставляемые производителем Измененная реализация класса Product приведена в листинге 18. Листинг 18.8. Реализация хранимого класса Product // ProductSrc. срр - Реализация хранимого класса Product / /==================
Объектно-ориентированные базы данных Глава 18 #include <odmg.h> // header-файл ODMG #include "Product.hxx" // конструктор и деструктор Product::Product() : mID(""), mNameC"), mPrice(O) О Product::Product(const d_StringS newID, const d_StringS newHame, const d_Long newPrice) : mlD(newID), mName(newName), mPrice(newPrice) О Product::-Product() U // перегруженный оператор вставки 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.cpp в ProductSrc.cpp, поскольку обработчик схемы, возможно, сгенерирует Product.срр, в котором будут реали- реализованы различные функции поддержки класса 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 и Invoiceltem, рассмотрим три понятия: коллекции, итераторы и отношения. Коллекции Коллекция — это набор объектов одного типа. Стандартная библиотека 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); -d_Collection(); unsigned long cardinality() const; dJBoolean is_empty() const; dBoolean is_ordered()const; dBoolean allow_duplicates() const; d_Boolean containselement (const T& v) const; d_Boolean insert_element (const t& v) const; void remove_element(const T& v); void remove_all (const T& v); void remove_all(); d_lterator<T> create_iterator() const; d_lterator<T> begin() const; d_lterator<T> end() const; friend d_Booleanoperator== (const d_Collection<T>& d, const d„Collection <T>& c2); friend d Boolean operator!= (const d_Collection<T>& d, const d_Collection <T>& c2); Операции над коллекциями Создает коллекцию без элементов Создает коллекцию и копирует все элементы из коллекции с, используя копирующий конструктор Т. Удаляет все существующие элементы с помощью деструктора Т и копирует все элементы коллекции с с помощью копирующего конструктора Т. Функция assign_from() обычно используется для копирования элементов коллекции другого типа, скажем, копирования элементов из списка в множество. Удаляет все существующие элементы с помощью деструктора Т. Операции над элементами Возвращает количество элементов в коллекции Определяет, пуста ли коллекция Определяет, упорядочена ли коллекция Определяет, могут ли элементы иметь одинаковые значения Определяет, могут или нет один или более элементов принимать значение v. Вставляет элемент. Возвращает Dtrue, если вставка прошла успешно, и d_False — в противном случае. Используйте эту операцию для вставки элемента в множество, которое уже содержит значение v. Удаляет первый или все элементы со значением v. Удаляет все элементы. Итерация Возвращает итератор, указывающий на первый элемент. Функция begin() совместима со стандартными константными двусторонними итераторами. Возвращает итератор, указывающий на последний элемент. Сравнение с1 и с2 равны, если имеют одинаковое количество элементов и каждый элемент с1 равен соответствующему элементу с2. Элементы с1 и с2 могут быть различными типами коллекций. Возвращает !(с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> Неупорядоченная коллекция пар key, value. Доступ к значениям осуществляется посредством ключа (подобна стандартному контейнеру C++ multimap).
Объектно-ориентированные базы данных Глава 18 Кроме всего прочего, ODMG поддерживает подмножество стандартных контейнеров C++, приведен- приведенных в табл. 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> mltems; Итераторы Стандарт ODMG предоставляет класс d_Iterator, который может использоваться для итераций по кол- коллекциям. Как говорилось ранее в этой главе, класс dJUerator реализует стандартный двунаправленный константный итератор 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> rProduct; Получить доступ к члену 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; >; // B.h class A; extern const char _mRelVarA [] ; class В : public d_Object { d_Rel_Ref<A, _mRelVarA> mB; >; // C.cpp const char _mRelVarA [] = "mA"; const char _mRelVarA [] = "mB"; Вы можете также иметь двунаправленное отношение один-ко-многим. Например, элемент счета может иметь только один связанный счет, но счет может иметь много элементов. В ODMG определены два класса двунаправленных отношений один-ко-многим: d_RelSet и d_RelList. Первый из них представляет неупоря- неупорядоченное отношение, тогда как второй — упорядоченное. Классы двунаправленных отношений приведены в табл. 18.4.
Таблица Класс 18.4. Классы двунаправленных отношений Базовый класс ODMG Ооъектно-ориентированные оазы данных ШСВШ Глава 18 Цв] d_Rel_Ref<T, const char* Member> d_Ref<T> d_Rel_Set<T, const char* Member> d_Set<d_Ref<T» d_Rel_List<T, const char* Member> d_List<d_Ref<T» Например, отношение между классами Invoice и Invoiceltem в приложении ведения счетов может быть определено, как показано в листинге 18.10. Листинг 18.10. Отношение между классами Invoice и Invoiceltem extern const char _mlnvoice[] , _ml terns []; class Invoice : public d_Object { d_Rel_Set<InvoiceItem, _mlnvoice> mltems; }; class Invoiceltem : public d_Object { d_Rel_Ref<Invoice, _mltems> mpInvoice; }; II В некотором (исходном) файле const char _mlnvoice[] = "mplnvoice" ; const char _mlterns [] = "mltems"; Теперь рассмотрим изменения, которые будут внесены в классы Invoice и Invoiceltem. Два header-файла соединены и показаны в одном листинге, чтобы проще можно было описать отношения между ними. В листинге 18.11 приведены объявления хранимых классов Invoice и Invoiceltem. Листинг 18.11. Объявления хранимых классов Invoice и Invoiceltem // Invoice.h - Объявление класса Invoice // ===== =========== #ifndef Invoice_H «define Invoice_H #include <iostream.h> // 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 Invoiceltem; extern const char _mlnvoice[], _mltems[]; // Определены в исходных файлах 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_ULong mID; // Идентификатор счета d Date mOate; // Дата создания счета
Живучесть объектов и шифрование Часть IV InvoiceltemList mlterns; // Список ссылок счетов void AddItem(d_Ref<InvoiceItem> pltem); // Добавить элемент в список счетов d_ULong GetTotal() const; // Полная стоимость счета // Перегруженный оператор вставки friend ostreams operator«(ostreams os, const Invoices anlnvoice); }; class Invoiceltem : public d_Object { public: // Конструкторы и деструктор Invoiceltem() ; Invoiceltem(d_Ref<Product> pProduct, const d_UShort newQuantity); virtual ~lnvoiceltem(); // Переменные-члены // Ссылка на связанный объект счета d_Rel_Ref<Invoice, _mltems> mplnvoice; d_Ref<Product> mpProduct; // Ссылка на объект продукта d_UShort mQuantity; // Продаваемое количество // Другие функции-члены // Получить количество элементов d_UShort GetTotal() const; // Перегруженный оператор вставки friend ostreams operator«(ostreams os, const InvoiceltemS anlnvoiceltem); >; #endif Сначала рассмотрим класс Invoiceltem. Переменная количества продаваемых продуктов mQuantity объяв- объявлена как dJUShort и, следовательно, может сохраняться в БД. Переменная mpProduct первоначально была определена как указатель на объект Product — Product*. В C++ указатель хранит адрес объекта в памяти, это хорошо до тех пор, пока объект остается в памяти. После закрытии приложения и при его повторном запуске адрес в памяти будет другим. После сохранения элемента счета в БД адрес в памяти станет неуме- неуместен. Хранение такого указателя в БД бесполезно. Таким образом, переменная mpProduct определена следу- следующим образом: d_Ref<Product> mpProduct; Подобным образом отношение один-ко-многим между классами Invoice и Invoiceltem представлено чле- членом d_Rel_Ref<Invoice, _mlnvoice> mplnvoice; в классе Invoiveltem и членом d_RelSet<Invoiceltem, _mlnvoice> mltems; в классе Invoice. Переменная-член mID в классе Invoice теперь определена как d_ULong и, таким образом, может быть сохранена в БД. Переменная-член mDate предназначена для сохранения даты создания счета. В ODMG определен класс d_Date, представляющий дату как "год, месяц, день". Он имеет статическую функцию- член current(), возвращающую объект d_Date с текущей системной датой. Стандартный конструктор клас- класса d_Date инициализирует экземпляр текущей системной датой. Это идеально для класса Invoice, поскольку тот сохраняет именно дату создания счета. ПРИМЕЧАНИЕ ODMG также предлагает класс d_Time, представляющий время суток. Он содержит часы, минуты и секунды. Стандар- Стандартный конструктор устанавливает экземпляр в текущее время. Еще один класс, d_TimeStamp, представляет вместе дату и время. Последний класс, djnterval, предназначен для хранения промежутков времени. Должна быть изменена реализация классов Invoice и Invoiceltem. В листинге 18.12 приведен измененный файл реализации класса Invoiceltem. Листинг 18.12. Реализация хранимого класса Invoiceltem // InvoiceltemSrc.cpp - Реализация класса Invoiceltem
Объектно-ориентированные базы данных Глава 18 #include <odmg.h> #include "Product.hxx" #include "Invoice.hxx" // Переменная-член представляет связь с Invoice const char _mlnvoice[] = "mplnvoice"; // Конструкторы и деструктор Invoiceltem::InvoiceltemO : mpProduct@), mQuantity@) О Invoiceltem::Invoiceltem (d__Ref<Product> pProduct, const d_UShort newQuantity) : mpProduct(pProduct), mQuantity(newQuantity) О // Деструктор // He удаляйте объект продукта Invoiceltem::-Invoiceltem() U // Получить количество элементов d_UShort Invoiceltem::GetTotal() const { return mpProduct->mPrice * mQuantity; } // Перегруженный оператор вставки ostreams operator« (ostreams os, const InvoiceltemS 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 () ; Функция Open() объявлена следующим образом: 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: dJTransaction tx; // создать транзакцию tx.begin(); // запустить транзакцию DeleteObjects() ; // операции над объектами базы данных AddObject() ; tx.Commit; // совершить транзакцию Операции в транзакции должны заключаться между операторами begin() и end(). Функция commit() выполняет операции DeleteObjects() и AddObjects(). Если одна из них потерпит сбой, другая не должна выполняться; если одна из них была выполнена, то должна быть откачена. В определенных ситуациях при- приложение также может вызвать команду abort(): d_Transaction tx; // создать транзакцию try { tx.begin; // запустить транзакцию DeleteObjects() ; // операции над объектами базы данных AddObjects() ; tx.Commit(); // совершить транзакцию } 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> // header-файл ODMG #include "Product.hxx" #include "Invoice.hxx" // Переменная-член, представляющая связь с Invoiceltem const char _mltems[] = "mltems"; // Конструкторы и деструктор Invoice::Invoice() : mID@) О Invoice::Invoice(const d_OLong newID) : mID(newID) {} Invoice::"Invoice() { // Удалить все элементы счета for (IIL_CItor i = mltems. begin () ; i != mltems .end () delete (*i) .ptr() ; > // Добавить элемент в список счетов void Invoice::AddItem(d_Ref<InvoiceItem> pltem) { d Transaction tx;
Объектно-ориентированные базы данных Глава 18 try < tx. begin () ; mltems.insert_element(pltem); tx. commit () ; } catch (d_Error derr) { tx.abort(); cerr « "ODMG Error [" « derr .get_]cind() « "] " « derr.what() B« endl; } > // Получить полную стоимость счета d DLong Invoice::GetTotal() const Г int intTotal = 0; // Выполним итерации по списку элементов и вычислим полную стоимость for (IIL_CItor i = mltems .begin () ; i != mltems .end(); ++i) intTotal += (*i)->GetTotal () ; re turn in tTo tal; } // Перегруженный оператор вставки 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 приведен класс операций над продуктами.
Живучесть объектов и шифрование Часть IV Листинг 18.14. Класс операций над продуктами // FroductOps.h - Объявление класса операций над продуктами // Этот класс инкапсулирует операции над объектами // продуктов, находящимися в базе данных //==== #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 () Listf); // Переменные-члены d_Database* pDB; // Добавить продукты в базу данных // Изменить продукты в базе данных // Удалить продукты из базы данных const; // Проверить, есть ли продукты в 6ase данных // Перечислить все продукты в базе данных // Указатель на базу данных #endif //= // ProductOps.срр - файл реализации класса операций над продуктами #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_Transaction Product* char char unsigned short tx; pProduct; strlD [MAXLEN + 1] ; strName [MAXLEN + 1] ushortPrice; cout « "Add Products: \n" ; whileA) { cout « "ID (Type 0 to finish) : cin.getline(strID, MAXLEH); if (strID[O] == '0') break;
Объектно-ориентированные базы данных ^i_f .1 Глава 18 cout « "Name : "; cin.getline(strName, MAXLEN); cout « "Price : "; cin » ushortPrice; cin.ignore(); try { tx.begin(); pProduct = new(pDB) Product(strID, strHame, 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_lcind() « "] " « derr.what() « endl; return; // Изменить продукты void ProductOps: :Edit () { d_Transaction tx; d_Ref<Product> pProduct; char strlD[MAXLEN + 1] ; char StrNewID[MAXLEN + 1] ; char strName [MAXLEN + 1] ; unsigned short ushortPrice; // Используем функцию d_Database lookup_object() для поиска продукта // и изменения его в случае нахождения cout « "Edit Products:\n"; whileA) { do { cout « "ID (Type 0 to finish): "; cin.getline(strID, MAXLEN); if (strID[0] == '0') break; tx.begin(); pProduct = pDB->loolcup_object(strID) ; 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. getjcind () « "] " « derr.what() « endl; // Удалить продукта void ProductOps::Delete() < d_Transaction tx; d_Ref<Product> pProduct; char strID[MAXLEN + 1] ; // Используем функцию d_Database lookup_object() для помеха продукта // и изменения его в случае нахождения cout « "Delete Products:\п"; whileA) { cout « "ID (Type 0 to finish): "; cin.getline(strlD, MAXLEN); if <strID[0] = '0') break; tx.begin(); pProduct = pDB->loolcup _object(strID) ; 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. getjeind () « "] " « derr.what() « endl; return; else cerr « "Invalid product ID, please try again." « endl; // Проверяем, есть ли в базе данных объекты продуктов int ProductOps::IsEmpty() 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->mID != \"\""); cout « "Product Listing:\n"; // Метод 1 for (d_Iterator< d_Ref<Product> > i2 = ProductBag.begin (); i2 != ProductBag.endO; cout « (*i2).ptr{) « "\n"; // Метод 2 PrintPtr<d_Ref<Product> > DoPrintPtr; std: : for_each (ProductBag.begin() ,ProductBag.endO , 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() показаны две полезные функции класса dDatabase Первой является функция 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 — экземпляром пространства Т. Простран- Пространство автоматически обновляется БД, поэтому в него добавляются и удаляются элементы при добавлении либо удалении их из БД. Процесс синхронизации может заметно сказаться на производительности прило- приложений. Поэтому вы должны избегать долговременного хранения экземпляра пространства. 16 Зак. 53
Живучесть объектов и шифрование Часть 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. Разнообразные функции // 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 () (Ref S r) { cout « *r « "\n"; } }; // Выводит объект, на который ссыпается параметр d_Ref<T> // Используется перегруженный оператор вставки «(ostreamS, T*) template<class Ref> class PrintPtr : public std::unary_function<Ref, void> { public: void operator () (Ref S r) { cout « r.ptr() « "\n"; } }; // Произвольная длина строки const unsigned MAXLEN = 30 ; Функциональный объект PrintPtr принимает параметр d_Ref<T> и выводит его значение с помощью T::operator(ostream&, T*). В листинге 18.16 он используется для вывода всех счетов. В этом файле также определена стандартная константа MAXLEN, используемая в листингах 18.14 и 18.16 для принятия ввода пользователя. Первоначальный класс InvoiceList был переименован в InvoiceOps и изменен таким образом, чтобы получить все преимущества живучести объектов. В листинге 18.16 приведен класс InvoiceOps. Листинг 18.16. Класс операций над счетами // InvoiceOps.h - Объявление класса InvoiceOps // Этот класс инкапсулирует операции над объектами Invoice // в базе данных //= #ifndef InvoiceOps_H
Объектно-ориентированные базы данных Глава 18 tdefine InvoiceOps_H tinclude <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(); // Создать счета // Удалить счета // Вывести список счетов dDatabase* pDB; private: // Интерактивное добавление продуктов в список, ссылка на продукт передается // для проверки на наличие продукта в элементах счета. void Addltems(ProductOpsS POps, Invoice* plnvoice); }; tendif // 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) U // Деструктор InvoiceOps::-InvoiceOps() U // Ввести счета void InvoiceOps::Add(ProductOpsS POps) { if (POps.IsEmptyO) { cerr « "No product available, " « "please enter products and try again!" « endu- endure turn; d_Transaction Invoice* long char tx; plnvoice; lnglD; strID[MAXLEN + 1] cout « "Add Invoices:\n"; whileA) { cout « "Invoice ID (Type cin » lnglD ; cin.ignore(); 0 to finish):
Живучесть объектов и шифрование ЧастЫУ if (lnglD = 0) break; _ltoa(lngID, strlD, 10); try { tx.begin() ; plnvoice = new(pDB) Invoice(lnglD); pDB->set_object_name(plnvoice, strlD); tx.commit() ; cout « "Invoice added to the list\n\n"; Addlterns(POps, plnvoice); } catch (d_Error derr) { tx.abortO ; if (derr.get_kind{) == d_Error_NameNotUnique) { cerr « "Invoice already exists!" « endl; continue; 1 else { cerr « "ODMG Error [" « derr .get_kind() « "] " « derr.what() « endu- endure turn; // Удалить счета из базы данных void InvoiceOps::Delete() { d_Transaction tx; d_Ref<Invoice> plnvoice; long lnglD; char strID[MAXLEN + 1] ; // Используем функцию d_Database lookup_object() для поиска счета // и удаления его в случае нахождения cout « "Delete Invoices:\n"; whileA) { cout « " ID (Type 0 to finish) : " ; cin » lnglD ; if (lnglD == 0) break; _ltoa(lngID, strlD, 10); tx.begin () ; plnvoice = pDB->lookup_object(strID); tx.commit() ; if (plnvoice.is_null() == d_False) { try { tx.begin () ; plnvoice.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::Addltems(ProductOpsS POps, Invoice* plnvoice) { d_Transaction tx; Invoiceltem* pltem; d_Ref<Product> pProduct; char strProductID[MAXLEN + 1] ; int intQty; cout « "Add Invoice Items:\n"; while A) { do { cout « "Product ID (Type 0 to finish): " ; cin.getline(StrProductID, MAXLEN); if (strProductID[0] == '0') 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) Invoiceltem(pProduct, intQty); pltem->mplnvoice = plnvoice; 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++, практически все произ- производители предоставляют функцию преобразования типа _ltoa(). (Вы всегда можете написать собственную версию функции, если в вашем компиляторе нет таковой.) Функция Addltem() демонстрирует одну важную возможность баз данных ODMG. Вспомните, между счетом и его элементом существует двунаправленное отношение. Это отношение автоматически поддержи- поддерживается БД. Нам всего лишь необходимо задать объекты, между которыми необходимо установить отноше- отношения. Объекту pltem->mplavoice можно присвоить ссылку pinvoice на объект Invoice: pltem->mplnvoice = pinvoice; В этом операторе мы сообщаем БД, что элемент относится к счету, на который указывает pinvoice. БД автоматически добавит этот элемент в множество plnvoice->mltems. Мы можем использовать функцию d_Set<T>::insert_element_iast() для добавления pltem в множество plnvoice->mltems, как показано в следующем примере: plnvoice-^ml terns . insert_element_last (pltem) ; В этом операторе БД автоматически присваивает pinvoice переменной-члену pltem->mplnvoice. Это га- гарантирует, что отношение между элементами и счетами нарушаться не будет. Функция Deiete() практически идентична ProductOps::DeIete(), за исключением того, что идентифика- идентификатор счета мы должны из длинного целого преобразовать в строку символов. Функция List() также исполь- использует стандартный алгоритм поиска C++ foreach и функцию PrintRef, определенную в листинге 18.15, для вывода всех счетов, находящихся в БД. Теперь недостает только одного — функции main(). Она приведена в листинге 18.17. Лиаинг 18.17. Главная программа / / ————-—~—————._—_—_—~_~___——__—___—___—— —_.__.____— _„. _..__ // main.cpp - Программа для приложении (tinclude <iost.ream.h> #include <odmg.h> tinclude "ProductOps.h" #include "InvoiceOps.h" const char dbNaffle[] = "InvApp"; int main() { d_Database* db = new d_Database; try { db->open(dbName); } catch (d Error derr) { cerr « "ODMG Error [" « derr.get_kind() « "] " « derr.what() « endl; return 1; } int intChoice = 0; ProductOps Products(db); InvoiceOps Invoices(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: case 2: case 3: case 4: case 5: case 6: case 7: case 9: default: Invoices.Add(Products); Invoices .Delete () ; Invoices.List() ; Products.Add() ; Products.Edit() ; Products . Delete () ; Products.List () ; cout « "Bye!\n\n"; cout « "Please select } while (intChoice != 9) ; db->close() ; delete db; return 0; break.- break; breaks- break; break; break; breaks- break; 1, 2, 7 or 9.\n" Основное отличие от первоначальной версии главной программы заключается в том, что перед выпол- выполнением операций, должна быть открыта БД. По завершению всех операций мы закрываем БД и удаляем объект БД. Мы рассмотрели, как с использованием стандарта 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, несколько объектов Invoiceltem и несколько объектов 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, у гражданского населения тоже появилась необходимость в шифрова- шифровании. В 1960 г. группа по исследованию криптографии была учреждена председателем 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. 5g789AB?DEFgHiJKj,M Использование шифра сдвига на пять позиций. SIUVWXY2012345fiZfi2 NQPQRSIUVWXYZfii234 Сообщение теперь кодируется следующим образом: MOVIES => HJQD9N DINNER => 8DII9M HOME => CJH9 Самый большой недостаток шифров в том, что они без труда могут быть взломаны опытным криптоа- нализатором. Реализация подстановочного шифра приведена в листинге 19.1. Листинг 19.1. Реализация подстановочного шифра //Автор: Megh Thakkar // Простая реализация подстановочного шифра // Назначение: Этот шифр запрашивает у пользователя // количество позиций для сдвига и затем сдвигает символы // на заданное число позиций. tinclude <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' <= с SS с <= '1' ) с += sf_places; else if ('A1 <= с && с <= 'L' )
Живучесть объектов и шифрование Часть IV с += sf_places; else if I'm' <= с && с <= 'z' ) с -= sf_places; else if ('M1 <= с SS с <= 'Z' ) с -= 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 roain(int argcchar *argv[]) { char с; 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 = KOLL 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) { с = (char)z; if (isalpha(c)) { с = 12n(c) ; if (! decrypt) { с = (с + 12n(*key)) % ALPHABET_LEN; } else { с = (с + ALPHABET_LEN - 12n(*key)) % ALPHABET_LEN; } с = n21(c) ; } putc(с,outfile); } return @) ;
Защита приложений с помощью шифрования Глава 19 Шифр Vernam 1. Give 2. Take 3. No 4. Car 5. Yes 1. Car 2. Take 3. No 4. Yes 5. Give Шифры Vemam также называются одноразовыми прокладками. Используя их, вы получите гибкость шифров и безопасность кодов. В своей основе эта технология использует набор кодовых таблиц, каждая из которых представляет отдельную часть сообщения. По существу, она использует слово в одной таблице для создания части сообщения, а затем смешивает слова в таблице для создания новой таблицы, которая используется для записи другой части сообщения, слова второй таблицы затем смешиваются опять для за- записи третьей части сообщения. Этот процесс продолжается до тех пор, пока сообщение не зашифруется полностью. Единственный способ взлома зашифрованного таким образом текста заключается в исполь- использовании точно такого же смешивания и точно такого же количества слов. На рис. 19.2 показана одноразовая прокладка. 1. Yes 2. No 3. Take 4. Give 5. Car 1. No 2. Take 3. Give 4. Yes 5. Car РИСУНОК 19.2. Пример одноразовой прокладки. Чтобы определить, как смешивать символы, можно воспользоваться генератором случайных чисел; пе- перемешиваться должен каждый символ. Одноразовое использование кодовой таблицы прежде чем та будет перемешана, дает ей необходимую безопасность. Каждая таблица может представлять листок из одноразо- одноразовой прокладки. Другими словами, только одноразовая прокладка, используемая для шифрования сообще- сообщения, может быть использована для преобразования зашифрованного текста в обычный. Каждая страница прокладки содержит различный набор кодов. Часть сообщения можно создать с помо- помощью одной страницы, другую часть сообщения — с помощью второй страницы и т.д. Иными словами, части сообщения конструируются с помощью различных кодовых таблиц; для расшифровки этого сообще- сообщения вы должны использовать кодовые таблицы точно в такой же последовательности, что и при шифрова- шифровании сообщения. В листинге 19.3 приведена реализация одноразовой прокладки. Листинг 19.3. Реализация одноразовой прокладки //Автор: Megh Thakkar //Реализация одноразовой прокладки //Формат запуска: входной файл и выходной файлы //Описание: Программа принимает в параметрах три файла. // Над первыми двумя файлами она выполняет исключающее ИЛИ // и помещает результат в третий файл. // #include <stdio.h> #include <stdlib.h> tinclude <iostream.h> // Смещение в файле ключей. #define BHF_SZ 32768О long offset = 0; FILE *infile = 0; FILE *keyfile = 0 ; FILE *outfile = 0; size_t amt_read = BHF_SZ; size_t amt_write = BOF_SZ; size_t amt_key; //amt_read, amt_write и amt_key используются во время шифрования для определения //количества используемых символов. // Выделить дисковые буферы. // Ы используется для входного и выходного файлов //Ь2 используется для ключевого файла char * iobuf; char * kbuf; int i; //Используется для цикла, int main(int argc, char * argv[]) if (argc != 5)
Живучесть объектов и шифрование 13 Часть IV cout «"\nOsage: onetime input_file " « "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; // Открыть файлы infile = fopen(argv[l] , "rb") ,- keyfile = fopen(argv[2], "rb"); outfile= fopen(argv[3] , "v»b") ; 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 roeroory\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] Л= 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-раз- 128-разрядный ключ и кажется очень сильной (хотя истинная природа обеспечиваемой им безопасности неизвестна). ¦ Skipjack. Этот секретный алгоритм был разработан NSA для гражданских целей. Он использует 80- разрядный ключ.Он является сердцем Chip Clipper, используемого правоохранительными организа- организациями для выполнения легальных прослушиваний.Chip Clipper не настолько безопасен, как Skipjack. Шифрованию по частному ключу свойственны несколько недостатков: ¦ Как отмечалось ранее, самой большой проблемой с шифрованием по частному ключу является ме- метод распространения ключа. На рис. 19.3 показано, что для секретной связи между тремя людьми необходимо три ключа. На рис. 19.4 показано, что для подобной связи между четырьмя людьми уже необходимо вдвое больше ключей. Если в этот секретный узел добавить еще двух человек, потребу- потребуется 15 ключей. В общем, для секретного узла, включающего п человек, вам необходимо п(п-1)/2 ключей. Для управления ситуациями реального мира, например финансовыми учреждениями, может
Живучесть объектов и шифрование Часть IV понадобиться очень большое число ключей. Такое большое число ключей необходимо в связи с тем, что каждая пара людей, вовлеченных в шифрование по частному ключу, совместно использует этот ключ. Второй проблемой в шифровании по частному ключу является безопасность, касающаяся распрос- распространения самого ключа. Ключ i Ключ2 РИСУНОК 19.3. Частное шифрование для трех человек. РИСУНОК 19.4. Частное шифрование для четырех человек. Механизмы шифрования по секретному ключу Наиболее удачные методы шифрования по секретному ключу для преобразования обычного текста в зашифрованный предполагают использование простого набора процедур и функций. Очень широко для этой цели используется понятие поблочное шифрование; вместо одного байта или символа при шифровании оно использует блок, или группу байтов. Каждый блок может обрабатываться любой комбинацией нескольких процессов. Окончательный зашиф- зашифрованный текст может быть сгенерирован с применением следующих процессов во время нескольких ите- итераций или раундов шифрования: ¦ Методы подстановки. Очень широко используются в алгоритмах шифрования. Как мы видели ранее, знание языка и контекста сообщения дает хакеру массу информации о способе расшифровки кода. Поэтому вместо выполнения подстановки битов или символов используется поблочное шифрование. Безопасность достигается отображением один-к-одному блоков символов обычного текста и блока- блоками зашифрованного текста того же размера, но связь между ними определить не так-то просто. Методы подстановки обычно предполагают использование некоторой простой стратегии, например, табли- таблицы поиска или функции XOR. ¦ Перестановка. Вы можете переупорядочить символы обычного текстового сообщения для преобразо- преобразования его в анаграмму, которая выглядит как сообщение со случайными символами. Например, боль- большинство сообщений состоят из 7-разрядных ASCII-символов. Смешивая биты для создания случайного набора битов, вы можете получить желаемую шифровку. Методы перестановки обычно используются совместно с другими способами, такими как подстановка. ¦ Функции шифрования. Исключающее ИЛИ — пример функции шифрования (будет описана в следу- следующем разделе). Широко используются и другие функции, такие как двоичное сложение, умножение и модульные арифметические функции. Использование исключающего ИЛИ (XOR) для выполнения поблочного шифрования Популярным методом поблочного шифрования является функция XOR. Функция исключающее ИЛИ используется для указания, что если существует два условия (например, условия А и В), то истинно либо условие А, либо условие В, но не оба. Ниже приведены полный набор всех возможных сочетаний опери- оперируемых значений и результат операций над ними: XOR@,0) = О XOR@,1) = 1 XOR A,0) = 1 XORA,1) = 0
Защита приложений с помощью шифрования Глава 19 Преимущество функции XOR в том, что она может обращать себя и, следовательно, может использо- использоваться для шифрования. Предположим, что мы взяли значения А = 10101000 и В = 00111001. Следовательно, С = XOR(A,B) = 10010001. Теперь если мы выполним операцию над В и С, то получим А: XOR(B,C) = XOR@0111001, 10010001) = 10101000 = А В листинге 19.4 показано, как можно реализовать функцию XOR: Листинг 19.4. Реализация функции XOR //Автор: Megh Thakkar // Простая реализация функции XOR // Формат запуска: входной файл и выходной файлы // Предназначение: эта программа принимает текстовый файл, выполняет // операцию XOR между каждым символом и предоставленным ключом // и помещает зашифрованный результат в "зашифрованный" файл. tinclude <stdio.h> tinclude <iostream.h> int main(int argc, char *argv[]) { FILE *plain, *cipher; char *cp; int c; if ( (cp = argv[lj) &? *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 = argvfl] ; с л= *ср++; putc(c, cipher); } fclose(cipher); fclose(plain) ; ) return 0; } Использование блоков подстановки Популярный метод реализации функции подстановки состоит в использовании конструкции, называе- называемой блоком подстановки или S-блоком. Функция S-блока принимает на вводе некоторый бит или набор битов и предоставляет на выводе другой бит или набор битов. Для выполнения преобразования в методе исполь- используется таблица замещения. ПРИМЕЧАНИЕ Такие таблицы замещения могут на один и тот же вывод отображать" более одного ввода. В результате хакер не мо- '-г.% жет получить вывод S-блока и определить, сколько вводов выполнялось для генерирования вывода. > Использование расширенной перестановки Расширенная перестановка принимает блок данных и расширяет его в набор перекрывающихся групп; каждая группа может быть мало похожа на оригинальный блок. Предположим, что у нас есть 24-разрядный
Живучесть объектов и шифрование Часть IV блок; чтобы преобразовать его в 36-разрядный блок мы можем выполнить расширенную перестановку сле- следующим образом: 1. Разбить 24 бита на шесть групп по 4 бита в каждой. 2. В каждую группу добавить по биту спереди и сзади. Теперь мы имеем шесть групп по 6 битов каждая, что в сумме составляет 36 битов. Технологии, приведенные в этом и предыдущих разделах, являются просто широко используемыми методами в алгоритмах шифрования. Для получения окончательного алгоритма можно смешивать и соче- сочетать их. Впрочем, секретность алгоритма не связана с использованием особых методов или, по крайней мере, определенного числа этих методов. О безопасности алгоритмов рассказывается в разделе "Строгие алгоритмы" далее п этой главе. Использование циклов шифрования Алгоритмы шифрования становятся более сложными и одновременно более безопасными при исполь- использовании в них различных методов шифрования по очереди. Впрочем, важно на различных циклах исполь- использовать различные методы шифрования. Например, если в первом цикле шифрования вы воспользовались подстановкой (или итерацией), а во втором цикле опять воспользовались подстановкой (даже если симво- символы подстановки на двух циклах различаются), результирующий текст будет не безопаснее зашифрованного в одном цикле. Фактически, даже если вы используете тысячи циклов подстановки, безопасность будет иметь такой же уровень, что и при одном цикле, поскольку между обычным текстом и окончательным шифром в обоих случаях будет отображение типа один-к-одному. Более безопасное шифрование может быть достигнуго благодаря использованию одного цикла подстановки, за которым будет следовать второй цикл перестановки. В популярных алгоритмах используется от 8 до 16 циклов различных методов шифрования. Использование центров распространения ключей Оригинальное сообщение Общий ключ получателя Зашифрованное сообщение Одной из широко используемых технологий в шифровании по частному ключу являются центры рас- распространения ключей (Key Distribution Center — KDC). Всякий раз, когда пользователь А хочет связаться с пользователем В, он вызывает KDC, который генерирует случайный одноразовый ключ сеанса. Сгенери- Сгенерированный ключ шифруется и рассылается пользователям, которые хотят связаться друг с другом. Проблема этого метода в том, что ключ, используемый для шифрования ключей сеансов находится в файле в KDC. Поэтому любой имеющий доступ в KDC может получить ключи шифрования. Недавно секретная служба разоблачила агента NSA, который продавал криптографические ключи в другие страны. Из предыдущей дискуссии ясно, что частные ключи трудно использовать для передачи информации между гражданским населением. Решением этой проблемы являются общедоступные ключи шифрования. Криптография по общему ключу Криптография по общему ключу называется также асим- асимметричной криптографией и является результатом математи- математического открытия, сделанного в 1970 г. В отличие от методов симметричного ключа, использующих один ключ для шиф- шифрования и расшифровки, в асимметричных методах исполь- используются два ключа: секретный и общий (общедоступный). Общий ключ используется для шифрования сообщения, а сек- секретный — для его расшифровки. Получатель имеет секретный ключ, который должен быть защищен. Для генерирования двух ключей, связанных математически, используется математи- математический процесс. Обратитесь к рис. 19.5, чтобы понять, как в криптографии по общему ключу могут использоваться раз- различные ключи. Цель криптографии по общему ключу заключается в уст- устранении самой большой проблемы с распространением по частному ключу. С годами в области криптографии по обще- общему ключу было определено несколько методов, которые опи- описаны в следующих разделах. Программа шифрования Оригинальное сообщение Секретный ключ получателя Программа расшифровки РИСУНОК 19.5. Понятие криптографии по общему ключу и использование общих и частных ключей.
Защита приложений с помощью шифрования Глава 19 Метод головоломки Ральфа Меркле Ральф Меркле (Ralph Merkle) опубликовал свою работу в Communications of the ACM — передовом жур- журнале по теории вычислительной техники, где он определил, что его работа — это безопасная связь по небезопасным каналам. В основу своего подхода он положил головоломки. Чтобы понять этот метод, пред- предположим, что Джон и Джейн хотят связаться друг с другом по каналу, о котором известно, что тот небе- небезопасен. Сначала Джон создает большое количество ключей шифрования, например, миллион. Ключи Джон помещает в эти головоломки — по одному на головоломку. Затем эти ключи Джон помещает в головолом- головоломки. Решение каждой головоломки занимает около одной минуты. Джон посылает головоломки Джейн, ко- которая выбирает одну из головоломок и связанный с нею ключ. С помощью этого ключа Джейн шифрует сообщение и посылает его Джону. Ключ, выбранный Джейн, Джон определяет по своему списку ключей. Последующие сеансы связи Джона и Джейн будут проходить по этому ключу. Подслушивающий будет знать, что туда и назад ходили головоломки, но определение нужного ключа займет чрезвычайно долгое время. Говоря проще, Джон создает очень много ключей и "заворачивает", или прячет их в некоторую "обер- "обертку" — по одному ключу на обертку — и посылает их Джейн. Джейн случайным образом выбирает одну из оберток и, следовательно, один ключ, шифрует с его помощью сообщение и посылает его Джону. Джон может определить, какой из ключей был выбран, поскольку у него есть весь список ключей. Затем этот ключ станет ключом для последующих переговоров. Многопользовательские криптографические методы Диффи-Хельмана В 1975 г. Уайтфилд Диффи (Whitfield Diffie) и Мартин Хельман (Martin Helman) опубликовали научную работу "Многопользовательские криптографические технологии". В их криптографических методах исполь- использовались концепции, широко применяемые сейчас в криптографии по общему ключу. Основная идея этой стратегии заключалась в том, чтобы сообщение можно было зашифровать с помощью этого ключа, а рас- расшифровать — другим ключом. Диффи и Хельман внесли несколько предложений о том, как можно достичь такого эффекта, среди которых были следующие: ¦ Умножение простых чисел, что делается легко; разложение же соответствующего результата на про- простые множители намного сложнее. ¦ Использование дискретного возведения в степень; дискретное логарифмирование соответствующего результата на простые множители намного сложнее. Для проведения последующих исследований Диффи и Хельман выбрали второй подход. Подход обмена экспоненциальным ключом Диффи-Хельмана был опубликован в их научной работе "Новые направления в криптографии" в IEEE Transactions on Information Theory. Подход Диффи-Хельмана основан на предложении Джона Гилла (John Gill): взять экспоненты чисел и вычислить результат по модулю некоторого простого числа. Этот метод работает следующим образом: 1. Оба участника должны оговорить два числа, р и q. Числа р и q могут быть общеизвестны. 2. Теперь каждый участник должен выбрать число, провести математическую операцию, включающую р, q и выбранное число, и передать результат другому участнику. Предположим, первый участник выбрал Ml, а второй — М2. Результатами их отдельных математи- математических операций будут числа N1 и N2. 3. С помощью второй математической формулы оба участника могут выбрать еще одно число, К, явля- являющееся результатом функции от 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: 1. Выберите случайным образом два очень больших простых числа, например, М и N. 2. Получите Z (модуль шифрования), умножив М на N. Другими словами, Z = M*N. 3. Выберите Е, относительно простое число для (M-1)*(N-1). 4. Объявите Z и Е RSA-ключами, которые могут использоваться для шифрования информации. Предположим, что вы выбрали M = 47hN = 71. Следовательно, Z = 47*71 = 3337. Теперь вы должны выбрать Е, для которого вычисляется (M-1)*(N-I) = 46*70 = 3220. Предположим, что вы выбрали Е = 79. Теперь ключ шифрования вычисляется с использованием расширенного алгоритма Эвклида и простых чисел следующим образом: D = 79 - 1 (mod 3220) = 1019 ПРИМЕЧАНИЕ Этот алгоритм находится в исходном коде PGP; здесь он не описан, поскольку выходит за рамки этой книги. Теперь пользователь, который хочет зашифровать и послать нам некоторую информацию, для шифро- шифрования может использовать Z и Е. Предположим, что кто-либо хочет послать нам число 688. Для этого он делает следующее вычисление: 688" mod 3337 = 1570 Мы получим число 1570 и расшифруем его следующим образом: 15701019 mod 3337 = 688 Безопасность RSA зависит от следующих факторов: ¦ Числа М и N должны храниться в секрете. Это очевидно: если Z и Е общеизвестны, а вы знаете еще и М и N, не очень сложно определить ключ ашфрования D. ¦ Z должно чрезвычайно сложно раскладываться на множители. Если Z будет раскладываться легко, то вы очень просто сможете извлечь из него М и N, и затем определить ключ шифрования. ¦ Не должно быть других математических методов изъятия ключа с использованием Z и Е. Использование Pretty Good Privacy Pretty Good Privacy (PGP) — очень популярный метод шифрования, использующий криптографию как по частному ключу, так и по общему ключу. Последняя версия, PGP 5.x, для создания безопасной среды передачи сообщений выпочняст следующие операции:
Защита приложений с помощью шифрования Глава 19 1. Создает случайный ключ сеанса для каждого сообщения. 2. Шифрует сообщение по ключу сеанса с помощью алгоритма шифрования по частному ключу IDEA. 3. Шифрует ключ сеанса по общему ключу получателя с помощью алгоритма общедоступного ключа RSA. 4. Отправляет по почте зашифрованные сообщение и ключ сеанса. В табл. 19.2 представлено сравнение систем общедоступного и частного ключей. Таблица 19.2. Сравнение методов шифрования по общему и частному ключам Системы общедоступного ключа Системы частного ключа Могут сделать ключ шифрования общим Не могут сделать ключ шифрования общим Только получатель владеет секретным ключом Секретный ключ должен быть как у получателя, так и у отправителя Асимметричное шифрование Симметричное шифрование Может использоваться для электронной подписи Не может использоваться для электронной подписи Перед переговорами нет необходимости в Перед переговорами необходимо обменяться ключом обмене ключом Выбор простых чисел в PGP Для генерирования простого числа, состоящего из п битов, PGP использует следующий алгоритм: 1. Сначала PGP генерирует случайное двоичное число, содержащее две единицы, за которыми следуют (п-2) случайных двоичных разрядов. 2. Для определения, является ли сгенерированное число простым, PGP использует очень эффективный алгоритм (называемый алгоритмом быстрого отсеивания). 3. Если алгоритм определит, что сгенерированное случайное число не является простым, PGP пробует следующее нечетное число. 4. Если алгоритм определит, что число может быть простым, PGP выводит точку (.) и использует ма- малую теорему Ферма для точного определения, является ли это число простым. Как минимум 50% всех чисел отсеивается в каждом проходе алгоритма Ферма. При каждом успешном тесте PGP выво- выводит звездочку (*). Эта информация может использоваться для понимания вывода PGP, когда вы пытаетесь найти пару ключей. Предположим, что вы пытаетесь сгенерировать 512-разрядное простое число и получаете следую- следующий вывод PGP: Этот вывод указывает, что алгоритм PGP нашел 20 кандидатов в простые числа, которые были просе- просеяны через решето алгоритма Ферма. Четыре звездочки указывают, что первое число, прошедшее первый тест Ферма, прошло все четыре теста. Следующий набор точек (.) указывает, что, прежде чем было най- найдено второе простое число, алгоритм Ферма отсеял 12 различных чисел. В следующем фрагменте программного кода показана простая реализация алгоритма отсеивания, кото- который может использоваться для проверки того, является ли определенное число г простым: int primes[100]; // Массив, используемый для хранения // сгенерированных простых чисел, int total_primes =0; // 'total_primes' используется для хранения количества // сгенерированных на данный момент простых чисел check_for_prime(int r) < 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; } return 1;
Живучесть объектов и шифрование Часть IV Использование случайных чисел в криптографии В целях повышения безопасности в криптографических алгоритмах широко используются случайные числа. Случайные числа в алгоритме PGP используются для определения: ¦ Секретного ключа ¦ Ключа сеанса, используемого для шифрования каждого сообщения ¦ Ключа, используемого в оговоренных криптографических алгоритмах Случайные числа могут генерироваться с помощью случайного природного процесса, такого как ра- радиоактивный распад. Компьютеры оснащены функциями случайных чисел, генерирующих числа, случай- случайные в том плане, что их нелегко предсказать. Хакер может знать внутреннее состояние компьютера и начальное число и взломать такие псевдослучайные функции. PGP генерирует случайное число, попросив пользователя ввести что-либо с клавиатуры. Он определяет время между нажатиями клавиш и создает слу- случайное число. Как только генерирование случайного числа завершено, PGP просит пользователя прекра- прекратить ввод. Это случайное число может использоваться в качестве начального числа для генератора случайных чисел. Для каждого нового ключа генерируется новое начальное число. Генерирование начального числа с учетом скорости ввода с клавиатуры не совсем случайно; фактически в программе PGP 2.6 было выявлено упущение в генерировании начального числа. Это упущение препятствовало генерированию действительно случайного числа, и число становилось предсказуемым опытному хакеру. Это упущение было исправлено в программе PGP 2.6.I. Шифрование файлов с помощью PGP Простейшее использование программы PGP заключается в шифровании файлов и защиты информации на компьютере от хакеров. Имеется несколько причин для шифрования файлов на вашем компьютере: ¦ Если вы совместно с другими используете компьютер и компьютер содержит частную информацию, которую вы хотите сохранить в тайне. ¦ Если на своем компьютере вы храните конфиденциальную информацию, которую не хотите открыть общественности, чтобы она не была украдена или потеряна. ¦ Если вы не хотите сделать информацию доступной, чтобы та не была продана или подарена, прежде чем вы уничтожите все данные. Шифрование файла производится с помощью опции PGP -с. Например, чтобы зашифровать файл secret, необходимо ввести команду: с:> рдр -с secret Программа PGP отвечает на эту команду, запрашивая фразу пароля. Затем она шифрует файл и создает файл secret.pgp. Теперь безопасность файла, защищенного PGP, зависит от фразы пароля и ее охраны. Для обеспечения секретности фразы можно принять следующие меры: ¦ Не записывать фразу. ¦ Не хранить фразу пароля в файле на компьютере. ¦ Не выбирать фразу пароля, легко угадываемую другими. ¦ Выбрать для пароля фразу, которую вы можете легко запомнить. Следующие виды информации легко угадываются; вы не должны выбирать для пароля фразы, состоя- состоящие исключительно из таких элементов, как: ¦ Ваше имя ¦ Имя вашего супруга ¦ Имя родителей ¦ Ваше ласкательное имя ¦ Имя вашего ребенка ¦ Ваш любимый герой мультфильма ¦ Название используемой вами операционной системы ¦ Номер вашего телефона
Защита приложений с помощью шифрования Глава 19 ¦ Номер вашей кредитной карточкой ¦ Номер чьей-либо социальной страховки ¦ Чей-либо день рождения ¦ Ваш любимый фильм ¦ Ваше любимое место отдыха ¦ Любой из предыдущих элементов в обратном направлении ¦ Любой из предыдущих элементов, за которым следует или которому предшествует цифра После того как файл зашифрован, следующим шагом должно быть удаление из системы исходного незашифрованного файла. Стандартные команды удаления во многих операционных системах на самом деле не уничтожают файл, а просто удаляют из каталога запись о файле. В результате файл можно восстановить. Опция -w используется в PGP для безопасного удаления. В процессе удаления, выполняемом в PGP, файл заполняется случайными данными. Использование случайных данных вместо нулей или единиц усложняет получение текста хакером. Впрочем, существует несколько случаев, когда простого удаления файла недо- недостаточно. Если вы, например, используете накопитель для одноразовой записи, то в результате удаления старые секторы не будут уничтожены. Убедитесь также, что все резервные копии исходного текста (вклю- (включая программные кэши, автоматические и пользовательские резервные копии) тоже уничтожены. Файл, зашифрованный PGP, может быть расшифрован при запуске PGP с именем файла в качестве единственного параметра: с: > рдр secre t. рдр Программа PGP читает файл, определяет шифрование и запрашивает ввод пароля. Когда пароль предо- предоставлен, PGP расшифровывает файл и записывает расшифрованную информацию в отдельный файл. По фразе пароля PGP с помощью хеш-функции генерирует 128-разрядный код. Для шифрования PGP использует алгоритм IDEA со 128-разрядным ключом. В табл. 19.3 перечислены стандартные расширения файлов и дана их интерпретация. Таблица 19.3. Расширения файлов, используемые PGP Расширение Использование файла .txt Текстовый файл, созданный с помощью текстового редактора или текстового процессора .рдр Зашифрованный файл; он является двоичным файлом 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 и Когп установка переменных производится следующим образом: $ 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 (PKP). Взыскание за нарушение патента находится под юрисдикцией гражданского суда. Много программ, таких как 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, о и t), и букв, используемых редко (таких, как x,q и z), могут помочь хакеру. ¦ Родство букв. Некоторые буквы часто идут вслед за какой-либо другой (например, сочетания /о и ing), тогда как другие очень редко идут за другими (сочетания tp и iy). Это тоже может помочь в отгадывании букв. ¦ Длина слов. В английском языке однобуквенными словами обычно являются / или А. Существует так- также ограниченное количество двухбуквенных слов. ¦ Контекст сообщения. Может помочь атакующему узнать контекст сообщения по информации об от- отправителе, получателе, а также общее содержание сообщения и его вид. Довольно широко используются следующие атаки: ¦ Атака для поиска ключа шифрования, предпринимаемая после того, как известен прямой текст сооб- сообщения. Этот тип атаки довольно обычен при взломе зашифрованной электронной почты или защи- защищенного жесткого диска. Электронная почта имеет стандартный заголовок, который хакер может использовать в качестве основы для расшифровки. Жесткие диски используются для сохранения информации в определенных позициях. Другими словами, хакер может использовать обычные зна- знания для определения остатка зашифрованной информации. ¦ Атака для поиска ключа шифрования по предопределенному прямому тексту. При таком подходе хакер может заставить жертву бессознательно зашифровать определенную информацию, а затем для полу- получения ключа поработать над зашифрованным текстом, созданным из известных данных. Хакер смо- сможет использовать ключ для расшифровки всех сообщений, созданных с помощью этого ключа шифрования. ¦ Дифференциальный криптоанализ. Эта атака дает возможность сравнить результаты шифрования не- нескольких сообщений, очень похожих между собой, за исключением некоторых второстепенных деталей. Взлом файла, зашифрованного программой PGP Для взлома файла, зашифрованного PGP, хакеры могут применить несколько стратегий. Некоторые из них основаны на факте использования в программе PGP алгоритма IDEA: ¦ Атаки грубой силы против ключа IDEA. Алгоритм IDEA, применяемый в PGP, использует 128-раз- 128-разрядные ключи, что приводит к возможности существования нескольких миллиардов ключей B128 клю- ключей). Хакер может попробовать все ключи поочередно (такой подход займет очень много времени на угадывание ключа). Очевидно, этот подход не идеален для взлома кода. ¦ Атаки грубой силы против фразы пароля. Вместо того чтобы определять ключ шифрования, хакер мо- может атаковать фразу пароля, выбранную для шифрования файла. Выбранная длина фразы пароля очень важна в оценке ее безопасности. Автор рекомендует использовать как минимум 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 и определяет способ представления зашифрованных данных. Для генерирования цифровой подписи используются следующие операции: 1. К подписываемым данным применяется криптографическая хеш-функция. 2. Для шифрования результатов криптографической хеш-функции используется алгоритм шифрования по общему ключу RSA. 3. В цифровую подпись помещаются результат хеш-функции и тип криптографического смешивания, используемого в формате PKCS. Digital Signature Standard (DSS) В 1991 г. организация N1S при сотрудничестве с 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 p. Заметьте, что v не может равняться 1. ¦ Значение п, вычисляемое с помощью частного ключа К, такое, что gk = 1 mod p и К < 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.netscape.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.,
.:':,,¦, учл'л;. обпехгжт и шифрование 4<acib iV и бесплатная версия Eudora Light доступна на Web-узле по адресу: www.eudora.com/eudoraUght. Клиенты Eudora доступны длч Windows 3.x, Windows 95, Windows NT, Macintosh OS и Macintosh Newton. В 1997 г. Qualcomm начала поставку Eudora с полнофункциональной включаемой версией PGP Personal Privacy 5.0, позволяю- позволяющей отправлять v получать почту, зашифрованную с помощью программы PGP. Продукты для защиты рабочего стола Главный фокус безопасности рабочего стола заключается в защите информации, хранимой на рабочем столе. Эти продукты позволяют сохранять информацию так, чтобы она была доступна только человеку, владеющему ею, а также другим людям, получившим разрешение владельца. Вот некоторые наиболее об- |;:ие возможности продуктов для защиты рабочего стола: ш Шифрование и расшифровка файлов. Выбранные файлы могут шифроваться или расшифровываться на честг, позволяя только зарегистрированным пользователям получать доступ к файлам после пре- .'i<4:iaB;h ши! пароля для расшифровки фразы. Программный продукт может требовать верификации пол метателя во время загрузки системы или при каждой попытке доступа к файлу. н Шифрование и расшифровка каталогов. Некоторые программы позволяют задавать определенные ка- каталоги зашифрованными, так что любой файл, созданный или перемещенный в этот каталог, авто- мтически шифруется и может быть доступен только зарегистрированному пользователю. ш Проверки пароля во время загрузки. Некоторые программы позволяют защищать данные, требуя вво- ввода пароля прежде, чем успешно завершится загрузка. ш Блокировка системы. Некоторые программы блокируют систему в целях защиты от несанкциониро- несанкционированного использования. Такая системная блокировка реализуется блокированием клавиатуры или экранной заставкой, защищенной паролем. В! Безопасное удаление. Файлы и каталоги, удаляемые в среде DOS и Windows, на самом деле не унич- уничтожаются, просто удаляются их записи в каталогах. Различные коммерческие программы позволяют воссоздавать удаленную информацию. Это может быть хорошо, когда вы действительно хотите вос- восстановить потерянные данные, но может стать проблемой, когда вы хотите уничтожить конфиден- конфиденциальную информацию. Некоторые продукты позволяют удалять данные таким образом, чтобы не голько удалялись записи в каталогах, но и биты каждого байта устанавливались в случайный набор нулем и единиц. ш Желательна поддержка электронной подписи и криптографии по общему ключу. я Доступ к данным лиц-хозяев. Некоторые программы сохраняют данные таким образом, что некото- некоторое особое чицо по паролю и идентификатору может расшифровать данные, зашифрованные любым пользоп-пелем программы в отдельной системе или в нескольких системах. RSA SecurPC ?2.0 Компания RSA Data Security, Inc. обладает некоторыми наиболее важными реализациями криптографи- криптографических алгоритмов, используемыми в текущее время. Она разработала продукт шифрования персонального рабочего стола SecurPC, использующий криптографию по общем ключу. Хотя этот продукт и не использу- использует цифровые подписи, он имеет следующие важные возможности: я Шифрования файлов и каталогов по секретному ключу ¦ Шифрования файла и сохранения его в виде исполняемого модуля, который может быть открыт только при вводе пароля после запуска программы ¦ Автоматического шифрования каталогов ш Идентификации пользователя при загрузке системы ¦ Безопасного удаления ¦ Доступа к данным ¦ ИнТ'"гт>;1ции с Windows 95 ч !.!щ>пы системы по "горячим" клавишам Pretxy Good Privacy (PGP) for Personal Privacy 5.0 Программа PGP предоставляет несколько важных возможностей в версии Personal Privacy 5.0: н Шифрования и/или цифровой подписи данных, сообщений электронной почты и файлов
Защита приложений с помощью шифрования Глава 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 правоохранительные агентства 17 Зак. 53
Живучесть объектов и шифрование Часть IV могли получить ключи Clipper, которые могли использоваться для чтения зашифрованных данных. LEAF защищен 16-разрядной контрольной суммой, и Блейз понял, что, если вы можете повредить ключ, прави- правительственные агентства не смогут расшифровать зашифрованные данные. Резюме Криптография и шифрование долгое время были сферой правительственных организаций и большого бизнеса. Эра использования компьютеров, Internet и криптографии по общему ключу дала людям возмож- возможность защищать свои данные с помощью различных методов шифрования. В этой главе были рассмотрены стратегии, используемые алгоритмами шифрования, а также наиболее широко используемые методы, с помощью которых опытный хакер может взломать код и расшифровать сообщение. Некоторые коммерчес- коммерческие продукты (такие, как PGP for Personal Privacy и Norton Your Eyes Only) могут использоваться для шифрования сообщений. Различные виды безопасности предоставлены в Internet-броузерах, таких как Netscape Navigator/Communicator и Microsoft Internet Explorer. Здесь были проанализированы широко используемые способы, среди которых подстановка, перестановка, исключающее ИЛИ и другие криптографические функции. Не имеет значения, какой из способов вы ис- используете, — запомните, что отъявленный хакер всегда сможет расшифровать сообщение. Вы должны при- принять необходимые меры предосторожности, чтобы защитить данные. Эти меры разнообразны — от правильного выбора пароля до физической защиты компьютера.
Распределенные вычисления ЧАСТЬ CORBA СОМ Java и C++ та г
CORBA В ЭТОЙ ГЛАВЕ Теория и обоснование IDL: соглашение связывания Брокер объектных запросов Сравнение сред CORBA Создание C++ Client Создание C++ Server Java Client Стратегии тестирования Служба имен и способность к взаимодействию Производительность
CORBA Глава 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 рассматриваются в разделе "Служба имен и способность к взаимо- взаимодействию" далее в этой главе. Среды CORBA значительно отличаются в зависимости от того, каким образом внедряется ORB конкретным постав- поставщиком. Как клиент, так и сервер могут иметь доступ к сво- своим собственным интерфейсам ORB. И уже от поставщика зависит, представляют ли эти ин- интерфейсы несколько ORB или же это библиотеки, компи- компилируемые в каждый исполняемый модуль. Важно лишь то, что интерфейс ORB доступен для среды. На рис. 20.1 пока- показано взаимодействие клиента, сервера и ORB. Для взаимодействия в CORBA клиент и сервер соединя- соединяются друг с другом с помощью среды CORBA. Конкретное наполнение среды существенно зависит от поставщика, но следующие три операции являются необходимыми: 1. Объект сервера самостоятельно регистрируется в сре- среде CORBA. 2. Клиент запрашивает среду CORBA о связи объекта сервера с клиентским объектом. 3. Клиент начинает выполнять вызовы метода для клиентского объекта; эти вызовы направляются объекту сервера. Приложили клиента —-— © _bind(TNbrkfk*O\ —© * method () <.. . ¦ Приложение сервере S«Lis ready () jS impTjsjeady () ORB РИСУНОК 20.1. Связь между клиентом и сервером CORBA, а также ORB. Каркас объектной технологии Полное понимание ООП и проектирования чрезвычайно важно для разработчика высокой квалифика- квалификации. Поскольку OMG сформулировала определение каркаса, то все разработчики CORBA используют в своей работе одни и те же программные коды и стандарты. Многие организации используют CORBA в своих промышленных разработках. Дело в том, что каркас интерфейсов и служб определен спецификацией CORBA. Большим удобством для разработчика также оказывается наличие полной распределенной вычислительной архитектуры. Разработчик имеет возможность многократно использовать заранее определенные интерфейсы для создания новых компонентов. Технология CORBA позволяет разработчикам создавать объектную технологию в виде более сложного пакета, чем это было возможно ранее. В CORBA определены объектные интерфейсы, которые отсутствуют в сетях и операционных системах. Поэтому устраняются неуклюжие "перемычки" для объектов, которые обычно необходимы для интегрирования объектов с процедурными интерфейсами. Стремление применять объектную модель наиболее распространено при работе с языками, операцион- операционными системами и БД. Объекты были заново приспособлены к языкам программирования Visual Basic, Lisp,
CORBA Глава 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 с ПОР. ПОР определяет восемь различных типов сообщения: ¦ Request (Запрос) ¦ Reply (Ответ) ¦ Cancel Request (Запрос отмены задания) ¦ Locate Request (Разместить запрос) ¦ Locate Reply (Разместить ответ) ¦ Close Connection (Закрыть соединение) ¦ Message Error (Ошибка сообщения) ¦ Fragment (Фрагмент) Большинство этих сообщений имеет заголовок и тело. Часть типов сообщений содержит дополнитель- дополнительные данные, присоединяемые к их телу. Среди данных в заголовке сообщения содержится тип сообщения и размер тела сообщения. В заголовке указано, кодируются ли целые числа для Intel или Motorola. Кодирование позволяет процессорным архи- архитектурам подключать байты, которые составляют целые числа. Если два компьютера используют одни и те же целочисленные типы, то целые числа транслироваться не будут. Только клиенту позволено пересылать и отменять задания, а посылать ответы и исключения может только сервер. Сообщение о запросе — это метод работы с объектом. Тело сообщения содержит входные парамет- параметры метода в качестве дополнения. Ответ содержит возвращаемое значение и любые параметры, содержа- содержащие выходные данные, присоединенные к телу сообщения. Припасам! ежена Клиентская заглушка Возвращаемые значения Параметры inout Параметры out Объектный адаптер ORB ЮР Канаты - TCP/IP РИСУНОК 20.2. Связь CORBA с ПОР.
Распределенные вычисления Часть V Механизм ПОР совершенно прост и фактически универсален. Каждый индивидуальный тип закодиро- закодирован специфическим способом. Если данные перемещаются между клиентом и заглушкой одним и тем же поставщиком, то нет необходимости использовать протокол ПОР. Если клиент к сервер постоянно нахо- находятся на одном и том же компьютере, многие поставщики оптимизируют связь, пересылая данные, ис- использующие межпроцессорную связь. Компонентная модель Одни разработчики могут использовать CORBA для формирования компонентов ПО без привязки к определенному языку, платформе или сети. Другие разработчики могут использовать CORBA для интегри- интегрирования их приложений с теми те же самыми компонентами ПО, при этом IDL они используют в каче- качестве компонента. Снабжая объект дополнительными интерфейсами, вы создаете компоненты, которыми можно управ- управлять и манипулировать. Например, если ваш объект осуществляет транзакцию интерфейса ресурса, про- просмотр транзакции, то системы, устойчивые к сбоям, а также другие службы могут взаимодействовать с вашим объектом. Объект, который осуществляет транзакцию интерфейса ресурса, позволяет выполнять тран- транзакции с вашим объектом с большой степенью надежности. Поэтому на этот объект можно положиться при работе со сложными системами транзакции. IDL соглашение связывания В среде CORBA IDL представляет соглашение между клиентом и сервером. IDL представляет собой со- согласование функциональных возможностей, которые должен поддерживать компонент. В качестве эквива- эквивалента процедуры объявления класса в языке C++ IDL является первичный механизмом интеграции CORBA. Творческий проект, который больше всего напоминает CORBA IDL, — это Facade. В книге Design Patterns указано, что Facade призван поддерживать унифицированный интерфейс пользователя по отношению к набору интерфейсов в подсистеме. Facade определяет интерфейс более высокого уровня, который значи- значительно облегчает применение подсистемы. (Гамма: 185). Типичным примером реализации Facade как интерфейса объекта является Singleton (Гамма: 127). Интерфейсы, определенные в 1DL, не имеют подобного ограничения. При определении интерфейса необходимо обеспечить гибкость, предусмотреть принцип многократного использования и замены компонентов ПО. Ниже перечислены другие преимущества IDL: ¦ Независимый от языка механизм для определения методов и параметров. ¦ Обработка исключительных ситуаций способом, исключающим противоречия в процессе примене- применения различных языков с IDL. Листинг 20.1 представляет собой практический пример использования IDL в приложении workflow. Мы будем рассматривать этот пример до конца этой главы. Листинг 20,1. 1DL для сиаемы Workflow: Workflow.lDL /* Workflow.lDL */ module Workflow { typedef string Taskld; typedef string Personld; enura TaskStatusCode // TSC { TSC_NOT_STARTED, TSC_IN_PROGRESS, TSC_ON_HOLD, TSC_FINSHED }; struct Task { Taskld id; TaskStatusCode status; string description;
CORBA Глава 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 ); void 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 устанавливает отображение между типами IDL и типами в различных языках. Разработчики, которые работают на языках программирования C++ и Java, имеют дело с непосредствен- непосредственными отображениями между языками. Типы данных и определения IDL тесно связаны с проблемой языка. IDL должен читать header-файл C++. IDL содержит директивы #include и #pragma. IDL определяет такие типы: целые и десятичные числа, булевы и строковые. Есть только один тип для хранения: последовательность. Многие из простых типов C++ имеют эквиваленты CORBA. Например, typedefs, cnums и structs эквивалентны конструкциям C++ с аналогичными именами. Ключевое слово class заменено ключевым словом interface. Параметры метода имеют дополнительные спецификаторы, неизвестные разработчикам C++: in, out и inout. Эти спецификаторы указывают необходимое направление данных: в метод или из него. Специфика- Спецификатор in подобен const. He предполагается, что значение параметра обновляется методом. Для большинства интерфейсов typedefs, structs, enums и interfaces могут быть объединены при выпол- выполнении многих типичных проектов. Однако если необходимы дополнительные возможности, то IDL обеспе- обеспечивает наличие атрибутов и исключений. Хотя объектные данные реализации не являются непосредственно доступными для клиента, отображе- отображение языка предполагает, что компилятор IDL генерирует методы доступа для установки (_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 (POA — Переносимый объектный адаптер) — это часть спецификации CORBA 2.2, которая гарантирует переносимость со стороны сервера. При наличии РОА можно выдвигать специфические требования к способу обработки объектов сервера. Конкретно объект сервера определяет опции для реализации и постоянно их поддерживает. Время жизни объектов Объекты в CORBA являются постоянными: требуется, чтобы объект существовал в течение всего вре- времени. В C++ большинство объектов временные: создаются во время выполнения и уничтожаются по окон- окончании приложения. Один из методов по обработке объектов состоит в создании рабочего объекта сервера для того, чтобы создать тип необходимого объекта. Создание рабочего интерфейса полезно при определенных обстоятель- обстоятельствах, но выполнение этой процедуры для каждого интерфейса чрезвычайно утомительно. Поставщики ORB могут использовать псевдообъект для того, чтобы имитировать постоянство объекта сервера. Псевдообъект упрощает просмотр клиентом распределенной среды, позволяя ей проявляться так, как если бы это был одиночный объект, в то время как фактически может иметь место множество объектов. Для реализации этого эффекта одиночный псевдообъект регистрируется на сервере имен или же ис- используется механизм расположения ORB. Если клиенту необходимо внедрить объект, то он пытается свя- связаться с ORB, содержащим объект. Затем ORB создает объект и перенаправляет клиент новому объекту. Для клиента все выглядит так, как если бы объект всегда присутствовал в клиенте. Для сервера объект создается и разрушается на основании связи с клиентом.
CORBA Глава 20 Среды разработки Поскольку CORBA — открытый стандарт, реализованный несколькими поставщиками, можно совме- совмещать и согласовывать среды разработки и выполнения. Так можно поступать, если избегать частных расши- расширений, поддерживаемых некоторыми поставщиками. Однако, как всегда и происходит при наличии нескольких поставщиков, необходимо быть уверенным в том, что при комбинировании поставщиков мож- можно получить от этого существенную выгоду, которая примирит вас с потенциальными проблемами. Например, можно генерировать заглушки клиента Java, используя VisiBroker, и применить Orbix для заглушек клиента C++ и скелетов сервера. Необходимость применения VisiBroker со стороны клиента мо- может быть обоснована наличием ORB VisiBroker на каждом броузере Netscape 4.x/5.x. Такой подход ускоряет распределение клиентов Java CORBA для среды выполнения. В среде выполнения можно использовать Web-сервер Netscape для HTTP, службы имен и службы ката- каталогов LDAP при использовании среды выполнения Orbix для перехода к управлению Microsoft ActiveX. ПРИМЕЧАНИЕ Если ваша организация не имеет приложений в среде CORBA, некоторые поставщики предлагают воспользоваться их инструментальными средствами разработки (например, Inprise и lona). Другие предлагают воспользоваться лишь их ORB без среды разработки (например, ObjectSpace). Само наличие лишь одного ORB не обязательно принесет пользу, если не производить проверку способности взаимо- взаимодействия. Двумя ведущими средами разработки C++ CORBA являются VisiBroker от Inprise и Orbix от lona. Оба этих программных продукта похожи и поддерживают все инструментальные средства, которые необходимы при создании решений даже для наиболее сложных проблем. Эти поставщики способствуют тому, чтобы ваша разработка была успешной. Многие компании используют эти ORB в качестве ускорителя для. серве- серверов приложения и более сложных сред разработки. ПРЕДОСТЕРЕЖЕНИЕ Будьте внимательны при обращении к общедоступным доменам ORB. Разработка CORBA — не самое лучшее поле для экспериментов без поддержки поставщика. Если вы достаточно опытны, просмотрите ORB по адресу: research.iphil.net/Corba. Не забудьте проверить лицензионные ограничения для коммерческой разработки при исполь- использовании ORB свободного доступа. Сравнение сред CORBA Каждая среда разработки имеет свои сложности, особенности и недостатки. Если необходимо получить среду с большим перечнем возможностей, то нелегко указать, какие возможности являются частными расширениями спецификации CORBA. Недостаточные исследования могут вынудить вас остановиться на одном частном решении. Способность ORB к взаимодействию Реализация способности к взаимодействию являлась большой проблемой для поставщиков ORB. Несмотря на то что стандартизация на ПОР была подспорьем, неоднозначность в спецификации означает, что не- несовместимость все еще существует. Большинство поставщиков ORB не уделяют должного внимания тестирова- тестированию своих ORB в сравнении с конкурирующими ORB. Если вы когда-либо имели дело с несовместимостью между реализациями компиляторов C++, то поймете, что при реализациях CORBA возникают аналогич- аналогичные затруднения. Ниже приведены некоторые вопросы, на которые следует ответить прежде, чем выбрать определенный ORB: ¦ Имеются ли все отображения языка на одном и том же уровне спецификации CORBA? Поставщики могут быть больше заинтересованы в языке Java, чем в C++. Это может привести к впечатлению, что поставщик предлагает среду CORBA 2Л для C++, хотя фактически в данном случае среда Java является совместимой с версией 2.1, а среда C++ совместима с какой-либо ранней версией. ¦ Взаимодействует ли данный ORB с JavalBL/Visigenics/Orbix? Если они не взаимодействуют (или не протестированы на взаимодействие), то ORB может вызвать некоторые проблемы. Требуйте подтвер- подтверждения способности к взаимодействию и информации о поддержке в будущем.
Распределенные вычисления Часть V Какие компиляторы языка и версии тех компиляторов требуются для вашей среды разработки? Ком- Компоновка библиотеки C++ пока что несколько затруднительна, и поставщики компилятора сохраня- сохраняют свой стиль кодирования; необходимо быть внимательным при определении подробностей вашей целевой среды разработки. Поставщики должны предоставить список версий компилятора, которые являются совместимыми. Какая версия операционной системы необходима? Как и в случае с компилятором, убедитесь в том, что ORB проверен для работы с последней версией вашей операционной среды. Если это не так, то можно сделать вывод о том, что поставщик не заинтересован в поддержке платформы. Имеется ли механизм прокси для брандмауэров и броузеров? Механизмы защиты в брандмауэрах и Web-серверах редко разрабатываются для управления ПОР. Удостоверьтесь в том, что у поставщика есть решение для работы ORB в сложных условиях, в реальных сетях. Не столь важно, является ли оно совершенным, но необходимо, чтобы было произведено тестирование. Какой уровень ПОР поддерживается? Большинство поставщиков должно поддерживать ПОР версий 1.0 или 1.1. Если они не сообщают об этом или не знают сами, то это существенный недостаток. Убеди- Убедитесь в том, что поставщик не использует частный протокол interORB. Некоторые поставщики, пере- перестроившие свои серверы приложений в ORB, столкнулись с этой проблемой. Если это так, то при реализации взаимодействия могут возникнуть серьезные проблемы, особенно если проявится несов- несовместимость с ПОР. Когда будет поддерживаться спецификация CORBA 2.2/3.0? Убедитесь в том, что поставщик ORB постоянно работает над усовершенствованием ORB, уделяя внимание вопросу поддержания совмес- совместимости с новыми спецификациями. Поставщики могут требовать столь большой объем ресурсов для частных расширений, что будет утеряна способность к взаимодействию. ПРИМЕЧАНИЕ Приложения, представленные в этой главе, совместимы с VisiBroker Inprise для C++ и компилятором C++ Borland. Если используется Orbix от lona, то сгенерированные файлы будут иметь различные имена, но типовой программный код должен быть совместимым. Детали реализации CORBA и среды разработки C++ не описываются в этой книге, цель авторов — сформировать подробное концептуальное представление о предметной области. Скомпилируйте приложения этой главы и обратитесь к документации поставщика для установки среды CORBA и ра- работы с ней. Создание клиента С+ Клиентское приложение состоит из связующего ORB-кода и обращений к методам со стороны прокси- клиентов. Прокси-клиент — это класс C++; он отображается для разработчика клиента так, как будто это локальный, а не удаленный класс. Вызовы в клиентской разработке обычно осуществляют связь со службой имен или сервером ORB, ко- который управляет объектом. В разделе "Стратегии тестирования" далее в этой главе описаны некоторые ме- методы устранения проблем, связанных с соединениями. Воспользуйтесь этими действиями при создании клиентского приложения: 1. Получите IDL для интерфейса, к которому будете обращаться. 2. Сгенерируйте клиентский программный код заглушки из IDL. 3. Запишите программный код для соединения клиентского объекта с ORB. 4. Используя метку Workflow, создайте реализацию клиентской заглушки. Реализованная заглушка и является вашим клиентским объектом. 5. Теперь напишите программный код для вызова методов клиентского объекта. Генерирование заглушки Воспользуемся модулем workflow.ldl из листинга 20.1. В среде VisiBroker имеется следующая команда для генерирования классов CORBA из шаблона 1DL: dos> idl2cpp -src_suffxx срр -no_tie workflow, xdl
CORBA Глава 20 При выполнении этой команды создаются четыре файла: ¦ workflow_c.hh ¦ workflow_c.cpp ¦ workflow_s.hh ¦ workflow_s.cpp Header-файлы и файлы исходного кода с добавлением _с предназначены для клиентского приложения; файлы с добавлением _s предназначены для серверного приложения. Сразу же обратите внимание на фай- файлы, которые сгенерированы для клиента. Классы, определенные в этих файлах, и образуют клиентскую заглушку. Будьте внимательны: программный код сгенерирован с учетом производительности, а не для удобства чтения. Для ознакомления с подробностями интерфейса класса C++ просмотрите файл workflo\v_c. hh. Открой- Откройте его и обратите внимание на метод bind(). Ознакомьтесь с методами, расположенными непосредственно после этого метода. Для удобства можно скопировать следующие объявления метода и переместить их в комментарий в программном коде клиента: /* Методы: virtual void taskStarted( const char* _aTaskId ) ; virtual void updateTask( Tasks _aTask ) ; virtual void taskOnHold( const char* _aTaskId ); virtual void createNewTask( const Tasks _aTask ) ; virtual void assignTask( const char* _aTaskId, const char* _aPersonId); virtual void taskFinished( const char* _aTaskId ) ; virtual void getTasksForPerson( const char* _aPersonId, TaskList*S _aList ) ; virtual void getTask( const char* _aTaskId, Task*S _aTask ); virtual void updateTaskList( TaskListS _aList ); */ Эти методы будут использоваться для обращения к функциональным возможностям сервера. Утилита idl2cp генерирует специфические отображения для каждого метода, типа и параметра. Связь с ORB Воспользуйтесь этими действиями для соединения с объектом сервера: 1. Инициализируйте локальный ORB. 2. Осуществите привязку к объекту. Надеемся, что вам не покажется это слишком сложным. Необходимо задать имя объекта, определенное разработчиком сервера. В этом случае разработчик сервера предоставил имя Workflow Server. Рассмотрим, каким образом написан программный код: CORBA: :ORB_ptr broker = CORBA: :ORB_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", Stask ); cout « "TASK123: " « task, description () « endl; Метод description() противоположен объекту клиента workflow. Функциональные возможности по до- добавлению новой задачи постоянно находятся на объекте сервера и на удаленном объекте. В этом фрагменте программного кода полностью скрыты распределенные функциональные возможнос- возможности. Объект сервера может выполняться на том же самом компьютере, что и клиентский объект, либо в локальной сети или даже в Internet. Специалисты, создававшие ORB, проявили заботу о сетевом програм- программировании. Завершенное клиентское приложение C++ В листинге 20.2 показано завершенное клиентское приложение. Листинг 20.2. Завершенное клиентское приложение: dientcpp /* client.cpp */ #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_yar 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", Stask ) ; cout « task.id{) « ": " « task.description() « endl; } catch ( CORBA::SystemException& anException ) { cout « "CORBA Exception: " « anException « endl; return 1; } return 0; } Клиентское приложение создает структуру Task и запрашивает сервер о создании задачи, основанной на этой структуре. Клиентское приложение затем запрашивает ту же самую задачу из сервера. Ожидается, что Task будет возвращена из getTask() в результате выполнения методом createNewTask() процедуры. Сервер может добавлять либо заменять данные при необходимости. Статус задания может измениться при установ- установке задания в начальное состояние. Создание сервера C++ Будем работать над созданием той части приложения CORBA, которая имеет отношение к серверу. Для создания объекта сервера C++ выполните следующие действия: 1. Создайте IDL. 2. Сгенерируйте скелет сервера. 3. Наследуйте свой класс сервера из скелета. 4. Инициализируйте соединение с ORB.
CORBA Глава 20 5. Создайте образец класса сервера и определите метку в конструкторе. 6. Инициализируйте BOA. 7. Присоедините объект сервера к BOA/ORB. 8. Сервер теперь готов принять вызовы метода из клиентского объекта. Генерирование скелета Воспользуемся модулем workflow.idl, который описан в листинге 20.1. В среде VisiBroker имеется следу- следующая команда для генерирования классов CORBA из шаблона IDL: dos> idl2cpp ~src_suff±x срр -no_tie workflow, idl При выполнении этой команды создаются четыре файла: ¦ workflow_c. hh И workflow_c. xpp ¦ workflow_s. hh И workflow_s. xpp Для клиентской заглушки предназначен заголовок, заканчивающийся _с. Заголовки, оканчивающиеся на _s, предназначены для скелета сервера. Опция -no_tie подавляет генерирование классов TIE. Эти классы поддерживают альтернативную модель внедрения для объектов сервера. TIE может использоваться в тех ситуациях, при которых ваш объект сер- сервера должен наследоваться из класса, не являющегося скелетом. Процесс наследования используется в данном случае для обеспечения выполнения объекта сервера. Ком- Компилятор IDL создал чисто виртуальный метод для каждого метода, определенного в интерфейсе для Workflow. Каждый из этих методов должен выполняться в полученном классе. Реализация методов сервера Генератор IDL создал скелетный класс _sk_Workflow с чисто виртуальными методами для каждого метода, определенного в интерфейсе IDL. Получим теперь 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::ORBjptr broker = CORBA::ORB_init (argc, argv); Загрузка BOA в ORB Basic Object Adapter представляет собой плохо определенный интерфейс, который используется объек- объектами сервера для соединения с соответствующими ORB. Спецификация CORBA 2.2 поддерживает Portable Object Adapter — интерфейс, который значительно лучше BOA, и он должен переноситься различными поставщиками ORB. Теперь продолжим реализацию, используя BOA, который определен спецификацией CORBA 2.1 подоб- подобно тому, как это реализовано VisiBroker. Теперь снабдим наш объект сервера меткой. Такую метку клиент использует при ссылке на этот объект сервера: Workflow::Workflowjptr 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 void tasXStarted void updateTask void taskOnHold void createNewTask void assignTask const char* aTaskld ); Workflow::Tasks aTask ); const char* aTaskld ); const Workflow::Tasks aTask); 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*& aTask ); void updateTaskList ( Workflow::TaskListS aList ) ; Листинг 20.6. Завершенное тело сервера C++: Workflow.cpp // Workflow.cpp ¦include "workflow.hpp" void Workflow::taskStarted( const char* aTaskld )
void Workflow::updateTask( Workflow::Tasks aTask ) void Workflow::taskOnHold( const char* aTaskld ) void Workflow::createNewTask( const Workflow::Tasks aTask ) // 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 ) { // readTaskFromDatabase( aTaskld, aTask ); // Установить значение для тестирования aTask->setId( aTaskld ); aTask->setDescription( "The method was called!" ) ; void Workflow::updateTaskList( Workflow::TaskListS aList ) /* server.cpp */ #include "workflow.hpp" #includa <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 jptr adapter = broker->BOA_init( argc, argv ) adapter->obj_is_ready( workflow ); adapter->impl_is_readyО; } catch ( CORBA::SystemExceptioni anException ) { cout « "CORBA Exception: " « anException « endl; return 1; CORBA Глава 20 return 0;
Распределенные вычисления Часть V Клиент Java Способность к интеграции с другими языками — одна из известных возможностей CORBA. Имея лишь определение IDL, можно создавать клиентское приложение, которое органически интегрируется с вашим объектом сервера. В следующих разделах этой главы описан эквивалент Java для клиента C++. Здесь будут описаны лишь различия в генерировании объектного кода, запуске и вызове метода. Наша цель состоит в том, чтобы ознакомить вас с альтернативными привязками языка для CORBA. ПРИМЕЧАНИЕ! Клиент Java использует VisiBroker для Java. Компания JavaSoft включила свободно распространяемый ORB в пакет JDK 1.2. Для того чтобы этот ORB работал с VisiBroker, необходимо использовать не всегда удобную ссылку IOR. Более подробная информация об этом содержит- содержится в разделе этой главы "Служба имен и способность к взаимодействию". Генерирование заглушки Для генерирования клиентских заглушек в Java используется workflow.idi. Выполните следующую строку: dos> idl2java -no_tie workflow.idi VisiBroker создает каталог Workflow со следующими файлами: ¦ PersonldHelper.Java Я PersonldHolder.Java Я Task.Java В TaskHelper.java ¦ TaskHolder.java a TaskldHelper.java H TaskldHolder.java И TaskListHelper.java Я TaskListHolder.java в TaskStatusCode.java a TaskStatusCodeHelper.java Я TaskStatusCodeHolder.java ¦ Workflow.java Я WorkflowHelper.java ¦ WorkflowHolder.j ava Я WorkflowOperations.java Я _example_Workflow.java ¦ _sk_Workflow.java Я _st_Workflow.java Я _WorkflowImplBase.java Здесь приведены классы Java как для клиентской заглушки, так и для скелета сервера. Укажем файлы, специфические для сервера: _example_Workflow. java и _sk_Workflow. java. Все другие файлы необходимы для клиентского приложения. Запуск и программный код вызова метода Ниже приведен Java-эквивалент для запуска клиента C++ и программный код вызова метода для кли- клиента Workflow. org.omg.CORBA.ORB broker = org.omg.CORBA.ORB.init( args, null ); Workflow.Workflow workflow =
CORBA Глава 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, то генерируется дополнительный программный код. Стратегии тестирования Какого рода проблемы могут возникать с объектами, выполняющими удаленные вызовы методов? Да, воз- возможно, эти проблемы не столь значительны, как можно предположить. Поскольку ORB используются в прило- приложениях, которые выполняют сложные задачи, брокеры ORB должны быть устойчивыми в различных ситуациях. Может сложиться мнение, что дополнительный уровень объектов CORBA снижает вероятность отказов. Может быть, и невозможно смоделировать среду, в которой произошла ошибка. В этих случаях могут использоваться более традиционные инструментальные средства сетевого программирования. В последующих разделах описаны методы, которые помогут при отладке приложений CORBA. Трассировка Почти каждый ORB обладает возможностью трассировки. Воспользуйтесь этим. При реализации некото- некоторых возможностей трассировки используется Implementation Repository (Архив реализаций) для хранения информации; можно также использовать пульты управления или файлы двумерных массивов. Обратите внимание, каким образом осуществляется этот процесс, когда все идет успешно. Затем при возникновении проблемы можно будет быстро ее устранить. Службы мониторинга и регистрации Регистрация события является трассировкой во время выполнения. Перешлите события регистрирую- регистрирующим интерфейсам и дайте вашим объектам вторую жизнь. Если происходят сбои, то пользователи могут воспользоваться файлом регистрации и, возможно, четко зафиксировать свои собственные проблемы. Обработка исключений IDL позволяет выявлять исключения структурированно и подробно. Используя раскрытие динамических типов, инструментальные средства регистрации могут отобразить данные, оказавшиеся в особой ситуации. Например, следующее исключение может быть добавлено к примеру Workflow: // Добавить к Workflow.IDL: exception TaskAlreadyAssigned { Taskld assignedToId; } // Заменить метод в Workflow.IDL void assignTask( in Taskld aTaskld, in Personld aPersonld ) raises (TaskAlreadyAssigned); // Добавить к Workflow.cpp try { workflow->assignTask( "TASK123", "CHUCKPACE" ); } catch ( TaskAlreadyAssignedS anException )
Распределенные вычисления Часть V cout « "TASK123 already assigned to: " « anException.assignedToId() « 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, больше не должно быть проблем. На этом этапе связь с каналом установлена, и объект должен существовать до тех пор, пока не будет устранен класс заглушки прокси-заглушги.
CORBA Глава 20 Обнаружение службы имен на первоначальном этапе может вызвать затруднения. В спецификации CORBA 2.1 отсут- отсутствует какой-либо протокол начальной загрузки для клиента, который помог бы в нахождении первого брокера ORB. Именование контекстов В CORBA именование контекстов призвано решать проблемы конфликта имен в языке C++. Именова- Именование контекстов производится в иерархии, подобной структурам каталогов файловой системы или иерар- иерархии классов. Контекст имени — это последовательность именований контекстов, которые идентифицируют одиночный объект уникальным образом. На рис. 20.3 показано, каким образом имена создаются из именования контекстов. Имя, разделяемое точкой с запятой, представляет собой имя объекта. Если пространства имен C++ сохраняют имена пере- переменных классов, то именование контекстов CORBA гарантирует уникальность имен объектов. реализация РИСУНОК 20.3. Иерархическое вложение для предотвращения конфликтов имен объектов. Проблемы взаимодействия Метод _Ыш1() представляет собой механизм начальной загрузки VisiBroker, который позволяет выпол- выполнить разрешение имен. Этот метод _bind() исключает возможность переноса клиентского программного кода, но позволяет его прочесть. Универсальная начальная загрузка должна быть включена в спецификацию CORBA 3.0. Для различных ORB вам, вероятно, придется заменить _bind() специализированным механизмом, разработанным постав- поставщиком, для выполнения разрешения имен. Чрезвычайно важно понять три уровня возможной начальной загрузки: ¦ Использование частного метода в клиентском объекте, например, _bind(). Этот метод используется для небольших приложений и для приложений среднего размера, где клиент и сервер находятся на одном и том же компьютере или в локальной сети. ¦ Частное соединение со службой имен, когда эта служба и ORB имеют частный механизм иницииро- инициирования соединения. В данном случае приложение более сложно и нуждается в службе имен для того, чтобы найти объекты сервера, которые могут находиться на локальной сети или обнаруживаться в Internet. ¦ Если служба имен и клиентский объект получены от разных поставщиков, то необходима стринги- фикация. Это означает, что служба имен записывает стрингифицированную IOR (строка, которая может быть повторно сконструирована в IOR) в файловую систему. Клиент затем читает строку из файло- файловой системы, повторно конструируя IOR и связываясь с первым ORB. Производительность Производительность в среде CORBA достаточно высокая. Когда автор приступал к разработке, то ожи- ожидал получить невысокую производительность при опытной разработке OLE/COM.
Распределенные вычисления В следующих разделах описаны области, в которых ваши приложения будут работать наиболее устойчи- устойчиво. Многие из этих областей подобны тем, которые имеются в разработке C++, — там, где возникает связь с распределенными вычислениями. Перерасход памяти со стороны ORB Если приложение выполняется с порожденным процессом, то очень трудно определить причины пере- перерасхода памяти. Операционная система действует как сборщик мусора для приложения Но если перерасход памяти возник внутри одиночного процесса, приложение должно выполнить собственную сборку "мусора". Большинство приложений C++ не содержит схемы сборки "мусора". Следовательно, программисты C++ должны быть внимательны к утечкам в приложениях сервера при работе с большинством сред CORBA. Простая утечка, которая может быть скрыта в CGI-порожденном процессе, может превратиться в огром- огромную проблему для ORB. Стоимость со стороны CGI обычно отображается во время запуска процесса и неуклюжих архитектур. Степень детализации интерфейса Поскольку механизмом, соединяющим ПОР, являются каналы, передача данных обычно происходит намного медленнее, чем при использовании !РС или при прямой передаче в память. Для того чтобы это компенсировать, можно консолидировать методы доступа в одиночные вызовы метода, которые устанав- устанавливают или получают значения из передаваемой структуры. ПРИМЕЧАНИЕ Поставщики брокеров ORB могут оптимизировать саои модули с помощью !РС или прямого доступа в память, когда клиент и сервер находятся на одном компьютере. Некоторые ORB даже используют более быстрые протоколы, чем ПОР, если их программный продукт используется на клиенте и сервере. Если клиент и сервер находятся на различных компьютерах, то ORB, выполняющий роль мрокси, можег быть назначен самому медленному соединению в целях ускорения прохождения запросов ORB, Некоторые реализации TCP/IP используют IPC, если клиент и сервер находятся на одном компьютере. Можно поддерживать дополнительные методы, которые выполняют пакетную обработку для часто вы- вызываемых методов. Да я выполнения этого создайте структуру с параметрами, возвращаемыми значениями и информацией об исключениях. Создайте последовательность этих структур и используйте ее, как параметр для пакетной версии функции. Избегайте передачи слишком большого количества данных. Если сервер обеспечивает клиент списком данных из таблицы базы данных и пользователь отмечает строки для удаления, вставки или изменения, передавайте обратно только строки, которые должны измениться. Высокая производительность аппаратных средств устраняет много проблем, имеющих отношение к сте- степени детализации, если клиент и сервер работают на одном компьютере. Ссылки на передаваемый объект IOR в CORBA является эквивалентом ссылки в C++. Ссылка IOR может быть передана как параметр в методе. Затем объект сервера может обращаться к тому объекту для выполнения любых необходимых опе- операций. Этот косвенный подход предполагает непроизводительные затраты; не перегружайте 10R таким об- образом. Резюме Спецификация CORBA и ее реализации представляют собой крупное достижение в сфере разработки ПО. Теперь разработчик может сосредоточиваться на решении архитектурных проблем, не тратя свои уси- усилия на решение проблем интеграции. Разработчики, имеющие скромный опыт, и даже новички могут воспользоваться гибкостью и везде- вездесущностью CORBA при создании ПО следующего поколения. Эти системы будут отличаться тем ценным качеством, что можно будет управлять распределенными вычислительными средами и поддерживать их работу точно так же, как в случае со старыми мэйнфреймами, а гибкости работы с ними смогут позавидовать старые приложения клиент-сервер.
COM В ЭТОЙ ГЛАВЕ Основы СОМ Использование СОМ-объектов в C++ Создание СОМ-объектов в C++
Распределенные вычисления Часть V Объектно-ориентированные языки — значительный шаг вперед в развитии индустрии ПО. Большое ко- количество книг посвящено обоснованию процесса построения объектно-ориентированных проектов на базе 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 Глава 21 Архитектура COM Модель Component Object Model определяет следующие основные сущности: ¦ Объект. Этот элемент тесно связан с классом C++. Объекты реализуют функциональные возможно- возможности и представляют основное содержание СОМ. ¦ Интерфейс. Этот элемент обеспечивает определение некоторых функциональных возможностей, ко- которые являются общими для многих объектов. Интерфейсы определяют правила для взаимодействия с объектами. ¦ Библиотека СОМ времени выполнения. Этот элемент содержит небольшой набор подпрограмм для необходимой поддержки объектных служб. Каждый объект реализует один или большее количество интерфейсов, которые дают возможность пользо- пользователям осуществлять доступ к нему. Интерфейсы, реализуемые объектами, представляют их функциональ- функциональные возможности. Выбор реализуемых интерфейсов осуществляется конструктором объектов. Каждый объект может быть выбран для создания другого объекта при использовании метода с помо- помощью одного из интерфейсов (при реализации метода интерфейса создается объект и возвращается указа- указатель на один из его интерфейсов через выходной параметр). Этот процесс представляет собой часть семантики интерфейса. Не часто извлекают выгоду из того, что один объект производит другой, и затем второй объект производит иной объект, относящийся к объектам первого класса. Чаще происходит так: имеется какой- либо объект, создаваемый непосредственно, который, в свою очередь, создает другие объекты. Эти объек- объекты создают другие объекты и т.д. В результате получим дерево объектных классов, в котором каждый родительский класс может производить объекты своих потомков. Иерархия объектов, созданных одним корневым объектом и его потомками, называется компонентом. Компонент представляет собой нечетко определенный термин. Иногда он относится к группе объектных иерархий, реапизованных в одиночном модуле (исполняемая программа или DLL), который инкапсулирует необходимые функциональные воз- возможности для выполнения определенной задачи. Объекты из различных иерархий обычно строго взаимо- взаимосвязаны (например, один объект активно используется другим при выполнении задачи). Один специфический класс объектов СОМ, который может быть создан непосредственно во время выполнения СОМ, представляет особый интерес. Это класс СОМ, или кокласс. Как будет показано далее в этой главе, этот класс связан с другим классом объектов СОМ — фабрикой классов, которая производит их. Объект фабрики классов представлен методом в одном из своих интерфейсов, которые производят эк- экземпляр кокласса и, таким образом, могут рассматриваться как независимая от языка конструкция, ана- аналогичная оператору new в языке C++. СОМ-объекты могут использоваться их клиентами в двух различных режимах (которые прозрачны для объекта и клиента): прямом или упорядоченном. Если объект и клиент находятся в едином контексте выпол- выполнения (который называется апартаментом), клиент использует прямые указатели на объектные интерфей- интерфейсы. Если объект и клиент находятся в различных апартаментах, то СОМ вклинивается между ними и обеспечивает необходимую поддержку для вызова методов интерфейса и возврата результатов. Два объекта обеспечивают необходимую поддержку. Прокси постоянно находится з клиентском контексте и действует как объект для этого определенного интерфейса (клиент не может различить объект и его прокси). Заглуш- Заглушка находится в контексте объекта и действует как клиент интерфейса объекта. Соединение между объекта- объектами использует протоколы, соответствующие определенному соединению (сообщения Windows, RPC или ДР-)- Интерфейсы В СОМ интерфейсы определяют функции, которые реализуются объектами. Они больше всего относят- относятся к классам C++, которые не содержат каких-либо элементов данных, а лишь чисто виртуальные методы. Таким образом интерфейсы СОМ реализуются в языке C++. Ключевое слово interface, которое применя- применяется в программном коде для описания интерфейса, заменяется на struct в заголовке C++: #define interface struct. Все методы интерфейса объявляются как чисто виртуальные методы в операторе struct из C++. Рас- Рассмотрим следующее определение C++ для интерфейса ICar: interface ICar { void SetSpeed{ long nSpeed ) ,"
Распределенные вычисления Часть 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 добавили некоторые расширения для специальных сообщений СОМ. В результате СОМ-интерфейсы могут применяться удаленно с помощью RPC (это зна- значит, что как для процессов, так и для компьютеров эта тема не является предметом рассмотрения данной главы).
COM Глава 21 Синтаксис IDL подобен синтаксис;*- объявлений C++ с некоторыми дополнительными аннотациями. Ниже приводится вариант определения интерфейса ICar, написанного на язьже IDL: [object, uuid(C2"iTH200-?FB6-lld2~8952-444553540000) ] interface ICar { void SetSpeed', [ibj long nSpeed ) ; }; Ключевое слово object указывает на то, что это СОМ-интерфейс (а не RPC-интерфейс), uuid() предо- предоставляет IID-интерфейс, in обозначает параметр, используемый в качестве входного. Имеются инструмен- инструментальные средства, которые обращаются к определению файла IDL, описывающему интерфейс и создающему header-файт, совместимый с С/С ++. Этот файл используется при выполнении интерфейса. Эти инстру- инструментальные средства также генерируют программный код прокси/заглушки для удаленного интерфейса и файл, который определяет fiD (GUID представлены как структуры и должны быть распределены). Множе- Множественные интерфейсы могут быть определены в одиночном файле IDL. Компиляции не состоится, ест отдельный интерфейс будет определен таким образом. Если использо- использовать компилятор Microsoft IDL (MIDL), то в результате выдаются две ошибки. Первая ошибка вытекает из того факта, чн» СОМ-интерфейсы должны наследоваться из IUnknown, как будет отмечено далее в этой главе. Вторая ошибка имеет отношение к тому факту, что все методы в интерфейсах СОМ должны возвра- возвращать HRESULT. Запишем пример иным образом, чтобы он мог восприниматься MIDL: import "unknwn.idl" [object, uuid(C21DG200-2F36-llcl2-8952-444553540000) ] interface ICar : lUnknown { HRESULT SetSpeed{ [in] long nSpeed ); }; Возникающий в результате header-файл содержит среди всего прочего следующее определение интер- интерфейса C+ + : interface DECLSPEC_UUID("C21D0200-2FB6-lld2-8952-444553540000") ICar : public iankiiown I public: virtual HRESULT STDMETHODCALLTYPE SetSpeed( /* I in] */ long nSpeed ) = 0; }; Поскольку interface определяется как класс в заголовке СОМ, то в записи появляется public. Параметр STDMETHODCALLTYPE усиливает соглашение о вызовах stdcall. Параметр DECLSPEC_UUID можно иг- игнорировать, поскольку это расширение C++. представленное Microsoft в Visual C++ 5.0. Интерфейс IUnknown наследуется общедоступным способом, поскольку основной интерфейс является частью VTBL, и это дол- должно быть заметно. Возникающий в результате класс C++ содержит только чисто виртуальные и общедос- общедоступные методы. С помощью HRESULT представлены 32-разрядные целые числа, которые описывают состояние вызова метода. Имя подразумевает, что это будет читаться как дескриптор для результата, которого не было. (Ра- (Ранее предполагалось, что HRESULT будет дескриптором, и имя было установлено: позже было замечено, что 32 разряда вполне достаточно для непосредственного описания результата.) СОМ предписывает всем методам возвращать HRESULT, поскольку необходимо сообщать о сетевых отказах вызывающему операто- оператору универсальным способом. HRESULT состоит из следующих разрядных полей: SRRFFFFFFFFFFFFFCCCCCCCCCCCCCCCC S (I бит) Важный код: SEVERITY_SUCCESS или SEVER1TY_FAIL R B бита) Резервный, должен быть нуль F (I! битов) Возможность С A6 битов) Код состояния Код состояния непосредственно занимает первые 16 битов HRESULT. Затем располагается указание на то, какая подсистема возвратила программный код. Все пользовательские коды привязаны к FACILITY_ITF
^^^ Распределенные вычисления Часть V D), что определяет специфику интерфейса. Наиболее важный бит представлен битом важности: SEVERITY_SUCCESS @) означает, что действие произошло успешно, SEVERITY_FAIL A) означает, что операция завершилась неудачно. Два макроса 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 @x80010105), если интерфейс является удаленным. Если объект находится в процессе, то исключение, вероятно, разрушит клиент. Интерфейсы могут наследоваться. Это кажется естественным, поскольку они — всего лишь классы C++. Однако в этом аспекте имеется существенная разница. Поскольку интерфейсы состоят исключительно из чисто виртуальных методов, полученный интерфейс содержит все методы основного интерфейса. Это озна- означает, что объект, который осуществляет полученный интерфейс, должен также выявить основной интер- интерфейс (он реализуется в любом случае). Если же два или более интерфейса получены из некоторой основы, их связывают с помощью интерфейса и все они реализованы на одном объекте, то методы основного интерфейса могут иметь различные реализации (иногда это желательно). В общем, нежелательно использо- использовать наследование интерфейса, за исключением особых обстоятельствах. СОМ предлагает лучший способ достижения полиморфизма — метод Querylnterface() интерфейса lUnknown (будет рассмотрен в следующем разделе). Наследование интерфейса необходимо только в том случае, если интерфейс не может существо- существовать без реализации методов основного интерфейса. В качестве примера можно рассмотреть lUnknown, ко- который должен быть основным интерфейсом для всех интерфейсов СОМ. В листинге 21.1 показан IDL, используемый в примерах этой главы. Некоторые подробности этого про- программного кода станут ясны при дальнейшем рассмотрении главы, при обсуждении библиотек типов. Листинг 21.1. Пример IDL для Car Object и его интерфейсов import "unknwn.idl" [ object, uuid(C21D0200-2FB6-lld2-8952-444553540000), helpstring("Car driving") ] interface ICar : lUnknown < HRESULT SetSpeed( [in] long nSpeed ); object, uuid(C21D0200-2FB6-lld2-8952-444553540000) helpstring("Engine control") ] interface IEngine : lUnknown { HRESULT Start () ; HRESULT Stop() ; uuidC10C97F4-3ABE-lld2-915E-52544C004D83) versionA.0), helpstring("Car library 1.0") ] library YourLib { importlib "stdole2.tlb"
COM Глава 21 importlib "stdole32.tlb" [ uuidC10C97D0-3ABE-lld2-915E-52544C004D83), helpstring("Car class") ] coclass Car { [default] interface ICar; interface lEngine; Интерфейс lUnknown Все интерфейсы COM должны наследоваться из lUnknown. Их определение таково: [ local, object, uuid@0000000-0000-0000-0000-000000000046), pointer_default(unique) ] interface lUnknown { HRESULT Querylnterface( [in] REFIID riid, [out, iid_is(riid)] void **ppvObject ); ULONG AddRef( void ); ULONG Release( void ); }; Ключевое слово local означает, что интерфейс не является удаленным (т.е. никакой упорядоченный код не должен быть сгенерирован компилятором 1DL). Это кажется странным, потому что интерфейсы СОМ вообще являются удаленными (т.е. они могут вызываться через границы контекста выполнения). Интер- Интерфейс lUnknown является удаленным. Удаленная схема полагается на другой интерфейс (IRemUnknown), используемый внутренним образом, который позволяет выполнять некую оптимизацию вызовов. Указатель Pointer_default (unique) инструктирует упорядоченный код о том, что указатели NULL могут быть возвра- возвращены вызовами методов. I1D инструктирует упорядоченный код о том, что необходимо обработать указа- указатель как указатель интерфейса и именовать его. Здесь IID впервые вступает в игру. REFIID является IID и единственным не С-совместимым элементом, используемым в методах интерфейса. При этом используется тот факт, что компиляторы C++ передают ссылочные параметры как указатели. Учитывая эти дополнения, исследуем методы интерфейса lUnknown. Метод Querylnterface() используется для опроса объекта в результате которого выясняется, поддержи- поддерживает ли он специфический интерфейс, получивший имя с его IID. Результат возвращается во втором пара- параметре. Поскольку Querylnterface() возвращает общий указатель интерфейса, недопустимо определение, сохраняющее типы. Однако возможно, по крайней мере, возвращать lUnknown **. Вызывающий оператор передает адрес указателя интерфейса типа, именованный с помощью параметра riid (при этом необходимо ссылаться на корректную VTBL для возвращаемого интерфейса). Метод Querylnterface() должен возвратить S_OK при успешной реализации или E_NOINTERFACE, если рассматриваемый интерфейс не реализован с объектом (в этом случае выходному параметру ppv присваивается значение NULL). Все это напоминает оператор dynamic_cast из C++ (который можно принять в качестве модели). Метод Querylnterface() пред- предлагает клиенту способ выполнения запроса функциональных возможностей объекта во время выполнения. Ниже будет показано, что к поведению Querylnterface() предъявляются строгие требования. Методы AddRef() и ReleaseQ используются для управления указателем интерфейса во время его суще- существования путем подсчета ссылок. Изначально каждый указатель интерфейса имеет ссылку I (которая явля- является первым требованием для Querylnterface() — в случае успеха вызывается AddRef() для возвращенного указателя интерфейса). Когда клиент заканчивает использование интерфейса, для указателя вызывается метод Release(). Каждый раз, когда указатель интерфейса дублируется (сохраняется в другой переменной или в поле структуры или в другом месте), метод AddRef() должен вызываться для увеличения числа подсчитан- подсчитанных ссылок. Перед разрушением копии (например, при освобождении кучи или когда локальная перемен-
Распределенные вычисления Часть V ная выходит за пределы диапазона) должен вызваться метод Release(). Реализация метода Release() долж- должна позволить проверить, когда подсчет ссылок достигает нуля, чтобы освободить любые ресурсы, требуе- требуемые интерфейсом. ПРИМЕЧАНИЕ Каждый интерфейс объекта управляет собственным подсчетом ссылок. Фактическая реализация освобождается для выполнения единственного подсчета ссылок, а следовательно, освобождаются и все интерфейсы для объекта. Однако • если количество ссылок для всех интерфейсов объектов достигает нулевого значения, объект самоуничтожается. (Ко- (Конечно, количество ссылок для любого интерфейса не должно быть меньше нуля, иначе клиент сгенерирует ошибку.) Поскольку все интерфейсы наследуются от lUnknown, они имеют метод QuerylnterfaceO для своих VTBL. Объекты обычно заполняют VTBL для всех интерфейсов с помощью единственной реализации QuerylnterfaceO, хотя это и не обязательно. Все реализации Queryluterface() для объектов должны соответ- соответствовать следующим правилам (все рассматриваемые интерфейсы соответствуют одному объекту): ¦ QuerylnterfaceO для lUnknown всегда успешно выполняется и возвращает один и тот же указатель на интерфейс, независимо от интерфейса, который вызывался. lUnknown — это базовый интерфейс для любого другого интерфейса; обычно один интерфейс выби- выбирается с тем, чтобы быть возвращенным, и никакой специальной VTBL для lUnknown не назначает- назначается. Это правило гарантирует, что lUnknown уникален для объекта и может использоваться как объектная сущность. Когда возникают сомнения относительно того, указывают ли два интерфейса на одич и тот же объект, можно использовать QuerylnterfaceO для lUnknown на обоих интерфейсах и сравни- сравнивать генерируемые в результате указатели. ¦ Если QuerylnterfaceO для интерфейса IX успешно выполняется один раз, то он должен успешно вы- выполняться во время жизни объекта независимо от интерфейса, для которого используется. Это правило гарантирует стабильность реализации объекта и поддерживает для клиентов некоторую степень доверия так, что они не должны хранить указатели интерфейса, если только получают их: клиент гарантированно получает указатели на интерфейс, если это потребуется в будущем. Конечно, клиент должен включать, по крайней мере, один указатель интерфейса, если требуется хранить объект в активном виде. ¦ Если QuerylnterfaceO для интерфейса IY успешно выполняется на интерфейсе IX, то QuerylnterfaceO для интерфейса IX также успешно выполняется на интерфейсе IY. QuerylnterfaceO для интерфейса IX, вызываемый интерфейсом IX, должен всегда успешно выпол- выполняться. Если QuerylnterfaceO для интерфейса IY успешно выполняется на интерфейсе IX, и QuerylnterfaceO для интерфейса IZ успешно выполняется на интерфейсе IY, то QuerylnterfaceO для интерфейса IX успешно выполняется на интерфейсе IZ. Эти три правила гарантируют безопасность при управлении с помощью объектных интерфейсов и га- гарантируют, что все интерфейсы будут достижимы отовсюду. Ниже находится пример простой реализации QuerylnterfaceO для объекта, который предлагает два интерфейса, lEngine и ICar, путем множественного наследования: STDMETHODIMP CCar::QueryInterface( REFIID riid, void **ppv ) HKESULT hr = S_OK; // Обратите внимание, что lEngine выделен для запросов lUnknown 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 ; )
COM Глава 21 // Обратите внимание, что вызывается AddRef для возвращенного указателя if ( SUCCEEDED( hr ) ) { reinterpret_cast<IUnknown*>( *ppv )->AddRef () ; } return hr; } Важный момент — это обработка запроса IUnknown и заключительный вызов метода AddRef(). Объект назначает интерфейс lEngine как сущность и возвращает его, когда выполняется запрос для IUnknow. Пе- Перед возвратом указателя интерфейса метод AddRef() обращается к возвращенному указателю интерфейса. Этот процесс выполняется согласно правилу, когда каждый интерфейс поддерживает собственный под- подсчет ссылок. 'ПРИМЕЧАНИЕ Реализации, подобные описанным, редко используются. Если имеются несколько интерфейсов, используемых объек- объектом, размеры программного кода увеличиваются. Подход, предполагающий использование таблиц, является более пред- предпочтительным. Однако рассмотренная реализация является хорошим примером. СОМ-объекты До сих пор механизм использования и управления с помощью интерфейсов иллюстрировался для един- единственного объекта, но клиент ие получал начальный указатель IUnknow на объект. Существует четыре различных способа получения начального указателя интерфейса для объекта: ¦ Через обобщенные СОМ-функции создания, подобные CoGetClassObject(), CoCreatelnstance() и др. ¦ Через метод интерфейса, осуществляющий вызов другого объекта, возвращающего интерфейс для нового объекта. ¦ Когда клиент объекта передает указатель интерфейса другого объекта первому объекту, используя вызов метода интерфейса. Таким образом первый объект получает указатель на новый объект в целях внутреннего использования — первый объект является клиентом. ¦ Через другую функцию API, которая генерирует специфический объект и возвращает один из ин- интерфейсов вызывающему оператору. В качестве примера может служить CreateStreamOnHGlobal(), ко- который создает стандартный OLE-объект, представляющий поток памяти и возвращающий интерфейс IStream. Последний метод используется редко. Он был актуальным в эпоху начала развития OLE (которая бази- базировалась на Win32 API). Второй метод представляет объектную навигацию в иерархии. Третий метод пред- представляет объекты согласно способу, с помощью которого они взаимодействуют с клиентами, и кратко описывается в разделе "Другие СОМ-технологии" далее в этой главе. Первый метод — это наиболее широко распространенный способ распределения СОМ-компонентов. Он работает с коклассами. Каждый кокласс имеет связанный объект, называемый объектом класса или фабри- фабрикой класса. Хотя и не в обязательном порядке, но этот объект предлагает интерфейс IClassFactory: [ object, uuid@0000001-0000-0000-C000-000000000046), pointer_default(unique) ] interface IClassFactory : IUnknown { HRESULT Createlnstance( [in, unique] IUnknown *pUnkOuter, [in] REFIID riid, [out, iid_is(riid)] void **ppvObject ); HRESULT LockServer( [in] BOOL bLock ); ); Метод LockServerQ используется, чтобы предотвратить выгрузку сервера в то время, когда нет актив- активных объектов. При этом объекты реализованы на сервере. Таким образом, запросы на создание генериру- генерируются позже и обслуживаются значительно быстрее. Все вызовы LockServer (TRUE) должны быть согласованы
вычисления Часть V с соответствующим числом вызовов LockServer (FALSE) (в качестве истинного значения с AddRef() и Release() для интерфейсов). Перейдем к рассмотрению важного метода — Createlnstance(). Createlnstance() использует три параметра. Последние два из них передаются Querylnterface() и дают возможность клиенту непосредственно получить указатель для нужного интерфейса, причем не только ILnknow. Первый параметр — это новый IUnknow, управляющий объектом, который используется при аг- агрегировании, как описано далее. Для обычных клиентов он всегда является NULL. Этот метод хорошо зна- знаком с объектом, который должен создать. Во время выполнения СОМ используются механизмы размещения фабрик класса, которые здесь не будут обсуждаться. Функция СОМ, которая делает это, — CoGetCIassObject(): STDAPI CoGetClassObject( REFCLSID rclsid, DWORD dwClsContext, COSERVERINFO *pServerInfo, REFIID riid, void **ppv ) ; Последние два параметра — это те параметры, которые передаются методу Querylnterface() фабрики класса и представляют непосредственно запрошенный интерфейс из фабрики класса (обычно IClassFactory). Параметр pSp^verlnfo используется для описания компьютера и контекста защиты вызова. На одном и том же компьютере, использующем заданную по умолчанию защиту, pServerlnfo может иметь значение NULL. С помощью dwClsContext вызывающий субъект определяет, что нужно сделать, чтобы объект был создан. Наиболее важным является первый параметр: REFCLSID является CLSID&, и CLSID — GU1D (это иллюстрация того, что мы столкнулись с большим количеством GUID). Каждый кокласс имеет CLSID, связанный с ним (каждый интерфейс, именованный с помощью IID). Таким образом, любые два кокласса могут в достаточной степени различаться. Следовательно, rclsid благополучно вызывает запрошенный кок- кокласс. Это CLSID кокласса, который производит фабрика класса (фабрики класса не имеют CLSID). Если эта функция успешно выполняется, то вызывающий оператор удерживает указатель на фабрику класса для кокласса, представляющую интерес, и может создавать столько экземпляров, сколько нужно (в качестве интерфейса обычно используется IClassFactory). Когда работа с фабрикой класса завершается, клиент дол- должен вызвать метод ReleaseQ (как правило). ПРИМЕЧАНИЕ СОМ-объект может быть активизирован в трех различных контекстах класса: "в процессе", локально и удаленно. Кон- Контекст "в процессе" означает, что объект создан в адресном пространстве вызывающего процесса. Объектный сервер ; размещается в DLL. На локальные и удаленные активизации ссылаются как на активизации внешних процессов. Объек- Объектный сервер постоянно находится в отдельной выполняемой программе, и объект создается в другом процессе. При этом определяется, находится ли постоянно объектный сервер на том же самом компьютере или на другом. Если фабрика класса отображает IClassFactory (рекомендуется), библиотека СОМ времени выполнения поддержи наст обертку, необходимую для создания единственного экземпляра кокласса: STDAPI CoCreateInstance( REFCLSID rclsid, IUnknown *pUnkOuter, DWORD dwClsContext, REFIID riid, void **ppv ) ; Эго несколько устаревшая функция, поскольку она не поддерживает параметр COSERVERINFO*. Од- Однако для нашего обсуждения эта функция вполне удовлетворительна, rclsid и dwClsContext передаются CoGetClassObjectQ с тем, чтобы получить фабрику класса объекта (передаваемый IID — IID_IClassFactory). Если фабрика класса получена, вызывается IClassFactory::CreateInstance(), передавая остающиеся три па- параметра. После этого фабрика класса освобождается. Выходной параметр содержит запрошенный указатель интерфейса, который был успешно вызван. Технология СОМ имеет все характеристики, необходимые для того, чтобы рассматривать ее в качестве объектно-ориентированной системы: инкапсуляция, полиморфизм и возможность многократного исполь- использования. Отсутствует единственная возможность — повторное использование реализации (также называе- называемая возможностью многократного использования). Для решения этой проблемы используется методика, называемая агрегацией. Благодаря агрегации один объект может непосредственно выставлять реализацию не-
COM Глава 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), который называется языком описания объектов (ODL — Object Description Language).Обсуждение ODL находится вне контекста этой главы.При- главы.Примите к сведению то, что IDL может включать определение библиотеки типа: t uuid(98178CD0-3467-lld2-914B-52544C004D83), version A.0) , helpstring("This is type library") ] library YourLib Обычно кокласс описывается в библиотеке типов: [ uuidE5712EB0-3468-lld2-914B-52544C004D83), helpstring("This is coclass") ] coclass YourClass { [default] interface IMain; interface ISecond; ); В дополнение к коклассам это определение влечет за собой определение двух интерфейсов: IMain и [Second. Библиотека типов может также содержать определения типов, счетчики, структуры С, объединения и др. Библиотека типов генерируется компилятором IDL или отдельным инструментальным средством. В ре- результате создается двоичный файл, который может распределяться вместо исходного IDL. Затем пользова- пользователь применяет некоторый инструмент декодирования для преобразования библиотеки типов в собственный языковой формат. Эта тема будет рассмотрена далее в этой главе. Технология СОМ определяет два интерфейса для непосредственного чтения содержимого библиотеки типов: ITypeLib и ITypelnfo. Хотя маловероятно, что обычная программа C++ использует библиотеку типов 18 Зак. 53
Распределенные вычисления Часть V непосредственно, можно выполнить ее обработку и сгенерировать соответствующие вызовы методов во время выполнения или соответствующие VTBL для выходных интерфейсов объекта (эта тема кратко рассматрива- рассматривается в следующем разделе). Другие СОМ-технологии В следующих разделах производится обзор некоторых технологий, основанных на использовании СОМ. Эти темы описываются очень кратко, поскольку эти технологии широко используются в большинстве СОМ- приложений. Многие завершенные СОМ-технологии формируются на этой базе. Управление памятью Если клиент и объект находятся в одном апартаменте, клиентские вызовы методов интерфейса исполь- используют непосредственный указатель таким образом, чтобы можно было осуществлять безопасную передачу указателей. Если подключаются прокси и заглушка, то это часто бывает необходимо для размещения дан- данных, указывающих на параметр, находящийся между клиентом и объектом. Это делается для того, чтобы удовлетворить ожидание объекта на получение реальных данных в указанном месте. СОМ предоставляет разрешение параметрам [in] на поддержку со стороны клиента. Когда объект должен вернуть данные в па- параметре [out] метода интерфейса, он распределяет данные и клиент выполняет очистку. Для сохранения двоичной независимости необходим некий универсальный механизм для управления памятью. При этом используется интерфейс IMalloc: [ local, object, uuid@0000002-0000-0000-C000-000000000046) ] interface IMalloc : IUnknown { void *Alloc( [in] ULONG cb ); void *Realloc ( [in] void *pv, [in] OLONG cb ) ; void Free ( [in] void *pv ) ; ULONG GetSizef [in] void *pv ); int DidAlloc( void *pv ); void HeapMinimize () ; }; Первые три метода осуществляют непосредственное обращение к функциям времени выполнения С malloc(), realloc() и free(). Интерфейс IMalloc реализуется на объекте, если СОМ использует функцию CoGetMalIoc(). В целях обеспечения удобства работы программиста в СОМ поддерживаются три функции обертки, позволяющие обратиться к первым трем методам IMalloc — CoTaskMemAlloc(), CoTaskMemRealloc() и CoTaskMemFree(). Объектам предоставляется право распределения всех возвращенных данных, использу- использующих IMalloc. При этом данные освобождаются с помощью заглушки; когда заглушка достигает прокси, она распределяется снова через IMalloc прокси. Затем клиент обрабатывает возвращенные данные и осво- освобождает копию также с помощью IMalloc. Подключаемые объекты Объекты могут возвращать результаты клиенту при каждом вызове метода. Однако некоторые объекты могут пересылать информацию клиентам при возникновении определенных ситуаций — независимо от на- намерения клиента выполнить вызовы интерфейса. Одно возможное решение заключается в том, что, если клиент передает указатель интерфейса на объект, объект удерживает указатель (т.е. вызывает метод AddRef()) и использует его, когда необходимо. Если же клиент не хочет больше получать информацию о событиях, он останавливает этот процесс, вызывая тот же самый метод и передавая указатель NULL, который фак- фактически разрывает соединение. Объект клиента, реализующий интерфейс, называется стоком. Интерфейс называется выходом объекта или источником, поскольку объект не реализует интерфейс, а просто исполь- использует его. Соединение, установленное между стоком клиента и объектом, называется консультативным. Этот подход использовался для интерфейса lAdviseSink. При использовании подобного подхода возника- возникают некоторые неудобства. Только один клиент может получать уведомления — удовлетворительная ситуа- ситуация, если объект совместно используется некоторыми контекстами. Если объект возбуждает несколько наборов событий, то несколько методов могут быть выделены для прохождения стоков клиента. В связи с
COM Глава 21 тем что многим объектам придется возбуждать события, несколько интерфейсов будут совместно исполь- использовать один и тот же консультативный метод. Для решения этих проблем определяются два главных и два вспомогательных интерфейса. Метод IConnectionPoint обслуживает соединения одиночного выходного интерфейса для объекта. Этот метод не реализуется на основном объекте (QuerylntefaceO не обнаруживает его). Вместо этого каждая точка соединения выполняется на маленьком отдельном объекте. Для обращения к точкам соединения объект реализует интерфейс IConnectionPoint Container: [ object, uuid(B196B286-BAB4-101A-B69C-00AA00341D07), pointer_default(unique) ] interface IConnectionPoint : IUnknotm { 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] IEnumConnections **ppEnum ); } [ object, uuid(B196B284-BAB4-101A-B69C-00AA00341D07), pointer_default(unique) ] interface IConnectionPointContainer : IUnknown { HRESULT EnumConnectionpoints( [out] IEnumConnectionPoints ** ppEnum ) ; HRESULT FindConnectionPoint( [in] REFIID riid, [out] IConnectionPoint ** ppCP ) ; } Используя методы в интерфейсе IConnectionPointContainer, клиент может перечислять поддерживаемые выходные интерфейсы и присоединять стоки для тех точек соединения, которые его распознают, или могут производить непосредственный опрос об указанном выходном интерфейсе. Используя эти два способа, клиент завершает работу указателем на IConnectionPoint для объекта, который распознает указанный исходный интерфейс. Используя метод IConnectionPoint::Advise(), клиент присоединяет сток и получает дескриптор для консультативного соединения, называемого cookie. Позднее cookie используется для вызова IConnectionPoint::Unadvise(), чтобы прервать соединение. Существует метод перечисления стоков, подклю- подключенных к точке соединения. Перечислители Два интерфейса, используемые для перечисления точек соединения и соединений для указанной точ- точки, — это часть семейства интерфейсов, называемых перечислителями. Общее определение перечислителя будет следующим: interface IEnumXXX { HRESULT Next( [in] ULONG nCount, [out, size_is(nCount), length_is(*pnFetched)] XXX *pXXX, [out] ULONG *pnFetched ) ; HRESULT Skip( [in] ULONG nCount ); HRESOLT Reset(); HRESULT Clone( [out] IEnumXXX **ppEnum ) ;
Распределенные вычисления Часть V Это несуществующий интерфейс. Для каждого типа, который перечисляется, определяется новый ин- интерфейс, который использует четыре метода для частичного перечисления объектов. Метод Next() исполь- используется для выборки следующей части элементов. Метод Skip() выполняет игнорирование части элементов. Метод Reset() приводит к началу перечисления с самого начала, а С1опе() создает копию перечислителя объекта. В случае с точками соединения перечисляемые объекты являются указателями интерфейса. Они принимаются с ожидающей обработки ссылкой, поскольку клиент отвечает за освобождение каждого по- полученного указателя. Структурная память и живучесть объектов Если кокласс создается фабрикой класса, объект, как говорят, находится в неинициализированном состоянии. Одни объекты не нуждаются в дальнейшей инициализации и могут успешно действовать в этом состоянии. Другие объекты требуют инициализации из некоторого предварительно сохраненного состояния для собственной активизации. Эти объекты называются живучими. СОМ определяет два способа достиже- достижения живучести объектов: во-первых, обеспечивается стандартная модель памяти, называемая структурной памятью, а во-вторых, определяется модель живучести, которая может сопровождаться этими объектами. Структурная память сформирована на базе двух интерфейсов: 1 Storage и IStream. COM во время выпол- выполнения включает готовую реализацию для обоих интерфейсов. Эти реализации помещаются в традиционном файле и в системной памяти. Другие реализации также могут быть выполнены (например, на записях базы данных и полях). Память — это коллекция потоков или других подсистем памяти типа каталога в традици- традиционной файловой системе. Поток — это двоичная последовательность, аналогичная дисковому файлу. Поток управляется таким же способом, как файл операционной системы. Интерфейсы обеспечивают большую гибкость, чем файловая система, определяя операции транзакций и некоторые другие расширения. Объекты соответствуют модели живучести СОМ и реализуют один или большее количество интерфей- интерфейсов живучести: IPersistStorage, IPersistStream, IPersistStreamlinit или IPersistFile (другие интерфейсы жи- живучести существуют, но являются более фундаментальными). Каждый интерфейс определяет специфическую модель живучести. IPersistStorage определяет постоянство на уровне памяти и предлагает наибольшую гиб- гибкость реализации, но при этом он является самым тяжелым в реализации. Объект может создавать потоки и дополнительные подсистемы памяти. Интерфейс IPersistStream определяет живучесть в одиночном пото- потоке и является самым простым в реализации. Интерфейс IPersistStreamlnit идентичен IPersistStream, но содержит дополнительный метод для инициализации пустых объектов. Интерфейс IPersistFile используется для инициализации объекта из файла операционной системы. Всг интерфейсы живучести являются порож- порожденными от интерфейса IPersiet, который имеет единственный метод возврата объектного CLSID вызыва- вызывающему оператору. Автоматизация В мире компонентов для среднего пользователя значительно упрощается выполнение некоторых общих задач путем использования макросов или сценариев на некотором языке высокого уровня. В целях облегче- облегчения этого процесса СОМ определяет стандарт для доступа к серверам объекта СОМ, называемый автома- автоматизацией. Автоматизация определяет другой тип интерфейса, который будет называться интерфейсом диспетчеризации (dispinterface) (также на них иногда ссылаются как на интерфейсы автоматизации). Эти интерфейсы группируются вокруг объектов, реализующих интерфейс IDispatch: [ object, uuid@0020400-0000-0000-COOO-000000000046), pointer_default(unique) ] interface IDidspatch : IUnknown { HRESULT GetTypelnfoCount( [out] DINT *pctinfo ) ; HRESULT GetTypelnfо( [in] UINT iTInfo, [in] LCID lcid, [out] ITypelnfo **ppTInfo ) ; HRESULT GetIDsOfNair.es ( [in] REFIID riid, [in, size_is(cNames) ] LPOLESTR *rgszNames, [in] UINT cNames, [in] LCID lcid,
COM Глава 21 [out, size_is(cNames)] DISPID *rgDispId ); HRESULT ] [in] [in] [in] [in] [in, [out] [out] [out] Cnvoke ( DISPID REFIID LCID WORD out] DISPPARAMS VARIANT EXCEPINFO OINT dispIdMember, riid. lcid, wFlags, ¦pDispParams, ¦pVarResult, *pExcepInfо, *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(). СОМ-интерфейсы используют раннюю схему связывания, поскольку клиент осуществляет привязку к интерфейсу 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. Использование СОМ-объектов в C++ Использование СОМ-объектов в C++ почти идентично использованию классов C++ посредством ука- указателей. Различия начинаются лишь тогда, когда речь идет о времени жизни интерфейсов.
Распределенные вычисления Часть V Использование интерфейсов Raw Создание объекта (который демонстрирует интерфейс) происходит в первую очередь. Существуют раз- различия между классами C++ и интерфейсами. Каждый интерфейс реализован на некотором объекте. Объект также отображает другие интерфейсы. Для того чтобы получить другой интерфейс, клиент осуществляет запрос Querylnterface(), обращаясь к указателю интерфейса для получения указателя на другой интерфейс на том же самом объекте. Этот подход подобен реализации оператора dynamic_cast в языке C++. Другое различие состоит в механизме разрушения. Вместо использования оператора удаления клиент вызывает метод Release(), который передает инструкции объекту о его саморазрушении (некоторые классы C++ спроекти- спроектированы таким же образом). Если объект находит, что это была последняя ожидающая обработки ссылка (для всех интерфейсов), он уничтожает себя. Ниже находится пример реализации этого механизма: HKESULT hr; ICar *pCar; IEngine *pEngine; hr = Colntialize( NULL ); assert( SUCCEEDED( hr ) ) ; hr = CoCreatelnstance( CLSID_Car, NULL, CLSCTX_ALL, IID_ICar, (void**)&pCar ) ; if ( SUCCEEDED ( hr ) ) { hr = pCar->QueryInterface( IID_IEngine, (void**)SpEngine ); if ( SUCCEEDED ( hr ) ) { pEngine->Start(); Drive( pCar ); pEngine->Stop() ; pEngine->Release() ; > pCar->Release() ; > CoUnitialize () ; Этот фрагмент программного кода инициализирует СОМ-библиотеку, создает объект Саг (Саг — это кокласс) и получает интерфейс ICar. Затем выдается запрос об интерфейсе IEngine, запускается двигатель (engine), вызывается некоторая функция для управления автомобилем и в конце останавливается двига- двигатель. В заключение, когда и IEngine, и интерфейсы ICar освобождены, СОМ-библиотека также освобожда- освобождается. Вторичное освобождение оставляет объект без ожидающих обработки ссылок для всех интерфейсов, вследствие чего объект самоуничтожается. Все это очень просто и не требует дальнейшего объяснения. В целях упрощения результаты, возвращенные метода, не проверяются. На практике же необходимо прове- проверять эти результаты. Даже если гарантируется, что метод не потерпит неудачу, программный код мог бы претерпеть неудачу при реализации в силу разных обстоятельств (включая сетевые отказы). Более интересным является то, что функция drive() в соответствии с законами СОМ должна вызывать метод AddRef() при получении указателя и Release() — по окончании работы с ним. Ясно, что в этом нет необходимости, поскольку время существования параметра интерфейса указателя равно времени существо- существования вызова функции и является подмножеством времени существования контекста вызова, который включает ссылку на интерфейс. Следовательно, во время выполнения функции интерфейс содержит поло- положительную ссылку и объект не удалит себя. Благодаря этому может быть выполнена оптимизация и пара методов AddRef()/Release() может быть опущена. Во всех случаях, когда возможен выход за пределы логи- логики программирования, переменная, которая получает копию интерфейса, имеет время существования, которое является подмножеством времени существования первоначальной переменной, хранящей ссылку интерфейса (время существования определяется временем инициализации, пока не вызывается заключи- заключительная функция Release()). При этом оптимизация разрешена. Если начальная переменная имеет более короткое время существования, то копия содержит недопустимый указатель и оптимизация не может быть выполнена. Обычно методы AddRef() и Release() очень просты в реализации, поэтому оптимизация необ- необходима достаточно редко, за исключением тех случаев, когда быстродействие очень важно. При передаче локального указателя интерфейса другим функциям можно безболезненно опустить пару AddRef()/ReIease(). Если указатель интерфейса содержится в глобальной переменной и процесс имеет единственный поток выполнения, который использует эту переменную, то риска при этом также не возникает.
COM Глава 21 Использование интеллектуальных указателей Время существования любого указателя интерфейса должно начинаться с AddRef() — либо когда полу- получена любая внешняя функция или метод, либо когда выполнена локальная копия (в явном виде). Время существования должно заканчиваться при вызове функции Release(). Можно легко забыть о необходимости вызова AddRef() или Release() или вызывать Release() больше одного раза. Последствия этого часто быва- бывают печальными, к тому же становятся затруднительными отладка и локализация ошибки. Поэтому лучше скрыть эти действия в классе обертки интерфейса. Этот класс в соответствующее время автоматически вызовет AddRef() и Release() для содержащего указатель интерфейса. Класс обертки в конце концов вызовет метод Release() в деструкторе. Класс этой категории — это интеллектуальный указатель класса. Интеллектуальный указатель для пользо- пользователя выглядит как и обычный указатель. Отличие заключается в возможности управления сроком службы содержащегося указателя интерфейса. Любой вызов метода, доступный через первоначальный указатель, доступен также через интеллектуальный указатель. Эта ситуация легко реализуется с помощью перекрытия оператора класса ->. Поскольку интеллектуальный указатель не может различать вызовы методов AddRefO и Reiease() через указатель, программист должен подчиняться некоторым правилам и не вызывать AddRefO и Release() непосредственно через содержащийся указатель. Вместо этого интеллектуальный указатель дол- должен поддерживать методы AddRefO и Release(), которые позволят программисту непосредственно управ- управлять временем существования содержащегося указателя. В листинге 21.2 показан пример интеллектуального указателя класса. Листинг 21.2. Простой интеллектуальный указатель класса template<class Itf> class CSmartPtr { // Конструктор и деструктор public: CSmartPtr() : m__pltf( null ) < } CSmartPtr ( Itf *pltf ) : m__pltf( null ) { Store( pltf ); } --CSmartPtr () { // Освобождение указателя интерфейса, если он не пустой Release () ; > // Экстракторы public: // Получение базового указателя для вызовов методов Itf* operator->() { assert( m_pltf != null ); return m_pltf; > // Получение адреса основного указателя Itf** operators() { // Убедитесь, что предыдущий указатель освобожден Release () ; return im_pltf; ) // Присваивание public: Itf* operator=( Itf *pltf ) { // Убедитесь, что предыдущий указатель освобожден
Распределенные вычисления Часть V Release () ; // Сохранение нового указателя Store( pltf ); return m pltf; } // Тестирование контекста public: bool operator! () { return ( mjpltf == null ) ; } operator bool() { return ( m_pltf != null ) ; } // Реализация protected: void Release () { if ( m__pltf != null ) { m_pItf->Release(); m_pltf = null; void Store( Itf *pltf ) { // Сохранение нового указателя m__pltf = pltf; // Call AddRef on the new pointer, check for null if ( m__pltf •= null ) { m_pItf->AddRef() • // Элементы реализации protected: Itf *m__pltf; Это самая простая форма интеллектуального указателя. Он обеспечивает присваивание из другого указа- указателя того же самого типа и получение адреса содержащегося указателя для назначения необработанного указателя, возвращенного функцией или методом. При этом учитывается определение интеллектуального указателя, поскольку обработка AddRefQ и Release() происходит автоматически. Кроме того, поддержива- поддерживается оператор ->, необходимый для обеспечения доступа к методам в содержащемся указателе. Добавляют- Добавляются также операторы тестирования, определяющие, содержит ли класс указатель интерфейса. Конечно, при этом также может использоваться конструктор копии. Проблема с интеллектуальным указателем, показанным в листинге 21.2, состоит в том, что он всегда нуждается в вызовах QuerylnterfaceO или CoCreateInstance() (или некоторых других средств), необходи- необходимых для заполнения значений: CSroartPtr<IMyInterface> pMylnterace; HRESULT hr ; hr = pSomeInterface->QueryInterface( IID_IMyInterface, (void**)SpMyInterface ) ; Будет лучше, если интеллектуальный указатель также будет обрабатывать Querylnterface(). Проблема зак- заключается в том, что QuerylnterfaceO возвращает HRESULT в случае неудачи. Интеллектуальный указатель должен принять решение, как выйти из этой ситуации. Самое очевидное решение — использовать исклю- исключение, но при этом появляется проблема, связанная с тем, что семантика оператора = не подразумевает обработку катастрофического отказа. Лучше всего проигнорировать отказ и предоставить программисту воз-
COM Глава 21 можность проверки каждого присваивания с помощью operator! или оператора bool. Конечно, класс дол- должен запоминать последний HRESULT, что позволит программисту идентифицировать причину отказа. В листинге 21.3 содержится предложенное добавление. Листинг 21.3. Дополнения к интеллектуальному указателю класса /* в конструкторах */ CSmartPtr( IUnknown *pOnk ) : m_pltf( null ), m_hr( E_POINTER ) { StoreOnk( pUnk ) ; > /* в экстракторах */ operator IUnknown*() { return m_pltf; } /* в присваивании */ Itf* operator=( IUnknown* pUnk ) < // Убедитесь, что предыдущий указатель освобожден ReleaseO ; // Сохранение нового указателя StoreOnk( pUnk ) ; return m_pltf; } /* при реализации */ void StoreUnk( IUnknown *pUnk ) { if ( pUnk != null ) { m_hr = pUnk->QueryInterface( IID_##Itf, (void**)&m_pltf ) ; } else { m_hr = E_POINTER; /* в контексте тестирования */ HRESULT GetLastErrorO { return m_hr; } /* в элементах реализации */ HRESULT m_hr; Другие конструкторы должны инициализировать элемент m_hr;- другой оператор присвоения должен установить его. Оператор Store() заканчивается этой строкой: m_hr = (m__pltf ! = NULL ) ? S_OK : E_POINTER; В целях непротиворечивости метод ReleaseO должен завершить оператор if следующей строкой: m_hr = Е_РОINTER; 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; // Querylnterface here if ( (bool)pEngine ) { pEngine->Start(); Drive( pCar ); pEngine->Stop() ; } } CoUninitializeO ; Обратите внимание, что вызов CoCreateInstance() не изменился. Это потенциально опасно, поскольку IID может быть неуместным. Для того чтобы избежать этой проблемы, можно добавить новый метод — CreatelnstanceO, как продемонстрировано в листинге 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**)Sm_pltf ) ; return m_hr ; } При использовании метода CreatelnstanceO фрагмент программного кода еще больше упрощается: CSmartPtr<ICar> pCar; CSmartPtr<IEngine> pEngine; hr = Colntialize( NULL ); assert( SUCCEEDED( hr ) ) ; pCar.Createlnstance( CLSID_Car ) ; if ( (bool)pCar ) ( pEngine = pCar; // Здесь находится Querylnterface if ( (bool)pEngine ) { pEngine->Start(); Drive( pCar ); pEngine->Stop() ; ) ) CoUninitializeO ; В этом фрагменте синтаксис, осуществляющий вызов метода Drive(), изменяется — передается интел- интеллектуальный указатель вместо ожидаемого необработанного указателя. Конечно, программный код может
COM Глава 21 быть переписан для использования интеллектуальных указателей, но это не всегда возможно (например, если он был написан посторонним человеком и отсутствует текст исходного кода). Можно было бы сохра- сохранить параметр, используя 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; } operator IUnknown*() { CheckPointer () ; return m_pEngine; } // Информация о статусе (состоянии) public: HRESULT GetLastStatus() { return m_hr; } // Присваивание public:
Распределенные вычисления Часть V void Createlnstance( REFCLSID rclsid, DWORD dwClsContext = CLSCTX_ALL, IUnknown *p0nkOuter = NULL ) { Release(); CheckError( : :CoCreatelnstance( rclsid, pUnkOuter, dwClsContext, IID_IEngine, (void**)Sm_pEngine ) ) ; } IEngine* operator=( IUnknown *pUnk ) { // Убедитесь, что предыдущий указатель освобожден Release(); //- Сохранение нового указателя Store( pUnk ); return m_pEngine; } // Методы интерфейса public: void Start () { CheckPointer () ; CheckError( m_pEngine->Start() ) ; } void Stop() { CheckPointer(); CheckError( m_pEngine->Stop() ); > // Реализация protected: void Store( IUnknown *pUnk ) { Release () ; if ( pUnk != null ) { CheckError( pUnk->QueryInterface( IID_IEngine, (void**)Sm_pEngine ) ) ; ) else { CheckError( E_POINTER ); void Release () { if ( !IsEmpty() ) { m_pEngine->Release(); m_pEngine = null; m_hr = S_OK ; bool IsEmptyO { return ( m_pEngine == null ) ; } void CheckPointer() { if ( IsEmptyO ) {
COM Глава 21 CheckError( E_POINTER ); } } void CheckError{ HRESULT hr ) { m_hr = hr ; if ( FAILED( hr ) ) { throw hr; // Элементы реализации protected: IEngine *m_pEngine; HRESULT m_hr ; }; Большая часть этого программного кода подобна реализации интеллектуального указателя с семанти- семантической разницей, когда используются исключения вместо обеспечения способа проверки успешного вы- выполнения или неудачи. Метод GetLastStatus() необходим для того, чтобы различать большое количество кодов успешного завершения в том случае, когда не используется исключение. Как видно из примера про- программного кода, только небольшая его часть специфична для интерфейса. Оставшаяся часть стандартна и может быть создана с помощью шаблона. Исходя из предложения, что существует подобная обертка для ICar, фрагмент, использующий описанный механизм, будет выглядеть так: CCarPtr CarPtr; CEnginePtr EnginePtr; HBESULT hr; hr = Colntialize( NULL ); assert( SUCCEEDED( hr ) ) ; try { CarPtr.Createlnstance( CLSID_Car ); EnginePtr = CarPtr; // здесь находится Querylnterface EnginePtr.Start(); Drive( CarPtr ) ; EnginePtr.Stop(); } catch ( HRESULT hr ) { } CoUninitialize(); Обертки могут быть выполнены вручную, но это утомительная и неблагодарная работа. Было бы лучше, если бы некоторый инструмент получал описание интерфейса и производил класс обертки. Поскольку IDL- файлы интерфейсов, определенных сторонними разработчиками, вообще недоступны и к тому же header- файлы C++ не содержат достаточной информации для описания всех параметров, необходимо предложить другой способ, помогающий обнаружить специальные сообщения интерфейса. В этом случае используются библиотеки типов. Библиотеки типов включают описание IDL интерфейса в двоичной форме — так чтобы они были меньше по объему и лучше распространялись. Еще одно преимущество библиотек типов заклю- заключается в том, что они описывают интерфейсы диспетчеризации, что позволяет также генерировать для них обертки. Дальнейшее обсуждение специфично для компилятора Visual C++ 5.0. Компилятор реализует расшире- расширение Microsoft для препроцессора — директиву #import: fimport <type_lib> no_namespace Параметр no_namespace используется для того, чтобы подавить генерирование отдельного пространства имен для определений библиотеки типов. Несколько других опций управляют поведением директивы #import. Если препроцессор обнаруживает #import в исходном программном коде, он просматривает вызванную библиотеку типов и генерирует два файла с расширениями .tlh и .tli (эти расширения замещают заголовок библиотеки типов и реализацию библиотеки типов). Здесь же содержится заголовок для сгенерированного класса; последний содержит реализацию методов обертки. Любые другие детали лежат вне контекста этого обсуждения. Сгенерированные средства оберток в существующем шаблоне интеллектуального указателя имеют семантику исключения. Все оборачивающие методы также генерируют исключения при возникновении отказа.
Распределенные вычисления Часть V Создание СОМ-объектов в C++ Следующие разделы этой главы посвящены созданию объектов СОМ. Здесь не рассматривается программ- программный код шаблона, необходимый для создания СОМ-сервера, который будет полностью работоспособным. Представлено несколько стратегий, которые могут быть использованы для поддержки объектов СОМ. Множественное наследование Компилятор IDL генерирует определения интерфейса, подобные классам C++, содержащим только чисто виртуальные методы. Естественно будет реализовать объекты СОМ с классами C++, которые наследуют определения отображаемых интерфейсов, как показано в листинге 21.6. Листинг 21.6. Определение класса C++, реализующего объект СОМ с помощью метода множественного наследования из реализованных интерфейсов class CCar : public ICar, public IEngine { // Конструктор и деструктор public: CCar(); -CCar () ; // Методы IUnknown public: STDMETHOD( Querylnterface ) ( 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::Querylnterface( REFIID riid, void **ppv )
COM Глава 21 HRESULT hr = S_OK; // Заметьте, что IEngine выделен для запросов 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 ( mJbEngineStarted ) { if ( nSpeed >= 0 ) { m_nSpeed = nSpeed ; } else { hr = E_INVALIDARG; } ) else < hr = CAR_E_POWER; ) return hr; } STDMETHODIMP CCar::S tart() { HRESULT hr = S_OK; if ( 'mJbEngineStarted ) { mJbEngineStarted = true; m_nSpeed = 0; } else { hr = CAR_EJ?OWER; ) return hr; } STDMETHODIMP CCar::Stop() { HRESULT hr = S OK;
Распределенные вычисления 4l!J~~4^CTb~V if ( m_bEngineStarted ) { if ( m_nSpeed == 0 ) { m_bEngineStarted = false; } else { hr = CAR_E_SPEED; } else { hr = CAR_E_POWER; return hr; При использовании метода множественного наследования все интерфейсы разделяют общий подсчет ссылок. Поскольку методы части IUnknown для всех интерфейсов реализованы в классе, все интерфейсы имеют единственную реализацию этих методов в своих VTBL. Когда все интерфейсы объекта освобождены, объект самоуничтожается. Реализация, показанная в листинге 21.7, не имеет безопасного потока. Если не- несколько потоков выполняют методы AddRef() и Release() одновременно, появляется элемент соревнова- соревновательности и объект может быть разрушен преждевременно. Однако можно просто изменить эту реализацию, чтобы создать безопасный поток. Модифицированное множественное наследование должно реализовывать специфические для интерфей- интерфейса методы для всех интерфейсов на отдельных классах и иметь объектный класс, порожденный от классов реализации. Методы интерфейса IUnknown должны быть реализованы в объектном классе. Этот подход не- необходим, если два интерфейса совместно используют методы с одинаковым определением, но нуждаются в различной реализации. Недостаток множественного наследования заключается в том, невозможен под- подсчет ссылок для каждого интерфейса в отдельности. Положительный момент состоит в том, что необходи- необходимость в этом возникает редко. Вложенные классы При другом подходе реализация каждого интерфейса осуществляется в отдельном классе. Эти вспомога- вспомогательные классы обычно определяются внутри объектного класса, который включает переменные-члены для каждого из вспомогательных классов. Объектный класс реализует методы IUnknown. При реализации вло- вложенных классов в IUnknow выполняется простое делегирование к реализации объекта 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 () // Методы IUnknown public: STDMETHOD( Querylnterface )( REFIID riid, void **ppv ) STDMETHOD_( ULONG, AddRef ) () ; STDMETHOD_( ULONG, Release ) () ; // Методы ICar public: STDMETHOD( SetSpeed ) ( long nSpeed ) ;
COM Глава 21 // Реализация protected: CCar *m_pOwner; }; // Реализация lEngine // Конструктор и деструктор public: CCar О; -CCar () ; // Методы lUnknown public: STDMETHOD( Querylnterface )( REFIID riid, void **ppv ) STDMETHOD_( OLONG, AddRef ) (); STDMETHOD_( ULONG, Release ) () ; // Вложенные классы protected: XCar m_ICar; XEngine m_IEngine; // Реализация protected: OLONG m_nRef; bool inJaEngineStarted; long m_nSpeed; Каждый вложенный класс хранит указатель на экземпляр основного объекта и делегирует ему вызовы lUnknown. Данные, которые являются обшими множественными интерфейсами, также размещены там. Наш пример не очень хорошо подходит для иллюстрирования вложенной реализации; он предлагает только объяснение управления вложенными классами. Реализация методов ICar и lEngine опущена. В листинге 21.9 показан пример реализации. Листинг 21.9. Реализация класса C++ для объекта СОМ с помощью вложенных классов STDMETHODIMP CCar::XCar::Querylnterface( REFIID riid, void **ppv ) return m_pOwner->QueryInterface( riid, ppv ) ; } STDMETHODIMP_(OLONG) CCar::XCar::AddRef() { return m_pOwner->AddRef(); } STDMETHODIMP_(OLONG) 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::QueryInterface( REFIID riid, void **ppv ); { HRESULT hr = S_OK; // Обратите внимание, что lEngine выделен для запросов IUnknow if ( IsEqualIID( riid, IID_IUnknown ) ) { *ppv = (void*)static_cast<IUnknown*>( this ); } else if ( IsEqualIID( riid, IID_IEngine ) ) { *ppv = (void*)static_cast<IEngine*>( Sm_IEngine ) ; } else if ( IsEqualIID( 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; } Подход с использованием вложенных классов подобен модификации подхода с множественным насле- наследованием, когда используются базовые классы реализации интерфейса. Основное различие заключается в том, что вложенные классы реализуют методы lUnknown, в то время как классы реализации этого не де- делают. Реализации вложенных классов методов lUnknown делегируют к реализации объекта lUnknown. Реали- Реализация Querylnterface() изменяется, чтобы осуществить поиск реализации интерфейса из переменных-членов, в то время как lUnknown имеет отдельную реализацию в базовом классе. Можно объединить этот подход с методом множественного наследования и реализовать несколько ин- интерфейсов на одном вложенном классе. Каждая группа интерфейсов вложенного класса будет иметь един- единственную реализацию lUnknown, делегирующую к основному объекту при использовании множественного наследования для реализации интерфейсов. Использование классов tear off Существуют случаи, когда некоторый интерфейс объекта редко используется, но потребляет большое количество ресурсов. Предпочтительно избегать пустой траты ресурсов, если интерфейс никогда не будет использован. Подходы к реализации, рассматриваемые до сих пор, не могут решать проблему непроизводи- непроизводительных затрат ресурсов, поскольку распределяются все объектные ресурсы. Решение заключается в исполь- использовании так называемого интерфейса tear-off. Если интерфейс запрашивается впервые, при реализации Querylnterface() динамически создает экземпляр класса, который реализует интерфейс. Элементы класса lUnknown делегируют к lUnknown основного объекта точно так же, как и в случае с вложенными классами. Поддерживается отдельный подсчет для интерфейса tear-off; когда интерфейс освобождается в последний раз, класс tear-off, в свою очередь, разрушается. Реализация классов tear-off почти идентична реализации, используемой для вложенных классов, и здесь не рассматривается. Отличие заключается в том, что вместо элементов объектный класс хранит указатели
COM Глава 21 на классы tear-off и дополнительный подсчет ссылок на каждый элемент. Изменены методы Querylnterface() и Release(). Другой подход должен позволить классу tear-off реализовать собственную ссылку и очистить объектный указатель в ходе выполнения операции удаления внутри собственной реализации Release(). Резюме В этой главе были рассмотрены наиболее важные вопросы, касающиеся технологии СОМ. Многие не- незначительные детали были опущены. Книги, перечисленные в разделе "Дополнительная литература", помо- помогут вам расширить свои познания о СОМ и дадут возможность рассмотреть широкий спектр технологий СОМ. Эта глава посвящена подробностям использования и реализации объектов СОМ. Даже если вы знали о СОМ ранее, автор надеется, что сможете найти кое-что новое на этих страницах. Дополнительная литература В качестве трамплина для перехода от C++ к СОМ, а также для того, чтобы узнать все подробности реализации ядра СОМ обратитесь к книге Дона Бокса (Don Box) Essential COM. Кроме того, первые све- сведения о СОМ вы можете почерпнуть из книги Дейла Роджерсона (Dale Rogerson) Inside COM. Для тех, кто знаком с основами СОМ, предназначена книга издательства Macmillan Publishing COM/DCOM Unleashed. Классическая книга по этой тематике — Inside OLE Крейга Брокшмидта (Kraig Brockschmidt). В ней рас- рассматривается в деталях спецификация первой реализации СОМ: связывание и внедрение объектов (OLE).
Java и C++ В ЭТОЙ ГЛАВЕ Общие черты C++ и Java Различия между C++ и Java Объектно-ориентированные возможности Java
Java и C++ Глава 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 IOException * @author: Some Java Developer * (Aversion: 1.0 */ public void sleepMethod( ) thows IOException При выполнении утилиты 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 1 байт (8 битов) boolean 1 байт (8 битов) char 2 байта Unicode A6 битов, целое) short 2 байта A6 битов, целое) int 4 байта C2 бита, целое) long 8 байтов F4 бита, целое) float 4 байта C2 бита, целое) double 8 байтов F4 бита, целое) Каждый тип примитивных данных в 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; // неправильно! Java не позволяет этого 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++. Она является переменной, которая осуществляет связь с некоторым объектом, отличным от исход- исходного.
Java и C++ Глава 22 Рассмотрим пример, написанный на языке C++: int value = 5; int S rv = value; В этом примере rv фактически ссылается на value. Можно осуществить обращение к содержанию по имени value, на которое ссылается rv. Нечто подобное происходит в языке Java. Следующий пример является примером ссылки Java. (Некото- (Некоторые программисты, работающие на языке Java, называют ссылки дескрипторами.) String str = new String () ; System.out.println (" 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.println( "anlnt before is:" + anlnt ) ; anlnt = anlnt >» 1 ; System.out.println( "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.println( "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( ) ;
Java и C++ Глава 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. :: Доступ определяется не только к каждому индивидуальному элементу, но и для класса. Точка с запятой не ставится после заключительной фигурной скобки класса. В языке 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.println("l. theValue is: " + theValue ) Goofy g = new Goofy ( ) ; g. doubleArg( theValue ) ; System.out.println(. theValue is: " + theValue ) В результате выполнения этого приложения выводится следующее: 1. theValue: 5 2. theValue : 5 Этот программный код показывает, что функция-член doubleArg() просто получает копию, а не ссылку на исходный объект 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.printlnC'l. theValue is: " + theValue. intValue ) ; Goofy g = new Goofy ( ) ; g.doubleArg( theValue ) ; System.out.println(. theValue is: " + theValue.intValue ) ; Вот результат работы этого приложения: 1. theValue: 5 2. theValue: 10 В заключение — одно замечание: массивы являются объектами первого класса в Java. В предыдущем при- примере, если theValue — целочисленный массив, то метод doubleArg() заменит фактический параметр на ар- аргумент. Внешние функции В Java каждая функция должна быть элементом класса. Нет каких-либо глобальных функций. Перечислители В языке C++ ключевое слово enum доступно при определении объекта, содержащего перечисляемые значения. Java не содержит ключевого слова enum, но можно его смоделировать, создавая класс, который
Java и C++ Глава 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 предлагается класс StringBufTer. Хотя в Java отсутствует перегрузка оператора, класс String предлагает оператор + для конкатенации объектов Strings. He забывайте, что объекты 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 array3[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 ; } Сразу же необходимо сделать три замечания об особенностях этого примера: спецификатор доступа применяется к классу и элементам класса, функции определены внутри объявления класса, и нет точки с запятой после заключительной фигурной скобки. Как упоминалось выше, можно применить четыре спецификатора доступа: 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( ) ;
Java и C++ Глава 22 public final long HowLong = 5000 ; // длинное целое > В приведенном выше примере содержится объявление того, что Silly — класс интерфейса. Нельзя обра- образовать объект Silly, как это сделано в следующем примере: / /. . - 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 ){/*...*/} 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 ? = new Joke( ) ; f.laugh( ) ; ?.smile ( ?.HowLong ) ; } > Обратите внимание на то, что нет необходимости в определении функции-члена smile(). В контексте класса Joke очевидно, что реализация sraile() внутри Funny является адекватной. Можно, однако, пере- перекрывать функции-члены, определенные внутри абстрактного класса. Следующий пример показывает, ка- каким образом происходит перекрытие функции-члена: public class Hilarious extends Funny { public void laugh( ){/*...*/} public void smile( long timeSpan ){/*...*/}// перекривив public static void main( String args[] ) { Hilarious ? = new Hilarious ( ) ; f.laugh( ) ; f.smile( ?.HowLong ) ; } ) В предшествующем примере перекрывается функция-член sraile(). На этом этапе может возникнуть воп- вопрос: "Каким образом можно вызвать функцию-член суперкласса?" Ответ можно получить с помощью клю- ключевого слова 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, рассмотрим, как это применяется на практике:
Java и C++ Глава 22 public class Goofy { public void laugh( ){/*...*/} static final int value =10 ; private boolean notLaughing ; private String name ; } В предшествующем примере переменная value не должна быть инициализирована вне класса, как требу- требуется в C++. Следуя правилам языка Java, все должно существовать внутри класса. Экземпляр переменной notLaughing (boolean) по умолчанию устанавливается в значение false. Переменная-член name объявляется как ссылка на 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 int ivall = 10 ; // явно private int ival2 ; // гарантированная инициализация private int ival3 ; // выполняется в конструкторе } Метод 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.
Java и C++ Глава 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 ex-tends Goofy { Silly( ) { super( ) ; } Silly( String name ) { } Silly( String name, int value ) < this( name ) ; > public void publicFunc( ) { super.publicFunc( ) ; } public int publicValue ; } Функция-член pub!icFunc() в 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 ){/*...*/} public void pause( ) {/*...*/}
Java и C++ Глава 22 Обработка исключений Механизм обработки исключений в Java очень похож на механизм, используемый в C++. В Java добавля- добавляется новое ключевое слово к средствам обработки исключений: finally. Блок finally всегда выполняется пост ле выполнения всех блоков try и catch. Любой объект, который попал в Java, должен быть унаследован от класса Throwable. Рассмотрим пример: public void func( ) throws IOException { IraportantObject 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 e ) //... catch(Throwable) может использоваться как "весь 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 1. Вставьте CD-ROM в CD-ROM-дисковод. 2. В окне File Manager или Program Manager в меню File выберите команду Run. 3. В появившемся окне введите <flHCK>\START.EXE, где <диск> соответствует букве CD-ROM-дисково- CD-ROM-дисковода, и нажмите клавишу Enter. 4. Следуйте появляющимся на экране монитора указаниям, чтобы завершить процесс установки. Указания по установке в Windows 95/98 и NT4 1. Вставьте CD-ROM в CD-ROM-дисковод. 2. Дважды щелкните на пиктограмме My Computer на рабочем столе. 3. Дважды щелкните на пиктограмме, соответствующей CD-ROM-дисководу. 4. В появившемся окне дважды щелкните на пиктограмме файла START.EXE. 5. Следуйте появляющимся на экране монитора указаниям, чтобы завершить процесс установки. ПРИМЕЧАНИЕ Для Windows 95/98 и NT4 можно активизировать функцию Ayto Play, что позволит запускать на выполнение START.EXE после вставки CD-ROM в CD-BOM-дисковод. ..,•''. . Указания по установке в среде UNIX Для пользователей UNIX предлагаются специфические указания по установке этой версии программы из CD-ROM. Обратитесь к администратору UNIX, чтобы получить помощь для применения команд по ус- установке. ПРИМЕЧАНИЕ Перед подключением CD-ROM командой <mountpoint>, убедитесь в том, что она существует или работоспособна. Если операционная система работает, то подключение будет выполнено автоматически. Для установки выполните следующие действия: 1. Вставьте CD-ROM в CD-ROM-дисковод. 2. Подключите CD-ROM, используя команду mount [options] dev/cdrom /<mountpoints> 3. Создайте каталог, например cppunl вводом команды mkdir /cppunl 4. Перейдите в созданный каталог cd /eppunl 5. Теперь вы готовы к копированию программного кода. Для копирования источников кода из CD-ROM в локальный каталог введите ср -г /<mountpoints/source/*.