Текст
                    I
ufh п о।p: ‘.'ирлван 
Стандартная
библиотека
на примерах
Научитесь использовать строковые
объекты, потоки и средства STL
при разработке собственных приложений
Пабло Халперн
W PUE

Г1РИЯ П₽А1РЯММНР0ВДИЙЯ Стандартная библиотека С+4 примерах Пабло Халперн Издательский дом “Вильямс" Москва • Санкт-Петербург • Киев 2001
ББК 32.973.26-018.2я75 Х17 УДК 681.3.07 Издательский дом “Вильямс” Перевод с английского О. И. Ревы По общим вопросам обращайтесь в Издательский дом “Вильямс” по адресу: info@williamspublishing.com, http://www.williamspublishing.com Халперн» Пабло. XI7 Стандартная библиотека C++ на примерах. : Пер. с англ. : — М. : Издатель- ский дом “Вильямс”, 2001. — 336 с. : ил. — Парал. тит. англ. ISBN 5-8459-0154-5 (рус.) В большинстве книг, посвященных программированию, основное внимание уделяется изучению синтаксиса языка, средств и приемов написания программ, а в качестве примеров приводятся довольно простые проекты. Эта книга построена совсем иначе. В ходе работы над проектом TinyPIM автор шаг за шагом проведет вас через все фазы разработки объектно-ориентированного приложения на основе классов и функций стандартной библиотеки C++. Будут рассмотрены вопросы по- становки задач, анализа, выбора оптимальных стандартных средств программиро- вания и реализации проекта с проверкой роботоспособности приложения. Цель книги состоит не только в том, чтобы познакомить вас с большинством средств стандартной библиотеки C++, но и в том, чтобы объяснить основные концепции, положенные в основу разработки стандартных шаблонов классов и функций, а также научить правильно выбирать необходимые средства для дос- тижения максимальной эффективности и устойчивости к ошибкам создаваемых приложений. Книга рассчитана главным образом на начинающих программистов, желаю- щих познакомиться со стандартной библиотекой C++, но она также будет инте- ресна и профессиональным программистам, которые перешли к C++ от С или других языков программирования. ББК 32.973.26-018.2-75 Л Все названия программных продуктов являются зарегистрированными торговыми марками соответствующих фирм. Никакая часть настоящего издания ни в каких целях не может быть воспроизведена в ка- кой бы то ни было форме и какими бы то ни было средствами, будь то электронные или меха- нические, включая фотокопирование и запись на магнитный носитель, если на это нет пись- менного разрешения издательства Que Corporation. Authorized translation from the English language edition published by Macmillan Computer Publishing, Copyright © 2000 All rights reserved. No part of this book may be reproduced or transmitted in any form or by any means, electronic or mechanical, including photocopying, recording or by any information storage retrieval system, without permission from the Publisher. Russian language edition published by Williams Publishing House according to the Agreement with R&I Enterprises International, Copyright © 2000 ISBN 5-8459-0154-5 (рус.) © Издательский дом “Вильямс”, 2001 ISBN 0-7897-2128-7 (англ.) © by Pablo Halpern, 2000
Оглавление Вступление 14 Глава 1. Знакомство с TinyPIM 25 Глава 2. Реализация класса Address для работы с текстовыми строками 38 Глава 3. Создание адресной книги с помощью контейнера vector 69 Глава 4. Альтернативная реализация адресной книги с помощью контейнера list 89 Глава 5. Редактирование записей адресов с помощью функций класса String и операторов ввода-вывода 109 Глава 6. Усовершенствование адресной книги с использованием алгоритмов и сортированных контейнеров 137 Глава 7. Прокручивание экранных списков с помощью двухсторонних очередей и потоков ввода-вывода 172 Глава 8. Простая система меню 205 Глава 9. Классы даты и времени с пользовательской системой ввода-вывода 239 Глава 10. Сборка блоков программы 268 Глава 11. Научитесь профессионально работать со стандартной библиотекой C++ 321 Предметный указатель 323
Содержание Вступление 14 Что особенного в этой книге 14 В изучении стандартной библиотеки C++ нет ничего сложного 14 Научитесь выбирать наиболее подходящие средства программирования 15 Для чего нужна стандартная библиотека C++ 15 Что представляет собой стандартная библиотека C++ 16 Какова разница между стандартной библиотекой C++ и стандартной библиотекой шаблонов 19 Наш рабочий набор инструментов 20 Какой компилятор вам подойдет * 21 Где можно достать стандартную библиотеку 22 Соглашения, используемые в книге 23 Глава 1. Знакомство с TinyPIM 25 Объектно-ориентированный анализ проекта 26 Формулирование требований к проекту 26 Анализ ситуаций использования 27 Планирование классов 29 Двухуровневый логический анализ 29 Краткий вводный курс в UML 30 Функциональное ядро TinyPIM 31 Блок текстового пользовательского интерфейса 32 Блок функций сохранения 36 Можно ли уже приступать к написанию кода 36 Глава 2. Реализация класса Address для работы с текстовыми строками 38 Использование строк с фиксированной длиной 38 Объявление класса Address 38 Использование массивов в буфере динамического распределения памяти 51 Использование указателей на динамически распределяемую память 52 Утечка памяти и повторное удаление 56 Самоприсвоеиие 57 Конструктор-копировщик и оператор присваивания 58 Обобщение: использование динамически выделяемых строк 59 6 Содержание
Упрощение реализации класса Address с помощью библиотечного класса string 60 Общие представления о классе string 60 Естественный интерфейс 63 Автоматическое управление памятью 63 Использование программы тестирова! !ия с новым интерфейсом класса Address 63 От строковых объектов назад к строкам с нулевыми окончаниями 65 Резюме 68 Глава 3. Создание адресной книги с помощью контейнера vector 69 Добавление в класс Address целочисленных идентификационных номеров 69 Класс AddressBook 71 Реализация класса AddressBook с помощью вектора 72 Реализация свойств простейшего контейнера с помощью массива 73 Объявление вектора объектов Address 74 Использование вектора как расширяемого массива 78 Доступ к элементам вектора 81 Удаление записей адресов 82 Завершение реализации класса AddressBook 83 Проверка работы программы 84 Резюме 87 Глава 4. Альтернативная реализация адресной книги с помощью контейнера list 89 Общие представления о контейнере list 90 Реализация класса AddressBook с использованием списка 91 Перемещение по списку с помощью итераторов 92 Удаление элементов с помощью итераторов 95 Редактирование элементов с помощью итераторов 96 Использование итераторов в константных контейнерах 96 Поддержание списка в алфавитном порядке 103 Добавление в класс Address операторов отношений 103 Ввод объектов Address в алфавитном порядке 105 Резюме 108 Глава 5. Редактирование записей адресов с помощью функций класса String и операторов ввода-вывода 109 Определение требовании к редактору 109 Базовый класс редактора 110 Содержание 7
Вывод приглашений пользователю 112 Прием данных, вводимых пользователем, и контроль за ошибками 116 Добавление и замена отдельных строк в многострочной записи 119 Редактирование объекта Address с помощью интегрированного редактора 123 Новое требование к программе: форматированный номер телефона 129 Анализ набора символов 130 Вычленение и подсчет цифр 132 Форматирование номера 133 Теперь мы довольны 134 Резюме 136 Глава 6. Усовершенствование адресной книги с использова- нием алгоритмов и сортированных контейнеров 137 Требования к интерфейсу адресной книги 137 Идиоматическое решение предоставления доступа к закрытому контейнеру 138 Недостатки использования обычных функций доступа 138 Использование интерфейса в стиле STL с классом AddressBook 139 Выявление дубликатов записей с помощью алгоритма count 141 Поиск точных совпадений 142 Совпадение значений 143 Воспользуйтесь алгоритмом count_if и объектами функций 144 Поиск с помощью lower bound и find if 150 Поиск по имени 150 Поиск строк 152 Поиск записи по идентификационному номеру 154 Эффективная сортировка объектов Address с помощью контейнера Set 154 Хранение объектов Address в контейнере multiset 155 Добавление, удаление и возвращение записей адресов 157 Поиск в multiset 160 Двойное индексирование в контейнере с установкой соответствий 162 Вторичное индексирование записей путем установки соответствий 163 Поддержание соответствия между двумя контейнерами 165 Резюме 171 Глава 7. Прокручивание экранных списков с помощью двухсторонних очередей и потоков ввода-вывода 172 Требования к экранному списку 172 Разработка экранного списка 173 8 Содержание
Класс DisplayList 175 Прокручивание списка 178 Отслеживание ошибок с помощью макроса assert 180 Заполнение контейнера deque с помощью алгоритмов сору и 180 backinserter Добавление записей в начало буфера с помощью обратных 182 итераторов Форматирование с помощью манипуляторов ввода-вывода 185 Реализация дополнительных функций класса DisplayList 190 Избегайте появления бессмысленных итераторов 194 Считывание вводимых чисел и отслеживание ошибок 195 Определение класса AddressDisplayList 197 Резюме 204 Глава 8. Простая система меню 205 Требования к системе меню 205 Конструирование системы меню 206 Создание иерархии меню с помощью шаблона stack 207 Семантика поддержания взаимодействия с пользователем 211 Считывание данных, введенных пользователем 212 Класс AddressBookMenu 213 Функция mainLoop 214 Функция viewEntry 216 Функция createEntry 216 Функции deleteEntry и editEntry 218 Поиск и фильтрация записей 220 Соберем части вместе 222 Исполняемая программа 222 Функция генерирования данных с помощью потоков строк 224 Нет предела совершенству: выполнение поиска независимо от 231 регистра букв Создание объекта функции сравнения, нечувствительной к регистру 233 Написание нового алгоритма 234 Резюме 238 Глава 9. Классы даты и времени с пользовательской 239 системой ввода-вывода Реализация классов даты и времени 239 Использование типа time t 239 Компоновка и извлечение значений типа time t 242 Реализация функций ввода и вывода времени 245 Базовый ввод и вывод 245 Устранение недостатков ввода-вывода 249 Использование вспомогательных строк для большей гибкости 252 программирования Использование класса DateTime в книге контактов 256 Резюме 267 Содержание 9
Глава 10. Сборка блоков программы 268 Написание главной программы 269 Реализация класса PIMDate с использованием шаблона auto_ptr 271 Классы Appointment и DateBook 275 Функция generateAppointment 278 Главное меню 282 Создание меню книги контактов 285 Класс DateBookMenuCatalog 288 Класс MonthlyDateBookMenu 292 Реализация класса DateBook 295 Показ календаря для текущего месяца 298 Переходы между видами 299 Экранные списки книги контактов 302 Классы, производные от ListBasedDateBookMenu 311 Создание записи контакта 316 Редактирование записи контакта 316 Завершение работы приложения 320 Резюме 320 Глава 11. Научитесь профессионально работать со стандартной библиотекой C++ 321 Полезные советы 321 Достижение максимальной эффективности работы 322 Управление памятью 324 Устойчивость к ошибкам 326 О чем вы еще не знаете 327 Библиотеки языковой поддержки, средств диагностики и специальных утилит 327 Библиотека региональных установок 328 Библиотека чисел 329 Где можно узнать больше 329 Источники в Internet 329 Техническая документация компилятора 330 Стандартная документация ISO 330 От автора 331 Предметный указатель 332 10 Содержание
Вступительное слово Обратите внимание на книги серии На примерах Джесса Либерти (Jesse Liberty). Я занялся созданием этой серии книг, поскольку был убежден, что традиционные учебные пособия по программированию подходят далеко не всем. Как правило, они начинаются с изучения ряда стандартных прикладных приемов, представленных в определенной логической последовательности, а затем автор книги переходит к рас- сказу о практическом применении этих приемов. Этот традиционный подход обуче- ния годится д ля большинства людей, но не для всех. За свою практику я обучил основам программирования, наверное, около 10 000 человек, при этом мне приходилось работать как индивидуально, так и с большими группами, а также проводить занятия по Internet. Мне приходилось слышать от мно- гих моих слушателей, что для них было бы предпочтительнее изучать не абстракт- ные приемы программирования, а просто сесть вместе с преподавателем у компью- тера и начать с создания реального проекта, усваивая необходимые навыки по мере продвижения работы над реальной программой. Именно эта идея легла в основу книг серии На примерах. В каждой из этих книг профессиональные программисты создают вместе с вами реальный сложный про- граммный продукт. Вы начинаете работу с “чистого листа” и проходите через все этапы конструирования и реализации модулей программы. Книги серии На примерах могут быть исходной, точкой знакомства с новой для вас темой, хотя их можно использовать и для того, чтобы закрепить материал, полу- ченный раньше из традиционных пособий по программированию. Трудно сказать, какой подход лучше. Это зависит исключительно от особенностей вашего воспри- ятия новой темы. В основу всех книг серии На примерах положен ряд некоторых общих принципов. В частности, мы всегда исходим из того, что вы абсолютно ничего не знали по дан- ной теме до того, как купили эту книгу. Несмотря на то что я не принимал непосредственного участия в написании всех книг данной серии, как ответственный редактор этой серии, я несу полную ответст- венность за каждую из них. Дополнительные материалы по каждой книге и темати- чески связанные с ними телеконференции вы найдете на моей Web-странице (www. libertyassociates. com). Если у вас возникнут какие-либо вопросы или пред- ложения, напишите мне по адресу j liberty^libertyassociates. com. Огромное вам спасибо за выбор книг этой серии. Джесс Либерти (Jesse Liberty). ответственный редактор серии книг На примерах.
Об авторе Пабло Халперн (Pablo Halpern) работает в индустрии компьютерных программ уже более 17 лет и является крупным специалистом в программировании на С и C++. Как главный консультант компании Halpern-Wight Software Inc., он работал со мно- гими корпорациями и частными компаниями, которые обращались за помощью в разработке проектов и обучении персонала программированию на C++ и объектно- ориентированному анализу. Пабло был автором курса, обучающего работе со стан- дартной библиотекой C++, и принимал участие в разработке ряда других курсов по углубленному изучению программирования на C++. Пабло живет вместе с женой Ненси и дочерью Джулией в индустриальном центре штата Массачусетс недалеко от Бостона и Кембриджа. Вы можете связаться с ним по электронной почте: phalpern@newview.org.
Посвящение Эта книга посвящается моему отцу Теодору Халперну (Teodoro I lalpem), который всегда был для меня примером красноречия. Благодарности В действительности один автор может быть только у научно-популярных книг. Хорошее обучающее пособие— это всегда результат сотрудничества многих людей, хотя их фамилии не указаны на обложке. Если вы хотите узнать, кто работал над той или иной книгой, обязательно посмотрите раздел “Благодарности”. Я никогда не написал бы эту кишу без поддержки моей жены и дочери. Они все- гда разделяли со мной как успехи, так и разочарования, засиживались допоздна и проводили за компьютером свои выходные и каникулы. Ненси и Джулии пришлось взять на себя большую часть домашних забот. Я благодарен им и люблю их обеих. Я не смог бы оплатить счета, если бы не нашел понимания у своих работодателей из компании Ironbridge Networks, где я получал зарплату консультанта даже в то время, когда был полностью погружен в работу над книгой. Я особо признателен Джессу Либерти — ответственному редактору серии книг На примерах, за то что он верил в мои способности даже больше, чем я сам. Именно Джесс предложил мне написать эту книгу, и я безмерно благодарен ему за это. Пер- вым человеком, с которым я встретился в издательстве Que, был Холли Аллендер (Holly Allender). Благодаря его помощи мои первые шаги в работе над книгой были не столь мучительны. Огромное спасибо Полу Снейсу (Paul Snaith) за то, что он дважды вычитал мой труд и устранил все неточности, а также всем многочисленным редак- торам, через чьи руки прошла эта книга, прежде чем вышла в свет.
Вступление Что особенного в этой книге Эта книга отличается от всех других книг, когда-либо написанных по данной те- ме. И вот в чем состоит отличие: все другие книги по программированию начинают- ся с изучения основных приемов программирования в порядке возрастания их сложности. После изучения всех этих приемов авторы переходят к демонстрации их применения, т.е. к созданию простейших программ. Данная книга начинается не со списка классов и функций, а с создания нового проекта. После анализа проблем и конструирования структуры программы мы пе- рейдем к методам реализации ваших идей. Знакомство с основными средствами стандартной библиотеки C++ происходит по мере их использования для решения конкретных задач. Сначала вы научитесь планировать проект, а потом освоите средства реализации поставленных задач. В изучении стандартной библиотеки C++ нет ничего сложного На многих людей само название “стандартная библиотека C++” действует устра- шающе. Я думаю, что проблема связана с тем. что в большинстве случаев изучение стандартной библиотеки начинается с рассмотрения ее многочисленных компонен- тов, а не с сосредоточения на проблемах, для решения которых она создавалась. Стандартная библиотека C++ настолько огромна, что пытаться изучить ее по списку компонентов — равносильно попытке выучить французский язык, зазубривая ор- фографический словарь. Есть два способа изучения иностранного языка. Один состоит в том, чтобы вы- учить некоторый набор слов и правила спряжения глаголов. Другой путь состоит в том. чтобы поехать на некоторое время в эту страну и освоить ее язык в живом об- щении с людьми. Каждый волен выбирать свой метод, по что касается меня, то за неделю, проведенную во Франции, я продвинулся в изучении французского дальше, чем за несколько лет изучения его по учебникам. Если бы вы пришли ко мне на занятия с целью научиться программированию в C++, то я бы не дал вообще никаких учебников. Мы бы просто сели у компьютера и стали вместе писать программу. И по ходу работы над программой я бы учил вас всему необходимому на конкретных примерах, лишь время от времени давая читать выдержки из учебников для закрепления материала в памяти. Именно в этом стиле написана данная книга. Мы вместе с вами займемся созда- нием реальной программы, и в ходе работы вы освоите все, что вам необходимо. С самых первых страниц вы сосредоточитесь на анализе проблем и моделировании программы, с помощью которой эти проблемы будут решаться, вместо того чтобы изучать абстрактные классы и методы. 14 Вступление
Н а Поскольку эта книга рассчитана также и на тех, кто никогда не работал Замётку с C++, в ней дан небольшой экскурс по концепциям, лежащим в основе этого языка программирования. Если вы уже хорошо знакомы с C++, просто пропускайте данные разделы. Научитесь выбирать наиболее подходящие средства программирования Еще один недостаток традиционного подхода обучения программированию состоит в том, что при ошеломляющем множестве средств и подходов мало вни- мания уделяется тому, как выбрать наиболее подходящее средство для решения конкретной задачи. Поскольку мы с вами собираемся работать над созданием реальной программы, я смогу продемонстрировать альтернативные пути реше- ния возникающих проблем и объяснить, почему выбор того или иного средства в данном случае предпочтительнее. Возвращаясь к аналогии с изучением ино- странного языка, можно сказать, что вы изучите не только стандартные словар- ные фразы, но и идиоматические выражения, что сделает ваш язык богаче и об- разнее. т Под идиоматическим использованием языка программирования I ирмип СЛедует понимать использование цельных конструкций, направлен- ных на решение конкретных проблем, без детального анализа от- дельных частей. Использование идиоматических конструкций позволяет вам быстро находить общие решения и облегчает понимание программного кода другими людьми, кто знаком с этими идиомами. Для чего нужна стандартная библиотека C++ Стандарты языка C++ описываются в документе, состоящем из 776 страниц (ISO/IEC 14882, 1998). Примерно половина этого документа посвящена описанию библиотеки функций и классов, которые поставляются вместе со стандартным ком- пилятором языка C++. Поэтому, если вы не знакомы со стандартной библиотекой C++, то половину его возможностей вы не знаете. Если вы еще не знакомы с программированием на C++, отложите Совет! эту книгу в сторону и идите в тот магазин, где вы ее купили. JL. Там вы наверняка найдете книгу Джесси Либерти Освой салюстоятель- но C++ за 21 день, 3-е издание (ISBN: 5-8459-0056-5ы. Прочтите снача- ла ее, а потом сразу возвращайтесь к следующему абзацу данной книги. Большая часть программного кода отводится выполнению многочисленных рутинных задач, таких как управление коллекцией объектов, манипулирование текстовыми строками, сортировка данных и форматирование ввода и вывода. Большинство этих задач приходится выполнять в любой программе, причем на- Вступление 15
писание программных кодов для их выполнения достаточно сложно и утомительно. Представьте, что вам каждый раз приходится по-новому созда- вать алгоритм сортировки данных. Достаточно велика вероятность допустить какую-нибудь незначительную ошибку, которая проявится только при опреде- ленных условиях, например при сортировке массива, содержащего три одинако- вых значения. Нет сомнений, что эта ошибка обнаружится в самый неподходя- щий момент. Умелое использование стандартной библиотеки гарантирует выполняемость соз- даваемых программ при использовании разных компиляторов и операционных сис- тем. Использование стандартных классов позволяет легко наладить обмен данными между программами, тем более, что в последнее время независимые создатели про- граммных библиотек все чаще используют в интерфейсах классы из стандартной библиотеки C++. Научившись работать со стандартной библиотекой C++, вы будете избавлены от необходимости изобретать колесо. Средства стандартной библиотеки призваны уп- ростить вашу работу над проектом и повысить надежность создаваемых программ- ных продуктов. Программисты, которым придется в будущем использовать ваши программные модули в своих проектах, будут безмерно благодарны вам, что им при- ходится работать с понятными стандартными функциями, а не разбираться в автор- ских хитросплетениях. (Даже если они не скажут вам спасибо за это, то, по крайней мере, вам не будет икаться от их мыслей в ваш адрес.) Что представляет собой стандартная библиотека C++ Стандартная библиотека C++ призвана выполнять две основные задачи. Прежде всего — это набор стандартных типов данных и функций, готовых к употреблению в любом компиляторе, который отвечает требованиям международных стандартов языка C++. т Библиотекой называется набор программных компонентов многоразо- :©рмип вого использования (классы, функции, макросы и т.д.), который созда- ется для облегчения выполнения стандартных, наиболее общих задач d программирования. Существуют специализированные библиотеки (например, программирование для Internet) и библиотеки общего на- значения (сортировка данных, сохранение файлов и т.д.). т Стандартной называется библиотека, состав, назначение и прин- I ©РмИН ципы использования которой определяются утвержденной докумен- тацией, например стандартами Международной организации стан- дартизации (ISO— International Standards Organization). Ниже речь пойдет о стандартной библиотеке C++, свойства которой описаны в специальном документе ISO. Мы будем называть ее просто стан- дартной библиотекой. й- О компиляторе говорят, что он отвечает стандартам, если он и его встроенные библиотеки функционируют в соответствии с между- народными стандартами ISO для языка программирования C++. 16 Вступление
В то же время стандартная библиотека C++ представляет собой среду разра- ботки для создания и сохранения новых библиотечных компонентов. В боль- шинстве случаев работа со стандартной библиотекой не подразумевает создание новых компонентов. Но если для вашего проекта необходимы новые уникальные программные модули, то в стандартной библиотеке вы найдете мощные встро- енные средства программирования. Умение создавать и использовать новые библиотечные компоненты как раз и отличает профессионала от новичка в про- граммировании. Я надеюсь, что, прочитав эту книгу и ближе познакомившись с возможностями стандартной библиотеки C++, вы значительно продвинетесь на пути от новичка к профессионалу. Части стандартной библиотеки Стандартная библиотека C++ в действительности состоит из десяти небольших библиотек. 1. Библиотека языковой поддержки, которая содержит типы и функции, непо- средственно связанные с работой компилятора языка программирования C++. Например, в эту библиотеку входят константы, определяющие диапазоны и уровень точности вычислений функций, использующих значения с пла- вающей запятой. 2. Библиотека диагностики содержит классы исключений и другие средства ди- агностики кодов и возвращения сообщений об ошибках. 3. Библиотека общего пользования содержит большой набор средств, которые трудно отнести к какой-нибудь специализированной библиотеке. 4. Библиотека строк содержит средства манипулирования текстовыми строка- ми. 5. Библиотека расположений поддерживает наборы международных и национальных символов и форматов, необходимых для обработки и возвращения данных о дате, времени или национальной валюте. 6. Библиотека контейнеров содержит родовые контейнеры для создания коллек- ций объектов. 7. Библиотека итераторов поддерживает функции навигации по коллекции объ- ектов. 8. Библиотека алгоритмов содержит многие стандартные алгоритмы обра- ботки данных. 9. Библиотека чисел предоставляет пользователям комплексные числа, специ- альные типы числовых массивов и алгоритмы вычислений. 10. Библиотека ввода-вывода предлагает средства форматирования и управления операциями ввода и вывода данных в файлы и другие устройства. Что нам будет необходимо в работе К некоторым из описанных выше десяти библиотек вам придется обращаться практически каждый день, другие, возможно, вам никогда не понадобятся. В этой книге основное внимание будет сосредоточено на рассмотрении пяти библиотек, ко- торые, с моей точки зрения, являются наиболее важными. Это библиотеки строк, контейнеров, итераторов, алгоритмов и ввода-вывода. Вступление 17
Нельзя сказать, что стандартная библиотека C++ совершенна и содержит все не- обходимое для разработки любого проекта. Специальному комитету при ISO, ответ- ственному за разработку и поддержание стандартов языка C++, приходится ежегод- но рассматривать сотни предложений от программистов всего мира о том, какие до- полнительные средства следовало бы включить в стандартный набор. После тщательного рассмотрения некоторые из предложенных средств, которые представ- ляют интерес для пользователей и не противоречат базовым концепциям языка C++, ежегодно добавляются в стандартную библиотеку. В то же время, если проследить историю развития и становления языка C++, то следу- ет признать, что некоторые довольно перспективные средства общего пользования были отклонены комитетом, а некоторые добавленные средства чрезмерно специфичны, что- бы стать популярными среди широкого круга пользователей. Позже я обращу ваше вни- мание на некоторые наиболее существенные промахи. Кроме того, при рассмотрении стандартной библиотеки мы пропустим некоторые средства, назначение которых либо слишком специфично, либо не вполне понятно. Отбирая средства программирования по значимости, я совершенно не хочу обидеть членов комитета ISO и отдаю должное их ад- скому труду над стандартами языка C++. В то же время многолетний опыт практического программирования позволяет мне высказать свое мнение по поводу тех или иных биб- лиотечных средств. О наследстве стандартных библиотечных функций, оставшемся от языка С Если вам приходилось программировать на языке С, то вы, безусловно, знакомы с библиотечными функциями стандартной библиотеки С. Такие функции, как print f, strcpy, atoi и прочие, по-прежнему доступны в языке C++, и позже мы рас- смотрим их использование. Некоторые части библиотеки С, такие как функции вво- да-вывода, редко используются в C++, поскольку современная стандартная библио- тека предлагает более совершенные и удобные средства ввода-вывода. В то же время некоторые другие средства, перешедшие в C++ из языка С, по-прежнему пользуются популярностью у программистов. Компоненты стандартной библиотеки С претерпели некоторые изменения, преж- де чем были включены в библиотеку C++. Были изменены имена файлов заголовков, например файл stdio.h был заменен на cstdio. При смене имен других файлов со- блюдался тот же принцип: расширение . h удалялось, а в начале имени добавлялась буква с в нижнем регистре. Благодаря новым возможностям перегрузки имен функций в C++ некоторые функции языка С были изменены для облегчения применения ключевого слова const. Например, функция языка С char* strchr(const char* s, char c); была заменена в C++ следующими двумя перегруженными функциями: const char* strchr(const char* s, char c); char* strchr( char* s, char c); Первая форма функции ведет поиск заданных элементов в массиве констант- ных символов и возвращает константный указатель на первый из обнаруженных символов. Вторая форма функции работает с неконстантным массивом и возвращает неконстантный указатель. Такая перегрузка имен функций пре- дотвращает потенциальную опасность возврата неконстантного указателя на массив константных значений. 18 Вступление
Перегрузка имен функций также позволяет использовать одни и те же функции для работы со значениями, отличающимися заданным уровнем точности. Напри- мер, функция языка С double sin(double); была перегружена в C++ следующим образом: float sin(float); double sin(double); и long double sin(long double); И наконец, все компоненты, унаследованные из библиотеки С, за исключением мак- росов, были добавлены в пространство имен std. (Макросы языка С по-прежнему дос- тупны, но поскольку они не подчиняются правилам видимости, их нельзя назначить пространству имен.) Мы разберем подробнее тему пространства имен std в главе 2. Какова разница между стандартной библиотекой C++ и стандартной библиотекой шаблонов Стандартную библиотеку шаблонов еще называют библиотекой STL (Standard Tem- plate Library). Под этим термином понимают набор интерфейсов и компонентов, разрабо- танных Александром Степановым (Alexander Stepanov) и другими сотрудниками AT&T Bell Laboratories и Hewlett-Packard Research Laboratories. Степанов поставил перед собой цель создать стандартный набор компонентов многоразового использования, макси- мально реализовав все возможности родового программирования. На сегодняшний день библиотека STL включает в себя библиотеки контейнеров, итераторов, алгоритмов и не- которые части библиотеки общего пользования, которые, в свою очередь, являются со- ставными частями стандартной библиотеки C++. Другими словами, стандартная биб- лиотека шаблонов STL является составной частью библиотеки C++, но при этом весьма существенной частью. Большая часть этой книги посвящена изучению использования средств библиотеки STL. — Интерфейсом называется любое формальное описание того, каким об- разом программист или пользователь может получить доступ к средствам и возможностям, предоставляемым функцией, классом, макросом или любым другим программным компонентом. Интер- фейс— это не ключевое слово языка, а абстрактная концепция про- граммирования . Термин Термин Интерфейс класса описывает набор открытых (объявленных с ключевым словом public) функций, типов и переменных данного класса. Интерфейс функции описывает типы аргументов и возвратов функции (другими словами, прототип функции). Вступление 19
Принципы, положенные в основу библиотеки STL Когда в конце 1993 года комитету по стандартизации языка C++ были предложе- ны основные идеи создания библиотеки STL, они были приняты и поддержаны с большим энтузиазмом. Это был тот редкий случай, когда предлагалось простое и универсальное решение для многих назревших проблем. Ниже перечислены ос- новные принципы, которых придерживались создатели STL. • Общее пользование. При рассмотрении компонентов библиотеки STL становится понятным, что их создавали как основу для решения наиболее общих и рутинных проблем в любой программе. Компоненты STL представ- ляют собой шаблоны решений, которые можно легко адаптировать для лю- бой конкретной программы. • Простота. Хотя сами по себе программные модули STL нельзя назвать про- стыми, большинство из них имеет предельно упрощенные и понятные ин- терфейсы. • Эффективность. Использование шаблонов позволяет максимально повысить эффективность работы программы и создавать программные модули, отве- чающие за выполнение конкретных задач. Поскольку особенности работы ком- понентов STL подробно описаны в документации, пользователь может подоб- рать наиболее эффективные решения для своей программы. • Гибкость. Компоненты STL довольно компактны и их можно использовать в самых произвольных комбинациях. В процессе программирования вы легко можете заменить один компонент другим, что не повлечет за собой необходи- мость вносить существенные изменения в другие модули программы. • Дополняемость. Библиотека STL представляет собой не просто некоторый стандартный набор компонентов, а набор интерфейсов. Настраивая интер- фейсы, вы можете создавать новые библиотечные компоненты, которые бу- дут совместимы и со стандартными компонентами библиотеки. Если вам уже приходилось слышать много хвалебных высказываний о библиотеке STL и вы не понимали, о чем идет речь, то надеюсь, теперь вы знаете, что у программистов было достаточно поводов для восторга. Это действительно мощное средство программирования, со всеми возможностями которого вы очень скоро познакомитесь. Наш рабочий набор инструментов По правде говоря, вы можете читать эту книгу, даже не дотрагиваясь до компью- тера. Но чтобы процесс обучения был максимально эффективным, желательно, что- бы вы скомпилировали все или хотя бы некоторые примеры, предлагаемые в книге. Еще лучше будет, если вы попробуете поэкспериментировать с кодом и получить свой вариант программы, что закрепит ваши навыки работы со стандартными биб- лиотечными компонентами. Чтобы не вводить код вручную, вы можете выгрузить листинги из этой книги с Web-страницы издательского дома “Вильямс” http: / /www. wil liamspubl ishing. com. Чтобы скомпилировать и запустить примеры программ, вам потребуются интег- рированная среда разработки IDE (Integrated Development Environment) или любой текстовый редактор, современный компилятор C++ и сама стандартная библиотека. 20 Вступление
Какой компилятор вам подойдет В основу данной книги положены стандарты языка C++, принятые ISO в июле 1998 года. Теоретически любой отвечающий стандартам компилятор должен работать с предлагаемыми кодами одинаково хорошо. Проблема состоит в том, как определить компилятор, отвечающий стандартам, положенным в основу этой книги. Вполне воз- можно, что ваш компилятор может не содержать некоторые средства современной стандартной библиотеки C++, если он был выпущен раньше. Некоторые непрофессио- нальные версии компиляторов в принципе не могут использовать все средства профес- сиональной стандартной библиотеки. Вам не следует использовать компиляторы, в ос- нову которых положены стандарты, датируемые до декабря 1996 года. Но даже те ком- пиляторы, которые были выпущены между декабрем 1996 года и июлем 1998 года, могут оказаться ограниченными в использовании средств современной стандартной библиотеки C++. Примеры, приведенные в этой книге, протестированы на компиляторах Micro- soft Visual C++ 6.0 в среде Windows и GNU egcs 1.1.2 в среде UNIX/Linux. Компилятор egcs представляет собой новое поколение компиляторов GNU и в большей степени соответствует новым стандартам ISO, чем предыдущие версии. Вы можете бесплатно загрузить этот компилятор с Web-страницы http: / /egcs. cygnus. сот/. Другой популярный компилятор, который предназначен для работы в среде Microsoft Windows, — C++ Builder 4.0 компании Borland. (Его следует отличать от ус- таревшей версии компилятора Borland C++ 4.0, который поддерживает лишь огра- ниченные возможности современной стандартной библиотеки C++.) Недавно были опубликованы обзоры, свидетельствующие о том, что компиляторы компании KAI и производные компилятора Edison Group в полной мере реализуют возможности стандартной библиотеки. Эти компиляторы хорошо работают с UNIX и со многими другими операционными системами. Поскольку одной из чрезвычайно популярных операционных систем для разра- ботки программного обеспечения является Solaris компании Sun, следует упомянуть о SunPro 5.0 — еще одном компиляторе, который можно использовать со стандарт- ной библиотекой C++. Ранняя версия SunPro 4.2 лишь частично реализует возмож- ности современной стандартной библиотеки. Особенности компиляции Даже используя перечисленные выше компиляторы, следует помнить о возможных ошибках, недостатках и некоторой специфичности в работе каждого из них. В том случае, когда особенности работы могут повлиять на процесс компиляции программных кодов, в книге будут даны примечания, которые выглядят следующим образом. На Особенности компиляции. Компилятор XYZ не распознает директи- ва метку ву #include. Для преодоления этого недостатка перед компиляцией следует добавить коды всех файлов источников и библиотечных файлов заголовков в один огромный общий файл с кодом программы. В некоторых случаях подобные примечания будут относиться к целому классу компиляторов, например ко всем компиляторам, выпущенным до 1998 г. В других случаях будет указываться компилятор определенного изготовителя или версия компилятора. Некоторые примеры из данной книги невозможно скомпилировать на первом попавшемся компиляторе и даже не на всех компиляторах, рекомендован- ных выше. Примите к сведению примечания об особенностях компиляции, но пом- Вступление 21
ните, что, поскольку особенности компиляции не являются основной темой данной книги, многие проблемы, связанные с компиляцией программных кодов, здесь опу- щены» Внимательно просмотрев все примечания, вам может показаться, будто, осо- бое внимание я уделил компилятору компании Microsoft. В действительности это не так. Просто при создании примеров для этой книги я использовал компилятор Microsoft, поэтому в примечаниях сравнивал с ним работу других компиляторов. Мой же выбор компилятора компании Microsoft основывался на том факте, что, нра- вится это кому-нибудь или нет. именно он сейчас наиболее популярный (или наибо- лее продаваемый) в мире. Где можно достать стандартную библиотеку Многие компиляторы, перечисленные выше, содержат встроенную стандартную библиотеку. йИИ Особенности компиляции. Версия стандартной библиотеки, встро- енная в компилятор Microsoft Visual C++ 6.0 и его ранние версии, содер- жит РОД известных ошибок, , допущенных изготовителями. По крайней мере одна из этих ошибок может быть причиной, по которой некоторые из приведенных примеров невозможно скомпилировать. Компания Dinkumware, Ltd, которая разрабатывала стандартную библиотеку для этого компилятора, занималась поиском и устранением ошибок в про- граммном коде. Полный список обнаруженных ошибок представлен на Web-странице http: //www. dinkumware. com/vc_f ixes. html, на которой вы можете также приобрести обновленную версию библиотеки. Некоторые устаревшие версии компиляторов (например, SunPro 4.2) содержат средства управления потоками, не относящиеся к библиотеке STL или стандартному классу строк. Вы можете обновить ваш компилятор, приобретая соответствующие библиотечные блоки из коммерческих или бесплатных источников (некоторые из них перечислены ниже). Страница стандартной библиотеки шаблоновзть http://www.stlport.org/ Проект STLport разрабатывался с целью бесплатного предоставления средств библиотеки STL и стандартного класса строк для добавления в библиотеки различ- ных компиляторов. STLport встроен также в коммерческую версию библиотеки SGI. Если у вас возникли проблемы с компиляцией, то. вероятно, это первый источник, к которому вам следует обратиться для обновления своей стандартной библиотеки. На данной Web-странице вы также найдете ссылки на другие тематически связан- ные с ней страницы. Objectspace, Inc. http://www.objectspace.com/ Компания ObjectSpace реализует некоторые библиотеки классов для C++ и Java. Многие из библиотек классов C++ содержат встроенные, компактные вер- сии STL и библиотеки строк (Standards<Toolkit>). Раньше библиотеки Ob- jectSpace можно было загружать бесплатно, но сейчас все они реализуются по коммерческим ценам. 22 Вступление
Rogue Wave, Inc. http:/ / www.roguewave.com/ Подобно Objectspace, компания Rogue Wave реализует библиотеки классов для C++, включая стандартную библиотеку. Следует обратить внимание, что многие продукты компании Rogue Wave основываются не на стандартной библиотеке, а на библиотеке Tools.h++ — ее собственной разработке. Эта библиотека представляет со- бой набор классов оболочек (wrapper), модифицирующих интерфейсы классов STL в стиле ранних компиляторов. Стандартная библиотека компании Rogue Wave со- вместима с большинством современных компиляторов C++. Оболочками (wrapper) называются классы или функции, которые изме- У ? ‘ - няют интерфейсы других классов или функций, не привнося новых г : • возможностей. Соглашения, используемые в книге В этой книге вам повстречаются следующие пиктограммы и врезки, ' Данная пиктограмма на полях указывает на определение нового тер- Гг” мина, который выделен в тексте курсивом. 1^-"" Справочная информация ^Экскурс / В этой врезке приводится дополнительная информация по рассмат- риваемой теме, которая поможет вам глубже понять и разобраться в вопросе. Иногда эти врезки будут даваться с подзаголовком Будьте внимательны, где вы узнаете о некоторых вещах и возможных проблемах, с которыми лучше бы вам не знакомиться на практике. Спешу вас заверить, что в примерах данной книги нет таких кодов, которые демонстрировали бы вам на практике, какие проблемы могут возникать с вашим компью- тером из-за ошибок программирования. В связи с особенностями построения книг данной серии, одна и та же тема может рассматриваться несколько раз по ходу изложения материала, если, например, одно и то же средство используется на разных этапах работы над проектом. Чтобы лучше разобраться с особенностями выбора и использования различных средств програм- мирования, в книге приводятся многочисленные примечания, советы и предупреж- дения, призванные выделить концептуальные моменты работы над проектом. В книге вам повстречаются следующие пиктограммы. Дань1 краткие комментарии и сноски по рассматриваемой теме, а так- же объяснения концепции применяемого подхода. Вступление 23
. . :!:!7 • '"У?:? ’ ' Г.у** л‘:’’ •"'• ’•* Предупреждение'.^ 6^i^::;:^opy^|^nycKaioTr\H0QqbiTHHe; программисты. ‘ .. Н *. s ’ ’ J . . < • . / ..-.-A ....’ Кроме того, в тексте вы увидите слова и выражения, выделенные тем или иным образом. При выделении мы придерживались таких соглашений. • Команды, переменные и прочие элементы программного кода выделены в тексте следующим стилем. • Мы будем работать над листингами программных кодов, совершенствуя их от раздела к разделу. Новые блоки в листингах будут выделены следующим стилем. • В листингах вывода программ тестирования данные, вводимые пользова- телями, будут выделены следующим стилем. 24 Вступление
Глава 1 Знакомство с TinyPIM В этой главе... • Объектно-ориентированный анализ проекта 26 • Планирование классов 29 • Можно ли уже приступать к написанию кода 36 Как уже говорилось во введении, мы будем изучать средства стандартной библио- теки C++ по мере создания реальной программы. Сейчас пришло время познакомить вас с нашим проектом. Программа, которую нам нужно создать, — электронная записная книжка, назовем ее TinyPIM (Tiny Personal Information Manager — карманная личная за- писная книжка). С помощью PIM можно записывать и сохранять в памяти ком- пьютера адреса, примечания, деловые заметки, причем программу нужно снаб- дить средствами, которые позволят сортировать и обрабатывать эти записи для организации вашей деловой жизни. Программу можно сделать настолько слож- ной, насколько вы захотите. Это идеальный проект для обучения программиро- ванию, над которым вы сможете затем поработать самостоятельно. Мы начнем работу с простейшего варианта программы, постепенно добавляя все новые и новые возможности. Но прежде чем приступать к написанию кода, давайте вкратце проанализи- руем проблему и выработаем наши базовые требования к проекту. Именно с эта- па анализа начинается любой проект. В этой книге мы проведем анализ по со- кращенной схеме, хотя в своей работе вам нужно будет уделить этому моменту гораздо больше времени и внимания. Анализ проблемы важен для выполнения даже такого небольшого проекта, как наш TinyPIM, но поскольку данная книга посвящена изучению стандартной библиотеки C++, а не основам объектно- ориентированного анализа, мы не станем углубляться во все детали этого про- цесса. Объектно-ориентированный анализ программы — многоэтапный цикли- ческий процесс, при котором нам приходится снова и снова возвращаться к от- правной точке и рассматривать проблему под новым углом зрения. Возможно, вы настолько гениальны, что сможете с одного взгляда вникнуть в суть пробле- мы, но в большинстве случаев объектно-ориентированный анализ— это важ- ный и необходимый этап работы над любым проектом. Для документирования результатов анализа используется UML (Unified Modeling Language— унифицированный язык моделирования}. Ниже мы познакомимся с базо- выми элементами этого языка. От анализа поставленных проблем мы перейдем к выработке требований к проекту, а затем к созданию общей схемы. Потом мы про- работаем детали схемы и приступим непосредственно к написанию кода. Наш ана- лиз будет несколько прямолинейным, чего редко удается достичь в реальной жизни. Обычно после проработки деталей приходится еще несколько раз возвращаться к формулировке новых требований и изменению общей схемы проекта.
Термин Унифицированный язык моделирования (UML)— это набор стандартных элементов для схематического представления структуры объектно- ориентированной программы. В этой книге для создания схемы программы использовался набор инструментов Rose от Rational Software. Но многие специалисты объектно-ориентированного ана- лиза для схематического представления проекта используют другие наборы элемен- тов, разработанные ранее в Rational. Познакомиться с ними можно на Web-странице по адресу http: / /www. rational. сот/. Объектно-ориентированный анализ проекта Чтобы выработать требования к проекту, нужно четко представить себе по- требности будущих пользователей вашей программы и то, каким образом про- грамма должна удовлетворять эти потребности. Можно выделить два основных момента в проведении объектно-ориентированного анализа. Первый — это краткое, наиболее общее формулирование требований к программе с точки зре- ния конечного пользователя. Второй момент — анализ ситуаций использования, где рассматриваются все возможные цели и действия пользователей при исполь- зовании программы и то, каким образом она должна реагировать на каждую из возможных ситуаций. Формулирование требований к проекту 1. Программа TinyPIM должна объединять в себе возможности адресной книги и календаря контактов. Адресная книга позволит пользователям сохранять и редактировать адреса и номера телефонов, просматривать и выбирать запи- си по именам, а также осуществлять простой поиск записи по ключевому сло- ву. Пользователь должен иметь возможность выбрать запись из общего списка и открыть ее для просмотра или редактирования. 2. Календарь контактов позволит пользователям записывать и редактировать сведения о деловых контактах и событиях в хронологической последователь- ности. Нужно позволить просматривать список событий по месяцам, неделям и дням. В любом режиме просмотра пользователь должен иметь возможность выбрать запись и открыть ее для просмотра или редактирования. 3. Первая простейшая версия TinyPIM будет иметь примитивный интерфейс ввода данных из командной строки. 4. Первая версия TinyPIM будет просто сохранять данные в оперативной памяти компьютера. 5. Программу следует выполнить, таким образом, чтобы предусмотреть возмож- ность модернизации ее в будущем: • заменить интерфейс командной строки на графический или интер- фейс Web-страницы; 26 Глава 1. Знакомство с TinyPIM
• добавить возможность сохранения данных в отдельных файлах или базе данных; • добавить дополнительные функции обработки списков данных. 6. Программа будет создаваться прежде всего с целью продемонстрировать раз- личные возможности стандартной библиотеки C++. Последнее требование существенно повлияет на наш выбор решений при созда- нии программы. Хотя мы займемся написанием реальной программы, которая будет отвечать конкретным практическим требованиям и должна работать настолько эф- фективно, насколько это возможно, время от времени мы будем рассматривать ис- пользование средств программирования, без которых в нашем конкретном примере можно было бы запросто обойтись. Но если мы не рассмотрим их сейчас, у нас не бу- дет другой возможности познакомиться со многими важными средствами и приема- ми, которые, безусловно, окажутся полезными для вас во время работы над другими проектами. В некоторых случаях мы будем добиваться поставленной цели одним способом, но затем переделаем программу, чтобы научить вас использовать альтер- нативные средства программирования и показать всю мощь и гибкость стандартной библиотеки. Именно обучение использованию разнообразных средств стандартной библиотеки C++ и является основным требованием к нашему проекту. Все остальные требования призваны лишь помочь достижению этой основной цели. Анализ ситуаций использования Теперь, после того как мы уяснили для себя, над чем мы будем работать, рассмот- рим вкратце некоторые возможные ситуации, на которые наша программа должна уметь адекватно реагировать. Работу следует начать с описания наиболее общих си- туаций использования, уделив особое внимание тому, когда и в каких целях пользо- ватель может обратиться к программе. Затем перейдем к детальной проработке си- туаций использования, рассмотрим возможные последовательности действий, по- стараемся ответить на вопросы, которые могут возникнуть у пользователя, и спланируем наиболее корректное поведение программы в ответ на возможные си- туации в работе. Сейчас мы пропустим все промежуточные этапы работы и перей- дем к рассмотрению возможных ситуаций. Ж Под ситуацией использования понимается описание особенностей iepiyil4H взаимодействия создаваемой системы с одним или несколькими субъ- ектами , использующими эту систему. Термин Под субъектом понимается пользователь, другая программа или уст- ройство, которые взаимодействуют с нашей системой. Ситуации ввода данных 1. Пользователь хочет ввести новую запись адреса. Программа предлагает ввести фамилию, имя, номер телефона и адрес. Только ввод фамилии является обяза- тельным требованием, все остальные данные пользователь может вводить по желанию. Объектно-ориентированный анализ проекта 27
2. Пользователь хочет изменить существующую запись адреса. Для этого он вы- бирает соответствующую запись либо из списка, либо с помощью функции по- иска, после чего получает возможность изменить данные в любом поле. 3. Пользователь хочет удалить запись адреса. Для этого он выбирает запись либо из списка, либо с помощью функции поиска и дает команду на удаление. 4. Пользователь хочет создать контакт. Программа предлагает ввести начальную и конечную даты, а также описание контакта. 5. Пользователь хочет изменить существующий контакт. Для этого он выбирает его либо из списка, либо с помощью функции поиска и открывает для редакти- рования любого поля. 6. Пользователь хочет удалить контакт. Для этого он выбирает его либо из спи- ска, либо с помощью функции поиска и дает команду на удаление. Ситуации поиска и отображения списка записей 7. Пользователь хочет найти одну или несколько записей по имени. Программа предлагает ввести фамилию (полностью или частично) и, по желанию, имя. Если точное соответствие не обнаружено, программа должна возвратить спи- сок записей для выбора пользователем, в которых фамилии начинаются с ука- занной комбинации букв. Если несколько записей соответствуют установлен- ным критериям, то все они должны быть возвращены программой. 8. Пользователь хочет найти запись, содержащую ключевое слово (комбинацию слов) в любой части записи. Программа должна возвратить все записи, отве- чающие установленному критерию. 9. Пользователь хочет просмотреть все контакты, назначенные на определенный день. Программа должна возвратить первые строки описаний всех контактов, соответствующих указанной дате. 10. Пользователь хочет просмотреть все контакты, назначенные на определенную неделю. Программа должна возвратить первые строки описаний всех контак- тов, соответствующих указанной дате. 11. Пользователь хочет посмотреть, на какие дни месяца у него назначены кон- такты. Программа должна возвратить календарь, в котором дни с контактами выделены особым образом. 12. Пользователь хочет посмотреть назначенные контакты на следующий день или следующую неделю. Программа должна обеспечить возможность быстрого возвращения подобной информации в хронологическом порядке с выводом первых строк описаний назначенных контактов. 13. Пользователь хочет просмотреть все контакты, назначенные им на некоторый промежуток времени. Программа должна предоставить возможность пользо- вателю указать временной диапазон в днях, неделях или месяцах д ля возвра- щения всех соответствующих записей в хронологической последовательности. Ситуации возникновения ошибок 14. Пользователь ошибочно дважды ввел запись адреса с одинаковыми фамилией и именем. Программа должна предупредить пользователя о существовании подоб- ной записи и пред ложить подтвердить ввод новой информации или отменить его. 28 Глава 1. Знакомство с TinyPIM
Анализ ситуаций использования поможет нам лучше понять, как должна рабо- тать программа, и позволит выбрать наиболее оптимальные решения во время про- граммирования . Планирование классов Работа над схемой проекта начинается с выделения наиболее общих логических компонентов, называемых блоками. Затем проводится более детальный анализ каж- дого блока. Двухуровневый логический анализ Проанализировав требования к программе, мы можем выделить три логических блока, представляющих базовые функциональные возможности программы, пользо- вательский интерфейс и механизмы сохранения введенной информации в файлах или в базе данных. Хотя в исходной версии программы TinyPIM функции сохранения не будут представлены, нам следует отобразить их в общей схеме, чтобы предусмот- реть возможность дальнейшей модернизации программы. На рис. 1.1 схематически показано взаимодействие основных блоков программы TinyPIM. Рис. 1.1. Схема блоковой организации про- граммы TinyPIM В языке UML значок папки соответствует блоку программных классов, как это показано на рис. 1.1. Пунктирные стрелки представляют отношения использования. Другими словами, из схемы видно, что блок текстового интерфейса использует в своей работе функциональное ядро программы и блок функций сохранения. В схеме блоковой организации программы нужно отобразить наиболее общие взаимоотношения между блоками, не зависящие от внутренней структуры блоков интерфейса или функций сохранения. Обратите внимание, что функциональное яд- ро программы не должно в своей работе использовать ни блок интерфейса, ни блок функций сохранения. Это означает, что данный блок фактически независим от осо- бенностей работы других блоков и от того, как эти блоки будут изменяться во время модернизации программы. Анализ отношений между блоками позволяет выявить дальнейшие ограничения и требования к программе. Так мы можем заключить, что объекты, осуществляющие взаимодействие между блоками, должны быть предельно упрощены. Предположим, что текстовый интерфейс программы в будущем будет заменен на интерфейс Web-броузера. Планирование классов 29
Функционально ядро программы PIM будет связываться с интерфейсом посредством Internet. В таком случае блоки интерфейса программы и функционального ядра переста- нут быть частями одной программы, поэтому окажется невозможным передавать между ними указатели на данные. Вместо этого данные между блоками должны будут переда- ваться в виде последовательностей байтов и преобразовываться в значимую информа- цию в каждом взаимосвязанном блоке. Это следует учесть с самого начала работы над проектом, поскольку использование указателей, виртуальных функций и многих других средств объектно-ориентированного программирования существенно затруднит модер- низацию программы в будущем. Точно так же следует предусмотреть возможность мо- дернизировать в будущем блок функций сохранения для работы со сложными релятив- ными базами данных, даже если в исходной версии программы мы вообще не планируем сохранять записи. К вопросу анализа взаимодействий между блоками программы мы еще не раз будем возвращаться в ходе работы над проектом. Краткий вводный курс в UML Диаграмма программы PIM, построенная с использованием элементов и правил языка UML, показана на рис. 1.2. Рис. 1.2. Диаграмма классов программы PIM Диаграмма классов, показанная на рис. 1.2, описывает члены каждого класса и отношения между объектами этих классов. Отдельный класс представлен прямо- угольником, разделенным на три секций. В верхней секции показано имя класса, в средней— список переменных-членов класса (постоянных переменных), в ниж- ней — функции-члены класса. Степень детализации описания класса должна быть достаточной для полного отображения взаимодействия между классами. Далее вы 30 Глава 1. Знакомство с TinyPIM
узнаете, как средствами языка UML можно показать открытые, закрытые и защи- щенные члены класса, но об этом мы поговорим, когда перейдем к написанию кода. На данном этапе простого перечня членов класса вполне достаточно для планирова- ния отношений между классами. Обратите внимание, что на рис. 1.2 классы Address и Appointment выглядят так, будто они не содержат функций-членов, классы AddressBook и DateBook не содер- жат переменных-членов, а класс PlMData выглядит вообще пустым. Это также свя- зано с тем, что диаграмма классов призвана показать прежде всего отношения меж- ду классами, а не их структуру. Безусловно, отношения между классами базируются на использовании соответствующих членов класса. Так, класс PlMData должен иметь по крайней мере две переменных-члена для налаживания связей с классами AddressBook и DateBook. Язык UML позволяет показать имена членов, вовлеченных во взаимодействие классов. Но, с моей точки зрения, на данном этапе рассмотрения структуры программы это не так уж и важно. Сильной стороной UML является тот факт, что диаграмма сохраняет читабельность и объективность после удаления из нее информации, которая не представляет в данный момент интереса. Часто для анализа работы программы бывает полезно создать несколько диаграмм, отобра- жающих разные стороны взаимодействия классов. Линии, связывающие классы, показывают взаимоотношения между объектами этих классов. Однонаправленная стрелка свидетельствует о том, что информация передается лишь в одном направлении. Так, по схеме на рис 1.2 мы можем заклю- чить, что объект AddressBook знает, с каким объектом Address он взаимодействует, но не наоборот. Ромб на стрелке означает вхождение одного объекта в другой. Таким образом, объект Address является составной частью объекта AddressBook, а оба они являются составными частями объекта PlMData. Различия между белыми и черны- ми ромбами весьма незначительны. Черные ромбы означают композиционные отно- шения между объектами, т.е. вложенный объект существует ровно столько, сколько существует композиционный. Белые ромбы означают агрегацию объектов, когда объекты существуют независимо друг от друга. На практике на этом этапе планиро- вания очень трудно решить, будут ли композиционные отношения между объектами лучше агрегативных и наоборот. В данном случае решение было принято на основе более позднего анализа программы. “Звездочка” на конце стрелки означает, что здесь имеет место отношение “один ко многим”, т.е. с одним объектом может быть связано несколько объектов другого класса. Например, один объект AddressBook может содержать много объектов Address. Когда мы приступим к написанию программного кода этих классов, то уз- наем, что стандартная библиотека C++ содержит все необходимое для реализации разных типов отношений между классами. Функциональное ядро TinyPIM Теперь, когда вы уже знаете немного о языке UML, давайте внимательно проана- лизируем диаграмму, представленную на рис. 1.2. Функциональное ядро PIM, как и следует из названия, является сердцевиной нашей программы TinyPIM. Здесь со- держатся объекты адресной книги и книги контактов, а также даются описания объ- ектов, которые будут сохраняться в этих книгах. Адресная книга и книга контактов соответственно представлены классами AddressBook и DataBook. Объект PlMData композиционно включает два этих объекта в единый объект, представляющий все записи пользователя. Планирование классов 31
Объекты AddressBook и DataBook, в свою очередь, могут содержать соответст- венно наборы объектов Address и Appointment, представляющие собой записи базы данных. Эти же объекты предоставляют также интерфейсы для добавления, удале- ния и редактирования текстовых строк записей. Поскольку основным клиентом функции поиска является пользовательский интерфейс, мы отложим описание ар- гументов и возвращаемых значений этой функции до тех пор, пока не завершим ра- боту над созданием самого пользовательского интерфейса. Класс Address является базовым контейнером данных. Каждая запись содержит уникальный идентификационный номер, фамилию, имя, номер телефона и адрес человека или компании. Все это — текстовые строки, способы представления кото- рых мы будем рассматривать по мере написания программы. Поле адреса можно подразделить на ряд дополнительных полей, например улица, номер дома, номер факса, электронная почта и т.д. Но все эти дополнительные поля, по сути, ничем не будут отличаться от единого поля адреса, поэтому чрезмерная детализация записей мало чем поможет нам в освоении стандартной библиотеки C++. В нашей программе в классе Address были определены три ключевых поля: recordld__, lastname_ и f irstname__. Как вы увидите позже, все эти поля будут использоваться как ключе- вые при выполнении разных частей функции программы. С подобным подходом мы будем сталкиваться еще не раз при изучении стандартной библиотеки. Класс Appointment также является базовым контейнером данных. Ключевые по- ля этого класса: recordld__, startTime_ и, возможно, endTime__. Поля recordld_ и startTime содержат данные типа Time. Этот тип данных задается еще одним клас- сом, который нужно определить в программе. Еще одна особенность классов Address и Appointment— их объекты использу- ются во всех частях программы TinyPIM, включая блоки текстового интерфейса и функций сохранения. Эти классы содержат переменные-члены базовых типов String и Time. Единственные функции-члены, известные нам на данном этапе,— базовые функции доступа. Это все, что нам необходимо знать о классах, если потре- буется передавать объекты по сети или сохранять в релятивной базе данных. Блок текстового пользовательского интерфейса Блок текстового пользовательского интерфейса ответствен за налаживание взаимодействия между пользователем и программой. В основе его будут использо- ваться главным образом средства ввода-вывода стандартной библиотеки C++. Взаи- модействие системы с пользователем будет заключаться в показе меню, обработке выбранных опций, выводе предложений и считывании данных адреса или контакта, введенных пользователем с клавиатуры. Чтобы реализовать подобную систему на практике, нам потребуется достаточно простой программный код. Классы меню На рис. 1.3 показана часть классов блока текстового пользовательского интер- фейса, которые отвечают за работу меню программы. В этой диаграмме используются некоторые дополнительные элементы языка UML. Стрелки с белыми треугольными наконечниками обозначают отношения обобщения/специализации. В данном случае класс Menu является обобщением (также используются названия надкласс и базовый класс) для классов MainMenu, AddressBookMenu и DateBookMenu. В противоположном направлении это же отно- шение между классами называется специализацией. Можно сказать, что класс 32 Глава 1. Знакомство с TinyPIM
AddressBookMenu является специализацией (также используются названия подкласс и производный класс) класса Menu или наследован от класса Menu. Обратите внима- ние, что только отношения наследования являются действительно отношениями между классами, тогда как все остальные рассмотренные взаимосвязи являлись от- ношениями между объектами классов. Рис. 1.3. Диаграмма классов блока текстового пользовательского интерфейса Четыре прямоугольника в нижней части рис. 1.3 также представляют клас- сы, но для них не показано никаких переменных-членов или функций-членов. Это упрощенный способ отображения классов в схемах UML без показа внутрен- ней структуры. Структуру этих классов мы рассмотрим ниже, в следующей диа- грамме классов. Menu является абстрактным классом, который содержит наиболее общие методы показа меню и обработки ответов пользователей. Чтобы меню работало, виртуальная функция mainLoop должна выполняться в каждом производном классе. Функции getMenuSelection и clearScreen выполняют базовые операции, свойственные всем классам меню. Программа поддерживает стек меню. (Стек меню никак не свя- зан с подпрограммой, называемой стеком.) Когда возникает необходимость в подме- ню, оно вводится в стек с помощью функции enterMenu, при выходе из подменю оно удаляется из стека функцией exitMenu. Если стек оказывается пустым, выполнение программы завершается. Не путайте подменю с подклассом. Любое меню может сде- лать подменю простым перемещением в стеке объектов. Меню в стеке совсем не обя- зательно должны быть связаны отношениями наследования. Планирование классов 33
Термин Термин Абстрактным называется класс, который существует только как обоб- щение производных от него классов. Объекты абстрактного класса мо- гут относиться только к производным подклассам. В C++ на абстракт- ность класса обычно указывает наличие чисто виртуальной функции. Виртуальная функция, для которой может отсутствовать реализация, называется чисто виртуальной. Определение чисто виртуальной функ- ции заканчивается = 0. Для класса, содержащего чисто виртуальную функцию, невозможно создать объект. Чисто виртуальная функция обычно замещается в производных классах на обычную виртуальную, а такой класс называется конкретным. Для конкретного класса, в отли- чие от абстрактного, можно создать объект. Главное меню просто предлагает пользователю выбрать ввод адреса, контакта или выйти из программы. Если пользователь выберет редактирование адресной книги, то в стек меню будет введен указатель на объект класса AddressBookMenu. Выбор опции редактирования книги контактов введет в стек указатель на объект DateBookMenu. В классах AddressBookMenu и DateBookMenu определены собствен- ные функции ma inLoop и функции для каждой опции меню. Меню AddressBookMenu и DateBookMenu в конечном итоге нужны для вывода списков записей на экран. Это может быть список всех записей или выборка запи- сей, произведенная в соответствии с установленными критериями. Поскольку вы- водимый список ограничен размерами экрана, длинные списки следует разделить на блоки, прокручиваемые на экране вверх и вниз. Кроме того, каждая строка эк- ранного списка должна быть пронумерована, чтобы пользователь мог выбрать за- пись для просмотра, редактирования или удаления по номеру. Задачи по управле- нию списками и записями делегируются классам AddressDisplayList и AppointmentDisplayList, которые мы рассмотрим более подробно позже. Классы редакторов Чтобы пользователь мог создать или изменить запись в книгах адресов или кон- тактов, используются классы AddressEditor и AppointmentEditor, которые под- держивают некоторые базовые функции текстовых редакторов. На рис. 1.4 в наибо- лее общих чертах показана структура классов редакторов. Рис. 1.4. Диаграмма классов редакторов 34 Глава 1. Знакомство с TinyPIM
В целом структура классов редакторов довольно проста. Базовый класс Editor предоставляет функции редактирования однострочных полей (таких как поле фами- лии) и многострочных (поле адреса). Этот же класс поддерживает функцию строки состояния, показывающей, было ли редактирование поля завершено или прервано. Производные классы содержат копии отредактированных строк и предоставляют основную функцию редактирования, которая в зависимости от выбранного поля ис- пользует функции editSingleLine или editMultiLine. Классы отображения списков Вернемся к классам отображения списков книг адресов и контактов, структуры которых показаны на рис. 1.5. Рис. 1.5. Диаграмма классов отображения списков Основная работа по отображению списков возложена на базовый класс DisplayList. В нем представлены функции отображения текущей выборки записей, перехода к следующему или предыдущему экранному блоку записей, восстановления исходного списка, обновления списка по сохраненным записям и выбора записей по номерам. Используются также еще две защищенные виртуальные функции, предос- тавляемые производными классами. Работа класса DisplayList состоит в показе пользователю выборки записей из адресной книги или книги контактов. Если выборка записей не помещается на экра- не, то пользователь сможет прокручивать ее вниз и вверх. Чтобы сократить до ми- нимума взаимодействие с функциональным ядром TinyPIM (следует помнить о воз- можности физического разделения этих блоков в сети), класс DisplayList заносит в буфер список идентификационных номеров записей, которые следует отображать в списке. Если следующая страница записей не занесена в буфер, функция возвра- щает их из функционального ядра программы. При этом программа определяет, ото- Планирование классов 35
бражается ли в данный момент список адресов (объект Address) или список контак- тов (объект Appointment). Производные классы отвечают за отображение индивидуальных записей в со- кращенном формате при вызове функции displayRecord. Другая функция произ- водных классов fetchMore возвращает очередной блок идентификационных номе- ров записей из функционального ядра программы. Какой именно блок записей будет очередным, определяется с помощью остальных переменных-членов и функций- членов этих классов. Более подробно с их работой мы познакомимся при написании программных кодов. Блок функций сохранения Функции блока сохранения ответственны за сохранение содержимого адресной книги и книги контактов в файлах на жестком диске или в базе данных, а также за возвращение этих данных в оперативную память при запуске программы. Поскольку в исходной версии программы TinyPIM эти функции не будут представлены, мы не станем сейчас изучать их структуру и пока ограничимся тем, что следует учесть воз- можность их добавления в будущем. Можно ли уже приступать к написанию кода Построенная нами схема проекта еще далека от завершения. Мы создали лишь статичные диаграммы классов, которые не объясняют, как именно каждый класс выполняет свою работу и взаимодействует друг с другом. Чтобы завершить докумен- тацию проекта, нам еще нужно создать диаграммы последовательностей и диаграм- мы сотрудничества. (Это диаграммы на языке UML, которые показывают передачу сообщений между классами.) По правде говоря, мы не закончили даже этап анализа проекта. Чтобы завершить его, нужно описать все функциональные возможности программы, вплоть до описа- ния комбинаций быстрых клавиш. Нельзя приступать к программированию, пока мы в точности не определимся с полным набором команд и тем, как программа должна реагировать на каждую из них. Что же нам делать теперь? Можно было бы продолжить работу над анализом проекта и детальной функциональной спецификацией работы программы, спла- нировать структуру каждого класса и продумать алгоритмы выполнения функ- ций. Но поскольку наша цель сейчас состоит не в создании коммерческой про- граммы, а в обучении использования средств стандартной библиотеки C++, да- вайте совместим для наглядности дальнейшее планирование проекта с программированием элементов программы. По мере того как мы будем выпол- нять каждый отдельный функциональный элемент программы, мы прежде всего начнем с уточнения требований к нему, описания структуры и, если будет необ- ходимо, с создания подробной диаграммы. Такой подход к выполнению проекта называется поэтапной разработкой, при ко- тором последовательно создаются функциональные элементы программы путем по- вторения цикла определение спецификаций — планирование -— программирование. Этот подход стал популярным и рассматривается как альтернатива классическому 36 Глава 1. Знакомство с TinyPIM
подходу, при котором написание первой строки программы начинается только после того, как были выработаны и описаны требования ко всей программе в целом. Пре- имущество поэтапной разработки состоит в большей наглядности того, как выпол- нение одного блока программы может влиять на структуру и выполнение следующе- го блока. Но, честно говоря, я выбрал этот подход потому, что точно так же, как и вам, мне уже не терпится приступить к написанию программного кода и показать вам возможности стандартной библиотеки C++. Давайте займемся этим прямо со следующей главы. Можно ли уже приступать к написанию кода 37
Глава 2 Реализация класса Address для работы с текстовыми строками В этой главе... • Использование строк с фиксированной длиной 38 • Использование массивов в буфере динамического распределения памяти 51 • Упрощение реализации класса Addr еs s с помощью библиотечного класса string 60 • От строковых объектов назад к строкам с нулевыми окончаниями 65 • Резюме 68 Объекты класса Address используются в программе TinyPIM для сохранения отдель- ных записей адресной книги. На эти объекты возложено не так-то много функций, это просто вместилища данных. Все переменные-члены класса Address являются текстовы- ми строками. Следовательно, при реализации класса Address нам нужно сконцентриро- ваться на поиске лучших путей представления текстовых строк. Использование строк с фиксированной длиной Мы начнем с определения класса Address с помощью массивов символов с задан- ным размером и нулевым окончанием для сохранения текстовых строк фиксирован- ной длины. При этом мы познакомимся со средствами стандартной библиотеки, с помощью которых массивами символов можно управлять как текстовыми строка- ми. Многие программисты рассматривают эти средства как устарелое наследие стандартной библиотеки языка С. Тем не менее имеет смысл ближе познакомиться с ними, поскольку данные средства программирования все еще активно используют- ся и достаточно эффективны. В конце главы вам будут представлены новые возмож- ности стандартной библиотеки C++, которые позволяют усовершенствовать алго- ритмы обработки и представления текстовой информации в программе. Объявление класса Address Первый вариант определения класса Address показан в листинге 2.1. Для полей фамилии, имени, номера телефона и адреса использовались массивы символов с фиксированными размерами. Для вашего удобства строки листинга пронумерова- ны, но учтите, что этих номеров не должно быть в компилируемом коде.
! Лиатмиг 1 / I 1 !^иксяро^нь1моазмераЯи| 1://TinyPIM (с)1999 Pablo Halpern. Файл Address.h 2: 3: ftifndef Address_dot__h 4:#define Address_dot__h 1 5: 6://Реализация класса Address с использованием фиксированных строк 7:class Address 8: { 9:public: 10: // Конструктор 11: Address(); 12: 13: // Функции доступа к полям 14: const char*lastname () const {return lastname__; } 15: void lastname(const char*); 16: 17: const char*firstname () const {return firstname__; } 18: void firstname(const char*); 19: 20: const char*phone()const {return phone_;} 21: void phone(const char*); 22: 23: const char*address()const {return address_;} 24: void address(const char*); 25: 26:private: 27: // Перечисление длин строк 28: enum {namelen = 16,phonelen = 16,addrlen = 100 }; 29: 30: // Поля данных 31: char lastname_[namelen]; 32: char firstname__[namelen] ; 33: char phone_[phonelen]; 34: char address__[addrlen] ; 35: }; 36: 37:#endif // Address dot h Данный класс служит основным контейнером, его задача — только хранение данных. Единственными методами данного класса являются функции доступа к его переменным- членам, которые служат д ля присвоения и возвращения данных полей записи адреса. Давайте начнем с описания переменных-членов. В строке 28 определяются константы длин полей. Обратите внимание на использование в программах ли- теральных констант для определения различных параметров, таких, например, как размеры массивов. Данный подход позволит вам в будущем быстро находить место, где задаются эти параметры, и при необходимости изменять их значения сразу для всей программы. Благодаря использованию при объявлении констант закрытых перечислений enum вместо макросов # de fine мы достигаем сохране- ния принципов модульного построения объектно-ориентированной программы, поскольку в этом случае созданные константы становятся закрытыми членами класса Address. Использование строк с фиксированной длиной 39
ц а Особенности компиляции. Стандарты ISO C++ предлагают альтер- Заметку нативный метод определения констант внутри классов: static const int namelen - 16; static const int phonelen = 16; static const addrlen = 100; Но» к сожалению, большинство современных компиляторов не поддер- живает данный синтаксис определения статических констант-членов, хотя этот подход можно использовать для определения глобальных ста- тических констант. Как определено в строках 31-34, для сохранения данных полей адреса использу- ются классические (в стиле языка С) текстовые строки фиксированной длины с ну- левым окончанием. Символы строк просто занимают последовательные ячейки мас- сива символов. Так, для массива lastname__ задан размер 16 символов, но сохранить в нем можно не более 15 текстовых символов, так как последним всегда должен быть нулевой символ. Справочная информация: строки с нулевым Экскурс окончанием Для представления текстовой информации в программах C/C++ часто используются строки символов с нулевым окончанием. Они представ- ляют собой массивы символов с нулевым символом в последней ячейке. Нулевой символ обозначает окончание строки. В С и C++ в этих целях используется литерал \0. Длина строки рассчитывается без учета за- вершающего нулевого символа, но его следует учитывать при определе- нии массива. Термин Термин Текстовая строка с нулевым окончанием представляет собой массив последовательных символов с литералом \ 0 для обозначения окон- чания строки. Строковым литералом называется последовательность символов, ко- торая в коде программы заключена в парные кавычки. Компилятор ав- томатически преобразовывает строковый литерал в текстовую строку с нулевым окончанием. Текстовые строки с нулевым окончанием сохраняются в массивах типа char. Строка, объявленная как char string [15], может содержать 14 символов текста и нулевой символ окончания строки. В языках С и C++ при вызове функции текстовые строки передаются как указатели на пер- вый элемент массива. Так, указатель char* может представлять текстовую строку с нулевым окончанием. Но при этом вам следует помнить, что указа- тель обязательно должен быть связан с реальным элементом. Нельзя ис- пользовать в программе указатель на char как строку с нулевым окончани- ем, если предварительно данный указатель не был связан с конкретным элементом массива символов. В противном случае указатель возвратит случайное значение из оперативной памяти, что может привести к зависа- нию программы. 40 Глава 2. Реализация класса Address для работы...
Строки, заключенные в парные кавычки, называются строковыми ли- тералами. Строковый литерал "Hello" имеет тип const char[6] (требуется массив из 6 символов, поскольку один элемент массива отво- дится нулевому окончанию). Преобразование строк в массивы происхо- дит в C++ автоматически, поэтому возможна следующая строка кода: const char* р = "Hello"; где р определяется как указатель на пер- вую букву Н в слове Hello. В строке 11 объявляется конструктор, который делает все строки пустыми в мо- мент инициализации. Пустые строки содержат только нулевые окончания в первых элементах массивов. Переменные объекта Address не требуют каких-то особых приемов удаления, поэтому нам не нужно определять деструктор. Для данного клас- са также вполне подойдут заданные компилятором по умолчанию конструктор- копировщик и оператор присваивания. В строке 14 определяется и выполняется функция доступа для чтения, которая возвращает значение lastname__ в поле фамилии. Функция возвращает указатель на константный массив символов, в результате чего пользователь не может с помощью данного интерфейса изменить фамилию в записи. В строке 15 объявляется функция доступа для записи в поле фамилии. Подобные пары функций доступа определены для всех полей (строки 17-24). До сих пор мы не использовали никаких средств из стандартной библиотеки, а также директиву #include. Посмотрим теперь на листинг 2.2, где показана реали- зация класса Address и для копирования строк используется функция strcpy из стандартной библиотеки. Листинг 2.2. Реализация класса Address на основе массивов символов с фиксированными размерами Ч . я ' ..... ’ ж ' Ч J • * ’ • . Д 1://TinyPIM (с)1999 Pablo Halpern. Файл Address.срр 2: 3:#include "Address.h" 4:#include <cstring> 5: 6- .Address: :Address () 7: { 8: // Инициализация всех строк пустыми значениями. 9: lastname__ [0] = f irstname__ [ 0] = phone_[0] = address__[0] = ’\0’; 10: } 11: 12:void Address::lastname(const char*s) 13: { 14: std: : strcpy (lastname__, s) ; 15: } 16: 17:void Address::firstname(const char*s) 18: { 19: std: : strcpy (firstname__, s) ; 20: } 21: 22:void Address::phone(const char*s) 23: { 24: std: : strcpy (phone__, s) ; 25: } Использование строк с фиксированной длиной 41
26: 27:void Address::address(const char*s) 28: { 29: std::strcpy(address_,s); 30: } В строке 4 вставлен файл заголовка cstring, в котором объявляются стандарт- ные библиотечные функции манипулирования текстовыми строками с нулевыми окончаниями. Тот факт, что имя файла заключено в угловые скобки (о), говорит компилятору о том, что вставляется заголовок из стандартной библиотеки. Замена обычных прямых кавычек на угловые скобки сокращает время поиска компилято- ром необходимого файла заголовка. Стандарты языка C++ позволяют, чтобы содер- жимое библиотечных заголовков было встроено в компилятор, а не хранилось в от- дельных файлах на диске. Всегда при добавлении стандартных библиотечных заголовков исполь- 33 М етку зуйте угловые скобки (о), а при добавлении нестандартных файлов за- головков— прямые кавычки Если вам приходилось ранее программировать на С или C++, то вас может удивить, что вместо заголовка <string.h> используется <cstring>. Это относи- тельно недавнее изменение в синтаксисе стандартной библиотеки C++. Комитет по стандартизации ISO принял решение упразднить расширения . h, чтобы уни- фицировать использование расширений в файлах заголовков C++. (Одни изгото- вители компиляторов использовали . h, другие . hxx и . hpp.) Приняв решение об упразднении расширений, комитет испортил жизнь в равной мере всем изгото- вителям компиляторов. Первая с в имени заголовка указывает на то, что он унаследован из стандартной библиотеки языка С. В соответствии со станда^ом C++, все элементы, используемые в библиотеке языка С, должны быть доступны в стандартной библиотеке C++, причем их имена изменены следующим спосо- бом: расширение . h удалено, а в начале имени добавлена дополнительная буква с в нижнем регистре. На Особенности компиляции. Некоторые изготовители компиляторов замётку еще не перешли на новые соглашения об именах файлов заголовков. Чтобы избежать ошибок компиляции, вам, возможно, придется доба- вить к имени расширение .h и удалить первую букву с. Другое реше- ние — создать собственные файлы заголовков и сохранить их под стан- дартными именами. Причем в ваших пользовательских файлах заго- ловков достаточно ввести единственную директиву #include и указать соответствующий файл изготовителя. Строка 9 конструктора делает все строки пустыми, присваивая первым элемен- там массивов символ окончания строки. Для корректной работы библиотечных функций очень важно правильно инициализировать все массивы символов. К сожа- лению, компилятор не может по умолчанию задать корректные значения массивам символов, выступающих в качестве переменных-членов класса. В строке 14 используется первая библиотечная функция std: : strcpy. Функция strcpy определяется в заголовке <cstring>. Префикс std: : означает, что эта функция, как и все другие идентификаторы стандартной библиотеки, принадлежат пространству имен std. 42 Глава 2. Реализация класса Address для работы...
Экскурс Справочная информация о пространствах имен Пространства имен были добавлены в язык C++ всего несколько лет то- му назад и только с недавних пор реализуются компиляторами основ- ных изготовителей. Пространство имен определяется в коде программы следующим образом: namespace stuff { int g(); extern int x; class A { public: int f() {return g() + x;} }; } На первый взгляд определение stuff выглядит как определение класса. Но в действительности stuff является не типом данных, а группой взаимосвязанных имен. Пространство имен stuff объединяет три идентификатора, принадлежащих этому пространству: д, х и А. Вне пространства имен необходимо использовать полные имена со специ- фикатором : stuff: :g, stuff: :х и stuff: :А. Внутри пространства имен необходимость в спецификаторе отпадает, как, например, в функции stuff: : А: : f делаются ссылки на g и х. В отличие от классов, к объявлению пространства имен в программе можно возвращаться несколько раз. Если возникла необходимость до- бавить новый идентификатор в существующее пространство имен, это можно сделать следующим образом: namespace stuff { enum colors {red, green, blue}; } Тип данных colors и константы red, green и blue теперь стали час- тями пространства имен stuff, так же, как идентификаторы д, х и А. Благодаря этому пространство имен может быть разделено между не- сколькими файлами заголовков. Пространства имен используются для предупреждения конфликтов имен, которые могут возникать во время компоновки программного кода из раз- ных файлов источников. Объектно-ориентированная природа языка C++ предполагает активное использование независимых библиотек функций и классов многоразового использования. Велика вероятность того, что в не- зависимой библиотеке и в вашей программе могут найтись два идентифи- катора с одинаковыми именами, например print, в результате чего между разными частями программы возникнет конфликт имен. Для предупреж- дения подобных конфликтов изготовители библиотек вводят все свои эле- менты в уникальное пространство имен, заданное, например, по названию компании. Комитет, по стандартизации выбрал для стандартной библиоте- ки C++ пространство имен std. В стандарте также оговорено, что никто другой не может использовать это имя. Существуют три подхода для облегчения бремени, которое налагает на пользователей необходимость использования пространства имен при Использование строк с фиксированной длиной 43
обращении к независимым библиотекам. Прежде всего, это использо- вание псевдонимов пространств имен, как в следующем примере: namespace mlnn = my_long_namespace__name ; Суть этого объявления состоит в том, что теперь вместо my_long_namespace_name: : f () можно использовать обращение mlnn: : f (). Если для вас по-прежнему утомительно записывать даже сокращенное имя, можете импортировать выбранные литералы в свое пространство имен, используя для этого следующий синтаксис: using stuff::g; Далее в программе после этой строки можно вместо stuff: :g() ис- пользовать обращение g (). Импортирование имен выполняется обыч- но для часто используемых функций. Если же вы хотите упростить дос- туп ко всем литералам некоторого пространства имен, то для этого применяется третий способ, который состоит в импортировании с по- мощью ключевого слова using всего пространства имен: using namespace stuff; В программе после данной строки можно будет свободно обращаться ко всем литералам внешнего пространства имен без использования его имени в качестве идентификатора. Но подходите с осторожностью к использованию команды using. Импортирование литералов из внеш- него пространства имен всегда сопряжено с вероятностью возникнове- ния двусмысленностей и конфликтов имен. Эта вероятность особенно высока при импортировании без разбору всего внешнего пространства имен в код программы, который ранее уже был скомпилирован. Чтобы использовать функции, типы и переменные из стандартной библиотеки, нужно всегда добавлять к их имени спецификатор std:: или импортировать их с помощью команды using. Функцию доступа для записи в поле фамилии можно бы- ло записать еще следующим образом: void Address::lastname(const char* s) { using std::strcpy; // Импортирование std::strcpy strcpy(lastname_, s); // Вызов функции strcpy без std:: } или импортировать целиком все пространство имен std: using namespace std; // Импортирование всего пространства имен std void Address::lastname(const char* s) { strcpy(lastname_, s); // Вызов функции strcpy без std:: } В последнем случае появляется возможность использования в программе любого литерала из стандартной библиотеки без префикса std: Следует помнить при этом, что в новой версии стандартной библиотеки добавлено много новых иденти- фикаторов, которых не было в прежних версиях стандартной библиотеки C++. Неко- торые из этих идентификаторов представлены простыми словами типа copy, set и map. Поэтому теперь, после расширения стандартной библиотеки, значительно возросла вероятность того, что ее литералы могут вступить в конфликт с глобаль- ными именами вашей программы. Таким образом, не рекомендуется импортировать целиком все пространство имен std в программу, где были объявлены глобальные литералы, за исключением тех случаев, когда вы хотите перекомпилировать старую программу с помощью нового компилятора. 44 Глава 2. Реализация класса Address для работы...
ца Общий принцип. За исключением макросов, все идентификаторы Заметку стандартной библиотеки C++, включая унаследованные из языка С, принадлежат пространству имен s td. Вам сейчас может показаться утомительным использовать спецификатор std: : перед каждым идентификатором из стандартной библиотеки, включая такие широ- ко используемые команды, как cout. Но поверьте моему опыту, скоро этот специфи- катор не только перестанет вас раздражать, но даже понравится. Его использование облегчает чтение кода, так как наглядно документирует каждое обращение к стан- дартной библиотеке. Следуя моим рекомендациям, известную программу “Hello, world” можно переписать следующим образом: #include <iostream> int main() { std: : cout <<"Hello, world1' « std::endl; return 0; } Впрочем, если в вашей программе команды cout и endl встречаются достаточно часто, можете упростить свою работу, импортировав их с помощью команды using: #include <iostream> using std::cout; using std::endl; int main() { cout «"Hello, world" << endl; return 0; } ца Особенности компиляции. Некоторые компиляторы не поддер- ЗЭметку живают использование пространств имен и не определяют стан- дартное пространство имен std. Если вы хотите написать код для > этих компиляторов, но таким образом, чтобы его можно было ском- пилировать в будущем на более современном компиляторе, исполь- зуйте следующий макрос: # if def _NO_NАМЕSPACES_ #define std #endif Вам нужно будет определить где-нибудь NONAME SPACE, скорее всего, в командной строке компилятора. Но в некоторых стандартных биб- лиотеках, включая наиболее популярную бесплатную библиотеку SGI, это определение делается автоматически. Библиотека, распространяемая вместе с компилятором Microsoft Visual C++ 6.0, вставляет библиотечные элементы в пространство имен std, за ис- ключениемтех из них, что были унаследованы из языка С. В результате для обращения к литералам, пришедшим из языка С, не требуется специфика- тор s td::. Все жалобы — к разработчикам компании Microsoft. Функция std: : strcpy имеет следующий прототип: namespace std { char* strcpy(char* to, const char* from); } Использование строк с фиксированной длиной 45
Эта функция должна быть знакома многим читателям. Она создает копию строки с нулевым окончанием путем копирования последовательности символов из масси- ва, на который указывает from, в массив, на который указывает to. Работа функции автоматически прекращается после копирования символа окончания строки. Если последовательности символов, заданные from и to, перекрываются, или если мас- сив, заданный указателем to, недостаточно большой для того, чтобы вместить стро- ку, заданную указателем from, возникает ситуация неопределенности, так как функ- ция strcpy не выполняет контроль за ошибками. W Ситуация неопределенности возникает в результате некорректного вы- I ермин полнения некоторых операций. Обычно возникновение ситуаций неоп- ределенности заканчивается зависанием программы. Функция strcpy возвращает свой первый аргумент, т.е. указатель на новую ко- пию строки. Обратите внимание, что функция strcpy копирует символы в уже су- ществующий массив. При выполнении этой функции не происходит выделение до- полнительных ячеек памяти. Вернемся снова к строке 14 листинга 2.2, в которой функция Address : : lastname (const char*) копирует последовательность символов, пе- реданную с аргументом s, в переменную-член lastname_. Все другие функции доступа для записи в поля работают аналогичным образом, копируя введенные строки в переменные-члены. В листинге 2.3 показана небольшая программа для тестирования класса Addrе s s. Листинг 2.3, Программатестирования icnaccaAddress i; 1://TinyPIM (c)1999 Pablo Halpern. Файл testl.cpp 2: 3:#include <iostream> 4: 5:#include "Address.h" 6: 7:void dump(const Address&a) 8: { 9: std::cout « a.firstname() « ’ • « a.lastname() « ’\n’ 10: « a.address() « ’\n’ « a.phone() « ’\n’ 11: « std::endl; 12: } 13: 14: int main() 15: { 16: Address a; 17: a. lastname ("Smith") ; 18: a.firstname("Joan"); 19: a.phone(”(617)555-9876"); 20: a.address.("The Very Big Corporation \nSomewhere,MA 01000"); 21: dump(a); 22: 23: // Вводится новый номер телефона 24: a.phone(”(617)555-7777 ext.112"); 2 5: dump(a); 26: 27: return 0; 28: } 46 Глава 2. Реализация класса Address для работы...
В строке 3 в программу добавляется файл заголовка ввода-вывода. В соответ- ствии с ранее рассмотренным соглашением, имя файла заключено в угловые скобки, расширение . h отсутствует. Но поскольку этот заголовок не был унасле- дован из языка С, в начале имени префикс с отсутствует. В файле заголовка <iostream> определяются основные элементы стандартной библиотеки потоков, включая глобальные потоки с in, с out и сегг. Более подробно о стандартной библиотеке потоков мы поговорим в главе 5, когда перейдем к реализации клас- сов редакторов. Сейчас мы просто будем использовать поток cout и оператор endl (end-of-line— конец строки). В строках 7-12 выполняется функция dump, которая просто выводит перемен- ные-члены класса Address в стандартный поток вывода cout, используя для этого идентификаторы cout и endl со спецификатором пространства имен s td: :. В строке 16 создается объект класса Address, а в строках 17-20 заполняются поля адреса. Обратите внимание, что строка адреса содержит символ разрыва строки (\п), в результате чего адрес записывается в две строки. Значения пере- менных-членов первого объекта Address выводятся на печать вызовом функции dump в строке 21. В строке 24 мы изменяем номер телефона в объекте, а в строке 25 выводим объект на печать. Но если мы посмотрим на значения, выводимые программой, как это показано в листинге 2.4, то обнаружим, что не все идет правильно. l:Joan Smith 2:The Very Big Corporation 3:Somewhere, MA 01000 4:617) 555-9876 5: 6:Joan Smith 7:xt. 112 8: (617) 555-7777 ext. 112 Программа работает не так, как ожидалось. Исходный объект Address выводится в строках 1-4. В измененном объекте имя и номер телефона также выводятся пра- вильно в строках 6, 8. Но в строке 7 выводится что-то непонятное. Вместо адреса появились последние символы из поля номера телефона. Что же произошло? Проблема возникла из-за того, что номер телефона, который мы попытались ввести, содержит больше символов, чем выделенный для него массив. Поскольку в языках С и C++ массив и указатель на массив семантически однозначны, функ- ция strcpy не знает физического размера массивов, с которыми связаны ее аргу- менты. Если целевой массив оказывается меньше массива источника (включая нулевой символ окончания строки), скопированная последовательность символов продолжается за пределы целевого массива. В нашем примере полю address_ в памяти компьютера отведены ячейки, следующие сразу за ячейками поля phone_. При использовании большинства компиляторов результат будет такой, какой мы видели в выводе тестовой программы. Содержимое поля адреса будет за- мещено окончанием строки номера телефона. Хотя запись за пределы массива яв- ляется типичной причиной возникновения состояния неопределенности, резуль- татом может быть зависание программы. Использование строк с фиксированной длиной 47
К счастью, о чем знают все ветераны программирования на С, стандартная биб- лиотека предлагает для копирования строк более совершенную функцию strncpy со следующим прототипом: char* strncpy(char* to, const char* from, size_t n); Дополнительный параметр n типа std: : size_t определяет максимальное число символов для копирования. Использование функции strncpy для копирования строк позволяет предупредить возникновение ситуаций неопределенности. В листинге 2.5 показана модифицированная реализация класса Address, все из- менения в котором выделены полужирным шрифтом. Листинг 2.5. Использование strncpy для пре^уф^ждения записи за пределы массива / ; Si'.:z:.:. , ‘ ' ... . ,, л'Л. « х 4. ,, ... , . > > х ’ . < % Ы г * J w4 » ' U / х I /Л , . .> ,, к \ 1://TinyPIM (с)1999 Pablo Halpern, Файл Address.срр 2: 3:#include <cstring> 4: 5:#ifndef _MSC_VER б:// Эти объявления не нужны для компиляторов компании 7:// Microsoft, поскольку в их библиотеках данные функции 8:// не введены в пространство имен std. 9:using std::strncpy; 10:#endif 11: 12:#include "Address.h" 13: 14:Address::Address() 15: { 16: // Инициализация всех строк пустыми значениями. 17: lastname_[0] = firstname_[0] = phone_[0] = address_[0] = ’\0’; 18: } 19: 20:void Address::lastname(const char*s) 21: { 22: strncpy(lastname_, s, namelen); 23: } 24: 25:void Address::firstname(const char*s) 26: { 27: strncpy (firs tname__, s, namelen); 28: } 29: 30:void Address::phone(const charts) 31: { 32: strncpy(phone_, s, phonelen); 33: } 34: 35:void Address::address(const charts) 36: { 37: strncpy(address_, s, addrlen); 38: } Изменения не затронули файл Address.h и основную программу, поскольку интер- фейс класса остался прежним. Мы модифицировали только функции доступа д ля записи. 48 Глава 2. Реализация класса Address для работы...
В строках 22, 27 и 37 функция strcpy была заменена на strncpy. Функция strncpy практически в точности соответствует функции strcpy, за исключением того, что она принимает третий параметр, который опреде- ляет размер принимающего массива. Функция strncpy не копирует больше символов, чем задано в параметре п, что предупреждает запись за пределы массива. Строки 5-10 предназначены исключительно для компиляции программы на компиляторах компании Microsoft. В библиотеке, встроенной в компилятор Microsoft C++ 6.0, функции, унаследованные из языка С, не введены в простран- ство имен std. Чтобы написать код, совместимый с разными компиляторами, мы определяем условие не использовать компилятор Microsoft и при выполнении условия импортируем функцию std: : strncpy в глобальное пространство имен, в результате чего ее можно будет вызывать без указания спецификатора std: :. Еще раз подчеркну, что команда using будет выполняться только при условии, что используется компилятор любой другой компании, кроме Microsoft. В стро- ках 22, 27, 32 и 37 функция strncpy вызывается без спецификатора std: :, что стало возможным для большинства компиляторов благодаря импортированию функции с помощью using. Теперь тестовая программа, с которой мы работали ранее, выведет значения, по- казанные в листинге 2.6. Листинг 2.6. Вывод программы тестирования в случае использования функции > strncpy ' ; l:Joan Smith 2:The Very Big Corporation 3:Somewhere, MA 01000 4: (617) 555-9876 5: 6:Joan Smith 7:The Very Big Corporation 8:Somewhere, MA 01000 9: (617) 555-7777 eThe Very Big Corporation 10:Somewhere, MA 01000 11: Выглядит лучше, но все равно не так, как хотелось бы. По крайней мере, наши изменения не испортили вывод программы в строках 1-4, которые выводились пра- вильно и прошлый раз. В строках 7, 8 поле адреса повторно было выведено правиль- но. Но в строках 9, 10 мы видим что-то совершенно странное. При более тщательном рассмотрении можно заметить, что строка номера телефона прерывается после бук- вы е и к ней добавляется строка адреса. Чтобы понять, что произошло, необходимо войти в процедуру на шаге вызова a.phone ("(617) 555-7777 ext. 112"). (Если хотите, можете воспользоваться системой отладки вашего компилятора для выполнения описанных ниже процедур.) Функция strncpy копирует 16 символов из строки ввода в массив phone_, заполняя его символами (617) 555-7777 е. Чего не хватает, так это символа окончания стро- ки. В результате поток вывода продолжает считывать символы из ячеек памяти до тех пор, пока не обнаружит символ завершения строки, который в нашем случае оказывается в конце поля address . Считывание данных за пределами массива — это такая же ситуация неопределенности, как и при записи за пределы массива, в ре- Использование строк с фиксированной длиной 49
зультате которой также может произойти зависание программы при попытке вывес- ти бесконечный ряд символов. Чтобы избавиться от этой новой проблемы, нам нужно убедиться, что после вы- полнения функции strncpy все строки заканчиваются нулевыми символами окон- чания. В листинге 2.7 показан один из способов удостовериться в этом. 1://TinyPIM (с)1999 Pablo Halpern. Файл Address.срр 2: 3:#include <cstring> 4: 5:#ifndef _MSC_VER 6:using std::strncpy; 7:#endif 8: 9:#include "Address.h" 10: 11:Address : .-Address () 12: { 13: // Инициализация всех строк пустыми значениями. 14: lastname__[0] = firstname__[0] = phone_[0] = address_[0] = ’\0’; 15: } 16: 17:void Address::lastname(const char*s) 18: { 19: strncpy (las tname_, s, namelen - 1) ; 20: lastname_[namelen - 1] = ’\0'; 21: } 22: 23:void Address::firstname(const char*s) 24: { 25: strncpy (firs tname__, s, namelen - 1); 26: firstname_[namelen - 1] = ’\0’; 27: } 28: 29:void Address::phone(const char*s) 30: { 31: strncpy (phone__, s, phonelen - 1) ; 32: phone__ [phonelen - 1] = *\0’; 33: } 34: 35:void Address::address(const char*s) 36: { 37: strncpy(address^, s, addrlen - 1); 38: address_[addrlen - 1] = '\0*; 39: } В строке 31 в массив phone_ копируется на один символ меньше, чтобы оставить место нулевому символу окончания. В строке 32 мы добавляем нулевой символ окончания в конец строки на тот случай, если он отсутствовал в последовательности символов, связанной с указателем s. Окончательный вывод программы проверки показан в листинге 2.8. 50 Глава 2. Реализация класса Address для работы...
Листинг 2.8. Вывбд прбгоам^тйтЖ^ИйЭДОе^обавЛения нулевых 1 , символо&окончаний; • йыа*-*’ • " ” ’“ й? J l\ vzv> л&^*А\>х U5 ZHw.J'.L»! xs£ iSlL JMZ4. S Л Л «Л * «' I XZuXL&/As^ -, л _<1*-^..~h./ l:Joan Smith 2:The Very Big Corporation 3:Somewhere, MA 01000 4:617) 555-9876 5: 6:Joan Smith 7:The Very Big Corporation 8:Somewhere, MA 01000 9: (617) 555-7777 10: Мы видим, что в строке 9 был выведен измененный номер телефона, а все осталь- ные строки были такими же, как и при первом выводе. Но номер телефона был обре- зан на 15-м символе (15-м символом был пробел после последней цифры 7), 16-м символом стало нулевое окончание строки, а оставшаяся часть номера была полно- стью утеряна. Наша программа предельна проста, тем не менее, мы подошли к пре- делу возможностей использования строк с фиксированной длиной. Подводя итоги, можно назвать следующие преимущества использования массивов с фиксирован- ными размерами д ля сохранения строк текста: • простота программирования; • отсутствие проблем с управлением памятью, так как память выделя- ется автоматически при создании массива и автоматически очища- ется при удалении объекта. В то же время следует отметить недостатки: • высока вероятность допущения ошибок при расчете необходимого размера массива, что может привести к возникновению ситуаций неопределенности; • если массив оказывается слишком мал для сохранения строки, часть информации теряется безвозвратно (либо возникают ошибки во вре- мя выполнения программы); • если сохраненные строки текста малы по сравнению с выделенными для их сохранения массивами, программа слишком неэффективно расходует память компьютера. Ниже мы устраним многие из перечисленных недостатков, заменив массивы с фиксированными размерами на массивы с динамическим выделением памяти. Но, как мы еще не раз убедимся, каждое разрешение одной проблемы всегда рождает до- полнительные вопросы и проблемы. Использование массивов в буфере динамического распределения памяти Прежде чем мы перейдем к изучению библиотеки классов строк, давайте ос- тановимся на еще одной возможности использования строк с нулевым оконча- нием. Попробуем разрешить некоторые проблемы, с которыми мы столкнулись Использование массивов в буфере ... 51
ранее, используя массивы символов в области динамического распределения памяти. Благодаря тому, что память в этом случае выделяется динамически по запросу, мы всегда можем получить достаточно большой массив для сохранения строки любого размера. Звучит очень просто, но в действительности мы столк- немся с целым рядом проблем, для которых нужно будет найти корректные ре- шения. Знакомство с этими проблемами и с методами их устранения поможет вам лучше понять назначение Многих компонентов стандартной библиотеки и способы их использования. Если вы бывалый программист на C++, то вам, безусловно, будут знакомы про- блемы и методы их решений, опйсанные Ниже. В таком случае вы можете пропус- тить материал данного раздела и перейти сразу к обобщению в конце раздела. Использование указателей на динамически распределяемую память При работе со строками было бы здорово иметь возможность выделять ровно столько памяти, сколько необходимо для сохранения данной конкретной строки. Наиболее об- щий способ решения этой проблемы — использование области динамически распреде- ляемой памяти и указателя на первый элемёнт выделенного массива. В листинге 2.9 по- казан новый вариант определения класса Address в файле Address. h. В этой версии мы используем указатель на область динамически распределяемой памяти. Листинг 2.9. Определение класса Ь использованием массивов символов в областй^дИнамичйкого распределения памяти 1://TinyPIM (с)1999 Pablo Halpern/ Файл Address.h 2: 3:#ifndef Address__dot_h 4:#define Address__dot__h 1 5: 6: // Реализация класса Address с Использованием динамически выделяемых строк 7:class Address 8: { 9:public: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: // Конструктор Address(); // Деструктор ** Address () ; // Конструктор-копировщик и оператор присваивания Address(const Address&); const Address&operator—(coftst Address&); // Функции доступа к полям const char* lastname()cbnst {return lastname_;} void lastname(const char*); const char* firstname()const {return firstname_;} void firstname(const char*); 52 Глава 2. РеалйЗсЩИЯ класса Address для работы...
Термин 26: 27: const char* phone()const {return phone_;} 28: void phone(const char*); 29: 30: const char* address()const {return address_;} 31: void address(const char*); 32: 33:private: 34: // Поля данных 35: char* lastname_; 36: char* firstname__; 37: char* phone_; 38: char* address ; 39: 40: // Закрытая функция для копирования строк: 41: char* dup(const char* s); 42: }; 43: 44:#endif //Address dot h В строках 35-38 массивы символов были заменены на указатели. Вспомните, что в С и C++ массив всегда можно заменить указателем на первый элемент массива. Опытные программисты знают, что появление char* в программе почти всегда сви- детельствует об использовании текстовой строки с нулевым символом окончания. Поскольку в данном варианте программы строки могут быть произвольной длины, мы удалили из кода программы строку с перечислением определений максимально допустимых длин строк. В строке 41 объявляется закрытая функция dup, которая используется для созда- ния дубликатов строк в области динамического распределения памяти. Эта функция возвращает указатель на массив символов, заданный с помощью new [ ] и содержа- щий копию строки, с которой был связан указатель s (включая нулевой символ окончания строки). О работе функции dup мы поговорим более подробно, когда рас- смотрим ее реализацию. В строке 14 объявляется деструктор класса Address. В данном варианте про- граммы, когда мы используем область динамического распределения памяти, появ- ляется необходимость в деструкторе, который будет освобождать память во время удаления объекта Address. Другими нововведениями стали конструктор- копировщик и оператор присваивания, объявленные в строках 17, 18 в дополнение к конструктору по умолчанию, объявленному в строке 11. Необходимость в этих эле- ментах станет понятной, когда мы перейдем к реализации класса. Конструктором по умолчанию называется конструктор без аргументов. Если в классе не определен никакой конструктор (с аргументами или без), то компилятор автоматически создает конструктор по умолчанию, который используется для всех базовых классов программы и их пере- менных-членов . Термин Конструктор-копировщик— это конструктор, который можно вызы- вать с объектом этого же класса в качестве аргумента. Назначение кон- структора-копировщика состоит в создании копии объекта. Если в классе не определен конструктор-копировщик, то компилятор создает его по умолчанию для всех базовых классов программы и их перемен- ных-членов. Использование массивов в буфере... 53
т Оператор присваивания выглядит как знак равенства (=) и используется для »“РМИН копирования значений одного объекта в другой объект того же типа. Если оператор присваивания не будет определен в классе, компилятор автома- тически создает оператор присваивания по умолчанию, который вызыва- ется д ля всех базовых классов программы и их переменных-членов. Обратите внимание, что все остальные открытые члены класса Address не были изменены. Другими словами, интерфейс класса Address остался прежним, несмот- ря на то, что реализация его изменилась. В листинге 2.10 показан новый вариант кода реализации класса Address, кото- рый хранится в файле Address.cpp. Поскольку большая часть программного кода была изменена, не было смысла выделять изменения полужирным шрифтом. ; Листинг 2.10. Реализация класса Address с использованием массивов символов в области динамического распределенияпамяти 1://TinyPIM (с)1999 Pablo Halpern, Файл Address.cpp 2: 3:#include <cstring> 4: 5:#ifndef __MSC_VER 6:using std::strcpy, std::strlen; 7:#endif 8: 9:#include "Address.h" 10: 11:// Конструктор 12:Address::Address() 13: : lastname_(new char [1]), 14: firstname_(new char [1]), 15: address_(new char [1]), 16: phone (new char [1]) 17: { 18: // Инициализация всех строк пустыми значениями. 19: 20: } lastname_[0] = firstname_[0] = phone_[0] = address_[0] = ’\0’; 21: 22:// Деструктор 23:Address::-Address() 24: { 25:// Очистка памяти 26: delete[] lastname_; 27: delete[] firstname_; 28: delete[] phone_; 29: delete[] address_; 30: } 31: 32:char* Address::dup(const char* s) 33: { 34: // Выделение памяти по размеру строки с учетом нулевого окончания 35: char* ret = new char[strlen(s) + На- 36: 37: // Копирование строки во вновь созданный массив 38: strcpy(ret, s); 39: 40: return ret; 41:} 54 Глава 2. Реализация класса Address для работы...
42: 43:// Конструктор-копировщик 44:Address::Address(const AddresS& a2) 45: : lastname_ (0) , firstname_(0), phone_(0), address_(0) 46: { 47: // Выполнение этой сложной задачи с помощью оператора присваивания 48: *this = а2; 49: } 50: 51:// Оператор присваивания 52:const Address&Address::operator®(const Address& a2) 53: { 54: if (this != &a2) 55: { 56: lastname (a2 . lastname_J ; 57 : f irstname (a2. firstname__) ; 58: phone (a2 .phone_J ; 59: address(a2.address^); 60: } 61: 62: return *this; 63: } 64: 65:void Address::lastname(const char* s) 66: { 67: if (lastname_ ! = s) 68: { 69: delete[] lastname_; 7 0: las tname__=dup (s) ; 71: } 72: } 73: 74:void Address::firstname(const char* s) 75: { 76: if (firstname_! - s) 77: { 78: deleted firstrtame_; 79: firstname__ = dup (s'); 80: } 81: } 82: 83:void Address::phone(const char* s) 84: { 85: if (phone_ != s) 86: { 87: delete[] phone_; 8 8: phone_ - dup(s); 89: } 90: } 91: 92:void Address::address(const char* s) 93: { 94: if (address_ 1= s)‘. 95: { 96: delete [] address^; 97: address__ = dup(s); 98: } 99: } Использование массивов в буфере... 55
В строках 13-16 выделяются исходные массивы для всех полей. Будет вполне достаточно, если исходные массивы вместят только символы окончания строк. Тело конструктора не было изменено. В строках 23-30 деструктор освобождает всю па- мять, занятую объектом Address в области динамического распределения. Функция dup копирует заданную строку в область динамического распределения памяти. Нам необходимо выделить в этой области достаточный объем памяти, что- бы сохранить всю строку, включая нулевой символ окончания. Выделение памяти происходит в строке35. Функция std::strlen также определяется в файле <cstring>. Она возвращает число символов в строке без учета символа окончания. Поскольку в строке 6 мы импортировали эту функцию с помощью команды using, нет необходимости использовать спецификатор std: :. В строке 38 мы используем функцию strcpy для копирования s в выделенную область динамической памяти. Обратите вйймание, что функция strncpy в данном случае будет неуместна и неэффективйа, поскольку мы уверены, что выделенной па- мяти будет достаточно для сохранения строки, переданной с указателем s. Строка 40 возвращает только что скопированную строку. Утечка памяти и повторное удаление Забудем на некоторое время о конструкторе-копировщике и операторе присваи- вания и рассмотрим еще один пример использования функции доступа для записи lastname(const char* s): void Address::lastname(const char* s) {lastname_ = s;} Мы могли бы просто скопировать указатель, но это вызовет серьезную проблему. Предположим, что s указывает на строку Jones, a lastname_ — на Doe, как показано на рис. 2.1. Рис. 2.1. Значения указателей до изменения las tname_ После выполнения присваивания оба указателя будут связаны с одним и тем же значением, как показано на рис. 2.2. Рис. 2.2. Значения указателей после выполнения операции присваивания При этом возникают две проблемы. Во-первых, данные, с которыми ранее был связан указатель lastname_, становятся недоступными и блокируют часть памяти, которая становится бесхозной. Это явление называется утечкой памяти. Во-вторых, что еще более важно, деструктор класса при удалении объекта Address будет пы- таться освободить блок памятй, на который теперь указывает lastname_. Но другой объект Address также может иметь указатель на этот же блок памяти. Если несколь- ко объектов попытаются освободить один й тот же блок памяти, то, как вы уже дога- дались, возникнет ситуация неопределенности. 56 Глава 2. Реализация класса Address для работы...
Термин Термин Бесхозной памятью называется занятый блок ячеек динамической па- мяти, с которой не связан ни один указатель. Эту память невозможно возвратить в буфер динамического распределения. Утечка памяти возникает в результате логических ошибок в програм- ме, в результате чего блоки памяти не освобождаются даже после удале- ния связанных с ними объектов. В случае утечки памяти постепенно, по мере работы программы, блокируется вся доступная память компьюте- ра, что приводит к зависанию компьютера и потере данных. Чтобы избежать утечки памяти и возникновения ситуаций неопределенности, необ- ходимо выполнять глубинное копирование текстовых строк. Другими словами, нужно ко- пировать не указатели, а связанные с ними объекты. Затем, прежде чем изменить указа- тель, нужно освободить память, с которой он был связан. Этот подход показан на рис. 2.3. Рис. 2.3. Значения указателей после глубинного копирования После глубинного копирования каждый указатель связывается со своим блоком памяти. Изменение или удаление одного из них не влияет на другие указатели. Об- ласть памяти, на которую ранее указывал lastname_, возвращается в буфер дина- мического распределения. Термин Глубинное копирование означает копирование всех непрямых подобъ- ектов в один объект таким образом, что источник и копия объекта не содержат ссылок на одни и те же подобъекты. Вернемся теперь к реализации функции 1 astname (const char *) в строке 69 листин- га 2.10. Чтобы избежать утечки памяти, перед присвоением указателю lastname_ нового значения мы освобождаем блок памяти, с которым он был связан ранее. Затем в строке 70 используется функция dup для создания новой копии строки, с которой был связан указатель s, и возвращения результатам указателем lastname_. Самоприсвоение Строка 67 содержит интересное выражение. Предположим, есть две ссылки а и b на объекты типа Address, и вы хотите, чтобы оба объекта содержали одинаковые за- писи в полях lastname. Можно представить следующее выражение для выполнения этой задачи: а.lastname(b.lastname()); Но что произойдет, если окажется, что а и b в действительности ссылаются на один и тот же объект? Строка 69 сначала удалит массив lastname_, а затем стро- ка 70 будет пытаться скопировать уже удаленный массив! Чтобы предупредить Использование массивов в буфере... 57
ошибку, строка 67 прежде всего проверяет, не имеют ли два указателя одинаковые значения. Если значения одинаковы, то все поля и так соответствуют сами себе, т.е. нет необходимости еще что-либо делать. Функции доступа для записи в поля f irstname, phone и address работают точно так же, как и функция доступа для за- писи в поле lastname. Конструктор-копировщик и оператор присваивания Теперь обратим внимание на конструктор-копировщик и оператор присваива- ния, реализация которых повторена в листинге 2.11. Если в программе не будут оп- ределены операции копирования, то компилятор по умолчанию производит копиро- вание указателей, а не объектов. В результате появляются бесхозные блоки памяти и возникают ситуации неопределенности, как показано на рис. 2.2. Поэтому для вы- полнения глубинного копирования всех указателей-членов важно определить собст- венные операции копирования. . Листинг 2Л1. Конструктор<опй|ювщ|^%0о^а]Й^лрисЙивания 4 3:// Конструктор-копировщик 44:Address::Address(const Address& a2) 45::lastname_(0) , firstname_(0) , phone__(0) , address__(0) 46: { 47: //* Выполнение этой сложной задачи с помощью оператора присваивания 48: *this = а2; 49: } 50: 51:// Оператор присваивания 52:const AddressS Address::operator=(const AddressS a2) 53: { 54: if (this != &a2) 55: { 56: lastname (a2 . lastname_) ; 57: f irstname (a2 . firstname_) ; 58: phone(a2.phone_); 59: address(a2.address_); 60: } 61: 62: return *this; 63: } В реализации оператора присваивания в строке 54 производится такой же тест равенства указателей, какой мы видели в строке 67. В случае равенства указателей операция присваивания пропускается. Данный тест является обычным элементом реализаций большинства операторов присваивания. Он используется как для пре- дотвращения ненужной работы, так и для предупреждения возможных ошибок, ко- торые мы рассматривали ранее. В нашем случае выполнение этого теста можно было бы безболезненно пропустить, но мы оставили его для соблюдения стиля построения кода оператора присваивания. В строках 56-59 вызываются функции доступу записи, которые выполняют глу- бинное копирование строк всех полей. После завершения операции копирования мы получаем два одинаковых объекта Address, которые, тем не менее, сохранены в не- 58 Глава 2. Реализация класса Address для работы...
перекрывающихся областях динамической памяти. Конструктор-копировщик для выполнения своей работы использует перегруженный оператор присваивания. В строке 48 для выполнения глубинного копирования просто используется оператор присваивания. Но прежде всего в строке 45 всем указателям присваиваются исход- ные нулевые значения. Это обычная практика— вызывать перегруженный опера- тор присваивания из конструктора-копировщика. Но начинающие программисты часто пропускают этап инициализации указателей-членов, что ведет к появлению довольно сложных для выявления ошибок. Вас может удивить ряд нулевых указателей в строке 45, записанных За метку в виде zero (0), вместо использования макроса NULL. В ранних версиях языка С было необходимо использовать NULL для определения пустого значения, отличного от числа О. Поэтому для определения нулевого указателя считалось необходимым использовать NULL. В соответствии со стандартами языка С за 1990 год, значения NULL и 0 признаны взаимозаменяемыми. Кроме того, во многих компиляторах для исполь- зования NULL необходимо добавить в код файл заголовка. Не подходите к общепринятым правилам программирования как к религиозным догмам, а старайтесь развивать их творчески. Поскольку открытый интерфейс класса Address остался прежним, для тестиро- вания класса мы можем использовать ту же программу, с которой познакомились раньше в этой главе. Вывод программы показан в листинге 2.12. ’ Вывод программы тестирования при использовании массивов < символов в области динамического распределения памяти l:Joan Smith 2:The Very Big Corporation 3:Somewhere, MA 01000 4:617) 555-9876 5: 6:Joan Smith 7:The Very Big Corporation 8:Somewhere, MA 01000 9: (617) 555-7777 ext. 112 10: Строка 9 выглядит заметно лучше. Поскольку для сохранения данных память выделялась динамически в соответствии с реальным размером строк, номер телефо- на записался целиком без всяких потерь. Обобщение: использование динамически выделяемых строк Эврика! Все отлично! Мы написали реализацию класса Address с использовани- ем динамически выделяемых строк с нулевым окончанием. Все возможные утечки памяти учтены и устранены, и теперь программа может обрабатывать строки любой длины. При переходе от массивов символов с фиксированным размером к динамиче- ски выделяемым массивам нам пришлось не только добавить код динамического распределения памяти, но внести также следующие изменения: Использование массивов в буфере... 59
• добавить деструктор для очистки строк при удалении объекта Address; • добавить конструктор-копировщик и оператор присваивания, кото- рые предупреждают использование или очистку одной и той же об- ласти памяти двумя разными объектами Address; • изменить функции доступа для записи таким образом, чтобы они удаляли старые значения строк до присвоения новых значений; • удостовериться в необходимости тщательной проверки того, что все указатели инициализируются при создании каким-либо допустимым значением (например, NULL). Теперь мы полностью исчерпали все возможности, предоставляемые строка- ми в стиле языка С (массивы символов с нулевыми окончаниями). Пришло время узнать о более совершенных средствах стандартной библиотеки C++ для работы со строками текста. Упрощение реализации класса Address С ПОМОЩЬЮ библиотечного класса string Если у вас достаточно хорошо развит эстетический подход к программированию, то вы, вероятно, почувствовали себя не очень комфортно с предыдущим вариантом программы. Слишком много повторяющихся программных блоков для столь просто- го класса, как Address. Следует также учесть, что чем длиннее код и чем больше в нем повторений, тем выше вероятность допущения ошибок. Общие представления о классе string Что было бы здорово для вас, так это инкапсулировать все операции по динами- ческому выделению, копированию и очистке строк в отдельный класс. К счастью, создатели стандартной библиотеки уже позаботились об этом и создали класс string. Давайте перепишем код класса Address с использованием стандартного класса string. В листинге 2.13 показано новое описание класса Address. .Листинг 2.13. Определение класса Address с использованием стандартного ' класса string \ 1://TinyPIM (с)1999 Pablo Halpern, Файл Address.h 2: 3:#ifndef Address_dot_h 4:#define Address_dot_h 1 5: 6:#include <string> 7: 8:// Использование класса std::string в классе Address 9:class Address 60 Глава 2. Реализация класса Address для работы...
10: { 11:public: 12: // Следующий код автоматически генерируется компилятором: 13: // Address(); 14: // ^Address(); 15: // Address(const Addres^S); 16: // AddressSoperator=(const AddressS) ; 17: 18:// Функции доступа к полям 19:std::string lastname 0 const {return lastname_;} 20:void lastname(const std::strings); 21: 22:std::string firstname()const {return firstname_;} 23:void firstname(const std::strings); 24: 25: std: : string phone () const {return phone__; } 26:void phone(const std::strings); 27: 28:std::string address()const {return address_;} 29:void address(const std::strings); 30: 31:private: 32: // Поля данных 33: std::string lastname_; 34: std:: string firstname__; 35: std::string phone_; 36: std::string addressj 37:}; 38: ,39:#endif //Address dot h Первое, что нужно сделать, чтобы получить возможность использования стан- дартного класса строк, — включить в код заголовок этого класса, что мы сделали в строке 6. В этом заголовке описывается класс string, относящийся к пространству имен std. Данный класс содержит стандартные функции обработки строк, о чем свидетельствует его название. Комментарии в строках 12-16 говорят о том, что вам не нужно создавать конст- руктор по умолчанию, деструктор, конструктор-копировщик и оператор присваива- ния, поскольку компилятор сделает это за вас автоматически. Мы поговорим более подробно об этом далее в этой главе. В строках 19, 20 мы изменили функцию last name таким образом, чтобы она возвращала значение типа std: : string, и добавили аргумент этого же типа в строку параметров функции доступа для записи. Аналогичные изменения были внесены во все остальные функции доступа. “Постойте, — скажете вы, — таким образом мы изменили интерфейс класса, чего не хотели делать при всех преды- дущих модификациях программы!” В общем, да, но у нас есть смягчающие об- стоятельства. Прежде всего, если бы мы знали о классе string ранее, мы исполь- зовали бы именно его вместо указателей на массивы символов. Одна из наиболее важных особенностей всех стандартных библиотечных элементов состоит в том, что они стандартные. Это озйачает, что вы можете быть уверены в том, что данные элементы представлены в среде разработки компилятора. Другое смяг- чающее обстоятельство состоит в том, что внесенные изменения весьма незна- чительны и мало повлияют на практическое использование объектов, в чем вы скоро убедитесь. Упрощение реализации класса Address ... 61
Мы привели все переменные-члены класса Address к типу std: : string, задан- ному классом, в который инкапсулированы все функции копирования строк и управления памятью, которые в предыдущей версии программы нам пришлось создавать самостоятельно. Подробности класса string не должны нас интересовать, хотя его можно реализовать примерно так же, как это сделали мы в программе дина- мического выделения массивов символов. В то же время класс string создавался профессиональными программистами, поэтому можно рассчитывать на то, что его работа будет максимально эффективной по времени и расходу памяти. Использова- ние стандартного класса string позволяет также предельно упростить реализацию нашего класса Address, как это показано в листинге 2.14. Листинг 2.14. Реализация класса Address с использованием стандартного класса string " -• *. 1...... 1://TinyPIM (с)1999 Pablo Halpern, Файл Address.cpp 2: 3:#include “Address.h” 4: 5:void Address::lastname(const std::strings s) 6: { 7: lastname = s; 8: } 9: 10:void Address::firstname(const std::string& s) 11: { 12: firstname_ — s; 13: } 14: 15:void Address::phone(const std::string& s) 16: { 17: phone_ = s; 18:} 19: 20: void Address :: address (const std:‘.strings s) 21: { 22: address_ = s; 23: } Куда же делся весь остальной код? Может ли реализация класса быть таким малень- ким? Наконец то мы получили код, который в точности отражает всю простоту класса Address. Ключ к разгадке столь разительного упрощения кода лежит в семантической простоте класса string. В этом вы можете убедиться, взглянув хотя бы на строку 7. Что- бы копировать значение одной строки в другую, мы просто присваиваем ее. Оператор присваивания класса string сделает все остальное. Никаких вызовов функции strcpy, никакого выделения-освобождения памяти и никпктххуказаптелей! |_| а Общий принцип. Многие средства стандартной библиотеки призваны За метку сократить до минимума вашу работу с указателями, поскольку именно при их использовании чаще всего возникают ошибки программирова- ния. Обращение к стандартным библиотечным средствам сократит в будущем вашу работу над отладкой программы. Имеет смысл более подробно рассмотреть преимущества обработки строк в про- граммах, основанных на классе string. 62 Глава 2. Реализация класса Address для работы...
Естественный интерфейс В отличие от массивов, стандартные строки являются первоклассными объекта- ми, которые ведут себя именно так, как того можно было бы ожидать от строк текста, задействованных в программной обработке. По умолчанию каждая строка создается пустой. Семантика использования операторов также предельно логична, как вы ви- дели это на примере копирования строк с помощью оператора присваивания. Другой пример — сравнение строк: выражение stringl == string2 возвращает true, если две строки в точности, символ за символом, совпадают друг с другом (возможно про- тивоположное определение неравенств: stringl != string2). Со стандартными строками также можно использовать все операторы сравнения <, >, <= и >=, которые выполняют лексикографическое сравнение строк и возвращают true или false в качестве результата. Автоматическое управление памятью Рассмотрим теперь конструктор, деструктор и оператор присваивания, за- данные по умолчанию в классе string. Если в классе Address не будут объявле- ны конструкторы, компилятор автоматически сгенерирует конструктор по умолчанию и конструктор-копировщик. Созданный компилятором конструктор по умолчанию создает для объекта все переменные-члены, делая их пустыми. Это как раз то, что нам и нужно. Сгенерированный компилятором конструктор- копировщик копирует последовательно все элементы объекта, что опять-таки отвечает нашим требованиям. Если в программе не объявлен деструктор, то компилятор автоматически создает один деструктор для всех членов класса. Все переменные-члены класса Address имеют тип string. При удалении объекта Address все его строки авто- матически очищаются. Наконец, если в программе не будет описан собственный оператор присваи- вания, пригодный для копирования строк, то компилятор автоматически соз- даст такой оператор, который будет последовательно осуществлять копирование данных из одной переменной-члена в другую в результате выполнения простого выражения присваивания. Использование конструктора по умолчанию упрощает работу, поскольку он автоматически создает пустые строки, пригодные для заполнения. В случае ис- пользования заданных по умолчанию конструктора-копировщика, деструктора и оператора присваивания жизнь программиста облегчается за счет того, что автоматически и корректно выполняются все операции по выделению и очистке памяти. Использование программы тестирования с новым интерфейсом класса Address Вернемся к программе тестирования класса Address. Мы заменили в открытом интерфейсе класса Address указатели char* на стандартные строки. Посмотрим, какие изменения теперь следует внести в основную функцию программы тестирова- ния. Ответ вас поразит: ничего не нужно менять! Рассмотрим внимательно в лис- тинге 2.15 основную функцию программы и ее работу с новым классом Address. Упрощение реализации класса Address ... 63
{ Листинг 2.15. Программа тестирования ;-а . 1://TinyPIM (с)1999 Pablo Halpern. Файл testl.cpp 2: 3:#include <iostream> 4: 5:#include "Address.h" 6: 7:void dump(const Address& a) 8: { 9: std::cout << a.firstname() « ’ ’ « a.lastname() << ’\n’ 10: « a.address() « ’\n’ « a.phone() « ’\n’ 11: « std::endl; 12: } 13: 14:int main() 15: { 16: Address a; 17: a.lastname("Smith"); 18: a. firstname (’’Joan") ; 19: a.phone (" (617)555-9876’’) ; 20: a. address (’’The Very Big Corporation \nSomewhere,MA 01000"); 21: dump(a); 22: 23: // Вводится новый номер телефона 24: a.phone(”(617)555-7777 ext.112"); 2 5: dump(a); 26: 27: return 0; 28: } В строках 9-11 выводятся все поля объекта Address. Блок начинается выраже- нием std: : cout « а. firstname (). Данное выражение работает, поскольку заго- ловок <string> расширяет систему операторами ввода-вывода, которые работают со строковыми объектами, и подобное обращение к строковому объекту заставляет его возвратить самого себя в поток вывода. В программе мы просто заменили указа- тели char* на строки, сохранив в целостности весь остальной код. На данном при- мере хорошо демонстрируется сильная сторона стандартной системы ввода-вывода. Более подробно об этой системе и ее взаимодействии с данными разных типов вы уз- наете позже, когда мы перейдем к реализации классов обработки дат и времени. В строке 17 мы поместили литеральную строку ’’Smith’’ в функцию, которая ожидает аргумент типа string. Литеральная строка имеет тип const char [ ] и преобразовывает- ся в указатель типа const char* при передаче в аргумент функции. Класс string со- держит конверсионный конструктор, который принимает аргумент типа const char* и возвращает объект string с той же последовательностью символов (но без концевого символа окончания строки). В результате мы можем использовать литералы или указа- тели типа const char* практически во всех контекстах, где ожидается появление объек- та const string. Поэтому при смене интерфейса с char* на string изменения в основ- ной программе не потребовались. Таким образом, класс string языка C++ обеспечивает простейший ггереход от строк в стиле С к стандартнъш. строковым объектам Термин Строками в стиле языка С (или просто С-строками) называются масси- вы символов с нулевыми окончаниями. Это единственный тип строк текста, известный в С. 64 Глава 2. Реализация класса Address для работы...
Стандартные строковые объекты — это объекты класса std:-.string. Это наиболее предпочтительный способ представления текстовых строк в C++. Результат выполнения этой программы будет тот же, что и в листинге 2.12. От строковых объектов назад к строкам с нулевыми окончаниями Мы только что узнали, что указатели char* автоматически преобразовыва- ются компилятором в объекты string. Обратное утверждение в большинстве случаев неверно. У тех, кто привык работать с нестандартными классами string независимых изготовителей, удивление вызывает факт, что стандартная биб- лиотека C++ не поддерживает автоматическое преобразование строковых объек- тов C++ в С-строки (т.е. std:: string:: operator const char*() const). Ho у комитета по стандартизации были веские основания не включать подобные преобразования в класс string. Я постараюсь объяснить вам смысл этого пред- намеренного упущения, чтобы вы не поминали злым словом создателей стан- дартной библиотеки каждый раз при обращении к классу string. Эта дискуссия также даст вам некоторые представления о проблеме владельца указателя, что поможет затем лучше понять принцип работы некоторых других средств стан- дартной библиотеки. Представим на мгновение, что существует оператор преобразования объекта string, который возвращает указатель const char*. Первый вопрос, который сле- дует задать: “Кто владеет теми ячейками памяти, на которые ссылается возвращае- мый указатель?” Одно возможное решение состоит в том, что функция преобразова- ния создает новый массив символов в буфере динамического распределения, а обя- занности по очистке этой области памяти возлагаются на клиента. Пример такой функции показан в листинге 2.16. Листинг 2.16. Гипотетический код преобразования объекта string в const char* 1:extern void f(const char*); 2 : std:: string s("hello world ’’) ; 3:const char*p = s; // Преобразовывает s в const char* 4:f(p); // Передает p в другую функцию 5:delete[] p;;// He забудьте освободить память! В строке 1 объявляется функция, которая принимает строку в стиле С. В строке 2 используется существующий конверсионный конструктор для ини- циализации объекта string по const char*. В строке 3 мы преобразовываем объект обратно в const char*, используя для этого гипотетический оператор преобразования, который возвращает ссылку на массив в буфере динамического распределения памяти. В строке 4 вызывается функция f, в которую передается указатель на вновь созданный массив. В строке 5 освобождается память, кото- рую занимал этот массив. При выполнении данной программы высока вероят- От строковых объектов назад к строкам с нулевыми окончаниями 65
ность возникновения утечки памяти. Например, в листинге 2.17 мы делаем практически то же самое, но в более сжатой форме, и в результате получаем бес- хозную память. , Листинг 2.17. Утечка памяти, вызванная работой оператора преобразования 1:extern void f(const char*); 2:std::string s("hello world "); 3:f(s); // Объект s преобразовывается во временную переменную типа const char* В строке 3 переменная s преобразовывается в const char* в результате ав- томатического вызова функции f (const char*). Возвращаемый указатель сна- чала сохраняется во временной неименованной переменной, а затем передается в функцию f. Поскольку указатель остался неименованным, его невозможно удалить по завершении работы функции, следовательно, появляется область бесхозной памяти. Хорошо, предположим теперь вариант, при котором возвращаемый указатель const char* продолжает принадлежать самому объекту string. В таком случае массив символов в буфере динамического распределения будет очищаться автома- тически при удалении связанного с ним объекта string. Посмотрим, что произойдет при выполнении внешне безобидного кода, показанного в листинге 2.18. Листинг 2.18. Другой вариант реализации функции преобразования типов' i 1:extern void f(const char*); 2:extern std::string g() ; 3:const char*p = g(); // Преобразование результата функции g() 4:f(p); // Можно ли использовать полученную строку? В строке 2 объявляется функция д, которая возвращает строку как значение. В строке 3 мы вызываем функцию д и преобразовываем возвращенную строку в символьный указатель р, используя для этого гипотетический оператор преобразо- вания, который берет на себя владение возвращенным массивом. В строке 4 мы пе- редаем указатель р в функцию f. Строка, возвращаемая функцией g (), сохраняется во временной неименованной переменной, область видимости которой ограничива- ется выражением в строке 3. Удаление временной переменной будет сопровождаться очисткой массива символов, полученного в результате преобразования (поскольку временная переменная была владельцем этого массива). Таким образом, когда в строке 4 мы передаем указатель р в функцию f, этот указатель в действительности ссылается на уже очищенную область памяти, что вызовет возникновение ситуации неопределенности. Не существует универсального решения, которое гарантировало бы полную безопасность преобразования объектов string в указатели const char*, вот почему комитет по стандартизации C++ отказался вводить эти средства в стан- дартную библиотеку. В то же время иногда возникают обстоятельства, когда программисту просто не- обходимо преобразовать строковый объект в массив символов. Например, чтобы вы- звать функцию, написанную на С или другом языке программирования, часто быва- ет необходимо передать ей аргумент в виде простого массива символов, а не объекта C++. Даже некоторые функции стандартной библиотеки C++ рассчитаны на исполь- 66 Глава 2. Реализация класса Address для работы...
зование строк в стиле С. (Одним из подобных примеров может быть конструктор f stream.) Что же в таком случае предпринять? Листинг 2.19 представляет собой преобразованный листинг 2.17, в котором при- меняется функция копирования строковой переменной-члена. Листинг 2.19. Преобразование строк с помощью функции сору 1:extern void f(const char*); 2:extern std::string g(); 3:char p [100 ]; 4:p [g().copy(p,99)] = ’\0’; // Преобразование результата функции g() 5:f(p); // Использование преобразованной строки В строке 3 создается не указатель, а массив символов р. Вполне можно было создать массив р в буфере динамического распределения. Строка 4 чуть более сложная. Сначала мы вызываем функцию д, а потом функцию сору для резуль- тата первой функции. Функция сору принимает три параметра: массив симво- лов, размер массива символов и номер символа строки, с которого нужно начать копирование. Последний параметр по умолчанию задается равным нулю, поэто- му в списке аргументов его можно пропустить. Функция сору копирует последо- вательность символов из строкового объекта в массив. Запись за пределы мас- сива невозможна, так как размер массива является аргументом функции. Если строка меньше массива, то лишние ячейки останутся незаполненными. Функ- ция возвращает число скопированных символов. Это значение используется программой для локализации нулевого символа окончания строки. Строку 4 можно было бы заменить следующими тремя строками: std::string tempi = g(); int temp2 = tempi.copy(p, 99); p [temp2] = ’\0’; Эта запись может показаться вам проще. В листинге 2.20 показан аналогичный код, написанный с использованием функции-члена c str. МЛисшнг^даЛреооразование строк с помощью функции с str l:extern void f(const char*); 2:extern std::string g(); 3 : f (g () . c str () ) ; // Преобразование и использование строки В строке 3 мы преобразовываем результат вызова функции g в строку в стиле С и передаем указатель на строку в функцию f. Указатель, возвращаемый c str, ссы- лается на константный массив символов с нулевым символом окончания строки, владельцем которого является исходный строковый объект. При этом важно пом- нить, что любые изменения объекта string (включая его удаление) делают указатель бессмысленным. Так, код, показанный в листинге 2.21, будет работать некорректно. .................. ; . ....... ~л.7 ; ; Листинг 2.21. Некорректное использование функции c_str V < 1:extern void f(const char*); 2:extern std::string g(); 3:const char*p = g().c_str(); // Преобразовывает временную переменную в const char* 4:f(p); // Это недопустимо От строковых объектов назад к строкам с нулевыми окончаниями 67
В листинге 2.21 возникает та же самая проблема, с которой мы уже сталкивались в листинге 2.18. В строке 3 вызывается функция c_str, и результат сохраняется в указателе р. Но удаление временной строки в конце того же выражения делает ука- затель р бессмысленным. Когда в строке 4 мы передаем р в функцию f, возникает си- туация неопределенности. Разница между листингами 2.20 и 2.21 состоит в том, что в последнем мы сохранили результат вызова функции c str в именованной пере- менной. Но за время, прошедшее от сохранения указателя до его использования, указатель успел стать бессмысленным. "Л Избегайте сохранять результат, возвращаемый функцией c^str^S именованных переменных, I Совет! так как на практике сложно отследить момент, когда эта переменная становится бессмысленной. Если в двух листингах 2.18 и 2.21 возникли одни и те же проблемы с бессмыслен- ностью указателей, в чем же тогда разница между гипотетическим оператором пре- образования и функцией c str? Разница только в синтаксисе. Оператор преобразо- вания вызывается автоматически при возникновении определенной контекстной ситуации, превращая в прах с виду безвинную программу. В случае с функцией c str некорректный код теряет свою кажущуюся невинность. Явный вызов функ- ции позволяет начинающему программисту лучше проследить очередность событий и выявить ошибку, поэтому использование функций преобразования и копирования более безопасно, чем использование неявных операторов. Чтобы завершить рассматриваемую тему, следует еще упомянуть о функции пре- образования данных. Функция-член преобразования данных работает со строками примерно так же, как и функция c str, за тем лишь исключением, что в этом случае в конец строки не добавляется нулевой символ окончания. Это полезно в случае, если строка содержит не текст, а двоичные данные. Строковые объекты в принципе могут содержать любые символы, включая символ окончания в середине строки. Резюме К этому моменту мы создали класс Address, который содержит строки данных, за- носимых пользователем в адресную книгу. Мы научились включать в код библиотеч- ные файлы заголовков и использовать пространство имен std. Мы узнали, как мани- пулировать строками в стиле С (с нулевым символом окончания строки) с помощью библиотечных функций strcpy, strncpy и strlen, унаследованных из языка С. Для получения больших возможностей мы освоили методы сохранения строк с нулевыми окончаниями в области динамического распределения памяти. При этом мы узнали, что данный подход отличается сложностью программирования и чреват возникновением таких ошибок, как утечка памяти. Для упрощения программного кода мы обратились к стандартным библиотечным объектам класса string. Класс string освободил нас от головной боли, связанной с управлением памятью компьютера, и об- легчил выполнение таких операций, как создание, удаление и копирование строк. Следующая наша задача— создание класса адресной книги, который будет со- держать объекты класса Address. С этой целью мы будем использовать шаблоны классов-контейнеров, которые составляют ядро библиотеки STL, входящей в стан- дартную библиотеку C++. Затем нам нужно будет создать редактор для объектов Address. По мере выполнения работы мы познакомимся с другими средствами обра- ботки строк, включая функции извлечения и конкатенации строк, а также рассмот- рим более сложные процедуры по вводу и выводу строк. 68 Глава 2. Реализация класса Address для работы...
Глава 3 Создание адресной книги с помощью контейнера vector В этой главе... • Добавление в класс Address целочисленных идентификационных номеров 69 • Класс AddressBook 71 • Реализация класса AddressBook с помощью вектора 72 • Резюме 87 Теперь, после того как мы с радостью констатировали завершение работы над классом Address, нам необходимо собрать записанные в них адреса в одну ад- ресную книгу. Для этого нам потребуются функции ввода, удаления, замены и возвращения адресов из адресной книги. Нам необходим новый класс, кото- рый служил бы контейнером для объектов Address и поддерживал выполнение всех перечисленных операций. т Контейнером называется объект, который содержит коллекцию других 1ермин объектов. В этой главе мы займемся созданием класса AddressBook на основе библиотеч- ных классов-контейнеров, объекты которых будут содержать объекты Address. Добавление В класс Address целочисленных идентификационных номеров Вполне возможна ситуация, что пользователю потребуется ввести в адресную книгу записи о двух людях с одинаковыми фамилиями. Чтобы различать их, нам по- требуются уникальные целочисленные идентификационные номера ID для всех за- писей. Объекту Address присваивается ID при первом вводе в объект AddressBook, и этот номер используется в дальнейшем для возврата, замены и удаления записей. Мы зарезервируем нулевое значение ID для вновь созданных объектов Address, ко- торым еще не присвоен идентификационный номер. Изменения, введенные в класс Address, показаны в листинге 3.1.
Листинг 3.1. Добавление поля ID в класс Address" ’ '< < „ а. */ >, >„>!. \ > -ч „ (хл 5. „ J \ ‘ „ 1://TinyPIM (с)1999 Pablo Halpern. Файл Address.h 2: 3:tfifndef Address_dot_h 4:#define Address_dot_h 1 5: 6:#include <string> 7: 8:// Использование класса std::string в классе Address 9:class Address 10: { 11:public: 12: // Конструктор по умолчанию инициализирует recordld 13: // значением 0, а все строки делает пустыми. 14: Address::Address() : recordld (0){} 15: 16: // Следующий код автоматически генерируется компилятором: 17: // -Address(); 18: // Address(const AddressS); 19: // AddressS operator=(const AddressS); 20: 21: // Функции доступа к полям 22: int recordld() const {return recordld__;} 23: void recordld(int i){recordld =i;} 24: 25: std::string lastname() const {return lastname_;} 26: void lastname(const std::strings); 27: 28: std::string firstname() const {return firstname_;} 29: void firstname(const std::strings); 30: 31: std::string phone() const {return phone_;} 32: void phone(const std::strings); 33: 34: std::string address() const {return address_;} 35: void address(const std::strings); 36: 37:private: 38: // Поля данных 39: int recordld^; 40: std::string lastname_; 41: std::string firstname_; 42: std::string phone_; 43: std::string address_; 44: }; 45: 46:#endif // Address dot h Наше новое поле идентификатора объявляется в строке 39. В строке 14 вводится конструктор по умолчанию, который инициализирует переменную-член recordID_ значением 0. Функции доступа для чтения и записи в это поле определяются в стро- ках 22, 23. 70 Глава 3. Создание адресной книги с помощью контейнера vector
Класс AddressBook Начнем создание нового класса AddressBook с определения его открытого интерфейса. Затем опробуем различные подходы к реализации класса AddressBook, но его интерфейс при этом останется неизменным. Хотя по мере разработки программы TinyPIM нам придется внести некоторые изменения в интерфейс класса AddressBook, но этим мы займемся позже, в главе 6. Откры- тый интерфейс класса, позволяющий вводить, удалять, заменять и возвращать объекты Address, показан в листинге 3.2. В главе 6 в интерфейс будет добавлена функция поиска записей. Листинг 3.2. Интерфейс класса AddressBook 1://TinyPIM (c)1999 Pablo Halpern. Файл AddressBook.h 2: 3:#ifndef AddressBook_dot_h 4:#define AddressBook_dot_h 5: 6:#include "Address.h" 7: 8 .-class AddressBook 9: { 10:public: 11: AddressBook(); 12: -AddressBook(); 13: 14: // Классы исключений 15: class AddressNotFound {}; 16: class Duplicateld {}; 17: 18: int insertAddress(const Address& addr, int recordld = 0) 19: throw (Duplicateld); 20: void eraseAddress(int recordld) throw (AddressNotFound); 21: void replaceAddress(const Address& addr, int recordld = 0) 22: throw (AddressNotFound); 23: const Address& getAddress(int recordld) const 24: throw (AddressNotFound); 25: 26:private: 27: // Запрещение копирования 28: AddressBook(const AddressBook&); 29: AddressBook& operator=(const AddressBook&); 30: }; 31: 32:#endif // AddressBook dot h В строках 15, 16 объявляется пара классов исключений, которые запускают- ся в случае неуспешного поиска записи или при обнаружении двух записей с одинаковыми ID. т Исключением называется необычное событие (как правило, его можно Гермин расценить как ошибку), при возникновении которого запускается спе- циальная подпрограмма. Класс AddressBook 71
Запуском исключения называется инициализация специальной проце- дуры, назначенной данному исключению. т Класс исключения представляет собой специальный класс, который вы- I ерМИМ ступает параметром выражения запуска исключения. Выражение за- пуска исключения использует объект класса исключения для взаимо- действия с программным кодом обработки исключения. Не переживайте, если вы не знакомы с принципами разрешения исключи- тельных ситуаций в C++. По мере работы вы постепенно на практике разберетесь с этой темой. В строках 18, 19 показан интерфейс функции insert Address. В качестве аргу- ментов эта функция принимает объект Address и, по желанию пользователя, специ- альный идентификационный номер. По умолчанию идентификационному номеру новой записи присваивается значение 0. Это означает, что функция insert Address должна автоматически сгенерировать для новой записи уникальный идентифика- ционный номер. Ситуация, при которой новой записи изначально будет присвоено некоторое ненулевое значение ID, возможна только при введении записей из файлов или из релятивной базы данных. Если окажется, что новая запись имеет тот же идентификационный номер, что и запись, уже существующая в памяти компьютера, запускается исключение Duplicate Id. Все остальные функции используют номера ID для локализации требуемой запи- си в объекте AddressBook. Если будет указан несуществующий ID, то это событие вызовет запуск исключения AddressNotFound. В строке 10 объявляется функция eras eAddress, которая удаляет запись из адресной книги. Функция replaceAddress в строках 21, 22 позволяет заменять записи в адресной книги на новые. Если аргумент recordID передается в функцию replaceAddress с нулевым значением (по умолчанию), то идентификационный номер берется из аргумента Address. Для возвращения записи адреса по указанному идентификационному но- меру используется функция getAddress, объявленная в строках 23, 24. В нашем проекте не предполагается возможность копирования адресной книги, поэтому в строках 28, 29 конструктор-копировщик и оператор присваивания объяв- ляются закрытыми. Это освобождает нас от необходимости ломать голову над кор- ректностью семантики выполнения копирования. Сейчас в интерфейсе нашего класса отсутствуют функции поиска адресов по ключевым словам. Мы еще вернемся к этим функциям и к вопросу о структуре дан- ных, используемых программой, в последующих главах. Реализация класса AddressBook с помощью вектора Давайте рассмотрим принципиальную структуру класса AddressBook. На рис. 3.1 показана предельно сокращенная диаграмма классов на языке UML, пока- зывающая отношения между классами AddressBook и Address. 72 Глава 3. Создание адресной книги с помощью контейнера vector
Отношения класса AddressBook к классу Address строятся по принци- пу “один ко многим”. Необходимо иметь возможность сохранять много объектов Address в контейнере, осно- ванном на классе AddressBook. Это очень важная особенность класса AddressBook— быть контейнером объектов другого класса. Наиболее простым решением может быть массив объектов Address. Массив является примером последовательного контей- нера. Последовательными называются контейнеры, данные в которых хра- нятся в линейной последовательности, определяемой программным кодом, использующим этот контейнер. Стан- дартная библиотека содержит не- сколько встроенных последовательных контейнеров, работать с которыми проще, чем с обычным массивом. Рис. 3.1. Отношения между класса- ми AddressBook иAddress Mg Общий принцип. Отношение “один ко многим” в диаграмме классов Заметку практически всегда означает необходимость создания объекта, выпол- няющего роль контейнера для других объектов. Классы-контейнеры стандартной библиотеки C++ облегчат вам выполнение этой задачи. Реализация свойств простейшего контейнера с помощью массива Не беспокойтесь, мы не станем забираться в дебри реализации класса-контейнера на основе обычных массивов только лишь для того, чтобы показать бесперспектив- ность этого подхода по сравнению с использованием стандартных библиотечных классов. Просто имеет смысл в общих чертах рассмотреть свойства класса- контейнера, основанного на массиве, чтобы затем лучше понять идиоматические приемы, используемые в библиотечных решениях. В листинге 3.3 показан уже знакомый вам интерфейс класса AddressBook, в ко- торый мы добавили новую закрытую переменную-член. (Эти изменения были внесе- ны лишь для примера, поэтому программный код в этом варианте не был сохранен ни в каком файле.) Листинг 3.3. Использование массива в классе AddressBook 1:class AddressBook 2: { 3:public: 4: //...Тот же интерфейс, что и в листинге 3.2 5:private: Реализация класса AddressBook с помощью вектора 73
6: enum {numAddresses =100 }; 7: Address addresses [numAddresses ]; 8:); В строке 6 объявляется размер массива для сохранения адресов, а в стро- ке 7 объявляется сам массив. Вы сразу можете заметить одно из ограничений использования массивов: фиксированный размер. Мы не можем сохранить больше адресов, чем было жестко установлено в программном коде. Если же в нашей адресной книге будет всего несколько адресов, то память, выделенная для массива, пропадет впустую. Мы можем заменить переменную-член addresses_ указателем на динамически выделяемый массив, что позволит оптимизировать использование памяти. Но это потребует написания дополнительного программного кода управления памятью и создаст предпосылки для возникновения трудновыявляемых ошибок с утечкой па- мяти. Написание такой программы потребует много времени и внимания. Кроме то- го, любой массив всегда будет иметь ряд неиспользуемых элементов, или нам при- дется обновлять массивы при каждом вводе и удалении записи. Создание объектов Address для всех неиспользуемых элементов массива будет столь же расточитель- ным, как и частые обновления массива. Многие из возникших проблем вам уже знакомы. С ограничениями использо- вания массивов с фиксированным размером и проблемами управления динами- ческой памятью мы уже сталкивались при манипулировании текстовыми стро- ками. Нет смысла сейчас заниматься написанием программы с использованием массивов, чтобы прийти к тем же выводам, которые мы сделали в главе 2. Мы уже осознали бесперспективность этого пути и готовы перейти к поиску более совершенных решений. Стандартный класс string спас нас в свое время при решении проблем с обработкой и представлением строк. Сейчас мы вновь обра- тимся к стандартной библиотеке в поисках класса, который бы выполнял все функции массива произвольного типа. Объявление вектора o6beKTOBAddress Сейчас мы выполним класс AddressBook, используя для этого стандартный биб- лиотечный шаблон класса-контейнера vector. Вектор работает так же, как и мас- сив, за тем исключением, что приращение вектора происходит автоматически по мере добавления новых элементов. Управление памятью, так же, как и в случае ис- пользования класса string, происходит автоматически и прозрачно. В листин- ге 3.4 показана новая версия файла заголовка класса AddressBook. I Листинг 3.4. Построение класса AddressBook с помощью вектора 1://TinyPIM (с)1999 Pablo Halpern. Файл AddressBook.срр 2: 3:#ifndef AddressBook_dot__h 4:#define AddressBook__dot_h 5: 6:#include <vector> 7:#include "Address.h " 8: 9. -class AddressBook 10: { 74 Глава 3. Создание адресной книги с помощью контейнера vector
11:public: 12: AddressBook(); 13: -AddressBook(); 14: 15: // Классы исключений 16: class AddressNotFound {}; 17: class Duplicateld {}; 18: 19: int insertAddress(const Address& addr, int recordld = 0) 20: throw (Duplicateld); 21: void eraseAddress(int recordld) throw (AddressNotFound); 22: void replaceAddress(const Address& addr, int recordld = 0) 23: throw (AddressNotFound); 24: const Address& getAddress(int recordld) const 25: throw (AddressNotFound); 26: 27: // Тестовая программа для вывода на печать содержимого адресной // книги 28: void print() const; 29: 30:private: 31: // Запрещение копирования 32: AddressBook(const AddressBook&); 33: AddressBook& operator=(const AddressBook&); 34: 35: static int nextld_; 36: std::vector<Address> addresses^; 37: 38: // Возвращает запись по заданному ID. 39: // Возвращает notFound в случае отсутствия записи. 40: int getById(int recordld) const; 41: enum {notFound = -1}; 42: }; 43: 44:#endif // AddressBook dot h В строке 28 объявляется функция-член print. Функция print выводит со- держимое адресной книги на стандартное устройство вывода и будет использо- ваться только для тестирования класса. Из окончательного варианта программы TinyPIM эта функция будет удалена. В строке 35 объявляется статическая цело- численная переменная next ID, которая содержит идентификационный номер следующей записи. Каждый раз при вводе нового объекта Address в адресную книгу эта переменная приращивается на единицу. Таким образом, каждая сле- дующая запись получает свой уникальный идентификационный номер. Вспом- ните, что статические переменные используются всеми экземплярами класса, созданными в ходе выполнения программы. Поэтому два объекта AddressBook не могут сгенерировать одинаковые номера ID. Даже если мы будем копировать записи из одного объекта AddressBook в другой, все равно конфликты совпаде- ния идентификационных номеров не должны возникать. В строке 36 объявляется переменная addresses , которая будет выступать век- тором для объектов Address. Шаблон стандартного класса-вектора становится дос- тупным для использования благодаря включению в код в строке 6 соответствующего файла заголовка. Тип объектов, для которых создается вектор, передается в виде па- раметра шаблона при реализации вектора в строке 36. В нашем случае вектор addresses_ создается для объектов типа Address. Реализация класса AddressBook с помощью вектора 75
Термин Термин Термин Шаблоном называется программная заготовка, описывающая целое се- мейство родовых классов и функций. (См. ниже “Справочная информа- ция: общее представление о шаблонах”.) Параметром шаблона называется особый вид параметров, который оп- ределяется во время компиляции программы и представлен в коде в уг- ловых скобках (о) после имени шаблона. Параметром шаблона может выступать тип данных или значение (в зависимости от шаблона). Реализацией шаблона называется класс или функция, сгенерированные на его основе. Реализация шаблона происходит во время компиляции программы в результате передачи в шаблон параметров шаблона. В строке 40 объявляется закрытая функция getByld, которая отыскивает тре- буемый объект Address в векторе addresses . Эта функция возвращает индекс со- ответствующего объекта в векторе. Векторы, точно так же, как и массивы, можно индексировать с помощью целочисленных значений. К первому элементу вектора addresses_ можно обратиться как addresses (0), ко второму элементу — addresses (1) и т.д. В векторе не может быть элементов с отрицательными индек- сами, поэтому значение -1 назначено функции getByld в качестве возврата на тот случай, если искомый объект не будет обнаружен. В строке 41 это особое значение возврата присваивается константе not Found. Справочная информация: общее представление Экскурс о шаблонах^_________________________________________________ Шаблоны повсеместно используются в стандартной библиотеке. Этот небольшой обзор необходим вам, чтобы получить общее представление о том, что представляют собой шаблоны и как они работают. Шаблон представляет собой программную заготовку, используемую для создания классов и функций. Следует четко уяснить, что шаблон класса не является сам по себе реальным классом, а шаблон функции не явля- ется функцией (хотя для упрощения изложения в книгах шаблоны час- то представляются как готовые классы и функции). Для шаблонов оп- ределяются именованные параметры, установка которых как раз и не- обходима для превращения шаблона в реальный класс или функцию. При использовании шаблона в него передаются вместе с параметром шаблона определенный тип данных или значение, которые заполняют недостающие элементы в реализации класса или функции. Шаблоны позволяют описывать в общем виде структуры данных или алгоритмы решений, независимо от реальных типов данных, с которы- ми придется работать той или иной программе. Например, шаблон класса vector описывает такой класс-контейнер, элементы которого можно добавлять, удалять и возвращать независимо от их типов. Ана- логично, шаблон функции sort описывает алгоритм сортировки по- следовательных элементов независимо от их типов. Когда мы передаем в шаблон параметры, компилятор создает реальный класс или функцию для работы с конкретными данными. Процесс соз- дания реального класса или функции из шаблона называется реализа- 76 Глава 3. Создание адресной книги с помощью контейнера vector
цией шаблона. Реализация шаблона всегда происходит во время компи- ляции программы. В программном коде реализация шаблона задается путем определения параметров шаблона в угловых скобках сразу после имени шаблона (например, vector <int>). В шаблон функции аргу- менты передаются точно так же, как при вызове обычной функции. Компилятор автоматически распознает параметры шаблона функции и выполняет реализацию шаблона во время компиляции программы. Таким образом, шаблоны всегда выполняются в два этапа: сначала компилятор реализует шаблон в реальный класс или функцию, а затем этот класс или функция используется при выполнении программы. Простой пример использования шаблона показан в листинге 3.5. Листинг 3.5. Пример использования шаблона 1:// Объявления шаблона */ 2:template cclass Т> class mytmplt {/★ ... определение класса ...*/}; 3:template <class A, class В> В func(А а, В Ь); 4: 5:// Объявление класса (не обязательно шаблона) 6.-class myclass {/*... определение класса ...*/}; 7: 8:int main() 9: { 10: // Использование шаблона класса 11: mytmplt<int> v; 12: 13: // Использование шаблона функции 14: myclass х; 15: int ret = func(x, 5); 16: 17: //... 18: } В строке 2 объявляется шаблон класса под именем mytmplt, который принимает один параметр т. В объявлении параметра Т указан тип class. Несмотря на эту спецификацию, параметр Т может представ- лять любой тип данных, как стандартный, так и заданный пользовате- лем. (Современные компиляторы позволяют в объявлениях параметров шаблонов вместо class использовать логически более понятный спе- цификатор typename.) В строке 11 задается реализация шаблона mytmplt, в результате чего появляется новый класс mytmplt<int>, который тут же используется для создания переменной v. Хотя реализация шаблона и создание пе- ременной часто записываются в одной строке кода, нам следует четко разделять два этих процесса. В строке 15 реализуется шаблон func и создается новая функция func<myclass, int>, в которую мы пере- даем аргументы х и 5. Обратите внимание, что компилятор распознает параметры шаблона функции по типам переданных аргументов (х от- носится к типу myclass и соответствует параметру шаблона А, а значе- ние 5 относится к типу int, что соответствует параметру шаблона В.) Опять-таки, следует уяснить, что реализация шаблона функции и кода функции — это два самостоятельных процесса, хотя они задаются од- ной строкой программного кода. Процесс реализации наглядно проил- люстрирован на рис. 3.2. Реализация класса AddressBook с помощью вектора 77
Шаблон класса: Шаблон функции: template<class Т> template<class A, class В> class mytmplt В func(A а, В Ь) ,..... I ........t г I ( Реализация J ( Реализация J Класс: А Функция: tmplt<int> int func(myclasst int) _____________I_________Время компиляции____________|___________ (Создание объекта] Время вылолнения [выполнение функции]" Объект: Вызов функции: v func(x, 5) Рис. 3.2. Двухэтапный процесс использования шаблонов Использование вектора как расширяемого массива Первая функция, к которой мы обратимся в классе AddressBook, будет insertAddress. Эта функция добавляет новую запись в адресную книгу. В листин- ге 3.6 показана реализация функции insertAddress с использованием вектора addresses_. [ Листинг 3.6. Реализация фуНКЦИИ insertAddress 1://TinyPIM (с)1999 Pablo Halpern 2: 3:#ifndef _MSC_VER 4:#pragma warning(disable : 4786) 5:#endif 6: 7:#include <iostream> // Для использования функции print() 8: 9:#include "AddressBook.h " 10: 11:int AddressBook::nextld_ = 1; 12: 13:AddressBook::AddressBook() 14: { 15:} 16: 17:AddressBook::-AddressBook() 18: { 19: } 20: 21:int AddressBook::insertAddress(const Address& addr, 22: int recordld) throw (Duplicateld) 23: { 24: if (recordld == 0) 25: // Если recordld не задан, генерируется новый ID. 26: recordld = nextld_++; 27: else if (recordld >= nextld_) 78 Глава 3. Создание адресной книги с помощью контейнера vector
28: // Проверяет, чтобы nextld было больше идентификационных // номеров всех остальных записей. 29: nextld_ - recordld + 1; 30: else if (getByld(recordld) != notFound) 31: // Явно заданный ID не уникален 32: throw Duplicateld(); 33: 34: // Вставляет новую запись в вектор. 35: addresses_.push_back(addr); 36: 37: // Присваивает записи идентификационный номер 38: addresses—.back().recordld(recordld); 39: 40: return recordld; 41: } до а Особенности компиляции. В строках 3-5 отключается предупреж- Заметку дающее сообщение 4786 на случай использования компилятора Microsoft VC. Если оставить включенной эту опцию, то компилятор бу- дет показывать во время реализации шаблонов предупреждение о том, что во внешнем коде было сгенерировано слишком длинное имя. Это предупреждение вызвано ограничением, установленным в компилято- ре Microsoft (точнее, в системе отладки этого компилятора). В действи- тельности код не содержит никаких ошибок и может быть скомпилиро- ван. Я предпочитаю всегда заранее отключать сообщения о ложных ошибках, чтобы не пропустить среди них указания на действительные ошибки в программе. В строках 13-19 определяются конструктор и деструктор, которые ничего не делают. Конструктор просто явно вызывает конструктор по умолчанию шаблона vector. Конструктор vector, так же, как это было с классом string, создает по умолчанию пустой объект. Деструктор шаблона vector отвечает за то, чтобы вся память, занятая объектом, была возвращена в буфер динамического распреде- ления после удаления этого объекта. Деструктор класса AddressBook, так же, как и его конструктор, служит лишь для явного вызова деструктора класса- вектора addresses—. Справочная информация: Экскурс параметр распределения^ заданный поумолчшшю Шаблон vector в действительности принимает два параметра: тип эле- мента и класс распределения. Класс распределения служит для создания объекта, который занимается управлением динамически распределяемой памятью. Параметр распределения в векторе (и во всех других классах- контейнерах) используется классом для выделения памяти для своих эле- ментов. Стандартный алгоритм распределения задается компилятором по умолчанию при реализации шаблона класса-контейнера. Так, в случае объявления в программе std: : vector<Address> компилятор в действи- тельности реализует следующее объявление: std: :vector<Address, std::allocator<Address> >. Программа распределения, заданная по умолчанию, подходит в большинстве случаев. Хотя из-за добавления в объ- явление параметра распределения системы отладки некоторых компиля- Реализация класса AddressBook с помощью вектора 79
торов предупреждают о недопустимой длине имени параметра. Чтобы вас не пугали предупреждения о недопустимой длине имен, которые выводят программы отладки и связывания программ, следует сейчас разобраться, из-за чего это происходит. При использовании классов строк ситуация усложняется еще больше. Так, объявление std: :string во время компиляции замещается на сле- дующее обращение: std::basic_string<char, char_traits<char>, allocator<char> >. Хочется надеяться, что в следующих версиях ком- пиляторов эта несогласованность в отображении имен компилятором и системой отладки будет устранена. Использование программ распределения предоставляет программи- стам дополнительные возможности по настройке работы библиотечных классов. В то же время написание собственных программ распределе- ния требует высокого мастерства и профессионализма. (Изучение дан- ной темы выходит за рамки этой книги.) В строках 24-26 генерируются уникальные идентификационные номера для объ- ектов Address, если идентификационный номер не был задан явным образом (переменная recordld равна нулю). В строке 26 полю ID объекта Address присваи- вается значение статической переменной nextld_, после чего nextld_ приращива- ется на единицу. Если же при вызове функции явно указывается определенный идентификационный номер, следует проверить, нет ли других объектов Address с таким же ID. Если назначенный идентификационный номер больше последнего значения, сгенерированного программой, то этот номер, безусловно, будет уникаль- ным. Но после его присвоения объекту Address необходимо обновить значение пе- ременной next Id_, чтобы все последующие идентификационные номера также были уникальными. Обновление переменной-счетчика происходит в строке 29, в резуль- тате чего значение переменной nextld_ становится больше всех идентификацион- ных номеров, назначенных до сих пор. Другая ситуация, требующая особой обработки, возникает, если переданное зна- чение recordld меньше текущего значения nextld_. В таком случае, чтобы убедить- ся, что переданное значение является уникальным, необходимо провести поиск объ- екта по указанному ID и запустить исключение, если окажется, что объект с таким идентификационным номером уже существует. В программе эта операция выполня- ется в строках 30-32. Ожидается, что функция getByld возвратит значение notFound, в противном случае запускается исключение Dublicateld. Реализацию функции getByld мы рассмотрим ниже. Основной в коде функции insertAddress является строка 35. В ней мы вызыва- ем встроенную функцию push_back, которая вставляет новый элемент в конец век- тора addresses—. Функция push back является констанггиюй по времени выполне- ния. Векторы позволяют быстро и эффективно добавлять и удалять элементы, опе- рируя с большими массивами данных. — Константность времени выполнения— это характеристика функций lepMklH ивсех других процессов обработки данных, которая указывает на то, что время выполнения процедуры не зависит от числа элементов в кон- тейнере. Постоянство времени выполнения является одним из основ- ных требований к профессиональным программам обработки данных, поскольку исключает снижение эффективности работы программы по мере роста контейнера. 80 Глава 3. Создание адресной книги с помощью контейнера vector
В строке 35 объект Address копируется в вектор, но ему еще не присвоен уни- кальный идентификационный номер. В строке 38 с помощью функции back мы по- лучаем ссылку на последний элемент вектора. Поскольку мы только что ввели новый объект Address в конец списка, функция back возвратит ссылку на него. Теперь мы можем присвоить этому объекту идентификационный номер, вызвав recordld для значения, возвращенного функцией back. Функция back также является констант- ной по времени выполнения. Мы завершаем реализацию функции insertAddress возвратом значения recordld в строке 40. Справочная информация: Экскурс пользовательские и библиотечные контейнеры Вполне очевидно, что векторы основаны на манипуляциях с динамически распределяемой областью памяти. Но в том случае, когда вы создаете в бу- фере динамического распределения массив объектов, память выделяется сразу для всех элементов массива. Помимо того, что в этом случае трудно добиться эффективной работы программы, необходимо также создать для элементов всех типов свои конструкторы по умолчанию. Данный подход может быть оправдан только в случае создания массивов, использование которых программой не связано с частыми добавлениями и удалениями элементов. Библиотечные классы-контейнеры выполняют ту же работу, но имеют более совершенную систему управления памятью. Память выделя- ется по мере добавления индивидуальных элементов, а не всего массива. Когда элемент удаляется из контейнера, деструктор вызывается только д ля этого элемента. Библиотечные шаблоны также предоставляют универ- сальные конструкторы для данных всех типов. Доступ к элементам вектора В листинге 3.7 показана реализация функции getByld, которую мы использова- ли в предыдущем разделе для контроля за появлением записей с одинаковыми иден- тификационными номерами. Листинг 3.7 следует рассматривать как продолжение листинга 3.6. j Лист1{1НЁ^.7. Реализация функции getByld j! 42: 43:int AddressBook::getByld(int recordld) const 44: { 45: for (int i = 0; i < addresses_.size(); ++i) 46: if (addresses_[i].recordld() == recordld) 47: return i; 48: 49: return notFound; 50: }. Примерно такой код можно было написать для этой функции, если бы вместо вектора addresses_ использовался массив подтем же именем. Запускается цикл по всем элемен- там вектора, и их идентификационные номера сравниваются со значением, переданным в функцию. В строке 45 вызывается функция addresses_. size (), которая возвращает Реализация класса AddressBook с помощью вектора 81
число элементов в векторе. Все стандартные классы-контейнеры поддерживают функ- цию size, которая возвращает их размер. В данном случае мы используем функцию size для определения необходимого числа циклов. В строке 46 доступ к отдельным элементам вектора осуществляется с помощью оператора индексирования (operator [ ]). Оператор индексирования также является константным по времени выполнения и предоставляет возможность произвольного доступа к элементам коллекции. Функция recordld вызывается для результата вы- полнения операции индексирования, и в случае совпадения идентификационного номера записи и переданного значения функция getByld возвращает индекс i. Если же по завершении цикла совпадений не было обнаружено, функция возвратит кон- станту notFound. 7 7 Произвольным доступом называется возможность возвращать в произ- вольном порядке и с постоянной скоростью элементы коллекции, неза- U2?:. •; % висимо от ее размера. Функция getByld объявлена как const. Это означает, что с помощью данной функции можно получить доступ к объекту AddressBook только для чтения. Таким образом, код функции getByld будет скомпилирован только в том случае, если кон- стантными будут все операции, выполняемые этой функцией. Встроенная функция size является константной, так как она не изменяет вектор. Оператор индексирова- ния имеет два варианта — константный и неконстантный. Неконстантный вариант возвращает простую ссылку на указанный объект, а константный — константную ссылку. Такой тип интерфейса называется логически константным Логическая константность означает, что константный контейнер ведет себя как контейнер кон- стантных элементов, хотя все операторы существуют в двух вариантах и в потен- циале могут возвращать как константные, так и неконстантные ссылки на элементы коллекции. Все контейнеры стандартной библиотеки построены по принципу логи- ческой константности. Заметку Общий принцип. Контейнеры стандартной библиотеки построены по принципу логической константности. Это означает, что кон- стантный контейнер ведет себя так. как будто он содержит кон- стантные элементы. Удаление записей адресов Как ни печально, но иногда с друзьями приходится расставаться. В этом случае пользователю нужно удалить соответствующую запись из адресной книги. Для этого мы воспользуемся функциями, предоставляемыми классом-контейнером- Продол- жим рассмотрение класса (листингом 3.8), в котором показана реализация функции eraseAddress. 51: 52:void AddressBook::eraseAddress(int recordld) 53: throw (AddressNotFound) 54: { 55: int index = getByld(recordld); 82 Глава 3. Создание адресной книги с помощью контейнера vector
56: if (index == notFound) 57: throw AddressNotFound() ; 58: 59: // Копирование последнего элемента в позицию удаляемого. 60: addresses_[index ] == addresses_.back(); 61: 62: // Удаление последнего элемента вектора. 63: addresses_.рор_back(); 64: } Функция eraseAddress принимает с аргументом идентификационный номер за- писи, которую нужно удалить. В строке 52 для локализации удаляемого элемента используется функция getByld. В строках 56, 57 проверяется, что запись с указан- ным ID действительно существует в коллекции. Чтобы понять смысл строки 60, придется напрячь воображение. Удаление эле- мента из середины вектора будет малоэффективным, поскольку затем придется сдвигать все элементы вектора, находящиеся за удаленным элементом. Вместо этого мы копируем последний элемент массива в позицию, занятую сейчас удаляемым элементом, тем самым замещая его (строка 60). Затем в строке 63 мы удаляем теперь уже не нужный последний элемент коллекции. Удаление последнего элемента векто- ра предпочтительнее, поскольку эта операция константна по времени выполнения. Для удаления последнего элемента используется встроенная функция pop back, ко- торая вызывает деструктор для последнего элемента вектора и удаляет его из памя- ти. Функция рор_back не возвращает никаких значений. Завершение реализации imaccaAddressBook Заключительная часть реализации класса AddressBook на основе стандартного класса-вектора показана в листинге 3.9. [Листинг 3.9. Заключит^л^нёя imacc^ AddressBook : \ 65: 66:void AddressBook::replaceAddress(const Address& addr, int recordld) 67: throw (AddressNotFound) 68: { 69: if (recordld === 0) 70: recordld = addr.recordld(); 71: 72: int index = getByld(recordld); 73: if (index == notFound) 74: throw AddressNotFound(); 75: 76: addresses—[index ] = addr; 77: addresses—[index ].recordld(recordld); 78: } 79: 80:const Address& AddressBook::getAddress(int recordld) const 81: throw (AddressNotFound) 82: { 83: int index = getByld(recordld); 84: if (index == notFound) 85: throw AddressNotFound(); 86: Реализация класса AddressBook с помощью вектора 83
87: return addresses_[index]; 88: } 89: 90 .-void AddressBook: : print () const 91: { 92: std::cout << и******************************************\пи; 93: for (int i = 0; i < addresses_.size(); ++i) 94: { 95: const AddressS a - addresses_[i]; 96: std::cout « "Recordld: " << a.recordld() « ’\n' 97: « a.firstname() << ' ’ « a.lastname() « '\n* 98: « a.address() « '\n’ « a.phone() « '\n’ 99: « std::endl; 100: } 101: } Функция replaceAddress использует идентификационный номер записи, сохра- ненный в аргументе addr, или аргумент recordld, если он не нулевой. В строках 69, 70 переменной recordld присваивается текущее значение ID. В строках 72-74 про- веряется наличие записи с указанным ID. Точно такую проверку мы делали с вами в функции eraseAddress. В строке 76 используется оператор индексирования. Но в этот раз мы имеем дело не с константным объектом. Поэтому ссылка, возвращае- мая оператором индексирования, также неконстантна, что позволяет нам присвоить объекту значение переменной addr. В строке 77 для ссылки, возвращенной операто- ром индексирования, вызывается неконстантная функция recordld. Функция getAddress в строках 80-88 служит расширенным интерфейсом из- вестной вам функции getByld и используется для отслеживания возможных ошибок (строки 84, 85). Функция print в строках 90-101 представляет собой простой цикл вроде того, что мы использовали в функции getByld. Чтобы избежать повторного индексирования вектора в цикле, используется ссылка, возвращаемая оператором индексирования в строке 95. С помощью ссылки а мы получаем доступ к различным атрибутам объектов Address, выделяемых при каждой итерации цикла (строки 96-98). Проверка работы программы Теперь протестируем реализацию класса AddressBook. Для этого используем программу, показанную в листинге 3.10. Листинг 3.10. Программа тестирования класса AddressBook 1://TinyPIM (с)1999 Pablo Halpern. Файл AddressBookTest.срр 2: 3:#include <iostream> 4:#include "AddressBook.h" 5: 6: int main() 7: { 8: AddressBook book; 9: 10: Address a; 11: a.lastname("Smith"); 12: a.firstname("Joan"); 13: a.phone("(617)555-9876"); 84 Глава 3. Создание адресной книги с помощью контейнера vector
14: а.address("The Very Big Corporation \nSomewhere, MA 01000"); 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: 57: 58: 59: 60: 61: 62: 63: } Address b; b.lastname("Adams"); b.firstname("Abigale"); b.phone("(212)555-3734"); b.address("743 Broadway\nNew York, NY"); Address c; c.lastname("Neighborhood Video"); c.phone("555-FILM"); int a_id = book.insertAddress(a); int b_id = book.insertAddress(b); int c_id = book.insertAddress(c); std::cout « "*** Three Address Entries ***\n"; book.print(); // Запись d имеет то же имя, что и b Address d; d.lastname("Adams"); d.firstname("Abigale") ; d.phone("(508)555-4 4 66") ; d.address("1 Small St.\nMarlboro,MA 02100"); // Ввод записи с тем же именем int d_id = book.insertAddress(d); std::cout « "*** After adding a duplicate Abigale Adams ***\n"; book.print(); // Удаление адреса book.eraseAddress(a_id); std::cout « "*** After erasing Joan Smith ***\n "; book.print(); // Замена адреса c.address("22 Main St.\nMy town,MA 02200"); book.replaceAddress(c, c_id); std::cout « "*** After replacing Neighborhood Video ***\n"; book.print(); // Возвращение и печать адреса const AddressS d2 = book.getAddress(d_id); std::cout « "*** Copy of d: ***\n" « d2.firstname() « ' ' « d2.lastname() « ’\n' « d2.address() « '\n' « d2.phone() « 1\n' « std::endl; return 0; В нашей тестовой программе испытываются всевозможные функции-члены класса AddressBook. В строках 10-28 создаются и вводятся в объект AddressBook три разных объекта Address. Результат выводится в строке 30. В строках 33-40 мы проверяем возможность ввода записи с тем же именем, которое уже существует в ад- ресной книге. Новая запись вводится в строке 50, а в строке 51 мы замещаем ею дру- Реализация класса AddressBook с помощью вектора 85
гую запись в адресной книге. Наконец, в строке 56 мы возвращаем один из адресов и выводим его на печать в строках 57-60. Если все сделано правильно, то это должен быть адрес, введенный в строке 40. Программа довольно простая, но проверяет практически все функции, которые мы ожидаем от класса AddressBook. Вывод про- граммы показан в листинге 3.11. j* Листинг 3.11. Вывод тестовой программы 1:*** Three Address Entries *** 2;****************************************** 3:Record Id: 1 4:Joan Smith 5:The Very Big Corporation 6:Somewhere, MA 01000 7: (617) 555-9876 8: 9:Record Id: 2 10:Abigale Adams 11:743 Broadway 12:New York, NY 13: (212)555-3734 14: 15:Record Id:3 16:Neighborhood Video 17: 18:555-FILM 19: 20:*** After adding a duplicate Abigale Adams 2i;****************************************** 22:Record Id:l 23:Joan Smith 24:The Very Big Corporation 25:Somewhere, MA 01000 26: (617)555-9876 27: 28:Record Id:2 29:Abigale Adams 30:743 Broadway 31:New York, NY 32: (212)555-3734 33: 34:Record Id: 3 35:Neighborhood Video 36: 37:555-FILM 38: 39:Record Id: 4 40:Abigale Adams 41:1 Small St. 42:Marlboro, MA 02100 43: (508) 555-4466 44: 45:*** After erasing Joan Smith *** 4g.****************************************** 47:Record Id:4 48:Abigale Adams 49:1 Small St. 86 Глава 3. Создание адресной книги с помощью контейнера vector
50:Marlboro, MA 02100 51: (508)555-4466 52: 53:Record Id: 2 54:Abigale Adams 55:743 Broadway 56:New York, NY 57: (212)555-3734 58: 59:Record Id: 3 60 Neighborhood Video 61: 62:555-FILM 63: 64:*** After replacing Neighborhood Video *** 65:****************************************** 66:Record Id: 4 67:Abigale Adams 68:1 Small St. 69:Marlboro, MA 02100 70: (508)555-4466 71: 72:Record Id: 2 73:Abigale Adams 74:743 Broadway 75:New York, NY 76: (212)555-3734 77: 78:Record Id: 3 79Neighborhood Video 80:22 Main St. 81:My town, MA 02200 82:555-FILM 83: 84:*** Copy of d: *** 85:Abigale Adams 86:1 Small St. 87:Marlboro, MA 02100 88: (508)555-4466 89: Программа работает так, как мы этого и хотели. Записи вводятся, заменяются, удаляются и возвращаются из объекта AddressBook. Мы пока не позаботились о том, чтобы упорядочить записи адресной книги в алфавитном порядке. До строки 43 за- писи следуют в порядке возрастания идентификационных номеров, но этот порядок нарушается в результате удаления записи, как это видно в строках вывода 45-62. Было бы неплохо иметь возможность сортировать записи адресной книги в алфа- витном порядке. Что ж, никто не мешает нам доработать в будущем класс AddressBook. Резюме В дополнение к классу Address мы завершили работу над новым классом AddressBook, который позволяет вводить, удалять и изменять записи адресной книги, обращаясь к ним по уникальным идентификационным номерам. Мы об- Резюме 87
судили возможность использования массивов для поддержания коллекции объ- ектов Address и пришли к выводу, что гораздо более перспективным подходом будет использование шаблона класса-контейнера vector из стандартной биб- лиотеки C++. Вектор представляет собой один из вариантов последовательных контейнеров, предоставляемых стандартной библиотекой. В своей работе для доступа к элементам вектора и управления динамически выделяемыми массивами мы использовали встроенные функции-члены push_back, pop_back и size, а также оператор индек- сирования. Также мы разработали базовую программу тестирования класса AddressBook, с помощью которой определили новое требование к классу, — воз- можность сортировки записей. В следующей главе мы опробуем альтернативный подход к реализации класса, позволяющий сортировать записи в алфавитном порядке. Мы познакомимся с библиотечным шаблоном контейнера list и узна- ем о работе итераторов. 88 Глава 3. Создание адресной книги с помощью контейнера vector
Глава 4 Альтернативная реализация адресной книги с помощью контейнера list В этой гл аве... • Общие представления о контейнере list 90 • Реализация класса AddressBook с использованием списка 91 • Навигация по списку с помощью итераторов 92 • Поддержание списка в алфавитном порядке 103 • Резюме 108 На первый взгляд реализация класса AddressBook, созданная нами в преды- дущей главе, кажется вполне удовлетворительной и эффективной. Работа про- граммы основана на константных по времени выполнения функциях push back, pop_back, size и операторе operator [ ]. В то же время реализация большинства открытых функций-членов основано на использовании закрытой функции клас- са getByld, которая осуществляет поиск соответствующей записи, сверяя ра- венство значений в поле ID с заданным значением для всех объектов коллекции. Функция getByld не константна по времени выполнения, поскольку время ее выполнения зависит от числа итераций цикла. О таких функциях говорят, что они линейно зависимы от размера контейнера. Следовательно, линейно зависи- мыми будут и все функции открытого интерфейса, основанные на функции getByld. При использовании достаточно большой адресной книги во время вы- полнения программы могут возникнуть существенные задержки. (Что касается нашей программы, то вряд ли эта проблема возникнет когда-нибудь. Трудно представить, что пользователь обзаведется такой огромной адресной книгой, которая повлияет на производительность современного компьютера. Тем не ме- нее в дальнейшем при работе над своим проектом вам следует учитывать это ог- раничение.) т Линейно зависимыми называются функции, время выполнения которых 1 ин в среднем линейно зависит от числа элементов контейнера. Другое ограничение использования вектора состоит в том, что записи в нем невозможно сортировать, не вызвав тем самым задержку выполнения програм- мы. Идеально было бы вставлять записи друг за другом в некоторой последова-
тельности, но проблема состоит в том, что вставка записи в середину вектора сразу сделает функцию insert Address вдвое медленнее. Для нашего проекта идеальным решением был бы такой контейнер, который позволял бы быстро вводить записи в середину списка; при этом контроль за появлением дубликатов записей можно оставить таким же линейно зависимым, как и в предыдущем ре- шении. Использование такого контейнера позволит легко сортировать записи в алфавитном порядке. ?Н д Основной принцип. В стандартной библиотеке часто можно найти не- заметку сколько разных контейнеров, пригодных для решения вашей пробле- мы. Чтобы выбрать наиболее подходящее решение, нужно обратить внимание на временные характеристики выполнения стандартных процедур в разных классах. Поскольку большинство классов- контейнеров имеет сходные интерфейсы, переход от одного класса к другому, как правило, не вызывает проблем. Общие представления о контейнере list Стандартная библиотека предлагает шаблон другого класса-контейнера, назы- ваемого списком. Список, так же, как и вектор, относится к последовательным кон- тейнерам. При работе с последовательными контейнерами программист имеет воз- можность выбирать место добавления новой записи: в начало, в конец или в середи- ну списка. В стандартной библиотеке представлены три типа последовательных контейнеров: вектор, список и двухсторонняя очередь. Все последовательные классы-контейнеры имеют сходные интерфейсы. Так, функции size, empty, push back, pop back и back служат для выполнения тех же задач и имеют одинаковые характеристики. Для всех этих классов определены кон- структоры по умолчанию, которые создают пустые контейнеры, и деструкторы, уда- ляющие выбранные объекты из памяти компьютера. Встроенные конструктор- копировщик и оператор присваивания можно использовать д ля копирования всего содержимого контейнера. Все эти классы также имеют встроенные функции insert и*erase с одинаковыми интерфейсами для всех классов, но (в чем вы убедитесь поз- же) с разной эффективностью работы. Как уже говорилось выше, все стандартные классы-контейнеры имеют сход- ные пользовательские интерфейсы, что позволяет программисту легко менять классы-контейнеры, не переписывая весь код программы. В документах стан- дартов четко описаны требования ко всем операциям, выполняемым встроен- ными функциями-членами классов-контейнеров. Класс, созданный в соответст- вии с установленными стандартами, называется отвечающим стандартам, да- же если он не входит в состав стандартной библиотеки. В свою очередь, все классы и функции, представленные в стандартной библиотеке, в обязательном порядке должны отвечать стандартам, благодаря чему достигается универсаль- ность их использования и взаимозаменяемость. Программист может расширить стандартную библиотеку своими наработками, отвечающими стандартам. О расширяемости стандартной библиотеки мы поговорим позже, а сейчас просто убедимся в том, насколько просто заменить в программе один библиотечный класс-контейнер другим. 90 Глава 4. Альтерантивная реализация адресной книги
Реализация класса AddressBook с использованием списка Замена вектора на список в программе реализации класса AddressBook показана в листинге 4.1. 1://TinyPIM (с)1999 Pablo Halpern. Файл AddressBook.h 2: 3: #ifndef AddressBook__dot__h 4:#define AddressBook_dot_h 5: 6:#include <list> 7:#include "Address.h" 8: 9:class AddressBook 10: { 11:public: 12: AddressBook() ; 13: -AddressBook(); 14: 15: // Классы исключений 16: class AddressNotFound {}; 17: class Duplicateld {}; 18: 19: int insertAddress(const Address& addr, int recordld = 0) 20: throw (Duplicateld); 21: void eraseAddress(int recordld) throw (AddressNotFound); 22: void replaceAddress(const Address& addr, int recordld = 0) 23: throw (AddressNotFound); 24: const Address& getAddress(int recordld) const 25: throw (AddressNotFound); 26: 27: // Программа тестирования для вывода содержимого адресной кни- ги 28: void print() const; 29: 30:private: 31: // Запрещение копирования 32: AddressBook(const AddressBook&); 33: AddressBook& operator^(const AddressBook&); 34: 35: static int nextld_; 36: std::list<Address>addresses ; 42:}; 43: 44:#endif // AddressBook dot h Открытый интерфейс не претерпел никаких изменений. Отличия состоят только в том, что в строке 6 вводится файл заголовка <list>, а в строке 36 для создания пе- ременной-члена используется спецификатор std:: list. Реализация класса AddressBook с использованием списка 91
Перемещение по списку с помощью итераторов Обратим теперь внимание на функцию insertAddress. Сначала вспомним, как она выглядела при использовании класса-контейнера vector. Код этой функции был представлен в листинге 3.6. Для удобства мы повторили интересующий нас фраг- мент в листинге 4.2. Листинг 4.2. Предыдущий вариант реализации функции InsertAddress в контейнере vector X 1:int AddressBook::insertAddress(const AddressS addr, 2: int recordld) throw (Duplicateld) 3: { 4: if (recordld == 0) 5: // Если recordld не задан, генерируется новый ID. б: recordld = nextld_++; 7: else if (recordld >= nextld__) 8: // Проверяет, чтобы nextId было больше идентификационных // номеров всех остальных записей. 9: nextld_ = recordld + 1; 10: else if (getByld(recordld) != notFound) 11: // Явно заданный ID не уникален 12: throw Duplicateld(); 13: 14: // Вставляет новую запись в вектор. 15: addresses_.push_back(addr); 16: 17: // Присваивает записи идентификационный номер 18: addresses_.back().recordld(recordld); 19: 20: return recordld; 21: } Основная часть этой функции — строки 15 и 18 — останется неизменной. Проблемы мохут возникнуть в строке 10. Списки, в отличие от векторов, не поддерживают индекси- рование записей и произвольный доступ к элементам. Поэтому функции, возвращающие индекс, как функция getByld, теряют смысл. В векторах всегда используются последова- тельности индексированных элементов, хотя на предыдущем примере вы, возможно, не оценили в полной мере преимущества произвольного доступа к индексированным эле- ментам. Хотя класс list не поддерживает произвольный доступ к элементам, в нем (и во всех других контейнерах) используется вспомогательный класс, называемый итерато- ром, который осуществляет последовательный доступ к элементам. Итератор работает наподобие курсора, “указывающего” на определенный элемент контейнера. Приращение (инкремент) итератора переводит его на следующий эле- мент. Обратное приращение (декремент) возвращает его на предыдущий элемент. Все стандартные библиотечные классы-контейнеры содержат встроенные классы итераторов, которые осуществляют навигацию по элементам коллекции. Классы- итераторы содержат функцию-член begin, которая возвращает итератор на первый элемент коллекции, и функцию end, которая переводит итератор на позицию, нахо- дящуюся за последним элементом коллекции. 92 Глава 4. Альтерантивная реализация адресной книги
В листинге 4.3 показана реализация функции insertAddress с использованием итераторов. >' ? • >. х;..**'* сл • • чг--,-|7 Г ' у4, ' **Г'Л ? " •<•*** ЛЛ *» л . •* V"** >. * Г55 ~ •£ •••'• • *' Листинг 4.3. Реализация функции insertAddress с использованием Г итераторов класса list (фрагмент файла AddressBook. срр) l:int AddressBook::insertAddress(const Address& addr, 2: int recordld) throw (Duplicateld) 3:{ 4: if (recordld == 0) 5: // Если recordld не задан, генерируется новый ID. б: recordld = nextld_++; 7: else if (recordld >= nextld__) 8: // Проверяет, чтобы nextld было больше идентификационных // номеров всех остальных записей. 9: nextld_ = recordld + 1; 10: else И: { 12: // Убеждаемся в отсутствии записей с такими же ID 13: for (std::list<Address>::iterator i — addresses_.begin(); 14: i != addresses^.end(); ++i) 15: if (i->recordld() == recordld) 16: // Явно заданный ID не уникален 17: throw Duplicateld(); 18: } 19: 20: // Вставляет новую запись в список. 21: addresses_.push_back(addr); 22: 23: // Присваивает записи идентификационный номер 24: addresses__.back () . recordld (recordld) ; 25: 26: return recordld; 27: } В строке 13 выражение std: : list<Address>: : iterator ссылается на класс итератора, принадлежащий реализации list<Address> шаблона list. Данный итератор отличается по типу от итератора std::list<int>::iterator или std: : vector<Address>:: iterator. Каждая реализация шаблона класса- контейнера имеет свой собственный итератор, отличный от других. Цикл с оператором for в строках 13, 14 является типичным примером использования итераторов. Графически эта процедура показана на рис. 4.1. В строке инициализации цикла for объявляется итератор i, указывающий на первый элемент контейнера addresses . Во время приращения цикла итератор переводится на следующий элемент. Цикл прекращается после приращения итератора за послед ний элемент списка. '^Еслм результат приращения итератора не используется в цикле, предпочтительнее прй- fСовет! мекять оператор; • преинкремента вместо постинкремента. В -случае с классами итераторов Ж М ДО* выполнения посТинкремента программа создает временную ненужную копию йгёрато- j Ра> Ж0 замедляет ее работу. Поэтому запись ^itet болеё эффективна, чем it’erff. В строке 15 листинга 4.3 с помощью итератора происходит вызов функции recordld для текущего элемента списка. Итератор разыменовывается с помощью оператора как если бы это был указатель. Если значение recordld совпадет с те- кущим значением элемента, значит, произошло дублирование записи. Это событие вызовет выполнение соответствующего исключения. Перемещение по списку с помощью итераторов 93
Рис. 4.1. Приращение итератора в списке addresses_ Разыменованием называется возвращение значения объекта с помо- " “Рмин щью другого объекта (указателя или итератора), ссылающегося на пер- вый объект. Итератор можно представить как интеллектуальный указатель. Аналогичный цикл for можно было бы использовать для просмотра массива объектов Address с помощью указателя вместо итератора (листинг 4.4). т Интеллектуальным указателем называется объект, который ведет се- 1 ©рмин как указателе в том смысле, что может быть разыменован с помо- J щью оператора operator* или operator-:*. Интеллектуальный указа- тель может содержать встроенные алгоритмы для манипулирования объектами, на которые он ссылается. Мистинг 4.4. Аналогияв работеуказателяиитератора l:Address myarray [5 ]; 2:Address* begin = &myarray[0]; 3:Address* end - &myarray[5]; 4:for (Address*p = begin; p != end; ++p) 5: if (p->recordld() == recordld) 6: throw Duplicateld(); Цикл for в листинге 4.4 практически идентичен циклу из листинга 4.3. Указа- тель р инициализируется первым элементом массива, а затем приращивается до тех пор, пока не выйдет за последний элемент массива. Работа цикла графически проил- люстрирована на рис. 4.2. Итераторы стали применяться в программировании после указателей. С итера- торами, так же, как и с указателями, можно использовать следующие операторы: ++, —, ===, ! =, * и ->. Кроме того, с некоторыми итераторами можно использовать такие дополнительные операторы: +, -, +=, -=, <, >, <= и >=. Во всех случаях итераторы ве- дут себя так же, как и указатели, связанные с массивом данных. В стандартах требо- вания к итератору полностью совпадают с требованиями к указателю, поэтому по- следний можно использовать в качестве итератора. Далее мы воспользуемся тем фактом, что указатель является видом итератора. 94 Глава 4. Альтерантивная реализация адресной книги
Рис. 4.2. Использование указатедя вместо итератора Н а Общий принцип. Итераторы ссылаются на элементы коллекции точно заметку так же, как указатели ссылаются на элементы массива. Удаление элементов с помощью итераторов Для удаления элемента коллекции мы можем использовать почти тот же код, который применяли в векторной реализации класса AddressBook. Функции back ирор_back ра- ботают в списках так же, как и в векторах. Но мы можем значительно упростить код, если воспользуемся функцией-членом erase, как показано в листинге 4.5. l:void AddressBook::eraseAddress(int recordld) 2: throw (AddressNotFound) 3: { 4: for (addrlist::iterator i = addresses_.begin(); 5: i ’= addresses_.end(); ++i) 6: if (i->recordld() == recordld) 7: * break; 8: 9: if (i == addresses—.end()) 10: throw AddressNotFound(); 11: 12: addresses—.erase(i); 13: } В строках 4, 5 выполняется тот же самый цикл, который мы уже видели в функ- ции insertAddress. В строках 6, 7 цикл завершается, если будет найдена искомая запись. Если запись с указанным- ID не будет обнаружена, то цикл завершается со значением i. равным addresses_. end. Это еще одна идиома использования итера- торов: при выполнении поиска по всему контейнеру присвоение итератору конечно- го значения означает отсутствие искомой записи в коллекции. Строки 9, 10 отсле- живают событие отсутствия записи, чтобы вызвать соответствующее исключение. Перемещение по списку с помощью итераторов 95
Собственно удаление записи происходит в строке 12. Здесь функция-член списка erase вызывает деструктор для текущего элемента, на который указывает итератор. Контейнер list поддерживает константное по времени выполнения удаление запи- си из середины списка. Хотя функция erase поддерживается также классами векто- ров и двухсторонних очередей, в них она является линейно зависимой. (Только уда- ление последнего элемента коллекции константно по времени выполнения.) Функ- ция erase перегружена таким образом, что может вызываться одновременно двумя итераторами. При этом она удаляет все объекты между элементами, на которые ука- зывает первый (включительно) и последний (исключительно) итераторы. Редактирование элементов с помощью итераторов Реализация функции replaceAddress показана в листинге 4.6. В данном приме- ре итератор используется для изменения выбранных элементов контейнера. Листинг 4.6. Реализация функции replaceAddress с использованием итераторов (фрагмент файла AddressBook. срр) l:void AddressBook::replaceAddress(const AddressS addr, int recordld) 2: throw (AddressNotFound) 3: { 4: if (recordld == 0) 5: recordld = addr.recordld(); 6: 7: for (addrlist::iterator i = addresses_.begin(); 8: i != addresses^.end(); ++i) 9: if (i->recordld() == recordld) 10: break; 11: 12: if (i == addresses_.end()) 13: throw AddressNotFound(); 14: 15: *i = addr; 16: i->recordld(recordld); 17: } Строки 7-12 точно такие же, как в функции eraseAddress. Программа просматрива- ет коллекцию объектов Address в поиске элемента с заданным ID и вызывает исключе- ние в случае отсутствия искомого элемента в списке. После выполнения цикла (если он не завершился вызовом исключения) итератор i ссылается на искомый элемент. В стро- ке 15 итератор разыменовывается и по ссылке, возвращенной оператором operator*, присваивает новое значение связанному с ним элементу. В строке 16 вызывается функ- ция recordld, которая изменяет элемент, указанный итератором i. Таким образом, мы получили механизм изменения элементов контейнера с помощью итератора. Использование итераторов в константных контейнерах Перейдем теперь к реализации функции getAddress. Наш первый вариант (листинг4.7) напоминает функцию eraseAddress, только в данном случае мы не удаляем элемент, а возвращаем его. 96 Глава 4. Альтерантивная реализация адресной книги
Листинг 4.7^ Первый вариант реализации функции getAddress l:const Address&AddressBook::getAddress(int recordld)const 2: throw (AddressNotFound) 3: { 4: for (addrlist::iterator i = addresses_.begin(); 5: i != addresses_.end(); ++i) 6: if (i->recordld() == recordld) 7: break; 8: 9: if (i == addresses__.end() ) 10: throw AddressNotFound(); 11: 12: return *i; 13: } Строки 4-10 идентичны функции eraseAddress (листинг 4.5). В строке 12 итератор разыменовывается, и функция возвращает ссылку на искомый объект Address. С этим простым кодом возникнет только одна проблема — его нельзя скомпили- ровать. Функция get Address объявлена как константная, что делает переменные- члены контейнера addresses_ константными по контексту. Если бы мы использо- вали обычный итератор в данном константном контейнере, то возник бы логический конфликт, так как итератор позволяет пользователю изменять элементы контейне- ра, как это было в функции replaceAddress. Чтобы лучше понять проблему поддержания константности, рассмотрим аналогичную программу с использованием указателя. Предположим, что мы за- менили контейнер addresses_ на массив объектов Address и для навигации по нему используем указатель на объект Address. Если предполагается открыть доступ к массиву только для чтения, то будет использоваться указатель типа const Address*. Следовательно, для работы с константным контейнером нам нужен итератор, аналогичный константному указателю. Для этого используется особый тип итераторов const iterator. Такой итератор работает точно так же, как и обычный, за тем исключением, что при разыменовании он возвращает константную ссылку. Таким образом, становится невозможным изменить эле- мент, на который ссылается константный итератор. Измененный вариант функции get Address показан в листинге 4.8. Листинг 4.8. Исправленный вариант функции getAddress (фрагмент файла AddressBook. срр) 1:const Address& AddressBook::getAddress(int recordld) const 2: throw (AddressNotFound) 3: { 4: for (addrlist::const^iterator i = addresses_.begin(); 5: i != addresses_.end(); ++i) 6: if (i->recordld() ~= recordld) 7: break; 8: 9: if (i == addresses_.fend()) 10: throw AddressNotFound(); 11: 12: return *i; 13: } Перемещение по списку с помощью итераторов 97
Единственное изменение было внесено в строку 4, где вместо ключевого слова iterator мы использовали const_iterator. При вызове для константных контейнеров функции begin и end возвращают константные итераторы. Так, в нашем примере i яв- ляется константным итератором. В строке 12 итератор разыменовывается, в результате чего получаем константную ссылку на объект Address, которая и возвращается функци- ей. Точно так же, как и при использовании указателей, в целях безопасности можно пре- образовать обычный итератор в константный, но не наоборот. В результате любые опе- рации, которые не должны изменять элементы контейнера, можно выполнять с кон- стантным итератором, даже если сам контейнер не является константным. Вы, вероятно, заметили, что код в строках 4-10 листинга 4.8 повторяется во всех функциях. Мы можем выделить этот код в закрытую функцию getByld. Объявление этой функции в файле заголовка класса AddressBook показано в листинге 4.9. Листинг 4.9. Объявление функции getByid v л”л" к ’ 1 и&лГИАAUWV.VAkw-avvjCw*.v‘4*v гг'деД-..-.-.де.-.vAv-. — •/Л->*.»Лде «дек 1://TinyPIM (c)1999 Pablo Halpern. Файл AddressBook.h 2: 3:#ifndef AddressBook_dot_h 4:#define AddressBook_dot_h 5: 6:#include <list> 7:#include "Address.h" 8: 9:class AddressBook 10: { 11:public: ... // Открытый интерфейс остался неизменным 29: 30:private: 31: // Запрещение копирования 32: AddressBook(const AddressBook&); 33: AddressBook& operator=(const AddressBook&); 34: 35: static int nextld_; 36: 37: typedef std::list<Address>addrlist; 38: addrlist addresses ; 39: 40: // Возвращает индекс записи с указанным ID. 41: // Возвращает end(), если запись не обнаружена. 42: addrlist::iterator getById(int recordld) 43: throw (AddressNotFound); 44: addrlist::const_iterator getById(int recordld)const 45: throw (AddressNotFound); 4 6:}; 47: 48:#endif // Address Boo k dot h В строке 37 определяется переменная addrlist как псевдоним для std: : list<Address>. Функция getByld, объявленная в строках 42, 43, возвращает addrlist: : iterator, соответствующий итератору std: : list<Address>: : iterator. Возвращенный итератор указывает на элемент с искомым идентификационным номе- ром. Если же элемент с таким номером не будет обнаружен, программа запустит ис- ключение AddressNotFound. В строках 44, 45 объявляется другая версия функции getByld. Эта версия объявлена константной, поэтому может вызываться из другой константной функции и возвращать константный итератор. 98 Глава 4. Альтерантивная реализация адресной книги
Полный код реализации класса AddressBook с использованием шаблона класса- контейнера list показан в листинге 4.10. j Листинг 4.10. Реализация класса AddressBook на основе контейнера list 1://TinyPIM (с)1999 Pablo Halpern. Файл AddressBook.срр 2: 3:#ifndef _MSC_VER 4:ftpragma warning(disable : 4786) 5:#endif 6: 7:#include <iostream> // Для функции print() 8: 9:#include "AddressBook.h" 10: 11:int AddressBook::nextld_= 1; 12: 13 .-AddressBook: : AddressBook () 14: { 15: } 16: 17:AddressBook::-AddressBook () 18: { 19: } 20: 21: int AddressBook: : insertAddress (const Address& addr, 22: int recordld) throw (Duplicateld) 23: { 24: if (recordld == 0) 25: // Если recordld не задан, генерируется новый ID. 26: recordld = nextld_++; 27: else if (recordld >= nextld_) 28: // Проверяет, чтобы nextld было больше идентификационных // номеров всех остальных записей. 29: nextld_ = recordld + 1; 30: else 31: { 32: for (addrlist:: iterator i = addresses__.begin () ; 33: i != addresses_.end(); ++i) 34: if (i->recordld() == recordld) 35: throw Duplicateld(); 36: } 37: 38: // Вставляет новую запись в список. 39: addresses_.push__back (addr) ; 40: 41: // Присваивает записи идентификационный номер 42: addresses_.back().recordld(recordld); 43: 44: return recordld; 45: } 46: 47 : AddressBook: : addrlist: : iterator 48: AddressBook:: getByld (int recordld) throw (AddressNotFound) 49: { 50: for (addrlist::iterator i = addresses_.begin(); Перемещение по списку с помощью итераторов 99
51: i != addresses_.end(); ++i) 52: if (i->recordld() == recordld) 53: return i; 54: 55: throw AddressNotFound(); 56: } 57: 58:AddressBook::addrlist::const—iterator 59:AddressBook::getByld(int recordld) const throw (AddressNotFound) 60: { 61: for (addrlist::const__iterator i = addresses_.begin() ; 62: i != addresses_.end(); ++i) 63: if (i->recordld() == recordld) 64: return i; 65: 66: throw AddressNotFound(); 67:} 68: 69:void AddressBook::eraseAddress(int recordld) 70: throw (AddressNotFound) 71: { 72: addrlist::iterator i = getByld (recordld) ; 73: addresses_.erase(i); 74: } 75: 76:void AddressBook::replaceAddress(const Address& addr, int recordld) 77: throw (AddressNotFound) 78: { 79: if (recordld == 0) 80: recordld = addr.recordld() ; 81: 82: addrlist::iterator i = getByld(recordld); 83: 84: *i = addr; 85: i->recordld(recordld); 86: } 87: 88:const Address&AddressBook: : getAddress (int recordld) const 89: throw (AddressNotFound) 90: { 91: return *getById (recordld) ; 92: } 93: 94:void AddressBook::print() const 95: { 96: for (addrlist::const—iterator i = addresses—.begin(); 97: i != addresses—.end(); ++i) 98: { 99: const Address& a = *i; 100: std:: cout « ’’Record Id:" « a. recordld () « ' \n' 101: « a.firstname()<< ’ ' « a.lastname() « ' \n’ 102: « a.address() « '\n’ « a.phone() « ’\n’ 103: « std::endl; 104: } 105: } 100 Глава 4. Альтерантивная реализация адресной книги
Пройдемся по узловым строкам листинга 4.10. В строках 47-56 определяется функция getByld. Строки 50-52 представляют собой все тот же цикл for, с которым мы уже знакомы. Отличие состоит только в том, что в случае обнаружения объекта по заданному ID функция возвращает итератор, указывающий на этот объект (см. строку 53). Если же выполнение программы дойдет до строки 55, это означает, что искомый объект не найден, в результате чего запускается исключение. Другая реа- лизация функции getByld представлена в строках 47-56. Оно отличается тем, что вместо обычного итератора используется константный. В результате эту версию функции getByld можно вызывать из константных функций-членов. В строках 72 и 82 явный цикл for просто был заменен на вызов функции getByld. Функция getByld возвращает итератор, указывающий на искомый объект Address (если только выполнение функции не завершится запуском исключения). Выполнение функции getAddress в строке 91 состоит в простом вызове функции getByld, после чего возвращенный итератор разыменовывается с целью получения ссылки на искомый объект Address. В строках 96, 97 для выполнения функции print вновь используется цикл for с итератором вместо индекса (поскольку объекты list не поддерживают индексиро- вание). Поэтому в строке 99 вместо оператора индексирования, используемого в классе-векторе, применяется разыменование итератора. Вывод программы тестирования для новой версии класса AddressBook (листинг 4.11) почти такой же, как и при тестировании предыдущей версии, осно- ванной на классе-векторе. Листинг 4.11. Вывод программы тестирования новой версии |. ,.А класса, AddressBook .... 1:*** Three Address Entries *** 2:Record Id: 1 3:Joan Smith 4:The Very Big Corporation 5:Somewhere, MA 01000 6: (617)555-9876 7: 8:Record Id: 2 9:Abigale Adams 10:743 Broadway ll:New York, NY 12:(212)555-3734 13: 14:Record Id: 3 15:Neighborhood Video 16: 17:555-FILM 18: 19:*** After adding a duplicate Abigale Adams *** 20:Record Id: 1 21:Joan Smith 22:The Very Big Corporation 23:Somewhere, MA 01000 24: (617) 555-9876 25: 26:Record Id: 2 27:Abigale Adams 28:743 Broadway Перемещение по списку с помощью итераторов 101
29:New York, NY 30: (212)555-3734 31: 32:Record Id: 3 33:Neighborhood Video 34: 35:555-FILM 36: 37:Record Id: 4 38:Abigale Adams 39:1 Small St. 40:Marlboro, MA 02100 41: (508)555-4466 42: 43:*** After erasing Joan Smith *** 4 4:Record Id:2 45‘.Abigale Adams 46:743 Broadway 47:New York, NY 48:(212)555-3734 49: 50:Record Id: 3 51:Neighborhood Video 52: 53:555-FILM 54: 55:Record Id: 4 56:Abigale Adams 57:1 Small St. 58:Marlboro, MA 02100 59:(508)555-4466 60: 61:*** After replacing Neighborhood Video 62:Record Id: 2 63:Abigale Adams 64:743 Broadway 65:New York, NY 66: (212)555-3734 67: 68:Record Id: 3 69:Neighborhood Video 70:22 Main St. 71:My town, MA 02200 72:555-FILM 73: 74:Record Id: 4 75:Abigale Adams 76:1 Small St. 77:Marlboro, MA 02100 78: (508)555-4466 79: 80:*** Copy of d: *** 81:Abigale Adams 82:1 Small St. 83:Marlboro, MA 02100 84: (508) 555-4466 85: 102 Глава 4. Альтерантивная реализация адресной книги
Обратите внимание, что в строках44-59 объекты Address следуют в порядке возрастания идентификационных номеров. Как вы помните, в векторе порядок был нарушен в результате удаления записи. Хотя для нашей программы это свойство списка может показаться не столь важным, тем не менее, на данном примере мы ви- дим, как возможность быстро вставлять и удалять элементы из середины списка по- зволяет сохранить исходный порядок записей. Далее мы используем это свойство списка для поддержания алфавитного порядка следования записей после сортиров- ки. Недостаток списка состоит в том, что вы не можете быстро получить доступ к объектам по их индексам. Поддержание списка в алфавитном порядке До сих пор работа класса AddressBook, основанного на контейнере-списке, не от- личалась большей эффективностью по сравнению с его реализацией на основе кон- тейнера-вектора. В обоих случаях поиск элементов выполнялся линейно зависимы- ми функциями, вставка и удаление элементов также были константными по време- ни выполнения в обоих случаях. Единственное отличие состояло в том, что появилась возможность во время вставки и удаления элементов в список поддержи- вать некоторый порядок следования элементов. Сейчас мы воспользуемся этой воз- можностью, чтобы сортировать записи в списке в алфавитном порядке. Добавление В класс Address операторов отношений Для сортировки записей контейнера нужно иметь возможность сравнить два объ- екта, чтобы определить, какой из них должен быть первым. Наиболее часто для сравнения переменных используются операторы отношений (<,>,== и т.д.), с помо- щью которых можно сортировать записи в естественном порядке. В нашем классе Address пока нет операторов отношений, поэтому определим их в листинге 4.12. Листинг 4.12. Определение операторов отношений для класса Address (дополнение к файлу Address. h) l:bool operator==(const Address&, const AddressS); 2:bool operator<(const Address&, const Address&); 3: 4:#include <utility> 5:using namespace std::rel ops; В строке 1 объявляется оператор равенства класса Address. Он возвращает true, если два объекта Address имеют одинаковые поля фамилии, имени, номера телефо- на и адреса. В строке 2 объявляется оператор < (меньше) класса Address. Он возвра- щает true, если значение фамилии первого объекта Address меньше значения фа- милии второго объекта. Если два объекта имеют одинаковые строки в полях true, то сравниваются поля имен. Другими словами, оператор < возвращает true, если пер- Поддержание списка в алфавитном порядке 103
вый объект в алфавитном порядке предшествует второму объекту. Этот оператор не сравнивает поля номера телефона и адреса. В строке 5 в код программы добавлен заголовок <utility>, в котором описано пространство имен std: : rel ops, вложенное в пространство имен std. В строке6 открывается глобальный доступ ко всем идентификаторам пространства std: : relops. Это пространство имен содержит шаблоны определений операторов отношений !=,>,<= и >=. Если в классе с помощью using открыт доступ к простран- ству std: : rel ops и даны определения операторов == и <, то в нем можно использо- вать все операторы отношений. Примите к сведению, что директива using открыва- ет глобальный доступ к операторам пространства имен во всей программе, а не толь- ко в классе Addrе s s. В некоторых случаях, особенно при использовании нестандартных компиляторов, гло- бальный доступ к std:: rel_ops может привести к возникновению конфликтов с опе- раторами отношений, определенными в других классах программы. В листинге 4.12 гло- бальный доступ открыт для упрощения кода, но в реальных программах от такого подхода; по возможности следует воздерживаться., Определения операторов ! = и < для класса Address показаны в листинге 4.13. { Листинг 4.13. Использование операторов отношений в классе Address |Я " (дополнение к файлу Address . срр) l:bool operator==(const Address& al, const Address& a2) 2: { 3: return (al.lastname() == a2.lastname() && 4: al.firstname() == a2.firstname() && 5: al.phone() == a2.phone() && 6: al.address() =- a2.address()); 7: } 8: 9:bool operator<(const Addressfi al, const Address& a2) 10: { 11: if (al.lastname() < a2.lastname()) 12: return t rue; 13: else if (а2. .lastname() < al.lastname ()) 14: return false; 15: else 16: return (al.firstname() < a2.firstname()); 17: } Выполнение операторов отношений достаточно прямолинейно. Оператор равен- ства в строках 1-7 возвращает true, если все переменные-члены двух объектов рав- ны между собой. Оператор < (меньше) в строках 9-17 прежде всего сравнивает фами- лии. Если строки фамилий равны между собой, сравниваются имена. Обратите вни- мание, что ни одна из функций оператора не требует объявления себя другом класса Address, поскольку в них используются только открытые функции доступа. Другом называется функция или класс, которым разрешен доступ к за- I “рмиг1 крытым членам другого класса. Для объявления функции или класса другом другого класса используется ключевое слово friend. (Чтобы больше узнать об объявлении и использовании друзей классов, обрати- тесь к любому справочнику по C++.) 104 Глава 4. Альтерантивная реализация адресной книги
Ввод объектов Address в алфавитном порядке Теперь наша задача— ввод объектов Address в адресную книгу в алфавитном порядке. Алфавитный порядок будет соблюдаться в том случае, если любая запись в списке равна или относительно больше последующих и равна или относительно меньше предыдущих. (Под больше и меньше при сравнении строк следует понимать истинность выполнения одноименных операторов, сравнивающих строки в соответ- ствии с заданным алгоритмом.) Таким образом, прежде чем ввести новую запись в список, нам нужно определить ее позицию, используя для этого операторы отно- шений. Эту процедуру мы выполним с помощью дополнительного цикла в функции insertAddress, как показано в листинге 4.14. Листинг 4.14. Ввод записей в алфавитном порядке с помощью функции insertAddress (фрагментфайлаAddressBook.срр) l:int AddressBook: : insertAddress (const AddressS addr, 2: int recordld) throw (Duplicateld) 3: { 4: if (recordld == 0) 5: // Если recordld не задан, генерируется новый ID. 6: recordld =nextld_++; 7: else if (recordld >= nextld_) 8: // Проверяет, чтобы nextld было больше идентификационных // номеров всех остальных записей. 9: nextld_ = recordld + 1; 10: else 11: { 12: for (addrlist::iterator i = addresses_.begin(); 13: i ’= addresses—.end(); ++i) 14: if (i->recordld() == recordld) 15: throw Duplicateld(); 16: } 17: 18: addrlist::iterator i; 19: for (i = addresses_.begin(); i != addresses_.end(); ++i) 20: if (addr < *i) 21: break; 22: 23: // Вставляет новую запись в список. 24: i = addresses .insert(i, addr); 25: 26: // Присваивает записи идентификационный номер 27: i->recordld(recordld); 28: 29: return recordld; 30: } В строке 18 объявляется итератор, указывающий на элемент, после которого нужно вставить новую запись. Этот элемент определяется с помощью цикла в стро- ке 19, который завершается после обнаружения объекта, относительно большего, чем addr (строки 20 и 21). В строке 24 вызывается функция insert, являющаяся членом класса list. Эта функция вставляет объект addr после элемента, на который ссылается итератор i. Функция insert возвращает итератор на вставленный эле- мент. Этот итератор используется в строке 27 для присвоения идентификационного Поддержание списка в алфавитном порядке 105
номера новому элементу. Функция insert выполняется таким образом во всех по- следовательных контейнерах, но только в списках ее работа константна по времени выполнения (в векторах и двухсторонних очередях время выполнения этой функции линейно зависимо от размера контейнера). Что произойдет, если цикл в строках 19-21 достигнет конца списка? Это событие будет свидетельствовать о том, что объект addr относительно больше всех других элементов списка addresses . В таком случае после завершения цикла итера- тор i будет иметь значение addresses_. end (). Ввод новой записи по данному ите- ратору будет эквивалентен вызову функции push buck, т.е. новый элемент будет до- бавлен в конец списка. Это как раз то, что и требуется. Аналогичный результат полу- чится, если список addres sеs_ будет пустым. Мы еще не закончили работу. Хотя записи вводятся в алфавитном порядке, этот порядок может быть нарушен в случае изменения поля имени с помощью функции replaceAddress. Чтобы проконтролировать изменения записей, проще всего уда- лить с помощью функции replaceAddress старую запись, а затем ввести новую, как показано в листинге 4.15. Листинг 4.15. Изменение функции replaceAddress ДЛЯ поддержания алфавитного порядка следования записей в списке l:void AddressBook::replaceAddress(const Address& addr, int recordld) 2: throw (AddressNotFound) 3: { 4: if (recordld == 0) 5: recordld = addr.recordld(); 6: 7: eraseAddress(recordld); 8: insertAddress(addr,recordld); 9: } Эта версия функции replaceAddress немного снизит эффективность выпол- нения программы, поскольку удаление и вставка записи будет происходить даже в том случае, если поля имени и фамилии останутся неизменными. Впрочем, со- всем не сложно устранить этот недостаток, проконтролировав, какие поля были изменены. Вы можете сами изменить код должным образом, поэтому не станем задерживаться на этом. Вывод программы тестирования для улучшенной версии класса AddressBook по- каз в листинге 4.16. { Листинг 4.16. Выполнение программы тестирования класса AddressBook, поддерживающего алфавитный порядок следования записей в списке'. V; 1:*** Three Address Entries *** 2:Record Id: 2 3:Abigale Adams 4:743 Broadway 5:New York, NY 6: (212)555-3734 7: 8 .-Record Id: 3 9:Neighborhood Video 106 Глава 4. Альтерантивная реализация адресной книги
10: 11:555-FILM 12: 13:Record Id: 1 14:Joan Smith 15:The Very Big Corporation 16:Somewhere, MA 01000 17: (617)555-9876 18: 19:*** After adding a duplicate Abigale Adams 20:Record Id: 2 21:Abigale Adams 22:743 Broadway 23:New York, NY 24: (212)555-3734 25: 26:Record Id: 4 27:Abigale Adams 28:1 Small St. 29:Marlboro, MA 02100 30: (508)555-4466 31: 32:Record Id: 3 33:Neighborhood Video 34: 35:555-FILM 36: 37:Record Id: 1 38:Joan Smith 39:The Very Big Corporation 40:Somewhere, MA 01000 41: (617) 555-9876 42: 43:*** After erasing Joan Smith *** 44:Record Id: 2 45:Abigale Adams 46:743 Broadway 47:New York, NY 48: (212) 555-3734 49: 50:Record Id: 4 51:Abigale Adams 52:1 Small St. 53:Marlboro, MA 02100 54: (508) 555-4466 55: 56. ’Record Id:3 57:Neighborhood Video 58: 59:555-FILM 60: 61:*** After replacing Neighborhood Video *** 62:Record Id: 2 63:Abigale Adams 64:743 Broadway 65:New York, NY 66: (212)555-3734 67:output Поддержание списка в алфавитном порядке 107
68 .-Record Id: 4 69:Abigale Adams 70:1 Small St. 71:Marlboro, MA 02100 72: (508)555-4466 73: 74:Record Id: 3 75 Neighborhood Video 76:22 Main St. 77:My town, MA 02200 78:555-FILM 79: 80:*** Copy of d: *** 81:Abigale Adams 82:1 Small St. 83:Marlboro, MA 02100 84: (508) 555-4466 85: Вы можете убедиться, что записи вводятся в алфавитном порядке и что этот по- рядок сохранился после удаления и изменения некоторых записей. Резюме Записи в нашей адресной книге теперь упорядочены по алфавиту. Мы добились этого, заменив шаблон класса-контейнера vector на list и воспользовавшись тем преимуществом списков, что в них вставка записей в середину списка константна по времени выполнения. Но в результате перехода к новому типу контейнера мы утра- тили возможность произвольного доступа к элементам по их индексам. Для навига- ции по спискам нам пришлось освоить работу с итераторами, которые в целом по своим свойствам напоминают знакомые нам указатели. Мы также узнали о концеп- циях, используемых при выборе типа классов-контейнеров. Следующая наша задача— создать редактор объектов Address, позволяющий создавать и изменять записи адресной книги. Для этого нам потребуются дополни- тельные средства класса string, включая функции извлечения и конкатенации строк, а также операторы ввода и вывода строк. 108 Глава 4. Альтерантивная реализация адресной книги
Глава 5 Редактирование записей адресов с помощью функций класса String и операторов ввода-вывода В этой главе... • Определение требований к редактору 109 • Базовый класс редактора 110 • Редактирование объекта Address с помощью интегрированного редактора 123 • Новое требование к программе: форматированный номер телефона 129 • Резюме 136 Переключим наше внимание с классов ядра программы на классы пользова- тельского интерфейса. Пользователь должен иметь возможность создавать и из- менять записи адресной книги с помощью какого-нибудь текстового редактора, который позволит вводить текст во все поля объекта Address и возвращать их для изменений. Определение требований к редактору В главе 1 мы определили основные требования к программе TinyPIM, оставив до- работку деталей на потом. Прежде чем приступать к созданию редактора адресов, давайте детально проанализируем требования к тому, что он должен выполнять. Каждый объект Address представляет собой коллекцию полей. Поля фамилии, имени и номера телефона содержат однострочные записи, а поле адреса может со- держать запись из нескольких строк. Для редактирования полей объекта Address необходимо создать конструкцию из двух вложенных редакторов. Простейший тек- стовый редактор должен вызываться из внешнего редактора, предоставляющего для редактирования одно конкретное поле. При создании пользовательского интерфейса будем опираться на следующие правила. • Для редактирования каждого поля записи должно открываться приглаше- ние, содержащее имя редактируемого поля (например, для поля фамилии — “First Name”) и текущую строку текста. Для однострочных полей текущая строка представляет все содержимое поля.
• После изменения пользователем строки и нажатия <ENTER> происходит за- мена старого текста на новый. • Если пользователь ничего не изменит в строке и нажмет <ENTER>, исходное содержимое поля должно сохраниться. • Если пользователь введет точку (.) и нажмет <ENTER>, текущая строка оста- ется прежней, изменения в других строках записи, сделанные ранее, сохра- няются, а сеанс редактирования завершается. Тем самым мы предоставим пользователю возможность выйти из сеанса редактирования и продолжить работу с программой. • Если пользователь введет !х и нажмет <ENTER>, сеанс редактирования за- вершается, а все внесенные изменения отменяются. Это позволит пользо- вателю отменить изменения записи, если была допущена ошибка. Правила редактирования многострочного поля адреса будут несколько отличаться. • Многострочное содержимое поля адреса выводится для редактирования та- ким образом, будто адрес завершается пустой строкой. Пользователь может начать редактирование сразу с заполнения этой пустой строки. • Если пользователь введет !п и нажмет <ENTER>, текущая строка остается прежней, а для редактирования открывается следующее поле записи. • Если пользователь введет !i и нажмет <ENTER>, пустая строка вставляется перед текущей. Пользователь продолжает редактирование с заполнения этой пустой строки. • Если пользователь введет !d и нажмет <ENTER>, текущая строка удаляется, а он переходит к редактированию следующей строки. Этих же правил мы будем придерживаться при создании редактора книги кон- тактов. Как вы видите, принципы редактирования достаточно просты. Никаких диалоговых окон, никаких курсоров и полноэкранного редактирования. Интерфейс программы предельно упрощен, поскольку стандартная библиотека C++ не предос- тавляет средств для создания графического пользовательского интерфейса. Для этих целей служат другие популярные библиотеки классов C/C++, такие как X Window, Microsoft Foundation Classes и Macintosh Toolkit. Библиотеки графического пользователь- ского интерфейса всегда рассматривались как особая область программирования, зависимая от конкретной операционной системы и платформы компьютера. Хотя в последнее время, после создания языка программирования Java и переносимой сис- темы создания окон X Window, многие проблемы совместимости были решены. Таким образом, поскольку стандартная библиотека C++ поддерживает только ба- зовый ввод и вывод текста и двоичных данных, мы сосредоточимся на программи- ровании текстового интерфейса командной строки и изучим средства, предостав- ляемые библиотекой потоков ввода-вывода. Это будут полезные для вас сведения, так как именно библиотека потоков ввода-вывода используется для организации взаимодействия программы с файлами данных. Базовый класс редактора Теперь, когда мы выработали требования к пользовательскому интерфейсу ре- дактора, перейдем к конструированию классов. На рис. 5.1 показана часть диаграм- мы классов, описывающая взаимодействие классов редактора. 110 Глава 5. Редактирование записей адресов с помощью функций...
Базовый класс Editor в нашей системе встроенных редакторов выполняет функ- ции внутреннего редактора. Он содержит две функции-члена: одна используется для редактирования однострочных полей, та- ких как поле фамилии, а другая— для ре- дактирования многострочных полей, та- ких как поле адреса. Производные классы выполняют функции внешнего редактора. Оба клас- са содержат функцию-член edit, кото- рая распознает поля записи. Класс AddressEditor вызывает функцию edit для всех полей объекта Address, а класс Рис. 5.1. Диаграмма взаимодействия клас- сов редактора AppointmentEditor вызывает функцию edit для всех полей объекта Appointment. Наши идеи реализованы в классе Editor, определение которого показано в листинге 5.1. ' Листинг 5.1. Определение базового класса Editor 1://TinyPIM (с)1999 Pablo Halpern. Файл Editor.h 2: 3:#ifndef Editor_dot_h 4:#define Editor_dot_h 1 5: 6:#include <string> 7: 8:// Базовый класс Editor. 9:// Предоставляет базовые функции редактирования однострочных и // многострочных полей. 10:class Editor И: { 12:public: 13: enum editStatus { 14: normalr // Нормальное редактирование 15: finished, // Завершение сеанса редактирования 16: canceled // Отмена редактирования. 17: }; 18: 19:Editor() :status (normal){} 20: 21: // Редактирование однострочных полей. // Принимает приглашение и текущую строку. 22: // Эта функция изменяет строку и возвращает true в // случае нормального 23: // редактирования или false — в случае завершения или отмены 24: // редактирования. Следующие ключевые символы имеют особое // значение при вводе: 25: // 26: // Нажатие <ENTER> сохраняет строку неизмененной. 27: // Точка означает завершение редактирования. 28: // !х Означает отмену редактирования. 29: bool editSingleLine(const std::string&prompt, std::string&value) Базовый класс редактора 111
30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44 : 45: 46: 47: 48: 49: 50: 51: // Редактирование многострочных полей. Принимает приглашение и // исходные значения всех строк поля. Измененная строка // сохраняется в поле. Каждая строка поля редактируется // по отдельности. Эта функция изменяет строки и возвращает true // в случае нормального редактирования или false — в случае // завершения или // отмены редактирования. Следующие ключевые символы имеют // особое значение при вводе: // // Нажатие <ENTER> без ввода текста оставляет // текущую строку неизмененной и переходит к редакти- // рованию // следующей строки. // . Точка означает завершение редактирования. // !х Означает отмену редактирования. // !п Завершение редактирования текущего поля и переход к // следующему полю (сохраняется состояние нормального // редактирования). // !i Вводит новую строку перед текущей. // !d Удаляет текущую строку. bool editMultiLine(const std::string&prompt, std::string&value); // Возвращает текущее состояние редактирования. editStatus status()const {return status ;} 52:private: 53: editStatus status_; 54: }; 55: 56:#endif //Editor dot h Большая часть определения приходится на комментарии. Обе функции editSingleLine и editMultiLine, объявленные в строках 29 и 47, принимают в ка- честве аргументов строку приглашения и текущее значение поля. Обе функции воз- вращают true при нормальном завершении редактирования: нажатие <ENTER> в случае редактирования однострочного поля или ввод !п при редактировании мно- гострочного поля. Если пользователь ввел точку (.) или символы !х, это означает соответственно пе- реход к состоянию редактирование завершено или редактирование отменено, как определено в строках перечисления 13-17. Функция status в строке 50 возвращает текущее состояние последнего вызова одной из функций редактирования. Конструк- тор в строке 19 инициализирует состояние редактирования как нормальное (normal). Если текущее состояние finished (редактирование завершено) или canceled (редактирование отменено), то функции editSingleLine и editMultiLine возвращают false. Вывод приглашений пользователю Начнем с реализации функции editSingleLine. Эта функция должна вывести приглашение пользователю, считать строку текста, введенную с терминала, и при- нять решение, что делать с этой строкой. Код функции editSingleLine показан в листинге 5.2. 112 Глава 5. Редактирование записей адресов с помощью функций...
Листинг 5.2. Реализация функции Editor:: editSingleLine t X...-. __iXXfr'Ss- _ !LZ_ 2 C-‘«z-.Л. ..V iZZzz‘z’ ' z= ?'X z-ч z 'V'L'^X . - * ^£^^*Xxrb^^^v^* ;viz - .•• — i >> 1://TinyPIM (c)1999 Pablo Halpern. Файл Editor.cpp 2: 3:#include <iostream> 4:#include "Editor.h" 5: 6:bool Editor::editSingleLine(const std::string& prompt, 7: std::strings value) 8: { 9: status_ = normal; 10: 11: std::cout « prompt; 12: if (!value.empty()) 13: std::cout « " [" « value « ’]’; 14: std::cout « 15: 16: std::string result; 17: std::getline(std::cin, result, ’\n‘); 18: 19: // Очищает состояние ввода и прерывает редактирование в случае // обнаружения ошибки 20: if (std::cin.fail()) 21: { 22: status_ = canceled; 23: return false; 24: } 25: 26: 27: //Оставляет значение неизмененным в случае 28: // нажатия пользователем клавиши <ENTER>. 29: if (result.empty()) 30: return true; 31: 32: //В случае ввода точки завершает редактирование. 33: if (result == ".") 34: { 35: status_=finished; 36: return false; 37: } 38: 39: // Отмена редактирования. 40: if (result == “lx”) 41: { 42: status_ = canceled; 43: return false; 44: } 45: 46: // Возвращает новую строку 47: value = result; 48: return true; 49: } 50: Код получился неожиданно длинным, но вполне прямолинейным. В строках 11-14 программа выводит приглашение пользователю. Строка 11 просто посылает текст при- глашения на стандартное устройство вывода. Если строку prompt не завершить симво- лом разрыва строки, то курсор в текстовом терминале окажется не на новой строке, а в конце строки приглашения. Если редактируемое поле не было пустым, то пользовате- Вазовый класс редактора 113
лю нужно показать его текущее содержимое. Чтобы отделить текущее значение поля от приглашения, мы заключаем его в квадратные скобки (см. строку 13 листинга 5.2). Стро- ка 14 завершает выводимый текст символом двоеточия и пробелом. Текст приглашения направляется в поток вывода оператором «. Строка 13 кода является хорошим примером составления с помощью оператора вывода комплекс- ных строк, состоящих из блоков текста, представленных литералами разных типов. Сначала выводится текстовая строка с нулевым окончанием " [" (тип const char*), затем строковый объект value (тип std: : string) и наконец одиночный символ ' ] ’ (тип char). Несмотря на однотипный вывод, обработка компьютером этих значений при печати существенно отличается. Следует добавить, что целые числа, числа с плавающей запятой и данные всех других типов также по-разному выводятся компьютером, хотя синтаксис вывода в C++ для всех одинаков. Универсальность синтаксиса программного кода при выводе данных разных типов в C++ достигается за счет перегрузки имен функций. Объект std: :cout представляет стандартное устройство вывода и является реализацией класса std: : ostream. (Объекты std: :cerr и std: :clog также являются реализациями класса std: :ostream и пред- ставляют соответственно стандартные потоки ошибок и журнала регистраций.) Когда компилятор обнаруживает выражение std: : cout « х, он просматривает все перегру- женные версии оператора вывода operator« и выбирает ту из них, в которой первый аргумент имеет тип std: : ostream, а второй аргумент имеет тип переменной х. Каждая перегруженная версия operator« работает по-своему, благодаря чему реализуются раз- личные механизмы вывода значений разных типов. Так, когда в коде встречается выражение std::cout « 5; компилятор выбирает следующую функцию-член класса ostream: class ostream { public: ostreams operator« (int) ; // ... } Другими словами, оператор «во время компиляции преобразовывается в сле- дующее обращение: std: : cout. operator« (5) ; Если же в программе встречаются выражения std::string s("hello"); Std::cout <<S; компилятор запускает следующую глобальную функцию (обратите внимание, что это глобальная функция, а не член класса ostream): std::ostream& operator<<(std::ostream&, const std::string&); Следовательно, в предыдущем примере с выводом значения типа std: : string оператор « преобразовывается в вызов следующей глобальной функции: operator« (std: :cout, s) ; Функция operator« класса ostream перегружена для вывода данных всех встроенных типов, таких как char, int, float и т.д. Вывод данных других типов, та- ких как std: : string, поддерживается глобальной версией оператора operator<<. Программисты, перешедшие к C++ от языка С, часто по-прежнему продолжа- ют использовать функцию форматированного вывода print f из стандартной библиотеки С. В функции printf для указания типа параметра используются символы форматирования. Например, %d определяет целое число. Если вы все 114 Глава 5. Редактирование записей адресов с помощью функций...
еще используете printf. примите к сведению следующие факты, которые, быть может, убедят вас перейти к потокам вывода C++. Прежде всего, обращает на се- бя внимание универсальность синтаксиса вывода в C++. Выбор соответствую- щей версии перегруженной функции осуществляется автоматически во время компиляции программы. Благодаря автоматизму выбора функции поддержива- ется сохранность типов. Тем самым перекрывается источник многочисленных ошибок, характерных для вывода данных в языке С, когда указанный символ форматирования вступает в противоречие с реально выводимым значением. Синтаксис вывода в C++ настолько прост, что кажется, будто это встроенная функция компилятора, хотя в действительности это объект библиотечного клас- са. И наконец, систему вывода в C++ можно расширить, как вы в этом убедитесь во время знакомства с классами дат и времени. т Сохранность типов — это концепция работы компилятора, призванная I ер МИН предотвращать конфликты между объявленным и реальным типом данных, который может привести к возникновению ситуации неопре- деленности. В языке C++ предусмотрены средства приведения типов, позволяющие программисту целенаправленно нарушать сохранность типов на свой собственный риск. Далее вы узнаете, что библиотечные потоки позволяют выполнять не только все операции, возможные с использованием функции printf (и ее напарницы по вводу данных— scant), но и реализовать многие дополнительные возможности. Справочная информация: Экскурс неформатированный ввод и вывод_______________________ Операторы ввода и вывода (соответственно «и ») осуществляют форма- тированный ввод-вывод. Под этим термином понимается обработка дан- ных с точки зрения человеческой, а не машинной логики. Например, при форматированном вводе-выводе происходит преобразование целого деся- тичного числа в двоичный код и обратно или преобразование двоичного кода в строку текста, состоящую из слов, разделенных пробелами. Но иногда нужно обработать поток данных просто как последователь- ность двоичного кода, не анализируя смысл передаваемой информа- ции. Для этого служит неформатированный ввод-вывод. Класс ostream предоставляет для осуществления неформатированного вывода следующие функции-члены: ♦ put (char с) — выводит отдельный символ с; ♦ write(const char* s, unsigned n) —выводитn символов, начиная c s. Функции класса istream для неформатированного ввода более разно- образны: ♦ get () — считывает и возвращает отдельный символ из пото- ка ввода; ♦ ge t (cha г & с) — считывает из потока ввода отдельный символ с; ♦ get(char* s, unsigned n, char delim = ' \n')—считы- вает из потока ввода последовательность из п-1 символов Базовый класс редактора 115
и помещает их в буфер, заданный указателем s, прерывая ввод в случае обнаружения символа разделителя, заданного параметром delim. Символ разделителя не считывается и ос- тается в потоке ввода. В конец последовательности символов всегда вводится нулевой символ окончания строки. ♦ getline(char* s, unsigned n, char delim = ‘ \n')—ра- ботает как функция get, но удаляет символ разделителя из потока ввода, не занося его в буфер. Кроме того, если вся по- следовательность п-1 символов считывается до обнаружения символа разделителя, функция устанавливает флаг потока ввода failbit, в результате чего проверочная функция fail () возвратит true. ♦ read (char* sf unsigned n) — считывает n символов и со- храняет их в буфере, заданном указателем s. Символ оконча- ния строки не добавляется. Если функции get, getline и read обнаружат символ окончания фай- ла до завершения ввода заданной последовательности символов, уста- навливается флаг eofbit. Функция read при этом также устанавлива- ет флаг failbit. Если поток вывода связан с устройством, контролирующим положение указателя вывода, можно использовать следующие функции: ♦ tel 1р () — возвращает текущее положение указателя вывода; ♦ seekp (position) — помещает указатель вывода в заданную позицию; ♦ seekp (offset, dir) —смещает указатель вывода на задан- ный сдвиг (offset). В параметре dir используются констан- ты ios : : beg, ios : : cur и ios : : end, которые определяют точ- ку отсчета сдвига (соответственно с начала, с текущей пози- ции и с конца потока). В потоке istream есть аналогичные функции tellg и seekg (перегружена так же, как и seekp). Как вы, наверное, догадались, окончание “р” в функциях потока ostream означает put (положить), а “д” в функциях istream — get (взять). Прием данных, вводимых пользователем, и контроль за ошибками Для ввода данных в C++, точно так же, как и для вывода, можно использовать универсальный синтаксис, независимый от типа данных. Для этого используется оператор ввода (») класса istream. В таком случае вас может удивить, почему в строке 17 листинга 5.1 мы не исполЁзовали оператор ввода. Проблема с этим опе- ратором состоит в том, что он осуществляет ввод по словам, т.е. ввод прекращается после обнаружения первого символа пробела. Если бы мы переписали стро- ку 17 с использованием оператора ввода, то она выглядела бы так: std::cin » result; 116 Глава 5. Редактирование записей адресов с помощью функций...
Это выражение считывает первое слово из стандартного потока ввода std: : cin. Если пользователь введет с клавиатуры 10 Oak St., TOBresult будет введено только значение 10. Остальная часть введенного текста останется в потоке ввода в ожида- нии нового обращения. Если мы хотим ввести всю строку, содержащую пробелы, то следует воспользоваться функцией get line. Функция get line определена в заголовке <string> следующим образом: std::istream& getline(std::istream&, std::string&, char delim = ’\n'); Эта функция считывает последовательность символов из потока ввода в объект string и прерывает ввод при обнаружении символа разделителя, заданного пара- метром delim. В строке 17 листинга в качестве разделителя задан конец строки ’ \п ’, в результате чего функция string считывает и заносит в result всю строку, введенную пользователем со стандартного устройства ввода (обычно терминала). Символ разделителя удаляется из потока ввода, но не вводится в строковую пере- менную. В нашем примере мы могли не указывать символ разделителя, так как ко- нец строки принимается этой функцией в качестве разделителя по умолчанию. В строке 20 проверяется ошибка ввода, которая может произойти в результате поступления символа конца файла или обрыва связи с устройством ввода. В потоках ввода и вывода определены четыре функции-члена для возвращения текущего со- стояния потока, одна из которых— fail. Она возвращает true, если ввод был пре- рван в силу каких-либо обстоятельств. Близкая к ней функция bad возвращает true, если ошибка была связана с некорректным вводом, например в результате неис- правности устройства ввода. Функция fail всегда возвращает true, если bad воз- вратила true. Другая функция eof возвращает true при обнаружении символа кон- ца файла. В зависимости от типа потока ввода, конец файла может быть не обнару- жен даже после возникновения ошибки, связанной со считыванием данных за пределами файла. Поэтому не стоит полагаться на eof, если fail уже возвратила true. Еще одна функция— good— возвращает true только при условии, когда все остальные функции состояния возвращают false. Если good возвратила false, то следующая операция ввода не будет выполнена, если good возвратила true, то сле- дующая операция ввода также может быть успешной, но это не гарантируется. Используйте функцию good для определения текущего состояния потока перед выпол- < Совет! нением ввода. Чтобы узнать; успешно ли прошел ввод данных, используйте функцию Ik JH fail. И если fail возвратила true, используйте bad и eof, чтобы уточнить причину ошибки. Функции good, bad, fail и eof определены в базовом классе ios, от которого произведены классы потоков istream и ostream. Поэтому эти функции одинаково работают как с потоками вывода, так и ввода. Но необходимости контролировать по- ток вывода обычно не возникает. При выводе данных не может неожиданно возник- нуть символ конца файла, и поскольку вывод осуществляет машина, а ввод — поль- зователь, то вывод данных обезопасен от некорректной работы пользователя. Будьте внимательны: basic_ios Экскурс В стандартной библиотеке C++ класс ostream в действительности яв- ляется псевдонимом, определенным с помощью ключевого слова typedef от basic_ostream<char, char__traits<char> >. Классы iostream и ios также получились в результате определения типа от более сложных реализаций шаблонов. Описание параметров шаблонов классов потоков ввода-вывода выходит за рамки этой книги. Достаточ- Базовый класс редактора 117
но сказать, что в результате реализации шаблона должен получиться класс, позволяющий создать поток данных, поддерживающий взаимо- действие с внешним устройством, для которого минимальная единица обмена данных превышает размер одного байта. Как правило, для вы- вода данных используется 16-разрядная таблица международных сим- волов. Определения типов wostream, wistream и wios автоматически настраиваются на обмен многоразрядными пакетами данных и не тре- буют установки параметров шаблона. В библиотеке также представле- ны стандартные объекты wcout, wcerr, wclog и wcin, которые поддер- живают обмен пакетами данных и являются аналогами объектов cout, cerr, clog и cin. В большинстве случаев для программиста нет разницы, какие объекты использовать, так как установка параметров шаблона выполняется компилятором. Но проблема может возникнуть при использовании программы отладки. С этой проблемой мы уже сталкивались при рас- смотрении класса string, когда компилятор показывает сообщение об использовании слишком длинных имен. В этой ситуации главное не паниковать, ничего страшного не происходит. Интересно, что это тот редкий случай, когда использование более ран- них версий стандартной библиотеки позволяет избежать проблемы. Дело в том, что в ранних версиях библиотеки разных изготовителей не было шаблона потоков, поэтому ostream и iostream были реальными классами, а не определениями типов. Если в строке 20 (листинг 5.2) обнаружится, что вызов функции readline про- шел неуспешно, нужно предпринять какие-то действия по разрешению проблемы. Можно либо запустить исключение, либо попытаться определить причину ошибки с помощью дополнительных функций состояния потока. Но в нашей программе мы просто прервем сеанс редактирования, установив в строке 22 состояние canceled, и возвратимся из функции edit SingleLine в строке 23. Поскольку в этом случае бу- дет отменен ввод всех данных, пришедших с текущего потока is tream (в данном слу- чае с объекта cin), нет смысла осуществлять проверку после каждого обращения к функции ввода. Достаточно в конце проверить состояние потока, чтобы опреде- лить, были ли допущены ошибки во время сеанса редактирования. Переход программы к выполнению строки 29 означает, что введенная строка бы- ла считана успешно. Теперь мы переходим к анализу текущего состояния сеанса ре- дактирования. Прежде всего определяется, не нажимал ли пользователь клавишу <Enter> без ввода текста. В строке 29 с помощью оператора i f мы определяем ввод пустой строки, используя функцию-член empty класса std: : string. Если перед на- жатием <Enter> текст не был введен, строка 30 возвратит true, что означает нор- мальное завершение редактирования текущего однострочного поля. Текущее значе- ние поля остается прежним, и состояние сеанса редактирования сохраняется nor- mal, как было установлено в строке 9. В строке 33 определяется ввод точки, что означает завершение сеанса редак- тирования. Обратите внимание, что в этом выражении с помощью оператора равенства (==) мы сравниваем объект std: : string с литеральной константой (const char*). В заголовке <string> определяются все операторы отношений для сравнения двух объектов std: : string, но с помощью тех же операторов можно сравнивать объекты с константами const char*. Причем слева от опера- тора может быть как объект, так и константа. Вообще в классе string преду- 118 Глава 5. Редактирование записей адресов с помощью функций...
смотрено автоматическое преобразование литеральных констант в объекты string (хотя в данном случае используются перегруженные функции операто- ров отношений), поэтому всюду, где по контексту ожидается объект string, можно использовать литеральную константу. Если пользователь введет точку (.), то редактирование строк завершает- ся, а сеансу редактирования присваивается состояние finished (строки 35, 36). Подобная конструкция используется в строках 40-44 для отмены редак- тирования при вводе пользователем символов !х. Если ни одно из особых состояний не было определено, то в строке 47 текущему объекту string присваивается строка текста, введенная пользователем. Наконец, в стро- ке 48 завершается выполнение функции editSingleLine. Таким обра- зом, строка, введенная пользователем, замещает текущее значение поля только в случае нормального завершения функции. В случае обнаружения ввода любого специального символа (пустая строка, точка или !х) исходное значение поля сохраняется. Добавление и замена отдельных строк в многострочной записи Алгоритм редактирования многострочных полей состоит в разделении записи на отдельные строки и вызове редактора однострочных полей для каждой отдельной строки. Пример такой программы показан в листинге 5.3. i Листинг 5.3. Реализация функции Editor::editMultiLine 51: 52:bool Editor: .-editMultiLine (const std::string& prompt, 53: std::strings value) 54: { 55: status_ - normal; 56: 57:// Добавляет символ разрыва строки. Это упрощает последующий код. 58:// Каждая введенная строка заканчивается символом разрыва строки. 59:// В конце записи всегда есть дополнительная пустая строка. 60: value += '\п'; 61: 62: std::string::size_type lineBegin = 0, lineEnd = 0, lineLen - 0; 63: for (;;) 64: { 65: // Добавляет новую пустую строку, если курсор находится в // конце строки 66: if (lineBegin >= value.length()) 67: value += '\n’; 68: 69: // Выделяет часть текста в новую строку 70: std::string line; 71: lineEnd = value.find('\n’, lineBegin); // Всегда добавляется // новая строка 72: lineLen = lineEnd - lineBegin; 73: line = value.substr(lineBegin, lineLen); 74: Базовый класс редактора 119
75: 76: // Редактирование однострочной записи if (’editSingleLine(prompt, line)) 77: break; 78: 79: // Определение специальных символов 80: if (line == "In”) 81: break; // Пользователь завершает ввод многострочной записи 82: 83: if (line == ”!i”) 84: { 85: // Пользователь хочет ввести новую строку. 86: value.insert(lineBegin, "\n"); // Добавляется пустая строка 87: continue; // Редактирование новой строки 88: } 89: 90: if (line == "!d") 91: { 92: // Пользователь хочет удалить строку. 93: // Удаляются вся строка и символ окончания строки // после нее. 94: value.erase(lineBegin, lineLen + 1) ; 95: continue; // Запуск цикла редактирования следующей строки 96: } 97: 98: // Заменяет строку новым значением 99: value.replace(lineBegin, lineLen, line); 100: 101: // Курсор переводится за символ окончания новой 102: // строки. 103: lineBegin += line.length() + 1; 104: } 105: 106: // Редактирование закончено. Удаляется ненужная пустая строка // в конце записи. 107: std::string::size_type strip = value.find_last_not_of('\n') + 1; 108: value.erase(strip, std::string::npos); 109: 110: return (status() == normal); 111: } В строке 62 инициализируются переменные, представляющие длину строки и позиции символов начала и конца строки в многострочной записи. Для этих переменных используется тип-член класса std: : string: :size_type, который предназначен для хранения беззнаковых целочисленных значений. Этим харак- теристикам как раз и отвечают значения позиций символов и длины строки. В большинстве случаев для таких значений вполне бы подошел тип unsigned int, но использование встроенного типа гарантирует большую совместимость и ошибкоустойчивость программы. Позиция начала строки хранится в перемен- ной lineBegin, конца строки— в переменной lineEnd и длина строки — в lineLen. Термин Тип-член является членом класса и представляет собой определение ти- па или другой вложенный класс. Например, в выражении class string { public: typedef unsigned long size_type }; size_type выступает типом-членом класса std: : string. 120 Глава 5. Редактирование записей адресов с помощью функций...
find(const string& str, size_type pos = 0); find (const char* p, size_type pos, size__type len) ; find(const char* p, size__type pos = 0) ; find(char c, size_type pos = 0); В строке 63 начинается цикл обработки многострочной записи, заданной значе- нием value. Это бесконечный цикл, который прерывается в случае обнаружения ввода пользователем специального символа. В начале выполнения цикла перемен- ная lineBegin содержит номер позиции первого символа текущей строки. В стро- ке 71 с помощью функции-члена find класса string мы находим конец строки. Функция find ищет символ или последовательность символов внутри текущей стро- ки. Существует четыре перегруженные версии функции find: size_type size_type size_type size_type Все четыре варианта служат одной и той же цели: ищут вложение искомой строки (str, р или с) в другой строке и возвращают номер первого элемента искомой строки от начала текущей строки. Аргумент pos определяет текущую позицию, от которой нужно начать поиск искомой строки. Аргумент len устанавливает длину строки, за- данной указателем р. Если len не задана, предполагается, что р указывает на строку с нулевым окончанием. Если любая из версий find не находит искомой строки, то возвращается значе- ние std: .-string: :npos— константа, равная максимальному значению, допусти- мому для типа std: : string: : size__type. Вызов функции find в строке 71 никогда не должен возвращать проз, поскольку в результате выполнения строк 60 и 67 в те- кущей строке (по крайней мере в ее начале) обязательно должен находиться искомый символ разрыва строки. Таким образом, переменные lineBegin и lineEnd определяют границы текущей строки в многострочной записи. В строке 72 простым вычитанием определяется длина текущей строки (исключая символ разрыва строки). В строке 73 текущая строка текста извлекается для редактирования из мно- гострочной записи с помощью функции substr. Эта функция принимает в каче- стве аргументов номер первого символа и длину строки, а возвращает новый строковый объект. В классе string фрагмент строки текста (подстрока) опреде- ляется не как особый тип данных, а как трио аргументов, представляющих ис- ходную строку, начальный символ и длину подстроки. Если аргумент длины опущен, то подстрока начинается с начального символа и продолжается до кон- ца исходной строки. Если мы посмотрим список всех функций-членов класса string, то обнаружим, что данный набор аргументов повторяется довольно час- то. Проще будет разобраться с работой многих функций обработки строк, если вы будете рассматривать эти три аргумента как логический комплекс, представ- ляющий подстроку текста. Хотя в функции find и некоторых других, начинаю- щихся с find, эти аргументы имеют другой смысл. На Общий принцип. Многие функции-члены класса string принимают Заметку в качестве аргументов другие строки. Многие из этих функций пере- гружены для приема и обработки строки разных типов. Строковых объектов: string s Подстрок: string s, size_type pos, size_type len Строк в стиле языка С: const char* s Базовый класс редактора 121
Массивов символов: const char* buf, size__type len Отдельных символов: size_type count, char c В последнем случае параметром count определяется число повторений символа с. После выполнения строки 73 переменной line присваивается копия одностроч- ного фрагмента исходной многострочной записи. Для редактирования этого текста в строке 76 вызывается уже знакомая вам функция editSingleLine. Если пользова- тель прервет каким-либо образом редактирование строки, то функция editSingleLine возвратит false. В этом случае выполнение функции editMultyLine просто завершается. Следующий блок выражений, начиная со стро- ки 80, отслеживает ввод пользователем специальных символов редактирования многострочных полей. Так, в строке 80 выполнение цикла редактирования завер- шится, если пользователь введет символы !п. В строке 83 проверяется ввод символов !i, что означает ввод пустой строки перед текущей. Ввод пустой строки происходит в строке 86 с помощью функции insert. Пер- вый аргумент функции insert определяет позицию, куда необходимо ввести новую строку. Вторым аргументом в нашем случае будет строка текста с нулевым окончани- ем. Есть также перегруженные версии функции insert, в которые можно передавать объект string, подстроку в виде известных вам трех аргументов или массив символов с указанием его д лины. В нашем случае мы просто вставляем в начало текущей строки литеральную константу "\п", означающую символ разрыва строки. Это равносильно вводу новой пустой строки по позиции, заданной переменной lineBegin. Со стро- ки 87 повторится цикл для редактирования новой введенной строки. В строке 90 определяется ввод символов !d, означающий желание пользователя удалить текущую строку. Для этого в строке 94 вызывается функция-член erase. Для этой функции задаются аргументы начала подстроки и ее длина. Поскольку пе- ременная lineLen содержит значение длины строки без учета символа разрыва строки, для функции erase нужно прирастить значение lineLen на единицу. В ре- зультате мы удалим как содержимое текущей строки, так и разрыв строк, сохранив целостность многострочной записи. После выполнения функции erase текущей станет строка, на которую указывает переменная lineBegin. В строке 95 повторяет- ся цикл редактирования текущей строки. Если пользователь не ввел никаких специальных символов, то старую строку нужно заменить новым текстом, введенным пользователем. Для этого в стро- ке 99 используется функция replace. Первые два аргумента функции replace ука- зывают исходную позицию и длину подстроки, которую следует заменить. Третий аргумент содержит текст, на который следует заменить указанную подстроку. Как и многие другие функции-члены класса string, функция replace перегружена таким образом, чтобы с третьим аргументом, помимо объектов string, можно было пере- давать переменные типов const char*, const char* и переменную длины строки size_type или три аргумента, определяющих подстроку. В строке 99 происходит замещение текущего значения переменной value. Пере- менная lineBegin теперь указывает на начало новой введенной строки. Мы подо- шли к концу цикла редактирования отдельной строки многострочной записи. Чтобы продолжить редактирование, нам нужно перейти к новой строке. Строка 103 увели- чивает значение lineBegin на число символов в новой строке плюс один символ 122 Глава 5. Редактирование записей адресов с помощью функций...
разрыва строки. Возвращаясь к началу цикла, программа проверяет в строке 66, не был ли достигнут конец записи. В этом случае программа добавит новую пустую строку. Оператор += в данном случае выполняет ту же роль, что и функция-член append. (Класс string поддерживает toperator, который выполняет конкатенацию двух строк и возвращает новый объект string.) Каждый раз, когда цикл возвраща- ется к строке 67, программа добавляет новую строку записи. Это будет продолжаться до тех пор, пока пользователь не введет какой-либо специальный символ прекраще- ния сеанса редактирования, — !п, !х или ., что приводит к выходу из цикла редакти- рования строк. Выполнение строк 60 и 67 гарантирует, что во время работы цикла редактирова- ния значение переменной value всегда будет завершаться символом разрыва стро- ки. Но после завершения редактирования нам уже не нужна пустая строка в конце записи. В строке 107 выполняется функция f ind__last_not_of, которая находит по- следний символ строки, не являющийся символом разрыва строки. В функции find_last_not_of используются те же наборы аргументов, что и в функции find. Эта функция начинает поиск с конца строки и возвращает номер первого обнару- женного ею символа, не соответствующего символам, переданным с аргументами. Для общего представления перечислим все другие функции-члены класса string, работающие аналогично функции find: find_last, find__first_of, f ind_last_of, find_first_not_of и find__last_not_of. Все эти функции, так же, как и find, имеют по четыре перегруженные версии. Функции find и find__last осуществляют поиск строк, а остальные ищут вхождения символов, представленных или не пред- ставленных в списках, переданных с аргументами. Функции, начинающиеся с f ind_last, ведут поиск с конца строки, остальные — с начала строки. После выполнения строки 107 переменной strip присваивается номер, на еди- ницу больший позиции последнего значимого символа. В строке 108 функция erase удаляет все символы от указанного и до конца строки. В результате из записи будут удалены все концевые пустые строки. Строка ПО возвращает true, если состояние сеанса редактирования, установленное функцией editSingleLine, осталось normal или false — во всех остальных случаях. Как вы видели, класс string предоставляет большой набор функций для измене- ния и поиска строк. Интерфейс многих, из этих функций достаточно громоздкий, но однотипный, что позволяет быстро в нем разобраться. Обычно с аргументами пере- даются объекты string, подстроки, заданные тремя аргументами, одиночные сим- волы, указатели на строки с нулевыми окончаниями или на массив символов с ука- занием длины. Функции поиска разнообразнее, но и в них прослеживаются общие принципы использования аргументов. Редактирование объекта Address с помощью интегрированного редактора Обратим теперь внимание на класс AddressEditor, унаследованный от базового класса. В нашей интегрированной системе из двух редакторов этот класс выполняет роль внешнего редактора. Назначение класса AddressEditor состоит в управлении редактированием всех полей конкретного объекта Address. Программа клиента просто дает команду, какой именно объект взять для редактирования. Все остальное выполняет объект AddressEditor. Редактирование объекта Address... 123
т Клиентом называется программа или часть программы, использующая другой модульный компонент. В объектно-ориентированном програм- мировании каждый класс и функция предлагают для использования свою службу. Части программного кода, которые используют службу другого класса или функции, называются клиентами этой службы. С развитием сетевых технологий под отношениями клиент-сервер в большей степени стали понимать отношения между двумя програм- мами, взаимодействующими по сети. В листинге 5.4 показано определение класса AddressEditor. i Листинг 5.4. Определение класса: AddressEditor * 1://TinyPIM (с)1999 Pablo Halpern, Файл AddressEditor.h 2: 3:#ifndef AddressEditor_dot_h 4:#define AddressEditor_dot_h 1 5: 6:#include "Editor.h" 7:#include "Address.h" 8: 9:// Класс редактирования объекта Address. 10. -class AddressEditor : public Editor 11: { 12 .-public: 13: // Пустой объект Address 14: AddressEditor() ; 15: 16: // Редактирование существующего объекта Address 17: AddressEditor(const AddressS a); 18: 19: // Использование деструктора, сгенерированного 20: // компилятором: ~AddressEditor(); 21: 22: // Главный цикл возвращает true в случае успешного редактирования 23: // записи и false — в случае отмены редактирования. 24: bool edit (); 25: 26: // Эта функция доступа возвращает измененный адрес. 27: Address addr()const {return addr_;} 28: 29: // Эта функция доступа предоставляет объект Address для // редактирования: 30: void addr(const AddressS a){addr_ = а;} 31: 32:private: 33: // Запрещение копирования 34: AddressEditor(const AddressEditorS); 35: const AddressEditorS operator^(const AddressEditorS); 36: 37: // Переменные-члены 38: Address addr_; 39: }; 40: 41:#endif //AddressEditor_dot_h 124 Глава 5. Редактирование записей адресов с помощью функций...
Интерфейс класса очень простой. Функция edit в строке 24 просто указывает, успешно ли прошел сеанс редактирования. Объект Address открывается для редак- тирования либо с помощью конструктора (строка 17), либо с помощью функции дос- тупа addr (строка 30). Как альтернатива, в строке 14 с помощью конструктора по умолчанию создается пустой объект Address. Эта опция редактирования использу- ется пользователем в том случае, когда ему нужно не изменять существующую за- пись, а создать новую. Если сеанс редактирования завершился успешно (функция edit возвратила true), измененный объект Address можно возвратить с помощью перегруженной функции доступа для чтения addr (строка 27). Благодаря функциональным возможностям, унаследованным от класса Editor, выполнить класс AddressEditor довольно просто, как это показано в листинге 5.5. Листинг 5.5. Реализация класса AddressEditor 1://TinyPIM (с)1999 Pablo Halpern, Файл AddressEditor.cpp 2: 3:#include <iostream> 4: 5:#include "AddressEditor.h" 6: 7:// Пустой объект Address. 8:AddressEditor::AddressEditor() 9: { 10: } 11: 12: 13:// Редактирование существующего объекта Address 14:AddressEditor::AddressEditor(const Address& a) 15: : addr_(a) 16: { 17: } 18: 19: // Главный цикл возвращает true в случае успешного редактирования 20:// записи и false — в случае отмены редактирования. 21:bool AddressEditor::edit() 22: { 23: // Извлечение полей записи 24: std::string lastname(addr_.lastname()) ; 25: std:: string f irstname (addr__. firstname ()) ; 26: std::string phone(addr_.phone()); 27: std:: string address (addr__. address () ) ; 28: 29: editSingleLine("Last name", lastname) && 30: editSingleLine("First name", firstname) && 31: editSingleLine("Phone Number", phone) && 32: editMultiLine("Address", address); 33: 34: if (statusO == canceled) 35: return false; 36: 37: // Сохранение изменений 38: addr_.lastname(lastname); 39: addr__. firstname (firstname) ; 40: addr__.phone (phone) ; Редактирование объекта Address... 125
addr__.address (address) ; return true; 41: 42: 43: 44: } Центральной в этом классе является функция edit. В строках 24-27 выполняют- ся функции доступа класса Address для извлечения отдельных полей записи. В строках 29-31 для каждого однострочного поля вызывается функция editSingleLine, а в строке 32— функция editMultiLine для изменения поля ад- реса. Обращения к функциям базового класса разделены оператором логическо- го И (&&), в результате каждый следующий вызов будет происходить только в случае успешного выполнения предыдущего вызова (функция редактирования возвратит true). Если во время работы над каким-либо полем пользователь захочет прервать редактирование и введет символы . или !х, последующие поля не будут открыты для редактирования, а состояние текущего сеанса редактирования можно будет возвра- тить с помощью функции status. Состояние проверяется в строке 34, строка 35 воз- вращает false, если пользователь отменил редактирование, введя в любом поле символы !х. Обратите внимание, что переменная-член addr_ до этого момента не изменялась в программе. Изменения вносятся в каждое поле в строках 38-41 только в том случае, если редактирование не было отменено. И, наконец, строка 43 возвра- щает true, если редактирование завершено успешно. Чтобы протестировать программу, мы создадим объект AddressEditor и будем вызывать функцию edit для разных полей объекта Address, выводя их на печать. Программа тестирования показана в листинге 5.6. ; Листинг 5.6. Программа тестирования класса AddressEditor . 1://TinyPIM (с)1999 Pablo Halpern. Файл AddrEditTest.срр 2: 3:#include <iostream> 4: 5:#include "Address.h" 6:#include "AddressEditor.h" 7: 8:void dump(const Address& a) 9: { 10: std::cout « "Record " « a.recordld() « '\n' 11: « a.firstname() « ' ' « a.lastname() « ’\n' 12: « a.address () «'\n' « a.phone () « ' \n’ « std::endl; 13: } 14: 15:int main() 16: { 17: Address a; 18: 19: AddressEditor editor(a); 20: while (a.lastname() != "done") 21: { 22: editor.edit(); 23: a = editor.addr(); 24: std::cout « std::endl; 2 5: dump(a); 26: } 27: 28: return 0; 29: } 126 Глава 5. Редактирование записей адресов с помощью функций...
В строках 8-13 нашей программы тестирования используется та же функция dump, с которой мы познакомились в главе 2. В строке 19 создается объект AddressEditor, ко- торый инициализируется пустым объектом Address. В строке 20 запускается цикл, ко- торый будет прокручиваться до тех пор, пока пользователь не введет слово done. В стро- ке 22 вызывается функция edit, а в строке 23 результат редактирования копируется в переменную а. Этот результат выводится на печать в строке 25. Давайте запустим программу и введем в поля некоторые данные. Результат вы- полнения программы представлен в листинге 5.7 (курсивом показан текст, вводи- мый пользователем). Листинг 5.7. Результат выполнения программы AddrEditTest l:Last name: Lincoln 2:First name: Abe 3:Phone Number: (202)555-9933 4:Address: The White House 5-.Address: Pennsylvania Ave. 6:Address: Washington, DC 7:Address:. 8: 9:Record 0 10:Abe Lincoln 11:The White House 12:Pennsylvania Ave. 13:Washington, DC 14: (202)555-9933 15: 16:Last name [Lincoln]: Washington 17:First name [Abe]: George 18:Phone Number [(202)555-9933]: . 19: 20:Record 0 21:George Washington 22:The White House 23:Pennsylvania Ave. 24:Washington, DC 25: (202)555-9933 26: 27:Last name [Washington]: Dole 28:First name [George]: Bob 29:Phone Number [(202)555-9933]: 30:Address [The White House]: !x 31: 32:Record 0 33:George Washington 34:The White House 35:Pennsylvania Ave. 36:Washington,DC 37: (202)555-9933 38: 39:Last name [Washington]: Bush 40:First name [George]: 41:Phone Number [(202)555-9933]: 202.555.0011 ext.l 42:Address [The White House]: In 43: 44:Record 0 Редактирование объекта Address ... 127
45:George Bush 46:The White House 47:Pennsylvania Ave. 48:Washington, DC 49:202.555.0011 ext.1 50: 51:Last name [Bush]: Gore 52:First name [George]: Al 53:Phone Number [202.555.0011 ext.l]: 1/202.555-0002 54:Address [The White House]: !d 55:Address [Pennsylvania Ave.]: !d 56:Address [Washington,DC]: 57:Address:. 58: 59:Record 0 60:Al Gore 61:Washington, DC 62:1/202.555-0002 63: 64:Last name [Gore]: 65‘.First name [Al]: 66:Phone Number [1/202.555-0002]: 67:Address [Washington,DC ]: !i 68:Address: Vice President 's Residence 69:Address [Washington, DC]:. 70: 71:Record 0 72:Al Gore 73:Vice President 's Residence 7 4:Washington, DC 75:1/202.555-0002 76: 77 .-Last name [Gore ]: done 78:First name [Al ]: . 79: 80:Record 0 81:Al done 82:Vice president rs Residence 83 Washington, DC 84:1/202.555-0002 85: При запуске программа создает пустой объект Address, после чего вызывает ре- дактор строк. В строках 1-3 редактор предлагает ввести значения в однострочные поля фамилии, имени и номера телефона. Мы вводим данные и, нажимая <ENTER>, переходим к следующему полю. В строках 4-6 данные вводятся в многострочное поле адреса. Редактор будет открывать для нас все новые и новые пустые строки до тех пор, пока мы не прервем его работу, введя символ точки (строка 7). Как вы помните, символ точки воспринимается функцией Editor: .-editMultiLine как сигнал к за- вершению редактирования многострочного поля. В строках 9-14 показан вывод по- лей объекта Address на печать. В строках 16, 17 программа вновь предлагает ввести имя и фамилию, но в этот раз на экране выводятся текущие значения полей, введенные ранее. Мы заменяем текущие имя и фамилию на новые и в строке 18 вводом точки завершаем сеанс ре- дактирования поля. При следующем выводе программой содержимого полей в стро- ках 21-25 мы видим, что внесенные изменения были сохранены, а остальные поля остались прежними. 128 Глава 5. Редактирование записей адресов с помощью функций...
Попробуем теперь прервать сеанс редактирования. В строках 27, 28 были введе- ны новые имя и фамилия, но затем, в строке 30, мы прервали редактирование, введя символы !х. Как видим, в строке 33 все внесенные изменения не сохранились. На следующей итерации мы опробовали свойство сохранения в поле старого зна- чения, если не был введен новый текст. Так, в строке 40 мы просто нажали <Enter>, а в строке 42, после открытия многострочного поля адреса для редактирования, вве- ли символы !п. Эта команда должна открыть для редактирования следующее поле, но поскольку все поля записи закончились, сеанс редактирования завершается. В строках 45, 49 мы видим, что поля фамилии и номера телефона изменились в со- ответствии с введенными новыми значениями, в то время как поля имени и адреса остались прежними. Таким образом, нажатие <Enter> без ввода текста открывает для редактирования следующее поле, а текущее поле сохраняется неизмененным. На следующем цикле мы испытали возможность удаления строки в многостроч- ной записи с помощью символов /d. Так, в строках 54-56 после ввода новых значе- ний для однострочных полей мы удалили первые две строки поля адреса, не изменяя последнюю строку. Вывод программы (строки 60-62) выглядел так, как мы того и ожидали: две удаленные строки адреса исчезли бесследно. Нам осталось испытать только возможность ввода новых строк в многостроч- ное поле адреса. В строке 67 программа открыла для редактирования первую строку адреса. Вместо нового текста мы ввели символы !i, которые служат ко- мандой ввода новой строки. В строке 68 программа открывает пустую строку для ввода новой информации, а после завершения ввода в строке 69 для редактиро- вания вновь открывается та же строка, которую мы видели в строке 67. Но те- перь это уже вторая строка поля адреса. Мы не стали менять эту строку, а просто ввели символ точки для завершения редактирования. Новая введенная строка отобразилась в выводе программы в строке 73. Мы завершили работу программы, введя команду done в поле фамилии в строке 77. Программа еще раз вывела содержимое полей (строки 80-85), после чего закрылась. Новое требование к программе: форматированный номер телефона Как это часто случается со всеми программистами, окончательное представ- ление о том, как должна работать программа, формируется во время тестирова- ния рабочей версии программы. Вас, вероятно, не удовлетворило (и уж тем более не понравится пользователям), как программа выводит номера телефонов. Для удобства пользователей было бы хорошо выводить номера в привычном форма- те. (Для жителей разных стран привычными будут принятые там форматы пред- ставления телефонных номеров. Программисту очень важно не только коррект- но написать программу, ио и учесть вкусы и привычки предполагаемых пользо- вателей.) Рассмотрим для примера следующие стандартные форматы записи телефонных номеров, широко используемые в США: 7-значный ddd-dddd 8-значный с единицей в начале 1 - ddd-dddd 10-значный (ddd) ddd-dddd 11 -значный с единицей в начале 1 (ddd) ddd-dddd Новое требование к программе: форматированный номер... 129
Мы хотим, чтобы программа автоматически форматировала записи номеров те- лефонов в соответствии с одним из этих стандартов. Программа должна выбирать подходящий формат, анализируя число цифр, введенных пользователем, а также принимать во внимание следующие факты. Если пользователь при вводе использо- вал символы разделителей “)” или **/”, они должны игнорироваться програм- мой. Если номер телефона содержит цифры и буквы алфавита, то форматированию должны подвергаться только цифры слева от первой буквы, а вся остальная часть номера сохраняется неизмененной. Если номер невозможно привести ни к одному из четырех указанных форматов, он сохраняется в исходном виде. В соответствии с этими правилами, номер 617/555.6262 ext. 123 будет преобразован в (617) 555- 6262 ext. 123. Решение поставленной задачи расширит ваши знания о средствах редактирования строк, представленных в классе string. Анализ набора символов Первая задача состоит в том, чтобы найти в исходной записи номера телефона буквы алфавита. Рассмотрим функцию editPhone, показанную в листинге 5.8. В листинге показан фрагмент файла AddressEditor.срр. Кроме того, объявление функции editPhone следует добавить в файл заголовка AddressEditor. h. Листинг 5.8. Функция editPhone l:bool AddressEditor::editPhone(const std::stringS prompt, 2: std::strings phone) 3: { 4: if (!editSingleLine(prompt, phone)) 5: 6: return false; 7: // Форматирование номера телефона. 8: // Телефонные номера форматируются в соответствии со // стандартами США. 9: // Общие принципы форматирования: 10: // 7 цифр => ddd-dddd 11: // '1' + 7 цифр => 1-ddd-dddd 12: // 10 цифр => (ddd) ddd-dddd 13: // '1' + 10 цифр => 1 (ddd) ddd-dddd 14: // Выделение суффикса, содержащего нестандартные символы. 15: static const std::string digits("0123456789"); 16: std::string::size_type suffix = 17: phone. find_first__not_of (digits + ”-()/. ”) ; 18: if (suffix == std::string::npos) 19: 20: suffix = phone.length(); 21: // Добавление в суффикс символов, находящихся справа от // последней цифры номера. 22: suffix = phone . f ind__last_of (digits, suffix); 23: if (suffix == std::string::npos) 24: return true; // В префиксе нет цифр. Запись не форматируется. 25: 26: ++suffix; // Исключение из суффикса последней цифры номера 27: // Теперь префикс содержит только цифры и знаки пунктуации 28: // Только цифры копируются в новый номер. 29: std::string newnum; 130 Глава 5. Редактирование записей адресов с помощью функций...
30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: 57: 58: 59: 60: 61: 62: 63: 64: 65: 66: 67: 68: 69: 70: 71: 72: 73: 74: 75: 76: 77: 78: 79: } std::string::size__type p = phone.find_first_of(digits); while (p < suffix) { std::string::size_type q = phone. find_first_not_of (digits, p); // Между p и q содержатся только цифры. // Добавляем их в новый номер. newnum.append(phone, р, q - р); if (q == std::string::npos) break; p = phone.find_first_of(digits, q); } // Теперь в номер добавляется необходимая пунктуация: switch (newnum.length()) { case 7: newnum.insert (3, 1, '-'); break; case 8: if (newnum [0] != '1') return true; // 8-значный номер без '1' в начале. newnum.insert(4, 1, newnum.insert(1, 1, '-'); break; case 10: newnum.insert(6, 1, '-'); newnum.insert(3, ") "); newnum.insert((int)0, 1, ' ('); break; case 11: if (newnum [0] != '1') return true; // 11-значный номер без '1' в начале. newnum.insert(7, 1, newnum.insert (4, ") "); newnum.insert (1, " ("); break; default: return true; // Нестандартный номер } // После завершения пунктуации к номеру добавляется суффикс. phone.replace(0, suffix, newnum); return true; В строке 4 номер телефона редактируется как обычно, т.е. с помощью функции editSingleLine. Форматирование введенного номера начинается в строке 16, где объявляется переменная suffix. Эта переменная содержит позицию первого не- стандартного символа в номере телефона (как правило, это буква алфавита). Под- строка от suffix до конца номера в строке phone исключается из процесса форма- тирования и будет добавлена к номеру в конце выполнения функции. В строке 17 для Новое требование к программе: форматированный номер... 131
выделения суффикса выполняется поиск первого символа в строке, который не от- носится к следующему набору символов: 0123456789-0/- (Обратите внимание, что последний символ в списке — пробел.) Для определения всего набора стандартных символов телефонного номера используется operators-, перегруженный в классе string как оператор конкатенации строк. С помощью этого оператора к символам, заданным в переменной digits, прибавляются символы строкового литерала. Ре- зультат конкатенации передается в функцию find_first_not_of, которая возвра- щает позицию первого нестандартного символа в номере. В строке 18 проверяется, был ли найден нестандартный символ в номере те- лефона. Если номер состоит только из стандартных символов, то переменной suffix в строке 19 присваивается индекс, на единицу больший индекса послед- него символа строки. Если между последней цифрой номера и первым символом суффикса будут какие- либо символы пунктуации, то их тоже следует отнести к суффиксу. В строке 22 осу- ществляется поиск первого вхождения цифрового символа в строке phone в направ- лении от символа, заданного переменной suffix, к началу строки. Функция find_last_of возвращает индекс последней цифры перед суффиксом. Обратите внимание, что если бы во втором аргументе в функцию f ind_last__of не была пере- дана переменная suffix, то функция могла по ошибке возвратить позицию цифро- вого значения, относящегося к суффиксу (например, расширения номера). В стро- ке 25 выполняется инкремент переменной suffix, чтобы исключить из суффикса самую последнюю цифру номера. Но сначала нужно определить, действительно ли функция find_last_of нашла что-нибудь. Все функции поиска в случае неудачи возвращают константное значение string: : проз. В строке 23 мы проверяем равен- ство переменной suffix этому значению. Если ни одной цифры не было найдено слева от суффикса, следовательно, и форматировать нечего. В этом случае строковое значение phone остается таким, как есть, а строка 24 возвращает true. Вычленение и подсчет цифр На следующем шаге нам нужно отделить цифры номера от знаков пунктуации и символов пробелов, чтобы затем отформатировать номер в соответствии с нашими стандартами. Строка цифр будет сохранена в переменной newnura, которая объявля- ется в строке 29. В строке 30 указателю р присваивается индекс первой цифры но- мера. В строке 31 начинается цикл, который будет отбирать цифровые значения из стандартной части телефонного номера (от начала строки и до первого символа суф- фикса). В строке 33 индекс первого обнаруженного нецифрового символа заносится в указатель q. Таким образом, строка между указателями р и q (исключая q) содер- жит только цифры. Эта последовательность цифр добавляется в строке 36 к значе- нию переменной newnura. Функция append, как и многие другие функции-члены класса string, принимает подстроку в виде трех аргументов, определяющих исход- ную строку, индекс первого символа и длину подстроки. Обратите внимание, что ес- ли q равно string::проз (означает, что функция find_first not of не нашла больше символов пунктуации), выражение q - р возвратит всю строку. Это общий принцип для всех функций класса, string, работающих с подстроками. Если длина подстроки превышает размеры исходной строки (как вы помните, константа string: :npos равна максимально большому числу, возможному для типа size type), то ее длина автоматически приводится в соответствие с длиной строки. В нашем случае, как и в большинстве других, это как раз то, что нужно. 132 Глава 5. Редактирование записей адресов с помощью функций...
—Только значение длины подстроки в числе определяющих ее трех аргументов может авто- г гениманиЯ матически корректироваться программой. Положениеподстроки в строке должно за- даваться действительным индексом. При достижении конца строки телефонного номера строка 38 завершает цикл. В противном случае строка 39 инициализирует указатель р индексом первого сим- вола нового блока цифр. По окончании цикла в переменную newnum будут добавлены все цифры телефонного номера. Форматирование номера Удалив все символы пунктуации, введенные пользователем, мы можем пе- рейти к форматированию номера в соответствии с нашими стандартами. Фор- мат выбирается по числу цифр в номере и присутствию единицы в первой пози- ции. Для выполнения этой задачи используется оператор switch. Стро- ка 46 программы вводит символ дефиса после третьей цифры, если номер состоит из семи цифр. Второй аргумент функции insert (1) определяет, сколько символов нужно ввести в строку. Ц-gi Вам может показаться странным, что нельзя ввести один символ без заметку задания числа повторов символа. Например, вызов функции insert (3, ) сгенерирует ошибку. Эта одна из причуд синтаксиса функций стандартной библиотеки C++, о которой следует помнить. В строках 50 и 64 мы определяем, не начинаются ли наши 8- и 11-значные номера с единицы. Если нет, то данный номер нестандартный и не будет форма- тироваться. Строки 53, 54 вводят дефис после первой и четвертой циф- ры 8-значного номера. Обратите внимание, что сначала вводится дефис после четвертого знака, чтобы избежать сдвига значимых символов вправо (ввод де- фиса после первой цифры сделает четвертую цифру пятой). Ввод символов все- гда следует начинать с конца строки, чтобы потом не пришлось заниматься пе- ренумерацией символов строки. В строках 58-60 вводятся дефисы, скобки и пробелы в 10-значный номер. В строке 59 в функцию insert передается строковый литерал, но в эту функцию также можно передавать подстроки, заданные тремя аргументами. В строке 60 мы явно приводим первый аргумент (О) к типу int, поскольку некоторые компиляторы по контексту могут воспринять его как нулевой указатель. Наконец, в стро- ках 67-69 вводятся символы пунктуации в 11-значный номер. Если число цифр в номере отличается от 7, 8, 10 или 11, то выполняется строка 73, которая оставляет номер в переменной phone точно таким, каким его ввел пользователь. В строке 77 мы имеем форматированный номер в переменной newnum и ис- ходный номер в переменной phone. При наличии в номере нестандартных суф- фиксов индекс первого его символа указан в переменной suffix. Теперь мы про- сто замещаем часть исходного номера, находящуюся слева от суффикса, форма- тированным номером из переменной newnum. Для этого используется функция replace. В первых двух аргументах этой функции указаны первый символ заме- няемой подстроки и ее длина. Остальные аргументы определяют объект string, подстроку, строку в стиле С или массив символов, которыми следует заменить исходную подстроку. Новое требование к программе: форматированный номер... 133
Теперь мы довольны Нам осталось внести небольшие изменения в функцию AddressEditor: :edit, как показано в листинге 5.9. Листинг 5.9. Функция edit, форматирующая телефонные номера с помощью: J функции edit Phone j 1:// Главный цикл возвращает true в случае успешного редактирования 2:// записи и false — в случае отмены редактирования. 3:Ьоо1 AddressEditor::edit() 4: { 5: // Извлечение полей записи 6: std::string lastname(addr_.lastname()); 7: std::string firstname(addr_.firstname()); 8: std::string phone(addr_.phone()); 9: std::string address(addr_.address()); 10: 11: editSingleLine("Last name”, lastname) && 12: editSingleLine("First name", firstname) && 13: editPhone("Phone Number", phone) && 14: editMultiLine("Address", address); 15: 16: if (status() == canceled) 17: return false; 18: 19: // Сохранение изменений 20: addr_.lastname(lastname); 21: addr_.firstname(firstname); 2 2: addr_.phone(phone); 23: addr_.address(address); 24: 25: return status() != canceled; 26: } Была изменена строка 13, в которой вместо функции editSingleLine теперь вы- зывается функция edit Phone. Проверим наш класс AddressEditor с помощью той же программы тестирования (см. листинг 5.6). При вводе тех же данных мы получим результат, показанный в листинге 5.10. S<; 'ft*' л* a J л .t „V- .v . •• ^-4><г К.. •* V/-v?, % V Л 4 W «А -..V , а%а a v; v..- 3 Ч , алча ; w.; Листинг 5.10. Вывод программы тестирования после добавления функции форматирования телефонных номеров i .ci., ку./.у. fa uv-..л zj'./o-a . - _..z-..;.ziz-z-.v^wz-^v.Iz.'-zz^-. .-bv-v ;z>zfozz<zkti.'z^-J.Cz.?,> - w>z:-v..z.«-i:.vzw\--.,^v*.z.<z-«z. l:Last name: Lincoln 2:First name: Abe 3:Phone Number: (202)555-9933 4:Address: The White House 5:Address: Pennsylvania Ave. 6:Address: Washington, DC 7:Address: . 8: 9:Record 0 10:Abe Lincoln 134 Глава 5. Редактирование записей адресов с помощью функций...
11:The White House 12:Pennsylvania Ave. 13:Washington, DC 14: (202)555-9933 15: 16:Last name [Lincoln]: Washington 17:First name [Abe]: George 18:Phone Number [(202)555-9933]: . 19: 20:Record 0 21:George Washington 22:The White House 23:Pennsylvania Ave. 24 Washington, DC 25:(202)555-9933 26: 27:Last name [Washington]: Dole 28:First name [George]: Bob 29:Phone Number [(202)555-9933]: 30:Address [The White House]: !x 31: 32:Record 0 33:George Washington 34:The White House 35:Pennsylvania Ave. 36:Washington, DC 37:(202)555-9933 38: 39:Last name [Washington]: Bush 40:First name [George]: 41:Phone Number [(202)555-9933]: 202,555,0011 ext.l 42:Address [The White House]: !n 43: 44- .Record 0 45- .George Bush 46:The White House 47:Pennsylvania Ave. 48:Washington, DC 49:(202)555-0011 ext.l 50: 51:Last name [Bush]: Gore 52:First name [George]: Al 53:Phone Number [ (202)555-0011 ext.1] : 1/202.555-0002 54:Address [The White House]: !d 55:Address [Pennsylvania Ave.]: !d 56:Address [Washington,DC]: 57:Address: . 58: 59.-Record 0 60:Al Gore 61 Washington, DC 62:1 (202)555-0002 63: 64:Last name [Gore]: 65:First name [Al]: 66:Phone Number [1 (202)555-0002]: 67:Address [Washington, DC]: !i Новое требование к программе: форматированный номер... 135
68:Address: Vice President's Residence 69:Address [Washington, DC]: . 70: 71:Record 0 72:Al Gore 73:Vice President 's Residence 74 Washington, DC 75:1 (202)555-0002 76: 77:Last name [Gore]: done 78:First name [Al]: . 79: 80:Record 0 81:Al done 82:Vice President's Residence 83:Washington, DC 84:1 (202)555-0002 85: В строке 49 показан довольно сложный случай. 10-значный телефонный номер с расширением был введен пользователем совершенно нестандартно. Программа ав- томатически привела правую цифровую часть номера в соответствии со стандартом и оставила расширение в том виде, как его ввел пользователь. В строке 62 показан случай с вводом 11-значного номера с единицей в начале. И в этот раз программа ус- пешно справилась с поставленной задачей. Мы могли бы протестировать все другие форматы, но вы можете заняться этим сами в свободное время. Резюме Мы создали программу редактора для ввода и изменения строк записей адресной книги. За время работы над программой мы освоили использование потоков ввода- вывода и методы отслеживания ошибок ввода данных. Затем мы использовали раз- личные встроенные функции класса string для редактирования однострочных и многострочных полей, а также для форматирования поля номера телефона. Сле- дующая наша задача — предоставить пользователю дополнительные средства с по- мощью расширения функциональных возможностей класса AddressBook. В сле- дующей главе мы познакомимся с сортированными ассоциативными контейнерами и библиотекой алгоритмов. 136 Глава 5. Редактирование записей адресов с помощью функций...
Глава 6 Усовершенствование адресной книги с использованием алгоритмов и сортированных контейнеров В этой главе... • Требования к интерфейсу адресной книги 137 • Идиоматическое решение предоставления доступа к закрытому контейнеру 138 • Выявление дубликатов записей с помощью алгоритма count 141 • Поиск с помощью lower_bound и find_if 150 • Более эффективная сортировка объектов Address с помощью контейнера Set 154 • Двойное индексирование в контейнере с установкой соответствий 162 • Резюме 171 Требования к интерфейсу адресной книги Прежде чем приступить к созданию пользовательского интерфейса адресной книги, давайте вспомним, какие требования к программе мы определили на этапе планирования в главе 1. Сконцентрируем наше внимание на задачах, которые непосредственно относятся к работе программы адресной книги. Пользовательский интерфейс класса AddressBook должен выполнять следующее. 1. Позволять выбирать записи адресов из списка. Интерфейс адресной кни- ги должен показывать список всех записей, чтобы пользователь мог выбрать нужную для просмотра, редактирования или удаления. Следовательно, в класс AddressBook нужно добавить функцию предоставления доступа к отдельным записям в списке. Функция print предназначалась только для тестирования и отладки программы. С ее помощью нельзя было выбирать записи из списка или прокручивать длинный список записей. 2. Автоматически выявлять дубликаты записей. Пользовательский интер- фейс должен выводить предупреждение перед сохранением записи адреса, ес-
ли запись с такими же именем и фамилией уже есть в адресной книге. Наша текущая версия AddressBook совершенно не принимает во внимание подоб- ные события. 3. Осуществлять поиск записи по имени. Интерфейс должен содержать функцию, выполняющую поиск записи по введенному имени. 4. Осуществлять поиск записи по ключевым словам. Еще одна функция- член класса AddressBook должна выполнять поиск записей по ключевым сло- вам, которые могут находиться в любом поле. А сейчас вернемся на некоторое время к ядру класса AddressBook и немного мо- дернизируем его. Идиоматическое решение предоставления доступа к закрытому контейнеру Прежде всего позаботимся о том, чтобы пользователи адресной книги могли по- лучать доступ к записям, отображенным в виде списка. Объекты Address уже сохра- нены в нашем контейнере-списке в алфавитном порядке. Текущая задача состоит в том, чтобы открыть доступ к этим объектам. Недостатки использования обычных функций доступа Один из путей предоставления доступа к интерфейсу AddressBook (или любому другому клиенту этого класса) и записям адресной книги состоит в возвращении клиенту ссылки на список объектов Address. С этой целью можно было бы опреде- лить в классе AddressBook следующую функцию доступа: const list<Address>& addresses() const { return addresses_ }; Программа клиента сможет затем обращаться по ссылке к разным записям спи- ска и выполнять другие операции над коллекцией объектов, которые предусмотрены в контейнере list. Но такой подход нарушает принцип модульности объектно- ориентированных программ. Он состоит в том, что разные блоки программы долж- ны сохранять максимальную независимость друг от друга, что позволит легко заме- нять один блок на другой без внесения существенных изменений в остальные части программы. С этой целью интерфейс класса разрабатывается таким образом, чтобы он не был зависим от внутренней структуры самого класса. В таком случае замена в реализации класса AddressBook контейнера vector на контейнер list никак не отразится на пользовательском интерфейсе данного класса. Если в программе поль- зовательского интерфейса будет использоваться ссылка на объект addresses_ или его копия, то такой интерфейс сможет нормально работать только с производными от контейнера list. Если в будущем для реализации класса адресной книги вы захо- тите использовать контейнер другого типа, придется вносить серьезные изменения в интерфейс класса. 138 Глава 6. Усовершенствование адресной книги...
Использование интерфейса в стиле STL С классом AddressBook Альтернативный подход состоит в том, чтобы предоставить клиенту доступ не к списку, а непосредственно к объектам Address. Для этого в классе AddressBook нужно создать итераторы. В листинге 6.1 показана реализация класса AddressBook, измененное в соответствии с этой концепцией. Помимо итераторов, данный листинг содержит объявления дополнительных функций, с которыми мы познакомимся под- робнее чуть позже. Листинг 6.1. Модернизированная реализация класса AddressBook 1://TinyPIM (с)1999 Pablo Halpern. Файл AddressBook.h 2: 3:#ifndef AddressBook_dot__h 4:#define AddressBook_dot_h 5: 6:.#include <list> 7:#include "Address.h" 8: 9:class AddressBook 10: { 11: // Псевдоним типа list 12: typedef std::list<Address>addrlist; 13: 14:public: 15: AddressBook(); 16: -AddressBook(); 17: 18: // Классы исключений 19: class AddressNotFound {}; 20: class Duplicateld {}; 21: 22: int insertAddress(const Address& addr, int recordld = 0) 23: throw (Duplicateld); 24: void eraseAddress(int recordld)throw (AddressNotFound); 25: void replaceAddress(const Address& addr, int recordld = 0) 26: throw (AddressNotFound); 27: const Address& getAddress(int recordld) const 28: throw (AddressNotFound); 29: 30: // Возвращает число записей с указанным именем. 31: int countName(const std::strings lastname, 32: const std::strings firstname) const; 33: 34: // Итератор для навигации no записям адресной книги 35: typedef addrlist: :const__iterator const__iterator; 36: 37: // Функции навигации по записям адресной книги 38: const_iterator begin() const {return addresses^.begin();} 39: const__iterator end() const {return addresses_.end() ;} 40: 41: // Находит первый объект Address с именем большим или равным 42: // заданному. Обычно это имя начинается с указанной 43: // последовательности символов. Идиоматическое решение предоставления доступа... 139
44: const_iterator findNameStartsWith(const std::strings lastname, 45: const std::string& firstname=,,H) const; 46: 47: // Находит следующий объект Address, // в полях которого содержатся заданные 48: // ключевые слова. Параметр start задает точку начала поиска. 49: const_iterator findNextContains(const std::strings searchStr, 50: const iterator start) const; 51: 52: // Возвращает итератор по заданному ID записи. 53: const_iterator findRecordld(int recordld) const 54: throw (AddressNotFound); 55: 56:private: 57: // Запрещение копирования 58: AddressBook(const AddressBook&); 59: AddressBookS operator=(const AddressBook&); 60: 61: static int nextld_; 62: 63: addrlist addresses_; 64: 65: // Возвращает индекс записи по заданному ID. 66: // Возвращает NULL, если запись не обнаружена. 67: addrlist::iterator getBy!d(int recordld) 68: throw (AddressNotFound); 69: addrlist::const_iterator getBy!d(int recordld) const 70: throw (AddressNotFound); 71:}; 72: 73:#endif //AddressBook—dot h Итератор класса AddressBook определяется в строке 35 как псевдоним от list<Address>: : const—iterator. (Обратите внимание, что в строке 35 вместо list<Address> используется псевдоним addrlist, объявленный в строке 12.) Функция begin в строке 38 просто возвращает итератор на первую запись в адрес- ной книге. Аналогично, функция end в строке 39 возвращает итератор на последнюю запись в списке. Теперь с помощью пользовательского интерфейса адресной книги мы можем переходить с начала списка от одной записи к другой путем приращения итератора, возвращенного функцией begin (), до тех пор, пока итератор не станет равным значению, возвращенному функцией end (). Поскольку пока мы предпола- гаем открыть пользователям доступ к записям только для чтения, итератор объявля- ется с ключевым словом const. Для изменения записей клиент должен использовать функции-члены insertAddress, eraseAddress или replaceAddress. Вместо ограниченной функции print, которую мы использовали для тестирова- ния и отладки программы, разработаем более совершенную функцию printAddressBook, код которой показан в листинге 6.2. Листинг 6.2. Функция printAddressBook, добавленная в программу тестирования .X 1://TinyPIM (с)1999 Pablo Halpern. Файл AddressBookTest.срр 2: 3:#ifdef _MSC_VER 4:ttpragma warning(disable : 4786) 5:#endif 140 Глава 6. Усовершенствование адресной книги...
6: 7:#include <iostream> 8:#include "AddressBook.h" 9: 10:void printAddressBook(const AddressBook& book) 11: { 12: for (AddressBook::const_iterator i = book.begin(); 13: i != book.end(); ++i) 14: { 15: const Address& a = *i; 16: std::cout « "Record Id: " « a.recordld() « r\n’ 17: « a.firstname() « 1 ’ « a.lastname() « ’\n’ 18: « a.address() « ’\n1 « a.phone() « ’\n’ 19: « std::endl; 20: } 21: } 22://... Остальная часть файла AddressBookTest.cpp сохранилась прежней Новая функция во многом напоминает функцию print, которая была членом класса AddressBook. В строках 12, 13 определяется цикл, в котором объект адресной книги используется как ординарный контейнер. При создании собственного класса-контейнера старайтесь придерживаться общих принци- f Совет! пбв Работы стандартных контейнеров и их интерфейсов. Это облегчит работу с вашими |L Л программными продуктами другим программистам (если, конечно,-они знакомы со стилем интерфейсов STL). Поскольку программа клиента использует итератор AddressBook: : cons t_i ter a tor вместо list<Address>: : const_iterator, структура данных контейнера может быть свободно изменена (например, список можно заменить на вектор), что не по- влияет на выполнение программы клиента. Если выбранная структура данных кон- тейнера не поддерживает использование подобных итераторов, можно разработать собственный контейнер на основе стандартного. В любом случае код программы клиента не будет зависеть от реализации самого класса. Такое полное отделение классов данных от их интерфейса стало возможным благодаря тому, что итераторы всех типов имеют однотипный универсальный интерфейс. Программа клиента в лю- бом случае может инкрементировать, сравнивать и разыменовывать итераторы, не- зависимо от типа контейнера, к которому они относятся. Выявление дубликатов записей с помощью алгоритма count Займемся теперь функцией предупреждения появления дубликатов записей в адресной книге. Смысл этой функции состоит в том, чтобы вовремя показать поль- зователю предупреждение о том, что в адресной книге уже есть запись с тем же име- нем и фамилией. В строке 31 листинга 6.1 определяется функция countName, с по- мощью которой можно выполнить поставленную задачу. Функция countName воз- вращает число записей в адресной книге, которые имеют указанные имя и фамилию. Если число соответствующих записей равно нулю, можно сохранить новую запись. В противном случае пользовательский интерфейс должен показать предупреждение. Выявление дубликатов записей с помощью алгоритма count 141
Следует заметить, что подсчет объектов в коллекции, соответствующих установ- ленным критериям, — обычная задача, для облегчения выполнения которой можно использовать средства, представленные в стандартной библиотеке C++. Поиск точных совпадений В листинге 6.3 показана реализация функции countName с использованием алго- ритма count из стандартной библиотеки. В представленном варианте функция будет работать не совсем так, как нам нужно, но эти недостатки мы легко устраним позже. / Листинг 6.3. Реализация функции countName с использованием алгоритма count ' l:#include <algorithm> 2: 3:// Возвращает число записей с указанным именем. 4:int AddressBook::countName(const std::strings lastname, 5: const std::strings firstname) const 6: { 7: Address srchAddr; 8: srchAddr.lastname(lastname); 9: srchAddr.firstname(firstname); 10: 11: // Возвращает совпадающие адреса 12: return std::count(addresses_.begin(), addresses__.end(), srchAddr); 13: } Строку 12 можно прочитать следующим образом: подсчитать число значений в диапазоне от begin () до end (), совпадающих с переменной srchAddr. Теперь под- робно рассмотрим концепции, положенные в основу этого решения. Что такое алгоритм В соответствии с толковым словарем Уэбстера (Webster's New Universal Unabridged Dictionary, Dorset & Baber, 1983), алгоритмом называется ‘‘...любой специальный метод решения определенного рода проблем”. Большинство алгоритмов призвано решать не какие-нибудь отдельные проблемы, а целые классы проблем. Например, можно разработать общий алгоритм сортировки объектов коллекции по их размеру, независимо от того, будут ли сортироваться яблоки, монеты или целочисленные значения. В терминологии стандартной библиотеки C++ под алгоритмом понимают шаблон функции, который можно использовать с различными типами данных и контейнеров. Алгоритм count, используемый в листинге 6.3, подсчитывает число элементов в контейнере, которые соответствуют установленному значению. Типы контейнера и установленного значения могут быть любыми, посколы^г реализация шаблона функции, заданная в строке 12, происходит во время компиляции программы. (Подробно о реализации шаблонов см. во врезке “Справочная информация: общие представления о шаблонах” в главе 3.) Таким образом, компилятор каждый раз соз- дает частный экземпляр функции count, соответствующий конкретным типам дан- ных и контейнера в нашей программе. Использование алгоритма count значительно упрощает код программы по сравнению с ситуацией использования для этих целей явного цикла. 142 Глава 6. Усовершенствование адресной книги...
На Общий принцип. Использование алгоритмов стандартной библиотеки Заметку позволяет существенно сократить количество циклов в программе. В стандартной библиотеке C++ представлено порядка 60 алгоритмов, определе- ния которых содержатся в файле заголовка <algorithm> (см. строку 1 листинга 6.3). Общие принципы использования алгоритмов, с которыми мы сейчас познакомимся на примерах, справедливы для всех других алгоритмов. Что такое диапазон итераторов Большинство алгоритмов стандартной библиотеки не работает с контейнера- ми непосредственно. Обычно в алгоритмах используется выборка элементов контейнера, заданная диапазоном итераторов. Алгоритмы, работающие с диа- пазонами итераторов, принимают два параметра, определяющих каждый диа- пазон. Аргументами выступают итераторы, один из которых указывает на пер- вый элемент диапазона, а второй— на последний. Так, в строке 12 листинга 6.3 в аргумент count передается диапазон элементов, заданный итераторами addresses_. begin () иaddresses_.end() . В данном случае диапазон итерато- ров включает все элементы контейнера addresses . Н а Общий принцип. Чтобы отличить функции и алгоритмы, принимающие Заметку диапазоны итераторов, общепринято для передаваемых с аргументами итераторов использовать имена start и finish, или begin и end. Естест- венно, что итераторы, задающие диапазон, всегда относятся к одному типу. Часто для обозначения диапазона итераторов используется математический син- таксис записи открытого справа интервала. Открытый справа интервал [X,Y] озна- чает ряд элементов от X до Y, причем X входит в интервал, a Y находится за его пре- делами. Квадратная скобка означает включение крайнего элемента в интервал, а круглая скобка означает исключение элемента из интервала. На рис. 6.1 закра- шенными сегментами показаны элементы диапазона, заданного итераторами [start,finish). container Рис. 6.1. Диапазон итераторов [start, finish) Термин Открытым справа интервалом называется ряд элементов [begin, end), причем begin входит в интервал, a end исключается из него. Например, за- пись [0,100) означает ряд положительных чисел меньше ста. Совпадение значений Обратимся опять к строке 12 листинга 6.3. Понемногу начинает проясняться, что тут происходит. Алгоритм count объявлен приблизительно следующим образом: Выявление дубликатов записей с помощью алгоритма count 143
template Cclass Iterator, class T> long count(Iterator start, Iterator finish, const T& value); Для заданного типа Iterator и T алгоритм возвращает число элементов в диапазоне [start, finish), которые совпадают со значением value. Тип Т дол- жен быть совместим с типом Iterator (т.е. выражение * start == value долж- но быть действительным). В нашем случае при реализации алгоритма count для Iterator задается тип list<Address>: : const_iterator, а для Т— тип Address. Чтобы вести поиск по полям фамилии и имени, объект Address должен содержать соответствующие переменные-члены. В строках 7-9 мы создаем объ- ект Address и заполняем эти поля, после чего в строке 12 объект передается в алгоритм count. На Особенности компиляции. Некоторые версии стандартных библио- заметку тек, особенно библиотека, входящая в состав компилятора SunPro 5.0, содержат старую версию алгоритма count, который не возвращает ни- каких значений, а вместо этого помещает результат в третий параметр, переданный как ссылка. Чтобы избежать проблем с компиляцией, нуж- но переопределить count следующим образом: tfifdef ____________SUNPRO_CC // Проблема с библиотекой SunPro: // старая версия алгоритма count(). // Необходимо перегрузить определение функции, namespace std { template Cclass Fwdlt start, Fwdlt end, const T& t) { unsigned ret = 0; count(start, end, t, ret); return ret; } } #endif // __SUNPRO_CC Аналогичная перегрузка потребуется также для алгоритмов count_if, distance и некоторых других. Если мы сохраним код таким, как он показан в листинге 6.3, то программа часто будет возвращать нуль даже в том случае, если искомое имя присутствует в адресной книге. Проблема состоит в том, что если два объекта Address имеют одинаковые значения в полях имени и фамилии, это еще не означает, что они совпадают. Опера- тор равенства проверяет совпадение объектов по всем полям и возвращает false, если они отличаются, например, номерами телефонов или адресами. Таким образом, оператор сравнения, перегруженный для объектов Address, и алгоритм count не вполне совместимы друг с другом. Воспользуйтесь алгоритмом count_if и объектами функций Как же сделать так, чтобы алгоритм count сравнивал только некоторые поля объ- ектов Address? Один из подходов состоит в том, чтобы использовать другую функ- цию сравнения объектов, отличную от operatot==. 144 Глава 6. Усовершенствование адресной книги...
Общие представления об объектах функций Объект функции определяет оператор вызова функции operator (). Например, в стандартной библиотеке определен класс объекта функции equal<T>, который вызыва- ет функцию сравнения для заданных аргументов. При этом выполнение операции срав- нения выглядит как вызов функции. Для реализации объекта функции нужно указать тип сравниваемых значений, например equal<int>. Так, фрагмент программы std::equal<int> intequals; int x, у; if (intequals(x, y)) // продолжение кода будет в точности соответствовать данному фрагменту: int х, у; if (х == у) // продолжение кода Польза от использования equal<int> состоит в том, что с помощью объектов функ- ций можно настраивать выполнение алгоритмов. Например, алгоритм, реализованный cequal<int>, будет выполняться иначе, чем реализация того же алгоритма cnot_equal<int>. Осталось разобраться, с какими алгоритмами можно использовать данную концепцию и чем это поможет нам. В стандартной библиотеке есть алгоритм count_if, который является вариацией алгоритма count. Этот алгоритм позволяет на- страивать выполнение операции сравнения путем передачи в него пользовательского объекта функции. В следующем разделе мы увидим, как это выполняется. Создание пользовательского объекта функции В листинге 6.4 показан новый вариант реализации функции countName с исполь- зованием созданного нами класса объекта функции и алгоритма count if. (Нумерация строк начинается с файла AddressBook. срр.) Листинг 6.4. Реализация функции countName с использованием объекта функции и алгоритма count_if 1://TinyPIM (с)1999 Pablo Halpern. Файл AddressBook.срр 2: 3:#ifndef _MSC_VER 4:ftpragma warning(disable : 4786) 5:#endif 6: 7:#include <algorithm> 8:#include <functional> ...// Данные строки кода остались такими же, как в листинге 4.10. 98:// Объект функции для сравнения поля имени и // фамилии двух объектов Address. 99:struct AddressNameEqual : 100: public std::binary._function<Address, Address, bool> 101: { 102: bool operator()(const Address& al, const Address& a2) const 103: { 104: // Возвращает true, если имя и фамилия совпадают 105: return (al.lastname() == a2.lastname() && Выявление дубликатов записей с помощью алгоритма count 145
106: al .f irstname () — a2 . f irstname ()) ; 107: } 108:}; 109: 110:// Возвращает число записей с указанным именем. Ill:int AddressBook::countName(const std::string& lastname, 112: const std::string& firstname) const 113: { 114: Address searchAddr; 115: searchAddr.lastname(lastname); 116: searchAddr.firstname(firstname); 117: 118: // Возвращает число совпадающих адресов 119: return std: : count__if (addresses__.begin () , addresses_.end() , 120: std::bind2nd(AddressNameEqual(), searchAddr)); 121: } 122: В строке 99 определяется класс AddressNameEqual, который представляет собой класс объекта бинарной функции. Бинарными называются функции, принимающие два аргумента, в отличие от одинарных, принимающих один аргумент. Мы произво- дим наш класс от стандартного шаблона binary_function, установив типы аргу- ментов и возврата функции. В шаблоне binary_function (и в аналогичном шабло- не одинарных функций unary__function) даются определения типов, которые потре- буются нам при использовании объекта функции в отрицателях и подшивках, речь о которых пойдет в следующем разделе. гюнимать как сокращение от определения класса ^ье/ста Функции. В действительности определение класса и создание объекта этого клас- ^ЩниманиеЛ; са совершенно разных процесса, хотя подобные сокращения допускались даже в этой книге. Начинающие программисты часто путают эти понятия. Поэтому будьте внима- тельны й старайтесь вникнуть в суть процессов, стоящих за терминами. В строках 102-107 определяется оператор вызова функции operator(), который срабатывает при использовании объекта AddressNameEqual в качестве функции. Этот оператор выполняет сравнение полей имени и фамилии двух объектов Address и воз- вращает true, если значения совпадают. Класс AddressNameEqual, как и большинство других классов объектов функций, не содержит переменных-членов, а только одну от- крытую функцию-член. Компилятор автоматически создает д ля класса объекта функции невыполняемые конструктор, деструктор и оператор присваивания. Чтобы использовать класс AddressNameEqual как функцию, нужно создать объ- ект этого типа, как показано в следующем фрагменте кода: Address NameEqual aneq; // Создание объекта функции Address а, Ь; if (aneq(а, Ь)) // Сравнение полей имени и фамилии в объектах а и Ь // Продолжение кода Поскольку aneq является пустым объектом (не содержит никаких переменных- членов) и конструктор класса AddressNameEqual не выполняет никаких функций, мы можем заменить выражение создания объекта функции простым вызовом: AddressNameEqual (). В этом случае объект функции создается неявно. После этой замены наш фрагмент кода примет следующий вид: Address а, Ь; if (AddressNameEqual()(а, Ь)) // Сравнение полей имени и фамилии // в объектах а и Ь // Продолжение кода 146 Глава 6. Усовершенствование адресной книги...
Дополнительная пара скобок после AddressNameEqual выглядит странно. Дело в том, что в этой компактной записи заключено выполнение двух последовательных операций. Сначала с помощью конструктора по умолчанию (первая пара скобок без аргументов) создается временный объект класса AddressNameEqual. Затем этот объ- ект используется как функция, и в него передаются два аргумента а и Ь во второй па- ре скобок. Эта идиоматическая конструкция встречается относительно не часто, но если она вам повстречается, не впадайте в панику. Первая пара скобок без аргумен- тов всегда служит для вызова конструктора по умолчанию, создающего временный объект указанного класса. Справочная информация: объекты функций Экскурс вместо^ указателей^ на функции_____________________________ Объекты функций работают примерно так же, как и указатели на функции. В обоих случаях с их помощью решается проблема передачи в функцию или алгоритм небольшой части кода программы клиента. Почему же тогда мы обратили внимание на объекты функций, а не на более традиционные указатели на функции? Дело в том, что объекты функций открывают больше возможностей. Объект функции может содержать дополнительные данные в своих пе- ременных-членах. С их помощью можно задавать дополнительные па- раметры при выполнении функции, влияющие на конечный результат. Кроме того, только объекты функций можно использовать в подшивках и отрицателях, речь о которых пойдет в следующем разделе. Объекты функции связываются во время компиляции программы, что позволяет встраивать их код в код программы (inline-функции). Функции, задан- ные указателями, связываются во время выполнения программы, по- этому их код почти невозможно встроить в программу. Последнее свойство — это палка о двух концах. Иногда бывает необходимо выбирать функцию во время выполнения программы. С этой целью в стан- дартную библиотеку были добавлены следующие адаптивные функции, ко- торые могут преобразовать указатели на функции в объекты функций. ♦ Функция ptr fun преобразовывает статический или гло- бальный указатель на функцию в объект функции. Если функция принимает один аргумент, возвращается одинар- ный объект функции, если два аргумента — бинарный объ- ект функции. ♦ Функция mem fun преобразовывает указатель на функцию- член класса в объект функции. Если функция не принимает аргументов, то возвращается одинарный объект функции, ар- гументом которого является указатель на объект класса. Со- ответственно, если функция принимает один аргумент, то возвращается бинарный объект класса, первый аргумент ко- торого является указателем на объект класса, а второй — ар- гументом исходной функции-члена. ♦ Функция mem_f un_ref работает как mem_f un, за исключением того, что первый аргумент возвращенного объекта функции является не указателем, а ссылкой на объект класса. Выявление дубликатов записей с помощью алгоритма count 147
Например, следующий код находит первый элемент контейнера v, для которого функция isValid возвращает true: class myclass { public: bool isValid(); }; std::vector<myclass> v; std::vector<myclass>::iterator vi - std::find_if(v.begin(), v.end(), std::mem_fun_ref(&myclass::isvalid)); Примите к сведению, что в ранних версиях стандартной библиотеки вместо перегруженной функции mem fun существовали функции mem funl и mem_fun2, возвращавшие соответственно одинарный и бинарный объекты функций. Функции mem_fun и mem fun ref стан- дартной библиотеки Microsoft 6.0 реализованы с ошибкой, в результате чего они не работают с константными функциями-членами. Использование подшивок и отрицателей Теперь, после того как у нас появился метод сравнения объектов Address только по полям имени и фамилии, нам нужно доработать код для использования объекта функции в алгоритме count_if. Определение алгоритма count if выглядит при- мерно так же, как и определение алгоритма count: template <class Iterator, class Predicate> long count_if(Iterator start. Iterator finish, Predicate pred); Третий аргумент алгоритма count if — объект функции одинарного предиката. Он принимает один аргумент и возвращает true, если данный объект соответствует установленному условию, и false— в противном случае. Функция count if пере- дает последовательно все объекты в диапазоне [start, finish) в объект функции pred и подсчитывает, сколько раз pred возвращает true. т Предикатом называется функция, возвращающая логическое значение I ермИп (да-нет). Предикаты часто используются для тестирования выполнения или невыполнения событий. Одинарным предикатом называется функция или объект функции, ко- 1 ер МИН ТОрЫе принимают один аргумент. Примером такого предиката может быть функция isupper, которая возвращает true, если в качестве ар- гумента в нее передан алфавитный символ в верхнем регистре. т Бинарным предикатом называется функция или объект функции, ко- I ер МИН ТОрЫе принимают два аргумента. Примером такого предиката может быть функция less<int>, которая возвращает true, если два передан- ных в нее целочисленных аргумента оказываются равными. Похоже, что у нас возникли проблемы. Алгоритм count if ожидает одинар- ный предикат, тогда как объект функции AddressNameEqual является бинарным предикатом. Чтобы лучше разобраться в происходящем, зададим себе вопрос: “Каким условиям должен отвечать объект Address, чтобы он был учтен алгорит- мом?” Очевидно, что алгоритм должен учесть объект Address, если его поля имени и фамилии соответствуют значениям в searchAddr. Поскольку нам нуж- но сравнить все элементы диапазона с одними и теми же ключевыми значения- 148 Глава 6. Усовершенствование адресной книги...
ми, переменная searchAddr остается константной на протяжении выполнения алгоритма count_if. Таким образом, в объекте функции AddressNameEqual только один аргумент реально будет изменяться, тогда как другой остается при- крепленной константой. Такие ситуации возникают чаще, чем вы могли ожидать. Очень часто в про- граммах приходится искать ответ на такие вопросы, как, например: какие из пе- речисленных значений больше 5? Стандартная библиотека содержит специаль- ную функцию для решения этих задач, которая называется подшивкой (binder). Функция подшивки принимает в качестве аргумента бинарный объект функции Fun с и возвращает одинарный объект функции. Функция bind Ind подшивает первый аргумент исходного объекта функции Fun с как константу, а функция bind2nd делает то же самое со вторым аргументом. Чтобы посмотреть на приме- ре, как работает функция подшивки, взгляните на строку 120 листинга 6.4. Пер- вым аргументом функции bind2nd выступает временный объект класса AddressNameEqual. На создание временного объекта указывает пара круглых скобок после имени класса. Вторым аргументом выступает переменная searchAddr, которая подшивается к объекту AddressNameEqual как константа. Функция bind2nd возвращает одинарный предикат, который затем передается как третий аргумент алгоритма count if. Синтаксис использования подшивок довольно специфичен. Подшивка служит для изменения одного из своих аргументов. Начинающие программисты часто пу- таются в том, как использовать bind2nd, и пытаются передать подшивку как аргу- мент в объект функции, вместо того чтобы делать наоборот. Запомните, что подшив- ка — это функция, которая принимает объект функции и преобразовывает его в дру- гой объект функции. Другой тип функций, работающих аналогично подшивкам, — отрииртели. Как и подшивка, отрицатель— это функция, которая принимает в качестве аргумента один объект функции и возвращает другой. Так, функция notl ожидает одинарный предикат и возвращает другой одинарный предикат. Функция not2 делает то же са- мое с бинарными предикатами. Предикаты, возвращаемые функциями notl и not2, работают как и исходные предикаты, с той лишь разницей, что значению true ис- ходного предиката будет соответствовать значение false возвращенного предиката. Например, функция not2 (equal<int> () ) возвратит бинарный предикат, который, в свою очередь, возвращает true в случае неравенства переданных аргументов. Дру- гими словами, функция-отрицатель преобразовывает equal<int> в not_equal<int>. В нашем случае нет такой необходимости, но на будущее вам следует знать, что с помощью функции-отрицателя можно преобразовать нашу функцию countName таким образом, чтобы она подсчитывала объекты, поля имени и фамилии в которых не соответствуют установленным значениям. Для этого строки 119, 120 нужно пере- делать следующим образом: 119: return std::count_if(addresses_.begin(), addresses_.end(), 120: std::notl(std::bind2nd(AddressNameEqual(), searchAddr))); Интересно, что это выражение можно заменить другим, совершенно экви- валентным: 119: return std: : count_i’f (addresses_. begin () , addresses^, end () , 120: std: :bind2nd (std: :notl (AddressNameEqual () , searchAddr))); Прежде чем вы с энтузиазмом приступите к реализации сложных алгебраических решений с помощью подшивок и отрицателей, хочу предупредить, что это далеко не универсальные средства программирования. Выявление дубликатов записей с помощью алгоритма count 149
Будьте внимательны: классы trai ts Экскурс В объявлении аргумента count в действительности не указано, что функция должна возвращать значения типа long. Возвращается зна- чение типа iterator_traits<Iterator>::difference_type, которое в большинстве случаев соответствует типу long. Класс iterator traits входит в число большой группы классов traits, шаблоны которых широко представлены в стандартной библиотеке. Эти шаблоны отличаются тем, что способны настраивать свойства своих параметров по контексту. Классы traits — это новейшие и наибо- лее сложные средства программирования современной стандартной библиотеки, но их изучение, к сожалению, выходит за рамки этой кни- ги. В их основу положен принцип частичной специализации, который пока еще не поддерживается многими компиляторами. Если компиля- тор не поддерживает частичную специализацию, то в большинстве случаев использование классов traits можно заменить конструкцией выражений с явными объявлениями. Впрочем, иногда не удается ре- шить все проблемы с помощью таких прозрачных конструкций и при- ходится прибегать к редактированию кода стандартных библиотечных средств. Пример выхода из ситуации, когда компилятор SunPro под- держивал лишь устаревшую версию алгоритма count, был представлен выше, во врезке “Особенности компиляции”. Поиск с помощью lower_bound И f ind_if Очень часто в коллекциях объектов требуется выполнить поиск элементов, отве- чающих различным наборам критериев. Например, мы планировали, что наша ад- ресная книга должна позволять пользователям отыскивать записи по фамилиям и ключевым словам, находящимся в любом поле записи. Для выполнения постав- ленной задачи нам придется объединить возможности разных средств поиска стан- дартной библиотеки C++. Поиск по имени Следующее свойство нашей программы, к реализации которого мы подо- шли, — это поиск записей по имени. Причем хорошо бы сделать так, чтобы поль- зователь мог вводить не только все имя целиком, но и часть его. После выполне- ния поиска программа TinyPIM должна прокрутить список записей, чтобы в пер- вой записи вверху экрана в полях имени и фамилии содержалось значение, которое равно или больше введенного пользователем. В основу системы поиска будет положена функция f indNameStartsWith (см. листинг 6.1), которая обна- руживает первую запись со значением, равным или большим установленного. Например, если пользователь ввел Саг, а в списке с Саг начинается только Carter, то это и будет первая обнаруженная запись. 150 Глава 6. Усовершенствование адресной книги...
То, что для поиска записи не требуется полное совпадение значений, — это очень важное и интересное свойство функции findNameStartsWith. Особо важное значение имеет также тот факт, что наши записи в списке отсортированы. Благодаря этому свой- ству списка мы можем вести дихотомический поиск. Этот подход позволяет существенно сократить время выполнения поиска. Так, в списке, содержащем порядка 1000 записей, для обнаружения нужной записи часто достаточно выполнить десяток сравнений. т Дихотомическим поиском называется особый подход к поиску записей I ер МИН в СОрТИрОванных последовательностях. Поиск начинается с середины списка. Если искомое значение меньше значения объекта в середине списка, то программа обращается к объекту в середине нижней полови- ны списка. Таким образом, на каждой итерации оставшийся список де- лится пополам до тех пор, пока не будет обнаружен искомый элемент. Для обнаружения объекта в списке из N элементов требуется log2(N) ите- раций (для миллиона записей — 20 итераций). т Линейным поиском называется самый простой подход, при котором I ерМИН ЭЛементы контейнера последовательно сравниваются с установленным значением до тех пор, пока искомый элемент не будет найден. Для по- иска записи среди N элементов может потребоваться N сравнений (в среднем N/2). Линейный поиск используется для небольших несорти- рованных последовательностей или в том случае, когда программист слишком ленив, чтобы написать программу дихотомического поиска. Преимущества дихотомического поиска понятны и очевидны для каждого. Но если предложить программисту написать программу такого поиска, то в боль- шинстве случаев он потратит уйму времени либо на поиск аналогов в литературе или в Internet, либо на написание и отладку программы. Окончательный код окажется на удивление простым, но найти оптимальное решение не так просто. К счастью, стандартная библиотека C++ избавит вас от необходимости изобре- тать вновь и вновь алгоритм дихотомического поиска. В листинге 6.5 показана реализация функции findNameStartsWith с использованием алгоритма дихо- томического поиска lower bound. Листинг 6.5. Реализация функций findNameStartsWith с использованием % алгоритма iower_bound 123:// Находит первый объект Address, имя которого больше или равно // заданному. 124:// Обычно это имя начинается с последовательности символов, 125:// введенных пользователем. 126:AddressBook::const_iterator 127:AddressBook::findNameStartsWith(const std::strings lastname, 128: const std::strings firstname) const 129: { 130: Address searchAddr; 131: searchAddr.lastname(lastname); 132: searchAddr.firstname(firstname) ; 133: 134: return std::lower_bound(addresses_.begin(), addresses_.end(), 135: searchAddr); 136: } 137: Поиск с помощью lower_bound и f ind_if 151
Функция lower_bound принимает три аргумента. Первые два задают диапазон ите- раторов, в пределах которого будет выполняться поиск. В третьем аргументе хранится критерий поиска. Функция lower_bound возвращает итератор на первый объект в спи- ске, значение которого равно или больше искомого. Если искомое значение больше зна- чений всех объектов диапазона, то возвращается концевой итератор диапазона. Данный алгоритм работает только с диапазонами сортированных элементов. Возвращенный итератор указывает на позицию в списке, куда можно было бы вставить объект с указан- ным значением, не нарушая порядок следования элементов списка. В строках 130-132 создается образец искомого объекта с заданными именем и фамилией (так же, как мы делали это при реализации функции countName). В строках 134, 135 вызывается алгоритм lower_bound и в него передается диапазон итераторов, охватывающий весь контейнер addresses , а также созданный образец объекта в качестве критерия поиска. Если в списке будет обнаружен объект со зна- чением, в точности соответствующим искомым, то функцией будет возвращен ите- ратор на этот объект. Если точного совпадения не будет обнаружено, то возвращает- ся итератор на объект, который следовал бы за искомым в сортированном списке. Благодаря использованию алгоритма lower_bound код функции f indNameStartsWith предельно прост, и нам не пришлось ломать голову над выпол- нением и отладкой программы дихотомического поиска. Общий принцип. Использование алгоритмов стандартной библиотеки заметку повысит ошибкоустойчивость вашей программы, поскольку они пре- доставляют стандартные и отлаженные блоки кода, самостоятельное написание которых может оказаться достаточно сложной задачей даже для опытного программиста. Кроме того, пользовательские решения редко соизмеримы по эффективности выполнения со стандартными алгоритмами. Например, использование линейного поиска записей вместо дихотомического — это типичный пример, когда программист делает свою программу малоэффективной, пытаясь решить рутинную задачу собственными силами. Еще одно замечание по поводу эффективности алгоритма lower_bound. Он рабо- тает эффективнее с контейнерами, предоставляющими произвольный доступ к эле- ментам (вектор или двухсторонняя очередь). При использовании в списках необхо- димость последовательного перехода от записи к записи начиная с начала списка существенно снижает эффективность поиска. Мы попытаемся устранить эту неэф- фективность выполнения далее в этой главе. Поиск строк Последнее требование к адресной книге, которое нам осталось решить, — это поиск записей по ключевым словам. Поскольку искомая строка может присутствовать в любом поле и не является критерием сортировки записей, дихотомический поиск в данном слу- чае нам не поможет. Эту задачу можно решить только простым линейным поиском. Интерфейсом класса AddressBook для выполнения этой задачи служит функция- член findNextContains (см. листинг 6.1). В функцию передаются искомая строка и точка начала поиска, а возвращается итератор на следующую запись, содержащую ключевые слова, или end (), если искомая строка не обнаружена. Чтобы продолжить поиск следующей записи, возвращенный итератор инкрементируется и передается вновь в функцию f indNextContains в качестве точки начала поиска. 152 Глава 6. Усовершенствование адресной книги...
Реализация функции f indNextContains с использованием алгоритма find_if показано в листинге 6.6. Листинг 6.6. Реализация функции f indNextContains с использованием 138:// Класс объекта функции для поиска строки в полях объекта Address. 139:class AddressContainsStr : public std::unary_function<Address, bool> 140: { 141:public: 142: AddressContainsStr(const std::strings str) : strjstr) {} 143: 144: bool operator()(const AddressS a) 145: { 146: using std::string; 147: 148: // Возвращает true, если поле объекта Address содержит str_ 149: return (a.lastname().find(str_) != string::npos || 150: a.firstname().find(str_) != string::npos || 151: a.phone().find(str_) != string::npos II 152: a.address().find(str_) != string::npos); 153: } 154: 155:private: 156: std::string str_; 157: }; 158: 159:// Поиск следующего объекта Address, в полях которого содержится // указанная 160:// строка. Точка начала поиска задается параметром start. 161:AddressBook::const_iterator 162:AddressBook::findNextContains(const std::strings searchStr, 163: const_iterator start) const 164: { 165: return std::find_if(start, addresses_.end(), 166: AddressContainsStr(searchStr)); 167: } 168: Начиная co строки 139 мы вновь определяем объект функции. Но в этот раз опре- деление несколько отличается. Класс AddressContainsStr представляет одинарный объект функции с конструктором (строка 142), который сохраняет в объекте значе- ние искомой строки. Это как раз одна из дополнительных возможностей объектов функций, которая отличает их от указателей на функции. С помощью объектов функций может передаваться не только код, но и данные в его переменных-членах. Кроме того, в ходе реализации шаблона код объекта функции можно встроить (inline) в код программы. В определении оператора вызова функции мы вводим класс string в область ло- кальной видимости (строка 146),- чтобы не использовать каждый раз префикс std: :. В строках 149-152 объект функции просматривает все поля выбранного объекта Address в поисках искомой строки. Для этого используется функция-член find класса string, которая возвращает string: :npos, если искомая строка не обнару- жена. Функция find может возвращать два значения— либо true, либо Поиск с помощью lower_bound и f ind_if 153
string: :npos. Таким образом, наш объект функции характеризуется следующими свойствами: функция запускается для объекта Address, который передается в нее как аргумент, и возвращает true, если в любом поле этого объекта обнаружена ис- комая строка. Искомая строка для экземпляра объекта функции является констан- той и задается во время создания объекта. В строках 165, 166 наш объект функции используется с алгоритмом f ind if. Этот алгоритм принимает три аргумента. Первые два задают диапазон итерато- ров, в котором будет производиться поиск. В качестве третьего аргумента вы- ступает одинарный предикат. Алгоритм find_if возвращает итератор на пер- вый элемент в диапазоне, для которого предикат возвратит true. Следует обра- тить внимание на диапазон итераторов алгоритма find if. В первый раз мы передаем диапазон, который не включает весь контейнер. Поиск осуществляется в диапазоне от точки start до конца контейнера addresses . Алгоритм возвра- тит итератор на первый обнаруженный объект Address, для которого функция AddressContainsStr возвратит true. Обратите внимание, что после AddressContainsStr для вызова конструктора используется не пустая пара ско- бок, как в случае объекта функции AddressNameEqual, а передается аргумент searchStr. Важно не путать создание объекта функции с вызовом объекта функ- ции. То, что мы видим в строке 166,— это создание объекта. За вызов AddressContainsStr ответствен алгоритм f ind_if. Поиск записи по идентификационному номеру В класс AddressBook была добавлена еще одна функция, f indRecordld, которая работает почти так же, как getAddress, только она возвращает итератор на объект, а не сам объект. Мы будем использовать ее для возвращения точки начала поиска строк. Реализация функции f indRecordld показано в листинге 6.7. Листинг 6.7. Реализация функции f indRecordld • П .'ЧЯГЬ. ...Л..- ’ - .,1 J.* Л - . :. . . УМЯВ ЛЛ*7Л..’ 169:// Возвращает итератор на объект, заданный по ID. 170:AddressBook::const_iterator 171:AddressBook::findRecordld(int recordld) const throw (AddressNotFound) 172: { 173: return getByld(recordld); 174: } Как вы видите, поиск объекта переадресовывается функции getByld. Поскольку getByld выполняет линейный поиск, функция f indRecordld характеризуется та- кой же эффективностью. Эффективная сортировка объектов Address с помощью контейнера Set Давайте окинем взглядом все наши текущие достижения в деле реализации клас- са AddressBook. Всю необходимую функциональность нам удалось достичь с помо- щью средств шаблона класса-контейнера list. Мы отдали предпочтение использо- 154 Глава 6. Усовершенствование адресной книги...
ванию списков вместо контейнеров vector, поскольку они позволяют эффективно добавлять объекты в середину списков, благодаря чему поддерживается определен- ный порядок следования записей. В тоже время наша работа с адресной книгой не ограничилась только добавлением новых записей. А такие процедуры, как поиск за- писей по имени, ключевым словам и идентификационным номерам, выполняются неэффективно из-за линейного характера списков. Кроме того, многие операции в своей работе полагаются на функцию getByld, которая основана на малоэффек- тивном линейном поиске записей. Видимо, подошло время вернуться к стандартной библиотеке в поисках контей- нера, наиболее подходящего по характеристикам и структуре данных классу AddressBook. Возможно, для небольшой адресной книги указанные проблемы не существенны. Но, опять-таки, вспомним, что наша основная задача состоит в обуче- нии возможностям стандартной библиотеки. Поэтому представим, что нам нужно найти оптимальный контейнер для работы с большой базой данных, по структуре и требованиям напоминающей нашу адресную книгу. Хранение объектов Address в контейнере multiset Основная характеристика данных, сохраняемых в AddressBook, состоит в их ес- тественной упорядоченности по именам в алфавитном порядке. Поддержание следо- вания записей в алфавитном порядке позволяет быстро отыскивать необходимые элементы в коллекции, например с помощью дихотомического поиска. Но проблема с классом list состоит в линейном доступе к элементам списка. Поиск объекта в списке напоминает листание с самого начала телефонного справочника в поисках требуемого номера. Наиболее подходящим решением для адресной книги было бы использование стандартного библиотечного шаблона сортированного ассоциативного контей- нера. Контейнеры этого типа автоматически выполняют сортировку записей и оптимизированы для добавления, удаления и поиска элементов по ключевым Ассоциативные контейнеры упорядочивают свои элементы по их зна- чениям, а не по очередности их добавления. (Сравните с определением последовательных контейнеров.) Последовательные контейнеры упорядочивают свои элементы в линей- ную последовательность в соответствии с очередностью и позициями добавления объектов. (См. главу 3.) Простейшими сортированными ассоциативными контейнерами являются set (набор) и multiset (множественный набор). Отличие между ними состоит в том, что в наборе ключевые значения объектов должны быть уникальными, тогда как множе- ственный набор допускает дублирование записей. Поскольку в AddressBook допус- тимо дублирование записей, попробуем в определении класса заменить контейнер list на multiset, как показано в листинге 6.8. значениям. Термин Термин Эффективная сортировка объектов Address ... 155
Листинг 6.8. Новое определение класса AddressBook с использованием multiset вместо list 1://TinyPIM (с)1999 Pablo Halpern. Файл AddressBook. h 2: 3:#ifndef AddressBook—dot_h 4:#define AddressBook_dot_h 5: 6:#include <set> 7:#include ’’Address, h" 8: 9:class AddressBook 10: { 11: // Сокращения имен элементов структуры данных 12: typedef std: :multiset<Address> addrByName_t; 13: 14:public: 15: AddressBook(); 16: ^AddressBook(); 17: 18: // Классы исключений 19: class AddressNotFound {}; 20: class Duplicateld {}; 21: 22: int insertAddress(const AddressS addr, int recordld = 0) 23: throw (Duplicateld); 24: void eraseAddress(int recordld) throw (AddressNotFound); 25: void replaceAddress(const AddressS addr, int recordld = 0) 26: throw (AddressNotFound); 27: const Address& getAddress(int recordld) const 28: throw (AddressNotFound); 29: 30: // Возвращает число записей с указанными именем и фамилией. 31: int countName(const std::strings lastname, 32: const std::strings firstname) const; 33: 34: // Итератор на точку начала поиска 35: typedef addrByName_t::const_iterator const_iterator; 36: 37: // Функции выбора всех записей контейнера 38: const_iterator begin() const {return addresses_.begin();} 39: const—iterator end()const {return addresses_.end();} 40: 41: // Поиск первой записи с фамилией большей/равной указанной. 42: // Как правило, это будет фамилия, начинающаяся с символов, 43: // введенных пользователем. 44: const—iterator findNameStartsWith(const std::stringslastname, 45: const std::stringS firstname==””) const; 46: 47: // Поиск следующего объекта Address, в полях которого // содержится указанная 48: // строка. Точка начала поиска задается параметром start. 49: const—iterator findNextContains(const std::strings searchStr, 50: const—iterator start) const; 51: 52: // Возвращает итератор на объект, заданный по ID. 53: const—iterator findRecordld(int recordld) const 156 Глава 6. Усовершенствование адресной книги...
54: throw (AddressNotFound); 55: 56:private: 57: // Запрещение копирования 58: AddressBook(const AddressBook&); 59: AddressBook& operator=(const AddressBook&); 60: 61: static int nextld_; 62: 63: addrByName__t addresses_; 64: 65: // Принимает индекс записи с указанным ID. 66: addrByName_t::iterator getByld(int recordld) 67: throw (AddressNotFound); 68: addrByName_t::const_iterator getByld(int recordld) const 69: throw (AddressNotFound); 70: }; 71: 72:#endif // AddressBook dot h Изменения весьма незначительны и не затрагивают пользовательский интер- фейс. В строке 6 добавляется заголовок <set>, в котором даны определения контей- неров set и multiset. В строке 12 основной тип данных меняется с list<Address> на multiset<Address>, а также с addrlist на addrByName_t меняется имя псевдо- нима. Хотя тип итератора изменился, он работает так же, как и в списке. Добавление, удаление и возвращение записей адресов Контейнеры set и multiset поддерживают внутреннюю кластерную структуру данных, что позволяет сохранять упорядоченность элементов. Благодаря поддержа- нию внутренней структуры добавление, удаление и возвращение элементов выпол- няется с высокой эффективностью. Чтобы максимально использовать преимущества совершенной структуры данных, мы заменим многие алгоритмы на вызовы функ- ций-членов класса-контейнера multiset. Причем это не потребует существенных изменений программного кода, как показано в листинге 6.9. Листинг 6.9. Первая часть реализации класса AddressBook с использованием контейнера multiset 1://TinyPIM (с)1999 Pablo Halpern. Файл AddressBook.срр 2: 3:#ifdef _MSC_VER 4:#pragma warning(disable : 4786) 5:#endif 6: 7:#include <algorithm> 8: 9:#include "AddressBook.h" 10: 11:int AddressBook::nextld_ = 1; 12: 13:AddressBook::AddressBook() 14: { Эффективная сортировка объектов Address ... 157
15: } 16: 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 :AddressBook::-AddressBook() : { : } :int AddressBook::insertAddress(const AddressS addr, : int recordld) throw (Duplicateld) :{ : if (recordld == 0) : // Если recordld не задан, генерируется новый ID. : recordld = nextld_++; : else if (recordld >= nextld_) : // Проверяет, чтобы nextId было больше идентификационных // номеров всех остальных записей. : nextld_ = recordld + 1; : else : { for (addrByName_t::iterator i = addresses_.begin(); : i != addresses_.end(); ++i) : if (i->recordld() == recordld) : throw Duplicateld(); : } // Присваивает recordld копии объекта Address : Address addrCopy(addr); : addrCopy.recordld(recordld); : // Вставляет запись в набор : addresses_.insert(addrCopy); : return recordld; : I :AddressBook::addrByName_t::iterator :AddressBook::getByld(int recordld) throw (AddressNotFound) : { : for (addrByName_t::iterator i = addresses_.begin(); : i != addresses_.end(); ++i) : if (i->recordld() == recordld) : return i; : throw AddressNotFound(); : } :AddressBook::addrByName_t::const_iterator :AddressBook::getByld(int recordld)const throw (AddressNotFound) : { : for (addrByName__t: : const__iterator i = addresses_. begin () ; : i !=addresses_.end(); ++i) : if (i->recordld() == recordld) : return i; : throw AddressNotFound(); : } :void AddressBook::eraseAddress(int recordld) : throw (AddressNotFound) : { 158 Глава 6. Усовершенствование адресной книги..
73:addrByName_t::iterator i = getByld(recordld); 74:addresses_.erase(i); 75: } 76: 77:void AddressBook::replaceAddress(const AddressS addr, int recordld) 78:throw (AddressNotFound) 79: { 80:if (recordld == 0) 81:recordld = addr.recordld(); 82: 83: eraseAddress(recordld); 84: insertAddress(addr, recordld); 85: } 86: 87:const Address&AddressBook: :getAddress (int recordld) const 88: throw (AddressNotFound) 89: { 90: return *getByld(recordld); 91: } 92: Строка 37 специально оставлена пустой, чтобы показать отсутствие линейно- го поиска позиции, в которую будет добавлена новая запись. Такой подход ис- пользовался в списках, но в контейнере-наборе в этом нет необходимости. Мы просто добавляем в строке 43 копию объекта Address в контейнер multiset, ко- торый полностью берет на себя ответственность за поддержание упорядоченно- сти элементов. Функция-член insert характеризуется логарифмическим време- нем выполнения. т В отличие от функций, константных по времени выполнения, работа I ермИН функций с логарифмическим временем выполнения несколько замедля- ется по мере увеличения числа элементов в контейнере. Причем между размером контейнера и средним временем выполнения функции суще- ствует логарифмическая зависимость. Такие функции работают с большими контейнерами значительно эффективнее, чем линейные, но менее эффективно, чем константные по времени выполнения. В строках 39,40 создается копия объекта Address и перед вводом в набор ей присваи- вается уникальный идентификационный номер. В списках нам не приходилось созда- вать копии объектов, зачем же они здесь? В списках функция insert возвращает итера- тор на новый элемент. Этот итератор используется затем для присвоения добавленному элементу уникального ID. Хотя функция insert контейнера multiset также возвращает итератор, мы не можем использовать его для изменения нового элемента. Дело в том, что изменение элемента в ассоциативном контейнере может нарушить порядок следования объектов в наборе, что вызовет внутреннюю неопределенность контейнера. Д ля преду- преждения таких изменений ассоциативные контейнеры никогда не возвращают ссыл- ки, по которым можно было бы изменять их элементы. Справочная информация: Экскурс нестандартность стандартной библиотеки В большинстве реализаций стандартных библиотек итераторы, воз- вращаемые контейнерами set и multiset, по типу близки const_iterator или даже совпадают с ними. Эффективная сортировка объектов Address ... 159
К сожалению, в документации по стандартизации нет четких указаний по поводу константности итераторов контейнеров set и multiset. В некоторых вариантах библиотеки, например, в той, что входит в со- став популярного компилятора Microsoft 6.0, допускается изменение элементов контейнера set с помощью неконстантного итератора. И это совершенно не противоречит принятым стандартам языка C++. В ок- тябре 1999 года проходило заседание Комитета по стандартизации, по- священное поиску решений преодоления нестандартности реализаций классов -конте йнеров. Даже если в стандартах будет позволено открывать доступ к элемен- там контейнеров set и multiset для их изменений, я вам рекомен- дую никогда этого не делать. По крайней мере, не следует изменять значения, по которым происходит сортировка элементов, поскольку это может привести к возникновению ситуаций неопределенности. Кроме того, открытие доступа к элементам наборов может вызвать ошибку компиляции на тех компиляторах, где подобные действия запрещены. Мы добились повышения эффективности выполнения программы за счет замены ли- нейного поиска точки ввода новой записи на операцию с логарифмическим временем выполнения. Но функция getByld по-прежнему работает линейно, так как записи в кон- тейнере отсортированы по именам, а не по ID. Поэтому упорядоченность элементов в на- боре никак не способствует поиску записей по идентификационным номерам. Рассмот- рим детальнее реализацию функций поиска в новом контейнере. Поиск в multiset Ассоциативные контейнеры были разработаны специально для эффективного поиска записей. Дополнительные возможности, предоставляемые этими контейне- рами, демонстрируются в листинге 6.10. Новый вариант программы будет выпол- нять те же функции, что и предыдущая версия на основе контейнера list, но значи- тельно эффективнее. Листинг 6.10. Функции поиска адресной книги на основе контейнера multiset 93:// Возвращает число записей с указанным именем. 94:int AddressBook::countName(const std::strings lastname, 95: const std::strings firstname) const 96: { 97: Address searchAddr; 98: searchAddr.lastname(lastname); 99: searchAddr.firstname(firstname); 100: 101: // Возвращает число совпадающих записей 102: return addresses^.count(searchAddr); 103: } 104: 105:// Находит первый объект Address с именем большим или равным 106:// заданному. Обычно это имя начинается с указанной 160 Глава 6. Усовершенствование адресной книги...
107:// последовательности Символов. 108:AddressBook::const_iterator 109:AddressBook::findNameStartsWith(const std::string& lastname, 110: const std::strings firstname) const 111: { 112: Address searchAddr; 113: searchAddr.lastname(lastname); 114: searchAddr.firstname(firstname); 115: 116: return addresses .lower bound(searchAddr); 117: } 118: 119:// Класс объекта функции для поиска строки в полях объекта Address. 120:class AddressContainsStr : public std::unary_function<Address, bool> 121: { 122:public: 123: AddressContainsStr(const std::string& str) : str_(str) {} 124: 125: bool operator()(const AddressS a) 126: { 127: using std::string; 128: 129: // Возвращает true, если поле объекта Address содержит str__ 130: return (a.lastname().find(str_) != string::npos || 131: a.firstname().find(strJ != string::npos || 132: a.phone().find(str_) != string::npos || 133: a.address().find(str_) != string::npos); 134:} 135: 136:private: 137: std::string str_; 138: } ; 139: 140:// Поиск следующего объекта Address, в полях которого содержится // указанная 141:// строка. Точка начала поиска задается параметром start. 142:AddressBook::const_iterator 143:AddressBook::findNextContains(const std::strings searchStr, 144: const__iterator start) const 145: { 146: return std::find_if(start, addresses_.end(), 147: AddressContainsStr(searchStr)); 148: } 149: 150:// Возвращает итератор на запись с указанным ID. 151:AddressBook::const_iterator 152:AddressBook::findRecordld(int recordld) const throw (AddressNotFound) 153: { 154: return getByld(recordld); 155: } Сначала рассмотрим небольшие изменения, которые мы внесли в функцию findNameStartsWith. В строке 116 алгоритм std: : lower_bound был заменен на функцию-член lower_bound класса multiset. Суть функции осталась прежней, но функция-член эффективнее использует преимущества кластерной структуры дан- ных класса multiset. Эффективная сортировка объектов Address ... 161
Аналогично в функции countName вместо алгоритма count_if используется функция-член count. Во всех ассоциативных контейнерах функция-член count возвращает число элементов с указанным значением. Использование count бо- лее эффективно, поскольку вместо линейного поиска эта функция использует кластерный подход. Но подождите! Что произошло с нашим классом AddressNameEqual? Разве без него функция count не станет сравнивать между собой все поля записи? Ответ удивит вас. Ни функция count и никакие другие функции сортирован- ного ассоциативного контейнера не используют оператор равенства (==). Два объекта х и у считаются эквивалентными, если ни одно из выражений х<у и у<х не возвращает true. Но поскольку оператор < определен для сравнения только полей имени и фамилии объекта Address, функция работает в соответствии с нашими требованиями. Термин Термин Во время сортировки и поиска элементов в ассоциативном контей- нере два объекта считаются эквивалентными, если для них задан одинаковый ранг в последовательности элементов. Равенство всех полей объектов для эквивалентности не обязательно. Так, два объ- екта Address в контейнере, отсортированном по именам, будут эк- вивалентны, но не равны, если у них равны значения в полях имени и фамилии. Два объекта считаются равными, если их сравнение с помощью опера- тора равенства (==) возвратило true. Функции f indNextContains и f indRecordld остались прежними. Переход к кон- тейнеру multiset сделал реализацию функций insertAddress, countName и f indNameStartsWith более эффективным и простым. Но другие функции по- прежнему полагаются в своей работе на старую функцию getByld, которая линейно отыскивает записи. Может, записи в адресной книге следовало отсортировать в со- ответствии с идентификационными номерами? Это ускорило бы выполнение неко- торых задач, но замедлило бы выполнение других, где используются итераторы на алфавитные последовательности. В следующем разделе мы рассмотрим возмож- ность одновременной сортировки записей по именам и ID. Двойное индексирование в контейнере с установкой соответствий Нам нужно, чтобы в классе AddressBook поиск записей по идентификационным номерам выполнялся с такой же эффективностью, как и поиск записей по именам. Для этого нам придется поддерживать два контейнера. Один будет отсортирован по именам, а другой — по идентификационным номерам. Но с точки зрения сокраще- ния расхода памяти и предотвращения возникновения несоответствий между кон- тейнерами было бы нерационально создавать еще одну копию всех записей адресной книги. Чтобы разрешить это противоречие, второй контейнер можно построить из указателей на элементы первого контейнера. 162 Глава 6. Усовершенствование адресной книги...
Вторичное индексирование записей путем установки соответствий В текущей реализации контейнера adrresses_ на основе шаблона muliset записи отсортированы по именам. Следовательно, во втором контейнере записи нужно сортиро- вать по значениям ID, причем между объектами обоих контейнеров следует установить четкие связи. В терминологии баз данных это называется вторичным индексированием элементов контейнера (первичное индексирование было выполнено в исходном контей- Hepemuliset). Отношения между двумя контейнерами показаны графически на рис. 6.2. addrByName_ addresses ID Запись Телефон, Фамилия Имя ID адрес 1 Bush George 3 555-1989 5 Iraq Dr 3 Carter Jimmy 5 555-1977 7 Iran St 4 Clinton William 1 555-1993 9 Monica 5 Ford Gerald 9 555-1974 8 Golf Dr 9 Johnson Lyndon 16 555-1964 1 War Path 11 Nixon Richard 11 555-1969 8 Water Gt К Г 16 Reagan Ronald 4 555-1981 4 Contra Рис. 6.2. Вторичное индексирование объектов адресной книги Нужно иметь возможность после обнаружения в контейнере addrById_ элемента с требуемым ID быстро находить соответствующий ему объект в контейнере addresses . Другими словами, нужно установить соответствие (mapping) между значениями ID од- ного контейнера и элементами другого контейнера. В стандартной библиотеке для вы- полнения указанной задачи служит специальный шаблон контейнера—тар. Шаблон класса тар реализуется минимум с двумя параметрами. Первый пара- метр шаблона— это тип ключа, по которому сортировка записей выполняется так же, как в контейнерах типа set. Другой параметр — это тип содержимого, с которым будет связан каждый ключ. Еще один контейнер, multimap, отличается от тар тем, что в нем разрешено дублирование записей. (Аналогичной парой контейнеров были set и multiset.) Хотя в адресной книге допускалось наличие записей с одинаковыми именем и фамилией, дублирование ID было строго запрещено. Поэтому для адресной книги лучше подойдет контейнер тар. Термин Термин Ключом называется атрибут записи базы данных, по которому можно отличать записи друг от друга, например во время сортировки. Под содержимым понимается элемент контейнера, ассоциированный с определенным ключом. В терминологии баз данных часто говорят о значениях, связанных с ключами. Но поскольку в терминологии стан- дартной библиотеки слово значение может использоваться для всего объекта вместе с ключом, для предупреждения двусмысленности мы бу- дем использовать термин содержимое. Двойное индексирование в контейнере с установкой соответствий 163
Ясно, что ключ контейнера addrById_ должен быть целым числом и пред- ставлять идентификационные номера записей адресной книги. Что в таком слу- чае будет содержимым? Это не может быть объект Address, поскольку в этом случае записи контейнера addrById_ будут дублировать записи набора addresses . В качестве содержимого можно было бы использовать указатели на объекты Address, но есть более совершенное решение. Вместо указателей мы бу- дем использовать итераторы на элементы набора addresses_. По мере анализа реализации класса вы увидите, какие преимущества дает использование итера- торов. Для начала просто вспомним, что итераторы во многом напоминают ука- затели на объекты и легко могут использоваться вместо них. Новое определение класса AddressBook с добавленным вторичным индексом addrByld показано в листинге 6.11. : Листинг 6.11. Класс AddressBook с добавленным вторичным индексом 1://TinyPIM (с)1999 Pablo Halpern. Файл AddressBook.h 2: 3:#ifndef AddressBook_dot_h 4:#define AddressBook_dot_h 5: 6:#include <set> 7:#include <map> 8:#include ’’Address.h” 9: 10:class AddressBook 11: { 12: // Псевдонимы переменных-членов 13: typedef std::multiset<Address> addrByName_t; 14: typedef std::map<int, addrByName__t::iterator> addrBy!d_t; 15: 16:public: 17: AddressBook(); 18: -AddressBook(); 19: 20: // Классы исключений 21: class AddressNotFound {}; 22: class Duplicateld {}; 23: 24: int insertAddress(const Address& addr, int recordld = 0) 25: throw (Duplicateld); 26: void eraseAddress(int recordld) throw (AddressNotFound); 27: void replaceAddress(const Address& addr, int recordld = 0) 28: throw (AddressNotFound); 29: const Address& getAddress(int recordld) const 30: throw (AddressNotFound); 31: 32: // Возвращает число записей с указанным именем. 33: int countName(const std::strings lastname, 34: const std::strings firstname) const; 35: 36: // Итератор на записи адресной книги 37: typedef addrByName__t: : const_iterator const__iterator; 38: 39: // Функции доступа к записям адресной книги 164 Глава 6. Усовершенствование адресной книги...
40: const__iterator begin () const {return addresses_.begin();) 41: const_iterator end() const {return addresses_.end();} 42: 43: // Находит первый объект Address с именем большим или равным 44: // заданному. Обычно это имя начинается с указанной 45: // последовательности символов. 46: const—iterator findNameStartsWith(const std::strings lastname, 47: const std::string& firstname = const; 48: 49: // Поиск следующего объекта Address, в полях которого // содержится указанная 50: // строка. Точка начала поиска задается параметром start. 51: const—iterator findNextContains(const std::string& searchStr, 52: const—iterator start) const; 53: 54: // Возвращает итератор на запись по указанному ID. 55: const—iterator findRecordld(int recordld) const 56: throw (AddressNotFound); 57: 58:private: 59: // Запрещение копирования 60: AddressBook(const AddressBook&); 61: AddressBook& operator=(const AddressBooks); 62: 63: static int nextld_; 64: 65: addrByName_t addresses_; 66: addrByld—t addrBy!d__; 67: 68: // Принимает индекс записи с указанным ID. 69: addrByName_t::iterator getByld(int recordld) 70: throw (AddressNotFound); 71: addrByName_t::const—iterator getByld(int recordld) const 72: throw (AddressNotFound); 73: }; 74: 75:#endif // AddressBook dot h Изменения в файле заголовка затронули только строки 7, 14 и 66. В стро- ке 14 определяется тип addrById__ для класса тар, реализуемого на основе шаб- лона тар, который добавляется в код программы в строке 7. В шаблон передают- ся два параметра, задающие тип int для ключа и тип addrByName_t: : iterator для содержимого. В строке 66 контейнер addrById_map объявляется как член класса AddressBook. Поддержание соответствия между двумя контейнерами Теперь каждый раз при добавлении или удалении элемента AddressBook необхо- димо обновлять два контейнера: addresses_ и addrById_. Для этого нам необходимо внести изменения в реализации функций insertAddress, eraseAddress и getByld, как показано в листинге 6.12. Остальные функции не требуют изменений, так как в своей работе не используют идентификационные номера записей. Двойное индексирование в контейнере с установкой соответствий 165
^Листинг 6.12. Изменениефункций запиЬейдляподдеряйния (^теет^п^^ме^ду контейнерами "; 1://TinyPIM (с)1999 Pablo Halpern. Файл AddressBook.срр 2: 3:#ifdef _MSC_VER 4:#pragma warning(disable : 4786) 5:#endif 6: 7:#include <algorithm> 8: 9:#include ’’AddressBook.h” 10: 11:int AddressBook::nextId_ = 1; 12: 13:AddressBook::AddressBook() 14: { 15: } 16: 17:AddressBook::-AddressBook() 18: { 19: } 20: 21: int AddressBook: : insertAddress (const Address&addrf 22: int recordld)throw (Duplicateld) 23: { 24: if (recordld == 0) 25: // Если recordld не задан, генерируется новый ID. 26: recordld = nextld__++; 27: else if (recordld >= nextld_) 28: // Проверяет, чтобы nextId было больше идентификационных // номеров всех остальных записей. 29: nextld_ = recordld + 1; 30: else if (addrById_.count(recordld)) 31: // Такой ID уже есть в контейнере map 32: throw Duplicateld(); 33: 34: // Присваивает recordld копии объекта Address 35: Address addrCopy(addr); 36: addrCopy.recordld(recordld); 37: 38: // Ввод записи в набор 39: addrByName__t::iterator i = addresses_.insert(addrCopy); 40: 41: // Добавляет итератор на Address в контейнер map 42: 11 addr By I d__. insert (std: :make__pair (recordld, i)) ; 43: addrBy Id__[recordld ] == i; 44: 45: return recordld; 46: } 47: 48:AddressBook::addrByName_t::iterator 49:AddressBook::getByld(int recordld) throw (AddressNotFound) 50: { 51: // Поиск записи no Id. 52: addrById_t::iterator idlter = addrBy Id_.find(recordld); 53: if (idlter == addrById_.end()) 54: throw AddressNotFound(); 166 Глава 6. Усовершенствование адресной книги...
55: 56: return idlter->second; 57:} 58: 59:AddressBook::addrByName_t::const_iterator 60:AddressBook::getByld(int recordld)const throw (AddressNotFound) 61: { 62: // Поиск записи no Id. 63: addrBy Id__t: : const__iterator idlter = addrById__. find (recordld) ; 64: if (idlter == addrById_.end()) 65: throw AddressNotFound0; 66: 67: return idlter->second; 68: } 69: 70:void AddressBook::eraseAddress(int recordld) 71: throw (AddressNotFound) 72: { 73: addrByName_t::iterator i = getByld(recordld); 74: 75: // Удаление записи из обоих контейнеров 76: addresses_.erase(i); 77: addrById_.erase(recordld); 78: } В строке 30 цикл линейного поиска заменен на вызов функции-члена count кон- тейнера тар. Эта функция работает так же, как и аналогичные функции-члены кон- тейнеров set и multiset, т.е. подсчитывает, сколько раз заданный ключ встречает- ся в контейнере тар. Поскольку контейнер тар запрещает дублирование записей, функция count может возвращать только 0 (заданный ключ отсутствует) или 1 (заданный ключ есть в контейнере). Если count возвратит true (1), будет запущено исключение дублирования записи. Контейнер тар как ассоциативный массив После того как программа убедится, что новая запись не является дубликатом уже существующей, она добавляется, как и раньше, в строке 39 в контейнер multiset, но в этот раз программа также сохраняет значение, возвращаемое функцией insert. Функция-член insert класса set возвращает итератор на добавленный объект. Как раз этот итератор и следует сохранить в нашем новом контейнере-карте тар. В стро- ке 43 показан один из способов добавления элементов в контейнер-карту. Шаблон класса тар предоставляет оператор индексирования (operator [ ]), который позволя- ет работать с контейнером-картой как с ассоциативным или рассеянным массивом. т Ассоциативным массивом называется структура данных, в которой ин- I ермИН дексация элементов выполняется как в обычном массиве, за тем ис- ключением, что индексы не обязательно должны быть положительны- ми целыми числами. Например, можно создать массив записей о сту- дентах факультета, в качестве индексов которого будут выступать фамилии студентов. Термин Рассеянным называется массив, содержащий неиспользуемые (пустые) элементы (другими словами, индексы массива не образуют сплошной чи- словой ряд). Тем не менее структура данных рассеянного массива устроена таким образом, чтобы предотвратить траты памяти на пустые элементы. Двойное индексирование в контейнере с установкой соответствий 167
В строке 43 в действительности выполняются две операции. Сначала выполняется оператор индексирования addrByld [recordld], а затем— присвоение значения пе- ременной i. Оператор индексирования ведет поиск записи в контейнере-карте по ука- занному ключу и возвращает ссылку на содержимое, связанное с этим ключом. Если ключ не будет найден, то он добавляется в контейнер тар с помощью конструктора по умолчанию для объекта содержимого. Поскольку recordld является новым ключом, оператор индексирования вставляет его в карту и связывает с итератором, заданным по умолчанию, после чего возвращает ссылку на этот бессмысленный итератор. Эта ссылка сразу же присваивается переменной i. Примите к сведению, что этот метод доступа к контейнеру-карте не работает с контейнером multimap, поскольку в нем ин- декс не обязательно представляет уникальный элемент. Структура pair Альтернативный способ добавления значений в объект тар показан в стро- ке 42 (в этой версии реализации класса AddressBook операция заблокирована опе- ратором комментариев //). В данном случае ключ и содержимое объединяются в од- ном объекте, который передается в функцию insert. Результатом вызова функции make pair является объект структуры pair, стандартное определение которой пока- зано в листинге 6.12. Листинг 6.12. Шаблон структуры pair в заголовке <utility> 'jF'sf 1:template cclass Tl, class T2> 2:struct pair 3: { 4: typedef Tl first_type; 5: typedef T2 second_type; 6: 7: Tl first; 8: T2 second; 9: 10: pair(); 11: pair(const T1& x, const T2& y); 12: template cclass U, class V> pair(const pairCU, V>& p); 13: }; Структура pair содержит только открытые члены, включая ее переменные. Для реализации шаблона в него нужно передать два значения произвольного типа. Наи- более важными переменными-членами структуры pair являются first и second, определенные в строках 7, 8. Шаблон pair определен в заголовке <utility> и, без- условно, принадлежит пространству имен std. Функция make pair принимает два значения произвольного типа и возвращает объект pair, содержащий эти значения. На Особенности компиляции. Функция make pair слишком бук- ЗЭметку вально распознает типы своих аргументов. Например, вызов make_pair(5, "hello”) возвратит объект типа paircint, const char[6]> вместо ожидаемого paircint, const char*>. Это обычно проходит незаметно для программиста благодаря работе операторов преобразований, встроенных в pair (строка 12 листинга 6.13). Но мно- гие компиляторы по-прежнему не поддерживают шаблоны-члены, не- обходимые для работы операторов преобразований. В результате во 168 Глава 6. Усовершенствование адресной книги...
время компиляции может возникнуть конфликт типов, если объект pair окажется не того типа, который вы ожидали. Чтобы избежать это- го, не используйте вообще функцию make pair. Тип объекта можно оп- ределить явно с помощью конструктора, как, например, в следующем примере: pair<int, const char*>(5, "hello"). Применительно к нашему контейнеру тар проще и лучше использовать определение типа value_type: addrBy!d_t::value_type(recordld, i). Структура pair используется в некоторых стандартных библиотечных классах. Кроме того, что с ее помощью можно задавать аргумент, передаваемый в функцию insert классов тар и multimap, эту структуру можно использовать для определения типа, возвращаемого при разыменовании итератора. Так, если вас интересует нави- гация только по ключам или объектам содержимого контейнера-карты, можно явно ссылаться на переменные-члены first и second. Например, для вывода на печать всех ключей контейнера addrByld можно использовать следующее выражение: for (addrBy!d_t::iterator i = addrByld.begin(); i != addrByld.end(); ++i) std::cout « i->first « std::endl; |_| a Особенности компиляции. В некоторых устаревших компиляторах, осо- ЗЭ М етку бенно в SunPro 4.2, оператор разыменования -> нельзя применять к итера- торам. Это создает препятствие к использованию контейнеров-карт, по- скольку в них очень часто приходится разыменовывать итераторы. Реше- нием может быть замена выражения i->firstHa (* i) .first. Одно существенное замечание, о котором вам следует помнить: результат разыме- нования итератора тар или multimap будет не pair<key, content>&, apair<const key, content>&. Другими словами, в объекте pair можно изменять со- держимое (переменную-член second), но не ключ (переменную-член first). Изменение ключа могло бы привести к нарушению порядка следования элементов в контейнере- карте. Если нужно изменить ключ, связанный со значением, то сначала следует уда- лить значение из контейнера, а затем вновь добавить его с новым ключом. И наконец, объект pair также возвращается в результате выполнения функции insert в контейнерах тар или set. Функции insert контейнеров multimap или multiset возвращают итераторы на добавленный объект. Также поступают функции insert контейнеров тар или set, но в них не допускается дублирование элементов. Что же возвратит операция добавления элемента, если его дубликат будет обнаружен в контейнере? Контейнер возвратит объект типа pair<iterator, bool>. Переменная- член second возвращенного объекта pair будет установлена на true, если вставка элемента прошла успешно, или false, если дубликат записи обнаружен в контейнере. Переменная-член first возвращенного объекта pair будет указывать на успешно до- бавленный элемент или на ранее введенный, чей ключ совпал с ключом добавляемого элемента. Если вас интересует только один из компонентов объекта pair, можно явно обратиться к переменным-членам first или second. Например: if (’ myset.insert(х).second) std::cout « "Duplicate item, " « x << std::endl; Реализация оставшихся функций Вернемся к листингу 6.12 и рассмотрим реализацию функции getByld в стро- ках 48-57. Как вы помните, эта функция используется в разных местах программы, где требуется отыскать запись с определенным ID. В предыдущих версиях функции Двойное индексирование в контейнере с установкой соответствий 169
выполнялся линейный поиск. В текущей версии программы в строке 52 вызывается функция-член find, которая возвращает итератор на элемент контейнера addrById_. Если указанный ключ не будет найден, функция find возвратит addr By Id. end (). Ситуация отсутствия записи в контейнере отслеживается в стро- ке 53. В строке 56 функция getByld возвращает переменную-член second объекта pair, на которую указывает idlter. Операция разыменования возвращает объект содержимого, ассоциированный с заданным recordld, который в то же время явля- ется итератором набора addresses . Немного сложно? Рассмотрим еще раз рис. 6.2. Если мы ищем запись с идентификационным номером 9, функция find возвратит итератор, указывающий на объект pair с ID равным 9. В результате разыменования итератора и явного обращения к переменной-члену second будет получен новый итератор, указывающий на запись Gerald Ford в наборе addresses . Реализацию функции getByld можно было бы свести к одной строке: return addrByld[recordld].second; Но в этом варианте реализации пришлось пожертвовать проверкой на отсутствие заданного ключа в контейнере. В таком случае отсутствующий ключ автоматически добавляется в контейнер тар и с ним связывается бессмысленный итератор контей- нера set. Таким же бессмысленным будет значение, возвращенное функцией getByld, что приведет к возникновению ситуации неопределенности. Лучший спо- соб предупреждения появления таких ошибок — определение для объекта содержи- мого такого конструктора по умолчанию, который бы всегда создавал значимые ите- раторы. Еще одно замечание: поскольку оператор индексирования возвращает ссылку на объект pair, а не итератор на него, для разыменования его переменных- членов нужно использовать оператор (.), а не (->). И наконец, рассмотрим функцию eraseAddress, реализация которой начинается со строки 70 листинга 6.12. После того как в строке 76 запись удаляется из набора addresses , в следующей строке ассоциированная с ней запись удаляется из кон- тейнера addrById_. Все ассоциированные контейнеры содержат перегруженные версии функции erase. Версия, реализацию которой вы видите в строке 77, удаляет запись по заданному ключу. А версия в строке 76 для определения удаляемой записи использует итератор. Справочная информация: дробленые ассоциа- Экскурс тивные контейнеры__________________________________________ Многих программистов удивило, что дробленые [hashed) ассоциатив- ные контейнеры не были добавлены в стандартную библиотеку C++ из- за того, что это предложение слишком поздно поступило в Комитет по стандартизации языка C++. В дробленых контейнерах функция поиска элементов константна по времени выполнения. Более подробно о дроб- леных контейнерах вы можете прочитать в любом пособии, посвящен- ном структурам данных. Дробленые контейнеры относятся к ассоциа- тивным, т.е. элементы в них сохраняются и возвращаются по значени- ям, а не по ссылкам. В отличие от сортированных контейнеров, в дробленых элементы не упорядочены. Но в тех случаях, когда порядок следования элементов не имеет значения, дробленые контейнеры по- зволяют существенно повысить эффективность программы за счет за- мены функций поиска с логарифмическим временем выполнения на константные. 170 Глава 6. Усовершенствование адресной книги...
Хотя дробленые контейнеры не вошли в стандарты языка C++, многие версии стандартной библиотеки содержат их шаблоны. Интерфейс дробленых контейнеров разных изготовителей, несмотря на отсутствие стандартов, удивительно типичен. Для реализации таких контейнеров всегда требуются два объекта функции: дробления и равенства. Объект функции дробления часто задается по умолчанию для всех встроенных типов данных, указателей и стандартных объектов string. Для всех ас- социативных контейнеров, которые мы рассмотрели в этой главе, су- ществует аналогичный дробленый контейнер. Определения их шабло- нов показаны ниже: hash_set<T, Н = hash<T>, EQ = equal<T> > hash_multiset<T, H = hash<T>, EQ = equal<T> > hash_map<Key, T, H = hash<Key>, EQ = equal<Key> > hash_multimap<Key, T, H = hash<Key>, EQ = equal<Key> > Примите к сведению, что в разных реализациях дробленых классов мо- хут использоваться разные имена переменных, например hashset вме- сто hash_set. Кроме того, некоторые изготовители включают дробленые контейнеры в пространство имен std, хотя они не являются стандарт- ными средствами программирования. Просмотрите техническую доку- ментацию, предоставленную изготовителями, чтобы узнать больше о приобретенной вами стандартной библиотеке Резюме В этой главе мы существенно модернизировали наш класс AddressBook. Были добавлены функции поиска записей по именам и по ключевым словам, содержащим- ся в любом поле записи. Для реализации этих функций мы использовали стандарт- ные библиотечные алгоритмы и объекты. Для повышения эффективности выполне- ния функций мы заменили тип контейнера с последовательного list на ассоциа- тивный multiset, к которому затем был добавлен контейнер тар для вторичной индексации записей. Новая версия программы с высокой эффективностью выполняет операции до- бавления, удаления, возвращения и поиска записей, но у нас не было возможности проверить все эти функции на практике. В следующей главе мы займемся разработ- кой пользовательского интерфейса, основанного на функциях, которые мы добавили в класс AddressBook. Для реализации пользовательского интерфейса нам придется воспользоваться потоками библиотеки ввода-вывода, а также некоторыми дополни- тельными контейнерами и алгоритмами. Резюме 171
Глава? Прокручивание экранных списков с помощью двухсторонних очередей и потоков ввода-вывода В этой главе... • Требования к экранному списку 172 • Разработка экранного списка 173 • Класс DisplayList 175 • Прокручивание списка 178 • Считывание вводимых чисел и отслеживание ошибок 195 • Определение класса AddressDisplayList 197 • Резюме 204 В предыдущей главе мы добавили в класс AddressBook функции, которые позво- ляют получать доступ к записям адресной книги и находить требуемые записи по именам и ключевым словам. Эти средства потребуются нам также для выполнения следующей задачи: разработки функций управления экранными списками. Класс экранных списков должен отображать списки записей и позволять пользователю выбирать записи для просмотра, редактирования или удаления. Поскольку в адрес- ной книге может быть больше записей, чем умещается на экране терминала, класс экранных списков также должен позволять прокручивать записи вверх и вниз, от- крывая для пользователя один экранный список. Требования к экранному списку Давайте детальнее определимся с требованиями к экранному списку. 1. Записи в экранном списке должны быть представлены в виде резюме. Так, для записей адресной книги на экране должны отображаться фамилия, имя и но- мер телефона, чтобы пользователь мог определить телефонный номер абонен- та, не открывая запись для просмотра. 2. В экранном списке должна быть представлена выборка записей, умещающая- ся па экране. Для простоты примем, что экранный список должен состоять не более чем из 15 записей, а каждая запись должна быть пронумерована от 1 до . 15. При переходе в режим просмотра записей на экране должны отобразиться первые 15 записей списка.
3. Если пользователь введет команду прокрутки вперед, на экране должен ото- бразиться следующий экранный список, а после ввода команды прокрутки на- зад — предыдущий экранный список. 4. При достижении последней записи должно появляться соответствующее со- общение и блокироваться прокрутка вперед. При отображении первого экран- ного списка также следует показать сообщение о достижении начала списка и заблокировать прокрутку назад. Если в списке вообще нет записей, то на эк- ране должно отобразиться сообщение об этом. 5. Пользователь может выбрать запись для просмотра, редактирования или удале- ния, указав ее номер в экранном списке. Прежде всего программа должна прове- рить, входит ли введенный номер в диапазон от 1 до 15. Если нет, то пользовате- лю нужно показать сообщение об ошибке и предложить повторить ввод. Разработка экранного списка Как мы решили еще в главе 1, наша программа TinyPIM будет строиться из трех ос- новных модулей: ядра программы, пользовательского интерфейса и (в перспективе) бло- ка функций сохранения. Вспомните также о том, что в будущем нам может потребовать- ся разделить пользовательский интерфейс и ядро программы, поместив их в разные приложения, связанные по сети. Чтобы не перегружать сеть, интерфейс приложения должен возвращать не все записи сразу, а только выборку записей, умещающуюся на эк- ране. Причем, чтобы избежать частых обращений к ядру программы, пользовательский интерфейс должен уметь выбирать ту часть списка, где находятся записи, необходимые пользователю. Например, если пользователь ищет запись из середины списка, то было бы не рационально, если бы программа возвращала первый экранный список и вынуждала пользователя прокручивать список до нужного места. Каждое такое прокручивание будет сопровождаться обменом пакетами данных между интерфейсом и ядром программу, что повысит нагрузку на сеть. Из этих же соображений имеет смысл сохранять в пользова- тельском интерфейсе предыдущий экранный список, чтобы операции прокручивания списка вперед и назад на один экран также не нагружали сеть. Для сохранения предыду- щего экранного списка программа помещает его в буфер. Буфером (cache) называется контейнер, в котором поддерживаются не- кермин давно используемые объекты, что позволяет применять их в будущем без необходимости повторного вычисления или возвращения из базы данных. Термин буферизация означает помещение данных в буфер. Нам нужна структура данных, которая под держивала бы текущий набор записей, отображаемых на экране, а также предыдущий набор. По мере прокручивания спи- ска пользователем все новые наборы записей добавляются в буфер. Для наглядности на рис. 7.1 показана схема отношений буфера и основного списка записей. В буфер помещена непрерывная выборка записей из основного контейнера. На экране в определенный момент отображается непрерывная выборка записей из бу- фера, соответствующая размеру экрана. Если пользователь прокручивает список за- писей за пределы буфера, очередная порция записей возвращается из адресной кни- ги и добавляется в начало (прокрутка вперед) или в конец (прокрутка назад) выбор- ки, ранее занесенной в буфер. Экранный список отображает выборку записей из буфера, причем отображаемые записи могут находиться в начале, в конце или в се- редине буферизованного списка. Разработка экранного списка 173
Таким образом, наша структура данных должна также в равной степени поддер- живать ввод данных как в начало, так и в конец текущего списка и открывать произ- вольный доступ к записям, находящимся в середине списка. AddressBook Экранный список Буфер 1: Manning, Hillary 2: O'Maliy, John 3: Peters, Mary 4: Rivers, Maxine 5: Schmidt, Wolfgang 6: Smith, Joan 7: Smith, John 8: Smith, Mark 9: Smith, Maurine Puc. 7,1. Буфер экранных списков Произвольным доступом называется возможность получать доступ к элементам контейнера независимо от их позиции. Контейнер list позволяет добавлять записи с обоих концов списка, а контейнер vector открывает произвольный доступ к записям в середине списка, но нам нужен кон- тейнер, который бы делал и то и другое. Шаблон такого контейнера также представлен в стандартной библиотеке. Это— deque, или двухсторонняя очередь. По функциональ- ному набору и характеристикам эффективности выполнения контейнер deque не отли- чается от контейнера vector, но в нем есть две дополнительные функции: push_f ront и pop_f ront, которые добавляют и удаляют элементы в начало списка и характеризуют- ся константным временем выполнения. Добавление записи в середину списка, как и в случае с vector, линейно зависит от размера контейнера. Как вы видите, по своим характеристикам двухсторонняя очередь вполне подходит д ля сохранения буферизован- ного списка. С работой контейнера этого типа мы познакомимся более подробно в сле- дующих разделах, когда приступим к созданию класса буфера. Шаблон класса deque берет на себя все функции по сохранению, добавлению и удалению записей из списка, тем не менее, для нас остается еще много работы по программированию средств манипулирования экранными списками и орга- низации взаимодействия интерфейса с ядром программы. Поскольку экранные списки нужны как для отображения записей адресной книги, так и записей кни- ги контактов, имеет смысл предусмотреть возможность совместного использо- вания обеими книгами общих для них функций. Для этого подойдет следующее программное решение. Создадим базовый класс DisplayList и произведем от него два подкласса AddressDisplayList и AppointmentDisplayList. Диаграм- ма отношений между этими классами показана на рис. 7.2. 174 Глава 7. Прокручивание экранных списков...
Рис. 7.2. Диаграмма отношений классов экранных списков Поскольку буферизация списков необходима как для записей адресов, так и для запи- сей контактов, эту функцию следует определить в базовом классе DisplayList. Общим элементом объектов Address и Appointment является поле ID, следовательно, контейнер буфера может содержать значения ID записей. Функции возвращения и отображения за- писей по их ID должны взять на себя производные классы. Базовый класс делегирует эти задачи производным классам путем вызова виртуальной функции fetchMore для воз- вращения экранных списков и функции displayRecord — для показа резюме. Базовый класс также должен содержать функции прокручивания списка и выбора записей. Класс DisplayList В листинге 7.1 показано определение класса DisplayList. 1://TinyPIM (с)1999 Pablo Halpern. Файл DisplayList.h 2: 3:#ifndef DisplayList_dot__h 4:#define DisplayList_dot_h 1 5: 6:#include <deque> Класс DisplayList 175
7:#include <vector> 8: 9:// Этот абстрактный класс используется для показа списка 10:// записей, причем тип отображаемой записи 11:// определяется в производных классах. 12:// Если в списке больше записей, чем можно вывести 13:// на экран, отображается часть списка и предлагаются 14:// функции для прокручивания записей вверх и вниз. 15:class DisplayList 16: { 17:public: 18: DisplayList(int linesPerScreen = 15); 19: virtual -DisplayList(); 20: 21: 22: void display(); void pageDown(); // Показывает список // Прокручивает список вниз 23: void pageUp(); // Прокручивает список вверх 24: void toStart(); // Показывает первый экранный список 25: bool atStart(); // Возвращает true, если текущим является // первый экранный список 26: bool atEnd(); // Возвращает true, если текущим является // последний экранный список 27 : void reset(); // Очищает буфер от записей 28: int screenRecord(int n)const; // Возвращает ID записи, // представленной в экранном // списке под номером п 29: 30: // Делает выбранную запись первой в экранном списке. 31: void scrollToTop(int recordld); 32: 33: // Запрашивает у пользователя номер записи и возвращает // соответствующий recordld 34: // Возвращает 0, если не выбрано ни одной записи //(отмена режима просмотра пользователем). 35: int selectRecord(); 36: 37:protected: 38: // Функция displayRecord определяется в производных 39: // классах для возвращения типа отображаемой записи. 40: virtual void displayRecord(int recordld)=0; 41: 42: // Функция fetchMore определяется в производных классах для // возвращения 43: // новой порции numRecords записей, начиная с записи, 44: // следующей за startld. Результатом будет вектор ID // возвращенных записей. 45: // Если значение numRecords отрицательно, возвращаются записи, // ПРЕДШЕСТВУЮЩИЕ startld. 46: // Если startId=0, то достигнут первый (или последний) элемент // исходного списка. 47: // Функция возвращает true, если в конечном векторе представлена 48: // первая или последняя запись. 49: virtual bool fetchMore(int’startld, int numRecords, 50: std::vector<int>& result) = 0; 51: 52:private: 53: 176 Глава 7. Прокручивание экранных списков...
54: typedef std::deque<int> cache_t; // Тип буфера записей 55: 56: int linesPerScreen__; // Число строк в экранном списке 57: cache__t cache_; // Буфер известных записей. 58: bool cachedFirst_; // Возвращает true, если cache_ содержит // первую запись 59: bool cachedLast_; // Возвращает true, если cache_ содержит // последнюю запись 60: int firstVisible!dx_; // Индекс первой видимой записи // в контейнере deque 61: 62: // Наращивает буферизованный список спереди. 63: // Устанавливает начальный индекс и требуемое число записей. Если 64: // указанное число записей недоступно, устанавливает cachedLast_. 65: void fillCacheFwd(int start, int numNeeded); 66: 67: // Наращивает буферизованный список сзади. 68: // Устанавливает начальный индекс и требуемое число записей. 69: // Если указанное число записей недоступно, устанавливает // cachedLast_. 70: void fillCacheBkwd(int start, int numNeeded); 71: }; 72: 73:#endif //DisplayList dot h В строке 6 добавляется заголовок <deque>. В строке 54 определяется псевдо- ним для двухсторонней очереди целых чисел, который используется затем в строке 57 для объявления буфера. В классе также объявляется ряд перемен- ных-членов, которые используются для отслеживания состояния экранного спи- ска. Переменная linesPerScreen_ (строка 56) устанавливается конструктором и больше не меняется за время жизни . объекта DisplayList. Переменные cachedFirst_ и cachedLast_ (соответственно строки 58 и 59) указывают, при- сутствуют ли в экранном списке первый и последний элементы и, следовательно, можно ли выполнять прокручивание списка вверх и вниз. Переменная firstvisibleldx_ содержит индекс элемента буфера, который является первым в текущем экранном списке. В строках 21-31 объявляются основные функции, используемые как кодом кли- ента, так и производными классами экранных списков. Назначение каждой функ- ции описано в комментариях. В строке 35 объявляется функция selectRecord, под- держивающая взаимодействие с пользователем. Ожидается, что пользователь введет в командной строке номер записи от 1 до значения linesPerScreen_, после чего функция selectRecord возвратит идентификационный номер recordld записи, представленной в экранном списке под введенным номером. Если пользователь вве- дет О, то функция также возвратит О, что будет означать конец сеанса просмотра списка записей. Если пользователь введет любое другое значение меньше О или больше linesPerScreen_, то программа выведет сообщение об ошибке и предложит пользователю повторить ввод. Функция displayRecord, объявленная в строке 40, используется классом DisplayList для вывода резюме выбранных записей. Это чисто виртуальная функция (определение чисто виртуальных функций см. в главе 1), которая долж- на быть замещена в каждом производном классе. Только конкретный производ- ный класс знает, какого типа записи следует отображать (например, класс AddressDisplayList показывает записи типа Address) и в каком виде (например, для записей адресной книги: фамилия, имя и номер телефона). Класс DisplayList 177
Другая чисто виртуальная функция fetchMore объявляется в строках 49, 50. Эта функция возвращает из ядра программы очередную порцию строк и добавляет их в буфер. Когда программа определяет, что требуется очередная порция записей, вы- зывается функция fetchMore, в которую передается требуемое число записей и точ- ка начала отсчета st rat Id. В ходе выполнения функция fetchMore (реализация этой функции записывается в производном классе) возвращает указанное число до- полнительных записей и помещает их идентификационные номера в результирую- щий вектор. Если необходимо добавить записи, предшествующие текущим (т.е. осу- ществляется прокрутка назад), идентификационный номер передается не первой, а последней записи, а число возвращаемых записей обозначается отрицательным значением. В таком случае в возвращенном векторе переменная st rat Id будет ука- зывать не на начало, а на конец списка. Следует отметить, что в обоих случаях иден- тификационный номер st rat Id не включается в возвращаемый вектор. Если функ- ция fetchMore не может выполнить поставленную задачу, поскольку достигла конца или начала исходного списка записей, то возвращается логическое значение true. Функция fetchMore— одна из самых любопытных среди функций-членов произ- водных классов экранных списков. Подробнее мы познакомимся с ее работой в сле- дующих разделах. В строках 65 и 70 объявляются две функции, предназначенные для заполнения буфера. Это закрытые внутренние функции-члены класса, которые проверяют, со- держит ли буфер требуемые записи, прежде чем отобразить их на экране. В этих функциях заключена логическая схема работы буфера. Прокручивание списка Проследим теперь логику буферизации записей, начиная с исходного состояния буфера, и пройдем через все процессы его заполнения. В листинге 7.2 показана пер- вая часть реализации класса DisplayList. М'-V , .._ -. w ... Л... ... .. •. -. у.... ~ - у ?>£££ Листинг 7.2. Принципы инициализации и заполнения буфера вклассе ! DisplayList __........ ___________л.: ___di. .... .• ...... .......... 1://TinyPIM (c)1999,Pablo Halpern. Файл DisplayList.срр 2: 3:#ifdef _MSC_VER 4:#pragma warning(disable : 4786) 5:#endif 6: 7:#include <cassert> 8:#include <iostream> 9:#include <iomanip> 10:#include <algorithm> ll:#include <iterator> 12: 13:#ifdef _MSC_VER 14:#define min _cpp_min 15:#define max _cpp_max 16:#endif 17: 18:#include "DisplayList.h" 178 Глава 7. Прокручивание экранных списков...
19: 20:// Конструктор устанавливает размер экрана. 21:DisplayList::DisplayList(int linesPerScreen) 22: :linesPerScreen_(linesPerScreen) 23: { 24: reset (); 25:} 26: 27:// Деструктору досталось немного работы. 28:DisplayList::-DisplayList(){} 29: 30:// Очистка всех данных 31:void DisplayList::reset () 32: { 33: cache_.clear(); 34: cachedFirst_ = false; 35: cachedLast— = false; 36: firstvisibleldx_ = 0; 37: } 38: 39:// Добавление элементов в конец буфера. 40:// Задается начальный индекс и число добавляемых записей. Если 41:// записей меньше, чем затребовано, устанавливается флаг // cachedLast_. 42:void DisplayList::fillCacheFwd(int start, int numNeeded) 43: { 44: int startld = 0; 45: if (cache_.empty()) 46: // Запуск буферизации с начала списка 47: cachedFirst_ = true; 48: else 49: { 50: // Определяется последний элемент буфера 51: assert(start < cache_.size()); 52: startld = cache_.back(); 53: } 54: 55: int recordsTillEnd = cache_.size() - start; 56: if (! cachedLast_ && recordsTillEnd < numNeeded) 57: { 58: // В буфере не хватает записей для вывода на экран. Нужно // возвратить дополнительную порцию. 59: 60: std::vector<int> moreRecords; 61: cachedLast__ = fetchMore(startld, numNeeded - recordsTillEnd, 62: moreRecords); 63: 64: std::copy(moreRecords.begin(), moreRecords.end(), 65: std::back_inserter(cache_)); 66: } 67: } 68: Конструктор, деструктор и функция reset работают достаточно прямолинейно. Во время инициализации экранного списка буфер остается пустым. На этом этапе в нем пока нет ни первого, ни последнего элементов. Прокручивание списка 179
Функция fillCacheFwd, начинающаяся в строке42, принимает два аргумента. Первый аргумент задает точку отсчета в буфере, а второй сообщает число элементов, которые нужно показать. Если в буфере меньше записей, чем нужно отобразить, не- достающие записи возвращаются из ядра программы. Заполнение пустого буфера равносильно добавлению записей в начало буфера. Следовательно, в строке 47 пере- менной cacheFirst присваивается значение true. Отслеживание ошибок с помощью макроса assert В строке 51 используется макрос assert для проверки, не превышает ли индекс start наибольшее значение индекса в контейнере deque. Макрос assert использу- ется для предотвращения появления ошибок. Он проверяет истинность условия, ко- торое обязательно должно выполняться для правильной работы программы. Этот макрос принимает один аргумент и прерывает выполнение программы, если в него будет передано значение false. Это событие называется ложным утверждением (failed assertion) и означает, что в ходе выполнения программы произошла какая-то ошибка, приведшая к тому, что требуемое логическое условие перестало быть истин- ным. В строке 51 мы проверяем, входит ли индекс в допустимый диапазон. Если в результате ошибки индекс вышел за пределы допустимого диапазона, то лучше контролируемо прервать программу, чем дожидаться, что возникнет ситуация неоп- ределенности. связанная с ошибкой индексации. Обратите внимание, что assert— это макрос, а не функция. Как вы помните, с макросами не используется идентификатор стандартного пространства имен std: :. Макрос assert обычно определяется условно, т.е. в случае отключения про- граммы отладки во время компиляции кода он будет преобразован в пустую строку. Это дает дополнительную гибкость программистам, так как появляется возможность неограниченно использовать макрос assert во время разработки программы, а пе- ред компиляцией окончательной версии отключить одним действием все обращения к условным макросам, чтобы облегчить код программы. Но даже в случае отключе- ния использования макросов присутствие обращений assert в исходном программ- ном коде облегчит документацию приложения, так как явно укажет краеугольные условия правильного выполнения программы. Поскольку макрос assert можно удалить из программного кода во время компиляции, !Совет! важн0 проследить, чтобы отмена выполнения выражений проверки условий не повлекла за . jL Лсобой п°б°чнь|е эффекты. Иначе работа программы будет зависеть от используемой сис- темы отладки или от ее отключения. Например, никогда нельзя записывать выражение, по- добное assert (i++), поскольку значение 1 будет приращиваться только при включен- ном режиме отладки. Макрос assert унаследован из языка С и определен в библиотечном файле заго- ловка <cassert> (добавлен в код программы в строке 7). Заполнение контейнера deque с помощью алгоритмов сору И back__inserter Продолжим анализ листинга 7.2. В строке 56 мы проверяем, достаточно ли запи- сей в контейнере deque, чтобы возвратить запрашиваемое число записей, — numNeeded. Если записей недостаточно, в строке 61 вызывается функция 180 Глава 7. Прокручивание экранных списков...
fetchMore, которая запрашивает из ядра программы новую порцию недостающих записей. В строке 64 значения ID записей вектора moreRecords, возвращенного функцией fetchMore, копируются в буфер. Для этого используется алгоритм сору, который принимает три итератора в качестве аргументов. Первые два из них задают диапазон итераторов, определяющий ряд элементов для копирования. Третий ите- ратор определяет место вставки новой порции записей. Забудем на время об алго- ритме back inserter и представим, что строки 64 и 65 выглядят так: 64: std::copy(moreRecords.begin(), moreRecords.end(), 65: cache_.begin()); Первые два аргумента задают диапазон итераторов, включающий все записи век- тора moreRecords. Третий аргумент, cache_. begin (), указывает, что новые записи должны быть добавлены в начало буфера. Алгоритм сору отлично подходит для ко- пирования записей между контейнерами разных типов, поскольку допускает, чтобы третий итератор отличался по типу от первых двух. Размер диапазона итераторов в данном и подобных случаях целиком определяет- ся числом элементов в исходном контейнере. Так, если вектор moreRecords содер- жит 12 элементов, то показанные выше инструкции скопируют все 12 элементов в первые 12 позиций контейнера cache_, заменив их значения новыми. Постойте, речь идет о замене значений? Да, это так, целью алгоритма сору явля- ется диапазон существующих элементов, значения которых следует изменить. Но это совсем не то, что нам нужно. Мы хотим добавить элементы из moreRecords в cache_. Положение может измениться, если в третьем аргументе будет указан ко- нец буфера, а не начало: 64: std::copy(moreRecords.begin(), moreRecords.end(), 65: cache_.end()); Это довольно распространенная ошибка начинающих программистов. Данный код пытается заменить элементы буфера cache_ начиная с позиции cache_. end (). Но за cache , end () уже нет никаких элементов, в результате чего возникает ситуа- ция неопределенности. Для алгоритма сору аргумент cache_.end() не более чем обычный итератор. Алгоритм не может самостоятельно определить, указывает ли этот итератор в начало, середину или конец целевого контейнера. Поэтому возвра- тимся опять к той версии кода, которая показана в листинге 7.2. В строке 65 вызы- вается функция back—inserter, в которую cache_ передается как аргумент. Функ- ция back—inserter возвращает итератор особого типа, который добавляет, а не за- мещает элементы целевого контейнера. Следующие две инструкции выполняются совершенно одинаково: cache_.push—back(х); *std::back—inserter(cache_) = x; Операции разыменования и присвоения с использованием алгоритма back—inserter равносильны вызову функции push_back для текущего контейнера. Передача функции back—inserter в алгоритм сору приводит к тому, что в целевой контейнер добавляются все элементы, заданные диапазоном итераторов. Пусть у вас в памяти отложится взаимосвязь алгоритмов сору и back_inserter, поскольку они очень часто используются вместе. Работа функции fillCacheFwd завершается добавлением в буфер идентифика- ционных номеров новой порции записей. В результате ее выполнения либо в буфере становится достаточно записей для выполнения запроса пользователя, либо пере- менной с ache La st_ присваивается значение true, сигнализирующее о том, что ис- ходный набор записей исчерпан. Прокручивание списка 181
Справочная информация: Экскурс копирование контейнеров Стандартная библиотека предоставляет несколько возможностей копи- рования контейнеров. Простейший состоит в инициализации с копи- рованием или в использовании оператора присваивания. Например, предположим, что у нас есть контейнер х типа std: :set<Address>. Копию контейнера х можно создать в ходе инициализации: std::set<Address> z(x); или с помощью оператора присваивания: У = х; • В результате выполнения этих инструкций будут получены два контей- нера z и у, содержащие копии всех элементов контейнера х. Все эле- менты, ранее содержащиеся в у, удаляются. Другой подход состоит в использовании алгоритма сору, как мы видели это в строках 64, 65 листинга 7.2. Алгоритм сору можно использовать как для замены элементов целевого контейнера, так и для добавления в него новых элементов (с помощью алгоритма back inserter). Дан- ный способ копирования удобен, когда нужно копировать не весь кон- тейнер, а только некоторый диапазон элементов, когда нужно добавить элементы в другой контейнер, не изменяя его исходные элементы, или когда исходный и целевой контейнеры различаются по типу (например, в случае копирования элементов из вектора в двухстороннюю очередь). Какой-бы метод копирования вы ни избрали, особую осторожность сле- дует проявлять, если исходный контейнер содержит указатели. Убеди- тесь, что копируются только указатели, а не связанные с ними объекты. В результате будут получены два контейнера, ссылающиеся на одни и те же объекты. Добавление записей в начало буфера с помощью обратных итераторов Зеркальным отражением функции fillCacheFwd является f illCacheBkwd, кото- рая проверяет, достаточно ли записей в начале буфера, чтобы прокрутить записи назад на один экранный список. В основу положен тот же принцип, что и для функ- ции fillCacheFwd, но выполнить добавление элементов в начало контейнера, не на- рушая их порядок следования, в действительности не так просто. Чтобы лучше разо- браться в проблеме, представим себе колоду нумерованных карт, сложенных в по- рядке возрастания номеров снизу вверх. (Контейнер двухсторонняя очередь вообще во многом напоминает колоду карт.) Если мы будем добавлять новые карты вверх колоды в порядке возрастания номеров, то общий порядок следования карт сохра- нится. Но если добавлять те же карты по одной вниз колоды, то их порядок следова- ния изменится на противоположный (убывающий). Проанализируем решение этой проблемы, показанное в листинге 7.3. 182 Глава 7. Прокручивание экранных списков...
Листинг 7.3. Реализация функции fillCacheBkwd 69:// Добавление элементов в начало буфера. 70:// Задаются начальный индекс и число добавляемых записей. Если 71:// записей меньше, чем затребовано, устанавливается флаг cachedLast_. 72:void DisplayList::fillCacheBkwd(int start, int numNeeded) 73: { 74: int startld = 0; 75: if (cache_.empty()) 76: // Запуск буферизации с конца списка 77: cachedLast_ = true; 78: else 79: { 80: // Определяется первый элемент буфера 81: assert(start < cache_.size()); 82: startld = cache_.front(); 83: } 84: 85: int recordsTillStart = start; 86: if (! cachedFirst_ && recordsTillStart < numNeeded) 87: { 88: // В буфере не хватает записей для вывода на экран. // Нужно возвратить дополнительную порцию. 89: 90: std::vector<int> moreRecords; 91: cachedFirst— = fetchMore(startld, 92: - (numNeeded - recordsTillStart), 93: moreRecords); 94: 95: std::copy(moreRecords.rbegin(), moreRecords.rend(), 96: std::front_inserter(cache_)); 97: 98: // Новые элементы вводятся перед первым элементом буфера. 99: // После ввода нужно обновить переменную // firstVisible!dx_to, чтобы она указывала на первый // элемент. 100: firstVisibleIdx_ += moreRecords.size(); 101: } 102: } 103: Функция fillCacheBkwd начинается так же, как и функция fillCacheFwd, за исключением замены литералов cachedFirst_ на cachedLast_ и back () на front () в строках 77, 82 и 86. В строках 91-93 вызывается функция fetchMore, но в этот раз с негативным значением числа возвращаемых записей. Это означает, что записи следует брать до индекса startld, а не после него. После перехода программы к стро- ке 95 в векторе moreRecords уже содержатся идентификационные номера возвра- щенных записей в порядке их возрастания. Теперь нужно скопировать эти иденти- фикационные номера в начало буфера cache_. В строке 96 вы видите функцию front inserter, которая, как вы могли уже догадаться, работает так же, как и back_inserter, но добавляет элементы в начало контейнера (аналогично выпол- нению функции pushf ront). В стандартной библиотеке, помимо front_inserter и inserter, есть еще функция inserter, которая принимает два аргумента: контейнер и итератор, ука- зывающий на точку ввода. Разыменование итератора приводит к вводу элементов Прокручивание списка 183
в указанную точку. Все три функции определены в файле заголовка <iterator>, который в листинге 7.2 включается в код программы в строке 11. Функцию back inserter можно использовать со всеми контейнерами, которые поддержи- вают выполнение функции push back, т.е. с вектором, списком и двухсторонней очередью. Функцию f ront inserter можно использовать со всеми контейнерами, которые поддерживают выполнение функции push_front, т.е. со списком и двух- сторонней очередью, но не с вектором. Функция inserter применима ко всем кон- тейнерам, включая ассоциативные. Хотя установка точки ввода теряет смысл в контейнерах, основанных на собственном алгоритме упорядочивания элементов, в ассоциативных контейнерах этот аргумент все же используется для ускорения поиска позиций для новых элементов. Если мы просто используем конструкцию алгоритма сору и функции f ront_inserter для копирования элементов из вектора moreRecords в начало буфера cache_, полу- чим инвертированную последовательность элементов, как это было на примере с ко- лодой карт. Нам нужно начать копирование элементов с конца вектора. Эту задачу можно решить с помощью обратного итератора. Обратный итератор— это вариант обычного итератора, который следует по элементам контейнера в обратном порядке, т.е. приращение обратного итератора переводит его на следующий элемент, который находится ближе к началу контейне- ра. Все контейнеры поддерживают тип reverse—iterator и функции rbegin и rend, которые соответственно возвращают обратный итератор на последний элемент и на значение, предшествующее первому элементу. Отношения между обычным и обрат- ным итераторами показаны на рис. 7.3. Прямой (нормальный) итератор £ begin)) -Приращение—► end() rend() —Приращение— rbegin()— Обратный итератор Рис. 7.3. Обычный и обратный итераторы Обычный итератор можно преобразовать в обратный с помощью явного при- ведения типа. В результате такого преобразования получается итератор, указы- вающий на элемент, предшествующий тому, на который указывал исходный обычный итератор. В связи с этим два следующих выражения будут выполнять- ся совершенно одинаково: mylist.rbegin() std::list<int>::reverse_iterator(mylist.end()) Из эквивалентности этих выражений мы можем заключить, что между обычным (прямым) и обратным итераторами существует четкая взаимосвязь, включая итера- торы, возвращаемые функциями end () и rend (). Также верно утверждение, что для двух итераторов а и b диапазон [а, Ь) будет точно соответствовать диапазону [reverse_iterator (b) r reverse_iteratdr (а) ). Обратный итератор можно вновь конвертировать в прямой с помощью функции-члена base. Поэтому равнозначными будут два следующих выражения: mylist.begin() mylist.rend().base() 184 Глава 7. Прокручивание экранных списков...
Осталось еще заметить, что обратный итератор также может быть констант- ным. Для этого в каждом контейнере определен специальный тип const_reverse__iterator. Использование обратного итератора показано в строке 95 листинга 7.3. Благодаря копированию элементов вектора с конца к началу с помощью обрат- ных итераторов rbegin и rend мы преодолеваем эффект инвертирования последовательности элементов при выполнении функции f rontinserter. Для алгоритма сору и всех других обратные итераторы ничем не отличаются от обычных. Благодаря этому с помощью обратных итераторов можно инвер- тировать последовательность элементов в контейнере при выполнении любо- го алгоритма. Еще один эффект ввода элементов в начало буфера состоит в необходимости из- менения индексации элементов. Если текущий первый видимый элемент имел ин- декс 5, то после добавления 10 записей в начало буфера его индекс станет 15. Чтобы скорректировать индексацию, в строке 100 переменная f irstVisible!dx_ увели- чивается на число добавленных элементов. Форматирование с помощью манипуляторов ввода-вывода Мы уже готовы к тому, чтобы вывести на экран выборку записей. Код реализации этой операции показан в листинге 7.4. Листинг 7.4. Функция display 104:void DisplayList::display() 105: { 106: // Заполнение буфера первым экранным списком 107: fillCacheFwd(firstVisibleIdx_, linesPerScreen_); 108: 109: // Если буфер остался пустым после попытки его заполнения, 110: // значит, записи для показа отсутствуют. Ill: if (cache_.empty()) 112: { 113: // Сообщение об отсутствии записей для показа. 114: std::cout « "============= No records selected ===============" 115: << std::endl; 116: return; 117: } 118: 119:// Вычисляет число записей для показа. 120:// Число выводимых записей может быть меньше стандартного, 121:// если отображается остаток в конце буфера. 122:int recsToShow = std::min(linesPerScreen_, 123: int(cache_.size() - firstVisible!dx_)); 124: 125:if (atStart ()) 126: // Сообщение о достижении начала списка. 127: std::cout « ”=============== Start of list ===============\n"; 128: 129:std::deque<int>::iterator start = cache_. begin ()+firstVisibleIdx__; 130:std::deque<int>::iterator finish = start + recsToShow; Прокручивание списка 185
131:for (std::deque<int>::iterator i = start; i != finish; ++i) 132: { 133: // Показ номера строки 134: int lineNum =i - start + 1; // Начало отсчета с 1 135: std::cout << std::setw(2) « std::setfill(’ ’) 136: « std::right « std::dec « lineNum « 137: displayRecord(*i); 138: std::cout << std::endl; 139: } 140: 141:if (atEndO) 142: // Сообщение о достижении конца списка. 143: std::cout << "=============== End of list ==============="; 144: 145: std::cout « std::endl; 146: } 147: Реализация функции display начинается с вызова функции f illCacheFwd, что- бы убедиться, что в буфере достаточно записей, начиная с f irstvisibleldx_, для заполнения текущего экранного списка. Если после выполнения функции f illCacheFwd буфер остался пустым, это говорит об отсутствии затребованных за- писей в исходном списке. В таком случае в строке 114 будет показано предупреж- дающее сообщение, а в строке 116 выполнение функции завершится. В строках 122, 123 подсчитывается число выводимых записей. Как правило, выво- дится заданное число записей linesPerScreen_. Но может случиться так, что функция f illCacheFwd возвратит оставшиеся записи из исходного контейнера и их не хватит для заполнения экранного списка, т.е. в буфере останется меньше записей, чем определено в linesPerScreen_. Для определения реального числа выводимых записей эти значения сравниваются с помощью функции min, определенной в файле заголовка <algorithm>. Особенности компиляции. В стандартной библиотеке, распростра- За метку няемой вместе с компилятором Microsoft VC 6.0, используются литералы _cpp_min вместо min и _срр_тах— вместо max, чтобы избежать кон- фликтов имен с литералами файла заголовка windef . h, относящегося к системе Windows. Поскольку мы не используем заголовок windef. h, то можем позволить себе сделать наш код более читабельным, переопреде- лив имена функций, как это сделано в строках 13-16 листинга 7.2. Чтобы сделать этот код корректным в программе, разрабатываемой для Windows, добавьте заголовок windef. h после директивы #def ines и вве- дите следующую инструкцию: using std::min; using std::max;. Обратите внимание, что в функцию min можно передавать аргументы только од- ного типа. Нельзя, например, сравнивать значения int и long. В противном случае компилятор не сможет выбрать правильный тип для реализации шаблона функции min и покажет сообщение об ошибке. Вот почему в строке 123 нам пришлось явно привести второй аргумент к типу int. В строках 129, 130 задается диапазон итераторов, определяющий записи для вывода на печать. Значение итератора start рассчитывается путем суммирова- ния индекса первого видимого элемента со значением итератора на первый эле- мент в буфере. Затем, для определения итератора finish, к полученному значе- нию start прибавляется значение recsToShow. В данном случае арифметические действия с итераторами выполняются так же, как и с указателями. Но будьте осто- рожны, это справедливо только для контейнеров, открывающих произвольный 186 Глава 7. Прокручивание экранных списков...
доступ к своим элементам. (Более подробно об этом — во врезке “Справочная ин- формация: категории итераторов” далее в этой главе.) В строке 131 начинается цикл for, основанный на итераторах. С его помощью обрабатывается диапазон записей, заданный итераторами [start, finish). В строке 134 вычисляется но- мер строки для записи, заданной итератором i. Нумерация строк начинается с единицы. Это значение присваивается строке, содержащей первую видимую за- пись. Таким образом, чтобы определить номер текущей строки, достаточно отнять st rat от i и прибавить 1. Такое использование итераторов для расчета порядково- го номера строки напоминает вычисление с указателями и также ограничено кон- тейнерами с произвольным доступом к элементам. Справочная информация: категории Экскурс итераторов^________________________________________________ Итераторы разных типов не похожи друг на друга. По функциональным возможностям выделяют пять категорий стандартных итераторов. Итераторы высшей категории обладают всеми функциональными воз- можностями низших категорий и при этом располагают рядом допол- нительных возможностей. Всюду, где допускается использование менее функциональных итераторов, можно использовать и более функцио- нальные. Отношения между более функциональными и менее функ- циональными итераторами такие же, как при наследовании классов, хотя в действительности говорить о наследовании категорий итерато- ров неправомочно. Наименее функциональные итераторы — это итераторы ввода и выво- да. Итератор ввода открывает односторонний доступ для чтения к эле- ментам контейнера. После возвращения объекта для чтения состояние итератора или контейнера изменяется таким образом, что вы уже не можете повторно возвратить тот же объект (отсюда односторонность доступа). С итератором ввода нельзя выполнять обратное приращение и операции сравнения. Нам еще не приходилось иметь дело с итерато- ром ввода. Этот итератор часто используется для считывания данных из потока ввода. Итератор вывода похож по своим свойствам на итератор ввода, но от- личается тем, что открывает односторонний доступ для записи элемен- тов. К элементу нельзя обратиться дважды с помощью одного и того же итератора вывода. Итераторы вывода создаются при вызове функций inserter для ввода элемента в контейнер, но его нельзя использовать для чтения только что введенного элемента. Прямой итератор напоминает указатель на элементы контейнера. Хотя с прямым итератором также нельзя выполнять обратное приращение, можно копировать его и использовать для повторного обращения к од- ному и тому же элементу. С помощью этого итератора можно открывать доступ к элементу как для чтения, так и для записи, поэтому его можно использовать при вводе данных в контейнер и для возвращения эле- ментов контейнера. Хотя работу контейнера вполне можно организо- вать на основе только прямых итераторов, все стандартные библиотеч- ные контейнеры базируются на использовании наиболее функцио- нальных итераторов. Прокручивание списка 187
Двухсторонний итератор выполняет все функции прямого итератора, но, кроме того, к нему можно применять оператор декремента (—). Тип iterator, задаваемый во всех стандартных контейнерах, кроме vector и deque, является двухсторонним. Наибольшей функциональностью обладает итератор произвольного доступа. Итераторы этой категории по своим свойствам в наибольшей степени напоминают указатели на элементы массивов. Помимо всех функций, свойственных двухсторонним итераторам, с итераторами произвольного доступа можно выполнять арифметические действия (+, += и -=), где они выступают как целые числа. Эти итераторы мож- но вычитать друг из друга и сравнивать с помощью операторов отно- шений <, >, <= и >=, чтобы определить очередность их следования. С итераторами произвольного доступа также можно использовать опе- ратор индексирования ([ ]). Предположим, что у нас есть итератор i. Тогда выражение i [п] будет соответствовать выражению *(i+n). Ите- раторы произвольного доступа используются в стандартных контейне- рах vector и deque. Указатели на объекты по своим свойствам в точно- сти соответствуют итераторам произвольного доступа. Поскольку к итераторам большинства категорий нельзя прибавлять целые числа для перевода их на несколько позиций вперед, в стандарт- ном заголовке <iterator> определена функция advance (i, n), кото- рая приращивает значение итератора i на п единиц. Для итераторов, не поддерживающих произвольный доступ, эта функция выполняет операцию инкремента п раз. Для двухсторонних итераторов п может быть отрицательным, что приводит к обратному приращению итерато- ра. Другая полезная операция, не поддерживаемая большинством ите- раторов, — вычитание одного итератора из другого, чтобы узнать ко- личество элементов между ними. В <iterator> для этой цели опреде- лена функция distance(il, i2), которая определяет, сколько раз нужно прирастить итератор il, чтобы сделать его равным итератору i2. Чтобы избежать ситуации неопределенности, итератор i2 всегда должен быть большем i 1 и относиться к тому же контейнеру. Отображение записей на экране начинается в строке 135. Нам нужно выводить номера строк перед каждой записью, чтобы пользователь мог ориентироваться в записях по номерам. Для облегчения чтения нужно выровнять нумерацию строк, зарезервировав для номера два символа и заполняя пробелами неиспользуемые символы слева. Форматирование вывода строк осуществляется с помощью специ- альных функций, называемых манипуляторами ввода-вывода. Манипулятор setw устанавливает ширину следующего поля вывода. С помощью set fill можно за- дать символы заполнения неиспользуемого пространства до заданной ширины по- ля. Манипулятор right указывает, что символы заполнения должны находиться слева от значения поля (выравнивание вправо), и манипулятор dec устанавливает вывод числовых значений в десятичном формате (альтернативные опции: hex и octal). Установка этих четырех манипуляторов ввода-вывода эквивалента вы- зову следующих функций: std::cout.width(2); std::cout.fill(1 ’); std::setf(std::ios::right, std::ios::adjust!ield); std::setf(std::ios::dec, std::ios::basefield); 188 Глава 7. Прокручивание экранных списков...
Функции fill и width задают соответственно символ заполнения и ширину по- ля. Но манипуляторы использовать гораздо проще, поскольку они устанавливаются в одной строке с операторами вывода. Для манипуляторов, принимающих аргумен- ты (set fill и setw), в код программы необходимо добавить заголовок <iomanip>. За исключением функции width и соответствующего ей манипулятора setw, все ос- тальные функции и манипуляторы форматирования вывода оказывают влияние на все последующие операции, производимые с текущим потоком вывода. Ширина поля, задаваемая с помощью width или setw, является минимально до- пустимым значением. Если текущее значение поля будет меньше установленной ши- рины, то лишние позиции будут заполнены символами заполнения (по умолчанию — пробелами). Но если текущее значение окажется больше установленной ширины, то программа не станет обрезать его, а выведет как есть. Функция width и манипулятор setw оказывают влияние только на следующее выводимое поле. Так, в строках 135, 136 только ширина lineNum регулируется манипулятором setw. После вывода значения lineNum автоматически выполняется функция width (0), отменяющая заполнение по- лей до определенного размера. Хотя следующая выводимая стока " : " также состоит из двух символов, это простое совпадение, а не результат установки манипулятора setw (2). (Для меня отыскать в стандартной документации пункт о временном харак- тере установок функции width было равносильно поиску иголки в стогу сена.) На Особенности компиляции. В стандартной библиотеке, распростра- заметку няемой с компилятором egcs 1.1.2, допущена ошибка в классе string, в результате чего при выводе строк игнорируются установки ширины поля. Устраним эту проблему на следующем примере со строковым объ- ектом s и потоком вывода st rm. Вместо strm « std::setw(n) « s.c_str(); следует ввести strm << std::setw(n) << s; Либо вы можете исправить ошибку в классе string и отправить ис- правленный вариант разработчикам компилятора egcs. Если вы уже работали с языком С и осуществляли форматированный вывод с по- мощью функции printf, то описанные методы форматирования могут показаться вам несколько громоздкими. Однако использование манипуляторов и функций фор- матирования позволит вам избежать многих ошибок несоответствия типов, свойст- венных функции printf. В функции printf вывод строки текста нельзя просто так заменить на числовую переменную, тогда как выполнение манипуляторов формати- рования независимо от типа данных. (Ошибки несоответствия типов в функции сложно выявить и они могут привести даже к зависанию программы.) После вывода порядкового номера записи в строке 137 вызывается функция displayRecord, ответственная за формирование краткого резюме для вывода на эк- ран. Реализация функции displayRecord задается не в базовом классе DisplayList, а в производных классах, так как записи адресной книги и книги контактов выводятся по-разному. После завершения цикла проверяется, не был ли достигнут конец основно- го списка записей. Если в исходном списке записей больше не осталось, на экран выво- дится сообщение дня пользователя (строки 141-143). И в строке 145 вводится символ разрыва строки. Обратите внимание, что endl — это тоже манипулятор форматиро- вания, который можно было бы заменить следующей парой инструкций: std::cout.put('\n'); std::cout.flush(); Прокручивание списка 189
Функция put направляет символы в поток вывода без какого-либо дополнительного форматирования. Поскольку поток вывода обычно буферизует часть выводимых дан- ных в оперативной памяти, функция flush необходима для того, чтобы экстренно от- править данные, оставшиеся в буфере, на текущее устройство вывода. Манипулятор endl автоматически выполняет вывод символа разрыва строки и очистку буфера, бла- годаря чему код программы становится более компактным. Хотя принудительная очи- стка буфера— это не обязательное условие для вывода данных на печать, некоторые устройства вывода не работают до тех пор, пока не будет вызвана функция flush. Это следует учитывать при поиске причин неправильной работы программы во время ее отладки. В потоках cout очистка буфера происходит автоматически при первом вызо- ве в программе оператора cin, поэтому использование стандартных потоков ввода- вывода повысит ошибкоустойчивость вашего приложения. т Термин buffer переводится на русский язык так же, как и термин cache, с ко- I ерм И Н ТОрым мы встретились при буферизации экранных списков. В обоих случа- ях речь идет об области памяти, выделенной д ля хранения данных. Но если область cache выделяется д ля хранения данных до завершения работы про- граммы, то под buffer понимают временно выделяемую область д ля накоп- ления данных в процессе их следования до конечного пункта назначения. Обычно буферизацию данных потоков используют для повышения эффек- тивности работы программы. Так, для компьютера эффективнее будет пе- реслать на устройство вывода сразу весь блок данных из буфера, чем обра- батывать каждый отдельный символ, направляемый программой. Теперь мы имеем возможность дополнять буферизованный список записей с кон- ца и с начала и выводить на печать экранные списки, в которых каждая запись будет представлена пронумерованным резюме. Завершив реализацию основного ядра класса DisplayList, повысим его эффективность, доработав некоторые дополни- тельные функции. Реализация дополнительных функций класса DisplayList Реализация оставшихся функций класса DisplayList, за исключением функции selectRecord, показано в листинге 7.5. Листинг 7.5. Функции класса DisplayList, не требующие ввода данных пользователем 148:void 149: { DisplayList::pageDown() 150: // Прокрутка текущей последней записи вверх следующего // экранного списка 151: if (atEnd()) 152: 153: 154: 155: 156: return; // Буферизация текущего и следующего экранных списков fillCacheFwd(firstvisibleldx_, 2 * linesPerScreen_); 157: // Приращение индекса первой видимой записи на один экранный 190 Глава 7. Прокручивание экранных списков...
// список, 158: // но только в том случае, если существует следующий экранный // список. 159: if (! atEnd()) 160: firstVisible!dx_ += linesPerScreen_; 161: } 162: 163:void DisplayList::pageUp() 164: { 165: // Прокрутка текущей первой записи вниз следующего экранного // списка 166: if (atStart()) 167: return; 168: 169: // Буферизация предыдущего экранного списка 170: fillCacheBkwd(firstVisible!dx__, linesPerScreen_); 171: 172: // Приращение индекса первой видимой записи на один экранный // список назад, 173: // но не за пределы первой записи буфера. 174: firstVisible!dx_ = std::max(firstVisible!dx_ - linesPerScreen_, 0); 175: } 176: 177:void DisplayList::toStart() 178: { 179: if (cachedFirst__) 180: firstVisibleIdx_ = 0; 181: else 182: // Буферизованные записи не содержат первой записи спи- ска . 183: reset (); 184: } 185: 186:bool DisplayList::atStart() 187: { 188: return cachedFirst_ && (firstvisibleldx__ == 0); 189: } 190: 191:bool DisplayList::atEnd() 192: { 193: return (cachedLast_ && 194: (cache__.size() - firstVisibleIdx_ <= linesPerScreen_)); 195: } 196: 197:// Установка указанной записи в начало экранного списка. 198:void DisplayList::scrollToTop(int recordld) 199: { 200: assert(recordld ! = 0); 201: 202:// Поиск заданной записи в буфере: 203:cache__t::iterator found = std::find(cache_.begin(), cache_.end(), 204: recordld); 205: 206:// Если запись в буфере не обнаружена, буфер очищается //и перезагружается из ядра программы. 207:if (found == cache .end()) Прокручивание списка 191
208: { 209: reset(); 210: cache__.push_back (recordld) ; 211: firstvisibleldx_ = 0; 212: } 213:else 214: firstvisibleldx_ = found - cache_.begin(); 215: 216: fillCacheFwd (f irstvisibleldx__, linesPerScreen_) ; 217: } 218: 219://Возвращает ID записи, выведенной на экран под номером п 220:int DisplayList::screenRecord(int n) const 221: { 222: if (f irstvisibleldx_ + n >= cache__. size () ) 223: return 0; 224: else 225: return cache_[firstvisibleldx_ + n]; 226: } 227 : Функция PageDown в строках 148-161 проверяет наличие по крайней мере двух экранных списков записей в буфере (текущего и следующего). Для этого в строке 155 вызывается функция fillCacheFwd. В строке 160 значение переменной firstvisibleldx приращивается на размер одного экранного списка, если только текущий список не является последним. (Поскольку пользователь продолжает работу с программой, функция etEnd может возвратить true после вызова fillCacheFwd, даже если перед этим она возвращала false.) Функция pageUp в строках 163-175 вызывает функцию f illCacheBkwd, чтобы проверить наличие записей в буфере, достаточных для формирования экранного списка, предшествующего текущему. В строке 174 от текущего значения переменной firstvisibleldx отнимается число строк в экранном списке. Функция min предупреждает присвоение переменной firstvisibleldx отрицательного значения, если оставшихся записей не хватит на формирование полного экранного списка. Функция toStart (строки 177-184) проверяет, находится ли в буфере первая за- пись исходного списка (в этом случае логическая переменная cachedFirst_ уста- новлена на true). Если первая запись обнаружена в буфере, ее индекс присваивается переменной firstvisibleldx, в результате чего она становится первой записью те- кущего экранного списка. В противном случае функция очищает буфер и возвраща- ет из ядра программы новый экранный список, в котором первая строка является первой записью базового списка. Функция at Start (строки 186-189) возвращает true, если первая запись базово- го списка представлена в текущем экранном списке. Для этого в строке 188 прове- ряются два условия: находится ли первая запись базового списка в буфере и являет- ся ли она первой видимой записью. Аналогичная функция atEnd (строки 191-195) возвращает true, если последняя запись базового списка выведена в данный момент на экран. Для выяснения этого определяется присутствие последней записи в буфере и проверяется условие, что между первой видимой записью и последней записью ба- зового списка заключено меньше элементов, чем в экранном списке. Функция scrollToTop (строки 198-217) выполняется несколько сложнее. Ее ра- бота состоит в прокручивании экранного списка таким образом, чтобы указанная запись стала первой видимой записью (находилась вверху экранного списка). В строке 200 с помощью макроса assert проверяется условие, что пользователь не 192 Глава 7. Прокручивание экранных списков...
ввел недопустимое нулевое значение для прокручивания списка. В строке 203 ис- пользуется алгоритм find, с помощью которого проверяется наличие требуемой за- писи в буфере. Алгоритм find возвращает итератор на объект в буфере, содержащий идентификационный номер, заданный в recordld. В строке 207 проверяется усло- вие возвращения алгоритмом find значения cache, end (), что означает отсутствие в буфере элемента с заданным ID. В этом случае программа не знает, следует ли лис- тать список вперед или назад, поэтому буфер очищается, а из основного списка воз- вращается новый экранный список, первый элемент которого содержит заданный ID. Для этого в строке 216 вызывается функция f illCacheFwd. Если же искомая за- пись обнаруживается в буфере, она становится первой видимой в новом экранном списке. Для этого в строке 214 вычисляется сдвиг (индекс) найденной записи от на- чала буфера, и это значение присваивается переменной f irstvisibleldx_. Справочная информация: соглашения Экскурс именования алгоритмов^___________________________________ Нам уже приходилось работать с парами связанных алгоритмов count и count_if, find и f ind__if. Эти названия были приняты в соответствии с общими соглашениями именования алгоритмов, разработанными для того, чтобы облегчить ориентацию пользователей среди более чем 60-ти различных алгоритмов стандартной библиотеки C++. Алгоритмы с име- нами, заканчивающимися на __if, используют для обработки элементов заданный пользователем одинарный предикат, тогда как их тезки без if сравнивают элементы с заданными значениями. Например, алгоритм count подсчитывает число элементов в заданном диапазоне итераторов, у которых значение определенного поля совпадает с заданным. Алгоритм count_if подсчитывает число элементов, для которых установленный предикат возвращает true. Таким образом, если вы знаете, как работает алгоритм count, то, следуя принятому соглашению, сможете предста- вить, как должен работать алгоритм count_if. Некоторые алгоритмы изменяют элементы в заданном диапазоне ите- раторов. Многие из этих алгоритмов имеют варианты с именами, за- канчивающимися на _сору. Данные версии алгоритмов не изменяют исходные элементы, а копируют их в целевой контейнер. Например, ал- горитм replace принимает в числе аргументов диапазон итераторов и два значения. При обнаружении в элементах диапазона первого зна- чения он заменяет его на второе. Алгоритм replace__copy, помимо диа- пазона итераторов, принимает еще итератор, задающий точку вставки в другом контейнере. В результате его выполнения из заданного диапа- зона выбираются элементы, содержащие первое значение, которое за- меняется на значение второго аргумента, и измененный элемент вставляется по месту, указанному итератором точки ввода. При этом происходит замещение элементов в целевом контейнере. Если же вы хотите не замещать, а добавлять элементы, не забудьте использовать одну из функций inserter. Следуя принятым соглашениям, попробуй- те разобраться сами, как работает алгоритм replace_copy_if. Другая категория соглашений относится не к работе алгоритмов, а к типу ожидаемых аргументов. Большинство алгоритмов прини- мает в числе аргументов диапазоны итераторов или объекты функ- Прокручивание списка 193
ций предикатов. В документации на эти алгоритмы описываются ожидаемые типы параметров. Чтобы пользователю легче было разо- браться, аргумент какого типа следует передать в шаблон алгорит- ма, приняты следующие соглашения имен параметров, определяю- щих их тип: Ini ter: итератор ввода или более высокой категории; Out Iter: итератор вывода или более высокой категории; Fwdlter: прямой итератор или более высокой категории; Bidi г Iter: двухсторонний итератор или более высокой категории; Randi ter: только итератор произвольного доступа: Comp: бинарный предикат, выполняющий сравнение меньше чем; Eq: бинарный предикат, выполняющий сравнение равенства; BinPred: бинарный предикат; UniPred: одинарный предикат; В in Func: бинарный объект функции (не обязательно предикат); Uni Func: одинарный объект функции (не обязательно предикат); Т: элемент любого типа. Например, в документации дано следующее описание алгоритма replace_if: template <class Fwdlter, class UniPred, class T> Outlter replace_if(Fwdlter start, Fwdlter finish, UniPred pred, const T& new_value); Примите к сведению, что соглашения именования параметров не яв- ляются частью стандартов языка C++, поэтому могут варьироваться в дот^гментациях разных разработчиков. Так, в документации по стан- дартизации ISO используются более длинные имена параметров, как, например, Forwarditerator вместо Fwdlter. Функция screenRecord (строки 220-226) возвращает идентификацион- ный номер записи, отображенной на экране под номером п (отсчет п начи- нается с единицы, а не с нуля). В строке 222 проверяется, присутствует ли стро- ка под номером п в буфере. Если нет, то в строке 223 возвращается 0. В нор- ме функция вычисляет индекс записи в буфере, прибавив п к значе- нию f irstvisibleldx__. Избегайте появления бессмысленных итераторов Вы, возможно, обратили внимание, что класс DisplayList в большей степени полагается на целочисленные значения индексов элементов в контейнере deque, то- гда как в классе адресной книги чаще использовались итераторы на элементы. Даже если итераторы используются, то они вычисляются по индексу элемента, а не по значению переменной-члена класса. Это делается для того, чтобы избежать появле- ния бессмысленных итераторов во время выполнения программы. При изменении элементов контейнера может получиться так, что элемента, на который ссылается итератор, уже не будет. Обращение к бессмысленному итератору вызовет ситуацию неопределенности. Поэтому при использовании итераторов следует учитывать мето- 194 Глава 7. Прокручивание экранных списков...
ды реорганизации элементов контейнера при сортировке, добавлении и удалении элементов. В целом принципы предупреждения появления бессмысленных указате- лей на объекты справедливы и для итераторов. Примите к сведению и запомните три следующих замечания, которые помогут вам избежать появления в программе бес- смысленных итераторов и связанных с ними проблем. 1. Функция erase всегда делает бессмысленным итератор, ссылающийся на уда- ленный элемент. 2. Любые операции добавления или удаления элементов в контейнерах vector и deque нарушают работу всех итераторов, связанных с элементами данных контейнеров. Итераторы контейнеров других типов значительно более ошибко- устойчивы, поскольку способны отслеживать изменения структуры контейнера. 3. Исключением из п. 2 является использование функции pop_back. Ее выпол- нение в контейнерах vector и deque может сделать бессмысленным только итератор, указывающий на удаляемый элемент. В классе DisplayList итераторы не сохраняются в переменных-членах, по- скольку они становились бы бессмысленными при каждом обновлении буфера. В отличие от итераторов, индексы элементов всегда будут указывать на реальные объекты, а при необходимости использования итераторов их легко можно вычис- лять по индексам. Считывание вводимых чисел и отслеживание ошибок Последняя функция класса DisplayList— selectRecord, которая поддержива- ет взаимодействие с пользователем и позволяет выбирать записи по номерам, под которыми они выведены на экран. Реализация функции selectRecord показана в листинге 7.6. Листинг 7.6. Функция selectRecord 228:// Предложение пользователю ввести номер записи и преобразование // его в recordld 229:int DisplayList::selectRecord() 230: { 231: while (std::cin.good()) 232: { 233: int maxSelection = std::min(int(cache_.size() - firstVisible!dx__) r 234: linesPerScreen_); 235: 236: if (maxSelection <= 0) 237: { 238: std::cout « "No records to select \n"; 239: return 0; 240: } 241: 242: // Предложение ввести номер записи из выведенного диапазона 243: std::cout « "Choose a record number between " 244: « 1 « "and " « maxSelection Считывание вводимых чисел и отслеживание ошибок 195
245: « "\nRecord number (0 to cancel)? "; 246: 247: unsigned selection =0; 248: 249: std::cin » selection; 250: if (std::cin.fail()) 251: { 252: if (std::cin.bad() || std::cin.eof()) 253: break; 254: 255: // Исправимая ошибка ввода. (Пользователь ввел // нечисловое значение). 256: // Очевидная ошибка, введенная строка удаляется, 257: // а цикл продолжается. 258: std::cin.clear(); 259: std::cin.ignore(10000, ’\n’); 260: std::cout « "Invalid selection, please try again.\n\n"; 261: continue; 262: } // завершение ситуации else 263: 264: // Сброс оставшейся части введенной строки 265: std::cin.ignore(10000, ’\n’); 266: 267: if (selection == 0) 268: return 0; 269: 270: // Проверка, входит ли выбранный номер в диапазон // отображенных строк. 271: if (1 <= selection && selection <== maxSelection) 272: return cache_[firstvisibleldx_ + selection - 1]; 273: else 274: std::cout « "Invalid selection, please try again.\n\n' 275: 276: } // конец цикла while 277: 278: return 0; 279: } В строке 231 начинается цикл ввода, который продолжается до тех пор, пока не будет осуществлен успешный ввод (функция good () возвратит true). В строках 233, 234 вычисляется диапазон номеров текущего набора записей, отображенного на эк- ране. Этот диапазон может соответствовать либо допустимому числу записей экран- ного списка, либо числу записей, оставшихся до конца исходного списка, в зависи- мости от того, какое значение меньше. Если экранный список пустой (эта ситуация проверяется в строках 243-245), выполнение функции завершается возвратом зна- чения О. В строках 243-245 пользователю предлагается ввести число в диапазоне от 1 до maxSelection. Число, введенное пользователем, помещается в переменную selection, объявленную в строке 247. В строке 249 программа считывает из потока ввода число типа unsigned. При дан- ном типе ввода все предшествующие символы пробелов (включая символ разрыва строки) не учитываются программой. Это означает, что нажатие пользователем <Enter> без ввода числа не будет расценено программой как действительный ввод ко- манды, поэтому система останется в ожидании ввода значения пользователем. После ввода числа программа проверяет успешность выполнения операции с помощью функции-члена потока ввода fail. В случае ошибки ввода программа должна уметь 196 Глава 7. Прокручивание экранных списков...
корректно выйти из сложившейся ситуации. Ошибки устройства ввода или обнаруже- ние конца файла не мотут быть исправлены программой, поэтому, в случае обнаруже- ния этих событий в строках 252 и 253, просто происходит выход из цикла. Если проис- ходит какая-нибудь другая ошибка, флаг failbit сбрасывается в строке 258 вызовом функции clear, а функция ignore в строке 259 очищает остаток строки в потоке вво- да. Функция ignore сбрасывает с потока ввода столько символов, сколько задано в ее первом аргументе, или все символы до заданного во втором аргументе символа разде- лителя (по умолчанию EOF). Установив доя первого аргумента достаточно большое число (1 000) и разрыв строки в качестве символа разделителя, мы гарантируем сброс всей строки, ошибочно введенной пользователем. После выполнения функций clear и ignore состояние потока ввода опять становится good. В строке 260 программа со- общает пользователю об ошибке ввода, и цикл повторяется вновь. Если пользователь ввел целое число, то выполнение программы переходит к строке 265. Существует вероятность, что после ввода числа пользователь случайно или по ошибке введет ненужные символы. Поэтому, чтобы предупредить ошибку при следующем вводе, нам нужно очистить поток ввода с помощью функции ignore. Ес- ли пользователь ввел О (эта команда зарезервирована для выхода из режима про- смотра), то цикл завершается в строке 268. В строке 271 проверяется, что введенное число входит в допустимый диапазон. Если введен действительный номер, то в стро- ке 272 мы вычисляем ID записи, прибавив порядковый номер к индексу f irstvisibleldx_ и уменьшив полученное значение на единицу (поскольку отсчет идентификационных номеров начинается с нуля, а не с единицы). Если введенный номер выходит за пределы диапазона, программа показывает в строке 274 соответ- ствующее сообщение и цикл ввода повторяется. Определение класса AddressDisplayList Как вы убедились, большая часть функций пользовательского интерфейса опре- деляется в базовом классе DisplayList. Производный класс AddressDisplayList должен взять на себя только операции, специфичные для манипулирования запися- ми конкретной книги. Так, в производном классе следует определить, как возвра- щать и отображать записи определенного типа, тогда как процедуры управления эк- ранными списками независимы от типа данных и могут контролироваться базовым классом. Определение класса AddressDisplayList показано в листинге 7.7. ! Листинг 7.7. Определение класса AddressDisplayList 1://TinyPIM (c)1999 Pablo Halpern. Файл AddressDisplayList.h 2: 3: tfifndef AddressDisplayList_dot__h 4:#define AddressDisplayList_dot__h 1 5: 6:#include "DisplayList.h" 7: 8:class AddressBook; 9: 10:// Специализированный класс показа записей адресной книги. 11:class AddressDisplayList : public DisplayList Определение класса AddressDisplayList 197
12: { 13:public: 14: //В конструкторе дается ссылка на класс адресной книги 15: AddressDisplayList(AddressBookS addrBook); 16: 17: // Прокручивание списка до объекта Address с именем большим // или равным заданному. 18: // Обычно это имя начинается с указанной последовательности 19: // символов. Если искомая строка не обнаружена, возвращает false. 20: bool findNameStartsWith(const std::strings lastname, 21: const std::stringS firstname = ; 22: 23: // Выводит список только тех записей, которые содержат // заданную строку 24: void listContainsString(const std::strings) ; 25: 26: // Выводит список всех записей (используется после // listContainsString) 27: void listAll(); 28: 29:protected: 30: // Выводит резюме указанных записей. 31: // Реализация чисто виртуальной функции базового класса. 32: virtual void displayRecord(int recordld); 33: 34: // Замещает функцию базового класса для возвращения большего // числа записей. 35: virtual bool fetchMore(int startld, int numRecords, 36: std::vector<int>s result); 37: 38 .‘private: 39: AddressBookS addressBook__; 40: 41: // Строка для использования в режиме listContainsString. 42: std:: string containsString__; 43: }; 44: 45:#endif // AddressDisplayList dot h В открытом интерфейсе представлено лишь несколько функций, которые не были унаследованы из базового класса. В строке 15 конструктор принимает аргу- мент AddressBook. Ссылка на объект AddressBook сохраняется в переменной- члене addressBook_, объявленной в строке 39. Функция findNameStartsWith (строка 20) выполняет поиск записей по имени, как указано в комментариях к ней. Эта функция изменяет текущий экранный список таким образом, что найденная запись становится первой видимой. Другая функция listContainsString, объяв- ленная в строке 24, также выполняет поиск записей по введенным ключевым сло- вам, но вместо прокручивания списка она осуществляет фильтрацию элементов контейнера таким образом, что на экране отображаются только записи, содержа- щие указанную строку. Чтобы отменить фильтрацию, используется функция listAll, объявленная в строке 27. В строках 32 и 35 объявляются функции, выполняющие операции над отдельны- ми элементами в соответствии с логикой обработки записей, заданной базовым классом. Так, функция displayRecord должна подготовить для показа резюме запи- си, содержащее значение полей фамилии, имени и номера телефона. Функция fetchMore возвращает новые записи из адресной книги. 198 Глава 7. Прокручивание экранных списков...
Рассмотрим реализацию функции displayRecord (листинг 7.8). I Листинг 7.8. Реализация функций класса AddressDisplayList 1://TinyPIM (с)1999 Pablo Halpern. Файл AddressDisplayList.срр 2: 3:#ifdef _MSC_VER 4: #pragma warning(disable : 4786) 5:#endif 6: 7:#include <iostream> 8:#include <iomanip> 9:#include <algorithm> 10:#include "AddressDisplayList.h" ll:#include "Address.h" 12:#include "AddressBook.h" 13: 14: // В конструкторе дается ссылка на класс адресной книги 15:AddressDisplayList::AddressDisplayList(AddressBook& addrBook) 16: : addressBook__ (addrBook) 17: { 18: } 19: 20:// Вывод резюме указанных записей 21:void AddressDisplayList::displayRecord(int recordld) 22: { 23: Address record = addressBook_.getAddress(recordld); 24: 25: // Создание строки в формате "фамилия, имя". 26: std::string name = record.lastname (); 27: if (! record.firstname().empty()) 28: name.append(", ").append(record.firstname()); 29: 30: // Вывод переменной name и поля номера телефона в одну строку // (две колонки). 31: std::cout « std::setfill(’ •) « std::setw(40) 32: « std::left « name « record.phone(); 33: } 34: Функция displayRecord вызывается из функции базового класса display, которая передает идентификационный номер текущей записи. В строке 23 про- грамма возвращает объект Address с указанным ID. (Благодаря изменениям, внесенным нами в класс адресной книги в главе 6, поиск записи по указанному идентификационному номеру выполняется быстро и эффективно.) В стро- ке 26 из возвращенного объекта Address извлекается значение поля фамилии, к которому в строке 28 добавляется значение поля имени, если оно не пустое. Обратите внимание, как используется функция append для добавления в строку символов запятой и пробела, отделяющих имя от фамилии. В строках 31, 32 на печать выводятся объединенная строка фамилии с именем в одной колонке и номер телефона во второй. С • помощью манипуляторов форматирования set fill, setw и left для вывода резюме записи отводится строка из 40 симво- лов с выравниванием влево и заполнением точками свободного места. Таким об- разом, на экране номера телефонов будут вынесены в отдельный столбец напро- тив фамилий и имен абонентов, что облегчит поиск нужного номера. Определение класса AddressDisplayList 199
Другая важная функция производного класса— fetchMore, реализация которой показана в листинге 7.9. Код функции получился довольно длинным из-за того, что пришлось обрабатывать различные ситуации выполнения. Листинг 7.9. Реализация функции fetchMore < t Л > < * Л i» «3<&. л aJ * а. X ' X waA а. А г Гг~~ Х.а^Ла.а^а^ Х^ха«& X. Z' а. X * * ЛХ А^л. г У * л XX 35:// Функция возвращения дополнительных записей из адресной книги 36:bool AddressDisplayList::fetchMore(int startld, int numRecords, 37: std::vector<int>& result) 38: { 39:// Удаляет старое значение результата 40:result.clear(); 41: 42:if (numRecords =- 0) 43: return false; 44: 45:bool forwards = true; 46:if (numRecords < 0) 47: { 48: forwards =false; 49: numRecords = -numRecords; 50: } 51: 52:// Проверка наличия в списке записей 53: if (addressBook_.begin() === addressBook_. end () ) 54: return true; 55: 56:// Объявление итератора 57 : AddressBook: : const__iterator iter; 58: 59:// Принимает итератор на запись с указанным startld. 60:// В случае прокручивания вперед итератор приращивается // на элемент, следующий 61:// за указанным, чтобы избежать дублирования записей в экранном // списке. 62:if (startld == 0) 63: iter =(forwards ? addressBook_.begin() : addressBook_.end()); 64:else 65: { 66: iter = addressBook__. findRecordld (startld) ; 67: if (forwards) 68: ++iter; 69: } 70: 71:if (containsString_.empty()) 72: { 73: // "List all" mode 74: 75: if (forwards) 76: { 77: // Возвращает записи, начиная c iter 78: while (iter != addressBook_.end() && numRecords— > 0) 79: result.push_back((iter++)->recordld()); 80: 81: // Возвращает true, если достигнут конец списка 82: return iter == addressBook__. end () ; 83: } 200 Глава 7. Прокручивание экранных списков...
84: else 85: { 86: // Возвращает записи, начиная со следующей после iter 87: while (iter != addressBook—.begin () && numRecords— > 0) 88: result.push_back((—iter)->recordld()); 89: 90: // Если нужно добавить записи в обратном порядке, они // инвертируются здесь: 91: std::reverse(result.begin(), result.end()); 92: 93: // Возвращает true, если достигает начала списка 94: return iter == addressBook—.begin(); 95: } 96: } 97: else 98: { 99: // Режим фильтрации строк 100: 101: if (forwards) 102: { 103: // Возвращает записи ПОСЛЕ startld 104: 105: // Поиск совпадающих записей, начиная с iter 106: iter = addressBook_.findNextContains(containsString__, iter); 107: while (iter != addressBook__. end () && numRecords— > 0) 108: { 109: result.push_back(iter->recordld()); 110: 111: // Поиск следующей записи 112: iter - addressBook_.findNextContains(containsString__, ++iter); 113: } 114: 115: // Возвращает true при достижении конца списка 116: return iter == addressBook_.end(); 117: } 118: else 119: { 120: // Возвращает записи ПЕРЕД startld 121: 122: // В AddressBook нет функции для выполнения поиска // в обратном направлении. 123: // Вместо этого мы возвращаем все записи до iter 124: AddressBook::const—iterator endlter = iter; 125: iter = addressBook_.findNextContains(containsString_, 126: addressBook—.begin()); 127: while (iter != endlter) 128: { 129: result.push—back(iter->recordld()); 130: iter = addressBook—.findNextContains(containsString_, ++iter); 131: } 132: 133: return true; // Это выражение всегда возвращает true. 134: } 135: } 136: } 137: Определение класса AddressDisplayList 201
В строках 40-50 выполняется обычная проверка условий ввода. В строке 53 про- грамма проверяет наличие в адресной книге хотя бы одной записи. Поскольку в классе адресной книги AddressBook не было функций возвращения числа записей в контейнере, отсутствие записей мы определяем по равенству итераторов, возвра- щаемых функциями begin () Hend(). В строке 57 объявляется итератор, который будет устанавливаться на первую возвращаемую запись. Поскольку базовый класс DisplayList ничего не знает ни о классе AddressBook, ни о типе AddressBook: : const_iterator, в функцию fetchMore просто передается ID записи, и нам теперь нужно разработать алгоритм преобразования идентификационного номера в итератор на соответствующую за- пись. Если startld равно нулю, то при листании вперед переменной iter в стро- ке 63 присваивается значение, возвращаемое функцией begin () или функцией end () при листании назад. Если же startld содержит ненулевое значение иденти- фикационного номера записи, то в строке 66 создается итератор на запись с этим значением ID. При листании вперед итератор приращивается на единицу, чтобы он указывал на запись, следующую после заданной. В строке 71 определяется, не был ли выбран режим фильтрации. В этом режиме на экран выводится список только тех записей, которые содержат в одном из своих полей заданную строку. Если функция containsString_. empty () возвратит true, значит, мы находимся в обычном режиме показа всех строк. В обычном режиме при листании вперед запускается цикл в строках 78, 79, в котором идентификационные номера записей последовательно добавляются в конечный вектор до тех пор, пока в векторе не наберется numRecords элементов или не будет достигнут конец списка адресов. При листании назад в строках 87, 88 запускается цикл, в котором номера ID до- бавляются в вектор в обратной последовательности. Обратите внимание, что в этом случае итератор используется с оператором преинкремента. Это связано с тем, что отсчет в обратном порядке всегда начинается с элемента, предшествующего тому, на который указывает итератор. Например, функция end () возвращает итератор, ука- зывающий на позицию за последним элементом списка. Точно так же элемент, на который указывает итератор iter, полученный в строке 66, не должен быть включен в конечный вектор. Использование обратного итератора сделало бы код проще и по- нятнее, но мы не позаботились в свое время об определении обратного итератора в классе AddressBook. Обратите внимание, что элементы конечного вектора result инвертированы по отношению к исходной последовательности записей. Для этого в строке 91 используется алгоритм reverse. В продолжение темы о соглашениях именования алгоритмов сообщу вам, Что в стандартной библиотеке существует так- же алгоритм reverse_copy, который не изменяет элементы исходного контейнера, а копирует их в новый. Если при прокручивании списка вперед или назад мы достигнем его границ, то в строках 82 и 94 будет возвращаться значение true. В случае выбора режима фильтрации выполнение функции сразу перейдет к строке 101. И опять-таки, далее идет разветвление кода программы, в зависимости от того, прокручивается список вперед или назад. При листании вперед программа продолжается со строки 106 в поисках первой записи, содержащей строку containsString_. Затем в строках 107-413 выполняется цикл отбора ID всех после- дующих записей, удовлетворяющих условию, в вектор result, до тех пор, пока не бу- дет достигнут конец списка или конечный вектор не заполнится до конца. При листании назад в режиме фильтрации программа выполняется со стро- ки 124. Поскольку у нас нет версии функции f indNextContains. которая вела бы 202 Глава 7. Прокручивание экранных списков...
поиск записей в обратном порядке, нам приходится проделать один маленький трюк. Вместо того чтобы вести поиск от заданного элемента к началу списка, мы проводим его от начала к заданному элементу. Если между началом списка и ко- нечным элементом окажется больше записей, удовлетворяющих условию, чем бы- ло затребовано, то это не страшно, так как список, находящийся в буфере, всегда можно прокрутить. В строке 125 обнаруживается первая искомая запись с начала списка. Цикл в строках 127-131 практически такой же, как и рассмотренный на- ми ранее цикл в строках 107-113. Отличие состоит лишь в упрощенном условии окончания. Цикл завершается только при достижении элемента, на который ука- зывает итератор endlter, независимо от того, сколько элементов, удовлетворяю- щих условию, будет возвращено. Поскольку записи на экран будут выводиться в исходной последовательности, в выполнении алгоритма reverse нет необходи- мости. А так как поиск всегда ведется с первой записи списка, то выражение в строке 133 всегда будет возвращать true. Теперь, завершив работу над базовыми функциями AddressDisplayList, рас- смотрим в листинге 7.10 открытые функции интерфейса этого класса. * Листинг 7.10. Реализация открытых функций класса AddressDisplayList 138:// Прокручивает список до первого объекта Address с именем большим 139:// или равным заданному. Обычно это имя начинается с указанной 140:// последовательности символов. Возвращает false, если записи не // обнаружены. 141:Ьоо1 142:AddressDisplayList::findNameStartsWith(const std::strings lastname, 143: const std::strings firstname) 144: { 145: containsString_ = // Отключает режим фильтрации 146: 147: reset(); 148: AddressBook::const_iterator iter 149: = addressBook_.findNameStartsWith(lastname, firstname); 150: 151: if (iter -- addressBook__. end () ) 152: return false; 153: 154: // Делает найденную запись первой в экранном списке 155: scrollToTop(iter->recordld()); 156: return true; 157: } 158: 159:// Выводит только те записи, которые удовлетворяют заданному условию 160:void AddressDisplayList::listContainsString(const std::stringS s) 161: { 162: if (containsString_ == s) 163: return; 164: 165: reset (); 166: containsString_ = s;. 167: 168: // Следующий вызов функции display() автоматически запускает // fetchMore 169: } 170: Определение класса AddressDisplayList 203
171:// Показ всех записей (для отмены режима фильтрации) 172:void AddressDisplayList::listAll() 173: { 174: listContainsString(""); 175: toStart(); 176: } В строках 148, 149 функция findNameStartsWith класса AddressDisplayList вызывает одноименную функцию класса AddressBook. Если требуемая запись обна- ружена, то программа прокручивает список записей в строке 155 таким образом, чтобы сделать ее первой в экранном списке. В строке 165 функция listContainsString сбрасывает содержимое буфера, что- бы заполнить его вновь с помощью функции fetchMore отфильтрованными запися- ми. Новое значение, по которому будет проводиться фильтрация записей, присваи- вается переменной containsString_ в строке 166. Функция fetchMore будет авто- матически запущена при следующем обращении к функции displayO. Функция list АН просто сбрасывает значение переменной containsString__ в стро- ке 174 и в строке 175 прокручивает список в начало. Резюме Мы завершили работу над классом AddressDisplayList. При его реализации мы воспользовались возможностями класса-контейнера deque добавлять записи с обоих концов и открывать произвольный доступ к элементам в середине списка. Мы также научились использовать обратные итераторы и алгоритмы reverse и find. Для управления выводом информации на экран мы воспользовались манипуляторами форматирования, такими как setw и endl, а также познакомились с некоторыми приемами отслеживания ошибок ввода. Следующая наша задача состоит в том, чтобы облегчить пользователям доступ к функциям пользовательского интерфейса с помощью простой системы меню. Для выполнения этой задачи нам потребуются адаптер на основе контейнера stack и не- которые дополнительные функции потоков ввода-вывода. 204 Глава 7. Прокручивание экранных списков...
Глава 8 Простая система меню В этой главе... • Требования к системе меню 205 • Конструирование системы меню 206 • Создание иерархии меню с помощью шаблона stack 207 • Семантика поддержания взаимодействия с пользователем 211 • Соберем части вместе 222 • Нет предела совершенству: выполнение поиска независимо от регистра букв 231 • Резюме 238 В этой главе мы продолжим работу над пользовательским интерфейсом и нако- нец сможем вводить реальные адреса в адресную книгу, редактировать записи и отыскивать нужные записи по именам. Наша программа TinyPIM обретет реальные черты. Мы воспользуемся потоком особого типа, stringstream, и узнаем еще об од- ном типе классов-контейнеров— stack. Кроме того, мы научимся расширять биб- лиотеку STL пользовательскими компонентами. Требования к системе меню Как всегда, мы начнем работу с анализа требований к системе меню текстового интерфейса программы TinyPIM. Класс AddressBook предоставляет пользователю функции добавления, удаления, переноса, замещения (редактирования) и поиска за- писей адресной книги. Теперь нужно позаботиться о том, чтобы пользователь мог легко выбирать эти функции с помощью системы меню. Идея состоит в том, чтобы позволить пользователю выбирать опции возможных операций из главного меню, а затем вводить данные и давать программе команды из подменю, специфичного для каждой операции. В некоторых случаях выбор опции будет раскрывать подменю. Ниже перечислены основные принципы работы нашего текстового меню. • Приглашения меню должны направляться на стандартное устройство вы- вода и появляться в виде списка опций. Весь список должен умещаться в окне стандартного терминала размером 25x80. • Для выбора опции пользователю нужно ввести односимвольную команду со стандартного устройства ввода. • В случае совпадения введенного символа с одной из допустимых команд выбора опции программа выполняет соответствующий набор инструкций. Командный символ должен распознаваться независимо от регистра. • Если пользователь введет символ, не совпадающий ни с одной командой, программа должна вывести предупреждение об ошибке ввода и новое при- глашение выбора опции меню.
• После выполнения заданного действия приглашение меню показывается вновь, если только не была выбрана команда выхода из меню. • Выбор опции меню может завершиться показом подменю. После заверше- ния работы с подменю программа возвращается к показу основного меню. Основное меню приложения TinyPIM содержит следующие опции (буквы, за- ключенные в скобки, обозначают горячие клавиши выбора соответствующей оп- ции меню): • (A)ddress Book — открывает подменю адресной книги; • (D)ate Book — открывает подменю книги контактов; • (Q)uit — сохраняет все данные и закрывает программу. Многие операции с адресной книгой предполагают выбор пользователем за- писей из списка. При выборе опции Address Book текущий экранный список от- крывается над командной строкой, в которой представлены следующие опции подменю адресной книги. • (V)iew — пред лагает пользователю выбрать запись из списка для показа. • (C)reat — создает новую запись адресной книги. • (D)elete — предлагает пользователю выбрать запись для удаления из адрес- ной книги. • (E)dite — предлагает пользователю выбрать запись для редактирования. • List (А)П — выбор режима показа всех записей списка. Используется для от- мены режима фильтрации. • (L)ookup— предлагает пользователю ввести фамилию и (по желанию) имя абонента для поиска соответствующей записи. Прокручивает экранный список таким образом, что первая найденная запись оказывается вверху списка. • (S)earch — предлагает пользователю ввести строку для поиска. Выполняет фильтрацию списка, оставляя только те записи, в которых представлена искомая строка. • (R)edisplay — обновляет экраннй список записей и строку меню. • (Q)uit — выход из адресной книги (возвращение к основному меню). Механику управления показом экранных списков и выбора элементов из спи- ска мы уже детально рассмотрели в главе 7. Наша следующая задача — создание системы меню, облегчающей пользователям доступ к функциям пользователь- ского интерфейса. Конструирование системы меню В общих чертах диаграмму классов системы меню мы построили еще в гла- ве 1 (см. рис. 1.3). Более детальная проработка той же схемы показана на рис. 8.1. Базовым является класс Menu, в котором выполняются наиболее общие функции поддержки взаимодействия с пользователем и отслеживания ошибок. В нем также находятся открытые функции запуска подменю и возврата к предыдущему меню. От класса Menu наследуются производные классы MainMenu, AddressBookMenu и DateBookMenu, в которых выполняются функции, специфические для данных под- меню. Так, все подклассы содержат закрытые функции для каждой опции меню. 206 Глава 8. Простая система меню
Рис. 8.1. Диаграмма классов системы меню В каждом производном классе замещается функция mainLoop, унаследованная от базового класса. Эта функция в своей работе по поддержанию и обработке опций ме- ню использует другие функции базового класса: clearScreen и getMenuSelection. Функция getMenuSelection возвращает выбранную пользователем опцию в mainLoop, которая вызывает соответствующую функцию обработки события. Код клиента обращается к функции mainLoop как в основном меню, так и в подменю. Создание иерархии меню с помощью шаблона stack Семантика запуска подменю и возвращения к основному меню после выхода из подменю лучше всего реализуется с помощью стека. Стек можно выполнить на ос- нове вектора или списка, но в стандартной библиотеке для этих целей припасен спе- циализированный шаблон. В листинге 8.1 показано определение класса Menu на ос- нове стандартного шаблона stack. — Стеком называется • структура данных, доступ к элементам которой осуществляется по принципу “последним положил — первым взял”. По- следний добавленный элемент называется вершиной стека. Новый эле- мент добавляется в стек с помощью операции push, а вершина стека возвращается с помощью операции pop. Создание иерархии меню с помощью шаблона stack 207
ниекл 1://TinyPIM (с)1999 Pablo Halpern. Файл Menu.h 2: 3:#ifndef Menu_dot_h 4:#define Menu_dot_h 1 5: 6:#include <string> 7:#include <stack> 8: 9:// Базовый класс системы меню 10:class Menu 11: { 12:public: 13: Menu(){ } 14: 15: virtual ~Menu(){ } 16: 17: // Выполнение основного цикла меню 18: virtual void mainLoop() = 0; 19: 20: // Возвращает текущее меню. 21: static Menu*activeMenu () {return menustack__. top (); } 22: 23: // Ввод меню или подменю 24: static void enterMenu(Menu* m) {menuStack__.push (m) ; } 25: 26: // Выход из подменю к меню более высокого уровня. 27: static void exitMenu(){menuStack_.pop();} 28: 29: // Возвращает true при наличии активного меню 30: static bool isActiveO {return ! menustack_.empty();} 31: 32:protected: 33: // Утилиты для использования в производных классах: 34: 35: // Показ строки меню и предоставление пользователю возможности 36: // ввести ключевой символ опции меню. Если пользователь вводит 37: // ошибочный символ, программа сообщает об этом и предлагает 38: // пользователю повторить ввод. После ввода правильного 39: // символа он возвращается программой. В случае ошибки 40: // ввода-вывода возвращается символ ’О’. Ключевые символы не // чувствительны 41: //к регистру, но возвращаемая буква всегда прописная. 42: static char getMenuSelection(const std::strings menu, 43: const std::strings choices); 44: 45: // Очистка экрана 46: static void clearScreen(); 47: 48:private: 49: // Стек меню и подменю. 50: static std::stack<Menu*> menuStack_; 51: }; 52: 53:#endif // Menu_dot_h 208 Глава 8. Простая система меню
В строке 50 на основе шаблона stack объявляется стек меню. Заголовок с опреде- лением шаблона stack добавляется в код программы в строке 7. Указатели на объек- ты в иерархии меню-подменю сохраняются в переменной menuStack_. Активным является меню в вершине стека. Хотя принципы работы стека довольно простые, не- лишне подробнее познакомиться с определением этого шаблона, большая часть ко- торого представлена в листинге 8.2. (Определение в листинге 8.2 извлечено из файла заголовка <stack>. Этот код не является частью нашего проекта, и вам не нужно пе- ренабирать его на своем компьютере.) 1:namespace std { 2: 3: templatecclass Т,class Container = deque<T> > 4: class stack { 5: public: 6: typedef Container::value_type value_type; 7: typedef Container:: size__type size_type; 8: typedef Container container_type; 9: 10: public: 11: explicit stack(const Containers - Container()); 12: 13: bool empty() const {return (c.empty());} 14: size_type size()const {return (c.size());} 15: value_type&top() {return (c.back());} 16: const value_type& top () const {return (c.backO);} 17: void push(const.value_type& x){c.push_back(x);} 18: void pop() {c.pop_back();} 19: 20: protected: 21: Container c; 22: ); 23: } В строках 6-8 определяются псевдонимы типов данных для упрощения записи ссылок на параметры шаблона. В строках 13, 14 определяются функции-члены empty и size, которые просто вызывают соответствующие функции базового кон- тейнера. Функция push в строке 17 добавляет элемент в конец последовательности, а в строке 16 определена функция top, которая возвращает последний элемент, до- бавленный функцией push. Функция pop (строка 18) удаляет последний элемент. Ес- ли вы уже знакомы с использованием стеков, то вас удивит, что функция pop не воз- вращает удаленный элемент. О том, почему так происходит, вы узнаете во врезке “Справочная информация: почему функция pop не возвращает значение”. Шаблон stack является примером адаптера— шаблона класса, который пре- доставляет альтернативный интерфейс другому классу. Как вы видите в определе- нии, все функции-члены класса stack представляют собой однострочные вызовы соответствующих функций базового класса Container. Класс Container является вторым параметром шаблона stack. Этим параметром определяется тип контей- нера, который будет содержать элементы стека. Если во время реализации шабло- на будет задан только параметр Т, для сохранения элементов стека по умолчанию будет выбран контейнер типа deque<T>. В качестве базового контейнера стека можно использовать любые классы, поддерживающие функции size, back, Создание иерархии меню с помощью шаблона stack 209
push back и pop_back. Так, в основу стека мы можем положить вектор, как в сле- дующем объявлении menuStack: stack<Menu*r vector<Menu*> > menu Stack Класс menuStack можно было бы просто объявить как вектор, список или двухсто- роннюю очередь и использовать функцию push back вместо push и т.д. Но интерфейс стека логически более понятен и предупреждает выполнение операций, несовместимых с принципами работы стека, как, например, добавление элементов в середину последова- тельности или произвольный доступ к элементам с помощью итераторов. Другие адапте- ры контейнеров, определенные в стандартной библиотеке, — это queue (очередь), где эле- менты добавляются и возвращаются по принципу “последним положил—первым взял”, и priority_queue (пр1юр1шъегпная очередь), где элементы располагаются в очереди по их приоритетности (размеру). Больше информации об адаптерах вы найдете в технической документации стандартной библиотеки и в справочной литературе. Справочная информация: Экскурс; почему функция pop не возвращает значение ................—.... .............................................. Важный аспект, на который обращали внимание разработчики стан- дартных контейнеров, — это их ошибкоустойчивость. Исключительная ситуация, возникшая при обращении к контейнеру, не должна нару- шать его внутреннюю структуру. Если бы шаблон stack позволял функции pop возвращать значение, его можно было бы использовать следующим образом: stack<someclass> mystack; mystack.push(х); // Вводит объект х в стек someclass у = mystack.pop(); // Гипотетическое возвращение объекта с помощью pop Если исключительная ситуация возникнет в работе конструктора ко- пировщика при создании объекта у, после того как функция pop уда- лит объект, то этот объект будет утерян навсегда. Но чтобы выполнить указанную задачу с реальным стеком, придется переписать выражение следующим образом: someclass у - mystack.top(); mystack.pop(); В данном случае, если конструктор-копировщик потерпит неудачу при создании объекта у, вершина стека не будет удалена. Функция pop вы- зывается только после того, как создание нового объекта будет успешно завершено. Той же логике разработчики следовали при создании функ- ций pop back и pop_f ront для последовательных контейнеров. Вернемся к листингу 8.1. В строке 24 функция enterMenu обращается к функции- члену push для добавления нового указателя на объект меню в вершину стека. В строке 21 функция activeMenu возвращает последний добавленный указатель. При вызове функции exit Menu (строка 27) удаляется указатель в вершине стека и активным вновь становится предыдущее меню. Для определения наличия актив- ного меню используется функция isActive, определенная в строке 30, которая про- веряет, есть ли элементы в стеке. Эту проверку желательно делать перед вызовом функций pop и top, поскольку при их вызове для пустого стека возникает ситуация неопределенности. Все функции класса Menu объявлены статическими (static), по- скольку стек совместно используется всеми экземплярами меню. 210 Глава 8. Простая система меню
Семантика поддержания взаимодействия с пользователем В строках 42, 43 листинга8.1 объявляется функция getMenuSelection, которая ис- пользуется в классах, производных от класса Menu, для поддержания взаимодействия пользователя с приложением посредством системы меню. Строка menu содержит список опций и предложений пользователю в том виде, в каком меню будет выводиться на экран (если нужно, то с символами разрывов строк). Строка choices содержит список ключе- вых символов, которые пользователь может использовать для выбора опций меню. Реа- лизация этой функции, а также функции clearScreen показано в листинге 8.3. [ Листинг й.3(. Реалм&ЩЙ^clearScreen ’ ’ 1://TinyPIM (с)1999 Pablo Halpern. Файл Menu.cpp 2: 3:#include <cctype> 4:#include <iostream> 5: 6:#if !(_MSC_VER || _GCC__) 7:using std::tolower; 8:using std::toupper; 9:tfendif 10: ll:#include "Menu.h" 12: 13:// Показ строки меню и предоставление пользователю возможности ввести 14:// ключевой символ опции меню. Если пользователь вводит ошибочный // символ, 15:// программа сообщает об этом и предлагает пользователю повторить // ввод. 16:// После ввода правильного символа он возвращается программой. 17:// В случае ошибки ввода-вывода возвращается символ * 0’. 18:// Ключевые символы не чувствительны к регистру. 19:char Menu::getMenuSelection(const std::string&menu, 20: const std::string& choices) 21: { 22: while (std::cin.good()) 23: { 24: std::cout « menu; 25: 26: char selection = ’\0’; 27: std::cin » selection; 28: if (std::cin.fail()) 29: break; 30: 31: // Сброс остатка введенной строки 32: std::cin.ignore(10000, ’\n’); 33: 34: // Распознавание ключевого символа независимо от регистра 35: if (choices.find(toupper(selection)) != std::string::npos I I 36: choices.find(tolower(selection)) != std::string::npos) Семантика поддержания взаимодействия с пользователем 211
37: return toupper(selection);// Корректный символ 38: else 39: std::cout << "Invalid selection, please try again.\n\n"; 40: } // Конец цикла while 41: 42: return ’\0’; 43:} 44: 45: // Очистка экрана 46: void Menu::clearScreen() 47: { 48: // Поскольку не все терминалы распознают символ подачи формы, 49:// для надежной очистки экрана мы также вводим 25 символов разрыва // строки: 50: std::cout <<"\f\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n" 51: « std::flush; 52: } 53: 54:// Определение переменной-члена menuStack_static 55:std::stack<Menu*> Menu::menuStack ; Считывание данных, введенных пользователем В строках 22-40 запускается цикл, который выводит на экран предложение выбрать опцию меню и ожидает ввод данных пользователем. Цикл завещается после ввода кор- ректного символа или в случае возникновения ошибки ввода. В условных выражениях, как, например, условие завершения цикла, инструкцию std:: cin можно использовать вместо явного вызова функции std:: cin. good (), но для большей наглядности я пред- почитаю последний вариант. Строка 24 просто вывод ит предложение на экран. Строка 27 считывает один символ из стандартного потока ввода. При данном ти- пе ввода все предшествующие символы пробелов, включая символ разрыва строки, игнорируются. Это означает, что если пользователь нажмет <Enter> без ввода сим- вола, то система останется в ожидании ввода командного символа. После заверше- ния ввода программа проверяет его успешность с помощью функции fail в стро- ке 28. В случае обнаружения ошибки ввода цикл завершается. Если ввод прошел успешно, то командный символ заносится в переменную selection. Пользователь мог ошибочно ввести не один, а несколько символов. Если их оставить в потоке ввода, то при следующем вызове функции будет счи- тан ошибочный символ, что вызовет ситуацию неопределенности. Поэтому оста- ток введенной строки сбрасывается с потока ввода в строке 32 с помощью функ- ции ignore. Функция сбрасывает либо заданное число символов, либо всю по- следовательность до первого обнаружения заданного символа разделителя {по умолчанию EOF). В нашем случае было установлено достаточно большое число удаляемых символов (1 000) и символ разрыва строки в качестве разделителя, что надежно гарантирует сброс всех символов в потоке ввода включительно со следующим символом разрыва строки. В строках 35, 36 введенный символ сравнивается с образцами командных симво- лов в строке choises. Как вы помните, функция find возвращает значение проз, ес- ли искомый символ не найден. Условие нечувствительности командных символов к регистру достигается за счет того, что для поиска используются два варианта вве- денного символа в нижнем и верхнем регистрах. Преобразование регистра символа 212 Глава 8. Простая система меню
осуществляется с помощью функций toupper и tolower, унаследованных из стан- дартной библиотеки языка С. Эти функции объявляются в заголовке <cctype>, до- бавленном в код программы в строке 3. Этот заголовок содержит определения мно- гих других утилит преобразования символов, включая функции isdigit — для оп- ределения принад лежности символа к цифрам, is space — для определения пробелов, is upper — для определения букв в верхнем регистре и т.д. Все эти функции введены в пространство имен std, но мы добавляем их в строках 7 и 8 в область видимости про- граммы с помощью директивы using. Это делается для того, чтобы предупредить кон- фликты имен в компиляторе компании Microsoft, описанные в главе 2. |^а Особенности компиляции. В компиляторе egcs версии 1.1.2 были метку допущены ошибки: все функции заголовка <cctype> определены как ;макросы, поэтому они не добавлены в пространство имен std. Для раз- решения этой ошибки поступайте так же, как было описано для компи- лятора Microsoft (см. строку 6 листинга 8.3), но применительно только к заголовку <cctype> (тогда как с компилятором Microsoft это решение предлагалось для всех литералов, унаследованных из языка С). Если введенный символ будет обнаружен среди символов choices, то программа переходит к выполнению строки 37. Введенный символ, предварительно преобразо- ванный в верхний регистр, возвращается пользователю. Если искомый символ не обнаружен, значит, он был введен пользователем ошибочно. В этом случае выполне- ние программы продолжится со строки 39 показом сообщения об ошибке, и цикл ввода повторится с начала. Класс Menu предоставляет вспомогательную функцию clearScreen, которая, как вы, наверное, догадались, служит для очистки экрана. Многие терминалы очищают экран при получении символа подачи формы (’ \ f ’), но не все. Так, эмулятор термина- ла Windows не распознает эту команду и вместо очистки экрана выводит странные символы. Чтобы сделать нашу программу независимой от типа терминала, в стро- ке 50 мы вводим как символ подачи формы, так и 25 символов разрыва строки. Этого будет достаточно, чтобы очистить стандартный 25-строчный экран любого терминала. Класс AddressBookMenu Класс AddressBookMenu наследуется от класса Menu. В нем задается набор опций ме- ню адресной книги программы TinyPIM и определяются функции, ответственные за вы- полнение каждой опции. Определение класса AddressBookMenu показано в листинге 8.4. 1://TinyPIM (с)1999 Pablo Halpern. Файл AddressBookMenu.h 2: 3:#ifndef AddressBookMenu_dot_h 4:#define AddressBookMenu_dot_h 1 5: 6: #include ’’Menu. h” 7 : #include ’’AddressDisplayList. h’’ 8: 9:class AddressBookMenu : public Menu 10: { 11:public: Семантика поддержания взаимодействия с пользователем 213
12: AddressBookMenu(AddressBook& addrBook) 13: :addressBook_(addrBook),displayList_(addrBook){} 14: 15: void mainLoop(); 16: 17:private: 18: void viewEntryO; 19: void createEntry(); 20: void editEntryO; 21: void deleteEntry(); 22: void listAHO;' 23: void lookup(); 24: void searchO; 25: 26: AddressBook& addressBook_; 27: AddressDisplayList displayList_; 28: }; 29: 30:#endif // AddressBookMenu_dot—h_____________________________________ В строке 26 объявляется переменная-член, которая содержит ссылку на объект AddressBook, а в строке 27 в качестве переменной-члена объявляется объект AddressDisplayList. Эти переменные требуются для выполнения опций меню ад- ресной книги. Их инициализация с помощью конструктора переданным объектом AddressBook происходит в строках 12, 13. В строке 15 объявляется основной цикл, который будет вызываться для показа меню и обработки опций, выбранных пользо- вателем. В строках 18-24 объявляются закрытые функции класса AddressBookMenu. Функция mainLoop Взаимодействие пользователя с программой поддерживается функцией mainLoop, реализация которой показана в листинге 8.5. 1://TinyPIM (с)1999 Pablo Halpern. Файл AddressBookMenu.срр 2: 3:#ifdef _MSC_VER 4: #pragma warning(disable : 4786) 5:#endif 6: 7:#include <iostream> 8:#include <iomanip> 9:#include <climits> 10:#include "AddressBookMenu.h" ll:#include "Address.h" 12:#include "AddressBook.h" 13:#include "AddressEditor.h" 14: 15:void AddressBookMenu: :mainLoop() 16: { 17: clearScreen(); 18: std::cout « "*** Address Book ***\n\n"; 19: 20: displayList—.display(); 21: std::cout « 1 \n’; 214 Глава 8. Простая система меню
22: 23: static const char menu[] = 24: "(P)revious, (N)ext, (V)iew, (C)reate, (D)elete, (E)dit,\n" 25: "list (A) 11, (L)ookup, (S)earch, (R)edisplay, (Q)uit ? 26: 27: static const char choicest] = "PNVCDEALSRQ"; 28: 29: switch (getMenuSelection(menu,choices)) 30: 31: { case ’P' : displayList_.pageUp(); break; 32: case ’N* : displayList_.PageDown(); break; 33: case ’V* : viewEntry(); break; 34: case •c* : createEntry() ; break; 35: case ’D’ : deleteEntry() ; break; 36: case ’E* : editEntryO; break; 37: case •A’ : listAll(); break; 38: case ’L* : lookup(); break; 39: case •S’ : search(); break; 40: case •R* : /* пустая инструкция */ break; 41: case ’Q’ : exitMenu(); break; 42: default: exitMenu(); break; 43: } 44: } 45: Первое, что выполняет функция mainLoop, — очищает экран и выводит заголо- вок Address Book. Затем, в строке 20, вызывается функция display, которая выво- дит текущий экранный список. Переменная-член displayList_ изначально пуста, поэтому при первом обращении к функции display отображается список из 15-ти первых записей адресной книги. С помощью экранного списка пользователь может просматривать записи ад- ресной книги. Теперь нам нужно снабдить пользователя возможностью выполнять операции с записями. Меню адресной книги выводится двумя строками под теку- щим экранным списком записей. Текст меню задается в строках 23-25 как ста- тичная строка текста с нулевым окончанием. Обратите внимание, что текст меню представлен двумя литералами без каких-либо знаков препинания между ними. Многие не знают, что в С и C++ допускается разделение длинных литералов таким образом. Во время компиляции эти два литерала будут автоматически объединены в сплошной текст. Обратите также внимание на то, что в конце первого литерала задан символ разрыва строки. В строке 27 массив choices инициализируется строкой символов выбора опций, показанных прописными буквами. В строке 29 в функцию getMenuSelection, унас- ледованную от базового класса Menu, с двумя аргументами передаются строка меню и строка командных символов. Функция getMenuSelection возвращает либо вы- бранный символ опции в верхнем регистре, либо нулевой символ, указывающий на ошибку ввода-вывода. Обработка возвращенного значения производится с помощью конструкции с оператором case в строках 31-42. Выбор опций Next (Следующий) и Previous (Предыдущий) осуществляется соответ- ственно в строках 31, 32 вызовом функций-членов класса экранных списков PageDown и pageUp. Опция Quit (Выход) и сообщение об ошибке обрабатываются в строках 41, 42 вызовом функции базового класса exitMenu, которая удаляет с вер- шины стека текущее меню и делает активным предыдущее. Выбор других опций об- рабатывается специальными функциями-членами класса AddressBookMenu. После выполнения соответствующей операции функция mainLoop завершается. Семантика поддержания взаимодействия с пользователем 215
Функция viewEntry При вводе пользователем символа V запускается функция viewEntry, реализация которой показана в листинге 8.6. Листинг 8Л^ ,,w J « : ! «• \t Zv*"Zv, Ял fwb^A', £ _JS_ > ' • , ,r Я?ГйИИ1|ИИ1аг:.^.1 46:void AddressBookMenu::viewEntry() 47: { 48: int recordld = displayList_.selectRecord(); 49: if (recordld == 0) 50: return; 51: 52: Address addr = addressBook_.getAddress(recordld); 53: std::cout « ”\nName:” « addr.lastname(); 54: if (! addr.firstname().empty()) 55: std:: cout « ”, 11 « addr.firstname(); 56: std::cout « "\nPhone: ” « addr.phone(); 57: std::cout « "\nAddress:\n" « addr.address(); 58: 59: std::cout « ”\n\nPress [RETURN ] when ready."; 60: std::cin.ignore(INT—Max, *\n’); 61:} 62: В строке 48 вызывается функция selectRecord. Пользователю будет пред- ложено ввести номер записи, под которой она выведена на экран, после чего воз- вращает идентификационный номер выбранной записи. Если пользователь от- менит просмотр, функция возвратит О. В строках 52-57 функция показывает на экране все поля объекта Address в удобном для просмотра формате. В стро- ке 60 с помощью функции ignore задается пауза, во время которой пользователь может считать информацию с экрана до его очистки и возвращения строки ме- ню. Функция ignore продолжает удерживать поток ввода до тех пор, пока поль- зователь не нажмет клавишу <Enter> (второй аргумент функции). Ввод всех дру- гих символов игнорируется. В первом аргументе функции ignore задается число символов, которые следует проигнорировать. Но поскольку это число неизвест- но, мы устанавливаем лимит с помощью макроса INT_MAX, который возвращает максимальное число, допустимое для типа int. Макрос INTJMAX вместе с другими подобными макросами (INT_MIN, UINTJMAX, LONG_MAX и т.д.) определяются в заголовке <climits>, который добавляется в стро- ке 9 листинга 8.5. Напомним, что макросы стандартной библиотеки не входят в про- странство имен std. Примите к сведению, что существует еще файл заголовка <limits> (без с в начале), содержащий сходные, но более сложные определения. Эти средства в большей степени подходят для авторизации сложных шаблонов, требую- щих установки числовых ограничений для своих параметров. Функция createEntry Если пользователь выбрал команду С для создания новой записи, открывается сеанс редактирования пустого объекта Address. После завершения сеанса редакти- рования нужно проверить в адресной книге наличие записей с тем же именем и фа- милией. В случае обнаружения дубликата пользователю следует предоставить на 216 Глава 8. Простая система меню
выбор три опции: намеренно добавить дублирующую запись, изменить текущую за- пись или отменить результаты редактирования. Функция возвращает значения в случае успешного ввода записи и при отмене сеанса редактирования. Реализация функции с г eat eEnt г у показана в листинге 8.7. 63:void AddressBookMenu::createEntry() 64: { 65: // Редактирование пустого объекта Address 66: AddressEditor editor; 67: Address addr; 68: 69: 70: 71: 72: 73: 74: 75: 76: 77: 78: 79: 80: 81: 82: 83: 84: 85: 86: 87: 88: 89: 90: 91: 92: 93: 94: 95: 96: 97: 98: 99: 100: 101: 102: 103: 104: // Продолжение редактирования до сохранения объекта // или отмены редактирования. while (editor.edit()) { addr = editor.addr(); if (addr.lastname().empty()) { std::cout « "Last name must not be empty." « std::endl; continue; // Продолжение цикла для внесения новых // изменений } // Поиск существующих записей с тем же именем и фамилией int duplicates = addressBook_.countName(addr.lastname(), addr.firstname()); int recordld = 0; if (duplicates == 0) { // Дубликатов нет, можно вводить новую запись, recordld =addressBook_.insertAddress(addr); // Прокручивание списка для отображения новой // добавленной записи displayList_.scrollToTop(recordld); return; } else { // Обнаружена запись с тем же именем и фамилией. // Показ опций пользователю. std::cout « "There are already " « duplicates « " records with this name.Xn"; switch (getMenuSelection( "(S)ave as new record,(E)dit record or (C)ancel?", "SEC")) { case ’S': //Сохранить запись (создать дубликат) recordld = addressBook_.insertAddress(addr); displayList—.scrollToTop(recordld); Семантика поддержания взаимодействия с пользователем 217
105: return; 106: 107: case ’Е': // Изменить запись 108: continue; // Повторение цикла редактирования 109: 110: case ’С’: // Отмена или 111: default: // ошибка ввода-вывода 112: return; // Возвращение без изменения // AddressBook 113: } // Конец конструкции switch 114: } // Конец условия else 115: } // Конец цикла while 116: } 117: Работа функции начинается с открытия в строке 70 сеанса редактирования пус- того объекта Address, объявленного в строке 67 под именем addr. Редактирование объекта начинается в теле цикла while, поскольку может возникнуть необходимость повторного редактирования того же объекта, если, например, будет обнаружен дуб- ликат записи и пользователь захочет изменить текущую вновь созданную запись. Цикл редактирования завершается в случае успешного сохранения новой записи в адресной книге. Пользователь также может прервать сеанс редактирования, введя командные символы !х, в результате чего функция editor. edit () возвратит false. В строках 73-77 проверяется наличие значения в поле фамилии. Если это поле оста- лось пустым, то программа покажет сообщение об ошибке и повторит цикл редакти- рования записи. В строках 80, 81 определяется число записей в адресной книге, в которых поля имени и фамилии содержат те же значения, что и в текущей записи. Если таких записей не обнаружено, значит, текущая запись уникальна. В этом случае она до- бавляется в адресную книгу (строка 87), и программа прокручивает экранный спи- сок таким образом, чтобы новая запись оказалась вверху списка (строка 90). Если запись с тем же именем и фамилией уже существует, то программа в строках 96, 97 выводит пользователю предупредительное сообщение о наличии дубликата записи. Затем инструкция в строке 99 выводит на экран новую строку меню с це- лью выяснить у пользователя, как следует поступить с дубликатом записи. Поль- зователь может ввести символы S для сохранения дубликата записи. Е — для по- вторного редактирования текущей записи и С — для отмены сеанса редактирова- ния без изменения адресной книги. Так же, как и при обработке опций предыдущего меню, используется конструкция с оператором case для выбора дей- ствия, отвечающего требованиям пользователя. Обратите внимание еще раз на то, что основной цикл while завершается только в случае успешного сохранения за- писи или отмены сеанса редактирования. Функции deleteEntry и editEntry Удаление и изменение записей может привести к тому, что в буфере экранных списков останутся идентификационные номера несуществующих записей или поря- док записей в буфере перестанет соответствовать действительному. Поэтому для со- гласованной работы программы после выполнения функций удаления и редактиро- вания необходимо позаботиться об обновлении записей в буфере экранных списков. Простейшим решением в обоих случаях будет сброс записей буфера и повторная пе- резагрузка их из адресной книги, как показано в листинге 8.8. 218 Глава 8. Простая система меню
118:void AddressBookMenu::deleteEntry() 119: { 120: int recordld = displayList—.selectRecord(); 121: if (recordld == 0) 122: return; 123: 124: // Ищет первую видимую запись на экране. Если это 125: // удаляемая запись, ищет вторую видимую запись. 126: int firstVisible = displayList—.screenRecord(0); 127: if (firstVisible == recordld) 128: firstVisible = displayList—.screenRecord(1); 129: 130: // Удаление записи 131: addressBook_.eraseAddress(recordld); 132: 133: // Удаление записи делает недействительным буферизованный // список. 134: // Перезагружаем его и прокручиваем список к исходной позиции. 135: displayList_.reset(); 136: if (firstVisible != 0) 137: displayList_.scrollToTop(firstVisible); 138: } 139: 140:void AddressBookMenu::editEntry() 141: { 142: int recordld = displayList—.selectRecord(); 143: if (recordld == 0) 144: return; 145: 146: // Открытие сеанса редактирования для выбранной записи 147: Address addr = addressBook_.getAddress(recordld); 148: AddressEditor editor(addr); 149 150: // Редактирование записи 151: if (editor.edit()) 152: { 153: // Замена записи измененной версией. 154: addressBook—.replaceAddress(editor.addr()) ; 155: 156: // Порядок следования мог измениться, нам следует // перезагрузить 157: // буфер экранных списков. 158: displayList—.reset(); 159: 160: // Прокручивание списка для отображения измененной // записи вверху экрана. 161: displayList—.scrollToTop(recordld); 162: } 163: } 164: В строке 120 функция deleteEntry предлагает пользователю выбрать запись для удаления, которая затем благополучно удаляется в строке 131. Но поскольку удале- ние записи делает недействительным буферизованный экранный список, мы пере- Семантика поддержания взаимодействия с пользователем 219
загружаем его в строке 135. Чтобы после перезагрузки отобразить на экране ту же часть адресной книги, нам нужно предварительно собрать информацию о текущем экранном списке. В строке 126 определяется идентификационный номер первой ви- димой записи на экране. Если это как раз та запись, которую мы хотим удалить, то в строках 127, 128 определяется вторая видимая запись. Затем, после удаления вы- бранной записи, программа восстанавливает экранный список в прежнем виде, что- бы первой видимой вновь оказалась та же запись, которая отображалась на экране первой (или второй) до удаления. Восстановление экранного списка выполняется в строке 137. Если бы программа не восстанавливала экранный список, то после ка- ждого удаления или редактирования список на экране прокручивался бы к самому началу, что раздражало бы пользователей. В строке 142 функция editEntry вновь предлагает пользователю выбрать запись для редактирования. Указанная запись возвращается в строке 147, и для нее создается объект editor класса AddressEditor. Если сеанс редактиро- вания завершится нормально (т.е. пользователь не прервет его вводом соот- ветствующей команды), то выбранная запись будет заменена измененным вариантом в строке 154. Поскольку входе редактирования могут быть изме- нены значения полей имени и фамилии, это приведет к смене очередности элементов списка. Поэтому нам опять-таки необходимо привести буфе- ризованный экранный список в соответствии с основным списком. Переза- грузка буфера производится в строке 158, после чего в строке 161 прог- рамма прокручивает экранный список таким образом, чтобы измененная запись стала первой видимой на экране. Мы могли бы немного повысить эффектив- ность работы программы, если бы добавили код проверки необходимости пере- загрузки буфера после завершения сеанса редактирования. Но наш вариант про- граммы также работает вполне нормально. Поиск и фильтрация записей Поиск записей по имени осуществляется программой путем прокручивания спи- ска и отображения тех записей, имя и фамилия в которых наиболее точно соответст- вуют введенному образцу. Во время поиска по ключевым словам происходит фильт- рация записей списка, в результате чего на экране отображаются только те записи, в полях которых представлены заданные ключевые слова. Реализация этих функций для адресной книги показана в листинге 8.9. pffircTl 165:void AddressBookMenu::lookup() 166: { 167: // Предложение ввести фамилию и (по желанию) имя. 168: std: .’string Ikupname; 169: std::cout « "lookup name (lastname[,firstname]):"; 170: std::getline(std::cin, Ikupname); 171: if (1kupname.empty()) 172: return; 173: // Поиск окончания фамилии и начала имени 174: 175: std::string::size_type lastNameEnd = Ikupname.find(’,'); 176: std::string::size_type firstNameStart = std::string::npos; 177: if (lastNameEnd != std::string::npos) 178: firstNameStart = Ikupname.find_first_not_of(”, \t\f\n\v”, 220 Глава 8. Простая система меню
179: lastNameEnd); 180: 181: if (firstNameStart == std::string::npos) 182: // Поиск только по фамилии 183: displayList_. findNameStartsWith (Ikupname. substr (0, lastNameEnd) ); 184: else 185: displayList_.findNameStartsWith(Ikupname.substr(0, lastNameEnd) , 186: Ikupname.substr(firstNameStart)); 187: } 188: 189:void AddressBookMenu::search() 190: { 191: std::string searchstring; 192: std::cout « "Search for string:"; 193: std::getline(std::cin, searchstring); 194: if (searchstring.empty()) 195: return; 196: 197: displayList__.listContainsString(searchstring); 198: } 199: 200:void AddressBookMenu::listAll() 201: { 202: displayList_.listAll (); 203: } В строках 168, 169 программа предлагает пользователю ввести фамилию и, по желанию, имя для поиска. Пользователю достаточно ввести лишь часть фамилии, так как программа не требует точного совпадения значений. Однако пользователь может ввести полностью имя и фамилию абонента, чтобы быстрее отыскать его в ад- ресной книге. В строке 170 программа считывает введенную строку со стандартного потока ввода с помощью функции getline. Как вы помните, эта функция возвраща- ет с потока ввода все символы до первого символа разрыва строки. Причем символ разрыва строки сбрасывается функцией с потока ввода. В строке 175 производится поиск окончания фамилии. Предполагается, что имя от фамилии должно быть отделено запятой. Поиск символа запятой осуще- ствляется в строке 177. Если запятая обнаружена, то выполнение программы переходит к строке 178, где символ запятой и следующий за ним символ пробела удаляются, а оставшаяся часть строки рассматривается как имя. Если запятая отсутствует или за ней не следуют никакие символы алфавита, то переменной firstNameStart присваивается значение проз. В строках 183 и 185 фамилия извлекается из введенной строки с помощью функции subst. Для этой функции индекс начального символа задается нулевым, а длина строки определяется по позиции символа запятой. В строке 186 опять-таки имя извлекается с помощью функции subst, но в этот раз начальный символ задается указателем firstNameStart, а длина извлекаемой строки не устанавливается. В этом случае по умолчанию функция subst извлекает символы от начала и до конца строки. В строке 181 определяется, было ли задано для поиска имя абонента. Если нет, то в строке 183 вызывается функция findNameStartsWith, в которой задан по- иск только по фамилии (аргумент-имени по умолчанию задается пустым). Если же имя присутствовало во введенной строке, то другой вариант функции findNameStartsWith с установленными двумя аргументами вызывается в строках 185, 186. Как вы помните, функция findNameStartsWith класса AddressDisplayList прокручивает экранный список таким образом, чтобы Семантика поддержания взаимодействия с пользователем 221
в верхней части экрана отобразилась запись со значениями имени и фамилии, которые совпадают с заданными или начинаются с введенных символов. Функция search работает аналогично. Ее работа также начинается в стро- ке 193 со считывания всей строки с потока ввода. Но при этом нам не нужно анали- зировать структуру строки и вычленять ее составные части. Поэтому полученная строка просто передается в функцию-член listContainsString класса AddressDisplayList. Эта функция, в свою очередь, вызывает функцию fetchMore для осуществления фильтрации записей. Для отмены режима фильтрации пользо- вателю нужно воспользоваться опцией List (А)Н (Показать все), при выборе которой вызывается функция listAll класса AddressDisplayList. Соберем части вместе В предыдущей главе мы разработали экранный список, но не проверили, как он работает. Теперь, после того как мы завершили работу над системами главного меню и меню адресной книги, с помощью которых был открыт доступ к функциям класса AddressDisplayList, мы можем свести все эти части кода воедино и испытать на практике работу адресной книги. Исполняемая программа В листинге 8.10 показана реализация функции main, которая активизирует показ главного меню пользователю. Чтобы создать файл исполняемой программы, нужно ус- тановить в нем связи с файлами всех классов, которые мы разработали до сих пор. 1://TinyPIM (с)1999 Pablo Halpern. Файл TinyPIM.срр 2: 3:#ifdef _MSC_VER 4:#pragma warning(disable : 4786) 5:#endif 6: 7:#include <iostream> 8: 9:#include "AddressBook.h" 10:#include "AddressBookMenu.h" 11: 12:// Исполняемая программа просто вызывает функцию main. 13:int main() 14: { 15: AddressBook addrBook; 16: 17: // Создает меню адресной книги и помещает его в стек. 18: AddressBookMenu addrBookMenu(addrBook); 19: Menu::enterMenu(&addrBookMenu); 20: 21: // Обработка выбора опций до выхода из меню. 22: while (Menu::isActive()) 2 3: Menu::act iveMenu()->mainLoop(); 24: 222 Глава 8. Простая система меню
std::cout « "\nThank you for using TinyPIM!\n" « std::endl; return 0; 25: 26: 27: 28: } В строке 15 создается объект AddressBook, а в строке 18— объект AddressBookMenu. В строке 19 мы помещаем созданный объект меню в стек. Глав- ный цикл программы осуществляется в строках 22, 23, где программа выбирает ак- тивное меню (объект которого находится в вершине стека) и вызывает для него функцию mainLoop. Цикл продолжается до тех пор, пока в стеке остается хотя бы один элемент (функция Menu: : is Active возвращает true). Стек очищается от объ- ектов меню, когда пользователь выбирает в активном меню опцию (Q)uit. После за- вершения главного цикла в строке 25 программа показывает последнее сообщение и завершает работу. Результат выполнения программы с вводом данных и выбором опций меню показан в листинге 8.11. 1 Листинг 8.11. Проверочный запуск и выполнение программы t J *** Address Book *** =============No records selected ================ (P)revious, (N)ext, (V)iew, (C)reate, (D)elete, (E)dit, list (A)11, (L)ookup, (S)earch, (R)edisplay, (Q)uit ? C Last name: Clinton First name: William Phone Number: 5551993 Address: 9 Monica Blvd Address: Little Rock, AK Address: . ' Очисткаэкрана (новая страница) *★* Address Book ★★★ l:Clinton, William.......................555-1993 ===============End of list =============== (P)revious, (N)ext, (V)iew, (C)reate, (D)elete, (E)dit, list (A)11, (L)ookup, (S)earch, (R)edisplay, (Q)uit ? C Last name: Ford First name: Gerald Phone Number: 555-1974 Address: 8 Golf Dr. Address: Washington, DC Address: . Очистка экрану (новая страница) • ’< s,<< *** Address Book *** l:Ford, Gerald........................555-1974 ===============End of list =============== (P)revious, (N)ext, (V)iew, (C)reate, (D)elete, (E)dit, list (A) 11, (L)ookup, (S)earch, (R)edisplay, (Q)uit ? a Очистка'''‘э^анА:^^ц^.?Ъ^аница^1Ж®;Ж1Я??Ж;^йЖ *** Address Book *** ===============Start of list =============== 1:Clinton, William..........................555-1993 2: Ford, Gerald.............................555-1974 ================End of list =============== Соберем части вместе 223
(P)revious, (N)ext, (V)iew, (C)reate, (D)elete, (E)dit, list (A) 11, (L)ookup, (S)earch, (R)edisplay, (Q)uit ? s Search for string: Monica *** Address Book *** ===============Start of list =============== 1: Clinton, William........................555-1993 =============:===End of list =============== (P)revious, (N)ext, (V)iew, (C)reate, (D)elete, (E)dit, list (A) 11, (L)ookup, (S)earch, (R)edisplay, (Q)uit ? v Choose a record number between 1 and 1 Record number (0 to cancel)? 1 Name: Clinton, William Phone: 555-1993 Address: 9 Monica Blvd Little Rock, AK Press [RETURN ] when ready. V vJ \\ Очистка экрана Дновдя страница) *** Address Book *** ===============start of list =============== 1: Clinton, Bill..........................555-1993 ===============End of list =============== (P)revious, (N)ext, (V)iew, (C)reate, (D)elete, (E)dit, list (A)11, (L)ookup, (S)earch, (R)edisplay, (Q)uit ? q Thank you for using TinyPIM! Мы могли бы продолжить работу с программой, добавлять все новые и новые за- писи, прокручивать экранные списки вверх и вниз, искать записи по именам и фильтровать список, открывать записи для просмотра и редактирования или уда- лять их. Но этим вы можете заняться самостоятельно и ввести сведения о своих друзьях, сотрудниках и родных. Сейчас нам нужно каким-нибудь простейшим спо- собом добавить в адресную книгу побольше произвольных записей, чтобы продол- жить тестирование программы. Функция генерирования данных с помощью потоков строк В листинге 8.12 показана функция generateAddresses, которая произвольным способом создает образцы записей и добавляет их в адресную книгу. 1://TinyPIM (с)1999 Pablo Halpern. Файл TestData.cpp 2: 3:#ifdef _MSC_VER 4:#pragma warning(disable : 4786) 5:#endif 6: 7:#include <cstdlib> 8:#include <sstream> 9:#include <iomanip> 224 Глава 8. Простая система меню
10: ll:#ifdef _MSC_VER 12:namespace std { 13: inline int rand() {return :: rand();} 14: inline void srand(unsigned s) {::srand(s);} 15:} 16:#endif 17: 18:#include "AddressBook.h" 19: 20:// Случайное возвращение элементов из константных массивов строк 21:template <class А> 22:inline const char* randomstring(A& stringArray) 23: { 24: int size = sizeof(A)/sizeof(stringArray[0]); 25: int index = std::rand() % size; 26: return stringArray[index]; 27: } 28: 29:void generateAddresses (AddressBook& addrbook, int numAddresses) 30: { 31: // Константный посев генератора случайных чисел, 32: // чтобы каждый раз получать одну и ту же // последовательность значений. 33: std::srand(100); 34: 35: static const char* const lastnames[] = { 36: "Clinton", "Bush", "Reagan", "Carter", "Ford", "Nixon", 37: "Johnson", "Kennedy" 38: }; 39: 40: static const char* const firstnames ] = { 41: "William", "George", "Ronald", "Jimmy", "Gerald", "Richard", 42: "Lyndon", "Jack", "Hillary", "Barbara", "Nancy", "Rosalynn", 43: "Betty", "Pat", "Ladybird", "Jackie" 44: }; 45: 46: // Названия деревьев будут использоваться для именования улиц //и городов. 47: static const char* const trees[] = { 48: "Maple", "Oak", "Willow", "Pine", "Hemlock", "Redwood", 49: "Fir", "Holly", "Elm" 50: }; 51: 52: static const char* const streetSuffixes[] = { 53: "St.", "Rd.", "Ln.", "Terr.", "Ave." 54: } ; 55: 56: static const char* const townSuffixes[] = { 57: "ton", "vale", "burg", "ham" 58: }; 59: 60: // Аббревиатуры названий штатов и территорий США. 61: //Получены с Web-страницы почтовой службы США: 62: //http://www.usps.gov/cpim/ftp/pubs/201html/addrpack.htm#abbr 63: static const char* const states[] = { Соберем части вместе 225
64: "AL", "АК", "AS", "AZ", "AR", "CA", "CO", "CT", "DE", 65: "DC", "FM", "FL", "GA", "GU", "HI", "ID", "IL", "IN", 66: "IA", "KS", "KY", "LA", "ME", "MH", "MD", "MA", "MI", 67: "MN", "MS", "MO" ,"MT", "NE", "NV", "NH", "NJ", "NM", 68: "NY", "NC", "ND", "MP", "OH", "OK", "OR", "PA", "PR", 69: "RI", "SC", "SD", "TN", "TX", "UT", "VT", "VA", "VI", 70: "WA", "WV", "WI", "WY" 71: }; 72: 73:for (int i = 0; i < numAddresses; ++i) 74: { 75: Address addr; 76: addr.lastname(randomstring(lastnames)); 77: 78: addr.firstname(randomstring(firstnames)) ; 79: // Генерирование номера телефона с помощью потока // stringstream 80: std::stringstream phonestream; 81: phonestream « •(' « (std::rand() % 800 + 200) « ")" 82: « (std::rand() % 800 + 200) « 83: « std::setfill(101) « std::setw(4) 84: «(std: : rand () % 10000); 85: 86: addr.phone(phonestream.str()); 87: std::stringstream addrstream; 88: // Генерирование названия улицы и номера дома. 89: addrstream « (std::rand() % 100 + 1) « " " 90: « randomstring(trees) « " " 91: 92; « randomstring(streetSuffixes) « ’\n’; 93: // Генерирование названий города, штата и почтового индекса. 94: addrstream « randomstring(trees) « randomstring(townSuffixes) 95: « "," « randomstring(states) « " " 96: « std::setfill(101) « std::setw(5) 97: « (std::rand() % 99999 + 1); 98: 99: addr.address(addrstream.str()); 100: addrbook.insertAddress(addr); 101: } 102: } В программе используются две функции из файла заголовка <cstdlib>, до- бавленного в код в строке 7: rand и srand. Поскольку они унаследованы из языка С, при использовании стандартной библиотеки компании Microsoft возникнет уже известная вам проблема с введением этих функций в пространство имен std. Строки 12-15 служат для устранения этой проблемы. Вместо импортирова- ния этих функций в глобальное пространство имен std, мы переопределяем их в пространстве имен std. Таким образом, в нашем распоряжении будут два варианта функции rand. Функция std: : rand будет просто переадресовывать вызов к глобальной функции : : rand, то же самое справедливо для функции srand. Можно по желанию использовать как импортирование функций в гло- бальное пространство имен, так и переопределение их в стандартном простран- стве имен std, но я предпочитаю всегда обращаться к внешним функциям с префиксом std: :. 226 Глава 8. Простая система меню
В строках 21-27 определяется довольно интересный шаблон функции randomstring. Эта функция не может самостоятельно создавать случайные строко- вые значения для полей записей, но зато умеет случайным образом выбирать их из заранее подготовленного массива. Массив строк-заготовок передается с единствен- ным аргументом этой функции. Поскольку размер массива является составляющей его типа, реализация функции randomstring для конкретного массива требует ука- зания размера массива в параметре шаблона. В строке 24 программа вычисляет число элементов в массиве путем деления действительного размера массива на раз- мер первого элемента. Таким образом, если в параметре А задан тип const char* [ 15 ], то размер массива будет равен 15. В строке 25 вызывается функция rand, которая возвращает псевдослучайное число в диапазоне, заданном константой RAND MAX (RAND МАХ, как минимум, равна 32 767, но обычно соответствует константе INT MAX). С помощью оператора деления по модулю (%) мы вводим возвращенное случайное значение в диапазон размера мас- сива и присваиваем результат переменной index. Таким образом, переменная index служит случайно сгенерированным индексом массива stringArray. Элемент масси- ва, на который указывает index, возвращается в строке 26. Поскольку ожидается возвращение значения типа const char*, то в функцию randomstring можно пере- давать массивы только этого типа. * Следующая функция generateAddress, которая начинается в строке 29, исполь- зует значение, возвращенное функцией stringArray, для создания образца записи адресной книги. В строке 33 с помощью функции s rand задается посев (seed) генера- тора случайных чисел. Следует напомнить, что функция rand возвращает псевдо- случайные. а не действительно случайные последовательности чисел, т.е. компьютер использует специальный алгоритм для возвращения последовательности чисел, распределение значений в которой близко к случайному. Причем последователь- ность чисел полностью определяется значением посева, которое нужно установить до обращения к генератору случайных чисел. Каждому значению посева соответст- вует своя уникальная последовательность псевдослучайных чисел. В нашей про- грамме мы устанавливаем константное значение посева, равное 100. Очень часто для лучшей рандомизации работы программы в качестве аргумента srand использу- ется функция time, которая возвращает текущую дату и время в виде целочисленно- го значения. (Ближе с функцией time мы познакомимся в следующей главе.) Но в нашем случае нам нужно, чтобы при каждом запуске программа генерировала одну и ту же последовательность случайных чисел, чтобы проследить влияние изменений исходного кода на работу программы. Именно поэтому для посева генератора слу- чайных чисел используется константное значение. В строках 35-38 определяется массив литеральных значений фамилий, а в стро- ках 40-44— другой массив, содержащий имена. Еще один массив в стро- ках 47-50 содержит английские названия деревьев, которые будут использоваться как названия улиц и городов. Массив в строках 52-54 содержит дополнения к назва- ниям улиц (типа St. (ул.). Ave (авеню) и т.д.). Окончания названий городов представ- лены в массиве, определенном в строках 56-59. И, наконец, в строках 63-71 опреде- лен массив сокращенных названий штатов, используемых почтовой службой Соеди- ненных Штатов Америки. Выбирая элементы из каждого массива, наша программа будет формировать записи для адресной книги. Каждая такая запись будет содер- жать случайно выбранные фамилии, имена, номера телефона, номера дома, назва- ния улицы, города, штата и почтового индекса. Цикл, начинающийся в строке 73, повторяется столько раз, сколько указано в пе- ременной numAddresses. В каждом цикле создается и инициализируется случайно Соберем части вместе 227
выбранными данными один объект Address, который в конце цикла добавляется в адресную книгу. Так. в строках 76, 77 случайным образом выбираются фамилия и имя. Создать случайный номер телефона не так просто, как могло показаться сна- чала. Следует учесть, что первая и четвертая цифры в 10-значном формате теле- фонных номеров США не могут принимать значения О или 1. Кроме того, номер те- лефона следует отобразить в формате (ddd) ddd-dddd, где d соответствует одной цифре номера. Выполнить задачу было бы проще, если бы вместо текстовой строки в поток вывода подавалось уже отформатированное значение. В языке С с этой це- лью используется функция sprint f. В C++ следует использовать потоки строк. Потоком строк называется объект класса stringstream, унаследованного от класса iostream. Определение этого класса находится в заголовке <sstream>, до- бавленном в код программы в строке 8. Поток строк, так же, как и другие потоки ввода-вывода (такие как cout и cin), поддерживает операторы « и » для ввода и извлечения форматированных данных из потока. Но, вместо того, чтобы посылать текст на терминал или считывать его с клавиатуры, поток строк сохраняет форма- тированное значение в объекте string или возвращает его из объекта string. В строке 80 определяется поток строк под именем phonestream, который будет ис- пользоваться для форматирования случайным образом сгенерированного номера телефона. Формула std:: rand () % 800 + 200, заданная в строке 81, генерирует случайные числа в диапазоне от 200 до 999, что соответствует первым трем цифрам номера, которые начинаются не с О и 1. Затем генерируются еще трехзначное и че- тырехзначное случайные значения, формирующие оставшуюся часть номера, и ме- жду ними вставляется дефис. Установки форматирования задаются для потока phonestream в строках 81-84. Обратите внимание, что все средства потоков ввода- вывода, включая такие манипуляторы форматирования, как setfill и setw, также поддерживаются потоком строк. Функция-член str потока stringstream возвра- щает результат форматирования заданных исходных данных, который и присваива- ется полю номера телефона объекта Address. В строке 87 поле адреса заполняется тем же методом, что и поле номера телефона. В строках 89-91 вводятся случайное число в диапазоне от 1 до 100 (номер дома), слу- чайное название улицы (из массива названий деревьев), случайно выбранное допол- нение к названию улицы и символ разрыва строки. В строках 94-97 название города также случайно выбирается из массива названий деревьев и добавляется окончание, запятая, случайно выбранное сокращенное название штата и пятизначный почто- вый индекс. Обратите внимание, что при создании почтового индекса были уста- новлены О в качестве символа заполнения и ширина поля, равная 5. В результате сгенерированное случайное число 102 примет вид 00102. Полученное отформати- рованное текстовое значение возвращается в строке 98 и присваивается полю адреса объекта Address. Наконец, в строке 100 программа добавляет в адресную книгу вновь созданный объект. Этот процесс повторяется столько раз, сколько было указа- но в аргументе numAddresses, переданном в функцию generateAddresses. Справочная информация: Экскурс потоки fs tream и s trs tream В стандартной библиотеке C++ определены три типа потоков ввода- вывода: stringstream, fstream и strstream. Для каждого типа суще- ствуют три варианта потоков, выполняющих функции вывода, ввода или обе функции одновременно. Классы ostringstream, ofstream и ostrstream произведены от класса ostream и используются для вы- 228 Глава 8. Простая система меню
вода данных, классы istringstream, if stream и istrstream произве- дены от класса istream и используются для ввода данных, классы stringstream, fstream и strstream произведены от класса iostream и используются для ввода-вывода данных. Средства форматирования всех этих классов совершенно одинаковы. Они отличаются только ис- точниками и целями данных. Мы только что познакомились с классом потоков строк stringstream, а теперь рассмотрим классы типов fstream и strstream . Классы fstream объявлены в файле заголовка <fstream> и использу- ются для выполнения операций ввода-вывода с файлами. Все классы типа fstream содержат функцию-член open, которая в качестве аргу- ментов принимает имя файла и (при необходимости) режим открытия файла. Можно устанавливать следующие режимы открытия файла: ios: : in — для ввода данных, ios: : out — для вывода данных, ios: : binary — для двоичного ввода-вывода данных, а также еще неко- торые дополнительные режимы (см. техническое описание вашей стан- дартной библиотеки). Таким образом, функция open открывает файл и подготавливает его к выполнению операций ввода-вывода. Функция close соответственно закрывает файл после его использования. Имя файла и режим открытия могут быть переданы непосредственно в кон- структоры классов if st ream и of st ream, что приводит к автоматиче- скому вызову функции open. Деструкторы этих классов, в свою очередь, автоматически вызывают функцию close. Если класс fstream открыт в текстовом режиме (по умолчанию), то символы разрывов строк записываются в файлы и возвращаются из них. (В DOS и Windows символы разрывов строк при выводе из файла преобразовываются в два символа: возврата каретки и подачи строки.) При открытии файла в двоичном режиме (установлен флаг ios: : binary) символы разрывов строк игнорируются. Класс strstream работает почти так же, как и stringstream, только в этом случае форматированный текст посылается в буфер обмена па- мяти, а не в объект string. Класс strstream рассматривается как от- мирающий, вытесняемый классом stringstream. Вполне вероятно, что он не войдет в следующие версии стандартной библиотеки, ожи- даемые в ближайшие 5-10 лет. Если раньше вы работали с классом strstream, обратите внимание, что манипулятор eos, с помощью ко- торого устанавливался символ нулевого окончания в буфере обмена, не следует использовать с потоками fstream и stringstream, так как в этом случае он добавляет в поток ошибочный нулевой символ. В листинге 8.13 показаны изменения, которые мы внесли в исполняемую про- грамму для подключения функции generateAddresses. 1://TinyPIM (с)1999 Pablo Halpern. Файл TinyPIM.срр 2: 3:#ifdef _MSC_VER 4:#pragma warning(disable : 4786) 5:#endif Соберем части вместе 229
6: 7:#include <iostream> 8: 9:#include "AddressBook.h” 10:#include "AddressBookMenu.h" 11: 12:// Функция генерирования образцов записей адресной книги // (в TestAddrData.срр) 13:extern void generateAddresses(AddressBook& addrbook, 14: int numAddresses); 15: 16:// Исполняемая программа просто вызывает функцию main. 17:int main() 18: { 19: AddressBook addrBook; 20: 21: // Создание 50 записей адресной книги 22: generateAddresses(addrBook, 50); 23: 24: // Создает меню адресной книги и помещает его в стек 25: AddressBookMenu addrBookMenu(addrBook); 26: Menu::enterMenu(&addrBookMenu); 27: 28: // Обработка выбора опций до выхода из меню. 29: while (Menu::isActive()) 30: Menu::activeMenu()->mainLoop(); 31: 32: std::cout « "\nThank you for using TinyPIM’Xn" « std::endl; 33: 34: return 0; 35: } В строках 13, 14 объявляется функция generateAddress. Обратите внимание, как мы обошлись без создания отдельного файла заголовка для этой функции. В строке 22 с помощью функции generateAddress создается 50 образцов записей адресной книги. После выполнения программы на экране отобразится информация, показанная в листинге 8.14. (Данные, полученные на вашем компьютере, могут от- личаться от показанных в листинге, в зависимости от особенностей работы генера- тора случайных чисел в вашем компиляторе, вызываемого функцией rand.) ^Листинг8.14. Информация, отображаемая на экране.сразу;после выполнения h wt функции генерирования образцов записей ; ; *** Address Book * *** =============== Start of list =============== l:Bush, Barbara.............................. 2:Bush, Jack................................. 3:Bush, Jack................................. 4:Bush, Lyndon............................... 5:Bush, Nancy............................... 6:Carter, Barbara............................ 7:Carter, George............................. 8:Carter, George............................. 9:Carter, Lyndon............................. 10:Carter, Rosalynn.......................... (994)342-1167 (793)651-9982 (932)794-5662 (491)421-9782 (420)672-5507 (642)628-2442 (581)409-9816 (483)515-8945 (741)590-6004 (568)301-0648 230 Глава 8. Простая система меню
ll:Carter, William...........................(620)256-2045 12:Clinton, Jimmy...........................(547)348-2090 13:Clinton, Jimmy...........................(499) 318-5124 14:Clinton, Ladybird........................(676) 341-0043 15:Clinton, Lyndon..........................(264)471-2286 (P)revious, (N)ext, (V)iew, (C)reate, (D)elete, (E)dit, list (A)11, (L)ookup, (S)earch, (R)edisplay, (Q)uit ? v Choose a record number between 1 and 15 Record number (0 to cancel)? 11 Name: Carter, William Phone: (620)256-2045 Address: 27 Hemlock Ln.- Maplevale, VA 13217 Press [RETURN ] when ready. Вы можете еще поиграть с этой программой: прокручивать список вниз и вверх, вставлять, изменять и удалять записи, выполнять поиск имен и строк и т.д. Нет предела совершенству: выполнение поиска независимо от регистра букв Давайте еще раз запустим нашу программу и попробуем найти в ней требуемые записи. Для начала найдем первую запись, которая начинается с буквы R, а затем повторим поиск, введя букву г. Результат показан в листинге 8.15. ; Листинг 8.15. Поиск фамилий, начинающихся с букв Лиг 1: 2:*** Address Book *** 3: 4:=============== Start of list =============== 5:1: Bush, Barbara...........................(994) 342-1167 6:2: Bush, Jack..............................(793) 651-9982 7:3: Bush, Jack..............................(932) 7 94-5662 8:4: Bush, Lyndon............................(491) 421-9782 9:5: Bush, Nancy.............................(420)672-5507 10:6: Carter, Barbara.........................(642)628-2442 11:7: Carter, George..........................(581) 409-9816 12:8: Carter, George..........................(483)515-8945 13:9: Carter, Lyndon..........................(741) 590-6004 14:10: Carter, Rosalynn.......................(568)301-0648 15:11: Carter, William........................(620)256-2045 16:12: Clinton, Jimmy.........................(547) 348-2090 17:13: Clinton, Jimmy.........................(499) 318-5124 18:14: Clinton, Ladybird......................(67 6)341-0043 19:15: Clinton, Lyndon........;................(264) 471-2286 20: 21: 22:(P)revious, (N)ext, (V)iew, (C)reate, (D)elete, (E)dit, 23:list (A)11, (L)ookup, (S)earch, (R)edisplay, (Q)uit ? L Нет предела совершенству... 231
24:lookup name (lastname[,firstname]): R 25: Очистка экрана (новая страница) ' 27: 28:*** Address Book *** 29: 30:1: Reagan, Barbara..........................(577)755-4801 31:2: Reagan, Jackie...........................(393)699-0036 32:3: Reagan, Jackie...........................(705)203-7648 33:4: Reagan, Ladybird.........................(625)867-5077 34:5: Reagan, Nancy...........................(981) 896-2771 35:6: Reagan, Ronald...........................(336)634-1605 36:7: Reagan, Ronald.........................(4 61) 478-6096 37:8: Reagan, Rosalynn.........................(636)506-9142 38:===============End of list =============== 39: 40:(P)revious, (N)ext, (V)iew, (C)reate, (D)elete, (E)dit, 41:list (A)11, (L)ookup, (S)earch, (R)edisplay, (Q)uit ? L 42:lookup name (lastname[,firstname]): r 43: : Очистка;экрана (новая страница) 45: 46:*** Address Book *** 47: 48:=============== Start of list =============== 49:1: Bush, Barbara............................(994)342-1167 50:2: Bush, Jack...............................(793)651-9982 51:3: Bush, Jack...............................(932)794-5662 52:4: Bush, Lyndon.............................(491) 421-9782 53:5: Bush, Nancy..............................(420)672-5507 54:6: Carter, Barbara..........................(642) 628-2442 55:7: Carter, George...........................(581)409-9816 56:8: Carter, George...........................(483) 515-8945 57:9: Carter, Lyndon...........................(741) 590-6004 58:10: Carter, Rosalynn.........................(568)301-0648 59:11: Carter, William..........................(620)256-2045 60:12: Clinton, Jimmy..........................(547) 348-2090 61:13: Clinton, Jimmy..........................(4 99) 318-5124 62:14: Clinton, Ladybird.......................(67 6) 341-0043 63:15: Clinton, Lyndon.........................(264)471-2286 64: 65: 66:(P)revious, (N)ext, (V)iew, (C)reate, (D)elete, (E)dit, 67:list (A)11, (L)ookup, (S)earch, (R)edisplay, (Q)uit ? В строке 24 задается поиск записей, в которых фамилии начинаются с буквы R. Как и ожидалось, экранный список был прокручен таким образом, что первой види- мой записью стала Reagan, Barbara— первая запись, начинающаяся с R (см. стро- ки 30-37). Но когда для поиска мы задали ту же букву, но в нижнем регистре, на эк- ране отобразилось начало списка (см. строки 49-63). Вряд ли это поведение про- граммы можно назвать дружественным. Кроме того, это означает, что если пользователь по ошибке введет фамилию со строчной буквы, например Clinton, Кеппу, то эта запись не будет размещена программой между записями Clinton, Jimmy и Clinton, Ladybird. Не только поиск, но и сортировка записей в нашей ад- ресной книге оказались чувствительными к регистру букв. Как же нам сделать сор- тировку и поиск записей нечувствительными к регистру? 232 Глава 8. Простая система меню
Создание объекта функции сравнения, нечувствительной к регистру Чтобы сделать сортировку и поиск записей адресной книги нечувствительными к регистру букв, нужно изменить подход к сортировке записей в классе multiset. Для этого нужно задать другой параметр шаблона при реализации класса multiset. Шаб- лоны set и multiset принимают три параметра: тип элемента, объект функции сравнения и функция распределения (allocator). Шаблоны тар и multimap принимают четыре параметра: тип ключа, тип содержимого, объект функции сравнения ключей и функция распределения. По умолчанию задается объект функции сравнения less<T>, где т— тип элемента. (Функция распределения также обычно задается по умолчанию и редко замещается пользовательской. Этот вопрос мы уже рассматривали в главе 3.) Объект функции less в своей работе использует оператор < (меньше). Что- бы изменить метод сортировки записей в контейнере multiset по своему усмотре- нию, нужно реализовать его с новым пользовательским объектом функции с замещен- ным оператором < (меньше), не чувствительным к регистру букв. Соответствующие изменения объявления класса AddressBook показаны в листинге 8.16. I Листинг 8.16. Изменение класса AddressBook для сортировки записей % j независимо от регистра букв j 1:// TinyPIM (с)1999 Pablo Halpern 2: 3:#ifndef AddressBook_dot_h 4:#define AddressBook_dot_h 5: 6:#include <set> 7:#include <map> 8:#include <functional> 9:#include "Address.h" 10: 11:// Объект функции "меньше чем", не чувствительной к регистру. 12:struct AddressLess 13: :public std::binary__function<Address, Address, bool> 14:{ 15: bool operator()(const Address& al, const Address& a2) const; 16:}; 17: 18:class AddressBook 19: { 20: // Определения типов 21: typedef std: :multiset<Address, AddressLess> addrByName__t; 22: typedef std::map<int, addrByName_t::iterator> addrById_t; 23: 24: // Продолжение объявления класса AddressBook В строках 12-16 определяется объект функции Address Less, которая выполняет сравнение объектов Address независимо от регистра букв. В отличие от исходной про- стой версии объекта функции, выполнение оператора operator () уже не следует в одной строке вслед за его объявлением. В строке 21 было изменено определение типа addrByName_t. Мы добавили в список параметров шаблона multiset объект функции сравнения. Теперь все операции сортировки и поиска в контейнере будут выполняться с использованием нового класса функции сравнения, который мы только что определили. Нет предела совершенству... 233
Справочная информация: Экскурс указатели как ассоциативные ключи В качестве ключей ассоциативных контейнеров можно использовать указатели. Действительно, набор указателей можно использовать для отслеживания отношений одии-ко-многим между объектами. (Один объект содержит набор указателей на другие объекты, с которыми он связан.) Потенциальная проблема состоит в том, что в стандартах язы- ков С и C++ не гарантирована работа операторов сравнения при при- менении их к указателям на объекты, не принадлежащих тому же мас- сиву. В стандартной библиотеке эта проблема решается путем предос- тавления специализированной версии шаблона less<T>, который позволяет указывать в параметре т тип указателя, даже если встроен- ный оператор < (меньше) не поддерживает сравнение указателей. Если в программе используется контейнер указателей, то следует пом- нить, что указатели не удаляются автоматически вместе с контейне- ром. Очистить память от указателей не так просто. Часто начинающие программисты пытаются сделать это с помощью конструкций, анало- гичных следующей: set<myclass*> myset; for (set<myclass*>::iterator j = myset.begin() ; j != myset.end(); ++j) { myclass* p = *j; myset.erase (j); delete p; } Эта конструкция работать не будет, поскольку вызов функции erase делает бессмысленным итератор, используемый в строке условий цик- ла. Проще всего решить проблему удаления контейнера вместе со всеми указателями следующим образом: set<myclass*> myset; while (! myset.empty()) { myclass* p = *myset.rend(); myset.erase(myset.rend() ) ; delete p; } Этот код просто удаляет последний элемент в контейнере до тех пор, пока контейнер не окажется пустым. Этот идиоматический подход го- дится для контейнеров всех типов. Написание нового алгоритма Принципы сравнения объектов по ряду признаков с целью определения первого в лексикографической последовательности хорошо известны. Поэтому для вас не бу- дет неожиданностью тот факт, что в стандартной библиотеке уже есть алгоритм, призванный выполнять данную задачу. Это алгоритм lexicographical compare, 234 Глава 8. Простая система меню
который принимает в качестве аргументов два диапазона итераторов и, при необхо- димости, объект функции сравнения (по умолчанию используется функция сравне- ния, основанная на операторе <). Алгоритм возвращает true, если первый ряд при- знаков оказывается меньше второго. На этом алгоритме основано сравнение объек- тов в новой версии класса адресной книги, как показано в листинге 8.17. т Лексикографическим упорядочиванием называется концепция словар- I ерМИН H0£ сортировки объектов разных типов, которые не обязательно долж- ны быть текстовыми строками. Элементы двух последовательностей сравниваются попарно друг с другом до тех пор, пока не будут найдены отличия. Набору элементов, содержащему меньший элемент, присваи- вается более низкий рейтинг в последовательности. Если между эле- ментами нет различий, то более низкий рейтинг присваивается объекту с меньшим числом элементов. Если два объекта совпадают друг с дру- гом как по числу элементов, так и по их значениям, то объектам при- сваивается одинаковый рейтинг. Листинг 8.17. Реализация сравнения, независимого от регистра букв 1://TinyPIM (с)1999 Pablo Halpern. Файл AddressBook.срр 2: 3:#ifdef _MSC_VER 4:#pragma warning(disable : 4786) 5:#endif 6: 7:#include <algorithm> 8:#include <cctype> 9: 10:#include "AddressBook.h" 11: 12:#if !(_MSC_VER || ___GCC__) 13:using std::tolower; 14:using std::toupper; 15:#endif 16: 17:// Объект функции "меньше чем", не чувствительный к регистру букв 18:struct ci_less__char : public std::binary_function<char, char, bool> 19: { 20: bool operator()(char cl, char c2) const 21: { 22: return toupper(cl) < toupper(c2); 23: } 24 : } ; 25: 26:// Алгоритм нечувствительного к регистру лексикографического сравнения 27:// двух последовательностей символов 28:template <class Fwdlterl, class Fwdlter2> 29:bool ci_less(Fwdlterl bl, Fwdlterl el, Fwdlter2 b2, Fwdlter2 e2) 30: { 31: return std::lexicographical_compare(bl, el, b2, e2, 32: ci_less_char()); 33: } 34: 35:// Нечувствительная к регистру функция сравнения строк 36:bool ciStringLess(const std::string& si, const std::string& s2) Нет предела совершенству... 235
37: { 38: return ci_less (si.begin () , sl.endO, s2. begin (), s2.end()); 39: } 40: 41:bool AddressLess::operator ()(const AddressS al, 42: const AddressS a2) const 43: { 44: if (ciStringLess(al.lastname(), a2.lastname())) 45: return true; 46:else if (ciStringLess(a2.lastname(), al.lastname())) 47: return false; 48: else 49: return ciStringLess(al.firstname() , a2.firstname()); 50: } В строках 18-24 создается объект функции для выполнения нечувствитель- ного к регистру сравнения двух символов. Для того чтобы сделать сравнение не- чувствительным к регистру, в строке 22 оба символа приводятся к верхнему ре- гистру. Если символы не являются буквами алфавита, то функция toupper воз- вращает их неизменными. В строках 28-33 мы делаем то, чем раньше не занимались: пишем наш собствен- ный алгоритм. Как уже говорилось раньше, в стандартной библиотеке и ее составной части, известной как STL, заложен принцип расширяемости, который позволяет нам добавлять собственные алгоритмы. Новый алгоритм мы назвали ci less (сокращение от case-tnsensitive-less— нечувствительный к регистру алгоритм less). Он принимает две последовательности элементов в виде диапазона итераторов и возвращает true, если первая последовательность лексикографически меньше вто- рой. Для определения двух диапазонов итераторов нам в общей сложности потребу- ется передать в алгоритм ci less четыре аргумента (см. строку29). Причем пары аргументов первый-второй и третий-четвертый должны быть представлены итера- торами одного и того же типа (соответственно Fwdlterl и Fwdlter2, которые могут совпадать или быть разными). В строке 28 Fwdlterl и Fwdlter2 объявляются как параметры шаблона. В соответствии с этим объявлением алгоритм ci less можно реализовать с любым типом данных, допустимым для прямого итератора (как вы помните, также допускается использование итераторов наиболее высокой катего- рии: двухстороннего и произвольного доступа). Алгоритм ci less реализуется в строках 31, 32 в класс объекта функции сравне- ния cilesschar, из которой вызывается стандартная библиотечная функция lexigcographical compare. (Вспомните, что пара пустых круглых скобок после ci_less_char означает вызов конструктора для создания объекта этого класса.) Поскольку функция ci less char: : operator () ожидает два аргумента типа char, то компилятор покажет сообщение об ошибке, если вы попытаетесь передать в шаб- лон функции ci less итераторы, которые не указывают на элементы типа char или другого типа, свободно конвертируемого в char. Что мы можем делать с нашим алгоритмом ciless? Если у нас есть два контей- нера vector<char> и list<char>, то с помощью ci less мы можем сравнить их элементы. Здорово! Но трудно представить, где это можно применить на практике. Хорошо, кроме того, используя указатели вместо итераторов, мы можем сравнить два массива типа char, в которых будут представлены строки в стиле С. Но что мы можем использовать в качестве итераторов для сравнения двух объектов std: : string? Сообщим еще не известный вам факт: объекты string не только под- держивают итераторы (включая функции begin, end, rbegin и rend), но и соответ- 236 Глава 8. Простая система меню
ствуют стандартам последовательных контейнеров. Таким образом, алгоритмы сравнения можно применять не только к контейнерам, но и к отдельным объектам string. В строках36-39 определяется функция ciStringLess, которая принимает в качестве аргументов два объекта string и возвращает true, если первый объ- ект лексикографически и без учета регистра букв будет меньше второго. Эта функция выполняется простым вызовом функции ci less в строке 38, где в ар- гументах вместо итераторов передаются обращения к функциям-членам begin () и end () объектов string. Теперь мы можем перейти к сравнению объектов Address, попарно сравнивая строковые значения их полей без учета регистра букв. В строках 41-50 определяется operator () для объекта функции AddressLess. Этот объект функции будет исполь- зоваться нами для упорядочивания элементов контейнера multiset. В стро- ке 44 сравниваются поля фамилий и возвращается true, если первое значение будет меньше второго, или false, если второе значение меньше. Если же значения равны, то выполнение программы переходит к строке 49, где сравниваются поля имен. Если мы теперь вновь запустим программу и введем запись Clinton, кеппу, то ре- зультат отобразится так, как показано в листинге 8.18. : Листинг 8.18. Результат применения алгоритма сравнения, нечувствительного регистру^ 1 *** Address Book *** ===============Start of list =============== l:Bush, Barbara............................(994) 342-1167 2:Bush, Jack...............................(793)651-9982 3:Bush, Jack...............................(932)794-5662 4:Bush, Lyndon.............................(4 91) 421-9782 5:Bush, Nancy..............................(420)672-5507 6:Carter, Barbara..........................(642) 628-2442 7:Carter, George...........................(581) 409-9816 8:Carter, George...........................(483) 515-8945 9:Carter, Lyndon...........................(741)590-6004 10:Carter, Rosalynn.........................(568)301-0648 ll:Carter, William..........................(620)256-2045 12:Clinton, Jimmy............................(547)348-2090 13:Clinton, Jimmy............................(4 99)318-5124 14:clinton, kenny............................(123)456-7890 15:Clinton, Ladybird.........................(67 6) 341-0043 (P)revious, (N)ext, (V)iew, (C)reate, (D)elete, (E)dit, list (A)11, (L)ookup, (S)earch, (R)edisplay, (Q)uit ? Вы можете самостоятельно опробовать поиск записей, вводя буквы в верхнем и нижнем регистрах, чтобы убедиться, что поиск стал нечувствительным к регистру. Функция фильтрации записей по ключевым словам не использует в своей работе ал- горитм сортировки, поэтому она осталась прежней, чувствительной к регистру. Но эту проблему можно устранить с помощью того же подхода. Алгоритм std: : search можно использовать для отысканйя вхождения последовательностей элементов в контейнерах. Если передать в эту функцию итераторы двух объектов string и не- чувствительную к регистру функцию сравнения, то поиск вхождений также станет нечувствительным к регистру. Вы уже в полной мере овладели этими подходами, чтобы самостоятельно справиться с поставленной задачей. Нет предела совершенству... 237
Резюме Итак, мы уже получили что-то, напоминающее задуманное приложение TinyPIM. Была разработана система меню, объединившая вместе разрозненные блоки про- граммы.'В ходе выполнения этой задачи мы познакомились с адаптером stack, средствами файла заголовка <climits> и потоками строк. Затем мы протестировали программу с помощью генератора случайных записей адресной книги, основанного на функциях rand и srand. Чтобы сделать поиск и сортировку записей нечувстви- тельными к регистру букв, мы создали собственный алгоритм сравнения ci less, расширив тем самым нашу библиотеку STL. Теперь мы можем заняться разработкой книги контактов. Прежде всего следует разобраться со стандартными средствами представления даты и времени. В сле- дующей главе мы создадим классы даты и времени на основе библиотечных шабло- нов и узнаем, как настроить систему ввода-вывода для поддержания созданных классов даты и времени. 238 Глава 8. Простая система меню
Глава 9 Классы даты и времени с пользовательской системой ввода-вывода В этой главе... • Реализация классов даты и времени • Реализация функций ввода и вывода времени • Использование класса DateTime в книге контактов • Резюме 239 245 256 266 Реализация классов даты и времени Завершив работу над адресной книгой приложения TinyPIM, займемся теперь созданием книги контактов. Для каждой записи в этой книге должны быть опреде- лены дата и время начала и окончания мероприятия. Эти значения используются для сортировки записей в книге. Очевидно, что представление даты и времени кон- такта, а также манипулирование этими значениями — одни из самых важных мо- ментов программирования книги контактов. Поэтому работу над этим блоком при- ложения TinyPIM мы начнем с создания классов даты и времени. Вы узнаете о стан- дартных библиотечных средствах, предназначенных для управления данными даты и времени, и некоторых дополнительных возможностях системы ввода-вывода. Использование типа Вы, наверное, с нетерпением ждете, что я познакомлю вас с какими-то супер- мощными классами библиотеки C++, манипулирующими датами и временем. Но, к сожалению, в C++ эта область программирования почему-то развивалась не осо- бенно стремительно, и стандартная библиотека располагает в основном средствами, унаследованными из языка С. А немногочисленные свежие наработки в этой области реализованы далеко не во всех современных компиляторах разных изготовителей. Эти, как и многие другие недостатки стандартной библиотеки, объясняются тем, что в комитет по стандартизации своевременно не поступили заявки от разработчиков на дополнение библиотеки новыми средствами. Большинством было принято реше- ние, что лучше сегодня иметь несовершенные стандарты, чем еще десять лет ждать появления совершенных.
Сейчас мы познакомимся с типом данных time t, унаследованным из языка С. Чтобы эффективнее использовать средства языка С, мы внедрим их в наш собствен- ный класс DateTime, который затем используем в приложении TinyPIM. Это будет небольшой класс, назначение которого состоит не в том, чтобы залатать дыры в стандартной библиотеке C++, а в том, чтобы разработать простейший интерфейс, оптимально подходящий для разработки нашего приложения. Содержимое файла заголовка этого класса показано в листинге 9.1. Листинг 9.1. Класс DateTime Ц| 1://TinyPIM (с)1999 Pablo Halpern. Файл DateTime.h 2: 3: ftifndef DateTime_dot__h 4:#define DateTime_dot_h 1 5: 6:#include <iostream> 7:#include <string> 8:#include <ctime> 9: 10:#ifdef _MSC_VER 11:// Проверка вхождения указанных типов и функций в пространство имен std: 12:namespace std { 13: typedef ::time_t time_t; 14: typedef ::tm tm; 15: inline double difftime(time_t tl, time_t tO) 16: {return ::difftime(tl, tO);} 17: } 18:#endif 19: 20:class DateTime 21: { 22:public: 23: DateTime() : theTime_(0) {} 24: DateTime(int year, int month, int day, int hour, int min); 25: 26: // Использование сгенерированных компилятором конструктора- // копировщика, деструктора и оператора присваивания 27: 28: // Функции доступа для чтения 29: void get(int& year, int& month, int& day, 30: int& hour, int& min) const; 31: 32: // Функции доступа для записи 33: DateTime& set(int year, int month, int day, int hour, int min); 34: 35: // Возвращение текущей даты и времени 36: static DateTime now(); 37: 38: friend bool operator == (const DateTime& dtl, const DateTime& dt2) 39: {return dtl.theTime_ == dt2.theTime_;} 40: 41: friend bool operator < (const DateTime& dtl, const DateTime& dt2) 42: {return std::difftime(dtl.theTime_, dt2.theTime_) < 0;} 43: 240 Глава 9. Классы даты и времени с пользовательской системой...
44: friend std::ostream& operator « (std::ostream& os, 45: const DateTime& dt); 46: friend std::istream& operator » (std::istream& is, DateTime& dt); 47: 48:private: 49: 50: std::time_t theTime_; 51: }; 52: 53: 54:#endif // DateTime dot h В строке 50 объявляется единственная переменная-член типа std: : timet. Это вычисляемый тип (т.е. он представлен целыми числами или значениями с плаваю- щей запятой), объявленный в файле заголовка <ctime> для поддержания значений даты и времени. Представление даты и времени в виде значений типа time t иде- ально подходит для реализации нашего класса DateTime, в котором для манипули- рования этими значениями будут использоваться стандартные функции, унаследо- ванные из языка С. Как вы уже знаете, компилятор компании Microsoft версии 6.0 не вставляет автоматически классы и функции, унаследованные из С, в пространство имен std. Строки 10-18 как раз и предназначены для того, чтобы создать в простран- стве имен std псевдонимы этих средств. Псевдонимы типов time t и tm созда- ются простым определением типов с помощью typedef. Функция difftime пе- реопределяется в пространстве std с вызовом к одноименной глобальной функ- ции. Безусловно, ввод этих типов и функции в стандартное пространство имен std не закрывает глобальный доступ к ним. Но обращение к ним со специфика- тором std: : позволит отличить внешние функции и типы функций от типов, определенных локально в классе. Интерфейс класса DataTime позволяет использовать данные, представляющие год, месяц, день, час и минуту. Объектам этого класса значения присваиваются с помощью конструктора, определенного в строке 24, или с помощью функции- члена set, определенной в строке 33. Другая функция доступа— get, определен- ная в строке 29, возвращает сохраненные данные даты и времени. Для наших це- лей достаточно устанавливать время с точностью до минуты. В строке 36 опреде- ляется статическая функция now, которая возвращает текущие системные дату и время. Поскольку значения даты и времени будут использоваться для сортиров- ки записей в книге контактов, для этих целей в строках 38-42 определяются опе- раторы отношений. Обратите внимание, что в строке 42 используется функция difftime, которая возвращает временной промежуток между двумя значениями типа time t. Если разница значений окажется отрицательной, значит, первое значение было меньше второго. Поскольку тип time t является вычисляемым, то мы могли бы сравнить эти значения также с помощью оператора <. Но проблема состоит в том, что числовое кодирование даты и времени не расписано в стандар- тах. Поэтому нельзя гарантировать, что во всех версиях компиляторов более позд- няя дата будет представлена большим числом, чем более ранняя. Идея использо- вать числовые значения дат и времени выглядит очень соблазнительно, тем более, что в трех наиболее популярных системах DOS, Windows и UNIX значения типа time t совершенно одинаковы и представляют число секунд начиная с полуночи 1 января 1970 года. В других операционных системах используются аналогичные Реализация классов даты и времени 241
подходы, но за основу берется другое число. Если вы уверены, что ваше приложе- ние будет использоваться только на компьютерах с указанными операционными системами, то код программы можно упростить и сделать более эффективным, за- менив многие косвенные операции со значениями даты и времени на обычные ма- тематические вычисления. В этой книге мы рассмотрим на примерах, как можно добиться большей совместимости приложения с разными системами, не полагаясь на арифметические операции с датами и временем. В нашем приложении наиболее часто используемыми операциями над датами и временем будут ввод и вывод. Операторы, объявленные в строках 44 и 46, добав- ляют в библиотеку потоков операции над объектами класса DateTime. Вот как это происходит. Если myTime— это объект класса DateTime, то при обнаружении ком- пилятором инструкции std::cout « myTime; он вызывает оператор, объявленный в строке 44. При этом в функцию оператора как параметр os передается объект std: : cout и как параметр dt — объект myTime. Оператор « определен как друг класса DateTime. Это означает, что, оставаясь гло- бальной функцией (обратите внимание, что оператор вывода не стал членом класса DateTime, хотя его функция объявлена в нем), функция-друг получает доступ к за- крытым членам класса DateTime. Реализация этой функции основана на базовых средствах потоков ввода-вывода, с помощью которых можно форматировать данные уже известным нам способом и посылать их в объекты потоков (в данном случае cout). Затем функция возвращает свой параметр os. Поскольку результатом выпол- нения функции operator« является ее же параметр, появляется возможность объ- единять строки вывода следующим образом: std::cout << "The time is " << myTime « std::endl; Каждый вывод значения в объект cout завершается возвращением ссылки на объект cout, который затем можно использовать для следующего вывода. При опре- делении нескольких версий перегруженной функции в C++ компилятор автоматиче- ски решает, какую функцию использовать по типу переданных аргументов. Функция оператора ввода operator« перегружается аналогичным образом. Обратите вни- мание, что для расширения системы ввода-вывода нам не пришлось изменять базо- вые классы ostream и istream. Более того, эти же функции будут работать со всеми другими потоками: stringstream, fstream, strstream и т.д. Такая расширяемость не была характерна для функций ввода-вывода языка С, таких как print f и scant. Компоновка и извлечение значений типа time_t Сейчас мы приступим к реализации функций ввода-вывода. Сначала рассмот- рим базовые функции-члены класса DateTime для заполнения объектов этого класса данными о годе, месяце, дне, часе и минуте, а также для извлечения этих данных по запросу пользователя. Реализация этих функций в классе DateTime по- казана в листинге 9.2. Листинг 9.2, Функции компоновки и извлечения значений класса DateTime 1://TinyPIM (c)1999 Pablo Halpern. Файл DateTime.cpp 2: 3:#include <iomanip> 4:#include "DateTime.h" 242 Глава 9, Классы даты и времени с пользовательской системой...
5: 6:#ifndef _MSC_VER 7:using std::mktime; ’8:using std::localtime; 9:using std::time_t; 10:#endif 11: 12:DateTime::DateTime(int year, int month, int day, int hour, int min 13: { 14: set(year, month, day, hour, min); 15: } 16: 17:void DateTime::get(int& year, int& month, int& day, 18: int& hour, int& min) const 19: { 20: std::tm* mytm = localtime(&theTime_); 21: year = mytm->tm_year + 1900; 22: month = mytm->tm_mon + 1; 23: day = mytm->tm_mday; 24: hour = mytm->tm_hour; 25: min = mytm->tm_min; 26: } 27: - 28:DateTime& DateTime::set(int year, int month, int day, 29: int hour, int min) 30: { 31: // Значения от 0 до 49 соответствуют годам 2000—2049. 32: // Значения, превышающие 1900, распознаются как соответствующие // годы. 33: //И прочие значения обозначают годы от 1950 до 1999. 34: if (year < 50) 35: year += 2000; 36: else if (year < 1900) 37: year += 1900; 38: 39: std: : tm mytm; 40: mytm.tm_year = year - 1900; 41: mytm.tm__mon = month - 1; // Начало отсчета с нуля 42: mytm.tm_mdaу = day; 43: mytm.tm_hour = hour; 44: mytm.tm_min = min; 45: mytm.tm_s e c = 0; 46: mytm.tm_isdst = -1; 47: 48: theTime_ = mktime (&mytm) ; 49: 50: return *this; 51: } 52: 53:// Возвращение текущей даты и времени 54:DateTime DateTime::now() 55: { 56: DateTime ret; 57: ret.theTime_ = time(0); // Вызов std::time 58: return ret; 59: } 60: Реализация классов даты и времени 243
В строке 20 функция get использует стандартную функцию localtime для пре- образования значения time t в структуру std: : tm. Аргументом функции localtime является указатель на значение типа const time t. Структура tm определяется следующим образом: struct tm { int tm_sec; int tm__min; /* /* секунд в минуте - [0,60] */ минут в часе - [0,59] * ч int tm_hour; /* часов после полуночи - [0,23] */ int tm_mday; /* дней в месяце - [1,31] */ int tm_mon; /* месяцев после января - [0,11] */ int tm_year; int tm_wday; /* /* лет с 1900 г */ дней после воскресенья - [0,6] */ int tm_yday; /* дней после 1-го января - [0,365] */ int tm_isdst; /* флаг автоматического перехода на летнее время и обратно */ }; На первый взгляд данная структура выглядит как довольно прямолинейное рас- членение значений даты и времени на составляющие элементы. Но при ближайшем рассмотрении можно заметить множество нюансов. Для первого дня месяца перемен- ная-член tm_mday устанавливается на 1. Но переменная-член tm__mon для первого ме- сяца в году устанавливается на 0, а не на 1. Поэтому в строке 22 листинга 9.2 для пере- хода к обычной нумерации месяцев »мы прибавляем единицу к текущему значению. Переменная tm year отсчитывает годы начиная с 1900 г., т.е. 2000 год в этой системе будет представлен значением 100. Можно было бы остановиться и на таком подходе, не опасаясь даже проблемы 2000 года, но почему бы не представлять годы привычными нам четырехзначными цифрами. Для этого вполне подойдет стандарт- ный 16-разрядный тип int. К нормальному виду значение года в нашей программе приводится в строке 21, где к текущему значению tm_year просто прибавляется 1900. Обратите внимание, что функция local time возвращает указатель на статическую структуру данных, принадлежащую библиотеке. Не пытайтесь удалять эту структуру или освобождать занятую ею память. Статическая структура даты и времени сущест- вует автономно и замещается новыми данными каждый раз при вызове функции local time. Возвращенные данные можно либо сразу выводить, либо сохранять в структуре tm, над которой вы имеете полный контроль. Это свойство статической структуры даты и времени ограничивает использование связанной с ней функции localtime, так как одновременное обращение к ней из конкурирующих процессов мо- жет привести к ситуации неопределенности из-за непредсказуемости замещений дан- ных структуры. Операционная система POSIX поддерживает вариант localtime, в ко- тором предусмотрено предупреждение конфликтов между конкурирующими процес- сами. Но эти моменты никак не отражены в современных стандартах языков С и C++. В строке 48 листинга 9.2 функция set вызывает функцию mktime, которая про- тивоположна по действию функции local time. Эта функция принимает адрес структуры tm и преобразовывает ее в соответствующее значение time t. Если структуру tm невозможно преобразовать в time t, то функция mktime возвращает time_t (-1). При обратном преобразовании даты и времени в структуру tm нужно вычесть 1900 от значения года и единицу — от значения месяца. Переменная-член tm_isdst определяет, будет ли функция mktime учитывать пере- ход с зимнего времени на летнее и наоборот. (Летнее время сдвинуто на один час впе- ред.) Если tm isdst установлена на нуль, то всегда используется стандартное время, если же tm isdst имеет положительное значение, устанавливается летнее время. Если 244 Глава 9. Классы даты и времени с пользовательской системой...
значение tm isdst отрицательно, то функция mktime вычисляет, следует ли приме- нять летнее время к указанной дате в соответствии с региональными соглашениями. В строке 46 переменной mktime присваивается значение -1, чтобы наше приложение учитывало региональные соглашения о переходе на летнее время и обратно. Перемен- ные-члены tm wday и tm yday не учитываются функцией mktime. Функция now просто вызывает в строке 57 стандартную функцию time, которая возвращает значение типа time t, соответствующее текущему системному времени и дате. Если в функцию time передается ненулевой указатель, то возвращенное значение сохраняется по указанной позиции. Так, строку 57 можно было бы перепи- сать следующим образом: time (sret. theTime) ;. Реализация функций ввода и вывода времени Операторы ввода и вывода (>> и « соответственно) представлены глобаль- ными функциями, которые объявлены друзьями класса DateTime. Благодаря этому они имеют доступ к закрытой функции-члену theTime_ класса DateTime. В следующем разделе мы займемся разработкой функций операторов ввода и вывода для нашего класса. Базовый ввод и вывод В листинге 9.3 показана базовая реализация функции оператора вывода, которая форматирует данные даты и времени в стиле mm/dd/yyyy hh::mmAM (месяц/день/год часы::минуты до/после полудня). Листинг 9.3. Оператор вывода класса DateTime 61:std::©streams operator«(std::©streams os, const DateTimes dt) 62: { 63: std::tm theTm = *localtime(Sdt.theTime_); 64: 65: os « (theTm.tm_mon + 1) << •/’ 66: « theTm. tm_mday « 67: « (theTm. tm_ye ar + 1900) « • 68: 69: int hour = theTm.tm_hour % 12; 70: const char* ampm ~ (theTm.tm_hour < 12 ? "am" :: "pm"); 71: 72: if (hour == 0) 73: hour =12; 74: 75: os « std::setfill(' ') « std::right « std::setw(2) « hour 76: « « std::setfill(’0’) « std::right « std::setw(2) 77: << theTm.tm_min << ampm; 78: 79: return os; 80: } 81: Реализация функций ввода и вывода времени 245
В строке 63 вызывается функция local time для возвращения текущих даты и времени и сохранения этих значений в переменной theTm. В строках 65-67 выво- дятся текущие месяц, день и год, разделенные дефисами. В строках 69-73 значения времени из 24-часового формата преобразуются в 12-часовой с добавлением ат (до полудня) и рт (после полудня). Строка времени в формате часы-минуты-am/pm на- правляется в поток вывода в строках 75-77. Для отображения времени в требуемом виде используются манипуляторы форматирования setfill, setw и right. Функ- ция оператора вывода возвращает в строке 79 объект потока, что позволяет конка- тенировать операции вывода в одну строку. Функция оператора ввода принимает данные даты и времени в стандартном формате и преобразовывает их в объект DateTime. В листинге 9.4 показано, как это происходит. i Листинг 9.4. Оператор ввода класса DateTime 82:std::istream& operator>>(std::istream& is, DateTime& dt) 83: { 84: char slashl, slash2, colon; 85: int mon, day, year, hour, min; 86: char ampm[3]; 87: 88: is » mon » slashl » day » slash2 » year 89: » hour » colon » min » std::setw(3)>> ampm; 90: 91: // Контроль за ошибками ввода 92: if (is.fail()) 93: return is; 94: 95: // Преобразование значения времени из 12-часового формата // в 24-часовой (0-23) 96: if (hour == 12) 97: hour = 0; 98: if (ampm [0] == ’p* || ampm[0] ~ 'P') 99: hour += 12; 100: 101: dt = DateTime(year, mon, day, hour, min); 102: 103: return is; 104: } Обратите внимание, что в строке 82 аргумент dt передается как ссылка для запи- си. Эта ссылка используется функцией оператора для записи в объект DateTime данных, считанных из буфера ввода. В строках 88, 89 считываются сразу все компо- ненты: месяц, день, год, час, минуты и индикаторы am/pm, а также символы пунк- туации. Пробел между годом и часом автоматически сбрасывается оператором при вводе. Индикаторы am/pm заносятся в буфер с фиксированной длиной, равной трем символам. Чтобы предупредить перезаполнение буфера в результате ошибки, в стро- ке 89 используется манипулятор setw. Если при считывании любого из компонентов произойдет ошибка (например, в позиции, где ожидается цифра, будет обнаружена буква), то функция is . f ail () в строке 92 возвратит true. Считывание следующих компонентов прекратится, объект по ссылке dt не будет изменен, а выполнение функции завершится возвра- том объекта ввода с установленным флагом ошибки. Если же ошибки не обнару- 246 Глава 9. Классы даты и времени с пользовательской системой...
жатся, то в строках 96-99 значения времени в 12-часовом формате преобразуются в 24-часовой формат с учетом индикаторов am/pm. Заполнение объекта DateTime считанными и отформатированными значениями происходит в строке 101, после чего функция возвращает объект ввода, что позволяет конкатенировать операции ввода в одну строку. Мы создали практически весь код, необходимый для работы класса DateTime, и теперь можем реализовать программу тестирования, представлен- ную в листинге 9.5. ' Листинг 9.5. Программа тестирования класса DateTime . 1://TinyPIM (с)1999 Pablo Halpern. Файл timeTest.cpp 2: 3:#include <iomanip> 4:#include "DateTime.h" 5: 6: int main() 7: { 8: // Проверка функции now() 9: std::cout « "Now =" « DateTime::now() « std::endl; 10: 11: // Проверка конструктора 12: std::cout << "Party =" << DateTime(0,1,1,0,0) « std::endl; 13: 14: DateTime dt; 15: 16: while (std::cin) 17: { 18: std::cout « "\nEnter a date and time:"; 19: if (std::cin.peek()== 'q* II std::cin.peek() == *Q’)) 20: break; 21: 22: std::cin » dt; 23: if (std::cin.fail()) 24: { 25: std::cout « "Bad input " << std::endl; 26: std::cin.clear(); 27: } 28: else 29: { 30: std::cout « "DateTime =" « dt « std::endl; 31: 32: std::cout « std::setfill('*') << std::left « std::setw(30) 33: « std::hex « dt 34: « ’ ’ « std::setw(10) « Oxff « std::endl; 35: } 36: 37: std::cin.ignore(INT_MAX, '\n’); // Цикл завершается // в случае ошибки 38: } 39: 40: return 0; 41: } Реализация функций ввода и вывода времени 247
В строке 9 проверяются функция now и оператор вывода. В строке 12 конструиру- ется объект DateTime, в который заносится значение полуночи 1 января 2000 года. В строке 16 начинается цикл ввода данных из потока cin в объект DateTime, про- должающийся до тех пор, пока не будет обнаружена ошибка ввода. (Использование объекта cin в логической конструкции равносильно вызову для него функции good.) Цикл можно завершить вводом символа q (команда quit) вместо даты или времени. Отслеживание командного символа в потоке ввода осуществляется в строке 19. В строке 22 используется оператор », реализацию функций которого мы рас- сматривали в листинге 9.3. При успешном выполнении эта функция заносит ин- формацию о дате и времени, введенную пользователем, в объект, заданный ссылкой dt. Успешность ввода проверяется в строке 23 по состоянию потока cin. В случае обнаружения ошибки пользователю выводится сообщение, и поток ввода очищается. При успешном вводе программа переходит к выполнению строки 30, в которой про- веряется оператор вывода класса DateTime, рассмотренный нами в листинге 9.2. В строках 32-34 вновь используется оператор вывода, но в более сложной конст- рукции с манипуляторами форматирования, устанавливающими символы заполне- ния, ширину поля и шестнадцатеричный числовой формат. Для вывода даты и време- ни задано поле шириной 30 символов и со звездочкой в качестве символа заполнения. Шестнадцатеричное значение Oxf f выводится в поле шириной 10 символов. Если вы- вод пройдет успешно, мы увидим значения даты и времени, за которыми следует ряд звездочек, заполняющих поле до 30-ти символов, пробел и значение ff, за которым должно следовать еще 8 звездочек. Другими словами, мы должны увидеть данные в по- лях с фиксированной шириной, выравниванием влево и заполнением недостающих символов звездочками. В листинге 9.6 показано, как будет в действительности выгля- деть экран (курсивом выделены данные, введенные пользователем). i Листинг9.6. Дервый запуск программытестирования l:Now = 9/5/1999 3:39pm 2:Party = 1/1/2000 12:00am 3: 4:Enter a date and time: 4/5/1996 3:11am 5:DateTime =4/5/1996 3:11am 6:4^i**^***^^*^*^*^^^**/5/7cc 3 : Obam OOOOOOOOff 7: 8:Enter a date and time: 4/5#96 3:74am 9:DateTime =4/5/7cc 4:Oeam 10;4*****************************/5/7CC 4:Oeam OOOOOOOOff 11: 12:Enter a date and time: q Строки 1, 2 корректно отображают текущее время (естественно, что текущим оно бы- ло на тот момент, когда я запустил программу) и время начата большой вечеринки (Party) — 1/1 /2000 12:00ат Строка 5 успешно возвращает дату и время, введенные пе- ред этим пользователем в строке 4. Но с 6-й строки начинаются странные вещи. Давайте посмотрим, что происходит. В нашей программе установлено выравнивание данных влево и заполнение пробелов символами звездочки. Перед выводом даты и времени ши- рина поля устанавливается равной 30 символам. Вспомните, что установка ширины по- ля действительна только для следующей операции вывода. Хотя вывод значений объекта dt выглядит как одна операция, в действительности, как мы знаем, наш перегруженный оператор вывода отображает каждый компонент по отдельности. В результате только значение 4 выводится в 30-символьном поле с заполнением звездочками, а все остальные компоненты даты и времени выводятся без форматирования. 248 Глава 9. Классы даты и времени с пользовательской системой...
Другая проблема возникла с выводом значения года в шестнадцатеричном формате. У вас может быть другое мнение, но я предпочитаю вообще не отобра- жать даты цифрами, тем более в шестнадцатеричном формате. Та же проблема возникла при отображении минут. К сожалению, наши проблемы не заверши- лись на выводе даты и времени, а продолжились с выводом шестнадцатеричного числа Oxf f. Предполагалось, что это значение будет отображено в 10-символь- ном поле с выравниванием влево и заполнением пробелов звездочками. Но вме- сто этого мы получили 10-символьное поле, выровненное вправо и заполненное нулями. Чтобы разобраться, откуда взялось такое форматирование, нужно воз- вратиться к коду функции оператора вывода в классе DateTime. Нуль в качестве символа заполнения и выравнивание вправо были установлены для отображе- ния минут (двухзначные числа с возможным нулем в начале). Но в отличие от манипулятора setw, эти установки сохраняются для всех следующих операторов вызова, пока не будут явно изменены. Результаты выполнения программы, показанные в листинге 9.6, выявили также ошибки при вводе данных. В строке 8 мы умышленно ввели неверный символ пунктуации между месяцем и годом и задали недопустимое число минут. Программа должна была выявить допущенные ошибки, но не сделала этого. Та- ким образом, хотя в целом наш класс DateTime вполне функционален, качество его работы не соответствует нашим требованиям и без доработок его нельзя ис- пользовать в приложении TinyPIM. Устранение недостатков ввода-вывода Настоящий, профессионально реализованный класс ввода-вывода должен отве- чать требованиям, определенным для стандартных библиотечных классов семейства iostream. Ввод и вывод данных в таком классе должны происходить с учетом задан- ного формата, и в случае обнаружения ошибок функции класса должны показать со- общения пользователю и воспрепятствовать изменению сохраненной информации ошибочными данными. В листинге 9.7 показаны изменения, внесенные в реализацию операторов ввода и вывода класса DateTime для устранения выявленных недостатков. Листинг 9.7. Изменения в реализации операторов ввода й вывода класса DateTime < * L*.' хл ' U ......... •' 61:std::ostream& operator« (std: :ostream& os, const DateTime& dt) 62: { 63: char oldfill = os.fill(•0•); // Сохранение значения символа // заполнения 64: std::ios::fmtflags oldflags = os.flags(); // Сохранение исходных // установок флагов 65: 66: std::tm theTm = *localtime (&dt. theTime__) ; 67: 68: os « std:: setw (0) « std: : dec « (theTm. tmjnon + 1) « •/’ 69: « theTm. tm__mday << ’/’ 70: « (theTm.tm_year + 1900) « ’ 71: 72: int hour = theTm.tm_hour % 12; 73: const char*ampm = (theTm.tm_hour < 12 ? "am" : "pm"); Реализация функций ввода и вывода времени 249
74: 75: if (hour == 0) 76: hour =12; 77: 78: os « std::setfill(' ') « std::right « std::setw(2) « hour 79: « « std::setfill(*0•) « std::right « std::setw(2) 80: « theTm.tm_min « ampm; 81: 82: os.flags(oldflags); 83: os.fill(oldfill); 84: return os; 85: } 86: 87:std::istream& operator>>(std::istream& is, DateTime& dt) 88: { 89: char slashl, slash.2, colon; 90: int mon, day, year, hour, min; 91: char ampm[3]; 92: 93: is » mon » slashl » day » slash.2 » year 94: » hour » colon » min » std::setw(3) » ampm; 95: 96: // Контроль за ошибками ввода-вывода 97: if (is.failO) 98: return is; 99: 100: // Проверка соответствия формату 101: if (slashl != •/’ II slash2 != '/' || colon != || 102: mon <1 || 12 < mon || day <1 || 31 < day || 103: hour <1 || 12 < hour || min <0 || 59 < min || 104: (ampm [0] != ’a1 && ampm[0] != 'p')) 105: { 106: // Обнаружена ошибка форматирования, установка флага // ошибки и возвращение из функции 107: is.clear(std::ios::failbit); 108: return is; 109: } 110: 111: // Преобразование значения времени из 12-часового формата //в 24-часовой (0-23) 112: if (hour == 12) 113: hour = 0; 114: if (ampm[0] == 'p’ || ampm[0] == TP’) 115: hour += 12; 116: 117: dt = DateTime(year, mon, day, hour, min); 118: 119: return is; 120:} В строке 63 нуль определяется в качестве символа заполнения. Функция-член fill возвращает прежний символ заполнения, который мы заполняем для даль- нейшего использования. (Функцию fill можно вызывать без параметров, чтобы просто возвратить текущий символ заполнения.) В строке 64 сохраняются текущие установки флагов форматирования. Значение, возвращаемое функцией flags, представляет собой целое число, биты которого в двоичном формате представляют установки различных флагов, таких как тип выравнивания, десятеричный, шестна- 250 Глава 9. Классы даты и времени с пользовательской системой...
дцатеричный или восьмеричный формат чисел и т.д. Эти значения используются для восстановления исходных установок форматирования в строке 82 и исходного символа заполнения в строке 83. Поскольку нам не нужно, чтобы номер месяца за- нимал все поле вывода, при выводе этого значения мы устанавливаем в стро- ке 68 размер поля равным нулю. Установка нулевого размера поля приводит к тому, что размер поля автоматически выравнивается по размеру значения. В этой же строке мы устанавливаем десятеричный формат для числовых значений, чтобы от- менить показ даты и времени в шестнадцатеричном формате. В строках 100-104 отслеживаются ошибки форматирования при вводе дан- ных. Если будут обнаружены недопустимые символы пунктуации или значения выйдут за допустимые диапазоны, условное выражение оператора if возвратит true. В этом случае программа выполнит строку 107, в которой для потока ввода устанавливается флаг ошибки. Имя функции clear (очистить) не вполне отра- жает ее назначение. При вызове без аргументов она сбрасывает все флаги со- стояния, возвращая поток к состоянию good (). Но в случае передачи аргументов эта функция устанавливает, а не сбрасывает флаги. Константа badbit (так же, как и eofbit) определена в классе std: :ios, который является базовым для классов std: : istream и std: : ostream. В следующей строке функция возвраща- ет объект ввода с установленным флагом ошибки. Обратите внимание, что в случае обнаружения ошибки возврат из функции опе- ратора ввода осуществляется до изменения аргумента dt. Это предупреждает замену исходных данных ошибочными. Если теперь мы запустим программу тестирования, то получим более приемле- мый результат, хотя еще не совершенный (листинг 9.8). Листинг 9.8. Результат выполнения программы тестирования, после внесения изменений в класс DateTime * ; l:Now = 9/5/1999 11:44pm 2:Party = 1/1/2000 12:00am 3: 4:Enter a date and time: 4/5/1996 3:11am 5:DateTime - 4/5/1996 3:11am 6:4/5/1996 3:11am ff******** 7: 8:Enter a date and time: 4/5#96 3:11am 9:Bad input 10: 11:Enter a date and time: 4/5/96 3:74pm 12:Bad input 13: 14:Enter a date and time: 4/5/96 3:11 15: am 16:DateTime = 4/5/1996 3:11am 17:4/5/1996 3:11am ff******** 18: 19:Enter a date and time: q В строке 6 мы видим, что дата и время представлены уже не в шестнадцате- ричном формате. Кроме того, больше нет длинного ряда звездочек после номера месяца. Число Oxff, как нам и требовалось, выровнено влево с заполнением звездочками 10-символьного поля. В то же время строка даты и времени пред- Реализация функций ввода и вывода времени
ставлена неправильно. Мы хотели вывести ее в 30-символьном поле с заполне- нием звездочками, а она выведена как по умолчанию. Хорошо, разберемся с этой проблемой в следующем разделе. При проверке функции ввода мы вводили в строках 8,11 неправильные значения даты и времени. Программа распознала все ошибки, установила для объекта cin флаг ошибки и показала сообщение для пользователя. В строке 14 мы не завершили ввод всех требуемых данных и нажали <Enter>. Программа не приняла неполные данные, но и не показала сообщения об ошибке, а осталась в состоянии ожидания ввода недостающих данных. В строке 15 мы ввели окончание ат, о котором забыли в строке 14, и программа приняла его. Но хорошо ли это, что в середине значения времени будет находиться символ разрыва строки? Эта проблема будет решена так же, как и проблема с выводом данных. Использование вспомогательных строк для большей гибкости программирования Как же устранить недостатки в работе оператора вывода, чтобы он корректно за- полнял и выравнивал значения даты и времени в соответствии с установками вы- равнивания, символа заполнения и размера поля. Довольно сложно будет вычислять число символов, занимаемых всей строкой, с учетом того, что однозначные номера месяцев и числа дней занимают меньше места, чем двухзначные. Следовательно, анализ и выбор установок форматирования нужно проводить с учетом размера вы- водимых данных. Проще всего было бы собрать строку даты и времени из составляющих ее компонентов и вывести затем одной операцией вывода. Для этого отлично по- дойдут объекты класса string, так как в них уже предусмотрены функции вы- равнивания и заполнения полей. Как вы помните, для сохранения выводимых данных в объектах string используется класс потока stringstream. Во время ввода данных мы можем не записывать их в объект DateTime, а сохранять во вспомогательной строке для дальнейшего анализа. После того как у нас будет вся строка целиком, значительно легче определить в ней недостающие данные или выбрать наиболее подходящий формат представления. Например, с помощью такого подхода мы можем позволить пользователям вводить время и в 12-часо- вом, и в 24-часовом форматах без окончаний аш/рш. В листинге 9.9 показана реализация функций операторов ввода и вывода с использованием вспомога- тельных строк и потока stringstream. Листинг 9.9. Эффективный и устойчивый к ошибкам ввод*вывод ШЖЖ с использованием вспомогательных строк 62:std::©streams operator« (std: : ©streams os, const DateTimes dt) 63: { 64: std::©stringstream tmpstrm; 65: 66: std::tm theTm = *localtime(Sdt.theTime_); 67: 68: tmpstrm « (theTm. tm^rnon + 1) « ’/’ 69: « theTm. tm_mday « 1/’ 252 Глава 9. Классы даты и времени с пользовательской системой...
70: « (theTm. tm_year + 1900) « 1 71: 72: 73: 74: 75: 76: 77: 78: 79: int hour = theTm.tm_hour % 12; const char* ampm = (theTm.tm_hour < 12 ? "am" :: "pm”); if (hour == 0) hour = 12; tmpstrm « std: : setfill (’ ’) « std:: right « std:: setw (2) « hour « « std::setfill(’01) « std::right « std::setw(2) 80: « theTm. tm_min « ampm; 81: 82: 83: } 84: 85:std: 86: { 87: 88: 89: 90: 91: 92: 93: 94: 95: 96: 97: 98: 99: 100: 101: 102: 103: 104: 105: return os « tmpstrm. str () ; :istream& operator » (std::istream& is, DateTime& dt) // Прежде всего считывается дата std::string date; is » date; if (is.fail()) return is; // Ошибка ввода-вывода char slashl, slash2, colon; int mon, day, year, hour, min; char ampm[3] ; // Распаковка данных даты с помощью потока stringstream std::istringstrearn tmpstrm(date); tmpstrm » mon » slashl » day » slash2 » year; // Контроль за ошибками в дате if (tmpstrm.fail() || slashl != ’/' II slash2 != II mon <1 || 12 < mon || day <1 || 31 < day) { // Ошибка форматирования, установка флага ошибки // и возвращение 106: is.clear(std::ios::failbit); 107: 108: 109: 110: 111: 112: 113: 114: 115: 116: 117: 118: 119: 120: 121: return is; } // Теперь считываем данные времени std::string time; is » time; if (is.failO) return is; // Ошибка ввода-вывода // Распаковка данных времени с помощью потока stringstream // без учета индикаторов am/pm tmpstrm.clear(); tmpstrm.str(time); tmpstrm » hour » colon » min; 122: // Контроль за ошибками в данных времени Реализация функций ввода и вывода времени 253
123: if (tmpstrm. fail () | | hour <0 | | 23 < hour | | min <0 | | 59 < min) 124: { 125: // Ошибка форматирования, установка флага ошибки // и возвращение 126: is.clear(std::ios::failbit); 127: return is; 128: } 129: 130: // Анализ индикаторов am/pm 131: tmpstrm » std::setw(3) » ampm; 132: bool useAmPm = ! tmpstrm.fail() ; 133: 134: // Преобразование значения времени из 12-часового // в 24-часовой формат (0-23) 135: if (useAmPm) 136: { 137: if (hour == 12) 138: hour ~ 0; 139: if (ampm[0] == ’p' || ampm[0] == 'P') 140: hour += 12; 141: } 142: 143: dt = DateTime(year, mon, day, hour, min); 144: 145: return is; 146: } В строке 64 объявляется объект tmpstrm класса stringstream, в котором мы будем выполнять форматирование данных даты и времени. В строках 68-80 ис- ходные данные выводятся в поток stringstream. Нам не нужно при выводе ус- танавливать десятичный формат для чисел, символ заполнения или ширину по- ля, поскольку данные заносятся пока только в автоматически создаваемый объ- ект stringstream со своими установками, заданными по умолчанию. В строке 82 мы извлекаем значение даты-времени из потока string str earn и по- сылаем его в поток os. Как обычно, обращение к потоку os возвращает этот же поток, что позволяет конкатенировать операции вывода. Но в нашем случае де- лается единственное обращение к потоку os с целью вывести уже готовую стро- ку значений даты и времени в соответствии с установками опций форматирова- ния, заданными для потока вывода. Обратите внимание, что нам не приходится сохранять и восстанавливать установки форматирования, поскольку нам доста- точно манипулировать объектом tmpstrm, тогда как установки потока os оста- ются постоянными. В функции оператора ввода мы считываем дату и время как два самостоя- тельных значения, разделенных пробелом. В строках 89-91 считывается дата и определяются ошибки ввода. В строке 98 создается объект stringstream, из которого затем в строке 99 считываются введенные компоненты значения даты: месяц, день и год. В строках 112-114 считывается значение времени и проверяются ошибки ввода. В строке 118 мы повторно используем объект tmpstrm класса stringstream. Поскольку объект tmpstrm к этому времени должен находиться в состоянии eof() (так как мы считали с него все ранее введенные данные), мы сбрасываем установки всех его флагов. Затем, в строке 119, этому объекту при- сваивается новое строковое значение. В следующей строке мы возвращаем из 254 Глава 9. Классы даты и времени с пользовательской системой...
объекта tmpstrm часы и минуты. После выполнения в строках 123-128 контроля за ошибками программа готова к считыванию индикаторов ат и рт. Если инди- каторы присутствуют, значит, время записано в 12-часовом формате, в против- ном случае пользователь использовал при вводе 24-часовой формат. Индикато- ры am/pm считываются в строке 131. В следующей строке логической перемен- ной useAmPm присваивается значение false, если индикаторы не обнаружены. Поскольку считывание данных осуществляется не со стандартного устройства ввода, а со строкового объекта, программа не будет приостановлена в ожидании ввода пользователем недостающей информации, а отсутствие индикаторов am/pm не будет расценено как ошибка ввода. После внесения указанных изменений класс DateTime наконец заработал как цельный объект для ввода и вывода данных даты и времени. Результат нового вы- полнения программы тестирования показан в листинге 9.10. Листинг 9.10. Результат тестирования класса DateTime с использованием ' • вспомогательных строк : ? \ l:Now = 9/6/1999 12:37am 2:Party = 1/1/2000 12:00am 3: 4:Enter a date and time: 4/5/1996 3:11am 5:DateTime = 4/5/1996 3:11am 6:4/5/1996 3 -11 am************** ff******** 7: 8:Enter a date and time: 4/5/96 3: 9:Bad input 10: 11:Enter a date and time: 4/5/96 3:11 12:DateTime = 4/5/1996 3:11am 13:4/5/1996 3 • 11 ятп*** *********** ff******** 14: 15:Enter a date and time: 4/5/96 15:11 16:DateTime = 4/5/1996 3:11pm 17:4/5/1996 3:llprn************** ff******** 18: 19:Enter a date and time: q В строке 6 мы видим значения даты и времени, выровненные влево в 30-сим- вольном поле, с заполнением пробелов звездочками, именно так. как и было оп- ределено в требованиях к программе. Ввод времени без указания минут воспри- нимается программой как ошибка (строки 8 и 9), но отсутствие индикаторов am/pm правильно распознается как альтернативный вариант форматирования (строки 11-13). В строках 15-17 повторен ввод времени в 24-часовом формате для значений, превышающих 12 (в случае следования за этим значением инди- каторов am/pm программа показала бы сообщение об ошибке). Мы могли бы пойти дальше и обучить программу распознавать введенные даты без указания года (устанавливать текущий год по умолчанию) или введенное время — без ука- зания минут (только часы). Это очень важно для профессионального проекта, так как позволяет пользователям вводить данные, используя всевозможные аль- тернативные форматы. Настоятельно рекомендую вам самостоятельно поуп- ражняться в этом направлении с нашей программой. Реализация функций ввода и вывода времени 255
Использование класса DateTime в книге контактов Прежде чем считать работу над классом DateTime завершенной, посмотрим, от- вечает ли он всем нашим требованиям к книге контактов. Наша электронная книга контактов должна уметь представлять дату и время, сравнивать их, считывать эти данные с потока ввода и записывать в поток вывода. Но следует учесть, что нам не всегда нужно будет представлять одновременно дату и время, да и формат вывода этих значений может меняться в зависимости от выбранного пользователем режима работы с книгой контактов. Так. в режиме отображения контактов на текущий день нужно показывать время, но не дату. В некоторых режимах также удобнее будет по- казывать дни недели и названия месяцев вместо их номеров. Программа, лежащая в основе книги контактов, должна распознавать границы между днями, неделями и месяцами. Например, вычислять, на какой день недели приходится начало и конец данного месяца, или работать в режиме календаря. В листинге 9.11 показаны некоторые дополнительные функции, которыми мы рас- ширили класс DateTime. ; Листинг 9.11. Расширенное определение класса DateTime К.. . -.Л.'.. Л . Л''.7 '. ... J •_ “V - ''Л? 1://TinyPIM (с)1999 Pablo Halpern. Файл DateTime.h 2: 3:#ifndef DateTime_dot_h 4 : #def ine DateTime_dot__h 1 5: 6:#include <iostream> 7:#include <string> 8:#include <ctime> 9: 10:#ifdef _MSC_VER 11:// Проверка вхождения указанных типов и функций в пространство // имен std: 12:namespace std { 13: typedef ::time_t time_t; 14: typedef ::tm tm; 15: inline double dif ftime (time__t tl, time_t tO) 16: {return ::difftime(tl, tO);} 17: } 18:ttendif 19: 20:class DateTime 21: { 22:public: 23: DateTime() : theTime_(0){ } 24: DateTime(int year, int. month, int day, int hour, int min); 25: 26: // Использование сгенерированных компилятором конструктора- // копировщика, деструктора и оператора присваивания 27: 256 Глава 9. Классы даты и времени с пользовательской системой...
28: // Функции доступа для чтения 29: 30: 31: 32: 33: void get(int& year, ints month, ints day, ints hour, ints min) const; void getDate(intS year, ints month, ints day) const; void getTime(int& hour, int& min) const; 34: // Возвращение дней недели в диапазоне от 0 (воскресенье) // до 6 (суббота) 35: int dayOfWeekO const; 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: 57: 58: 59: // Функции доступа для записи DateTimes set(int year, int month, int day, int hour, int min); DateTimeS setDate(int year, int month, int day); DateTimes setTime(int hour, int min); // Возвращает начало дня DateTime startOfDayO const; // Возвращает начало недели DateTime startOfWeek() const; // Возвращает начало месяца DateTime startOfMonth() const; // Прибавляет к указанной дате заданное число дней DateTime addDay(int days = 1) const; // Возвращение строковых значений std::string dateStr() const; // Дата в виде строки std::string timeStr() const; // Время в виде строки std::string wdayNameO const; // Названия дней недели std::string monthNameO const; // Названия месяцев года 60: // Следующие функции преобразовывают строковые значения // в дату и время. 61: // В случае ошибки возвращается false 62: 63: 64: 65: 66: 67: 68: 69: 70: 71: 72: 73: 74: 75: 76: dt) ; 77: bool dateStr(const std::strings); bool timeStr(const std::strings); // Возвращение текущей даты и времени static DateTime now(); friend bool operator==(const DateTime& dtl, const DateTimes dt2) {return dtl.theTime_ == dt2.theTime_;} friend bool operator<(const DateTimes dtl, const DateTimes dt2) {return std::difftime(dtl.theTime_, dt2.theTime_) < 0;} friend std::©streams operator« (std: : ©streams os, const DateTimes dt); •friend std::istreamS operator»(std::istreamS is, DateTimeS 78:private: 79: Использование класса DateTime в книге контактов 257
80: std::time_t theTime_; 81:}; 82: 83: 84:#endif // DateTime dot h Для выполнения различных операций над значениями даты и времени в классе DateTime определены функции get Date и getTime (строки 31, 32), а также set Date и setTime (строки 39, 40). В строках 43, 46 и 49 объявляются функции, которые воз- вращают для текущего значения DateTime объекты этого класса, представляющие начало дня, недели и месяца соответственно. Функция addDay, объявленная в стро- ке 52, выполняет простейшие арифметические действия с объектами DateTime (число добавляемых дней может быть отрицательным). В строках 55-58 объявляется набор функций, которые возвращают текстовые значения даты, времени, дня недели и месяца. Функции преобразования тексто- вых значений в дату и время определяются в строках 61, 62. Поскольку эти функции работают подобно оператору ввода, мы позаимствовали для их реали- зации часть кода функции этого оператора. Если в строках текста дата или вре- мя не распознается, например из-за ошибок форматирования, то данные функ- ции возвращают false. В листинге 9.12 показана окончательная версия файла DateTime. срр с реализа- цией всех дополнительных функций. 1://TinyPIM (с)1999 Pablo Halpern. Файл DateTime.срр 2: 3:#include <iomanip> 4:#include <sstream> 5:#include "DateTime.h" 6: 7:#ifndef _MSC_VER 8:using std::mktime; 9:using std::localtime; 10:using std::time_t; ll:using std::strftime; 12:#endif 13: 14:DateTime::DateTime(int year, int month, int day, int hour, int min) 15: { 16: set(year, month, day, hour, min); 17: } 18: 19:void DateTime::get(int& year, int& month, int& day, 20: int& hour, int& min) const 21: { 22: std::tm* mytm = localtime (&theTime_) ; 23: year = mytm->tm_year + 1900; 24: month - mytm->tm_mon + 1; 25: day = mytm->tm_mday; 26: hour = mytm->tm_hour; 27: min = mytm->tm_min; 28: } 29: 30:void DateTime::getDate(int& year, intfi month, intfi day) const 258 Глава 9. Классы даты и времени с пользовательской системой...
31: { 32: int hour, min; 33: get (year, month, day, hour, min) ; 34:} 35: 36:void DateTime::getTime(int& hour, int& min) const 37: { 38: int year, month, day; 39: get (year, month, day, hour, min) ; 40: } 41: 42:// Возвращение дней недели в диапазоне от 0 (воскресенье) // до 6 (суббота) 43:int DateTime::dayOfWeek()const 44: { 45: return local time (&theTime__) ->tm_wday; 46: } 47: 48:DateTime& DateTime::set(int year, int month, int day, 49: int hour, int min) 50: { 51: // Значения от 0 до 49 соответствуют годам 2000—2049. 52: // Значения, превышающие 1900, распознаются как // соответствующие годы. 53: //И прочие значения обозначают годы от 1950 до 1999. 54: if (year <50) 55: year += 2000; 56: else if (year < 1900) 57: year += 1900; 58: 59: std: :tm mytm; 60: mytm.tm_year = year - 1900; 61: mytm.tm_mon = month - 1; // Начало отсчета с нуля 62: mytm.tm_mday = day; 63: mytm.tm_hour = hour; 64: mytm.tm_min = min; 65: mytm.tm_sec = 0; 66: mytm.tm_isdst = -1; 67: 68: theTime_= mktime (&mytm) ; 69: 70: return *this; 71: } 72: 73:DateTime£DateTime::setDate(int year, int month, int day) 74:{ 75: int hour, min; 76: getTime(hour, min); 77: set(year, month, day, hour, min); 78: 7 9: return * this; 80: } 81: 82:DateTime& DateTime::setTime(int hour, int min) 83: { 84: int year, month, day; Использование класса DateTime в книге контактов 259
85: getDate(year, month, day); 86: set(year, month, day, hour, min); 87: 88: return *this; 89: } 90: 91:std::string DateTime::dateStr() const 92: { 93: char buf[20]; 94: s trf time (buf, 20, ,,%xn , local time (&theTime_)) ; 95: return buf; 96:} 97: 98:std::string DateTime::timeStr() const 99: { 100: char buf[20]; 101: strf time (buf, 20, ,,%I:%M%p", local time (&theTime__)) ; 102: return buf; 103: } 104: 105:// Возвращает дни недели 106:std::string DateTime::wdayName() const 107: { 108: char buf[30]; 109: strftime(buf, 30, , localtime(&theTime_)); 110: return buf; 111:} 112: 113://Возвращает названия месяцев 114:std::string DateTime::monthName()const 115:{ 116: char buf[30]; 117: s trf time (buf, 30, "%B", local time (&theTime__)) ; 118: return buf; 119: } 120: 121:bool DateTime::dateStr(const std::string& s) 122: { 123: char slashl, slash2; 124: int mon, day, year; 125: 126: // Распаковка данных даты с помощью потока stringstream 127: std::istringstream tmpstrm(s); 128: tmpstrm » mon » slashl » day » slash2 » year; 129: 130: // Контроль за ошибками в дате 131: if (tmpstrm.fail() || slashl != '/' II slash2 != /' || 132: mon <1 || 12 < mon || day <1 |I 31 < day) 133: return false; 134: 135: setDate(year, mon, day); 136: return true; 137: } 138: 139:bool DateTime::timeStr(const std::string& s) 260 Глава 9. Классы даты и времени с пользовательской системой..
140: { 141: 142: 143: char colon; int hour,min; char ampm [3 ]; 144: 145: std::istringstream tmpstrm(s); 146: tmpstrm » hour » colon » min; 147: 148: // Контроль за ошибками в данных времени 149: if (tmpstrm.fail() || hour <0 || 23 < hour || min <0 || 59 < min) 150: return false; 151: 152: // Анализ индикаторов am/pm 153: tmpstrm » std::setw(3) » ampm; 154; bool useAmPm = ! tmpstrm. fail () ; 155: 156: // Преобразование значения времени из 12-часового // в 24-часовой формат (0-23) 157: if (useAmPm) 158: { 159: if (hour == 12) 160: hour =0; 161: if (ampm[0] == 'p' II ampm[0] ~ 'P') 162: hour += 12; 163: } 164: 165: setTime(hour, min); 166: return true; 167: } 168: 169:// Возвращает начало дня 170:DateTime DateTime::startOfDay() const 171: { 172: std::tm theTm = * local time (&theTime__) ; 173: theTm.tm_hour = 0; 174: theTm. tm_min =0; 175: theTm.tm_sec =0; 176: theTm.tm_isdst = -1; 177: 178: DateTime result; 179: result.theTime_ = mktime(&theTm); 180: return result; 181: } 182: 183:// Возвращает начало недели 184:DateTime DateTime::startOfWeek() const 185:{ 186: std::tm theTm = *localtime(&theTime_); 187: theTm. tm_mday -= theTm. tm_wday; // Отнимает текущий день недели 188: theTm. tm_hour = 0; 189: theTm. tm__min = 0; 190: theTm.tm_sec =0; 191: theTm.tm_isdst = -1; Использование класса DateTime в книге контактов 261
192: 193: DateTime result; 194: result.theTime_ = mktime(&theTm); 195: return result; 196: } 197: 198:// Возвращает начало месяца 199:DateTime DateTime::startOfMonth() const 200: { 201: std::tm theTm = * local time (&theTime__) ; 202: theTm. tm_mday =1; 203: theTm. tm__hour = 0; 204: theTm. tm^min =0; 205: theTm. tm__sec = 0; 206: theTm.tm_isdst = -1; 207: 208: DateTime result; 209: result. theTime_ = mktime (&theTm) ; 210: return result; 211: } 212: 213:// Прибавляет к указанной дате заданное число дней 214:DateTime DateTime::addDay(int days) const 215: { 216: std::tm theTm = * local time (&theTime_) ; 217: theTm.tm_mday += days; 218: theTm. tm_isdst = -1; 219: 220: DateTime result; 221: result. theTime_ = mktime (&theTm) ; 222: return result; 223: } 224: 225:// Возвращение текущей даты и времени 226:DateTime DateTime::now() 227: { 228: DateTime ret; 229: ret.theTime_ = time(0); // Вызов функции std::time 230: return ret; 231: } 232: 233:std::ostreamS operator« (std: :©streams os, const DateTimes dt) 234: { 235: return os « (dt.dateStr() + " " + dt.timeStr()); 236: } 237: 238:std::istreamS operator»(std::istreamS is, DateTimes dt) 239: { 240: DateTime result; 241: 242: // Прежде всего считываем дату 243: std::string date; 244: is » date; 245: if (is.failO) 246: return is; // Ошибка ввода-вывода 247: 262 Глава 9. Классы даты и времени с пользовательской системой..
248: if (! result.dateStr(date)) 249: { 250: is.clear(std::ios::failbit); 251: return is; // Ошибка форматирования 252: } 253: 254: std::string time; 255: is » time; 256: if (is.failO) 257: return is; // Ошибка ввода-вывода 258: 259: if (! result.timeStr(time)) 260: { 261: is.clear(std::ios::failbit); 262: return is; // Ошибка форматирования 263: } 264: 265: dt = result; 266: return is; 267: } Рассмотрим новые методы класса DateTime. Функции getDate и get Time, опре- деленные в строках 30-40, просто вызывают функцию get, а затем сбрасывают зна- чения определенных элементов. В строке 45 функция dayOfWeek вызывает функцию local time, после чего извлекает из структуры tm и возвращает переменную-член tm_wday. Функция setDate, определенная в строках 73-80, сначала извлекает теку- щее значение времени и объединяет его с датой, переданной с аргументами, после чего возвращает новый объект DateTime. Функция setTime в строках 82-89 работа- ет аналогично. В строке 94 мы видим вызов функции std: : strftime, которая принимает четыре параметра: массив символов, длину массива символов, строку подста- новки и указатель на структуру tm. Функция strftime копирует строку подста- новки в массив символов, замещая код подстановки на соответствующие значе- ния даты или времени. Код подстановки состоит из символа процента (%) и сле- дующей за ним единственной буквы, которая определяет, какой компонент даты или времени следует ввести по указанной позиции в массиве символов. Напри- мер, код ”% у” замещается двухзначным числом, представляющим год (от ”00” до ’'9 9’'). Так, если в структуре tm записан 2001 г. и определена строка подстановки "The year is 1 %у ’”, то в конечный массив символов будет скопирована строка "The year is ’01’" с нулевым символом окончания. В нашей программе ис- пользуется строка подстановки ”%х", которая замещается текущей датой в регио- нальном формате. Термин Под региональным форматом понимают некоторые соглашения пред- ставления даты, времени, денежных единиц и пр. В строке 101 вновь используется функция strftime, в этот раз для форматиро- вания значения времени. В данном случае используются три кода подстановки: %1— представляет часы от 1 до 12; %м— минуты от 1 до 60; %р— индикаторы аш/рш в региональном формате. В строке подстановки установлен символ двоето- чия, отделяющий часы от минут. Вместо этого можно было бы использовать код %Х, который замещается значением времени в региональном формате, но, например, на Использование класса DateTime в книге контактов 263
моем компьютере региональный формат времени задан с показом секунд, что не тре- буется в нашей программе. В программе мы еще несколько раз используем коды подстановок: % А— в стро- ке 109 для представления дней недели, % В — в строке 117 для представления назва- ния месяца. Полный список кодов подстановок вы можете посмотреть в технической документации на ваш компилятор. Обратите внимание, как в строке 235 мы заменили весь алгоритм функции оператора вывода на операцию конкатенации вызовов функций dateStr и timeStr. Функция dateStr использует в строках 127, 128 поток stringstream для разбора значения даты на составляющие. Код этой функции был позаимст- вован почти без изменений из прежней реализации функции оператора ввода. На основе кода оператора ввода в строках 145, 146 точно так же выполняется функция timeStr. Чтобы не повторять один и тот же код в функции оператора ввода, мы просто вызываем функции dateStr и timeStr в строках 248 и 259 соответственно. Выполнение функции startOfDay начинается в строке 172 с разделения зна- чения времени на элементы структуры tm. В строках 173-175 значения часов, минут и секунд приводятся к нулю (полночь — начало нового дня). При этом мо- жет быть пройден рубеж перехода от зимнего времени к летнему или наоборот. Поэтому в строке 176 флагу tm isdst присваивается значение -1. По этой ко- манде функция mktime определяет, соответствует ли новая дата периоду ото- бражения летнего времени. Вызов функции mktime в строке 179 вычисляет но- вую переменную типа time t, представляющую полночь текущего дня. Обратите внимание, что исходный объект DateTime при этом не изменяется. Вычислен- ные данные заносятся в новый объект DateTime , который возвращается функ- цией в строке 180. Функция startOfWeek работает почти так же, как и startOfDay. Кроме при- ведения к нулю в строках 188-190 значений часов, минут и секунд, в строке 187 выполняется вычитание дней. В переменной tm wday порядковый номер дня не- дели выражается числами в диапазоне от О (воскресенье) до 6 (суббота). В таком случае, чтобы вычислить начало текущей недели (последнее воскресенье), от те- кущего значения tm mday нужно отнять значение tm wday (например, для среды это значение равно 3). Но что произойдет, если в результате вычитания полу- чится значение меньше единицы? Функция mktime допускает, чтобы значения переменных-членов структуры tm выходили за установленные диапазоны. Так, значение 13 переменной tm mon будет воспринято как февраль следующего года, а значение О переменной tm mday— как последний день предыдущего месяца. В результате не только будет возвращено значение, соответствующее типу time t, но и структура tm будет автоматически приведена в соответствии с ус- тановленными диапазонами значений. Автоматическая нормализация— это очень важное свойство структуры данных даты и времени, лежащее в основе всех вычислений в электронных календарях. Под нормализацией понимают преобразование значений к более 1ермИН удобной (нормальной) форме. Например, дробь 3/2 можно нормали- зовать к виду 1 1/2. Следующая функция startOfMonth вновь преобразовывает текущий объект DateTime в структуру tm, чтобы затем получить на ее основе новый измененный объект DateTime. В дополнение к операциям других функций start в стро- 264 Глава 9. Классы даты и времени с пользовательской системой...
ке 202 переменной tm_mday (день месяца) присваивается значение 1. Функция addDay работает аналогичным образом. В строке 217 она прибавляет заданное число дней переменной tm mday, полагаясь при этом на способность функции niktime автоматически приводить значения, выходящие за пределы диапазонов, к нормальному виду. Но в отличие от startOfMonth, функция addDay не приводит к нулю значения часов и минут. Чтобы испытать новые функции, несколько изменим нашу программу тестиро- вания, как показано в листинге 9.13. [ Листинг 9.13. Программа тестирования расширенного класса DateTime 1://TinyPIM (с)1999 Pablo Halpern. Файл timeTest.cpp 2: 3:#include <iomanip> 4:#include "DateTime.h" 5: 6: int 7:{ main() 8: // Проверка функции now() 9: std::cout « "Now =" « DateTime::now() « std::endl; 10: 11: // Проверка конструктора 12: std::cout « "Party =" « DateTime(0, 1, 1, 0, 0) « std::endl; 13: 14: DateTime dt; 15: 16: while (std::cin) 17: { 18: std::cout « "\nEnter a date and time: "; 19: if (std::cin.peek() == 'q' || std::cin.peek() == 'Q')) 20: break; 21: 22: std::cin » dt; 23: if (std::cin.fail()) 24: { 25: std::cout « "Bad input" « std::endl; 26: std::cin.clear(); 27: } 28: else 29: { 30:std: :cout « "DateTime =" « dt « std: :endl; 31:std: :cout « "Next day =" « dt.addDay() « std: :endl; 32:std: :cout « "Midnight =" « dt. startOfDay () « std:.:endl; 33:std: :cout « "Week =" « dt.startOfWeek() « std: :endl; 34:std: :cout « "Month »" « dt. startOfMonth () « std: :endl; 35:std: :cout « "Month name =" « dt.monthName() « std: :endl; 36:std: :cout « "Day name =" « dt.wdayName() « std: :endl; 37:std: 38: :cout « "Day number =" « dt.dayOfWeek() « std: :endl; 39:std: :cout « std::setfill('*1) « std::left « std::setw(30) 40: « std::hex « dt 41: « 1 1 « std::setw(10) « Oxff « std::endl; 42: } Использование класса DateTime в книге контактов 265
43: 44: std::cin.ignore(INT_MAX, *\n'); // Цикл завершается // в случае ошибки 45: } 46: 47: return 0; 48: } Изменения затронули строки 30-37, в которых программа выводит различные атрибуты объекта DateTime. Нет необходимости напрямую тестировать функции dateStr и timeStr, поскольку косвенно они выполняются при использовании опе- раторов ввода и вывода. Результат, возвращаемый при запуске программы тестиро- вания, показан в листинге 9.14. J Листинг 9.14. Результат выполнения программы тестирования для ррасширенной версии класса DateTime г ; l:Now = 09/06/99 02:06PM 2:Party = 01/01/00 12:00АМ 3: 4:Enter a date and time: 4/5/1996 3:11 5:DateTime = 04/05/96 03:11AM 6:Next day = 04/06/96 03:11AM 7:Midnight = 04/05/96 12:00AM 8:Week = 03/31/96 12:00AM 9:Month = 04/01/96 12:00AM 10:Month name = April 11:Day name = Friday 12:Day number = 5 13:04/05/96 03:11AM************** ff******** 14: 15:Enter a date and time: 4/5/96 3:75 16:Bad input 17: 18:Enter a date and time: q В строке 8 функция startOfWeek успешно вычислила начало недели, хотя оно пришлось на предыдущий месяц. Выравнивание выводимых данных и кон- троль за ошибками (строки 13 и 16) выполняются корректно, так же, как и в предыдущей версии программы. Но годы теперь показаны двухзначными числами, а индикаторы АМ/РМ выводятся в верхнем регистре. Первое измене- ние произошло в результате установок соответствующего формата кодом под- становки %х в функции strftime. Вывод индикаторов в верхнем регистре был предопределен использованием кода %р. Хотя в функции strftime можно ис- пользовать множество разнообразных кодов подстановки, соответствующих, ка- залось бы, всем вообразимым форматам представления дат и времени, в дейст- вительности достаточно трудно заставить эту функцию работать так, как нам хочется. При воспроизведении регионального формата практически всегда ока- зывается, что нужный код подстановки отсутствует, и, к сожалению, програм- мист не может переопределить работу кода подстановки (например, для показа или сокрытия значений секунд, использования четырехзначных чисел для годов или показа индикаторов в нижнем регистре). 266 Глава 9. Классы даты и времени с пользовательской системой...
Резюме Мы получили класс DateTime, пригодный для использования в книге контактов. В ходе работы над этим классом мы познакомились с типом данных time t, струк- турой tm и функциями mktime, localtime и strftime. Больше всего времени у нас ушло на разработку операторов ввода и вывода, которые расширили стандартную систему ввода-вывода возможностью вводить и выводить данные типа DateTime. Класс DateTime достаточно важный для нашего проекта, что оправдывает время, потраченное на кропотливую работу по выявлению и устранению всех потенциаль- ных проблем с вводом-выводом данных. Следующий наш шаг — создание книги контактов на основе класса DateTime. Для этого необходимо разработать средства сортировки объектов DateTime, сис- тему меню и функции управления экранными списками. Шаблон auto ptr по- может нам эффективно управлять памятью компьютера. Затем мы созда- дим приложение TinyPIM, связав воедино все разработанные раньше программ- ные блоки Резюме 267
Глава 10 Сборка блоков программы В этой главе... • Написание главной программы 269 • Реализация класса PIMDate с использованием шаблона auto_ptr 271 • Классы Appointment и DateBook 275 • Главное меню 282 • Создание меню книги контактов 285 • Класс MonthlyDateBookMenu 292 • Экранные списки книги контактов 302 • Классы, производные от ListBasedDateBookMenu 310 • Завершение работы приложения 320 • Резюме 320 Мы уже готовы к тому, чтобы свести все части программы воедино и завершить работу над проектом. Нам осталось только создать еще один класс, ответственный за управление книгой контактов, и класс главного меню. Но для написания этих клас- сов нам не потребуется никаких новых знаний, кроме тех, которыми мы овладели в предыдущих главах. В этой главе мы несколько изменим наш подход к работе над программой. Если раньше мы создавали ее снизу вверх, т.е. определяли класс Address для записей, а потом создавали для них контейнер AddressBook, то сейчас мы продолжим работу сверху вниз. Путь от частного к общему больше подходит для изучения различных концепций программирования, в то время как продвижение от общего к частному поможет нам лучше понять принципы объединения разрозненных блоков в единую программу. Впрочем, в этой главе мы также познакомимся с некоторыми новыми концепциями программирования. Сверившись с нашим планом, разработанным в главе 1, напишем список клас- сов, которые нам предстоит еще выполнить: • PIMDate (главный класс приложения); • Ma inMenu (главное меню); • Appointment (записи контактов); • DateBook (книга контактов); • DateBookMenu (меню книги контактов); • AppointmentDisplayList (экранный список контактов); • AppointmentEditor (редактор контактов).
Ниже вашему вниманию будут представлены листинги определений и реализа- ций всех этих классов, с выделением блоков, которые были добавлены или изменены по сравнению с уже созданными нами классами. Поскольку наша задача в этой главе состоит в том, чтобы научиться связывать классы воедино, продвижение по классам не будет прямолинейным. Напротив, структуры классов мы будем рассматривать с точки зрения взаимодействия программных блоков. Итак, приступим. Написание главной программы Главная программа, ответственная за инициализацию наших основных структур данных и показ главного меню, представлена в листинге 10.1. 1://TinyPIM (с)1999 Pablo Halpern. Файл TinyPIM.срр 2: 3:#ifdef _MSC_VER 4:#pragma warning(disable : 4786) 5:#endif 6: 7:#include <iostream> 8:#include <cstdlib> 9: 10:#ifndef __MSC_VER ll:using std::exit; 12:#endif 13: 14:#include "PIMData.h" 15:#include "AddressBookMenu.h" 16:#include "DateBookMenu.h" 17:#include "MainMenu.h" 18: 19:// Запуск проверочной программы генерирования записей адресов // (в файле TestAddrData.срр) 20:extern void generateAddresses(AddressBook& addrbook, 21: int numAddresses); 22: 23:// Запуск проверочной программы генерирования записей контактов // (в файле TestAddrData.срр) 24:extern void generateAppointments(DateBook& dateBook, int numDays) ; 25: 26:// Глобальный объект данных 27:PIMData myPIMData; 28: 29:int main() 30: { 31: try 32: { 33: // Создание адресной книги и книги контактов 34: std::auto_ptr<AddressBook> addrBookPtr(new AddressBook); 35: std::auto_ptr<DateBook> dateBookPtr(new DateBook); 36: 37: // Следующие инструкции выполняются, // если не было обнаружено исключительных ситуаций Написание главной программы 269
38: myPIMData.addressBook(addrBookPtr); 39: myPIMData.dateBook(dateBookPtr); 40: } 41: catch (...) 42: { 43: std::cerr « "Could not create address and date books.\n"; 44 : exit (EXIT_FAILURE) ; 45: } 46: 47:#ifndef NOGENERATE 48: // Создание 50-ти случайных записей адресной книги 49: generateAddresses(myPIMData.addressBook(), 50); 50: 51: // Создание случайных записей книги контактов для текущего года 52: generateAppointments(myPIMData.dateBook()r 366); 53:#endif 54: 55: // Создание меню адресной книги и книги контактов 56: AddressBookMenu addrBookMenu(myPIMData.addressBook()) ; 57: DateBookMenuCatalog dateBookMenus(myPIMData.dateBook()); 58: 59: // Создание главного меню и добавление его в стек 60: MainMenu mainMenu(&addrBookMenu, dateBookMenus.monthlyMenu()); 61: Menu::enterMenu(&mainMenu); 62: 63: // Обработка опций до выхода из меню. 64: while (Menu::isActive()) 65: Menu::activeMenu()->mainLoop(); 66: 67: std::cout « "\nThank you for using TinyPIM!\n" << std::endl; 68: 69: return 0; 70: } В строках 7, 8 добавляются два стандартных файла заголовка. Заголовок <iostream> содержит определения потоков ввода-вывода, а заголовок <cstdlib> нужен для определения функции exit. Буква с в начале имени заго- ловка <cstdlib> указывает, что этот заголовок унаследован от файла заголовка <stdlib.h>, входящего в стандартную библиотеку языка С. Отличие между ни- ми состоит лишь в том, что все компоненты заголовка <cstdlib>, за исключени- ем макросов, введены в пространство имен std. Но, как мы помним, в компиля- торе Microsoft 6.0 вышла неувязочка с добавлением стандартных функций, унаследованных из языка С, в пространство имен std. Поэтому для избежания конфликтов используется конструкция директив в строках 10-12. После выпол- нения строки 12 функция exit переносится в глобальную область видимости для всех компиляторов, поэтому обращаться к ней можно без префикса std: (Проблемы с пространствами имен в компиляторе компании Microsoft были под- робно рассмотрены в главе 2 при обсуждении листинга 2.5.) В строках 20 и 24 объявляются функции, которые в целях тестирования про- граммы генерируют случайные записи адресов и контактов. С функцией generateAddresses мы уже знакомы. Функция generateAppointment работает аналогично, заполняя случайно созданными записями книгу контактов. Реализа- цию функции generateAppointment мы рассмотрим ниже в этой главе. 270 Глава 10. Сборка блоков программы
Реализация класса PIMDate с использованием шаблона auto_j>tr В строке 27 листинга 10.1 создается объект типа PIMDate. PIMDate — это основ- ной класс приложения TinyPIM, который служит вместилищем для объектов AddressBook и DateBook. Определение класса PIMDate показано в листинге 10.2. Листинг 10.2.дпределениекласса^?1МОаЪе . ' I „ JL _______________\ й 1://TinyPIM (с)1999 Pablo Halpern. Файл PlMData.h 2: 3:#ifndef PIMData_dot_h 4:#define PIMData_dot_h 1 5: 6:#ifdef _MSC_VER 7:#pragma warning(disable : 4786) 8:#endif 9: 10: #include <memory> ll:#include "AddressBook.h" 12:#include "DateBook.h" 13: 14:// Класс для поддержания всех данных текущего файла PIM. 15:class PlMData 16: { 17:public: 18: PlMData (){ } 19: AddressBook& addressBook() {return *addressBook_;} 20: DateBook& dateBookO {return *dateBook_;} 21: 22: void addressBook(std::auto_ptr<AddressBook> ab) 23: {addressBook_ = ab;) 24: void dateBook(std::auto_ptr<DateBook> db) {dateBook_ = db;} 25: 26:private: 27: std::auto_ptr<AddressBook> addressBook_; 28: std::auto_ptr<DateBook> dateBook_; 29: 30: // Поскольку данный класс основан на использовании указателей // auto_ptr с нестандартным 31: // копированием, то мы просто запрещаем копирование, // чтобы избежать проблем. 32: PlMData(const PIMData&); 33: PIMData& operator=(const PIMData&); 34: }; 35: 36:#endif // PIMData dot h Поскольку реализация всех функций-членов класса PIMDate следуют в одной строке после их объявлений, нет необходимости в создании файла PIMDate.срр. В строках 26 и 27 объявляются указатели на объекты AddressBook и DateBook. Но вместо обычных указателей мы используем шаблон auto ptr, который определен Реализация класса PIMDate с использованием шаблона autopt г 271
в файле заголовка <memory>, добавленном в строке 10. Этот шаблон определяет тип “интеллектуальных” указателей, основанных на семантике строгого владения. Принцип строгого владения состоит в том, что в определенный момент времени только один указатель auto_ptr может ссылаться на данный объект. Другими сло- вами, говорят, что указатель autoptr владеет связанным с ним объектом. При удалении указателя-владельца объекта из памяти компьютера удаляется и сам объ- ект. Использование стандартных указателей auto ptr, как и многих других средств стандартной библиотеки C++, значительно сокращает вероятность утечки памяти при выполнении программ. Обратите внимание, что использование “интеллектуального” указателя позволяет обойтись в классе PIMDate без деструкто- ра. Деструктор, сгенерированный компилятором по умолчанию, просто удаляет ука- затели auto ptr, что приводит к очистке памяти, занимаемой объектами AddressBookиDateBook. Интересные события происходят при попытке присвоения указателя autoptr другой переменной такого же типа. Например, если две переменные а и Ь имеют тип std: : auto_ptr<sometype>, то выражение а = Ь; вызовет следующую последовательность событий. 1. Объект, которым владел указатель а, удаляется. 2. Владение объектом, на который ссылался указатель Ь, передается указателю а, который теперь становится владельцем и ссылается на данный объект. 3. Значение Ь становится равным NULL. Таким образом, в результате простого присвоения происходит изменение обоих элементов выражения, а не только того, который находится слева. То же справед- ливо при использовании конструктора-копировщика шаблона auto ptr. Строгое владение предупреждает возникновение конфликтных ситуаций по поводу того, кто является владельцем объекта и кто ответствен за его удаление из памяти. нимание После TprQ’KaKyrasaTeBioTMriaautopttonpMCBaMBaercflyiaKMM-^MeocnopoCioM^HaHeHiffi ^итератор ^ (аассы-контейнёры в своей работе полйгаи нежелательнриспользоват|Ь!В качестве элем»ц6в,|аю^^и типа auto_ptr Шаблон auto pt г довольно необычен. Объекты, принадлежащие указателям auto_ptr, можно создавать одним из следующих способов: auto_ptr<тип> ptrl(новый тип); auto__ptr< тип> ptr2 = auto_pt г <тип> (новый тип); Передать владение указателю можно следующим образом: auto_ptr<rnn> ptr3 = новый тип; Причины столь необычного поведения объектов типа autoptr состоит в осо- бенности конструктора этого класса, который имеет явное объявление. Это означает, что в нем запрещены неявные, встроенные в компилятор алгоритмы преобразова- ния обычных указателей в тип auto ptr. Но следует учесть, что некоторые компи- ляторы еще не распознают ключевое слово explicit и допускают выполнение встроенных алгоритмов, что может привести к ошибке выполнения класса, осно- ванного на объектах auto ptr. 272 Глава 10. Сборка блоков программы
внимание! Ие1да:в^ласедШ%Ж''дас г new). Дело в том,что деструктор |;|®]й^н^11рдо^дл^здйсгй:!» ^зд^©дядаюр»хся;.яд!^ъ?,4 ^йшё'чгйомбий) инструкции В дополнение к конструктору и деструктору в классе auto_ptr определены еще операторы разыменования * и ->, которые работают так же, как и с обычными ука- зателями. В этом классе также определены функция-член get, которая возвращает обычный указатель на объект, принадлежащий указателю auto ptr, и функция release, которая работает так же, как и get, но после выполнения прекращает вла- дение объектом-указателем auto ptr, присваивая ему значение NULL. Еще одна функция-член reset принимает в качестве аргумента указатель на объект и застав- ляет указатель autopt г отказаться от владения текущим объектом и присвоить се- бе объект, переданный с аргументом. :На Особенности компиляции. Класс autoptr претерпел изменения За м етку В ходе последнего пересмотра стандартов языка C++. Но многие компи- ляторы, включая компилятор Microsoft 6.0, по-прежнему используют версию auto ptr, описанную в стандартах за декабрь 1996 года. Ос- новное различие в использовании старой и новой версий класса auto_ptr состоит в том, что в старой версии не происходит присваи- вание указателю значения NULL после выполнения функции release. Кроме того, в старой версии нет функции reset. Ваша программа с ис- пользованием объектов auto ptr будет нормально скомпилирована на любой версии компилятора, если вы никогда не будете обращаться к указателю autoptr после прекращения владения объектом и ис- пользовать функцию reset. В строках 19, 20 листинга 10.2 мы разыменовываем объекты auto_ptr, чтобы возвратить ссылки на реальные объекты AddressBook и DateBook. Функция addressBook в строках 22, 23 принимает аргумент типа auto_ptr<AddressBook> и присваивает это значение переменной-члену addressBook^. Чтобы внимательнее рассмотреть ход событий, следующий за вызовом данной версии функции addressBook, взгляните на часть кода главной программы, взятую из листинга 10.1. 31: try 32: { 33: // Создание адресной книги и книги контактов 34: std::auto_ptr<AddressBook> addrBookPtr(new AddressBook); 35: std::auto_ptr<DateBook> dateBookPtr(new DateBook); 36: 37: // Следующие инструкции выполняются, только если не было // обнаружено исключительных ситуаций 38 : myPIMData.addressBook(addrBookPtr); 39: myPIMData.dateBook(dateBookPtr); 40: } 41: catch (...) 42: { 43: std::cerr « "Could not create address and date books.\n”; Реализация класса PlMDate с использованием шаблона auto ptr 273
44: exit(EXIT_FAILURE); 45: } В строках 34, 35 создаются объекты auto_ptr, указывающие на объекты AddressBook и DateBook. При вызове функции addressBook в строке 38 выполняются две операции передачи прав владения. Владение объектом AddressBook передается от объекта-указателя addrBookPtr к указателю аЬ, являющемуся аргументом функции PlMData: : addressBook. Затем право владения передается от аргумента аЬ перемен- ной-члену addressBook^ объекта myPIMData. В результате myPIMData получает во владение объект addressBook, а объекту-указателю addrBookPtr присваивается зна- чение NULL. Функция dateBook, которая вызывается в строке 39, поступает точно так же с объектом DateBook. Чтобы предоставить право владения объектом вызывающей функции, важно передать аргумент autoptr как значение, а не как ссылку. Если объ- ект autoptr будет использоваться в функции только для передачи права владения объектом, то его следует сделать константным. Неконстантные объекты auto__ptr ис- пользуются только в качестве реципиентов объектов от других указателей. Важной особенностью данного фрагмента программы является его ошибкоустой- чивость, связанная с использованием объектов auto_ptr. Что произойдет в случае возникновения исключительной ситуации при попытке создания объекта DateBook? Выполнение блока try немедленно завершится, и программа перейдет к выполне- нию блока catch в строке 43. Если бы переменная addrBookPtr была обычным ука- зателем, произошла бы утечка памяти из-за того, что указатель оказался бы за пре- делами видимости до того, как был удален его объект. Но поскольку addrBookPtr — это объект типа auto_ptr, при выходе из области видимости будет вызван деструк- тор класса auto_ptr, который удалит как сам объект-указатель, так и связанный с ним объект в области динамического обмена памяти. Мы могли бы ввести объект AddressBook непосредственно в объект PIMDate в качестве члена класса, не прибе- гая к промежуточным указателям. Но это вызвало бы другую проблему. В случае возникновения какой-либо исключительной ситуации с созданием объектов-членов объект myPINData оказался бы лишь частично инициализированным. В нашей (несколько надуманной) ситуации это не привело бы к страшным последствиям. Но в других случаях бывает важно удерживать в программе весь набор промежуточных объектов до тех пор, пока не завершится создание конечного объекта. Высокая ошибкоустойчивость программных блоков, основанных на использовании объектов auto pt г,— это одна из основных причин, почему данный шаблон был добавлен в стандартную библиотеку C++. Прежде чем покинуть выбранный нами фрагмент программного кода, обратите внимание на строку 44, которая выполняется только в том случае, если произошла ошибка при создании объектов AddressBook или DateBook. Функция std: : exit де- лает именно то, на что указывает ее имя. Она прекращает выполнение программы и вызывает деструкторы для всех глобальных и статических переменных. Поэтому будет не лучшим решением вызывать функцию exit где-либо, кроме главного блока программы, и то, только в том случае, если в этом блоке не были объявлены локаль- ные переменные, для удаления которых требуется деструктор. Выполнение про- граммы можно завершить путем отслеживания возникновения исключительных си- туаций и запуска для них кодов исключений в главной программе. Чтобы быть уве- ренным в успешном удалении локальных переменных функции main, можно просто возвратить константу EXIT_FAILURE вместо вызова функции exit (EXIT_FAILURE). Именно поэтому функция exit значительно реже используется в C++, чем в С. (Честно говоря, я не помню в своей практике ни одного случая использования функ- ции exit, кроме как в этой книге, где она используется для примера.) 274 Глава 10. Сборка блоков программы
Классы Appointment И DateBook Первое, что делает функция main,— создает объекты AddressBook и DateBook (см. строки 34, 35 листинга 10.1). С классом AddressBook мы уже хорошо познако- мились в предыдущих главах. Посмотрим теперь на заголовок класса DateBook, по- казанный в листинге 10.3. Глйсршг 10.3.>Опр^ЛеНИёМа&са DateBook 1://TinyPIM (c)1999 Pablo Halpern. Файл DateBook.h 2: 3:#ifndef DateBook_dot_h 4:#define DateBook_dot_h 1 5: 6:#include <set> 7:#include <map> 8: 9:#include "Appointment.h" 10: 11:class DateBook 12: { 13: // Псевдонимы типов данных 14: typedef std::multiset<Appointment> apptByTime_t; 15: typedef std::map<int, apptByTime_t::iterator> apptById_t; 16: 17:public: 18: DateBook(); 19: -DateBook(); 20: 21: //Классы исключений 22: class appointmentNotFound {}; 23: class Duplicateld {}; 24 = 25: int insertAppointment(const Appointments appt, int recordld = 0) 26: throw (Duplicateld); 27: void eraseAppointment(int recordld) throw (appointmentNotFound); 28: void replaceAppointment(const Appointments appt, int recordld = 0) 29: throw (appointmentNotFound); 30: const Appointments getAppointment(int recordld) const 31: throw (appointmentNotFound); 32: 33: // Итератор для навигации по записям книги контактов 34: typedef apptByTime__t::const_iterator const_iterator; 35: 36: // Функции навигации по книге контактов 37: const_iterator begin() const {return appointments_.begin();} 38: const_iterator end() const {return appointments_.end();} 39: 40: // Находит первую запись контакта со временем начала 41: // большим и равным указанному. 42: const—iterator findAppointmentAtTime(const DateTimes dt) const; Классы Appointment и DateBook 275
43: 44: // Находит следующую запись контакта, содержащую заданную 45: // строку. Начало поиска задается параметром start. 46: const__iterator findNextContains(const std::strings searchStr, 47: const_iterator start) const; 48: 49: // Возвращает итератор на запись с заданным ID. 50: const—iterator findRecordld(int recordld) const 51: throw (appointmentNotFound); 52: 53:private: 54: // Запрещение копирования 55: DateBook(const DateBook&); 56: DateBook& operator=(const DateBook&); 57: 58: static int nextld—; 59: 60: apptByTime_t appointments—; 61: apptByld—t apptById_; 62: 63: // Возвращает индекс записи по указанному ID. 64: apptByTime_t::iterator getByld(int recordld) 65: throw (appointmentNotFound); 66: apptByTime_t::const—iterator getByld(int recordld) const 67: throw (appointmentNotFound); 68: }; 69: 70: 71:#endif // DateBook dot—h Класс DateBook построен так же, как и класс AddressBook, поэтому их файлы за- головков отличаются только именами членов. Строки 14, 15 содержат псевдонимы структур данных, которые определяются в строках 60 и 61. Объекты Appointment сохраняются в контейнере multiset и сортируются по времени начала контакта. Контейнер multiset позволяет эффективно вводить, удалять и находить записи контактов по времени начала. Поскольку нам также необходимо иметь возможность быстро отыскивать записи по их идентификационным номерам, в классе определен вторичный индекс apptByld, который устанавливает взаимосвязи между номерами ID и соответст- вующими элементами Appointment с помощью шаблона контейнера тар. В качестве ключа контейнера-карты выступают идентификационные номера записей, пред- ставленные целочисленными значениями. Содержимым каждого элемента контей- нера тар является итератор, указывающий на соответствующий объект Appointment в исходном контейнере appointments-. (Подробно работу указанных структур данных мы рассматривали в главе 6.) Для навигации по записям книги контактов класс DateBook предоставляет ите- раторы и функции begin и end (строки 34-38). (Работу итераторов мы рассматрива- ли подробно в главе 4.) Функция findAppointmentAtTime, объявленная в стро- ке 42, возвращает итератор на первую запись контакта, начинающегося не раньше заданного времени. Этот же итератор может быть затем использован для получения доступа к следующим записям, например, чтобы посмотреть, какие еще контакты назначены до конца текущего дня. Функция findNextContains, определенная в строках 46, 47, возвращает итератор на следующую запись, содержащую заданную строку. Эта функция работает так же, как и соответствующая ей функция в классе 276 Глава 10. Сборка блоков программы
AddressBook. Другие функции, объявленные в строках25-31, просто добавляют, удаляют и возвращают записи, как аналогичные им функции в классе AddressBook. Реализацию класса DateBook мы рассмотрим ниже в этой главе. Класс DateBook используется в качестве контейнера объектов класса Appointment, определение которого показано в листинге 10.4. 1://TinyPIM (с)1999 Pablo Halpern. Файл Appointment.h 2: 3: #ifndef Appointment__dot__h 4:#define Appointment_dot_h 1 5: 6:#include <string> 7:#include "DateTime.h" 8: 9:class Appointment 10: { 11:public: 12: Appointment () : recordld__ (0) { } 13: 14: // Функции доступа к полям 15: int recordld() const {return recordld_;} 16: void recordld(int i) {recordld = i;} 17: 18: DateTime startTimeO const {return startTime__; } 19: void startTime(const DateTime& dt) ; 20: 21: DateTime endTimeO const {return endTime_;} 22: void endTime(const DateTime& dt); 23: 24: std::string description() const {return description_;} 25: void description(const std::strings s); 26: 27:private: 28: int recordld_; 29: DateTime startTime_; 30: DateTime endTime_; 31: std::string description_; 32: } ; 33: 34:inline bool operator<(const Appointments al, const Appointments a2) 35: {return al.startTime() < a2.startTime();} 36: 37:#endif // Appointment dot h Класс Appointment, как и класс Address, представляет собой базовую струк- туру данных, состоящую из набора скалярных полей. В классе определены функции доступа для установки и возвращения значений этих полей и выполне- ния некоторых других задач. Поля startTime^ H .endTime^, объявленные в стро- ках 29 й 30, являются объектами класса DateTime, который мы рассматривали в предыдущей главе. Оператор меньше (<) класса Appointment, определенный в строках 34, 35, сравнивает время начала в объектах контактов, переданных с аргументами. На основе функции этого оператора выполняется сортировка за- писей в книге контактов. Классы Appointment и DateBook 277
Реализация оставшихся функций класса Appointment, которые не были выпол- нены в одной строке сразу за определениями, довольно тривиальна (листинг 10.5). 1://TinyPIM (с)1999 Pablo Halpern. Файл Appointment.срр 2: 3:#include "Appointment.h" 4: 5:void Appointment::startTime(const DateTime& dt) 6: { 7: startTime_ = dt; 8:} 9: 10:void Appointment::endTime(const DateTime& dt) 11: { 12: endTime_ = dt; 13: } 14: 15:void Appointment::description(const std::string& s) 16: { 17: description^ = s; 18:} Функция generateAppointment После создания объектов AddressBook и DateBook функция main (см. лис- тинг 10.1) вызывает функции, которые заполняют адресную книгу и книгу контак- тов случайным образом созданными записями. (Эти вызовы можно будет отключить во время компиляции, установив символ препроцессора NOGENERATE.) Код этих функций записан в отдельном файле Test Data. срр, показанном в листинге 10.6. 1://TinyPIM (с)1999 Pablo Halpern. Файл TestData.cpp 2: 3:#ifdef _MSC_VER 4: #pragma warning(disable : 4786) 5:#endif 6: 7:#include <cstdlib> 8:#include <sstream> 9:#include <iomanip> 10: ll:#ifdef _MSC_VER 12‘.namespace std { 13: inline int rand(){return ::rand();} 14: inline void srand(unsigned s){::srand(s);} 15: } 16:#endif 17: 18:#include "AddressBook.h" 19:#include "DateBook.h" 278 Глава 10. Сборка блоков программы
20: 21:// Случайное возвращение элементов из константных массивов строк 22:template <class А> 23:inline const char* randomstring(A& stringArray) 24: { 25: int size = sizeof(A)/sizeof(stringArray[0]); 26: int index = std::rand() % size; 27: return stringArray[index]; 28: } 29: 30:void generateAddresses(AddressBook& addrbook, int numAddresses) 31: { 32: // Константный посев генератора случайных чисел, 33: //чтобы каждый раз получать одну и ту же последовательность // значений. 34: std::srand(100); 35: 36: static const char* const lastnames[] = { 37: "Clinton”, "Bush", "Reagan", "Carter", "Ford", "Nixon", 38: "Johnson", "Kennedy" 39: }; 40: 41: static const char* const firstnames[] = { 42: "William", "George", "Ronald", "Jimmy", "Gerald", "Richard", 43: "Lyndon", "Jack", ."Hillary", "Barbara", "Nancy", "Rosalynn", 44: "Betty", "Pat", "Ladybird", "Jackie" 45: }; 46: 47: // Названия деревьев будут использоваться // для именования улиц и городов. 48: static const char* const trees[] = { 49: "Maple", "Oak", "Willow", "Pine", "Hemlock", "Redwood", "Fir", 50: "Holly", "Elm" 51: }; 52: 53: static const char* const streetSuffixes[] = { 54: "St.", "Rd.", "Ln.", "Terr.", "Ave." 55: }; 56: 57: static const char* const townSuffixes[] - { 58: "ton", "vale", "burg", "ham" 59: }; 60: 61: // Аббревиатуры названий штатов и территорий США. 62: //Получены с Web-страницы почтовой службы США: 63: //http://www.usps.gov/cpim/ftp/pubs/201html/addrpack.htm#abbr 64: static const char* const states[] = { 65: "AL", "AK", "AS", "AZ", "AR", "CA", "CO", "CT", "DE", 66: "DC", "FM", "FL", "GA", "GU", "HI", "ID", "IL", "IN", 67: "IA", "KS", "KY", "LA", "ME", "MH", "MD", "MA", "MI", 68: "MN", "MS", "MO" ,"MT", "NE", "NV", "NH", "NJ", "NM", 69: "NY", "NC", "ND"*, "MP", "OH", "OK", "OR", "PA", "PR", 70: "RI", "SC", "SD", ilipjq n "TX", "UT", "VT", "VA", "VI", 71: "WA", "WV", "WI", "WY" 72: }; 73: Классы Appointment и DateBook 279
74: for (int i = 0; i < numAddresses; ++i) 75: { 76: Address addr; 77: addr.lastname(randomstring(lastnames)); 78: addr.firstname(randomstring(firstnames)); 79: 80: // Генерирование номера телефона с помощью потока // stringstream 81: std::stringstream phonestream; 82: phonestream « ’(' « (std::rand() % 800 + 200) « ")" 83: « (std::rand() % 800 + 200) « 84: « std::setfill(’0’) « std::setw(4) 85: «(std: : rand () % 10000); 86: addr.phone(phonestream.str()); 87: 88: std::stringstream addrstream; 89: // Генерирование названия улицы и номера дома. 90: addrstream « (std::rand() % 100 + 1) « " " 91: « randomstring(trees) « " " 92: « randomstring(streetSuffixes) « *\n'; 93: 94: // Генерирование названий города, штата //и почтового индекса. 95: addrstream « randomstring(trees) « randomstring(townSuffixes) 96: « « randomstring(states) « " ” 97: « std::setfill('0•) « std::setw(5) 98: « (std::rand() % 99999 + 1); 99: addr.address(addrstream.str()); 100: 101: addrbook.insertAddress(addr); 102: } 103: } 104: 105:// Вспомогательная функция для заполнения книги контактов 106:Appointment randomAppointment(DateTime date, int minHour, 107: int maxHour, int maxDuration, 108: const std::string desc) 109: { 110: // Генерирует значения часов в диапазоне от minHour до maxHour 111: int hour = std::rand() % (maxHour - minHour +1) + minHour; 112: 113: // Генерирует значения минут = 0, 15, 30 или 45 114: int min -(std::rand() % 4) * 15; 115: 116: // Генерирует продолжительность (в часах) // в диапазоне от 1 до maxDuration 117: int duration = std::rand() % maxDuration + 1; 118: 119: Appointment result; 120: date.setTime(hour, min); 121: result.startTime(date); 122: date.setTime(hour + duration, min); 123: result.endTime(date); 124: result.description(desc); 125: 126: return result; 280 Глава 10. Сборка блоков программы
127: } 128: 129:// Генерирует контакт между startDate и endDate 130:void generateAppointments(DateBookfi dateBook, int numDays) 131: { 132: // Константный посев генератора случайных чисел, 133: // чтобы каждый раз получать одну // и ту же последовательность значений. 134: std::srand(50); 135: 136: static const char* const meetingTypes[] = { 137: "Meeting”, "Review meeting", "Urgent meeting", "Status meeting" 138: }; 139: 140: static const char* const activities[] = { 141: "Golf", "Raquetball", "Dancing", "Piano Lesson" 142: }; 143: 144: static const char* const firstnames[] = { 145: "William", "George", "Ronald", "Jimmy", "Gerald", "Richard", 146: "Lyndon", "Jack", "Hillary", "Barbara", "Nancy", 147: "Rosalynn", "Betty", "Pat", "Ladybird", "Jackie" 148:}; 149: 150:// Вычисляем диапазон с равным числом дней до и 151:// после текущего дня. 152‘.DateTime startDate = DateTime::now().startOfDay().addDay(-numDays/2); 153:DateTime endDate = startDate.addDay(numDays); 154:std::string desc; 155:for (DateTime currDate ® startDate; currDate < endDate; 156: currDate « currDate.addDay(1)) 157: { 158: // Вероятность установки контакта на утро между 159: // 8:00 и 10:45am равна 1/4. 160:desc = std::string(randomstring(meetingTypes)) + " with " + 161: randomstring(firstnames); 162: if (std::rand() %4 < 1) 163: dateBook.insertAppointment 164: randomAppointment(currDate, 8, 10, 3, desc)); 165: 166: // Вероятность установки контакта на время обеда равна 2/11 167: desc = std::string("Lunch with ") + randomstring(firstnames); 168: if (std::rand() %11 < 2) 169: dateBook.insertAppointment randomAppointment(currDate, 12, 12, 170: 2, desc)); 171: 172: // Вероятность установки контакта на вечернее время // равна 1/6 173: desc = std::string(randomstring(activities)) + " with " + 174: randomstring(firstnames); Классы Appointment и DateBook 281
175: if (std::rand() %6 < 1) 176: dateBook.insertAppointment 177: randomAppointment(currDate, 18, 21, 4, desc)); 178: } 179: } Реализация функции geberateAppointment во многом напоминает работу функ- ции generateAddresses, которую мы рассматривали в главе 8. Первое, что мы дела- ем, — устанавливаем в строке 134 посев генератора случайных чисел. Установив константное значение посева, мы инструктируем программу генерировать каждый раз одну и ту же последовательность псевдослучайных чисел. В строках 152, 153 мы выполняем простые арифметические действия с датами, чтобы вычислить диапазон с равным числом дней до и после текущего дня (DateTime: :now()). Общее число дней передается с аргументом. В строке 155 начинается цикл, проходящий через все дни рассчитанного нами диапазона. Затем для каждого дня мы “бросаем кости” для установки с заданной вероятностью утренних мероприятий, совместных обедов и вечерних встреч. В строке 160 генерируется описание мероприятия. Для этого из соответствующе- го массива строк случайным образом выбираются типы контактов, а из массива имен — лица, с которыми назначена встреча. В строке 162 функция rand использу- ется для установки вероятности того, что назначенный контакт придется на утрен- ние часы. Выражение s td: : rand () % 4 возвращает случайные целые числа в диапа- зоне от 0 до 3. В строке 162 задано условие назначения контакта на утреннее время, если возвращенное случайное число будет равно нулю (что будет происходить с ве- роятностью 1/4). Для создания записи в книге контактов вызывается функция randomAppointment, выполнение которой начинается в строке 111. В эту функцию передаются диапазон часов, между которыми может быть установлен контакт (для утреннего контакта между 8ч и 10ч), и длительность контакта, не превышающая 3-х часов. Выражение в строке 111 сгенерирует случайное целое число в диапазоне от 8 до 10. В строке 114 возвращается значение минут, которое может быть рано 0, 15, 30 или 45 мин. Выражение в строке 117 устанавливает длительность контакта в пределах от 1 ч до значения maxDuration (для утреннего контакта равно трем). Завершив выбор числовых значений, программа создает в строках 119-124 новый объект типа Appointment и заносит его в книгу контактов. Аналогичные вычис- ления проводятся для установки контактов на время обеда (строки 167-170) и на ве- чер (строки 173-177), после чего цикл повторяется для следующего дня установлен- ного диапазона. Главное меню После того как функция main создала объекты AddressBook и DateBook и напол- нила их записями, программа переходит к выполнению блока, показанного ниже (взят из листинга 10.1). 55: // Создание меню адресной книги и книги контактов 56: AddressBookMenu addrBookMenu(myPIMData.addressBook()); 57: DateBookMenuCatalog dateBookMenus(myPIMData.dateBook()); 58: 59: // Создание главного меню и добавление его в стек 282 Глава 10. Сборка блоков программы
60: MainMenu mainMenu(&addrBookMenu, dateBookMenus.monthlyMenu()); 61: Menu::enterMenu(&mainMenu); 62: 63: // Обработка опций до выхода из меню. 64: while (Menu::isActive()) 65: Menu::activeMenu()->mainLoop(); В строке 56 определяется объект AddressBookMenu, а в следующей строке — объ- ект dateBookMenus типа DateBookMenuCatalog. Что представляет собой DateBookMenuCatalog, мы рассмотрим в следующем разделе. Сейчас достаточно бу- дет знать, что объект dateBookMenus содержит указатели на объекты меню, исполь- зуемые в книге контактов. В строке 60 создается объект mainMenu, в который пере- даются указатели на объекты меню адресной книги и книги контактов, которые ста- новятся подменю. Функция-член mainloop главного меню предлагает пользователю опции открыть адресную книгу, книгу контактов или выйти из программы. Прежде чем мы углубимся в изучение класса MainMenu, давайте завершим рас- смотрение выбранного блока функции main. В строке 61 главное меню делается ак- тивным за счет добавления указателя на ее объект в вершину стека с помощью ста- тической функции enterMenu класса Menu. Класс Menu поддерживает структуру данных типа стек, где доступ к элементам от- крывается по принципу “последним положил — первым взял”. Для реализации стека используется стандартный адаптер контейнеров stack. Функция enterMenu помещает новый указатель на объект меню в вершину стека. То меню, чей.указатель находится в вершине стека, является активным. Главное меню перестанет быть активным после того, как оно поместит в вершину стека указатель одного из своих подменю. В свою очередь, любое подменю перестает быть активным после вызова своей функции-члена exitMenu, которая удаляет указатель текущего меню из вершины стека и делает ак- тивным следующий элемент стека (главное меню). (Подробно о работе стека и системы меню см. в главе 8.) В строке 64 (листинг 10.1) запускается цикл while, который про- должается до тех пор, пока стек не окажется пустым, что может случиться только после выбора функции exitMenu для главного меню. Единственная строка цикла в конце функции main вызывает функцию ma inLoop активного меню (строка 65). Определение класса MainMenu показано в листинге 10.7. 1://TinyPIM (с)1999 Pablo Halpern. Файл MainMenu.h 2: 3:#ifndef MainMenu__dot_h 4:#define MainMenu_dot_h 1 5: 6:# include "Menu.h” 7: 8:class MainMenu : public Menu 9: { 10:public: 11: MainMenu(Menu* addrBookMenu, 12: Menu* dateBookMenu) 13: : addrBookMenu^(addrBookMenu), dateBookMenu_(dateBookMenu) { } 14: 15: void mainLoopO; 16: 17:private: Главное меню 283
18: void addressBook(); 19: void dateBook(); 20: void quit(); 21: 22: Menu* addrBookMenu__; 23: Menu* dateBookMenu_; 24: }; 25: 26:#endif // MainMenu dot h Как и можно было предположить, класс Ma inMenu производится от класса Menu (см. стронув). В классе определяются собственная функция mainLoop (строка 15) и три закрытые функции для каждой опции главного меню (строки 18-20). Указатели, опре- деленные в строках 22 и 23, используются для запуска подменю в соответствии с выбо- ром пользователя. Реализация функций класса MainMenu показана в листинге 10.8. 4s* Ss Г rti ~'*Ч‘ 1://TinyPIM (с)1999 Pablo Halpern. Файл MainMenu.срр 2: 3:#ifdef _MSC_VER 4:#pragma warning(disable : 4786) 5:#endif 6: 7:#include <iostream> 8:#include "PlMData.h" 9:#include "MainMenu.h" 10:#include "AddressBookMenu.h" ll:#include "DateBookMenu.h" 12: 13:void MainMenu::mainLoop() 14: { 15: clearScreen(); 16: std::cout « "Welcome to TinyPIM!\n\n" 17: « "*** Main Menu ***\n\n"; 18: 19: static const char menu[] - 20: "Please select from the following:\n\n" 21: " ((A)ddress Book\n (D)ate Book\n (Q)uit\n\n" 22: "Enter selection> "; 23: 24: switch (getMenuSelection(menu, "ADQ")) 25: { 26: case ’A’: addressBook(); break; 27: case ’ D’: dateBook(); break; 28: case *Q’: 29: default: quit(); break; 30: } // Конец блока switch 31:} \ 32: ' 33:void MainMenu::addressBook() 34: { 35: enterMenu(addrBookMenu_); 36: } 37: 38:void MainMenu::dateBook() 39: { 284 Глава 10. Сборка блоков программы
40: enterMenu(dateBookMenu_); 41:} 42: 43:void MainMenu::quit() 44: { 45: exitMenu(); 46: } В строке 24 функция mainLoop вызывает функцию-член базового класса getMenuSelection и передает в нее текст приглашения и строку значимых командных символов выбора опций. Если пользователь введет D, будет вызвана функция dateBook, показанная в строках 38-41. Эта функция помещает указатель на объект меню книги контактов в вершину стека, но не вызывает функцию главного цикла этого подменю. Функция MainMenu: : dateBook возвращается в функцию MainMenu: : mainLoop, которая, в свою очередь, возвращается в функцию main главной программы. Затем в функции main выполняется новый цикл, который вызывает функцию mainLoop для активного меню, которым теперь стало меню книги контактов. Создание меню книги контактов В главе 1 мы планировали создать для книги контактов только один класс меню DateBookMenu, производный от базового класса Menu. Но тогда мы не учли, что книга контактов должна работать в нескольких режимах: ежемесячника, еженедельника, ежедневника и в режиме поиска. Причем в разных режимах пользователь по- разному будет взаимодействовать с программой. Так, в режиме ежемесячника ка- лендарь текущего месяца будет предлагаться пользователю в следующем виде: 1* 2* 3 4 5 6* 7* 8* 9 10 11* 12 13 14 15* 16 17* 18* 19* 20* 21 22 23* 24 25* 26* 27 28* 29 30* Звездочками отмечены дни, на которые назначен хотя бы один контакт. В виде ежемесячника пользователю предлагается на выбор не так-то уж много опций: лис- тать календарь на следующие месяцы или выбрать другой вид. В еженедельном режиме программа показывает назначенные контакты по дням недели, как в следующем примере: 1:Sunday 09/26/99 2:12:45РМ - 01:45PM Lunch with Barbara 3:Tuesday 09/28/99 4:12:00PM - 02:00PM Lunch with Hillary 5:Thursday 09/30/99 6:09:00AM - 10:00AM Meeting with Pat 7:12:45PM - 01:45PM Lunch with Pat 8:Saturday 10/02/99 9:12:15PM - 01:15PM Lunch with Hillary В ежедневном режиме программа отображает контакты, назначенные на вы- бранный день: 1:09:00АМ - 10:00AM Meeting with Pat 2:12:45PM - 01:45PM Lunch with Pat В режиме поиска отображаются все записи контактов, содержащие заданное ключевое слово, например Pat: Создание меню книги контактов 285
1:03/30/99 06:15PM - 09:15PM Dancing with Pat 2:05/29/99 12:15PM - 02:15PM Lunch with Pat 3:09/07/99 10:45AM - 01:45PM Status meeting with Pat 4:09/30/99 09:00AM - 10:00AM Meeting with Pat 5:09/30/99 12:45PM - 01:45PM Lunch with Pat В режиме поиска, еженедельника и ежедневника пользователь может выполнять следующие операции: создавать, просматривать, удалять и редактировать записи книги контактов, но и здесь есть свои отличия. Для поддержания работы с книгой контактов в разных режимах нам придется до- бавить дополнительные классы. На рис. 10.1 показана детализированная диаграмма классов, отображающая иерархию классов меню книги контактов. Рис. 10.1. Диаграмма классов меню книги контактов 286 Глава 10. Сборка блоков программы
Базовый класс DateBookMenu объединяет функции смены режимов и выбора за- писей для показа, которые являются общими для меню всех режимов книги контак- тов. Класс MonthlyDateBookMenu добавляет функции показа календаря текущего месяца и перехода от месяца к месяцу. Класс ListBasedDateBookMenu является ба- зовым для меню всех режимов, в которых записи отображаются в виде прокручи- ваемого списка, и заключает в себе все функции, необходимые для управления эк- ранными списками. Классы WeeklyDateBookMenu и DailyDateBookMenu добавляют свои функции перехода от недели к неделе и ото дня ко дню. Класс StringSearchDateBookMenu содержит функцию-член поиска записей по ключевым словам. Все концевые классы (классы-листья) имеют свои версии функции mainLoop, поддерживающие обработку выбора опций, специфических для каждого режима. Т - В любом виде иерархических взаимосвязей листьями называются кон- “РМИп цевые элементы, которые не имеют подчиненных им элементов. Диа- граммы иерархических структур данных обычно имеют вид переверну- той ветки с элементами-листьями в нижней части. Класс DateBookMenuCatalog Класс DateBookMenuCatalog определен для того, чтобы управлять экземпляра- ми всех концевых классов меню. При выборе вида отображения книги контактов объект класса DateBookMenu инструктирует объект DateBookMenuCatalog, какое меню следует выбрать и сделать активным. Класс DateBookMenuCatalog также от- ветствен за создание и удаление объектов четырех меню книги контактов. Все классы, образующие системы меню книги контактов, определены в одном файле заголовка DateBookMenu. h, показанном в листинге 10.9. 3 янш. .;«?.< «кН* 'Ш* w ' ' .мН 1://TinyPIM (с)1999 Pablo Halpern. Файл DateBookMenu.h 2: 3:#ifndef DateBookMenu_dot_h 4:#define DateBookMenu_dot_h 1 5: 6:#include <memory> 7:#include "Menu.h" 8:#include "AppointmentDisplayList.h" 9: 10:// Прямые ссылки 11:class DateBookMenuCatalog; 12: 13:class DateBookMenu : public Menu 14: { 15:public: 16: DateBookMenu(DateBook& dateBook, DateBookMenuCatalog* catalog) 17: :dateBook_(dateBook), catalog_(catalog), 18: currDate (DateTime::now()) { } 19: 20: void setDate(DateTime dt) ; 21: 22:protected: 23: void createEntry(); Создание меню книги контактов 287
24: void showDay(); 25: void showWeeк(); 26: void showMonth(); 27: void search(); 28: void gotoDate(); 29: 30: virtual void reset(); 31: 32: DateBook& dateBook_; 33: DateTime currDate_; 34: DateBookMenuCatalog* catalog_; 35: }; 36: 37:// Производный класс ежемесячника 38-.class MonthlyDateBookMenu : public DateBookMenu 39: { 40:public: 41: MonthlyDateBookMenu(DateBook& dateBook, 42: DateBookMenuCatalog* catalog) 43: :DateBookMenu(dateBook, catalog), cacheGood_(false) { } 44: 45: void mainLoopO; 46: 47:private: 48: void displayMonth(); 49: void showNextO; 50: void showPrevious(); 51: 52: virtual void reset(); 53: 54: bool scoreBoard_[32]; // Нулевой индекс не используется 55: bool cacheGood__; // Истинно, если scoreBoard _ содержит // допустимое значение 56: }; 57: 541.// Базовый класс для всех видов, основанных на просмотре списков 59:class ListBasedDateBookMenu : public DateBookMenu 60: { 61:public: 62: ListBasedDateBookMenu(DateBook& dateBook, 63: DateBookMenuCatalog* catalog) 64: :DateBookMenu(dateBook, catalog), displayList_(dateBook) { } 65: 66:protected: 67: void viewEntryO; 68: void editEntryO; 69: void deleteEntry(); 70: 71: virtual void reset(); 72: 73: AppointmentDisplayList'displayList ; 74:}; 75: 76:// Производный класс для еженедельника 77:class WeeklyDateBookMenu : public ListBasedDateBookMenu 78: { 288 Глава 10. Сборка блоков программы
79:public: 80: WeeklyDateBookMenu(DateBookS dateBook, 81: DateBookMenuCatalog* catalog); 82: 83: void mainLoop(); 84: 85:private: 86: void showNext(); 87:void showPrevious(); 88: }; 89: 90:// Производный класс для ежедневника 91:class DailyDateBookMenu : public ListBasedDateBookMenu 92: { 93:public: 94 : DailyDateBookMenu (Dat eBooks dateBook, DateBookMenuCatalog* catalog); 95: 96: void mainLoop(); 97: 98:private: 99: void showNext(); 100: void showPrevious(); 101:}; 102: 103:// Производный класс для режима поиска 104:class StringSearchDateBookMenu : public ListBasedDateBookMenu 105: { 106:public: 107: StringSearchDateBookMenu(DateBookS dateBook, 108: DateBookMenuCatalog* catalog); 109: 110: std::string searchstring() const {return searchString__;} 111: void searchstring(const std::strings s); 112: 113: void mainLoop(); 114: 115:private: 116: std::string searchString_; 117: }; 118: 119:// Класс каталога всех объектов меню книги контактов 120:class DateBookMenuCatalog 121: { 122:public: 123: DateBookMenuCatalog(DateBookS dateBook); 124: 125: // Используется деструктор, сгенерированный компилятором 126: // -DateBookMenuCatalog(); 127: 128: DailyDateBookMenu* dailyMenuQ; 129: WeeklyDateBookMenu* weeklyMenu(); 130: MonthlyDateBookMenu* monthlyMenu(); 131: StringSearchDateBookMenu* stringSearchMenu(); 132: 133:private: 134: // Копирование запрещено, так как может вызвать сбой программы. 135: DateBookMenuCatalog(const DateBookMenuCatalogS); Создание меню книги контактов 289
136: DateBookMenuCatalog&operator=(const DateBookMenuCatalog&); 137: 138: // Данный объект владеет копиями всех типов меню 139: std::auto_ptr<DailyDateBookMenu> dailyMenu_; 140: std::auto_ptr<WeeklyDateBookMenu> weeklyMenu_; 141: std::auto_ptr<MonthlyDateBookMenu> monthlyMenu_; 142: std::auto_ptr<StringSearchDateBookMenu> stringSearchMenu_; 143:}; 144: 145:#endif // DateBookMenu dot h Как видите, структура классов довольно проста, и, почти все элементы классов были показаны еще на диаграмме на рис. 10.1. В строке 16 конструктор класса DateBookMenu принимает два параметра: ссылку на объект DateBook и указатель типа DateBookCatalog. Конструкторы всех производных классов принимают те же два параметра и передают в конструктор базового класса. Базовый класс также берет на себя ответственность за отслеживание текущего дня (и времени), записи для ко- торого отображаются на экране. Поскольку в виде ежемесячника экранный список записей не отображает, для класса MonthlyDateBookMenu в строке 54 определяется специальный буфер scoreBoard__, который отслеживает дни месяца, на которые назначены контакты. Флаг cacheGood_, определенный в строке 55, показывает текущее состояние буфера scoreBoard_. Объект экранного списка определяется в строке 73 в классе ListBasedDateBookMenu, который является базовым для всех видов, основанных на показе списков записей. Класс экранных списков книги контактов AppointmentDisplayList наследуется от класса DisplayList. Ниже мы рассмотрим его подробнее. Указатели на концевые классы меню книги контактов представлены в классе DateBookMenuCatalog в виде объектов auto ptr. Конструктор-копировщик и опе- ратор присваивания заблокированы для этого класса (определены в строках 135, 136 в разделе private и не выполняются), поскольку копирование объекта DateBookMenuCatalog, даже если бы оно требовалось, в действительности не приве- дет к созданию копии меню, а лишь передаст право владения другому объекту. Функция main главной программы создает объект DateBookMenuCatalog и пере- дает в его конструктор объекта DateBook. Фрагмент файла DateBookMenu. срр с реа- лизацией конструктора и некоторых других функций класса DateBookMenuCatalog показан в листинге 10.10. ^Лист^г10.10Г Реализация функций Miacca DateBobkMen.uCatalog ’ | 455:DateBookMenuCatalog::DateBookMenuCatalog(DateBook& dateBook) 456: : dailyMenu_(new DailyDateBookMenu(dateBook, this)), 457: weeklyMenu_(new WeeklyDateBookMenu(dateBook, this)), 458: monthlyMenU—(new MonthlyDateBookMenu(dateBook, this)), 459: stringSearchMenu_(new StringSearchDateBookMenu(dateBook, this)) 460: { 461: } 462: 463:DailyDateBookMenu* DateBookMenuCatalog::dailyMenu() 464: { 465: return dailyMenu_.get(); 466: } 290 Глава 10. Сборка блоков программы
467: ' 468:WeeklyDateBookMenu* DateBookMenuCatalog::weeklyMenu() 469: { 470: return weeklyMenu_.get(); 471: } 472: 473:MonthlyDateBookMenu* DateBookMenuCatalog::monthlyMenu() 474: { 475: return monthlyMenu_.get(); 476: } 477: 478:StringSearchDateBookMenu* DateBookMenuCatalog::stringSearchMenu() 479: { 480: return stringSearchMenu_.get() ; 481:} В строках 456-459 конструктор инициализирует каждый объект autoptr соот- ветствующим объектом меню, созданным в области динамического распределения памяти. В результате объект DateBookMenuCatalog становится владельцем всех объектов меню почти в той же мере, как если бы они были определены как члены этого класса. После создания объекта DateBookMenuCatalog функция main главной программы приступает к созданию объекта MainMenu: MainMenu mainMenu(&addrBookMenu, dateBookMenu.monthlyMenu()); В качестве второго параметра конструктора mainMenu выступает результат вы- зова функции-членаmonthlyMenu объекта класса DateBookMenuCatalog. Этот вызов переводит выполнение программы к строке 475 листинга 10.10. Функция-член get класса auto_ptr возвращает указатель на соответствующий объект, но не меняет владельца этого объекта. Затем этот указатель на объект меню ежемесячника поме- щается функцией-членом главного меню в вершину стека, после того как пользова- тель выберет опцию открытия книги контактов. Класс MonthlyDateBookMenu После того как функция главного меню поместит указатель на объект MonthlyDateBookMenu в вершину стека меню, функция main вызовет функцию mainLoop для активного меню. Реализация функций-членов класса MonthlyDateBookMenu (это также фрагмент файла DateBookMenu.срр) показана в листинге 10.11. Поскольку файл источника DateBookMenu. срр получился доста- точно большим, мы и дальше будем рассматривать его по фрагментам, чтобы облег- чить нахождение нужных блоков программы. 187: void MonthlyDateBookMenu: :mainLoop () 188:{ 189: clearScreen(); 190: int year, month, day; 191: currDate_.getDate(year, month, day); 192: std::cout « ”*** Appointment Book ***\n" 193: « "Month of " « currDate_.monthName () « • ’ « year Класс MonthlyDateBookMenu 291
194: « "\n\n"; 195: 196: 197: 198: 199: 200: 201: 202: 203: 204: 205: 206: 207: 208: 209: 210: 211: 212: 213: 214: 215: displayMonth(); std::cout « ’\n’; const char menu[] = ’’(P)revious month, (N)ext month, (C)reate, (S)earch, \n" ”(R)edisplay, d(A)ily view, (W)eekly view, (G)oto date," "(Q)uit ?’’; const char choicest] = "PNCSRAWGQ”; switch (getMenuSelection(menu, choices)) { case ’P’: showPrevious(); break; case ’N’: showNextO; break; case ’C’: createEntry(); break; case ’S’: search(); break; case ’R’: /* Пустой цикл */ break; case ’A’: showDayO; break; case ’W’: showWeek(); break; case ’G’: gotoDateO; break; case ’Q’ : exitMenuO; break; 21-Ct 217: default: exitMenuO; break; } 218: } 219: 220:void 221: { 222: 223: } 224: 225:void 226: { 227: 228: 229: 230: 231: 232: 233: 234: 235: 236: 237: 238: 239: 240: 241: 242: 243: 244: 245: MonthlyDateBookMenu::reset() cacheGood_= false; MonthlyDateBookMenu::displayMonth() using namespace std::rel_ops; // Вычисляет начало текущего и следующего месяцев DateTime startOfMonth = currDate_.startOfMonth(); DateTime nextMonth = startOfMonth.addDay(31).startOfMonth(); int year, month, day; if (! cacheGood ) { // Очистка буфера ежемесячника for (int i = 0;i < 32; ++i) s cor eBoa rd__ [i] - false; // Итерация по всем записям текущего месяца DateBook::const_iterator iter = dateBook_.findAppointmentAtTime(startOfMonth); DateBook::const_iterator endlter = dateBook__. f indAppointmentAtTime (nextMonth) ; 246: // При обнаружении Тапией в’буфере ежемесячника // для соответствующего дня устанавливается значение true. 247: for ( ; iter != endlter; ++iter) 248: 249: { iter->startTime().getDate(year, month, day); 292 Глава 10. Сборка блоков программы
250: scoreBoard [day] = true; 251: } 252: 253: } 254: // Вычисляет дни первой недели месяца. 255: 256: int startWday = startOfMonth.dayOfWeek(); 257: // Вычисляет дни последней недели месяца 258: nextMonth.addDay(-1).getDate(year, month, day); 259: 260: int monthLen = day; 261: std:: cout « " 11; 262: 263: int wday = 0; 264: // Недостающие дни первой недели месяца отмечаются пробелами. 265: for ( ; wday < startWday; ++wday) 266: 267: std::cout « " "; 268: // Вывод на печать дней месяца 269: std::cout.fill(’ ’); 270; std::cout.setf(std::ios::dec, std::ios::basefield); 271: std::cout.setf(std::ios::right, std::ios::adjustfield); 272: for (int mday « 1; mday <= monthLen; ++mday) 273: { 274: if (wday ~ 0 && mday != 1) 275: std::cout « "\n”; 276: std::cout « std::setw(3) « mday 277: «(scoreBoard__ [mday ] ? ’ ’); 278: wday = (wday +1) % 7; 279: } 280: 281: } 282: std::cout « std::endl; 283:void 284: { MonthlyDateBookMenu::showPrevious() 285: // Вычитает один месяц 286: int year, month, day, hour, min; 287: currDate—.get(year, month, day, hour, min); 288: 289: } 290: setDate(DateTime(year, month - 1, day, hour, min)); 291:void 292: { MonthlyDateBookMenu::showNext() 293: // Прибавляет один месяц 294: int year, month, day, hour, min; 295: currDate—.get(year, month, day, hour, min); 296: 297: } 298: setDate(DateTime(year, month + 1, day, hour, min)); Функция mainLoop, которая начинается в строке 187, подобна функциям mainLoop всех других классов меню. В строках 192-194 выводится заголовок с на- званием месяца и годом, которые в данный момент выводятся на экран. При первом вызове переменная currDate_ содержит текущую дату, следовательно, на экране отображается текущий месяц. В строке 196 вызывается функция displayMonth, ко- торая, до того, как будет показана строка меню, выводит на экран дни месяца в виде календаря. Класс MonthlyDateBookMenu 293
Реализация функции displayMonth начинается в строке 225. В строке 227 мы вводим пространство имен std: : rel_ops в текущую область видимости. В резуль- тате станут доступны определения операторов ’=,>,<== и >= для классов DateTime и Appointment, в которых до сих пор были определены только операторы == и <. В строках 230, 231 с помощью функций-членов класса DateTime вычисляются пер- вый и последний дни текущего месяца. В строке 234 проверяется состояние буфера ежемесячника. Состояние будет иметь значение true, если данный месяц уже ото- бражался в этом сеансе работы с программой и с тех пор данные не изменялись. Если состояние буфера— false, то прежде всего буфер очищается от старых значений (строки 237-238), а затем запускается процесс итерации по всем записям контактов, назначенных на текущий месяц, с установкой в буфере флагов д ля тех дней, на кото- рые были назначены контакты. Для этого в строках 241-244 задается диапазон ите- раторов . Один итератор указывает на первый контакт в текущем месяце, а второй итератор указывает на первый контакт, назначенный на следующий месяц. Реализация класса DateBook Мы дошли до того момента, когда функция main вызывает функцию mainLoop для объекта активного меню, которым сейчас является объект класса MonthlyDateBookMenu. Функция MonthlyDateBookMenu: :mainLoop, в свою очередь, вызывает функцию MonthlyDateBookMenu: : displayMonth, которая для возвращения диапазона итераторов передает вызов на функцию DateBook: : findAppointmentAtTime. Поэтому оста- вим пока меню книги контактов и рассмотрим реализацию функции DateBook:: findAppointmentAtTime, а также других функций класса DateBook. Файл DateBook. срр полностью приведен в листинге 10.12. 1://TinyPIM (с)1999 Pablo Halpern. Файл DateBook.срр 2: 3:#ifdef _MSC_VER 4:#pragma warning(disable : 4786) 5:#endif 6: 7:#include <algorithm> 8:#include "DateBook.h" 9: 10:int DateBook: .-nextId_= 1 11: 12:DateBook::DateBook() 13: { 14:} 15: 16:DateBook::-DateBook() 17: { 18: } 19: 20:inf DateBook::insertAppointment(const Appointments appt, 21:int recordld)throw (Duplicateld) 22: { 23: if (recordld == 0) 24: // Если recordld не задан, генерируется новый ID. 294 Глава 10. Сборка блоков программы
25: recordld = nextld_++; 26: else if (recordld >= nextld_) 27: // Проверяет, чтобы nextId было больше идентификационных // номеров всех остальных записей. 28: nextld_ = recordld + 1; 29: else if (apptBy!d_.count(recordld)) 30: // recordld уже представлен в контейнере map 31: throw Duplicateld(); 32: 33: // Присваивает recordld копии записи контакта 34\ Appointment apptCopy(appt); 35: apptCopy.recordld(recordld); 36: 37: // Вводит запись в набор 38: apptByTime_t::iterator i = appointments_.insert(apptCopy); 39: 40: // Вводит итератор на объект записи контакта в контейнер тар 41: // apptBy!d__. insert (std: :make__pair (recordld, i) ) ; 42: apptBy!d_[recordld ] = i; 43: 44: return recordld; 45: } 46: 47- .DateBook: : apptByTime_t: : iterator 48:DateBook::getByld(int recordld) throw (appointmentNotFound) 49: { 50: // Поиск записи no ID. 51: apptById_t::iterator idlter = apptById_.find(recordld); 52: if (idlter == apptById_.end()) 53: throw appointmentNotFound(); 54: 55: return idlter->second; 56: } 57: 58:DateBook::apptByTime_t::const_iterator 59:DateBook::getByld(int recordld) const throw (appointmentNotFound) 60: { 61: // Поиск записи no ID. 62: apptById_t::const_iterator idlter = apptById__.find(recordld); 63: if (idlter == apptById__.end() ) 64: throw appointmentNotFound(); 65: 66: return idlter->second; 67: } 68: 69:void DateBook::eraseAppointment(int recordld) 70: throw (appointmentNotFound) 71: { 72: apptByTime_t::iterator i = getByld(recordld); 73: 74: // Удаление записи из обоих контейнеров 7 5: appointments_.erase(i); 7 6: apptById_.erase(recordld); 77: } 78:» 79:void DateBook::replaceAppointment(const Appointments appt, 80: int recordld) Класс MonthlyDateBookMenu 295
81: throw (appointmentNotFound) 82: { 83: if (recordld == 0) 84: recordld = appt.recordld(); 85: 86: eraseAppointment(recordld); 87: insertAppointment(appt, recordld); 88: } 89: 90:const Appointments DateBook::getAppointment(int recordld) const 91: throw (appointmentNotFound) 92: { 93: return *getBy!d(recordld); 94: } 95: 96:// Поиск первого контакта, время начала которого больше или 97:// равно заданному. 98 : DateBook: : const__iterator 99:DateBook::findAppointmentAtTime(const DateTimeS dt) const 100: { 101: Appointment searchAppt; 102: searchAppt.startTime(dt); 103: 104: return appointments_.lower_bound(searchAppt); 105: } 106: 107:// Класс объекта функции для поиска строк в записи контакта 108:class AppointmentContainsStr 109: : public std::unary_function<Appointment, bool> 110: { 111:public: 112: AppointmentContainsStr(const std::stringS str) : str_(str) { } 113: 114: bool operator()(const Appointments a) 115: { 116:// Возвращает true, если в полях контакта содержится строка str_ 117: return (a.description().find(str_) != std::string::проз); 118: } 119: 120:private: 121: std::string str_; 122: }; 123: 124:// Поиск первой записи контакта, в любом из полей которой содержится 125:// заданная строка. Начало поиска задается параметром start. 126:DateBook::const_iterator 127:DateBook::findNextContains(const std::stringS searchStr, 128:const_iterator start) const 129: { 130: return std: : find_if (start, appointments__. end () , 131: AppointmentContainsStr(searchStr)); 132:} 133: 134:// Возвращает итератор на запись по указанному ID. 135:DateBook::const_iterator 136:DateBook::findRecordld(int recordld) const 296 Глава 10. Сборка блоков программы
137: throw (appointmentNotFound) 138: { 139: return getByld(recordld); 140: } Класс DateBook построен так же, как и класс AddressBook. Посмотрим, напри- мер, на функцию insertAppointment. После того как в строках 23-35 будет вычис- лен идентификационный номер для новой записи, объект Appointment добавляется в контейнер appointments_, реализованный KaKmultiset<Appointment>. Функция insert сохраняет копию объекта Appointment и возвращает итератор на новый элемент в множественном наборе. Элементы множественного набора автоматически сортируются в том порядке, какой был определен для оператора < в классе Appointment. Поскольку оператор < просто сравнивает объекты по дате начала кон- такта, то и элементы набора сортируются в возрастающем порядке по назначенным датам начала, что существенно облегчает их поиск. В строке 42 выполняется вто- ричное индексирование нового элемента с помощью контейнера apptBy!d_, реали- зованного как map<int, apptByTime t: : iterator^ Контейнер map устанавливает связи между отсортированными идентификационными номерами записей и итера- торами, возвращаемыми при добавлении элемента в набор. С помощью объекта apptBy!d_ мы можем быстро осуществлять поиск записей по ID, а затем получать доступ к объектам записей путем разыменования связанных с ними итераторов. Как вы помните, такая же схема двойного индексирования применялась в классе AddressBook, но тогда поиск записей можно было выполнять по идентификацион- ным номерам и именам абонентов. Функция findAppointmentAtTime не нуждается в двойном индексировании за- писей. Она просто создает в строке 101 заготовку объекта Appointment и присваива- ет ему в строке 102 заданное время начала контакта. Поиск записей осуществляется с помощью функции lower_bound, которая характеризуется логарифмическим вре- менем выполнения. Эта функция возвращает итератор на первый элемент контей- нера appointments_ с датой начала не менее чем dt. Если такой элемент не найден, то функция возвращает значение appointments—. end (). Чуть позже мы еще вер- немся к классу DateBook. А сейчас продолжим рассмотрение последовательности операций при реализации функции MonthlyDateBookMenu: :displayMonth (см. листинг 10.11). Показ календаря для текущего месяца Функция f indAppointmentAtTime, которая вызывается в строках 242 и 244 (см. листинг 10.11), возвращает итератор iter, указывающий на первый кон- такт текущего месяца, и итератор endlter, указывающий на первый контакт следующего месяца. Таким образом, мы получаем диапазон итераторов [iter, endlter), включающий все записи контактов текущего месяца. Цикл итерации по всем этим контактам запускается в строке 247. По заданному усло- вию цикл прекращается, как только итератор iter будет приращен до значения endlter. В строке 249 из даты начала контакта извлекаются значения года, ме- сяца и дня. В следующей строке значение дня (day) используется как индекс кон- тейнера scoreboard_ для установки флага в буфере ежемесячника. Если на этот день было установлено несколько контактов, то ничего страшного не произой- дет. Просто одному и тому же элементу scoreboard- значение true будет при- своено несколько раз. Класс MonthlyDateBookMenu 297
Таким образом; в результате выполнения цикла итерации по всем записям диапазона будет инициализирован буфер ежемесячника scoreboard . Теперь нам осталось вывести на печать дни текущего месяца с символом звездочки око- ло тех дней, на которые были назначены контакты. Другими словами, звездоч- ками будут отмечены те дни, которым соответствуют элементы массива scoreboard_ со значениями true. В строке 225 определяется день недели, с ко- торого начинается текущий месяц. Затем, в строке 258, вычисляется последний день месяца путем вычитания единицы от первого дня следующего месяца. В строках 265, 266 выводятся пробелы для тех дней первой недели, которые приходятся на предыдущий месяц (начиная с воскресенья). Прежде чем присту- пить к выведению дней текущего месяца, нужно сделать некоторые установки форматирования. В строках 269-271 в качестве символа заполнения устанавли- вается пробел, определяется вывод чисел в десятичном формате и выравнивание вправо. Эти три инструкции можно было бы заменить следующей строкой с ис- пользованием манипуляторов форматирования ввода-вывода: std::cout « std::setfill(’ ’) « std::dec « std::right; В строке 272 запускается цикл по дням месяца. Строки 274, 275 управляют выводом символов разрыва строки и пробелов, чтобы получить вид календаря, в котором для каждой недели выделяется своя строка. В строке 276 в 3-сим- вольном поле выводится число месяца с выравниванием вправо. В стро- ке 277 выводится звездочка в том случае, если соответствующему элементу scoreboard_ присвоено значение true, или пробел— если false. В стро- ке 278 обновляется переменная wday, которая определяет, какой день недели должен печататься следующим. От функции displayMonth вернемся к функции MonthlyDateBookMenu: : mainLoop, где в строке 205 (мы продолжаем рассматривать листинг 10.11) выводится приглаше- ние меню и с помощью функции базового класса Menu: : getMenuSelection обрабаты- вается выбор опции пользователем. К моменту вызова функции getMenuSelection на экране отображается, например, такая информация: ***Appointment Book *** Month of September 1999 1* 2* 3 4 5 6* 7* 8* 9 10 11* 12 13 14 15* 16 17* 18* 19* 20* 21 22 23* 24 25* 26* 27 28* 29 30* (P)revious month, (N)ext month, (C)reate, (S)earch, (R)edisplay, d(A)ily view, (W)eekly view, (G)oto date, (Q)uit ? Предположим, что пользователь ввел N для просмотра календаря на следую- щий месяц. Тогда в строке 208 будет вызвана функция showNext. В стро- ке 295 в теле функции showNext мы извлекаем компоненты текущих даты и вре- мени, а в строке 296 вновь объединяем эти компоненты, но значение месяца приращиваем на единицу. Конструктор DateTime автоматически нормализиру- ет полученные значения, если, например, номеру месяца будет присвоено зна- чение 13. Окончательное значение даты передается в функцию set Date, которая является функцией-членом базового класса DateBookMenu и используется для установки текущей (отображаемой в данный момент на экране) даты. Функция showNext возвращается в mainLoop, которая, в свою очередь, возвращается в main, после чего в функции main совершается новый цикл обращения к функ- ции mainLoop для показа календаря следующего месяца. 298 Глава 10. Сборка блоков программы
Переходы между видами Если пользователь в ответ на предложение меню введет букву W, будет вызвана функция showWee к, которая принадлежит базовому классу DateBookMenu. Рассмот- рим в листинге 10.13 еще один фрагмент файла DateBookMenu.срр с реализацией функций класса DateBookMenu. 1://TinyPIM (с)1999 Pablo Halpern. Файл DateBookMenu.срр 2: 3:#ifdef _MSC_VER 4: #pragma warning(disable : 4786) 5:#pragma warning(disable : 4355) 6:#endif 7: 8:#include <iostream> 9:#include <iomanip> 10:#include <sstream> ll:#include <climits> 12:#include "DateBookMenu.h" 13:#include "Appointment.h" 14:#include "DateBook.h" 15:#include "AppointmentEditor.h" 16: 17:void DateBookMenu::setDate(DateTime dt) 18: { 19: currDate_= dt; 20: reset(); 21:} 22: 23:void DateBookMenu::reset() 24: { 25: } 26: 27:void DateBookMenu::createEntry() 28: { 29: // Редактирование пустой записи контакта 30: Appointment appt; 31: appt. startTime (currDate__) ; 32: appt. endTime (currDate_) ; 33: AppointmentEditor editor(appt); 34: 35: // Продолжение редактирования до тех пор, // пока запись будет сохранена или отменена. 36: while (editor.edit()) 37: { 38: appt = editor.appt(); 39: if (appt.description().empty()) 40: { 41: std::cout « "Description must not be empty.” . « std::endl; 42: continue; // Повторение цикла редактирования 43: } 44: 45: // Добавление записи в контейнер. 46: dateBook_.insertAppointment(appt); Класс MonthlyDateBookMenu 299
47: 48:- 49: 50: 51:} 52: 53: void 54: { 55: 56: 57: 58: 59:} 60: 61: void 62: { 63: 64: 65: 66: 67:} 68: 69: void 70: { 71: 72: 73: 74: 75:} 76: 77:void 78: { 79; 80: 81: 82: 83: 84: 85: 86: 87: 88: 89: 90: 91: 92: } 93: 94:void 95: { 96: 97: 98: 99: 100: 101: 102: 103: 104: 105: setDate(appt.startTime()); reset(); break; } // Конец цикла while DateBookMenu::showDay() // Переход к виду ежедневника для текущей даты exitMenu(); catalog_->dailyMenu()->setDate(currDate_); enterMenu(catalog_->dailyMenu()); DateBookMenu::showWeek() // Переход к виду еженедельника для текущей даты exitMenu(); catalog_->weeklyMenu () ->setDate (currDate__) ; enterMenu (cat alog__->weeklyMenu ()); DateBookMenu::showMonth() // Переход к виду ежемесячника для текущей даты exitMenu(); catalog_->monthlyMenu()->setDate(currDate_); enterMenu(catalog_->monthlyMenu()); DateBookMenu::search() std::string searchstring; std::cout « ’’Search for string: ”; std::getline(std::cin, searchstring); // Прервать поиск, если строка поиска не была задана. if (std::cin.fail() || searchstring.empty()) return; // Установка строки поиска и даты exitMenu(); catalog__->stringSearchMenu()->searchString(searchstring) ; catalog__->stringSearchMenu()->setDate(currDateJ ; enterMenu(catalog_->stringSearchMenu()); DateBookMenu::gotoDate() std::string datestring; while (std::cin.good()) { std::cout « ’’Goto date [” « currDate_.dateStr () « ”] : ”; std::getline(std::cin, datestring); if (datestring.empty()) break; else if (datestring[0] == ’t’ || datestring[0] == ’T’) { u // Пользователь ввел ’’today” 300 Глава 10. Сборка блоков программы •
106: setDate(DateTime::now()); 107: break; 108: } 109: 110: // Установка значений переменных 111: DateTime newdate; 112: if (! newdate.dateStr(dateString)) 113: std::cout « "Invalid date, please try again\n"; 114: else 115: { 116: setDate(newdate); 117: break; 118: } // Конец блока if 119: } // Конец цикла while 120: } 121: 122: 123: В строке 64 функция showWeeк закрывает текущее меню. В нашем примере это было меню вида ежемесячника. Поскольку текущее меню (ежемесячника) и новое меню (еженедельника) содержат разные экземпляры переменной-члена currDate_, нам необходимо установить текущую дату для меню еженедельника, что и выполня- ется в строке 57. И наконец, меню еженедельника становится активным благодаря вызову функции enterMenu в строке 58. Поскольку функция showWeeк была реализована в базовом классе, она в таком виде становится доступной д ля всех производных классов. Так, в результате выбора пользо- вателем опции W в любом меню будет вызываться одна и та же функция showWeek. Та- ким же образом выполняются функции showDay (строки 53-59) и showMonth (строки 69-75) при выборе опций А и М соответственно. В строке 77 начинается функ- ция search, которая тоже выполняется аналогичным образом, за тем исключением, что сначала на экран пользователю выводится предложение ввести ключевые слова для поиска (строка 80), которые считываются программой в следующей строке с помо- щью функции getline из файла заголовка <string>. Вспомните, что функция get line считывает всю строку, включая промежуточные пробелы. Если во время вво- да данных происходит сбой или пользователь ввел пустую строку, функция завершает- ся в строках 84, 85 без каких-либо результатов. В случае успешного ввода ключевых слов текущее меню закрывается в строке 88 и в строках 89, 90 объект меню режима поиска инициализируется строкой поиска и текущей датой. В строке 91 меню поиска становится активным за счет передачи соответствующего указателя в вершину стека. Экранные списки книги контактов Все три концевых класса dailyDateBookMenu, WeeklyDateBookMenu и StringSearchDateBookMenu производятся от общего базового класса ListBasedDateBookMenu (см., рис. 10.1). Класс ListBasedDateBookMenu содержит объект класса AppointmentDisplayList, который является функциональным ядром для всех этих классов меню. Класс AppointmentDisplayList наследуется от базово- го класса DisplayList, который мы рассматривали в главе 7. В классах, производ- ных от DisplayList, должны быть определены частные функции возвращения за- писей и формирования текста резюме для всех записей. Определение класса AppointmentDisplayList показано в листинге 10.14. Экранные списки книги контактов 301
1://TinyPIM (с)1999 Pablo Halpern. Файл AppointmentDisplayList.h 2: 3:#ifndef AppointmentDisplayList_dot_h 4:#define AppointmentDisplayList_dot_h 1 5: 6:#include "DisplayList.h" 7:#include "DateTime.h" 8: 9:class DateBook; 10: 11:// Специализированный экранный список записей книги контактов. 12:class AppointmentDisplayList : public DisplayList 13: { 14:public: 15: // Конструктор принимает ссылку на книгу контактов 16: AppointmentDisplayList(DateBook& dateBook); 17: 18: DateTime currDateO const {return currDate_; } 19: void currDate(DateTime dt); 20: 21: // Список контактов на день, заданный переменной DateTime 22: void listDay(DateTime dt); 23: 24: // Список контактов на неделю, заданную переменной DateTime 25: void listWeek(DateTime dt); 26: 27: // Список записей контактов, содержащих указанную строку 28: void listContainsString(const std::strings); 29: 30:protected: 31: // Отображение выбранной записи контакта в одну строку. 32: // Реализация чисто виртуальной функции базового класса. 33: virtual void displayRecord(int recordld); 34: 35: // Замещение функции базового класса fetchMore. 36: virtual bool fetchMore(int startld, int numRecords, 37: std::vector<int>& result); 38: 39:private: 40: DateBookSdateBook_; 41: 42: // Перечисление экранных списков различных режимов 43: enum listMode {dayMode, weekMode, stringMode}; 44: listMode mode_; 45: 46: // Объект DateTime используется в ежедневнике и еженедельнике 47: DateTime currDate ; 48: 49: // Объект string используется в режиме поиска. 50: std::string containsString_; 51:}; 52: 53:#endif // AppointmentDisplayList dot h Функции listDay, listWeek и listContainsString используются для создания экранного списка в одном из трех режимов, определенных в строке 43 в перечисле- 302 Глава 10. Сборка блоков программы
нии listMode. Виртуальная функция базового класса displayRecord замещается в производном классе и используется для создания резюме записей контактов, ото- бражаемых в экранном списке. Еще одна виртуальная функция базового класса fetchMore замещается в строках 36, 37. Она вызывается для возвращения новой порции записей из исходного набора. Реализация функций класса AppointmentDisplayList показано в листинге 10.15. ь Листинг 10.15. Реализациям v j 1://TinyPIM (c)1999 Pablo Halpern. Файл AppointmentDisplayList.cpp 2: 3:#ifdef _MSC_VER 4:#pragma warning(disable : 4786) 5:#endif 6: 7:#include <iostream> 8:#include <iomanip> 9:#include <algorithm> 10:#include "AppointmentDisplayList.h" 11:# include "Appo intment.h" 12:#include "DateBook.h" 13: 14:#ifdef _MSC_VER 15:#define min _cpp_min 16:#define max __cpp__max 17:#endif 18: 19:// Конструктор принимает ссылку на объект книги контактов 20:AppointmentDisplayList::AppointmentDisplayList(DateBookS apptBook) 21: : dateBook_ (apptBook), mode_ (dayMode) t currDate_ (DateTime: : now () ) 22: { 23: } 24: 25:// Показ выбранной записи контакта в одной строке 26:void AppointmentDisplayList::displayRecord(int recordld) 27: { 28: if (recordld < 0) 29: { 30: // Отрицательный recordld представляет собой специальный 31: // маркер даты в диапазоне от -1 (воскресенье) // до -7 (суббота). 32: // Вывод маркера даты. 33: DateTime startOfWeek = currDate_.addDay (-currDate_.dayOfWeek()); 34: DateTime marker = startOfWeek.addDay(-recordld - 1); 35: std::cout « marker.wdayName() « ’ ’ « marker.dateStr(); 36: • return; 37: } 38: 39: Appointment record = dateBook_.getAppointment(recordld); 40: 41: // Вывод префикса записи. 42: switch (mode_) 43: { Экранные списки книги контактов 303
44: case dayMode: 45: break; 46: case weekMode: 47: // В режиме еженедельника все записи выводятся с отступом 48: std::cout « " 49: break; 50: case stringMode: 51: //В режиме поиска выводится дата контакта. 52: std::cout « record.startTime().dateStr() « " "; 53: break; 54: } 55: 56: // Форматирование времени начала и окончания контакта 57: std::cout « record.startTime().timeStr() 58: « " - " « record.endTime().timeStr(); 59: 60: // Вывод описания контакта до первого символа разрыва строки, 61: // но не более 45-ти символов. 62: int outlen = std::min(45, (int) record.description().find(’\n’)); 63: std::cout « " " « record.description().substr(0, outlen); 64: } 65: 66:// Возвращение новых записей из книги контактов 67:bool AppointmentDisplayList::fetchMore(int startld, int numRecords, 68: std::vector<int> &result) 69: { 70: // Очистка старого экранного списка 71: result.clear(); 72: 73: if (numRecords == 0) 74: return false; 75: 76: bool forwards « true; 77: if (numRecords < 0) 78: { 79: forwards = false; 80: numRecords =- numRecords; 81: } 82: 83: // Проверка наличия в списке записей 84: if (dateBook_.begin() == dateBook_.end()) 85: return true; 86: 87: // Объявление итератора 88: DateBook::const-iterator iter; 89: 90: // Получаем итератор на запись, указанную в startld. 91: // При прокручивании списка вперед итератор приращивается, 92: // чтобы избежать появления дубликатов в экранном списке. 93: if (startld == 0) 94: iter =(forwards ? dateBook—.begin():dateBook—.end()); 95: else 96: { 97: iter « dateBook—.findRecordld(startld); 98: if (forwards) 99: ++iter; 100: } 304 Глава 10. Сборка блоков программы
101: 102: 103: 104: 105: 106: 107: 108: 109: 110: 111: 112: 113: 114: 115: 116: 117: 118: 119: 120: 121: 122: 123: 124: 125: 126: 127: 128: 129: 130: 131: 132: 133: 134: 135: 136: 137: 138: 139: 140: 141: 142: 143: 144: 145: 146: 147: 148: 14 9: - 150: 151: if (mode_ != stringMode) { // Режимы еженедельника или ежедневника DateTime firstDate, lastDate; if (mode_ == dayMode) { // Установка диапазона времени с полуночи текущего // дня до полуночи следующего firstDate = currDate_.startOfDayО; lastDate = firstDate.addDay(); } else { // Установка диапазона времени с полуночи прошлого // воскресенья до полуночи следующего. firstDate = currDate_.startOfDay(); firstDate = firstDate.addDay(-firstDate.dayOfWeekO); lastDate = firstDate.addDay(7); } // Заполнение буфера экранного списка происходит // за один прием, // поэтому работа начинается с очистки буфера. // В действительности к моменту выполнения // следующих инструкций // буфер обычно уже бывает пустым, // за исключением случая, когда ... if (startld != 0) reset (); DateBook::const_iterator startiter = dateBook_.findAppointmentAtTime(firstDate); DateBook: : const__iterator endlter = dateBook_.findAppointmentAtTime(lastDate); // Возвращение записей начиная с указанной iter (неважно, // прокручиваем ли мы список вперед или назад, // так как всегда // возвращаются все записи, удовлетворяющие условию). DateTime prevDate; for (iter = startiter; iter != endlter; ++iter) { using namespace std::rel_ops; // Для получения доступа // к оператору != if (mode__== weekMode && iter->startTime (). startOfDay () !== prevDate) { // Начало нового дня. // Дни недели задаются отрицательными числами // в диапазоне от -1 (воскресенье) до -7 (суббота) result.push_back(-iter->startTime().dayOfWeek() - 1); prevDate = iter->startTime().startOfDay(); } result.push_back(iter->recordld()); } Экранные списки книги контактов 305
152: 153: // Возвращает true, поскольку достигнут конец списка return true; 154: } 155: else 156: { 157: 158: // Режим поиска строк 159: if (forwards) 160: { 161: 162: // Возвращает запись ПОСЛЕ startld 163: // Поиск совпадающих строк начиная с iter 164: iter = dateBook__. findNextContains (containsString_ iter)j 165: while (iter != dateBook ,end() && numRecords— > 0) 166: { 167: 168: result.push—back(iter->recordld()); 169: // Поиск следующей совпадающей записи 170: iter = dateBook .findNextContains(containsString , ++iter); 171: 172: } 173: // Возвращает true при достижении конца списка 174: return iter == dateBook ,end(); 175: } 176: else 177: { 178: 179: // Возвращение записей ПЕРЕД startld 180: // Класс DateBook не имеет функций поиска // в обратном направлении. 181: // Вместо этого мы возвращаем ВСЕ записи перед iter 182; DateBook::const—iterator endlter = iter; 183: iter = dateBook—.findNextContains(containsString_, 184: dateBook—.begin()); 185: while (iter != endlter) 186: { 187: result.push—back(iter->recordld()) ; 188: iter = dateBook .findNextContains(containsString , ++iter); 189: 190: } 191: return true; // Мы всегда достигаем начала списка. 192: } 193: 194: } 195: } 196:// Установка текущей даты 197:void 198: { AppointmentDisplayList::currDate(DateTime dt) 199: if (dt == currDate—) 200: 201: return; 202: currDate_ = dt; 203: 204: reset(); 205: 206: } // Следующий вызов display() обновляет буфер 306 Глава 10. Сборка блоков программы
207: 208:// Список контактов на день, указанный в DateTime 209:void AppointmentDisplayList::listDay(DateTime dt) 210: { 211: if (mode_== dayMode && currDate_ == dt) 212: return; 213: 214 :mode_ = dayMode; 215: currDate(dt); 216: } 217: 218:// Список контактов на неделю, указанную в DateTime 219:void AppointmentDisplayList::listWeek(DateTime dt) 220: { 221: if (mode_ == weekMode && currDate_ == dt) 222: return; 223: 224: mode_ = weekMode; 225: currDate(dt); 226: } 227: 228:// Список всех контактов, содержащих заданную строку 229:void AppointmentDisplayList::listContainsString(const std::string& s) 230: { 231: if (mode_ == stringMode && containsString_ == s) 232: return; 233: 234: containsString_ = s; 235:mode_ = stringMode; 236: reset(); 237: 238: // Следующий вызов display() обновит буфер 239: } После вызова функции mainLoop для одного из классов, произведенных от ListBasedDateBookMenu, сначала происходит вызов функции displayList_. display (). Переменная-член displayList_—это объект класса AppointmentDisplayList. Код реализации функции display был задан в базо- вом классе DisplayList и состоит в вызове функции fetchMore для возвраще- ния порции записей для экранного списка из исходного набора. Стро- ки 71-100 функции fetchMore служат для выполнения основных установок. Они практически такие же, как и в классе AddressDisplayList, который мы рассматривали в главе 7. В строке 102 выполнение программы разветвляется в зависимости от выбранно- го режима. В случае установки в переменной mode_ режимов dayMode или weekMode выполняется строка 105, в которой объявляются переменные начала и конца вре- менного диапазона отображения записей. В режиме ежедневника в строках 110, 111 переменной firstDate присваивается значение полуночи текущего дня, а перемен- ной lastDate— значение полуночи следующего дня. Если выбран режим ежене- дельника, то в строке 118 вычисляется начало недели путем вычитания от текущей даты значения дня недели. Так, если текущий день недели (переменная currDate_) — вторник, то от текущей даты отнимается 2, чтобы возвратить дату по- следнего воскресенья. Затем к полученному значению в строке 119 прибавляется 7, чтобы получить дату следующего воскресенья. Экранные списки книги контактов 307
Функция fetchMore должна возвращать столько записей, сколько необходи- мо для заполнения экрана. Но вряд ли на неделю будет назначено больше кон- тактов, чем может уместиться на экране (больше 50-ти), поэтому мы просто воз- вращаем все записи, попадающие в установленный диапазон. Итераторы, полу- ченные в строках 129-132, образуют диапазон итераторов [startiter, endlter), который определяет набор записей для показа. Затем используется традиционная идиоматическая конструкция с циклом for, выпол- няющим итерацию по всем записям диапазона и добавляющим ID записей в ко- нечный вектор с помощью функции push back (строка 149). Но в режиме ежене- дельника нам еще необходимо отследить переходы между днями недели. Для это- го перед первой записью контакта каждого дня мы добавляем специальный маркер в виде “ложного” номера ID. Ложные номера ID отсчитывают дни недели отрицательными цифрами от-1 (воскресенье) до-7 (суббота). Границы между днями определяются с помощью инструкций в строках 141, 142. В стро- ке 146 в вектор добавляется “ложный” ID, а в строке 147 обновляется перемен- ная prevDate, которая затем используется для отслеживания переходов между днями недели. В конечном итоге в векторе буфера сохраняется смесь значений из действительных ID и специальных маркеров. Если в переменной mode_ установлен режим поиска записей по ключевым словам (stringMode), нам нужно выбрать из набора те записи, которые содержат значение contains St ring_. В зависимости от того, будут ли выбираться записи после или до исходной позиции, будут выполняться соответственно циклы в строках 165-171 или в строках 185-189. В обоих случаях вызывается функция f indNextContains класса dateBook, в которую передается ранее полученный итератор исходной позиции по- иска. Функция f indNextContains и класс Appointment Contains St г из листин- га 10.12 повторены ниже. 107:// Класс объекта функции для поиска строк в записи контакта 108:class AppointmentContainsStr 109: : public std::unary_function<Appointment, bool> 110: { 111:public: 112: AppointmentContainsStr(const std::strings str) : str_(str) { } 113: 114: bool operator()(const Appointments a) 115: { 116: // Возвращает true, если в полях контакта содержится // строка str- 117: return (a.description(),find(str_) != std::string::npos); 118: } 119: 120:private: 121: std::string str_; 122: }; 123: 124:// Поиск первой записи контакта, в любом из полей которой содержится 125:// заданная строка. Начало поиска задается параметром start. 126:DateBook::const—iterator 127:DateBook::findNextContains(const std::strings searchStr, 128:const—iterator start) const 129: { 130: return std::find_if(start, appointments_.end(), 308 Глава 10. Сборка блоков программы
131: AppointmentContainsStr(searchStr)); 132: } AppointmentContainsStr — это класс объекта функции, в конструктор которого передается строка поиска. Оператор вызова функции (operator О) этого класса, представленный в строках 114-118, принимает строку поиска str_ с аргументом а типа string и использует для поиска функцию-член find класса string. Если иско- мая строка будет обнаружена, то функция find возвратит значение, отличное от проз, в результате чего оператор вызова функции возвратит true. (Функция find и другие функции-члены стандартного класса string подробно рассматривались в главе 5.) В строках 130, 131 функция findNextContains использует стандартный алго- ритм f ind if для поиска искомой строки в полях записи книги контактов. Для этого создается объект функции AppointmentContainsStr, в конструктор которого пере- дается искомая строка. Затем объект функции используется как предикат для тести- рования всех объектов, заключенных в диапазоне итераторов [start, appointments—end () ). Функция findNextContains возвращает итератор на первый объект Appointment в установленном диапазоне, для которого предикат возвратит true. Это свидетельствует о том, что в полях данного объекта содержится искомая строка. (Подробно алгоритм f ind if мы рассматривали в главе 6.) Давайте теперь освежим в памяти, на каком этапе выполнения программы мы сейчас находимся. Функция main вызывает функцию mainLoop для текущего актив- ного меню. Если активным является одно из меню, произведенное от класса ListBasedDateBookMenu, то функция mainLoop вызовет функцию display для объ- екта AppointmentDisplayList. Функция display заполняет буфер экранного спи- ска значениями ID соответствующих записей с помощью функции fillCacheFwd, которая, в свою очередь, вызывает функцию fetchMore. В зависимости от выбран- ного режима, функция fetchMore возвращает ID записей либо с помощью функции findAppointmentAtTime, либо с помощью findNextContains (обе принадлежат классу DateBook). После выполнения функция fetchMore возвращает вектор иден- тификационных номеров в функцию f illCacheFwd. Затем функция с помощью ал- горитма сору и функции back_inserter копирует вектор в буфер экранного списка, как показано в следующем фрагменте, взятом из листинга 7.2. 64: std::copy(moreRecords.begin(),moreRecords.end(), 65: std::back_inserter(cache_) ) ; После завершения функции f illCacheFwd программа возвращается к функции display, в которой выполняется цикл итерации по всем выбранным записям с вы- зовом для каждой функции displayRecord. Теперь пришло время возвратиться к листингу 10.15. В строке 28 проверяются значения специальных маркеров, с помощью которых отмечаются переходы между днями. Простые арифметические операции над датами выполняются в строках 33, 34 с целью выяснить, между какими днями недели про- ходит граница, заданная маркером. Дата и название дня недели выводятся на пе- чать в строке 35. Эта информация служит в режиме еженедельника заголовком для списка контактов, назначенных на определенный день. Если следующий элемент вектора не является специальным маркером, програм- ма переходит к выполнению строки 39, в которой по заданному ID соответствующая запись возвращается из объекта DateBook с помощью функции get Appointment. В строках 42-54 на печать выводятся префиксы строк, которые различаются от ре- жима к режиму и могут отсутствовать (ежедневник), содержать пробелы для отступа (еженедельник) или включать даты контактов (режим поиска). В строках 57, 58 вы- Экранные списки книги контактов 309
водятся даты начала и конца контакта. Следующий шаг — вывод одной строки опи- сания контакта. С этой целью в строке 62 используются функции-члены класса string для извлечения первых 45-ти символов описания или всего текста до первого символа разрыва строки, если в нем содержится меньше 45 символов. Извлечение выбранной части описания осуществляется в строке 63 с помощью функции substr. Классы, производные ОТ ListBasedDateBookMenu Теперь предположим, что пользователь в ответ на предложение главного меню ввел D, чтобы открыть книгу контактов в виде ежемесячника, а затем выбрал A, W или S. чтобы перейти к ежедневнигу, еженедельнику или режиму поиска. На экране появится информация вроде следующей (пример экрана еженедельника): *** Appointment Book *** Week of 10/10/99 to 10/16/99 ===============Start of list =============== l:Monday 10/11/99 2:09:45AM - 12:45PM Status meeting with Betty 3:09:45PM - 01:45AM Raquetball with Barbara 4:Wednesday 10/13/99 5:07:15PM - 11:15PM Golf with George 6:Friday 10/15/99 7:06:15PM - 08:15PM Piano Lesson with Betty ===============End of list =============== (P)revious week, (N)ext week, scroll (B)ackward, scroll (F)orward, (V)iew, (C)reate, (D)elete, (E)dit, (S)earch, (R)edisplay, d(A)ily view, (M)onthly view, (G)oto date, (Q)uit ? Программа в этот момент вернулась к вызову функции mainLoop для объекта DailyDateBookMenu, WeeklyDateBookMenu или StringSearchDateBookMenu. В лис- тинге 10.16 показана реализация функции mainLoop и других функций-членов классов, основанных на показе списков записей. DateBookMenuCatalog* catalog) :ListBasedDateBookMenu(datebook, catalog) displayList—. listWeek (DateTime: :now() ) ; ^йбтинг Жф Фикция mainLdpp r‘Л | у книгиконта!аов,основанных напбказеспйсков -1 299:WeeklyDateBookMenu::WeeklyDateBookMenu(DateBook& datebook, 300: 301: 302: { 303: 304: } 305: 306 -.void 307: { 308: 309: 310: 311: 312: 313: WeeklyDateBookMenu::mainLoop() clearScreen(); std::cout « "*** Appointment Book ***\n" « "Week of " « currDate_.startOfWeek().dateStr() « " to " « currDate_.startOfWeek().addDay(6).dateStr() 310 Глава 10. Сборка блоков программы
314: 315: 316: 317: displayList—.display(); std::cout « ’\n’; 318: const char menu[] = "(P)revious week, (N)ext week, \n’’ 319: ’’scroll (B) ackward, scroll (F)orward, (V) iew, \n" 320: ”(C)reate, (D)elete, (E)dit, (S)earch, (R)edisplay,\n” 321: "d(A)ily view, (M)onthly view, (G)oto date, (Q)uit ?”; 322: const char choicest] = ’’PNBFVCDESRAMGQ”; 323: 324: switch (getMenuSelection(menu, choices)) 325: { 326: case ’P’ : showPrevious(); break; 327: case ’N’ : showNext(); break; 328: case ’B’ : displayList_.pageUp(); break; 329: case ’F’ : displayList—.PageDown(); break; 330: case ’V’ : viewEntryO; break; 331: case ’C’ : createEntry(); break; 332: case ’D’ : deleteEntry(); break; 333: case •E’ : editEntry(); break; 334: case ’S’ : search(); break; 335: case ’R' : /* Пустой цикл */ break; 336: case ’A’ : showDay(); break; 337: case ’M’ : showMonth(); break; 338: case ’G’ : gotoDateO; break; 339: case 'Q' : exitMenu(); break; 340: default: exitMenu(); break; 341: } 342: } 343: 344:void WeeklyDateBookMenu::showPrevious() 345: { 346: setDate(currDate_.addDay(-7)); 347: } 348: 349:void WeeklyDateBookMfenu::showNext() 350: { 351: setDate(currDate_.addDay(7)); 352: } 353: 354:DailyDateBookMenu::DailyDateBookMenu(DateBook& datebook, 355: 356: 357: { 358: 359: } 360: 361:void 362: { 363: 364: 365: 366: 367: 368: 369: 370: DateBookMenuCatalog* catalog) : ListBasedDateBookMenu(datebook, catalog) displayList^.listDay(DateTime::now()); DailyDateBookMenu::mainLoop() clearScreen(); std:: cout « '•*** Appointment Book ***\n” « currDate .wdayNameO « ’ ’ « currDate . dateStr () « ”\n\n"; displayList_.display(); std::cout « ’\n’; 371: const char menu[] = ’’(P)revious day, (N)ext day, \n” Классы, производные от ListBasedDateBookMenu 311
372: "scroll (В)ackward, scroll (F)orward, (V)iew,\n" 373: "(C) reate, (D)elete, (E)dit, (S)earch, (R)edisplay,\n" 374: "(W)eekly view, (M)onthly view, (G)oto date, (Q)uit ?"; 375: const char choices[] = "PNBFVCDESRWMGQ"; 376: 377: switch(getMenuSelection(menu, choices)) 378: { 379: case .p. : showPrevious(); break; 380: case 'N* : showNext(); break; 381: case ’B1 : displayList_.pageUp(); break; 382: case ’F1 : displayList_.PageDown(); break; 383: case ’V1 : viewEntry(); break; 384: case ’C1 : createEntry(); break; 385: case ’D' : deleteEntry(); break; 386: case ’E’ : editEntryO; break; 387: case ’S’ : search(); break; 388: case ’R’ : /* Пустой цикл */ break; 389: case »w» : showWeek(); break; 390: case ’M1 : showMonth(); break; 391: case ’G’ : gotoDate(); break; 392: case ’Q1 : exitMenu(); break; 393: default: exitMenu(); break; 394: } 395: } 396: 397:void DailyDateBookMenu::showPrevious() 398: { 399: setDate(currDate_.addDay(-1)); 400: } 401: 402:void DailyDateBookMenu::showNext() 403: { 404: setDate(currDate_.addDay(1)); 405: } 406: 407:StringSearchDateBookMenu::StringSearchDateBookMenu(DateBookS db, 408: DateBookMenuCatalog *catalog) 409: : ListBasedDateBookMenu(db, catalog) 410: { 411: displayList_.listContainsString(""); 412: } 413: 414:void StringSearchDateBookMenu::mainLoop() 415: { 416: clearScreen(); 417: std::cout « "*** Appointment Book ***\n" 418: « "Records matching \"" « searchString_« "\"\n\n"; 419: 420: displayList_.display(); 421: std::cout « 1 Xn1; 422: 423: const char menu[] = 424: "scroll (B)ackward, scroll (F)orward, (V)iew,\n" 425: "(C) reate, (D)elete,’ (E)dit, (S) earch, (R) edisplay, \n" 426: "d(A)ily view, (W)eekly view, (M)onthly view, (G)oto date," 427: "(Q)uit ?"; 428: const char choicest] = "BFVCDESRAWMGQ"; 429: 312 Глава 10. Сборка блоков программы
430: switch (getMenuSelection(menu, choices)) 431: { 432: case ’B’: displayList__.pageUp () ; break; 433: case ’F1: displayList_.pageDown(); break; 434: case ’V* : viewEntryO; break; 435: case ’ C1: createEntry(); break; 436: case 1D1: deleteEntry(); break; 437: case 1E1: editEntry(); break; 438: case ’S’: search(); break; 439: case ’R’: /* Пустой цикл */ break; 440: case ’A1: showDayO; break; 441: case ’W1: showWeek(); break; 442: case ’M’ : showMonthO; break; 443: case 1G* : gotoDate () ; showDayO; break; 444: case 1Q1: exitMenu(); break; 445: default: exitMenuO; break; 446: 447: } 448: } 449:void 450: { StringSearchDateBookMenu::searchstring(const std::string& s) 451: searchString_ = s; 452: 453: } 454: displayList_.listContainsString(s); Реализация всех трех производных классов практически одинаковы и отли- чаются в основном только наборами опций меню. Но во всех меню есть опции создания записей книги контактов и выбора их для просмотра, удаления и ре- дактирования. Для реализации этих опций задействуются функция createEntry, определенная в базовом классе всех меню книги контактов dateBookMenu, и функции viewEntry, deleteEntry и editEntry, определенные в базовом классе меню со списками записей,— ListBasedDateBookMenu (листинг 10.17). Листинг 10.17. Реализация функций-членовкласса - ListBasedDateBooldfenuffl^Sgl 27:void DateBookMenu::createEntry() 28: { 29: 30: 31: 32: 33: 34: 35: // Редактирование пустой записи контакта Appointment appt; appt.startTime(currDate_); appt. endTime (currDate__) ; AppointmentEditor editor(appt); // Продолжение редактирования, пока запись //не будет сохранена или отменена. 36: while (editor.edit()) 37: 38: 39: 40: 41: { appt = editor.appt(); if (appt.description().empty()) { std::cout « "Description must not be empty." << std::endl; 42: continue; // Повторение цикла редактирования Классы, производные от ListBasedDateBookMenu 313
43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 124: 125: { 126: 127: 128: } 129: } // Добавление записи. dateBook_.insertAppointment(appt); setDate(appt.startTime()); reset (); break; } // Конец цикла while } ...// Другие функции уже были показаны в листинге 10.13 void ListBasedDateBookMenu::reset() displayList_.reset(); displayList_.currDate(currDate_); 130:void ListBasedDateBookMenu::viewEntry() 131: { 132: 133: 134: 135: 136: 137: 138: 139: 140: 141: 142: 143: 144: 145: 146: } 147: 148:void 149: { 150: 151: 152: 153: 154: 155: 156: 157: 158: 159: int recordld = displayList_.selectRecord() ; if (recordld <= 0) return; setDate(dateBook_.getAppointment(recordld).startTime()); Appointment appt = dateBook_.getAppointment(recordld) ; std::cout « "Date: ” « appt.startTime().dateStr() « ’ \n’; std::cout « "From " « appt.startTime().timeStr() « ” to " « appt.endTime().timeStr() « ’Xn1; std::cout « appt.description(); std::cout « "\n\nPress [RETURN] when ready."; std::cin.ignore(INT_MAX, 1Xn1); ListBasedDateBookMenu::deleteEntry() int recordld - displayList_.selectRecord(); if (recordld <= 0) return; setDate(dateBook_.getAppointment(recordld).startTime() ) ; // Удаление записи dateBook_.eraseAppointment(recordld); // Удаление записи делает недействительным // буфер экранного списка. 160: // Обновляем его, затем прокручиваем до исходной позиции 161: 162: } 163: 164:void 165: { 166: 167: 168: 169: displayList—.reset(); ListBasedDateBookMenu::editEntry() int recordld = displayList_.selectRecord(); if (recordld <= 0) return; 314 Глава 10. Сборка блоков программы
170: // Создание редактора для выбранной записи 171: Appointment appt = dateBook_.getAppointment(recordld); 172: AppointmentEditor editor(appt); 173: 174: // Редактирование записи книги контактов 175: if (editor.edit()) 176: { 177: // Замена записи измененной версией. 178: dateBook_.replaceAppointment(editor.appt()); 179: 180: // Порядок следования записей мог измениться. Нам следует 181: // обновить экранный список. 182: displayList—.reset (); 183: setDate(editor.appt().startTime()); 184: ’ } 185: } 186: Создание записи контакта Если пользователь вводит С в любом меню книги контактов, функция mainLoop активного меню вызывает функцию createEntry. В строках 30-32 функция createEntry создает объект Appointment с пустым полем описания. Полям даты и времени начала и окончания нового контакта автоматически при- сваиваются текущие дата и время, представленные в переменной currDate . В строке 33 создается объект редактора для этой новой записи. Класс AppointmentEditor работает практически так же, как и класс AddressEditor, который мы рассматривали в главе 5. Мы вернемся к классу AppointmentEditor чуть позже. Цикл в строках 33-50 продолжается до тех пор, пока пользователь не сохранит новый объект записи в контейнере либо отменит сеанс редактирова- ния командой !х. В строке 46 новая запись добавляется в книгу контактов, а в строках 47, 48 мы позаботились об обновлении экранного списка, чтобы при следующем просмотре в нем отобразилась новая запись. Редактирование записи контакта Если пользователь введет Е в любом меню книги контактов (за исключением ежемесячника), функция mainLoop активного меню вызовет функцию editEntry. В строке 166 функция editEntry принимает от пользователя номер записи, выбранной для редактирования. В строке 167 мы убеждаемся, что ID выбранной записи больше нуля. В противном случае это означает, что поль- зователь либо отменил редактирование, либо по ошибке вместо действительной записи выбрал маркер дня недели в режиме еженедельника. После возвращения в строке 171 соответствующего объекта Appointment в следующей строке для него создается объект редактора. Определение класса AppointmentEditor пока- зано в листинге 10.18. Классы, производные от ListBasedDateBookMenu 315
I Листинг 10.18. Определение: класса J > • > л» х, >.?\Г ‘'tUrOn v. Sil. J 1>Л« J ъь. lu.Tn’i’ /^’ >f т? 1://TinyPIM (с)1999 Pablo Halpern. Файл AppointmentEditor.h 2: 3:ttifndef AppointmentEditor_dot_h 4:#define AppointmentEditor_dot_h 1 5: 6:#include "Editor.h" 7:#include "Appointment.h" 8: 9:// Класс редактора объектов Appointment. 10:class AppointmentEditor : public Editor 11: { 12:public: 13: // Начало работы с пустым объектом Appointment 14: AppointmentEditor(); 15: 16: // Редактирование существующего объекта Appointment 17: AppointmentEditor(const Appointments a); 18: 19: // Использование сгенерированного компилятором 20: // деструктора-AppointmentEditor() ; 21: 22: // Главный цикл возвратит true, // если редактирование записи завершилось успешно, 23: // или false, если редактирование было прервано. 24: bool edit(); 25: 26: // Эта функция доступа используется // для возвращения измененной записи контакта. 27: Appointment appt() const {return appt_;} 28: 29: // Эта функция доступа используется // для установки объекта Appointment для редактирования: 30: void appt(const Appointments a){appt_ = a;} 31: 32:private: 33: // Запрещение копирования 34: AppointmentEditor(const AppointmentEditorS); 35: const AppointmentEditorS operator^(const AppointmentEditorS); 36: 37: // Переменные-члены 38: Appointment appt_; 39: 40:protected: 41: // Защищенные функции 42: bool editDate(const std::strings prompt, DateTimes dt) ; 43: bool editTime(const std::strings prompt, DateTimes dt) ; 44: }; 45: 46:#endif / / AppointmentEditor dot h Класс AppointmentEditor производится от класса Editor, который предоставля- ет наиболее общие функции редактирования. В производном классе AppointmentEditor собраны функции, специфичные для редактирования объектов Appointment, такие как edit, editDate и editTime. Эти функции объявлены соот- 316 Глава 10. Сборка блоков программы
ветственно в строках 24, 42 и 43. После создания объекта Appointment Editor вы- зывается его функция edit. Продолжим рассмотрение класса AppointmentEditor, обратившись к реализации его функций в листинге 10.19. ; Листинг 10.19. Реализация кла^ Sip ' ‘а . '‘Л Д . , <Ж.’а ' 1://TinyPIM (с)1999 Pablo Halpern. Файл AppointmentEditor.срр 2: 3:#include <iostream> 4:#include <sstream> 5: 6:#include "AppointmentEditor.h" 7: 8:// Начало работы с пустым объектом Appointment. 9:AppointmentEditor::AppointmentEditor() 10: { 11:} 12: 13: 14:// Редактирование существующего объекта Appointment 15:AppointmentEditor::AppointmentEditor(const Appointments a) 16: : appt_(a) 17: { 18: } 19: 20:// Главный цикл возвратит true, // если редактирование записи завершилось успешно, 21:// или false, если редактирование было прервано. 22:bool AppointmentEditor::edit() 23: { 24: // Распаковка записи контакта 25: DateTime startTime(appt_.startTimeО); 26: DateTime endTime(appt_.endTime()); 27: std::string description(appt_.description()); 28: 29: editDate("Date", startTime) && 30: editTime("Start Time", startTime) && 31: editTime("End Time", endTime) && 32: editMultiLine("Description", description); 33: 34: if (status() == canceled) 35: return false; 36: 37: // Проверка того, что дата окончания контакта установлена 38: // позже его начала и что между этими датами менее 24-х часов. 39: int year, month, day; 40: startTime.getDate(year, month, day); 41: endTime.setDate(year, month, day); 42: 43: // Если время окончания контакта оказалось раньше начала, // переустановить время окончания на то же время следующего дня. 44: if (endTime < StartTime) 45: endTime = endTime.addDay(); 46: 47: // Сохранение изменений 48 : appt__. startTime (startTime) ; 49: appt_.endTime(endTime) ; Классы, производные от ListBasedDateBookMenu 317
50: 51: appt_.description(description); 52: return status() != canceled; 53: } 54: 55:bool AppointmentEditor::editDate(const std::string& prompt, 56: DateTime&dt) 57: { 58: std::string dateStr = dt.dateStr(); 59: 60: for (;;) 61: { 62: if (! editSingleLine(prompt, dateStr)) 63: return false; 64: 65: // Отслеживание специальной строки "today": 66: if (! dateStr.empty() && 67: (dateStr[0] == 'T’ || dateStr[0] == 't')) 68: { 69: dt = DateTime::now(); 70: return true; 71: } 72: 73: // Обратное преобразование от string к объекту DateTime 74: if (! dt.dateStr(dateStr)) 75: std::cout « "Invalid date. Try again" « std::endl; 76: else 77: return true; 78: } 79: } 80: 81:bool AppointmentEditor::editTime(const std::string& prompt, 82: DateTime& dt) 83: { 84: std::string timeStr = dt.timeStr(); 85: 86: for (;;) 87: { 88: if (! editSingleLine(prompt, timeStr)) 89: return false; 90: 91: // Обратное преобразование от string к объекту DateTime 92: if (1 dt.timeStr(timeStr)) 93: std::cout « "Invalid time. Try again" « std::endl; 94: else 95: return true; 96: } 97: } В строках 25-27 функции edit создаются копии всех полей объекта Appointment. В строках29-32 каждое поле редактируется по отдельности. Поле даты начала контакта редактируется в два приема. Сначала изменения вносятся в компонент даты, а затем в компонент времени. В строках 66-71 функция edit Date проверяет, ввел ли пользователь командный символ Т, что означает Today (сегодня). Если да, то для даты контакта автоматически устанавливается текущий день. В противном случае программа ожидает в строке 74 ввода даты от 318 Глава 10. Сборка блоков программы
пользователя и показывает в строке 75 сообщение об ошибке, если будет введено недопустимое значение. После завершения редактирования всех полей записи следует проверить совмес- тимость введенных значений. Например, следует проверить, чтобы время оконча- ния контакта не оказалось раньше времени его начала, но и не было отдалено от на- чала более чем на 24 часа. В строках 40, 41 дата окончания контакта устанавливает- ся такой же, как и дата начала. Если будет обнаружено, что контакт оканчивается раньше, чем начинается, то в строке 45 дата окончания контакта приращивается на один день. Завершив проверку соответствия данных, строковые значения преобра- зовываются обратно в переменную-член appt__ типа DateTime. На этом редактиро- вание заканчивается, после чего измененный объект записи возвращается в функ- цию editEntry и заносится в контейнер книги контактов с помощью функции replaceAppointment (см. строку 178 листинга 10.17). Завершение работы приложения После завершения выполнения специальных операций, связанных с выбранной опцией меню (например, (E)dit), выполнение программы переходит к функции mainLoop, которая возвращает нас в функцию main, а та вновь вызывает функцию mainLoop для текущего активного меню. Если пользователь выберет опцию g, для текущего объекта меню вызывается функция Menu: : exitMenu, которая удаляет ука- затель на этот объект из вершины стека. В результате активным становится меню более высокого уровня, в нашем случае— главное меню. Если пользователь вновь введет Q, из стека удаляется указатель на объект главного меню, после чего стек ока- зывается пустым. Данное событие является условием завершения цикла функции main. Но это еще не конец истории. Объект myPIMDate типа PIMDate содержит указа- тели типа auto ptr на объекты AddressBook и DateBook. После завершения функ- ции main объект удаляет myPIMDate, что сопровождается удалением объектов AddressBook и DateBook деструктором класса auto ptr. Аналогично, объекты auto_ptr в составе объекта dateBookMenu (тип DateBookMenuCatalog) удаляют че- тыре объекта меню книги контактов. Мы могли бы продолжить рассмотрение реализации специальных функций, об- служивающих другие опции меню, но оставим эти вопросы в качестве вашего до- машнего задания. Все программные коды вы можете загрузить на свой компьютер с Web-страницы, указанной выше. Воспользуйтесь возможностями системы отладки программ вашего компилятора, чтобы проследить выполнение программы в поша- говом режиме. Постарайтесь использовать современный компилятор с мощной сис- темой отладки, позволяющей разбирать сложные выражения, такие как *iter- >first. Анализ подобных выражений— это, видимо, единственный путь проник- нуть в недра стандартных контейнеров, анализ которых с помощью программ от- ладки чрезвычайно затруднен. Резюме Поздравляю вас, мы только что получили первую версию нашего приложения TinyPIM 1.0. На завершающих этапах работы над приложением нам пришлось применить все те знания, которыми мы овладели, изучая материал предыдущих Классы, производные от ListBasedDateBookMenu 319
глав. Мы также узнали, как повысить надежность и ошибкоустойчивость про- граммы с помощью стандартного класса auto_ptr. Хотя, безусловно, еще оста- лось множество возможностей усовершенствования программы. Ниже перечис- лены некоторые пути повышения эффективности и надежности работы нашей программы. Постарайтесь решить эти задачи самостоятельно, чтобы закрепить в памяти полученные знания. 1. Сделайте функцию findNextContains в обоих классах AddressBook и DateBook нечувствительной к регистру. 2. Позвольте пользователю одновременно с выбором таких опций меню, как про- смотр, редактирование или удаление записи, вводить номер записи сразу бук- вой выбора опции, вместо того чтобы показывать новое приглашение. Напри- мер, ввод команды D 5 будет означать удаление записи, показанной на экране в 5-й строке. 3. Позвольте ввод записей контактов без указания времени окончания. 4. Сделайте указание года необязательным при вводе даты. Пусть в этом случае по умолчанию устанавливается текущий год. 5. Разработайте структуру данных, которая позволит устанавливать взаимосвя- зи между записями адресной книги и книги контактов. В следующей главе вы получите некоторые ценные советы о том, как повысить эффективность вашей работы со средствами стандартной библиотеки C++, начиная от раскрытия некоторых секретов и заканчивая ссылками на источники дополни- тельной информации. 320 Глава 10. Сборка блоков программы
Глава 11 Научитесь профессионально работать со стандартной библиотекой C++ В этой главе... • Полезные советы • О чем вы еще не знаете • Где можно узнать больше • От автора 321 327 329 331 Мне хочется верить, что, прочитав эту книгу, вы овладели основами исполь- зования стандартной библиотеки C++. В данной главе мы быстро пройдемся по некоторым темам, которых не касались в работе над приложением TinyPIM. Но вам следует знать, какие еще средства заложены в стандартную библиотеку и где их можно найти. Кто знает, какие проблемы вам придется решать в ваших соб- ственных проектах. Возможно, вам даже придется приобрести еще одну книжку по стандартной библиотеке C++, где раскрыты другие ее возможности. Поверьте, это будет дешевле, чем самостоятельно разрабатывать классы и алгоритмы, ко- торые уже есть в стандартной библиотеке. Кроме того, следует помнить о нали- чии других независимых библиотек средств программирования, которые могут даже в большей степени подойти для вашего проекта, чем стандартная библио- тека C++. Планируя проект, всегда следует рассматривать его финансовую сто- рону, чего мы не делали в этой книге с проектом приложения TinyPIM. Так вот, намотайте себе на ус, что лучше и выгоднее приобретать коммерческие библио- теки, чем безуспешно тратить время и деньги на попытки написать сложный программный код самостоятельно. Особое внимание следует обратить на те биб- лиотеки независимых изготовителей, компоненты которых совместимы со сред- ствами стандартной библиотеки C++. Полезные советы До сих пор мы рассматривали отдельные средства стандартной библиотеки C++ в контексте их использования для решения конкретных проблем, связанных с раз- работкой приложения TinyPIM Давайте немного отклонимся от этого подхода и рас- смотрим общие принципы эффективного использования стандартных средств C++, независимо от решаемых вопросов.
Достижение максимальной эффективности работы Вам может показаться, что вы уже постигли все вопросы повышения эффектив- ности выполнения программы. Но сразу хочу вас предупредить, что чрезмерное ув- лечение поиском максимально эффективных решений может привести не к повы- шению производительности программы, а к срыву сроков сдачи проекта. Всегда сле- дует тщательно обдумать, действительно ли повышение эффективности выполнения данного блока существенно скажется на работе всей программы. Если же разраба- тываемый блок является ключевым в вашем приложении, воспользуйтесь советами, представленными ниже. Тщательно выбирайте контейнер Если для решения задачи можно использовать контейнеры разных типов, то ключевым моментом при выборе контейнера должен стать анализ эффективно- сти выполнения наиболее часто используемых операций. Если необходимо бы- стро в произвольном порядке извлекать элементы контейнера по их индексам, воспользуйтесь шаблонами deque (двухсторонняя очередь) или vector (вектор). Если для вас важнее быстро добавлять и удалять элементы, то для этого лучше подойдет контейнер list (список). Кроме того, контейнер list позволяет объеди- нять несколько списков вместе и эффективнее расходует ресурсы центрального процессора. Контейнер deque можно использовать всюду, где используется кон- тейнер vector. При этом добавление элементов в двухстороннюю очередь выпол- няется эффективнее благодаря более совершенному алгоритму управления па- мятью. Стандарты контейнера vector сейчас пересматриваются, с тем чтобы по- высить его эффективность. Так, предполагается обязать сохранение элементов вектора в непрерывном ряде ячеек динамической памяти, что сделает контейнер vector эквивалентным массивам символов в стиле С и облегчит его использова- ние в приложениях с интерфейсом, написанном на языке С. Если необходимо часто выполнять поиск элементов по заданным значениям, то для этих целей лучше подойдут ассоциативные контейнеры, поиск в которых всегда осуществляется быстрее, чем в последовательных. Если необходимо со- вместить поиск элементов по индексам и по значениям, что случается не так уж часто, то элементы контейнеров vector или deque можно сортировать с помощью алгоритма sort, а поиск проводить с помощью алгоритмов lower_bound, upper_bound, equal_range и binaiy_search, которые характеризуются логариф- мическим временем выполнения. Если сортировка записей в контейнере не нужна или невозможна, а необходимо иметь возможность быстро извлекать нужные элементы, попробуйте использовать дробленые (hashed) ассоциативные контейнеры. Не нужно быть большим специалистом в программировании, чтобы создавать с помощью ассоциативных контейнеров и алгоритмов поиска доволь- но сложные программы поискал возвращения элементов. Использование итераторов как закладок в контейнерах Каким бы эффективным ни был контейнер, поиск и возвращение элементов, свя- занные со сравнением их значений (например, строк), может существенно замедлить работу программы, тем более, если эти операции выполняются часто. Если предпола- 322 Глава 11. Научитесь професионально работать...
гается, что найденный элемент будет использоваться в программе несколько раз, соз- дайте итератор на этот элемент, чтобы не искать его повторно. Так, вместо выражения if (mymap. f ind (i) ’== mymap.endO) mymap[i].second *=2.5; // Повторный поиск элемента i можно записать: if (г != mymap.end()) r->second *= 2.5; // Повторное использование результата, // возвращенного функцией поиска Во втором варианте мы отыскиваем элемент i только однажды, а затем ссылаем- ся на него с помощью итератора, вместо того, чтобы искать еще раз, хотя первый ва- риант выглядит компактнее и понятнее. В своей работе вам часто придется выби- рать между простотой и аккуратностью выражений и высокой эффективностью, и не всегда усложнение программы оправдывается реальным повышением эффективно- сти. Примите также к сведению, что функции ассоциативных контейнеров, такие как count и erase, осуществляют поиск записей, что делает время их выполнения логарифмически зависимым от размера контейнера, тогда как удаление записи по итератору константно по времени выполнения. Использование функции swap Все стандартные контейнеры и классы строк содержат функцию-член swap, кото- рая осуществляет обмен данными этих типов. Кроме того, существует еще одно- именный алгоритм, который позволяет выполнять обмен данными произвольных типов. В листинге 11.1 показана обычная реализация алгоритма swap. (Следует от- метить, что алгоритм swap устроен так, что при возможности автоматически заме- щается на вызов функций-членов swap стандартных контейнеров.) j Листинг 11.1. Алгоритм swap template <class Т> void swap(T& tl, T& t2) { T temp(tl); tl = t2; t2 = temp; J Как вы видите, в алгоритме swap дважды выполняется операция копирования объекта типа т. Если Т — это достаточно большой объект, такой как вектор, время выполнения алгоритма будет пропорционально удвоенному размеру вектора. Но контейнер типа vector, как и все другие стандартные контейнеры, имеет функцию- член swap, которая не копирует элементы вектора, а передает право владения содер- жимым контейнера. Дело в том, что все контейнеры содержат внутренние указатели на свои элементы и при вызове функции swap в другой вектор копируются только указатели на элементы, в результате чего операция выполняется быстрее, чем про- стое присваивание вектора. То же самое справедливо для всех остальных стандарт- ных контейнеров. Тот факт, что все стандартные контейнеры содержат эффективные функции- члены swap, позволяет выполнить одну интересную оптимизацию, проиллюст- рированную в листинге 11.2. В нем показана неоптимизированная функция, ко- Полезные советы 323
торая получает с аргументом вектор и изменяет его. Чтобы избежать появления бессмысленных итераторов, исходный контейнер не изменяется, пока не будет создана его копия. Листинг 11.2. Функция изменения вектора l:#include <vector> 2: 3:extern bool cond(int); 4:extern int g(int); 5: 6:void f(std::vector<int>& inout) 7: { 8: std::vector<int>result; 9: 10: for (std::vector<int>::iterator i = inout.begin(); 11: i ’= inout.end() ;++i) 12: if (cond(*i)) 13: result.push_back(g(*i)); // Вводится измененный вариант // объекта 14: 15: inout = result; 16: } Эта функция выполняет итерацию по всем элементам вектора (строки 10, 11) и вызывает функцию cond для каждого элемента (строка 12). Если cond возвращает true, для данного элемента вызывается функция д, которая добавляет возвращаемое значение в вектор result (строка 13). Затем, в строке 15. исходный вектор inout замещается на вектор result. Если вектор result достаточно большой, то его копи- рование займет много времени. Но строку 15 можно сделать константной по времени выполнения, если заменить ее следующим выражением: 15: inout.swap(result); При выполнении новой версии строки 15 содержимое контейнера inout заме- щается вектором result. Еще одно преимущество функции swap состоит в том, что при ее выполнении не затрачивается дополнительная память и никогда не возникают исключительные ситуации. Напротив, операция присваивания может вызвать исключительную ситуацию (например, если для создания копии не хватит свободной памяти). Как отмечалось выше, применение алгоритма swap к стан- дартным контейнерам ничем не отличается от вызова для них функций-членов swap, поэтому строку 15 можно было бы переписать следующим образом, ничуть не потеряв в производительности: 15: std::swap(inout, result); Управление памятью Иногда эффективное использование памяти в большей степени влияет на произ- водительность программы, чем эффективное использование ресурсов процессора. Действительно, проблемы с доступной памятью могут существенно замедлить вы- полнение программы из-за того, что процессор будет занят страничным обменом с областью виртуальной памяти на диске, вместо того чтобы выполнять программу. Ниже приведены полезные советы о том, как повысить эффективность использова- ния памяти компьютера с помощью средств стандартной библиотеки. 324 Глава 11. Научитесь професионально работать...
Функции capacity и reserve Шаблоны классов vector и string предоставляют две функции управления вы- делением и распределением памяти для элементов контейнера. При правильном ис- пользовании эти функции позволяют оптимизировать затраты памяти на сохране- ние данных в контейнере. Функция capacity возвращает емкость контейнера— число элементов или символов, которые еще можно сохранить в контейнере без дополнительного выделе- ния памяти. Если в ходе выполнения программы объекты vector или string исчер- пают свой ресурс, то для них может быть выделено слишком много дополнительной памяти, в результате чего значение capacity () существенно превысит значение size (). Это свидетельствует о неэффективном перерасходе памяти. Если объемы контейнеров будут приращиваться очень мелкими порциями, то программа будет затрачивать слишком много времени на частое выделение новых областей памяти и копирование в них текущего содержимого. Управлять емкостью контейнера можно с помощью функции reserve. Если вам приблизительно известен размер вектора или строки, то можно зарезервировать ровно столько памяти, сколько потребуется, чтобы предупредить ее перерасход. Если приблизительный размер не известен и в ходе выполнения программы предполага- ется стремительный рост элементов контейнера, с помощью reserve можно преду- предить слишком частые обращения программы за новыми порциями памяти. Если значение аргумента, переданного в функцию reserve, меньше текущего числа эле- ментов, то вызов функции игнорируется. В листинге 11.3 показано использование функции reserve дая предварительного резервирования памяти. Листинг 11.3. Предварительное резервирование памяти для вектора С ПОМОЩЬЮ функции reserve l:#include <iostream> 2:#include <vector> 3:#include <cassert> 4: 5: int main () б:{ 7: // Заполнение вектора значениями квадратов первой тысячи 8: 9: 10: 11: 12: 13: 14: // целых чисел std::vector<long> result; result.reserve(10000); for (long i = 1; i <= 10000; ++i) result.push_back(i * i); std::cout « "Size = " « result.size() « ", capacity - " « result.capacity() « std: :endl; 15: 16: // Проверка соответствия размера контейнера его емкости 17: 18: 19: 20: } assert(result.size() == return 0; result.capacity()); Функцию reserve можно использовать дая уменьшения объема памяти, выделен- ной дая объекта string, но не дая вектора. Емкость вектора невозможно уменьшить. Если нужно освободить память, занимаемую вектором, то классическим решением бу- дет вызов функции swap дая замены текущего вектора на пустой (листинг 11.4). Полезные советы 325
Листинг 11.4. Использование функции swap для очистки вектора template <class Т> void freeVector(std::vector<T>& v) { std::vector<T> empty; v. swap (empty) ; j Еще одна интересная возможность использования функции reserve состоит в том, что с ее помощью можно предупредить возникновение бессмысленного итера- тора. Так, функция push_back вектора не сделает бессмысленным никакой итера- тор, если size () будет меньше, чем capacity (). Шаблоны bitset и vector<bool> Если вы планируете создать большой массив логических значений, но не хотите тратить на него много памяти, то вам идеально подойдет шаблон класса bit set. Объект bit set представляет собой набор битов, который работает как массив с фик- сированным размером. Например, инструкция bitset<50> создает массив из 50 битов, доступ к которым можно получить с помощью индексов от О до 49. Обрати- те внимание, что размер массива bit set задается как параметр шаблона, поэтому в момент компиляции должен быть константой, а не переменной. Набор битов не может изменять размер во время выполнения программы. Наборы битов удобно ис- пользовать либо как массивы целых чисел (флагов), которые могут принимать зна- чение 1 или О, либо как маска битов, поскольку с ними можно применять операторы побитового И (&) и побитового ИЛИ (|). Наборы битов не являются настоящими кон- тейнерами, поэтому обычно нельзя создавать итераторы на их элементы, по крайней мере дня наборов битов библиотеки STL. Если вам нужен контейнер логических значений, который можно увеличивать и уменьшать, воспользуйтесь шаблоном vector<bool>. В стандартах контейнер vector<bool> определяется как специализированный вектор, элементы которого можно упаковывать таким образом, чтобы они занимали как можно меньше места. Упаковка контейнера vector<bool> не всегда оказывается столь эффективной, как ожидалось, поэтому ее можно отменить. Дело в том, что упакованный контейнер vector<bool> работает медленнее, чем обычный, поэтому выигрыш в памяти ниве- лируется проигрышем во времени выполнения. Кроме того, в упакованном состоя- нии объект vector<bool> не вполне соответствует требованиям стандартов к кон- тейнерам. Лично я советую вам по возможности держаться подальше от контейнеров этого типа. Для поддержания контейнера логических переменных лучше использо- вать шаблон deque<bool>. Неплохие специализированные контейнеры для поддер- жания больших массивов упакованных логических значений предлагаются в биб- лиотеках независимых изготовителей. Устойчивость к ошибкам В контейнерах стандартной библиотеки предусмотрено поддержание целостно- сти данных. Как минимум, в контейнере должен гарантироваться вызов конструк- тора-копировщика, оператора присваивания и деструктора для всех элементов. Что произойдет с контейнером, если исключительная ситуация возникнет при обработке одного из его элементов? 326 Глава 11. Научитесь професионально работать...
Минимальное требование к контейнерам стандартной библиотеки состоит в том. чтобы в результате их использования не происходила утечка памяти. Другими сло- вами, если в какой-то момент работы с контейнером возникнет исключительная си- туация, то все объекты контейнера либо должны сохраниться неизмененными, либо должны быть удалены деструктором. Область памяти, выделенная для контейнера, либо сохраняется за контейнером, либо должна быть освобождена. Если произошло удаление части объектов контейнера, то оставшиеся объекты также должны быть удалены, а область памяти, занятая контейнером, должна быть освобождена. Мини- мальные требования к контейнерам не гарантируют того, что после возникновения исключительной ситуации они останутся доступными для использования, посколь- ку трудно предсказать, будет ли его содержимое сохранено или удалено. Но для некоторых функций в стандартах определены дополнительные требова- ния к тому, чтобы в случае сбоя в их выполнении контейнер возвращался к тому со- стоянию, в котором он пребывал до начала операции. Например, такие требования определены для следующих функций-членов класса list: push front, push back, pop f ront, pop back и erase. В ассоциативных контейнерах такие требования оп- ределены для функций insert и erase. Если в вашей программе важно сохранить работоспособность контейнера после возникновения исключительной ситуации при обращении к нему, держитесь подальше от контейнеров типов vector и deque. Но следует учесть, что соответствие контейнеров стандартам гарантируется только в случае использования в качестве их элементов таких объектов, которые также отвеча- ют стандартным требованиям. Особо важно проследить, чтобы деструкторы элементов контейнеров гарантированно удаляли все переменные-члены и чтобы возникновение исключительных ситуаций не могло привести к частичному удалению объекта. О чем вы еще не знаете Как упоминалось во введении, цель данной книги — научить вас средствам стан- дартной библиотеки, которые наиболее часто используются при работе над самыми разными проектами. Многие мощные, но узкоспециализированные средства оста- лись вне поля нашего зрения. Но рано или поздно у вас наверняка возникнет необхо- димость в этих средствах. Поэтому не помешает сделать краткий обзор дополнитель- ных возможностей, которые мы обошли вниманием в этой книге. Кроме того, в сле- дующем разделе вам будут предложены источники дополнительной информации о средствах и возможностях стандартной библиотеки C++. Библиотеки языковой поддержки, средств диагностики и специальных утилит Важную часть стандартной библиотеки составляют небольшие классы, перемен- ные и функции, используемые для языковой поддержки, диагностики программ и выполнения других специальных операций. Компоненты библиотеки языковой поддержки наиболее тесно связаны с базовыми элементами языка C++ и работой компилятора. К средствам диагностики относятся некоторые широко используемые классы исключений, а в библиотеке утилит определены всевозможные специальные функции, которые трудно отнести к другим разделам стандартной библиотеки. Ниже представлены основные файлы заголовков этих библиотек. О чем вы еще не знаете 327
• <limits>, <climits>, <cf loat> и <cstddef> — определения многих техни- ческих характеристик компилятора и языка C++, таких как предельно до- пустимые значения целых чисел (константы INT_MIN и INT MAX), или спе- циальные типы данных, такие как тип ptrdif f_t для сохранения результа- та вычитания указателя из указателя; • <new> — варианты директив new и delete для выделения и очистки памяти в области динамического распределения. В отличие от старой версии new, которая в случае неудачи возвращает нулевой указатель, новый вариант этой директивы вызывает исключение bad_alloc, класс которого также описан в этом заголовке. Предоставляется также возможность перехваты- вать событие нехватки памяти и по возможности разрешать их. В заголовке <memory> определен класс программы распределения (allocator class), ис- пользуемый по умолчанию, а также предоставляется возможность созда- вать собственные программы распределения; • <exception> и <stdexcept> — содержат определения классов исключений, вызываемых компонентами стандартной библиотеки, и предоставляют средства отслеживания нестандартных исключений. В заголовке <cassert> определен макрос assert; • <utility>— определяет пространство имен std: : rel_ops, содержащее шаблоны операторов отношений (см. обсуждение листинга 4.12 в главе 4). В файле заголовка <functional> определены базовые классы unary_function и binary_f unction, а также стандартные классы объектов функций, такие как less<T>, подшивки (binder) и отрицатели (negator); • <сt ime> — объявления функций и типов даты и времени. Библиотека региональных установок Библиотека региональных установок, определенная в файлах заголовков <1оса1е> и <clocale>, включает средства, облегчающие (или делающие возмож- ным) использование языка C++ для написания программ с поддержкой региональ- ных особенностей. К этим особенностям относятся форматы даты и времени, ото- бражение национальной валюты, преобразование букв в верхний и нижний регист- ры, стандартные тексты на разных языках (например, для представления концепций “истинно” и “ложно”), а также возможность мультибайтового кодирова- ния символов букв для тех случаев, когда для поддержания национального алфавита требуются символы больше одного байта. Библиотека региональных установок языка C++, определенная в файле заголовка <1оса1е>, достаточно обширна. К счастью, большинство ее компонентов автомати- чески задействуются объектами стандартной библиотеки ввода-вывода. Объекты типа locale создаются для сохранения специальных региональных и национальных установок форматирования. Объекты locale обычно создаются по именам. Имя “С” соответствует установкам по умолчанию. Наборы других допустимых имен locale изменяются от компилятора к компилятору (см. техническую документацию, по- ставляемую с вашим компилятором). Взаимодействие объектов locale с объектами iostream устанавливается с помощью функции imbue, являющейся членом класса iostream, как в следующем примере: std::locale usa("C"); // Установки по умолчанию (в данном случае — США) std::locale trance("FR"); // Специальные установки 328 Глава 11. Научитесь професионально работать...
std::cout.imbue(france); std::cout « 3.5; // Будет выведено как "3,5" (вместо точки // будет выведена запятая) std::cout.imbue(usa); // Восстановление установок по умолчанию Если вы хотите, чтобы установки trance принимались по умолчанию для всех новых объектов iostream, воспользуйтесь функцией locale: : global, передав в нее объект trance в качестве аргумента. Обратите внимание, что вызов функции locale: :global не изменит установки форматов locale для всех уже существую- щих объектов ввода-вывода, включая cin, cout и се г г. Если подробно описывать средства библиотеки locale, то потребуется еще одна книга, поэтому, если вас заин- тересовал этот вопрос, обратитесь к специальной литературе. Особенности компиляции. Установки locale используются во всех но- 33метку вейших и наиболее усовершенствованных средствах языка C++, включая вложенные и частично специализируемые шаблоны, а также системы идентификации типа во время выполнения программы. Но при этом мно- гие компиляторы до сих пор не поддерживают класс locale языка C++, хо- тя в них можно использовать аналогичные средства, унаследованные из языка С и определенные в файле заголовка <clocale>. Библиотека чисел Если вы создаете специальные инженерные приложения, вам могут потребоваться сложные математические средства, которые не входят в обычные стандартные функции языка C++. Дополнительные средства определены в следующих файлах заголовков: • <complex>— предоставляет определения классов complex<f loat>, complex<double> и complex<long double>, используемые для тригоно- метрических вычислений с комплексными числами; • <valarray>— определяет шаблон класса valarray<T> (где Т почти всегда является числовым типом), который работает как специализированный вектор. Объект valarray представляет собой одномерный массив, оптими- зированный дня выполнения одновременно нескольких вычислений. При- мите к сведению, что объект valarray не поддерживает использование ите- раторов на его элементы и не может автоматически увеличиваться при до- бавлении новых элементов, поэтому его нельзя использовать как обычный контейнер, такой как vector. Где можно узнать больше Если стандартная библиотека C++ еще не утомила вас окончательно и вы хотите узнать больше о ее возможностях, ниже вы найдете ссылки на источники дополни- тельной информации по этой теме. Источники в Internet Полезную информацию о стандартной библиотеке C++ можно найти на многих сайтах в Internet, наиболее важные из которых перечислены ниже. Где можно узнать больше 329
STLport http://www.stlport.org/ На этой Web-странице вы получите доступ к бесплатным версиям библиотек STL и Strings, а также найдете ссылки на другие замечательные серверы. Руководство для программистов по стандартной библиотеке шаблонов STL http://www.sgi.com/Technology/STL/ Здесь вы найдете прекрасный обзор средств библиотеки STL (Standard Template Li- brary — стандартная библиотека шаблонов), содержащей определения шаблонов кон- тейнеров, итераторов, алгоритмов и объектов функций. Домашняя страница интерактивного руководства по использованию стандартной библиотеки шаблонов компании RPI http://www.es.rpi.edu/projects/STL/htdocs/stl.html Еще один ресурс сведений о библиотеке STL, но учтите, что он не обновлялся с мая 1996 года. Техническая документация компилятора Не забудьте внимательно изучить техническую документацию вашего компи- лятора. Качество и объемы вспомогательной информации, распространяемой с компиляторами, отличается от изготовителя к изготовителю. Наверное, одной из наиболее мощных систем технической поддержки является компилятор Microsoft для Windows. Эта компания предлагает интерактивный пакет MSDN, позволяющий с помощью указателя мыши выбирать интересующие вас разделы, имена функций и классов и щелчком открывать описания выбранных разделов. Кроме того, здесь вы найдете многочисленные ссылки на другие источники до- полнительной информации. Стандартная документация ISO Если у вас возникнут какие-либо сомнения, обращайтесь к первоисточникам. Стандартная документация ISO по языку C++ — не самое увлекательное “чтиво”, но зато это официальный документ, в котором описаны все требования к библиотечным средствам и компонентам. Много полезной информации можно почерпнуть из раз- дела, посвященного стандартным алгоритмам. Обратите внимание, что многие стандарты С не вошли в документацию языка C++. К счастью, документация по язы- ку С пока еще вполне доступна. Стандартную документацию* языка C++ можно заказать в Американском нацио- нальном институте по стандартизации: ♦ American National Standards Institute (ANSI) ♦ 11 West 42nd Street 330 Глава 11. Научитесь професионально работать...
♦ New York, New York 10036 ♦ Tel: 212.642.4900 ♦ Fax: 212.398.0023 ♦ Web:http://www.ansi.org/ ANSI продает электронную версию стандартной документации C++ в формате PDF (файл размером около 2,5 Мбайт) по цене $18. Загрузить ее можно с Web-страницы, указанной выше. Стандартную документацию также можно получить в региональном комитете по стандартизации, адрес которого указан на Web-странице организации ISO: http://www.iso.ch. Проект стандартной документации C++, открытый для публичного обсужде- ния, можно выгрузить по адресам URL, указанным ниже. В этих источниках представлено второе издание проекта CD2 (Second Public Draft), которое было опуб- ликовано примерно за полтора года до утверждения окончательной версии стан- дартов. К сожалению, за эти полтора года в стандарты было внесено довольно много изменений, особенно в разделы, касающиеся стандартных библиотек. (Например, были внесены принципиальные изменения в требования к шаблону autoptr.) Но большая часть документации сохранилась в том виде, в каком она представлена в следующих источниках: http://www.maths.warwick.ас.uk/cpp/pub/wp/html/cd2/http://www.cygnus.com/misc/wp/draft/ От автора С Пабло Халперном (Pablo Halpern) можно связаться по электронной почте phalpern6newview.org. Ему можно задать любые вопросы, которые возникнут при прочтении этой книги, но будьте терпеливы, ответ может прийти лишь через не- сколько дней из-за большой загрузки автора. От автора 331
Предметный указатель и UML, унифицированный язык моделирования, 26 А Алгоритм сору, 181 count, 142 countJf, 145 lexicographical_compare, 234 lower_bound, 151 reverse, 202 swap, 323 определение, 142 соглашения именования, 193 создание, 236 Б Библиотека графического интерфейса. 110 определение, 16 расширяемость. 236 региональных установок, 328 стандартная. 16 унаследованная от С, 18 чисел, 329 шаблонов (STL), 19 языковой поддержки, 327 Буферизация данных, 173 потока. 190 В Ввод-вывод в буфер памяти, 228; 229 в объект string, 228 в файл. 228; 229 времени. 245 данных разных типов, 114 контроль за ошибками, 196; 246 манипуляторы форматирования. 185 неформатированный, 115 ошибки, 116 Время ввод-вывод. 245 вычисления, 309 маркер перехода между днями. 309 нормализация. 264 переход на летнее/зимнее, 244 представление в C++, 239 Г Генератор случайных чисел. 227 д Деструктор, 63 Диаграмма классов. 30 Диапазон итераторов, 143 Друг класса, 104; 242 3 Заголовок algorithm. 143: 186 cassert. 180 cctype, 213 climits, 216 cstdlib, 226; 270 cstring, 42 ctlme, 241 deque, 177 fstream. 228: 229 iomanip. 189 iostream, 47; 270 Iterator, 183 locale, 328 set, 157 sstream, 228 stack. 209 utility, 104: 168 valarray, 329 соглашения имен, 42 соглашения именования, 18 И Идиоматический подход, 15 Индексирование вторичное. 163 Интерфейс. 19 Исключение, 71 Итератор, 92 бессмысленный, 194 ввода-вывода. 187 вычисления. 187 вычитание, 187; 188 двухсторонний, 187; 188 категории. 187 константный, 97 обратный, 184 повышения эффективности программы, 322 приращение, 187; 188 произвольного доступа. 187: 188 332 Предметный указатель
прямой, 187 разыменовывания, 93; 169 установка диапазона, 143 npos, 121 RAW-MAX, 227 литеральная, 39 К Календарь, 297 Класс Address, 38 AddressBook, 71; 91 AddressDisplayList, 197 AddressEditor, 123 DateBook, 276 DateTime, 240 DisplayList, 175 Editor, 111 fstream, 228; 229 istream, 115 MainMenu, 283 Menu, 207 ostream, 115 PIMDate, 271 string, 60; 117 strlngstream. 228; 252 strstream, 228; 229 traits, 150 абстрактный, 34 адаптер, 209 базовый и производный, 32 интерфейс, 19 исключения, 72; 327 контейнер, 69 концевой. 287 оболочка, 23 объекта функции, 146 планирование, 29 распределения, 79 стандартный, 90 Клиент, 124 Ключевое слово case, 215 catch, 274 enum. 39 explicit, 272 friend, 104 tiy, 274 using, 43; 44 Код подстановки, 263; 266 Компилятор. 16 выбор, 21 техническая документация, 330 Комплексные числа, 329 Конкатенация литералов, 215 операций вывода, 254 Константа badbit, 251 EXIT-FAILURE, 274 прикрепленная, 149 Конструктор, 53 Конструктор-копировщик, 53 Контейнер, 69 deque (двухсторонняя очередь), 174 hash (дробленый), 170 list (список). 91 тар (карта), 163 multiset [множественный набор), 155 set (набор), 155 string, 237 vector (вектор), 74 ассоциированный, 155 выбор, 322 емкость, 325 изменение с помощью swap, 323 копирование, 182 последовательный, 73; 90; 155 произвольный доступ к элементам, 82; 174 сортировка записей, 233 справочная информация, 81 удаление элементов, 325 установка соответствия. 165 Л Лексикографическое сравнение, 235 М Макрос assert, 180 INT-MAX, 216 NO_NAMESPACE, 45 языка С, 19 Манипулятор форматирования, 188; 246; 298 Маска битов, 326 Массив ассоциативный. 167 рассеянный. 167 символов, 40 Меню активное, 223 диаграмма классов, 287 дизайн, 206 удаление из стека, 319 н Набор битов, 326 Нормализация данных даты и времени. 264 О Объект cin, 248 cout, 242 pair, 169 Предметный указатель 333
string. 60; 228 владение, 272 функции.145 эквивалентный, 162 Оператор switch, 133 ввода. 116 вызова функции, 145 деления по модулю, 227 индексирования, 82; 167 логическое И, 126 меньше (<). 233 отношения. 103 побитовый, 326 присваивания, 54; 272 разыменования, 169 Отрицатель, 149 Ошибкоустойчивость. 326 П Память бесхозная, 57 динамическое распределение. 52 управление. 324 Перечисление, 39 Подшивка, 149 Поиск дихотомический, 151 линейный, 151 Поток буферизация, 190 ввода-вывода. 228 очистка, 190; 197 стандартный, 114 строк, 228; 252 Предикат, 148 Проект планирование. 26 поэтапная разработка, 36 ситуация использования. 27 Пространство имен reLops. 104:328 std, 19; 44 проблемы с компиляцией. 45 справочная информация. 43 Псевдослучаные числа, 227 Р Региональные установки, 328 С Символ заполнения, 189 препроцессора NOGENERATE, 278 Ситуация использования приложения. 27 неопределенности. 46 Среда разработки C++. 17 Стандартная библиотека C++, 22 Стек меню. 207 Строка текста анализ символов, 213 в стиле С, 64 ввод с клавиатуры. 221 копирование, 45 нулевой символ окончания, 40 объект string. 60 подстрока, 121 поиск, 121 преобразование из char в string. 64 преобразование из string в char. 67 преобразование регистра. 213 фиксированной длины. 38 Строковый литерал. 40 Структура pair, 168 tm. 244 Т Тип slzejype, 120 time_t, 240 приведение, 186 член класса. 120 У Указатель ассоциативный ключ, 234 интеллектуальный. 94:272 на функцию. 147 удаление из контейнера. 234 Утечка памяти. 57 Утилита библиотечная, 327 Ф Флаг ошибки, 251 Форматирование даты и времени, 246:250 манипуляторы ввода-вывода. 188 региональное, 263 функции printf/scanf, 114 Функция advance, 187; 188 append. 132 backjnserter. 181 begin. 140 blndlnd/bind2nd, 149 c_str. 67 capacity, 325 clear. 197; 251 close, 228; 229 cond. 324 copy, 67 difftime, 241 334 Предметный указатель
distance. 187; 188 end, 140 erase, 95; 170 exit, 274 fetchMore, 200 fill. 189:250 find, 121 findjast_not_of, 123 flags. 250 flush. 190 frontJnserter, 183 generateAddresses. 224 get. 273 getline. 117:221 global. 329 ignore. 197 insert. 105 less, 233 localtiine, 244 main. 222: 282 mainLoop, 214: 310 make_pair. 168 mem Jun, 147 min. 186 mktime, 244 notl/not2, 149 open, 228; 229 pop, 209 pop_back, 83 printf, 114; 189 ptr.fun, 147 push, 209 push__back, 80 put, 190 rand. 226: 282 rbegin. 184 release, 273 rend, 184 reserve, 325 reset, 273 size, 325 srand, 226 strcpy, 41 strftlme. 263 strncpy. 48 substr, 121 swap. 323 time. 245 tolower. 213 toupper, 213; 236 width, 189 бинарпая/одинарная. 146 друг класса, 242 интерфейс, 19 константная по времени выполнения, 80 линейная по времени выполнения, 89 логарифмическая по времени выполнения. 159 неформатированного ввода-вывода. 115 одинарный/бинарныйпредикат, 148 распределения, 233 состояния потока. 117 статическая, 210 чисто виртуальная. 34 Ш Шаблон, 76 auto_ptr, 271 bitset, 326 stack, 207 valarray, 329 vector<bool>, 326 изменение стандартного, 233 классов ввода-вывода, 117 параметр. 76 реализация, 76 создание, 227 справочная информация, 76 структуры pair, 168 функции, 145 Э Эквивалентность объектов, 162 Эффективность выполнения программы, 322 Предметный указатель 335